mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-02 20:55:48 +00:00
Smooth fullscreen transitions and tweaking PiP placeholder visibility
This commit is contained in:
parent
1424e7135d
commit
c8ab1b8971
@ -180,7 +180,15 @@ public final class _UpdatedChildComponent {
|
||||
var _opacity: CGFloat?
|
||||
var _cornerRadius: CGFloat?
|
||||
var _clipsToBounds: Bool?
|
||||
|
||||
/// Quick animation addition
|
||||
var _animations: [AnimationKey: AnimationType] = [:]
|
||||
|
||||
public typealias AnimationKey = String
|
||||
public enum AnimationType {
|
||||
case transition
|
||||
case custom(duration: TimeInterval, curveAndOtherParams: Any)
|
||||
}
|
||||
|
||||
fileprivate var transitionAppear: Transition.Appear?
|
||||
fileprivate var transitionAppearWithGuide: (Transition.AppearWithGuide, _AnyChildComponent.Id)?
|
||||
fileprivate var transitionDisappear: Transition.Disappear?
|
||||
@ -240,6 +248,11 @@ public final class _UpdatedChildComponent {
|
||||
self._position = position
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult public func animation(key: AnimationKey) -> _UpdatedChildComponent {
|
||||
self._animations[key] = .transition
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult public func scale(_ scale: CGFloat) -> _UpdatedChildComponent {
|
||||
self._scale = scale
|
||||
@ -695,6 +708,8 @@ public extension CombinedComponent {
|
||||
|
||||
view.insertSubview(updatedChild.view, at: index)
|
||||
|
||||
let currentPosition = updatedChild.view.center
|
||||
|
||||
if let scale = updatedChild._scale {
|
||||
updatedChild.view.bounds = CGRect(origin: CGPoint(), size: updatedChild.size)
|
||||
updatedChild.view.center = updatedChild._position ?? CGPoint()
|
||||
@ -702,6 +717,15 @@ public extension CombinedComponent {
|
||||
} else {
|
||||
updatedChild.view.frame = updatedChild.size.centered(around: updatedChild._position ?? CGPoint())
|
||||
}
|
||||
// for animation in updatedChild._animations { }
|
||||
|
||||
if updatedChild._animations["position"] != nil, let position = updatedChild._position {
|
||||
transition.animatePosition(view: updatedChild.view, from: currentPosition, to: position)
|
||||
}
|
||||
if updatedChild._animations["opacity"] != nil, let opacity = updatedChild._opacity {
|
||||
transition.animateAlpha(view: updatedChild.view, from: updatedChild.view.alpha, to: opacity)
|
||||
}
|
||||
|
||||
updatedChild.view.alpha = updatedChild._opacity ?? 1.0
|
||||
updatedChild.view.clipsToBounds = updatedChild._clipsToBounds ?? false
|
||||
updatedChild.view.layer.cornerRadius = updatedChild._cornerRadius ?? 0.0
|
||||
|
||||
@ -283,7 +283,11 @@ public final class MediaStreamComponent: CombinedComponent {
|
||||
let dismissTapComponent = Child(Rectangle.self)
|
||||
let video = Child(MediaStreamVideoComponent.self)
|
||||
let sheet = Child(StreamSheetComponent.self)
|
||||
let fullscreenOverlay = Child(StreamSheetComponent.self)
|
||||
// let fullscreenOverlay = Child(StreamSheetComponent.self)
|
||||
let topItem = Child(environment: Empty.self)
|
||||
// let viewerCounter = Child(ParticipantsComponent.self)
|
||||
let fullscreenBottomItem = Child(environment: Empty.self)
|
||||
let buttonsRow = Child(environment: Empty.self)
|
||||
|
||||
let activatePictureInPicture = StoredActionSlot(Action<Void>.self)
|
||||
let deactivatePictureInPicture = StoredActionSlot(Void.self)
|
||||
@ -346,7 +350,7 @@ public final class MediaStreamComponent: CombinedComponent {
|
||||
? (context.availableSize.width - videoInset * 2) / 16 * 9
|
||||
: context.state.videoSize?.height ?? (min(context.availableSize.width, context.availableSize.height) - videoInset * 2) / 16 * 9
|
||||
let bottomPadding = 40 + environment.safeInsets.bottom
|
||||
let sheetHeight: CGFloat = isFullscreen
|
||||
let requiredSheetHeight: CGFloat = isFullscreen
|
||||
? context.availableSize.height
|
||||
: (44 + videoHeight + 40 + 69 + 16 + 32 + 70 + bottomPadding)
|
||||
|
||||
@ -357,14 +361,14 @@ public final class MediaStreamComponent: CombinedComponent {
|
||||
safeAreaTopInView = context.view.safeAreaInsets.top
|
||||
}
|
||||
|
||||
let isFullyDragged = context.availableSize.height - sheetHeight + state.dismissOffset - safeAreaTopInView < 30
|
||||
let isFullyDragged = context.availableSize.height - requiredSheetHeight + state.dismissOffset - safeAreaTopInView < 30
|
||||
|
||||
var dragOffset = context.state.dismissOffset
|
||||
if isFullyDragged {
|
||||
dragOffset = max(context.state.dismissOffset, sheetHeight - context.availableSize.height + safeAreaTopInView)
|
||||
dragOffset = max(context.state.dismissOffset, requiredSheetHeight - context.availableSize.height + safeAreaTopInView)
|
||||
}
|
||||
|
||||
let dismissTapAreaHeight = isFullscreen ? 0 : (context.availableSize.height - sheetHeight + dragOffset)
|
||||
let dismissTapAreaHeight = isFullscreen ? 0 : (context.availableSize.height - requiredSheetHeight + dragOffset)
|
||||
let dismissTapComponent = dismissTapComponent.update(
|
||||
component: Rectangle(color: .red.withAlphaComponent(0)),
|
||||
availableSize: CGSize(width: context.availableSize.width, height: dismissTapAreaHeight),
|
||||
@ -742,23 +746,31 @@ public final class MediaStreamComponent: CombinedComponent {
|
||||
} else {
|
||||
if isFullyDragged || state.initialOffset != 0 {
|
||||
state.updateDismissOffset(value: 0.0, interactive: false)
|
||||
state.updateDismissOffset(value: 0.0, interactive: false)
|
||||
} else {
|
||||
activatePictureInPicture.invoke(Action {
|
||||
if state.isPictureInPictureSupported {
|
||||
activatePictureInPicture.invoke(Action {
|
||||
guard let controller = controller() as? MediaStreamComponentController else {
|
||||
return
|
||||
}
|
||||
controller.dismiss(closing: false, manual: true)
|
||||
})
|
||||
} else {
|
||||
guard let controller = controller() as? MediaStreamComponentController else {
|
||||
return
|
||||
}
|
||||
controller.dismiss(closing: false, manual: true)
|
||||
})
|
||||
}
|
||||
// let _ = call.leave(terminateIfPossible: false)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if isFullyDragged {
|
||||
state.updateDismissOffset(value: sheetHeight - availableSize.height + safeAreaTop, interactive: false)
|
||||
state.updateDismissOffset(value: requiredSheetHeight - availableSize.height + safeAreaTop, interactive: false)
|
||||
} else {
|
||||
if velocity.y < -200 {
|
||||
// Expand
|
||||
state.updateDismissOffset(value: sheetHeight - availableSize.height + safeAreaTop, interactive: false)
|
||||
state.updateDismissOffset(value: requiredSheetHeight - availableSize.height + safeAreaTop, interactive: false)
|
||||
} else {
|
||||
state.updateDismissOffset(value: 0.0, interactive: false)
|
||||
}
|
||||
@ -792,7 +804,7 @@ public final class MediaStreamComponent: CombinedComponent {
|
||||
.gesture(.pan(onPanGesture))
|
||||
)
|
||||
|
||||
if !isFullscreen {
|
||||
if !isFullscreen || state.isFullscreen {
|
||||
let imageRenderScale = UIScreen.main.scale
|
||||
let bottomComponent = AnyComponent(ButtonsRowComponent(
|
||||
bottomInset: environment.safeInsets.bottom,
|
||||
@ -872,10 +884,10 @@ public final class MediaStreamComponent: CombinedComponent {
|
||||
)),
|
||||
action: { [weak state] in
|
||||
guard let state = state else { return }
|
||||
guard state.videoIsPlayable else {
|
||||
state.isFullscreen = false
|
||||
return
|
||||
}
|
||||
// guard state.videoIsPlayable else {
|
||||
// state.isFullscreen = false
|
||||
// return
|
||||
// }
|
||||
if let controller = controller() as? MediaStreamComponentController {
|
||||
// guard let _ = state.videoSize else { return }
|
||||
state.isFullscreen.toggle()
|
||||
@ -907,25 +919,33 @@ public final class MediaStreamComponent: CombinedComponent {
|
||||
).minSize(CGSize(width: 44.0, height: 44.0)))
|
||||
))
|
||||
|
||||
let sheetHeight: CGFloat = max(requiredSheetHeight - dragOffset, requiredSheetHeight)
|
||||
let topOffset: CGFloat = isFullscreen
|
||||
? max(context.state.dismissOffset, 0)
|
||||
: (context.availableSize.height - requiredSheetHeight + dragOffset)
|
||||
|
||||
let sheet = sheet.update(
|
||||
component: StreamSheetComponent(
|
||||
topComponent: AnyComponent(navigationComponent),
|
||||
bottomButtonsRow: bottomComponent,
|
||||
topOffset: context.availableSize.height - sheetHeight + dragOffset,
|
||||
sheetHeight: max(sheetHeight - dragOffset, sheetHeight),
|
||||
topOffset: topOffset,
|
||||
sheetHeight: sheetHeight,
|
||||
backgroundColor: isFullscreen ? .clear : (isFullyDragged ? fullscreenBackgroundColor : panelBackgroundColor),
|
||||
bottomPadding: bottomPadding,
|
||||
participantsCount: context.state.originInfo?.memberCount ?? 0, // Int.random(in: 0...999998)// [0, 5, 15, 16, 95, 100, 16042, 942539].randomElement()!
|
||||
isFullyExtended: isFullyDragged,
|
||||
deviceCornerRadius: ((controller() as? MediaStreamComponentController)?.validLayout?.deviceMetrics.screenCornerRadius ?? 1) - 1,
|
||||
videoHeight: videoHeight
|
||||
videoHeight: videoHeight,
|
||||
isFullscreen: isFullscreen,
|
||||
fullscreenTopComponent: AnyComponent(navigationComponent),
|
||||
fullscreenBottomComponent: bottomComponent
|
||||
),
|
||||
availableSize: context.availableSize,
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
let sheetOffset: CGFloat = context.availableSize.height - sheetHeight + dragOffset
|
||||
let sheetPosition = sheetOffset + sheetHeight / 2
|
||||
let sheetOffset: CGFloat = context.availableSize.height - requiredSheetHeight + dragOffset
|
||||
let sheetPosition = sheetOffset + requiredSheetHeight / 2
|
||||
// Sheet underneath the video when in modal sheet
|
||||
context.add(sheet
|
||||
.position(.init(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2))
|
||||
@ -935,18 +955,26 @@ public final class MediaStreamComponent: CombinedComponent {
|
||||
if isFullscreen {
|
||||
videoPos = context.availableSize.height / 2 + dragOffset
|
||||
} else {
|
||||
videoPos = sheetPosition - sheetHeight / 2 + videoHeight / 2 + 50 + 12
|
||||
videoPos = sheetPosition - requiredSheetHeight / 2 + videoHeight / 2 + 50 + 12
|
||||
}
|
||||
context.add(video
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: videoPos))
|
||||
)
|
||||
} else {
|
||||
context.add(video
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2 + dragOffset)
|
||||
))
|
||||
}
|
||||
|
||||
if isFullscreen {
|
||||
|
||||
//
|
||||
//
|
||||
//
|
||||
var availableWidth: CGFloat { context.availableSize.width }
|
||||
var contentHeight: CGFloat { 44.0 }
|
||||
// print(topItem)
|
||||
// let size = context.availableSize
|
||||
|
||||
let topItem = topItem.update(
|
||||
component: AnyComponent(navigationComponent),
|
||||
availableSize: CGSize(width: availableWidth, height: contentHeight),
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
let fullScreenToolbarComponent = AnyComponent(ToolbarComponent(
|
||||
bottomInset: environment.safeInsets.bottom,
|
||||
sideInset: environment.safeInsets.left,
|
||||
@ -981,26 +1009,102 @@ public final class MediaStreamComponent: CombinedComponent {
|
||||
).minSize(CGSize(width: 64.0, height: 80)))/* : nil*/,
|
||||
centerItem: infoItem
|
||||
))
|
||||
let fullScreenOverlayComponent = fullscreenOverlay.update(
|
||||
|
||||
let buttonsRow = buttonsRow.update(
|
||||
component: bottomComponent,
|
||||
availableSize: CGSize(width: availableWidth, height: contentHeight),
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
let fullscreenBottomItem = fullscreenBottomItem.update(
|
||||
component: fullScreenToolbarComponent,
|
||||
availableSize: CGSize(width: availableWidth, height: contentHeight),
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
context.add(topItem
|
||||
.position(CGPoint(x: topItem.size.width / 2.0, y: topOffset + (isFullscreen ? topItem.size.height / 2.0 : 32)))
|
||||
.opacity((!isFullscreen || state.displayUI) ? 1 : 0)
|
||||
// .animation(key: "position")
|
||||
)
|
||||
|
||||
context.add(buttonsRow
|
||||
.opacity(isFullscreen ? 0 : 1)
|
||||
// .animation(key: "opacity")
|
||||
.position(CGPoint(x: buttonsRow.size.width / 2, y: sheetHeight - 50 / 2 + topOffset - bottomPadding))
|
||||
)
|
||||
|
||||
context.add(fullscreenBottomItem
|
||||
.opacity((isFullscreen && state.displayUI) ? 1 : 0)
|
||||
// .animation(key: "opacity")
|
||||
.position(CGPoint(x: fullscreenBottomItem.size.width / 2, y: context.availableSize.height - fullscreenBottomItem.size.height / 2 + topOffset - 0.0))
|
||||
)
|
||||
//
|
||||
//
|
||||
//
|
||||
} else {
|
||||
let fullScreenToolbarComponent = AnyComponent(ToolbarComponent(
|
||||
bottomInset: environment.safeInsets.bottom,
|
||||
sideInset: environment.safeInsets.left,
|
||||
leftItem: AnyComponent(Button(
|
||||
content: AnyComponent(BundleIconComponent(
|
||||
name: "Chat/Input/Accessory Panels/MessageSelectionForward",
|
||||
tintColor: .white
|
||||
)),
|
||||
action: {
|
||||
guard let controller = controller() as? MediaStreamComponentController else {
|
||||
return
|
||||
}
|
||||
controller.presentShare()
|
||||
}
|
||||
).minSize(CGSize(width: 64.0, height: 80))),
|
||||
rightItem: /*state.hasVideo ?*/ AnyComponent(Button(
|
||||
content: AnyComponent(BundleIconComponent(
|
||||
name: isFullscreen ? "Media Gallery/Minimize" : "Media Gallery/Fullscreen",
|
||||
tintColor: .white
|
||||
)),
|
||||
action: {
|
||||
state.isFullscreen = false
|
||||
state.prevFullscreenOrientation = UIDevice.current.orientation
|
||||
if let controller = controller() as? MediaStreamComponentController {
|
||||
if canEnforceOrientation {
|
||||
controller.updateOrientation(orientation: .portrait)
|
||||
} else {
|
||||
state.updated(transition: .easeInOut(duration: 0.25)) // updated(.easeInOut(duration: 0.3))
|
||||
}
|
||||
}
|
||||
}
|
||||
).minSize(CGSize(width: 64.0, height: 80)))/* : nil*/,
|
||||
centerItem: infoItem
|
||||
))
|
||||
let fullScreenOverlayComponent = sheet.update(
|
||||
component: StreamSheetComponent(
|
||||
topComponent: AnyComponent(navigationComponent),
|
||||
bottomButtonsRow: fullScreenToolbarComponent,
|
||||
topOffset: /*context.availableSize.height - sheetHeight +*/ max(context.state.dismissOffset, 0),
|
||||
sheetHeight: context.availableSize.height,// max(sheetHeight - context.state.dismissOffset, sheetHeight),
|
||||
backgroundColor: /*isFullscreen ? .clear : */ (isFullyDragged ? fullscreenBackgroundColor : panelBackgroundColor),
|
||||
backgroundColor: isFullscreen ? .clear : (isFullyDragged ? fullscreenBackgroundColor : panelBackgroundColor),
|
||||
bottomPadding: 0,
|
||||
participantsCount: -1,
|
||||
isFullyExtended: isFullyDragged,
|
||||
deviceCornerRadius: ((controller() as? MediaStreamComponentController)?.validLayout?.deviceMetrics.screenCornerRadius ?? 1) - 1,
|
||||
videoHeight: videoHeight
|
||||
videoHeight: videoHeight,
|
||||
isFullscreen: isFullscreen,
|
||||
fullscreenTopComponent: AnyComponent(navigationComponent),
|
||||
fullscreenBottomComponent: fullScreenToolbarComponent
|
||||
),
|
||||
availableSize: context.availableSize,
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
context.add(fullScreenOverlayComponent
|
||||
.position(.init(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2))
|
||||
.opacity(state.displayUI ? 1 : 0)
|
||||
)
|
||||
|
||||
context.add(video
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2 + dragOffset)
|
||||
))
|
||||
}
|
||||
|
||||
return context.availableSize
|
||||
@ -1446,11 +1550,21 @@ final class StreamTitleComponent: Component {
|
||||
|
||||
func update(component: StreamTitleComponent, availableSize: CGSize, transition: Transition) -> CGSize {
|
||||
let liveIndicatorWidth: CGFloat = 40
|
||||
self.titleLabel.text = component.text
|
||||
let currentText = self.titleLabel.text
|
||||
if currentText != component.text {
|
||||
if currentText?.isEmpty == false {
|
||||
UIView.transition(with: self.titleLabel, duration: 0.2) {
|
||||
self.titleLabel.text = component.text
|
||||
self.titleLabel.invalidateIntrinsicContentSize()
|
||||
}
|
||||
} else {
|
||||
self.titleLabel.text = component.text
|
||||
self.titleLabel.invalidateIntrinsicContentSize()
|
||||
}
|
||||
}
|
||||
self.titleLabel.font = Font.semibold(17.0)
|
||||
self.titleLabel.textColor = .white
|
||||
self.titleLabel.numberOfLines = 1
|
||||
self.titleLabel.invalidateIntrinsicContentSize()
|
||||
|
||||
let textSize = CGSize(width: min(availableSize.width - 4 - liveIndicatorWidth, self.titleLabel.intrinsicContentSize.width), height: availableSize.height)
|
||||
|
||||
@ -1483,7 +1597,13 @@ final class StreamTitleComponent: Component {
|
||||
let size = CGSize(width: textSize.width + sideInset * 2.0, height: textSize.height)
|
||||
let textFrame = CGRect(origin: CGPoint(x: sideInset, y: floor((size.height - textSize.height) / 2.0)), size: textSize)
|
||||
// self.textView.frame = textFrame
|
||||
self.updateTitleFadeLayer(textFrame: textFrame)
|
||||
if currentText?.isEmpty == false {
|
||||
UIView.transition(with: self.titleLabel, duration: 0.2) {
|
||||
self.updateTitleFadeLayer(textFrame: textFrame)
|
||||
}
|
||||
} else {
|
||||
self.updateTitleFadeLayer(textFrame: textFrame)
|
||||
}
|
||||
|
||||
liveIndicatorView.frame = CGRect(origin: CGPoint(x: textFrame.maxX + 6.0, y: /*floorToScreenPixels((size.height - textSize.height) / 2.0 - 2) + 1.0*/textFrame.midY - 22 / 2), size: .init(width: 40, height: 22))
|
||||
self.liveIndicatorView.toggle(isLive: component.isActive)
|
||||
@ -1563,7 +1683,11 @@ private final class NavigationBarComponent: CombinedComponent {
|
||||
let contentHeight: CGFloat = 44.0
|
||||
let size = CGSize(width: context.availableSize.width, height: context.component.topInset + contentHeight)
|
||||
|
||||
let background = background.update(component: Rectangle(color: UIColor(white: 0.0, alpha: context.component.backgroundVisible ? 0.5 : 0)), availableSize: CGSize(width: size.width, height: size.height), transition: context.transition)
|
||||
let background = background.update(
|
||||
component: Rectangle(color: UIColor(white: 0.0, alpha: 0.5/*context.component.backgroundVisible ? 0.5 : 0*/)),
|
||||
availableSize: CGSize(width: size.width, height: size.height),
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
let leftItem = context.component.leftItem.flatMap { leftItemComponent in
|
||||
return leftItem.update(
|
||||
@ -1600,6 +1724,8 @@ private final class NavigationBarComponent: CombinedComponent {
|
||||
|
||||
context.add(background
|
||||
.position(CGPoint(x: size.width / 2.0, y: size.height / 2.0))
|
||||
.opacity(context.component.backgroundVisible ? 1 : 0)
|
||||
.animation(key: "opacity")
|
||||
)
|
||||
|
||||
var centerLeftInset = sideInset
|
||||
@ -1660,7 +1786,7 @@ private final class OriginInfoComponent: CombinedComponent {
|
||||
component: ParticipantsComponent(
|
||||
count: context.component.participantsCount,
|
||||
showsSubtitle: true,
|
||||
fontSize: 24
|
||||
fontSize: 18.0
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height),
|
||||
transition: context.transition
|
||||
@ -1671,7 +1797,7 @@ private final class OriginInfoComponent: CombinedComponent {
|
||||
size.height = min(size.height, context.availableSize.height)
|
||||
|
||||
context.add(viewerCounter
|
||||
.position(CGPoint(x: size.width / 2.0, y: viewerCounter.size.height / 2.0))
|
||||
.position(CGPoint(x: size.width / 2.0, y: (context.availableSize.height - viewerCounter.size.height) / 2.0))
|
||||
)
|
||||
|
||||
return size
|
||||
|
||||
@ -120,7 +120,9 @@ final class MediaStreamVideoComponent: Component {
|
||||
private var noSignalTimer: Foundation.Timer?
|
||||
private var noSignalTimeout: Bool = false
|
||||
|
||||
private let maskGradientLayer = CAGradientLayer()
|
||||
private let videoBlurGradientMask = CAGradientLayer()
|
||||
private let videoBlurSolidMask = CALayer()
|
||||
|
||||
private var wasVisible = true
|
||||
private var borderShimmer = StandaloneShimmerEffect()
|
||||
private let shimmerBorderLayer = CALayer()
|
||||
@ -168,6 +170,8 @@ final class MediaStreamVideoComponent: Component {
|
||||
deinit {
|
||||
avatarDisposable?.dispose()
|
||||
frameInputDisposable?.dispose()
|
||||
self.x?.invalidate()
|
||||
self.x = nil
|
||||
}
|
||||
|
||||
public func matches(tag: Any) -> Bool {
|
||||
@ -316,10 +320,14 @@ final class MediaStreamVideoComponent: Component {
|
||||
UIView.animate(withDuration: 0.3) {
|
||||
videoBlurView.alpha = 1
|
||||
}
|
||||
self.maskGradientLayer.type = .radial
|
||||
self.maskGradientLayer.colors = [UIColor(rgb: 0x000000, alpha: 0.5).cgColor, UIColor(rgb: 0xffffff, alpha: 0.0).cgColor]
|
||||
self.maskGradientLayer.startPoint = CGPoint(x: 0.5, y: 0.5)
|
||||
self.maskGradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0)
|
||||
self.videoBlurGradientMask.type = .radial
|
||||
self.videoBlurGradientMask.colors = [UIColor(rgb: 0x000000, alpha: 0.5).cgColor, UIColor(rgb: 0xffffff, alpha: 0.0).cgColor]
|
||||
self.videoBlurGradientMask.startPoint = CGPoint(x: 0.5, y: 0.5)
|
||||
self.videoBlurGradientMask.endPoint = CGPoint(x: 1.0, y: 1.0)
|
||||
|
||||
self.videoBlurSolidMask.backgroundColor = UIColor.black.cgColor
|
||||
self.videoBlurGradientMask.addSublayer(videoBlurSolidMask)
|
||||
|
||||
}
|
||||
|
||||
if let videoView = self.videoRenderingContext.makeView(input: input, blur: false, forceSampleBufferDisplayLayer: true) {
|
||||
@ -431,6 +439,14 @@ final class MediaStreamVideoComponent: Component {
|
||||
|
||||
let videoSize: CGSize
|
||||
let videoCornerRadius: CGFloat = component.isFullscreen ? 0 : 10
|
||||
|
||||
let videoFrameUpdateTransition: Transition
|
||||
if self.wasFullscreen != component.isFullscreen {
|
||||
videoFrameUpdateTransition = transition
|
||||
} else {
|
||||
videoFrameUpdateTransition = transition.withAnimation(.none)
|
||||
}
|
||||
|
||||
if let videoView = self.videoView {
|
||||
if videoView.bounds.size.width > 0,
|
||||
videoView.alpha > 0,
|
||||
@ -482,15 +498,10 @@ final class MediaStreamVideoComponent: Component {
|
||||
videoView.clipsToBounds = true
|
||||
videoView.layer.cornerRadius = videoCornerRadius
|
||||
|
||||
let videoFrameUpdateTransition: Transition
|
||||
if self.wasFullscreen != component.isFullscreen {
|
||||
videoFrameUpdateTransition = transition
|
||||
} else {
|
||||
videoFrameUpdateTransition = transition.withAnimation(.none)
|
||||
}
|
||||
self.wasFullscreen = component.isFullscreen
|
||||
let newVideoFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - videoSize.width) / 2.0), y: floor((availableSize.height - videoSize.height) / 2.0)), size: videoSize)
|
||||
|
||||
videoFrameUpdateTransition.setFrame(view: videoView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - videoSize.width) / 2.0), y: floor((availableSize.height - videoSize.height) / 2.0)), size: videoSize), completion: nil)
|
||||
videoFrameUpdateTransition.setFrame(view: videoView, frame: newVideoFrame, completion: nil)
|
||||
|
||||
if let videoBlurView = self.videoBlurView {
|
||||
|
||||
@ -505,27 +516,51 @@ final class MediaStreamVideoComponent: Component {
|
||||
// videoBlurView.frame = videoView.frame.insetBy(dx: -69 * aspect, dy: -69)
|
||||
}
|
||||
|
||||
if !component.isFullscreen {
|
||||
videoBlurView.layer.mask = maskGradientLayer
|
||||
} else {
|
||||
videoBlurView.layer.mask = nil
|
||||
}
|
||||
videoBlurView.layer.mask = videoBlurGradientMask
|
||||
|
||||
self.maskGradientLayer.frame = videoBlurView.bounds
|
||||
if !component.isFullscreen {
|
||||
transition.setAlpha(layer: videoBlurSolidMask, alpha: 0)
|
||||
// if videoBlurView.layer.mask !== videoBlurGradientMask {
|
||||
// UIView.transition(with: videoBlurView, duration: transition.animation.isImmediate ? 0.0 : 0.3) { [self] in
|
||||
// videoBlurView.layer.mask = videoBlurGradientMask
|
||||
// }
|
||||
// }
|
||||
// videoBlurView.layer.mask = maskGradientLayer
|
||||
} else {
|
||||
transition.setAlpha(layer: videoBlurSolidMask, alpha: 1)
|
||||
// if videoBlurView.layer.mask != nil {
|
||||
// UIView.transition(with: videoBlurView, duration: transition.animation.isImmediate ? 0.0 : 0.3) {
|
||||
// videoBlurView.layer.mask = nil
|
||||
// }
|
||||
// }
|
||||
}
|
||||
//
|
||||
videoFrameUpdateTransition.setFrame(layer: self.videoBlurGradientMask, frame: videoBlurView.bounds)
|
||||
videoFrameUpdateTransition.setFrame(layer: self.videoBlurSolidMask, frame: self.videoBlurGradientMask.bounds)
|
||||
}
|
||||
} else {
|
||||
videoSize = CGSize(width: 16 / 9 * 100.0, height: 100.0).aspectFitted(.init(width: availableSize.width - videoInset * 2, height: availableSize.height))
|
||||
}
|
||||
loadingBlurView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - videoSize.width) / 2.0), y: floor((availableSize.height - videoSize.height) / 2.0)), size: videoSize)
|
||||
|
||||
loadingBlurView.layer.cornerRadius = videoCornerRadius
|
||||
let loadingBlurViewFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - videoSize.width) / 2.0), y: floor((availableSize.height - videoSize.height) / 2.0)), size: videoSize)
|
||||
videoFrameUpdateTransition.setFrame(view: loadingBlurView, frame: loadingBlurViewFrame)
|
||||
// loadingBlurView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - videoSize.width) / 2.0), y: floor((availableSize.height - videoSize.height) / 2.0)), size: videoSize)
|
||||
|
||||
placeholderView.frame = loadingBlurView.frame
|
||||
placeholderView.layer.cornerRadius = videoCornerRadius
|
||||
videoFrameUpdateTransition.setCornerRadius(layer: loadingBlurView.layer, cornerRadius: videoCornerRadius)
|
||||
// loadingBlurView.layer.cornerRadius = videoCornerRadius
|
||||
|
||||
videoFrameUpdateTransition.setFrame(view: placeholderView, frame: loadingBlurViewFrame)
|
||||
// placeholderView.frame = loadingBlurView.frame
|
||||
videoFrameUpdateTransition.setCornerRadius(layer: placeholderView.layer, cornerRadius: videoCornerRadius)
|
||||
// placeholderView.layer.cornerRadius = videoCornerRadius
|
||||
placeholderView.clipsToBounds = true
|
||||
placeholderView.subviews.forEach { $0.frame = placeholderView.bounds }
|
||||
placeholderView.subviews.forEach {
|
||||
videoFrameUpdateTransition.setFrame(view: $0, frame: placeholderView.bounds)
|
||||
// $0.frame = placeholderView.bounds
|
||||
}
|
||||
|
||||
shimmerBorderLayer.frame = loadingBlurView.bounds
|
||||
videoFrameUpdateTransition.setFrame(layer: shimmerBorderLayer, frame: loadingBlurView.bounds)
|
||||
// shimmerBorderLayer.frame = loadingBlurView.bounds
|
||||
|
||||
let borderMask = CAShapeLayer()
|
||||
borderMask.path = CGPath(roundedRect: .init(x: 0, y: 0, width: shimmerBorderLayer.bounds.width, height: shimmerBorderLayer.bounds.height), cornerWidth: videoCornerRadius, cornerHeight: videoCornerRadius, transform: nil)
|
||||
@ -607,26 +642,58 @@ final class MediaStreamVideoComponent: Component {
|
||||
|
||||
func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
|
||||
if let videoView = self.videoView, let presentation = videoView.snapshotView(afterScreenUpdates: false) {
|
||||
self.addSubview(presentation)
|
||||
presentation.frame = videoView.frame
|
||||
let presentationParent = self.window ?? self
|
||||
presentationParent.addSubview(presentation)
|
||||
presentation.frame = presentationParent.convert(videoView.frame, from: self)
|
||||
|
||||
if let callId = self.component?.call.peerId.id.description {
|
||||
lastFrame[callId] = presentation
|
||||
}
|
||||
|
||||
videoView.alpha = 0
|
||||
|
||||
UIView.animate(withDuration: 0.07, delay: 0.07, animations: {
|
||||
presentation.alpha = 0
|
||||
}, completion: { _ in
|
||||
presentation.removeFromSuperview()
|
||||
})
|
||||
lastPresentation?.removeFromSuperview()
|
||||
lastPresentation = presentation
|
||||
// UIView.animate(withDuration: 0.04, delay: 0.04, animations: {
|
||||
// presentation.alpha = 0
|
||||
// }, completion: { _ in
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
|
||||
self.lastPresentation?.removeFromSuperview()
|
||||
self.lastPresentation = nil
|
||||
self.x?.invalidate()
|
||||
self.x = nil
|
||||
}
|
||||
// })
|
||||
}
|
||||
UIView.animate(withDuration: 0.1) { [self] in
|
||||
videoBlurView?.alpha = 0
|
||||
}
|
||||
// UIApplication.shared.windows.first?.windowLevel == .normal
|
||||
// TODO: make safe
|
||||
UIApplication.shared.windows.first?/*(where: { $0.layer !== (UIApplication.shared.connectedScenes.first as? UIWindowScene)?.keyWindow?.layer })?*/.layer.cornerRadius = 10// (where: { !($0 is NativeWindow)*/ })
|
||||
UIApplication.shared.windows.first?.layer.masksToBounds = true
|
||||
|
||||
// UIApplication.shared.windows.first?.subviews[0].subviews[0].subviews[1].subviews[0].subviews[0].backgroundColor = .red
|
||||
// UIApplication.shared.windows.first?.subviews[0].subviews[0].subviews[1].subviews[0].subviews[0].setNeedsDisplay()
|
||||
// UIApplication.shared.windows.first?.subviews[0].subviews[0].subviews[1].backgroundColor = .red
|
||||
// UIApplication.shared.windows.first?.subviews[0].subviews[0].subviews[1].setNeedsDisplay()
|
||||
|
||||
self.x?.invalidate()
|
||||
let x = CADisplayLink(target: self, selector: #selector(observePiPWindow))
|
||||
x.add(to: .main, forMode: .default)
|
||||
self.x = x
|
||||
}
|
||||
|
||||
var lastPresentation: UIView?
|
||||
var x: CADisplayLink?
|
||||
|
||||
@objc func observePiPWindow() {
|
||||
let pipViewDidBecomeVisible = (UIApplication.shared.windows.first?.layer.animationKeys()?.count ?? 0) > 0
|
||||
if pipViewDidBecomeVisible {
|
||||
lastPresentation?.removeFromSuperview()
|
||||
lastPresentation = nil
|
||||
self.x?.invalidate()
|
||||
self.x = nil
|
||||
}
|
||||
}
|
||||
|
||||
public func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) {
|
||||
|
||||
@ -20,6 +20,10 @@ final class StreamSheetComponent: CombinedComponent {
|
||||
let deviceCornerRadius: CGFloat
|
||||
let videoHeight: CGFloat
|
||||
|
||||
let isFullscreen: Bool
|
||||
let fullscreenTopComponent: AnyComponent<Empty>
|
||||
let fullscreenBottomComponent: AnyComponent<Empty>
|
||||
|
||||
init(
|
||||
topComponent: AnyComponent<Empty>,
|
||||
bottomButtonsRow: AnyComponent<Empty>,
|
||||
@ -30,10 +34,13 @@ final class StreamSheetComponent: CombinedComponent {
|
||||
participantsCount: Int,
|
||||
isFullyExtended: Bool,
|
||||
deviceCornerRadius: CGFloat,
|
||||
videoHeight: CGFloat
|
||||
videoHeight: CGFloat,
|
||||
isFullscreen: Bool,
|
||||
fullscreenTopComponent: AnyComponent<Empty>,
|
||||
fullscreenBottomComponent: AnyComponent<Empty>
|
||||
) {
|
||||
self.topComponent = topComponent
|
||||
self.bottomButtonsRow = bottomButtonsRow
|
||||
self.topComponent = nil // topComponent
|
||||
self.bottomButtonsRow = nil // bottomButtonsRow
|
||||
self.topOffset = topOffset
|
||||
self.sheetHeight = sheetHeight
|
||||
self.backgroundColor = backgroundColor
|
||||
@ -42,6 +49,10 @@ final class StreamSheetComponent: CombinedComponent {
|
||||
self.isFullyExtended = isFullyExtended
|
||||
self.deviceCornerRadius = deviceCornerRadius
|
||||
self.videoHeight = videoHeight
|
||||
|
||||
self.isFullscreen = isFullscreen
|
||||
self.fullscreenTopComponent = fullscreenTopComponent
|
||||
self.fullscreenBottomComponent = fullscreenBottomComponent
|
||||
}
|
||||
|
||||
static func ==(lhs: StreamSheetComponent, rhs: StreamSheetComponent) -> Bool {
|
||||
@ -75,6 +86,19 @@ final class StreamSheetComponent: CombinedComponent {
|
||||
if lhs.videoHeight != rhs.videoHeight {
|
||||
return false
|
||||
}
|
||||
|
||||
if lhs.isFullscreen != rhs.isFullscreen {
|
||||
return false
|
||||
}
|
||||
|
||||
if lhs.fullscreenTopComponent != rhs.fullscreenTopComponent {
|
||||
return false
|
||||
}
|
||||
|
||||
if lhs.fullscreenBottomComponent != rhs.fullscreenBottomComponent {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@ -173,7 +197,7 @@ final class StreamSheetComponent: CombinedComponent {
|
||||
)
|
||||
}
|
||||
// TODO: replace
|
||||
let isFullscreen = context.component.participantsCount == -1
|
||||
let isFullscreen = context.component.isFullscreen // context.component.participantsCount == -1
|
||||
|
||||
context.add(background
|
||||
.position(CGPoint(x: size.width / 2.0, y: topOffset + context.component.sheetHeight / 2))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user