From 8db4792e616b14e813b2a3216b7deaa565e43ca3 Mon Sep 17 00:00:00 2001 From: mh <729263080@qq.com> Date: Tue, 2 Dec 2025 14:27:09 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=84=E7=90=86=E8=81=8A=E5=A4=A9UI=E7=BB=86?= =?UTF-8?q?=E8=8A=82=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Contents.json | 22 +++++++ .../role_chat_response@2x.png | Bin 0 -> 1316 bytes .../role_chat_response@3x.png | Bin 0 -> 2914 bytes .../role_chat_bg_send.imageset/Contents.json | 22 +++++++ .../role_chat_send@2x.png | Bin 0 -> 1439 bytes .../role_chat_send@3x.png | Bin 0 -> 2958 bytes .../Session/View/StreamChatBubbleCell.swift | 57 ++++++++++++++++-- .../Src/Utils/Extensions/UIImageExt.swift | 27 +++++++++ 8 files changed, 122 insertions(+), 6 deletions(-) create mode 100644 Visual_Novel_iOS/Assets.xcassets/Role/role_chat_bg_response.imageset/Contents.json create mode 100644 Visual_Novel_iOS/Assets.xcassets/Role/role_chat_bg_response.imageset/role_chat_response@2x.png create mode 100644 Visual_Novel_iOS/Assets.xcassets/Role/role_chat_bg_response.imageset/role_chat_response@3x.png create mode 100644 Visual_Novel_iOS/Assets.xcassets/Role/role_chat_bg_send.imageset/Contents.json create mode 100644 Visual_Novel_iOS/Assets.xcassets/Role/role_chat_bg_send.imageset/role_chat_send@2x.png create mode 100644 Visual_Novel_iOS/Assets.xcassets/Role/role_chat_bg_send.imageset/role_chat_send@3x.png diff --git a/Visual_Novel_iOS/Assets.xcassets/Role/role_chat_bg_response.imageset/Contents.json b/Visual_Novel_iOS/Assets.xcassets/Role/role_chat_bg_response.imageset/Contents.json new file mode 100644 index 0000000..722103e --- /dev/null +++ b/Visual_Novel_iOS/Assets.xcassets/Role/role_chat_bg_response.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "role_chat_response@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "role_chat_response@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Visual_Novel_iOS/Assets.xcassets/Role/role_chat_bg_response.imageset/role_chat_response@2x.png b/Visual_Novel_iOS/Assets.xcassets/Role/role_chat_bg_response.imageset/role_chat_response@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..b20b5c1c27ce930332c208ac2aff7cab129b87d7 GIT binary patch literal 1316 zcmV+<1>5?GP)D*OqNfRuCs;uAzV0ptY4Cm=b2%?WT$5OV^|2_#N{IDu6~B{D9m z2!DtT@(%q@R*J08S$){Ev9 z!fFUP%kYBBW;dolQyCmOfzNPe_9i^>fLYNOHF>7I9U52CAXYPk0ZcPE^#{XU50D?EL5}yJYp1nGCzlJv1GXb-y z)=x{ySsWj@0v-wQ$T!&mU#VsS=Dz+t$Dw60VlH(;UN%TF7qDe<%xp)#)$i<}y$JGL zz^uY&F&E6DRe;U)-H)+9&%uR&nZ>%H4SuKyQBT_O(m@dpR86_L5HQ~@4nJ~k7P$gC zu05+~y%g~IIgVi^egz!vX{YPuq@zm#=go9?azlF_r2@^#lN3tTX^?tchft`RrY08x>4gx$77+{pAjAwBW;14s{F&L#Pqn`l<0oIk} zU*OJ1@ZXDr0OvhvSSgPJB6v)1g8=i5W~EcWH`4~Vblk!n-uVQN4RE+#rF&;KK*Iog zY-v7-;41^1CR-ns10ES*FGZpcdIG`#C(1Stt8RdEB~qOk;8e-xaT(xTh*W0=I90NF zTn0E7BGuVqz^O)ki{diCiNiL)1~_pA5)aQZ*Z?Q4K;q$91{>hS6-Ye1A{l&pejA+* z^j;Cb_S4z&fiD2q;kWX|cgsD_@OO9y3+&$wC|7aHEEur;KdD@B>1_sdf&pLt1G|(1 z%2fvE=mrDM|Gr|7%fq-sX2F0NmvgW`f>Vfc*w1vc5MaBk5+y5#iG#nkN+G~ZdFX*# z&8D~&=v1rX2c`-EW=xlM`G3j~wT&|>449W2*66IWr6NQ+^>Rkuy~2RG)%Yib&&}dd zV36VYxHl^S&OafR#p6mwuq=Y3hc)q?tD!q90p`ZqOWT88G@MezVm)M-x;@g$fPF9R za=OA;qP=Z?NCgL;$wc5Q17g>qeceR`!e&Qv1|v<=TKb_;K+TEv?;uvDUdw*QY>F`$A6^?QU(rtelW_M+TW+oz?HdI=+#wJ8 zn$3Iq+>XmC!bYZdV-C-$Z75vscPoHdv1ap;LTTD3|mE5GL69IM4q;W z_y-kZ5ws9V#R!NX(t<)w6x-5b({Ag&X4_h7A8fa~bC2IM>1?NUXJ_u*J9qAGzms$J z-noz8`JK=C?wQ{?XC9gUL3jH=-tA|0%1#XO6EYa0PBsh-y4C#y?!mz!cSm2|T{1A> z&gkoNU0b%ecRA-WNWml1Vz|pW+)ZB&Fe{0`Lm7J7zXRW;z&|}W=uTO=(zQ%vB#C30Y}O8xi8_fgRmSuoFx!BIaQG}m{(ZFIXU_1a zbI!vfLqqP9J9oO-NmrHsXn+@WXNTQR$L^s6Bg{%75SBoO`kBL<^!B>WMtO6TpnV}79ky-M;VKyR zKo1d!i2!44KTvFESaMCv4E&LU$InsN?~cj*xS9VGv_Erl*4Dc*e4YFInUzE!ZUU!s z@VFu~13#y*KNjb#$b^FS-t#5mQEq?9955@1K->h{47h}|*DKCF4>!hTyrQ6UIeUxo z^l}#TZ)PPCXaoUc2+~Ev_c6AF`izF089P8vIsPi09Aam zdkeql-GNi1lryQIz2QOuKjn5k^R!K>Sn~<^K_f+dL94^6-n?5>uSZn{-Q8`&$lxn1 z;2^V-2sDg<7d4Ha#p#gW_7AxW>KQCn6*LRLtQq(xw@))Gi9iwr=5zeE^qi04{IG@9 zB_<(4_U<#5?&oCq1Abpw1d<|PhWB%k??-)o$kkG26+s_uA~=MHctr1;CIqEq2>5{_ zXFo3Fa=Bl*u3Bg)L9>@0hQo@gR&{|0#7m%+=586tyIZQwe1sA-jUq7k!J($`JPhTe z3EXn_dHB#|g@zV%TMIU_m)Bf9T$NH z639@{m(QAoiz<~7TF_m94>w+?L48z<5T;FI`67(DJDQ2UcY&>Yb&`}x1do#Z^?+ZBG5Df4hv6s=aG5o zx&<8s*xs~Cs9+H|LLg%fS8{O3VI5Gnpl8m+-Xk`(Cj!kQz!|qi`8?j^J*ZpI^X8cr zRy;xFG!Qt6tA`))j?^t^f3va(q=di;8N+D|J2KG(os!zfjpLp`hGECTty?i$fQmM zOJo8K6?A0pWIzN$5|E%n0@LN`CLlqlyM5$cNCFacNMO1=-2^1)bhnSZ3rRqN4hc+` zPe>p&2PNp#G=&_AgMb7b2SwRP4FL%{HBBK$;vgVF$3amxQbQoBp!L&kscA|%;t50* z^qby@@+1UwDoFx6q6%6)bdt@bvZ@i-6-m$z*ix+oofm<`2srGHBxnT4C+4gQ5`iiN zoWmQD1f4OwR;3J`7Jxp@*f<&MSfr7)@NP^A;#TdHY-&(2So=%HE zC<6Pu9wP}l2(XU>!)>98)x{#v00IVV^LmUV=#dfZGs7DV@J}X1AQS;d9eO=R67;F3 zQqaIUmK-oEi9oXm1ni;Lc|ArFw3!8g!)Dgv0JD+^G>3p;uPb1k*JC6>d+#$2f8q8& z%t|6qfk1-~JM1yT-@!*@L1(kr&hGvd+ENmMrV!x7#H*`Uqt_Q6SQDXS`Et=FSdd&IkKR=kpp}26#q-s`+a2*NSMH;Or{w27K@EV(5IbdBb)$&?V+$78!SW)g!Sbik z_On^@tA;D1S<&4hPy+$Ov+eEn^08OODrj%Ka0vIp(C_z^MW8_hHc@Bap2&9Wg6{6N zeg^8_U}5^aAt5NmPGH0wZu2^u$a3t0_Js@&b6(nE75BB+LI_H+5n$+yKa7pxxk?rr zB4`Hcd$@Xd9}6umM{-+7(k&uz3;~9+eD%MVz4Pv4&Wt~8h@idomtVwM1h^l1h(KHf z9PU}Y8mlKW*Dyi*we?)iyr6v^o6HHF6M@MH7}qwq68+;`*6#PZn(W$!3A$8hVH9^8 z@U(ZTbWB?jsFi@%4c9iF<*WVOwcOKiL3edwz_sF=jOPDUi$l6b1j-3eH=FqK{?*P- z)HD^`a6y}y%gwcog;rcgM|UtQiNMhWDnIkzRd@Z#?Y711sPbh;&NN)mrJ}m$+AEy- zxQ5R+F)N8cH3FNwUby-k>cb<63EF%A(x34>oxF+r>Zu7qsUm?LUMF5J6|bmpB2hv6 z5o%|H31bLXphs>bQC=$d}PsMgL zhLwhU3x}|X1HVW8yE5ahSE>YEsvL#gpKG^kT>)2c>2n!JzdfaM+D=IV8`zF_WgISM zyME2LZ=-6FN;Oran?Q71LrGpGZwsq%h>5LY9m@^_564FWl+X!qAz$5&l)N}#?J@8-nvW*?k zuQRRq%z?dF)S0zq?3jAZY?S94o>DL9asvJva&kF)=A{>LH3yF$=eaMM;SLxcr^x+( zzX!_0N(mA0zu>UW^Y`t*Hn_pJOSTVLj(JGnmR0zATa_*oae000I_L_t&o0N(1zqVH#KFaQ7m M07*qoM6N<$f*X!_O#lD@ literal 0 HcmV?d00001 diff --git a/Visual_Novel_iOS/Assets.xcassets/Role/role_chat_bg_send.imageset/Contents.json b/Visual_Novel_iOS/Assets.xcassets/Role/role_chat_bg_send.imageset/Contents.json new file mode 100644 index 0000000..a34a740 --- /dev/null +++ b/Visual_Novel_iOS/Assets.xcassets/Role/role_chat_bg_send.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "role_chat_send@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "role_chat_send@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Visual_Novel_iOS/Assets.xcassets/Role/role_chat_bg_send.imageset/role_chat_send@2x.png b/Visual_Novel_iOS/Assets.xcassets/Role/role_chat_bg_send.imageset/role_chat_send@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..2dc8770b3a1818e9381ba5eb8da8957062736c90 GIT binary patch literal 1439 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 Void)? + + lazy var sendBgImgView: UIImageView = { + guard let originalImage = UIImage(named: "role_chat_bg_send") else { + return UIImageView() + } + // 发送消息:尖尖在右上角,focus 点应该在右上角附近 + // 假设图片尺寸,focus 点设置为右上角区域(距离右边和顶部一定距离) + let imageSize = originalImage.size + let focusX = imageSize.width - 20.0 // 距离右边 20pt + let focusY = 20.0 // 距离顶部 20pt + let stretchedImage = originalImage.makeStretchable(from: originalImage, focus: CGPoint(x: focusX, y: focusY)) + let imgV = UIImageView(image: stretchedImage) + imgV.contentMode = .scaleToFill + return imgV + }() + + lazy var respBgImgView: UIImageView = { + guard let originalImage = UIImage(named: "role_chat_bg_response") else { + return UIImageView() + } + // 接收消息:尖尖在左上角,focus 点应该在左上角附近 + // focus 点设置为左上角区域(距离左边和顶部一定距离) + let imageSize = originalImage.size + let focusX = 20.0 // 距离左边 20pt + let focusY = 20.0 // 距离顶部 20pt + let stretchedImage = originalImage.makeStretchable(from: originalImage, focus: CGPoint(x: focusX, y: focusY)) + let imgV = UIImageView(image: stretchedImage) + imgV.contentMode = .scaleToFill + return imgV + }() + private lazy var longPressGesture: UILongPressGestureRecognizer = { let gesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress(_:))) gesture.minimumPressDuration = 0.4 @@ -34,8 +65,8 @@ final class StreamChatBubbleCell: UITableViewCell { private func setupViews() { contentView.addSubview(bubbleView) - bubbleView.layer.cornerRadius = 18 - bubbleView.layer.masksToBounds = true + bubbleView.addSubview(sendBgImgView) + bubbleView.addSubview(respBgImgView) bubbleView.setContentHuggingPriority(.defaultLow, for: .horizontal) bubbleView.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) contentView.addGestureRecognizer(longPressGesture) @@ -52,20 +83,30 @@ final class StreamChatBubbleCell: UITableViewCell { func configure(message: StreamChatMessageModel) { messageLabel.text = message.text + respBgImgView.isHidden = message.isSelf + sendBgImgView.isHidden = !message.isSelf if message.isSelf { - bubbleView.backgroundColor = UIColor.c.cpn + // 发送消息:深色背景,尖尖在右上角 messageLabel.textColor = .white + bubbleView.backgroundColor = .clear + bubbleView.snp.remakeConstraints { make in make.top.equalToSuperview().offset(6) make.bottom.equalToSuperview().offset(-6) make.width.lessThanOrEqualTo(UIScreen.width * 2.0 / 3.0) - make.width.greaterThanOrEqualTo(60) + make.width.greaterThanOrEqualTo(40) make.trailing.equalToSuperview().offset(-16) } + + sendBgImgView.snp.remakeConstraints { make in + make.edges.equalToSuperview() + } } else { - bubbleView.backgroundColor = UIColor.white.withAlphaComponent(0.2) - messageLabel.textColor = .white + // 接收消息:浅色背景,尖尖在左上角 + messageLabel.textColor = UIColor(hex: "#333333") + bubbleView.backgroundColor = .clear + bubbleView.snp.remakeConstraints { make in make.top.equalToSuperview().offset(6) make.bottom.equalToSuperview().offset(-6) @@ -73,6 +114,10 @@ final class StreamChatBubbleCell: UITableViewCell { make.width.greaterThanOrEqualTo(60) make.leading.equalToSuperview().offset(16) } + + respBgImgView.snp.remakeConstraints { make in + make.edges.equalToSuperview() + } } messageLabel.snp.remakeConstraints { make in diff --git a/Visual_Novel_iOS/Src/Utils/Extensions/UIImageExt.swift b/Visual_Novel_iOS/Src/Utils/Extensions/UIImageExt.swift index bb0357a..1315083 100755 --- a/Visual_Novel_iOS/Src/Utils/Extensions/UIImageExt.swift +++ b/Visual_Novel_iOS/Src/Utils/Extensions/UIImageExt.swift @@ -460,6 +460,33 @@ public extension UIImage { func pngBase64String() -> String? { return pngData()?.base64EncodedString() } + + /// 根据“焦点”生成四周可拉伸、四角不变的 UIImage + /// 返回的图 *本身* 就是 resizable,尺寸由外部 Auto Layout 决定 + func makeStretchable(from image: UIImage, + focus: CGPoint) -> UIImage { + + let scale = image.scale + let w = image.size.width * scale + let h = image.size.height * scale + let fx = focus.x * scale + let fy = focus.y * scale + + // 保护四角:以焦点为中心对称留 cap + let capLeft = min(fx, w - fx) * 0.5 + let capRight = w - capLeft + let capTop = min(fy, h - fy) * 0.5 + let capBottom = h - capTop + + let insets = UIEdgeInsets(top: capTop, + left: capLeft, + bottom: capBottom, + right: capRight) + + // 只生成可拉伸图,不立即渲染固定尺寸 + return image.resizableImage(withCapInsets: insets, + resizingMode: .stretch) + } /// Base 64 encoded JPEG data of the image. ///