From 1d4fd0974decef91de954bedc0cbdb52aa58b4bc Mon Sep 17 00:00:00 2001 From: masonqiao Date: Wed, 15 Jul 2026 17:36:53 +0800 Subject: [PATCH] =?UTF-8?q?=E3=80=90Room=E3=80=91Add=20screen=20orientatio?= =?UTF-8?q?n=20switching=20support=20for=20screen=20sharing=20in=20room=20?= =?UTF-8?q?scenes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/RTCube/AppDelegate.swift | 5 + .../Contents.json | 21 +++ .../room_switch_landscape@3x.png | Bin 0 -> 2269 bytes .../Contents.json | 21 +++ .../room_switch_portrait@3x.png | Bin 0 -> 2211 bytes room/Source/RoomMainViewController.swift | 34 ++++- room/Source/View/Main/RoomView.swift | 5 + .../View/Main/RoomView/StandardRoomView.swift | 4 + room/Source/View/RoomMainView.swift | 128 ++++++++++++++++++ 9 files changed, 213 insertions(+), 5 deletions(-) create mode 100644 room/Resources/TUIRoomKit.xcassets/room_switch_landscape.imageset/Contents.json create mode 100644 room/Resources/TUIRoomKit.xcassets/room_switch_landscape.imageset/room_switch_landscape@3x.png create mode 100644 room/Resources/TUIRoomKit.xcassets/room_switch_portrait.imageset/Contents.json create mode 100644 room/Resources/TUIRoomKit.xcassets/room_switch_portrait.imageset/room_switch_portrait@3x.png diff --git a/application/RTCube/AppDelegate.swift b/application/RTCube/AppDelegate.swift index 30c200c20..c0db1e403 100644 --- a/application/RTCube/AppDelegate.swift +++ b/application/RTCube/AppDelegate.swift @@ -14,6 +14,7 @@ import TCMediaX import TEBeautyKitWrapper #endif import TUICore +import TUIRoomKit import TXLiteAVSDK_Professional import UIKit import UserNotifications @@ -87,6 +88,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask { + if let topVC = window?.rootViewController?.topMostViewController(), + let roomVC = topVC as? RoomMainViewController { + return roomVC.isLandscapeMode ? .landscape : .portrait + } return AppDelegate.allowedOrientations } diff --git a/room/Resources/TUIRoomKit.xcassets/room_switch_landscape.imageset/Contents.json b/room/Resources/TUIRoomKit.xcassets/room_switch_landscape.imageset/Contents.json new file mode 100644 index 000000000..990bf4ef4 --- /dev/null +++ b/room/Resources/TUIRoomKit.xcassets/room_switch_landscape.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "room_switch_landscape@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/room/Resources/TUIRoomKit.xcassets/room_switch_landscape.imageset/room_switch_landscape@3x.png b/room/Resources/TUIRoomKit.xcassets/room_switch_landscape.imageset/room_switch_landscape@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..ab3cc42c3657ed3f3eb83e6f1a498a0ded0dde3b GIT binary patch literal 2269 zcmV<32qO21P)$~kXiO&Y4=|a;olz5YF|3U{ zHKR;23j;AIf{!pIFlYuG9s-3@ptL;Z_q*OUwX|^iqAkoX!SD9o?_TdY-#ORm2Z+3M z-`dpF)RNonUUoX2zKMwm-_p_&&n*YvUs6({HX4oD8jVI(T3VW8Hk-8t1qC@k;2p^f zNXT1^6N!?HxETC@83j~|yr9N4pGk9P0gy*jJanyb_4vgOl0{C|Ig zS^e=cA}$7;J$v>5UgSF=6FxaP>Fw$1am7n?B#;@04jsxjnM@k~_DjT_J9maejEIQ= z7cN{dV3q4}NL^1l;h})KWH&Z87M7Kj>G+$BP^;B{#3Bt*Gaz`?Js!`>^XJcs;a8;sTyE{SC)qUXr7TB&16{j8Vn>pdvI{j{q*TmCsu!1qzGeUV?NlKMT5bh z%FoZ&VH+*4t*sT^-Q5#PpP`rm=gysL!0-(X-23gwtYV8V!!`xP+&E8l{loWwtS_UyC6;qZtw10=x+y;}!WC^>xiaK-D_ zuLm}}Z&L=yD_>51fBg6{w(*ISiTq}mo10suN?_Z}h5wMJL=rY+09Ky*et^V3dGh2l zoR<}m=AcTD33j_(4^OlhlAwVkj7FLtkpZ;!7^-;;V>g-3D<29n0q^U9B;-LIN{=2r z>V5U<)l|5-VHv=E9=`YYy#D6Ro7r^K`OQFefTt)KMr_+AFyWt_ zhMeKy;f1cQuB1j|$pqgWF8MqdpphN7by&xM^XJdo7{O8R!7-P}aJ+c&!bMteXaQ}N zq1V4s0a^5E^)+y9XFq)S;1wAT`YOmr`1bAFmq%5w*=&P-eSHCAt`45;^?EDeQtG*% z_V)HJhy(*$QY)xFzC1b93`WxW5La^CL+?fC12bIz576 ztw=Ipk=-Xxp8Q?JfrW*I)@T?&PzsuwnzT5ik0UWaQUT`h{FXmeq3u?Hv@xEcq|15B zFARXUB5i2>k<#0_kF8Rzju3+5>~GGi@`wEh%-7^c_b`g0qsQ&I(TU=Sp+5OJWbt?kyOOP6{@1^|mK(aIvN%3r(;t<`O3_+5R{t`a*O z|1&`BaHv>V0bYs3x# zyHGf?ABhqmsQ|JhJ4OZAg2R!`SV*dm)8A1a1Z}AeSS14>?NK5Ieyz$Nl^F$JNxB38<*3kQ%Gl zc*#*>06Q?*WCS%(QAETx1!V^M?F*4B%sPoIt$^9N^y zpo4}-j~@Lf;sBo^q6BH<$Zj(Kxc~!ZXJ;L(m~c0U#Tde}+Lg{_LF2@U6TgTp1YLd7 zMoHJ}^#Ow$V8DY154o z_wHQ>Bw&KpvZ0~DC^8&ad86O*hij@1yTXNqg`G4OcDvnh2jZ1ip#q7O#^Jdsy!H5wcBD6^u4=uK= zQk%_|J3Kt>K^ZM0m_WU6Zf-Wwu>t2>`zdmWYNyk=oNASzuL5J2 zfAHYJd@N8FR^9>PyOiD^RT>PEFkV|*>jM)^{6d2&0dBAI%MFnNFlIS^{J05=!4Q}4 z)eGX^Me~f9QsMH~piTa|;`sRZVn;`ZYpd#uB%JYzKaEGnjlN5$m!|}A)dJX8%=WsV z0_zf6(USSuTrlC{%c^smbBGubcQJ(0Fg-o}t=wrQrJLDq$Br{LT_xhfNvqYWfmdn_ z`ihiIJ`SkgUm!j%^-EYah>Vz@pSJ`{1i^+RsEZeB;$;^K8!_065bo1iR;QrPIfzJe zn5Hi-F4hHqIx7-LxF1RTc$yNy_Pa!UJdGHhIm?l>{&+ePC>dDoVm4#3ABinFa#zZ_ r&*xdEWT_6<+!$wAvxvp8MT7qUit>0OSg`iT00000NkvXXu0mjf(ojc> literal 0 HcmV?d00001 diff --git a/room/Resources/TUIRoomKit.xcassets/room_switch_portrait.imageset/Contents.json b/room/Resources/TUIRoomKit.xcassets/room_switch_portrait.imageset/Contents.json new file mode 100644 index 000000000..ff7cb2550 --- /dev/null +++ b/room/Resources/TUIRoomKit.xcassets/room_switch_portrait.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "room_switch_portrait@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/room/Resources/TUIRoomKit.xcassets/room_switch_portrait.imageset/room_switch_portrait@3x.png b/room/Resources/TUIRoomKit.xcassets/room_switch_portrait.imageset/room_switch_portrait@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..449308e423d1c058921634a704261a7bf440ff41 GIT binary patch literal 2211 zcmV;U2weAxP)3WQa=i(KjEBMl+gB;?FRdL^GPGkG`3hOrmCR zCYeMdF=z$UKn;wV0cQY#aw*aa(D}Y&Pg824*CQ>RFTs7-F-zrSQ*}m6d7DX0t}8(@Ev!<@r{tRljA+mV6-e8>tLf$V-e9 z>9UNJ6%b8u3OzJ5oNj zB?X*0b7nVw$hRUW_~hiIueY~%E>)q|0+q3U|NbJ2#iHYD#Uid>zy4MviKG;8{``3p zR=EwA^mWw*j|ALBbMWB7;);q217A}STCMhvWUL`!1%$7<$KzRk{P?l!YY85ShK7d1 z`uh4Jx(1Zs3tGdQM7~3l$Ykv9O&BP=W_Wnm-PP6Q!s;)H4Z_&im>>JhqRC{Eii(O1 zI7Ta*o12Bh;h2c`JK|Qr*|TTcG1PK{d;R+L+><9y=9iY10%C))xVRX=VtC*JfuKv+ zT3T96P{L?DpN~rcSouy2l@$Kr!-pT8PNzp?7+?uLScGTWwrvKuLRn*DW7UfnFNR`$ zE~Wy4D_==}zkmNej`4{NGx=DUpPyf$OW@cnK>Sb`k0q?D0IWRyeK&=F@ZiBm1TV`X z!$Fsz5^8E{jEF=_VF@}|!oO?HUsD0x?J+d)8k^l@Ca-)Xs09384=kY&?ofW{{Goa^!ZqzJ0Mb&H%1)W7AWEGXV0GbBIilbK8)S{2MqZm zBO{-BdU{mN#?%CVJ3{gWC_ons+`hmU3OIM}Ts1Q|`aLu@C$bz*pFW+VEV#5mwq23; zS4Dy}eXc$g0O4JIeSM!qifCy>gZ!$`T6(Z<`B?#omSo=_9UWa1n;b0F1uyOVM^*qdB5UK_yLVX^@g)k@DLJxYEsxy& zY5q9>S2nZxD+qG~BYPGS5{DArn3{6*wgbFIxc5e~}}Awac=yRi{NEkrAL^w?-rZXz@?6LBQgDFOq;#+mQQ| z(1P65K>nQZ)t+<#i!IB_CaqR!UPkun5(SRX->{GL3ze~V@7{gL?v9ESpoIFAT`SUr z!NI}4zyghLWJj{g;kBgZ;3{VV_TgY3tZp{S+ z6mTT>kfW>soCG5rckbMo5~(p8P*qhW6RSwPfXXd=m!#_H(X50)J>Qgq#Xfzr_oa0p{tJDWsG`xHF?xfg6IC0`cS&%H@gv&dW_g7_u zo7D+cXZXEU3JCVi<(?V(Zzb3;vOZm=>61;$Yx+LV{_y)>2zcYh4JXWG zg1cpVd%Ia=Ia*p;%*xImjZ__Vg<&S$bcLFl8dGa)Yu3gFWT)WVFT|xE;qUL!=0r_^ zy1TpmO-)TJxU3{K9c5%MSyv*9P+3`-4>vC3H5Rk)>eZ`Jxq@qEfDVUa5}St}!BKg2 zb#=kW$cP7Rw5(79{r!!fHy}kdCZi+7{CH)kg)r?mHh41X_ zw9xMn;=hIPj`ilptO1Z~OX4xPw^RaJXc_AcG1>G z-62VX5I!aMOIrnpida}!u!UO$K|&JT#RoU>k%dAc2Du2)9-U=(3jUmr9z{OO^rfYx zhVVycMH&THpxVRJvXBLQ2R-A|;2clxUC7vroxZ l9qzd?&ah{Z%*K`o{sX3vKn6V6p@{$h002ovPDHLkV1hWFC0+mk literal 0 HcmV?d00001 diff --git a/room/Source/RoomMainViewController.swift b/room/Source/RoomMainViewController.swift index 0db11cf2c..d6a11df40 100644 --- a/room/Source/RoomMainViewController.swift +++ b/room/Source/RoomMainViewController.swift @@ -12,6 +12,18 @@ import SnapKit public class RoomMainViewController: UIViewController, RouterContext { // MARK: - Properties private let rootView: RoomMainView + + /// When `true`, the controller only allows landscape orientations; + /// when `false` (default) only portrait is allowed. + public var isLandscapeMode: Bool = false { + didSet { + if #available(iOS 16.0, *) { + setNeedsUpdateOfSupportedInterfaceOrientations() + } else { + UIViewController.attemptRotationToDeviceOrientation() + } + } + } // MARK: - Lifecycle @@ -41,18 +53,30 @@ public class RoomMainViewController: UIViewController, RouterContext { public override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) UIApplication.shared.isIdleTimerDisabled = false + + // Reset to portrait when leaving the room + isLandscapeMode = false + if #available(iOS 16.0, *) { + setNeedsUpdateOfSupportedInterfaceOrientations() + if let windowScene = view.window?.windowScene { + let geometry = UIWindowScene.GeometryPreferences.iOS(interfaceOrientations: .portrait) + windowScene.requestGeometryUpdate(geometry) + } + } else { + UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation") + UIViewController.attemptRotationToDeviceOrientation() + } } public override var supportedInterfaceOrientations: UIInterfaceOrientationMask { - return .portrait + return isLandscapeMode ? .landscape : .portrait } - - + public override var shouldAutorotate: Bool { return false } - + public override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation { - return .portrait + return isLandscapeMode ? .landscapeRight : .portrait } } diff --git a/room/Source/View/Main/RoomView.swift b/room/Source/View/Main/RoomView.swift index b745699ee..9eb361277 100644 --- a/room/Source/View/Main/RoomView.swift +++ b/room/Source/View/Main/RoomView.swift @@ -76,6 +76,11 @@ public class RoomView: UIView, BaseView { webinarRoomView.delegate = self } } + + public func setScrollEnabled(_ enabled: Bool) { + guard roomType == .standard else { return } + standardRoomView.setScrollEnabled(enabled) + } } extension RoomView: WebinarRoomViewDelegate { diff --git a/room/Source/View/Main/RoomView/StandardRoomView.swift b/room/Source/View/Main/RoomView/StandardRoomView.swift index 70796056c..f477c2ac0 100644 --- a/room/Source/View/Main/RoomView/StandardRoomView.swift +++ b/room/Source/View/Main/RoomView/StandardRoomView.swift @@ -121,6 +121,10 @@ class StandardRoomView: UIView, BaseView { backgroundColor = .clear } + public func setScrollEnabled(_ enabled: Bool) { + collectionView.isScrollEnabled = enabled + } + public func setupBindings() { // MARK: - Real Data Binding roomParticipantStore.state diff --git a/room/Source/View/RoomMainView.swift b/room/Source/View/RoomMainView.swift index 64031fe6a..7d3302184 100644 --- a/room/Source/View/RoomMainView.swift +++ b/room/Source/View/RoomMainView.swift @@ -52,6 +52,8 @@ public class RoomMainView: UIView, BaseView { private var roomType: RoomType = .standard private var localParticipant: RoomParticipant? private let deviceOperator = DeviceOperator() + private var currentScreenSharerID: String? + private var isCurrentlyLandscape: Bool = false // MARK: - UI Components private lazy var topBarView: RoomTopBarView = { @@ -93,6 +95,17 @@ public class RoomMainView: UIView, BaseView { return view }() + private lazy var orientationSwitchButton: UIButton = { + let button = UIButton(type: .custom) + button.setImage(ResourceLoader.loadImage("room_switch_landscape"), for: .normal) + button.imageView?.contentMode = .scaleAspectFit + button.contentHorizontalAlignment = .fill + button.contentVerticalAlignment = .fill + button.addTarget(self, action: #selector(onOrientationSwitchButtonTapped), for: .touchUpInside) + button.isHidden = true + return button + }() + private lazy var listView: ParticipantListView = { let listView = ParticipantListView(roomID: roomID, roomType: roomType) return listView @@ -151,6 +164,7 @@ public class RoomMainView: UIView, BaseView { addSubview(topBarView) addSubview(roomView) roomView.addSubview(screenShareOverlay) + roomView.addSubview(orientationSwitchButton) addSubview(barrageStreamView) addSubview(barrageInputView) @@ -201,6 +215,12 @@ public class RoomMainView: UIView, BaseView { make.top.equalTo(topBarView.snp.bottom).offset(12) make.leading.equalToSuperview().offset(16) } + + orientationSwitchButton.snp.makeConstraints { make in + make.width.height.equalTo(32) + make.trailing.equalToSuperview().offset(-12) + make.bottom.equalToSuperview().offset(-12) + } } public func setupStyles() { @@ -240,6 +260,16 @@ public class RoomMainView: UIView, BaseView { } .store(in: &cancellableSet) + participantStore.state.subscribe(StatePublisherSelector(keyPath: \.participantWithScreen)) + .map { $0?.userID } + .removeDuplicates() + .receive(on: RunLoop.main) + .sink { [weak self] userID in + guard let self = self else { return } + onScreenSharerChanged(newSharerID: userID) + } + .store(in: &cancellableSet) + participantStore.participantEventPublisher .receive(on: RunLoop.main) .sink { [weak self] event in @@ -302,6 +332,104 @@ public class RoomMainView: UIView, BaseView { .store(in: &cancellableSet) } + + public override func layoutSubviews() { + super.layoutSubviews() + let landscape = resolveIsLandscape() + if landscape != isCurrentlyLandscape { + isCurrentlyLandscape = landscape + applyOrientationLayout(isLandscape: landscape) + updateOrientationSwitchButtonImage(isLandscape: landscape) + } + } +} + +// MARK: - Orientation +extension RoomMainView { + @objc private func onOrientationSwitchButtonTapped() { + switchOrientation() + } + + private func switchOrientation() { + guard let viewController = routerContext as? UIViewController else { return } + + let isCurrentlyLandscape = bounds.width > bounds.height + let targetLandscape = !isCurrentlyLandscape + + if let roomVC = viewController as? RoomMainViewController { + roomVC.isLandscapeMode = targetLandscape + } + + if #available(iOS 16.0, *) { + viewController.setNeedsUpdateOfSupportedInterfaceOrientations() + if let windowScene = viewController.view.window?.windowScene { + let mask: UIInterfaceOrientationMask = targetLandscape ? .landscape : .portrait + let geometry = UIWindowScene.GeometryPreferences.iOS(interfaceOrientations: mask) + windowScene.requestGeometryUpdate(geometry) + } + } else { + let targetOrientation: UIInterfaceOrientation = targetLandscape ? .landscapeRight : .portrait + UIDevice.current.setValue(targetOrientation.rawValue, forKey: "orientation") + UIViewController.attemptRotationToDeviceOrientation() + } + } + + private func forcePortraitIfLandscape() { + guard let viewController = routerContext as? RoomMainViewController else { return } + if viewController.isLandscapeMode { + viewController.isLandscapeMode = false + } + } + + private func resolveIsLandscape() -> Bool { + guard let viewController = routerContext as? RoomMainViewController else { return false } + return viewController.isLandscapeMode + } + + private func onScreenSharerChanged(newSharerID: String?) { + if currentScreenSharerID == newSharerID { return } + currentScreenSharerID = newSharerID + updateOrientationSwitchButtonVisibility() + if newSharerID == nil || newSharerID?.isEmpty == true { + forcePortraitIfLandscape() + } + } + + private func updateOrientationSwitchButtonVisibility() { + let localUserID = LoginStore.shared.state.value.loginUserInfo?.userID + let hasRemoteSharer = currentScreenSharerID != nil + && !(currentScreenSharerID?.isEmpty ?? true) + && currentScreenSharerID != localUserID + let shouldShow = roomType != .webinar && hasRemoteSharer + orientationSwitchButton.isHidden = !shouldShow + } + + private func updateOrientationSwitchButtonImage(isLandscape: Bool) { + let name = isLandscape ? "room_switch_portrait" : "room_switch_landscape" + orientationSwitchButton.setImage(ResourceLoader.loadImage(name), for: .normal) + } + + private func applyOrientationLayout(isLandscape: Bool) { + topBarView.isHidden = isLandscape + bottomBarView.isHidden = isLandscape + subtitleView?.isHidden = isLandscape + if roomType == .webinar { + barrageInputView.isHidden = isLandscape + barrageStreamView.isHidden = isLandscape + } + recordingFloatingView.alpha = isLandscape ? 0 : 1 + roomView.setScrollEnabled(!isLandscape) + + roomView.snp.remakeConstraints { make in + if isLandscape { + make.edges.equalTo(safeAreaLayoutGuide) + } else { + make.left.right.equalToSuperview() + make.top.equalTo(topBarView.snp.bottom) + make.bottom.equalTo(bottomBarView.snp.top).offset(-10) + } + } + } } extension RoomMainView {