mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
[WIP] Video chats
This commit is contained in:
parent
3aceee1696
commit
ccedf9fc06
@ -471,7 +471,12 @@ public struct ComponentTransition {
|
||||
layer.removeAnimation(forKey: "opacity")
|
||||
completion?(true)
|
||||
case .curve:
|
||||
let previousAlpha = layer.presentation()?.opacity ?? layer.opacity
|
||||
let previousAlpha: Float
|
||||
if layer.animation(forKey: "opacity") != nil {
|
||||
previousAlpha = layer.presentation()?.opacity ?? layer.opacity
|
||||
} else {
|
||||
previousAlpha = layer.opacity
|
||||
}
|
||||
layer.opacity = Float(alpha)
|
||||
self.animateAlpha(layer: layer, from: CGFloat(previousAlpha), to: alpha, delay: delay, completion: completion)
|
||||
}
|
||||
|
@ -10,16 +10,22 @@ import BackButtonComponent
|
||||
final class VideoChatExpandedControlsComponent: Component {
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let isPinned: Bool
|
||||
let backAction: () -> Void
|
||||
let pinAction: () -> Void
|
||||
|
||||
init(
|
||||
theme: PresentationTheme,
|
||||
strings: PresentationStrings,
|
||||
backAction: @escaping () -> Void
|
||||
isPinned: Bool,
|
||||
backAction: @escaping () -> Void,
|
||||
pinAction: @escaping () -> Void
|
||||
) {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.isPinned = isPinned
|
||||
self.backAction = backAction
|
||||
self.pinAction = pinAction
|
||||
}
|
||||
|
||||
static func ==(lhs: VideoChatExpandedControlsComponent, rhs: VideoChatExpandedControlsComponent) -> Bool {
|
||||
@ -29,11 +35,15 @@ final class VideoChatExpandedControlsComponent: Component {
|
||||
if lhs.strings !== rhs.strings {
|
||||
return false
|
||||
}
|
||||
if lhs.isPinned != rhs.isPinned {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private let backButton = ComponentView<Empty>()
|
||||
private let pinStatus = ComponentView<Empty>()
|
||||
|
||||
private var component: VideoChatExpandedControlsComponent?
|
||||
private var isUpdating: Bool = false
|
||||
@ -52,6 +62,9 @@ final class VideoChatExpandedControlsComponent: Component {
|
||||
if let backButtonView = self.backButton.view, let result = backButtonView.hitTest(self.convert(point, to: backButtonView), with: event) {
|
||||
return result
|
||||
}
|
||||
if let pinStatusView = self.pinStatus.view, let result = pinStatusView.hitTest(self.convert(point, to: pinStatusView), with: event) {
|
||||
return result
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -86,6 +99,30 @@ final class VideoChatExpandedControlsComponent: Component {
|
||||
transition.setFrame(view: backButtonView, frame: backButtonFrame)
|
||||
}
|
||||
|
||||
let pinStatusSize = self.pinStatus.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(VideoChatPinStatusComponent(
|
||||
theme: component.theme,
|
||||
strings: component.strings,
|
||||
isPinned: component.isPinned,
|
||||
action: { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.pinAction()
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: 100.0)
|
||||
)
|
||||
let pinStatusFrame = CGRect(origin: CGPoint(x: availableSize.width - 0.0 - pinStatusSize.width, y: 0.0), size: pinStatusSize)
|
||||
if let pinStatusView = self.pinStatus.view {
|
||||
if pinStatusView.superview == nil {
|
||||
self.addSubview(pinStatusView)
|
||||
}
|
||||
transition.setFrame(view: pinStatusView, frame: pinStatusFrame)
|
||||
}
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import CallScreen
|
||||
import MetalEngine
|
||||
import SwiftSignalKit
|
||||
import AccountContext
|
||||
import RadialStatusNode
|
||||
|
||||
private final class BlobView: UIView {
|
||||
let blobsLayer: CallBlobsLayer
|
||||
@ -209,6 +210,8 @@ final class VideoChatMicButtonComponent: Component {
|
||||
|
||||
final class View: HighlightTrackingButton {
|
||||
private let background: UIImageView
|
||||
private var disappearingBackgrounds: [UIImageView] = []
|
||||
private var progressIndicator: RadialStatusNode?
|
||||
private let title = ComponentView<Empty>()
|
||||
private let icon: VoiceChatActionButtonIconNode
|
||||
|
||||
@ -330,8 +333,38 @@ final class VideoChatMicButtonComponent: Component {
|
||||
self.addSubview(self.background)
|
||||
self.background.frame = CGRect(origin: CGPoint(), size: CGSize(width: 116.0, height: 116.0))
|
||||
}
|
||||
transition.setPosition(view: self.background, position: CGRect(origin: CGPoint(), size: size).center)
|
||||
transition.setScale(view: self.background, scale: size.width / 116.0)
|
||||
|
||||
if case .connecting = component.content {
|
||||
let progressIndicator: RadialStatusNode
|
||||
if let current = self.progressIndicator {
|
||||
progressIndicator = current
|
||||
} else {
|
||||
progressIndicator = RadialStatusNode(backgroundNodeColor: .clear)
|
||||
self.progressIndicator = progressIndicator
|
||||
}
|
||||
progressIndicator.transitionToState(.progress(color: UIColor(rgb: 0x0080FF), lineWidth: 3.0, value: nil, cancelEnabled: false, animateRotation: true))
|
||||
|
||||
let progressIndicatorView = progressIndicator.view
|
||||
if progressIndicatorView.superview == nil {
|
||||
self.addSubview(progressIndicatorView)
|
||||
progressIndicatorView.center = CGRect(origin: CGPoint(), size: size).center
|
||||
progressIndicatorView.bounds = CGRect(origin: CGPoint(), size: CGSize(width: 116.0, height: 116.0))
|
||||
progressIndicatorView.layer.transform = CATransform3DMakeScale(size.width / 116.0, size.width / 116.0, 1.0)
|
||||
} else {
|
||||
transition.setPosition(view: progressIndicatorView, position: CGRect(origin: CGPoint(), size: size).center)
|
||||
transition.setScale(view: progressIndicatorView, scale: size.width / 116.0)
|
||||
}
|
||||
} else if let progressIndicator = self.progressIndicator {
|
||||
self.progressIndicator = nil
|
||||
if !transition.animation.isImmediate {
|
||||
let progressIndicatorView = progressIndicator.view
|
||||
progressIndicatorView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, removeOnCompletion: false, completion: { [weak progressIndicatorView] _ in
|
||||
progressIndicatorView?.removeFromSuperview()
|
||||
})
|
||||
} else {
|
||||
progressIndicator.view.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
if previousComponent?.content != component.content {
|
||||
let backgroundContentsTransition: ComponentTransition
|
||||
@ -340,7 +373,7 @@ final class VideoChatMicButtonComponent: Component {
|
||||
} else {
|
||||
backgroundContentsTransition = .immediate
|
||||
}
|
||||
let backgroundImage = generateImage(CGSize(width: 116.0, height: 116.0), rotatedContext: { size, context in
|
||||
let backgroundImage = generateImage(CGSize(width: 200.0, height: 200.0), rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.addEllipse(in: CGRect(origin: CGPoint(), size: size))
|
||||
context.clip()
|
||||
@ -366,11 +399,41 @@ final class VideoChatMicButtonComponent: Component {
|
||||
}
|
||||
})!
|
||||
if let previousImage = self.background.image {
|
||||
let previousBackground = UIImageView()
|
||||
previousBackground.center = self.background.center
|
||||
previousBackground.bounds = self.background.bounds
|
||||
previousBackground.layer.transform = self.background.layer.transform
|
||||
previousBackground.image = previousImage
|
||||
self.insertSubview(previousBackground, aboveSubview: self.background)
|
||||
self.disappearingBackgrounds.append(previousBackground)
|
||||
|
||||
self.background.image = backgroundImage
|
||||
backgroundContentsTransition.animateContentsImage(layer: self.background.layer, from: previousImage.cgImage!, to: backgroundImage.cgImage!, duration: 0.2, curve: .easeInOut)
|
||||
backgroundContentsTransition.setAlpha(view: previousBackground, alpha: 0.0, completion: { [weak self, weak previousBackground] _ in
|
||||
guard let self, let previousBackground else {
|
||||
return
|
||||
}
|
||||
previousBackground.removeFromSuperview()
|
||||
self.disappearingBackgrounds.removeAll(where: { $0 === previousBackground })
|
||||
})
|
||||
} else {
|
||||
self.background.image = backgroundImage
|
||||
}
|
||||
|
||||
if !transition.animation.isImmediate, let previousComponent, case .connecting = previousComponent.content {
|
||||
self.layer.animateSublayerScale(from: 1.0, to: 1.07, duration: 0.12, removeOnCompletion: false, completion: { [weak self] completed in
|
||||
if let self, completed {
|
||||
self.layer.removeAnimation(forKey: "sublayerTransform.scale")
|
||||
self.layer.animateSublayerScale(from: 1.07, to: 1.0, duration: 0.12, removeOnCompletion: true)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
transition.setPosition(view: self.background, position: CGRect(origin: CGPoint(), size: size).center)
|
||||
transition.setScale(view: self.background, scale: size.width / 116.0)
|
||||
for disappearingBackground in self.disappearingBackgrounds {
|
||||
transition.setPosition(view: disappearingBackground, position: CGRect(origin: CGPoint(), size: size).center)
|
||||
transition.setScale(view: disappearingBackground, scale: size.width / 116.0)
|
||||
}
|
||||
|
||||
let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) * 0.5), y: size.height + 16.0), size: titleSize)
|
||||
@ -434,6 +497,8 @@ final class VideoChatMicButtonComponent: Component {
|
||||
|
||||
blobTintTransition.setTintColor(layer: blobView.blobsLayer, color: component.content == .muted ? UIColor(rgb: 0x0086FF) : UIColor(rgb: 0x33C758))
|
||||
|
||||
switch component.content {
|
||||
case .unmuted:
|
||||
if self.audioLevelDisposable == nil {
|
||||
self.audioLevelDisposable = (component.call.myAudioLevel
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] value in
|
||||
@ -443,6 +508,13 @@ final class VideoChatMicButtonComponent: Component {
|
||||
blobView.updateLevel(CGFloat(value), immediately: false)
|
||||
})
|
||||
}
|
||||
case .connecting, .muted:
|
||||
if let audioLevelDisposable = self.audioLevelDisposable {
|
||||
self.audioLevelDisposable = nil
|
||||
audioLevelDisposable.dispose()
|
||||
blobView.updateLevel(0.0, immediately: false)
|
||||
}
|
||||
}
|
||||
|
||||
var glowFrame = CGRect(origin: CGPoint(), size: availableSize)
|
||||
if component.isCollapsed {
|
||||
|
@ -41,6 +41,7 @@ final class VideoChatParticipantVideoComponent: Component {
|
||||
let isSpeaking: Bool
|
||||
let isExpanded: Bool
|
||||
let bottomInset: CGFloat
|
||||
weak var rootVideoLoadingEffectView: VideoChatVideoLoadingEffectView?
|
||||
let action: (() -> Void)?
|
||||
|
||||
init(
|
||||
@ -50,6 +51,7 @@ final class VideoChatParticipantVideoComponent: Component {
|
||||
isSpeaking: Bool,
|
||||
isExpanded: Bool,
|
||||
bottomInset: CGFloat,
|
||||
rootVideoLoadingEffectView: VideoChatVideoLoadingEffectView?,
|
||||
action: (() -> Void)?
|
||||
) {
|
||||
self.call = call
|
||||
@ -58,6 +60,7 @@ final class VideoChatParticipantVideoComponent: Component {
|
||||
self.isSpeaking = isSpeaking
|
||||
self.isExpanded = isExpanded
|
||||
self.bottomInset = bottomInset
|
||||
self.rootVideoLoadingEffectView = rootVideoLoadingEffectView
|
||||
self.action = action
|
||||
}
|
||||
|
||||
@ -113,6 +116,8 @@ final class VideoChatParticipantVideoComponent: Component {
|
||||
|
||||
private var activityBorderView: UIImageView?
|
||||
|
||||
private var loadingEffectView: PortalView?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
@ -429,6 +434,18 @@ final class VideoChatParticipantVideoComponent: Component {
|
||||
self.videoSpec = nil
|
||||
}
|
||||
|
||||
if self.loadingEffectView == nil, let rootVideoLoadingEffectView = component.rootVideoLoadingEffectView {
|
||||
if let loadingEffectView = PortalView(matchPosition: true) {
|
||||
self.loadingEffectView = loadingEffectView
|
||||
self.addSubview(loadingEffectView.view)
|
||||
rootVideoLoadingEffectView.portalSource.addPortal(view: loadingEffectView)
|
||||
loadingEffectView.view.frame = CGRect(origin: CGPoint(), size: availableSize)
|
||||
}
|
||||
}
|
||||
if let loadingEffectView = self.loadingEffectView {
|
||||
transition.setFrame(view: loadingEffectView.view, frame: CGRect(origin: CGPoint(), size: availableSize))
|
||||
}
|
||||
|
||||
if component.isSpeaking && !component.isExpanded {
|
||||
let activityBorderView: UIImageView
|
||||
if let current = self.activityBorderView {
|
||||
|
@ -12,6 +12,23 @@ import TelegramPresentationData
|
||||
import PeerListItemComponent
|
||||
|
||||
final class VideoChatParticipantsComponent: Component {
|
||||
enum LayoutType: Equatable {
|
||||
struct Horizontal: Equatable {
|
||||
var rightColumnWidth: CGFloat
|
||||
var columnSpacing: CGFloat
|
||||
var isCentered: Bool
|
||||
|
||||
init(rightColumnWidth: CGFloat, columnSpacing: CGFloat, isCentered: Bool) {
|
||||
self.rightColumnWidth = rightColumnWidth
|
||||
self.columnSpacing = columnSpacing
|
||||
self.isCentered = isCentered
|
||||
}
|
||||
}
|
||||
|
||||
case vertical
|
||||
case horizontal(Horizontal)
|
||||
}
|
||||
|
||||
final class Participants: Equatable {
|
||||
let myPeerId: EnginePeer.Id
|
||||
let participants: [GroupCallParticipantsContext.Participant]
|
||||
@ -84,6 +101,7 @@ final class VideoChatParticipantsComponent: Component {
|
||||
let expandedVideoState: ExpandedVideoState?
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let layoutType: LayoutType
|
||||
let collapsedContainerInsets: UIEdgeInsets
|
||||
let expandedContainerInsets: UIEdgeInsets
|
||||
let sideInset: CGFloat
|
||||
@ -97,6 +115,7 @@ final class VideoChatParticipantsComponent: Component {
|
||||
expandedVideoState: ExpandedVideoState?,
|
||||
theme: PresentationTheme,
|
||||
strings: PresentationStrings,
|
||||
layoutType: LayoutType,
|
||||
collapsedContainerInsets: UIEdgeInsets,
|
||||
expandedContainerInsets: UIEdgeInsets,
|
||||
sideInset: CGFloat,
|
||||
@ -109,6 +128,7 @@ final class VideoChatParticipantsComponent: Component {
|
||||
self.expandedVideoState = expandedVideoState
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.layoutType = layoutType
|
||||
self.collapsedContainerInsets = collapsedContainerInsets
|
||||
self.expandedContainerInsets = expandedContainerInsets
|
||||
self.sideInset = sideInset
|
||||
@ -132,6 +152,9 @@ final class VideoChatParticipantsComponent: Component {
|
||||
if lhs.strings !== rhs.strings {
|
||||
return false
|
||||
}
|
||||
if lhs.layoutType != rhs.layoutType {
|
||||
return false
|
||||
}
|
||||
if lhs.collapsedContainerInsets != rhs.collapsedContainerInsets {
|
||||
return false
|
||||
}
|
||||
@ -214,15 +237,22 @@ final class VideoChatParticipantsComponent: Component {
|
||||
|
||||
struct ExpandedGrid {
|
||||
let containerSize: CGSize
|
||||
let layoutType: LayoutType
|
||||
let containerInsets: UIEdgeInsets
|
||||
|
||||
init(containerSize: CGSize, containerInsets: UIEdgeInsets) {
|
||||
init(containerSize: CGSize, layoutType: LayoutType, containerInsets: UIEdgeInsets) {
|
||||
self.containerSize = containerSize
|
||||
self.layoutType = layoutType
|
||||
self.containerInsets = containerInsets
|
||||
}
|
||||
|
||||
func itemContainerFrame() -> CGRect {
|
||||
switch self.layoutType {
|
||||
case .vertical:
|
||||
return CGRect(origin: CGPoint(x: self.containerInsets.left, y: self.containerInsets.top), size: CGSize(width: self.containerSize.width - self.containerInsets.left - self.containerInsets.right, height: self.containerSize.height - self.containerInsets.top - containerInsets.bottom))
|
||||
case .horizontal:
|
||||
return CGRect(origin: CGPoint(x: self.containerInsets.left, y: self.containerInsets.top), size: CGSize(width: self.containerSize.width - self.containerInsets.left - self.containerInsets.right, height: self.containerSize.height - self.containerInsets.top))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -276,6 +306,7 @@ final class VideoChatParticipantsComponent: Component {
|
||||
}
|
||||
|
||||
let containerSize: CGSize
|
||||
let layoutType: LayoutType
|
||||
let collapsedContainerInsets: UIEdgeInsets
|
||||
let sideInset: CGFloat
|
||||
let grid: Grid
|
||||
@ -284,36 +315,93 @@ final class VideoChatParticipantsComponent: Component {
|
||||
let spacing: CGFloat
|
||||
let gridOffsetY: CGFloat
|
||||
let listOffsetY: CGFloat
|
||||
let listFrame: CGRect
|
||||
let separateVideoGridFrame: CGRect
|
||||
let scrollClippingFrame: CGRect
|
||||
let separateVideoScrollClippingFrame: CGRect
|
||||
|
||||
init(containerSize: CGSize, sideInset: CGFloat, collapsedContainerInsets: UIEdgeInsets, expandedContainerInsets: UIEdgeInsets, gridItemCount: Int, listItemCount: Int, listItemHeight: CGFloat, listTrailingItemHeight: CGFloat) {
|
||||
init(containerSize: CGSize, layoutType: LayoutType, sideInset: CGFloat, collapsedContainerInsets: UIEdgeInsets, expandedContainerInsets: UIEdgeInsets, gridItemCount: Int, listItemCount: Int, listItemHeight: CGFloat, listTrailingItemHeight: CGFloat) {
|
||||
self.containerSize = containerSize
|
||||
self.layoutType = layoutType
|
||||
self.collapsedContainerInsets = collapsedContainerInsets
|
||||
self.sideInset = sideInset
|
||||
|
||||
self.grid = Grid(containerSize: CGSize(width: containerSize.width - sideInset * 2.0, height: containerSize.height), sideInset: 0.0, itemCount: gridItemCount)
|
||||
self.expandedGrid = ExpandedGrid(containerSize: containerSize, containerInsets: expandedContainerInsets)
|
||||
self.list = List(containerSize: CGSize(width: containerSize.width - sideInset * 2.0, height: containerSize.height), sideInset: 0.0, itemCount: listItemCount, itemHeight: listItemHeight, trailingItemHeight: listTrailingItemHeight)
|
||||
let gridWidth: CGFloat
|
||||
let listWidth: CGFloat
|
||||
switch layoutType {
|
||||
case .vertical:
|
||||
listWidth = containerSize.width - sideInset * 2.0
|
||||
gridWidth = listWidth
|
||||
case let .horizontal(horizontal):
|
||||
listWidth = horizontal.rightColumnWidth
|
||||
gridWidth = max(10.0, containerSize.width - sideInset * 2.0 - horizontal.rightColumnWidth - horizontal.columnSpacing)
|
||||
}
|
||||
|
||||
self.grid = Grid(containerSize: CGSize(width: gridWidth, height: containerSize.height), sideInset: 0.0, itemCount: gridItemCount)
|
||||
self.expandedGrid = ExpandedGrid(containerSize: containerSize, layoutType: layoutType, containerInsets: expandedContainerInsets)
|
||||
self.list = List(containerSize: CGSize(width: listWidth, height: containerSize.height), sideInset: 0.0, itemCount: listItemCount, itemHeight: listItemHeight, trailingItemHeight: listTrailingItemHeight)
|
||||
self.spacing = 4.0
|
||||
|
||||
self.gridOffsetY = collapsedContainerInsets.top
|
||||
|
||||
var listOffsetY: CGFloat = self.gridOffsetY
|
||||
if case .vertical = layoutType {
|
||||
if self.grid.itemCount != 0 {
|
||||
listOffsetY += self.grid.contentHeight()
|
||||
listOffsetY += self.spacing
|
||||
}
|
||||
}
|
||||
self.listOffsetY = listOffsetY
|
||||
|
||||
switch layoutType {
|
||||
case .vertical:
|
||||
self.scrollClippingFrame = CGRect(origin: CGPoint(x: self.sideInset, y: collapsedContainerInsets.top), size: CGSize(width: containerSize.width - self.sideInset * 2.0, height: containerSize.height - collapsedContainerInsets.top - collapsedContainerInsets.bottom))
|
||||
self.listFrame = CGRect(origin: CGPoint(), size: containerSize)
|
||||
|
||||
self.separateVideoGridFrame = CGRect(origin: CGPoint(x: -containerSize.width, y: 0.0), size: containerSize)
|
||||
self.separateVideoScrollClippingFrame = CGRect(origin: CGPoint(x: self.separateVideoGridFrame.minX, y: collapsedContainerInsets.top), size: CGSize(width: self.separateVideoGridFrame.width, height: containerSize.height - collapsedContainerInsets.top))
|
||||
case let .horizontal(horizontal):
|
||||
if horizontal.isCentered {
|
||||
self.listFrame = CGRect(origin: CGPoint(x: floor((containerSize.width - horizontal.rightColumnWidth) * 0.5), y: 0.0), size: CGSize(width: horizontal.rightColumnWidth, height: containerSize.height))
|
||||
} else {
|
||||
self.listFrame = CGRect(origin: CGPoint(x: containerSize.width - self.sideInset - horizontal.rightColumnWidth, y: 0.0), size: CGSize(width: horizontal.rightColumnWidth, height: containerSize.height))
|
||||
}
|
||||
self.scrollClippingFrame = CGRect(origin: CGPoint(x: self.listFrame.minX, y: collapsedContainerInsets.top), size: CGSize(width: self.listFrame.width, height: containerSize.height - collapsedContainerInsets.top))
|
||||
|
||||
self.separateVideoGridFrame = CGRect(origin: CGPoint(x: min(self.sideInset, self.scrollClippingFrame.minX - horizontal.columnSpacing - gridWidth), y: 0.0), size: CGSize(width: gridWidth, height: containerSize.height))
|
||||
self.separateVideoScrollClippingFrame = CGRect(origin: CGPoint(x: self.separateVideoGridFrame.minX, y: collapsedContainerInsets.top), size: CGSize(width: self.separateVideoGridFrame.width, height: containerSize.height - collapsedContainerInsets.top))
|
||||
}
|
||||
}
|
||||
|
||||
func contentHeight() -> CGFloat {
|
||||
var result: CGFloat = self.gridOffsetY
|
||||
switch self.layoutType {
|
||||
case .vertical:
|
||||
if self.grid.itemCount != 0 {
|
||||
result += self.grid.contentHeight()
|
||||
result += self.spacing
|
||||
}
|
||||
case .horizontal:
|
||||
break
|
||||
}
|
||||
result += self.list.contentHeight()
|
||||
result += self.collapsedContainerInsets.bottom
|
||||
result += 32.0
|
||||
result += 24.0
|
||||
return result
|
||||
}
|
||||
|
||||
func separateVideoGridContentHeight() -> CGFloat {
|
||||
var result: CGFloat = self.gridOffsetY
|
||||
switch self.layoutType {
|
||||
case .vertical:
|
||||
break
|
||||
case .horizontal:
|
||||
if self.grid.itemCount != 0 {
|
||||
result += self.grid.contentHeight()
|
||||
}
|
||||
}
|
||||
result += self.collapsedContainerInsets.bottom
|
||||
result += 24.0
|
||||
return result
|
||||
}
|
||||
|
||||
@ -326,7 +414,12 @@ final class VideoChatParticipantsComponent: Component {
|
||||
}
|
||||
|
||||
func gridItemContainerFrame() -> CGRect {
|
||||
switch self.layoutType {
|
||||
case .vertical:
|
||||
return CGRect(origin: CGPoint(x: self.sideInset, y: self.gridOffsetY), size: CGSize(width: self.containerSize.width - self.sideInset * 2.0, height: self.grid.contentHeight()))
|
||||
case .horizontal:
|
||||
return CGRect(origin: CGPoint(x: 0.0, y: self.gridOffsetY), size: CGSize(width: self.separateVideoGridFrame.width, height: self.grid.contentHeight()))
|
||||
}
|
||||
}
|
||||
|
||||
func visibleListItemRange(for rect: CGRect) -> (minIndex: Int, maxIndex: Int) {
|
||||
@ -338,7 +431,12 @@ final class VideoChatParticipantsComponent: Component {
|
||||
}
|
||||
|
||||
func listItemContainerFrame() -> CGRect {
|
||||
switch self.layoutType {
|
||||
case .vertical:
|
||||
return CGRect(origin: CGPoint(x: self.sideInset, y: self.listOffsetY), size: CGSize(width: self.containerSize.width - self.sideInset * 2.0, height: self.list.contentHeight()))
|
||||
case .horizontal:
|
||||
return CGRect(origin: CGPoint(x: 0.0, y: self.listOffsetY), size: CGSize(width: self.listFrame.width, height: self.list.contentHeight()))
|
||||
}
|
||||
}
|
||||
|
||||
func listTrailingItemFrame() -> CGRect {
|
||||
@ -389,9 +487,13 @@ final class VideoChatParticipantsComponent: Component {
|
||||
}
|
||||
|
||||
final class View: UIView, UIScrollViewDelegate {
|
||||
private var rootVideoLoadingEffectView: VideoChatVideoLoadingEffectView?
|
||||
|
||||
private let scrollViewClippingContainer: SolidRoundedCornersContainer
|
||||
private let scrollView: ScrollView
|
||||
private let scrollViewClippingShadowView: UIImageView
|
||||
|
||||
private let separateVideoScrollViewClippingContainer: SolidRoundedCornersContainer
|
||||
private let separateVideoScrollView: ScrollView
|
||||
|
||||
private var component: VideoChatParticipantsComponent?
|
||||
private var isUpdating: Bool = false
|
||||
@ -422,10 +524,11 @@ final class VideoChatParticipantsComponent: Component {
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.scrollViewClippingContainer = SolidRoundedCornersContainer()
|
||||
self.scrollViewClippingShadowView = UIImageView()
|
||||
|
||||
self.scrollView = ScrollView()
|
||||
|
||||
self.separateVideoScrollViewClippingContainer = SolidRoundedCornersContainer()
|
||||
self.separateVideoScrollView = ScrollView()
|
||||
|
||||
self.gridItemViewContainer = UIView()
|
||||
self.gridItemViewContainer.layer.anchorPoint = CGPoint(x: 0.5, y: 0.0)
|
||||
|
||||
@ -453,13 +556,30 @@ final class VideoChatParticipantsComponent: Component {
|
||||
self.scrollView.delegate = self
|
||||
self.scrollView.clipsToBounds = true
|
||||
|
||||
self.separateVideoScrollView.delaysContentTouches = false
|
||||
self.separateVideoScrollView.canCancelContentTouches = true
|
||||
self.separateVideoScrollView.clipsToBounds = false
|
||||
self.separateVideoScrollView.contentInsetAdjustmentBehavior = .never
|
||||
if #available(iOS 13.0, *) {
|
||||
self.separateVideoScrollView.automaticallyAdjustsScrollIndicatorInsets = false
|
||||
}
|
||||
self.separateVideoScrollView.showsVerticalScrollIndicator = false
|
||||
self.separateVideoScrollView.showsHorizontalScrollIndicator = false
|
||||
self.separateVideoScrollView.alwaysBounceHorizontal = false
|
||||
self.separateVideoScrollView.alwaysBounceVertical = false
|
||||
self.separateVideoScrollView.scrollsToTop = false
|
||||
self.separateVideoScrollView.delegate = self
|
||||
self.separateVideoScrollView.clipsToBounds = true
|
||||
|
||||
self.scrollViewClippingContainer.addSubview(self.scrollView)
|
||||
self.addSubview(self.scrollViewClippingContainer)
|
||||
self.addSubview(self.scrollViewClippingContainer.cornersView)
|
||||
self.addSubview(self.scrollViewClippingShadowView)
|
||||
|
||||
self.separateVideoScrollViewClippingContainer.addSubview(self.separateVideoScrollView)
|
||||
self.addSubview(self.separateVideoScrollViewClippingContainer)
|
||||
self.addSubview(self.separateVideoScrollViewClippingContainer.cornersView)
|
||||
|
||||
self.scrollView.addSubview(self.listItemViewContainer)
|
||||
self.scrollView.addSubview(self.gridItemViewContainer)
|
||||
self.addSubview(self.expandedGridItemContainer)
|
||||
}
|
||||
|
||||
@ -481,6 +601,8 @@ final class VideoChatParticipantsComponent: Component {
|
||||
} else {
|
||||
if let result = self.scrollViewClippingContainer.hitTest(self.convert(point, to: self.scrollViewClippingContainer), with: event) {
|
||||
return result
|
||||
} else if let result = self.separateVideoScrollViewClippingContainer.hitTest(self.convert(point, to: self.separateVideoScrollViewClippingContainer), with: event) {
|
||||
return result
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
@ -515,7 +637,10 @@ final class VideoChatParticipantsComponent: Component {
|
||||
if component.expandedVideoState != nil {
|
||||
expandedGridItemContainerFrame = itemLayout.expandedGrid.itemContainerFrame()
|
||||
} else {
|
||||
switch itemLayout.layoutType {
|
||||
case .vertical:
|
||||
expandedGridItemContainerFrame = itemLayout.gridItemContainerFrame().offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY)
|
||||
|
||||
if expandedGridItemContainerFrame.origin.y < component.collapsedContainerInsets.top {
|
||||
expandedGridItemContainerFrame.size.height -= component.collapsedContainerInsets.top - expandedGridItemContainerFrame.origin.y
|
||||
expandedGridItemContainerFrame.origin.y = component.collapsedContainerInsets.top
|
||||
@ -523,6 +648,17 @@ final class VideoChatParticipantsComponent: Component {
|
||||
if expandedGridItemContainerFrame.origin.y + expandedGridItemContainerFrame.height > itemLayout.containerSize.height - component.collapsedContainerInsets.bottom {
|
||||
expandedGridItemContainerFrame.size.height -= (expandedGridItemContainerFrame.origin.y + expandedGridItemContainerFrame.height) - (itemLayout.containerSize.height - component.collapsedContainerInsets.bottom)
|
||||
}
|
||||
case .horizontal:
|
||||
expandedGridItemContainerFrame = itemLayout.gridItemContainerFrame().offsetBy(dx: itemLayout.separateVideoScrollClippingFrame.minX, dy: 0.0).offsetBy(dx: 0.0, dy: -self.separateVideoScrollView.bounds.minY)
|
||||
|
||||
if expandedGridItemContainerFrame.origin.y < component.collapsedContainerInsets.top {
|
||||
expandedGridItemContainerFrame.size.height -= component.collapsedContainerInsets.top - expandedGridItemContainerFrame.origin.y
|
||||
expandedGridItemContainerFrame.origin.y = component.collapsedContainerInsets.top
|
||||
}
|
||||
if expandedGridItemContainerFrame.origin.y + expandedGridItemContainerFrame.height > itemLayout.containerSize.height {
|
||||
expandedGridItemContainerFrame.size.height -= (expandedGridItemContainerFrame.origin.y + expandedGridItemContainerFrame.height) - (itemLayout.containerSize.height)
|
||||
}
|
||||
}
|
||||
if expandedGridItemContainerFrame.size.height < 0.0 {
|
||||
expandedGridItemContainerFrame.size.height = 0.0
|
||||
}
|
||||
@ -533,7 +669,13 @@ final class VideoChatParticipantsComponent: Component {
|
||||
var validGridItemIds: [VideoParticipantKey] = []
|
||||
var validGridItemIndices: [Int] = []
|
||||
|
||||
let visibleGridItemRange = itemLayout.visibleGridItemRange(for: self.scrollView.bounds)
|
||||
let visibleGridItemRange: (minIndex: Int, maxIndex: Int)
|
||||
switch itemLayout.layoutType {
|
||||
case .vertical:
|
||||
visibleGridItemRange = itemLayout.visibleGridItemRange(for: self.scrollView.bounds)
|
||||
case .horizontal:
|
||||
visibleGridItemRange = itemLayout.visibleGridItemRange(for: self.separateVideoScrollView.bounds)
|
||||
}
|
||||
if visibleGridItemRange.maxIndex >= visibleGridItemRange.minIndex {
|
||||
for index in visibleGridItemRange.minIndex ... visibleGridItemRange.maxIndex {
|
||||
let videoParticipant = self.gridParticipants[index]
|
||||
@ -592,6 +734,16 @@ final class VideoChatParticipantsComponent: Component {
|
||||
itemFrame = itemLayout.gridItemFrame(at: index)
|
||||
}
|
||||
|
||||
var itemBottomInset: CGFloat = isItemExpanded ? 96.0 : 0.0
|
||||
switch itemLayout.layoutType {
|
||||
case .vertical:
|
||||
break
|
||||
case .horizontal:
|
||||
if isItemExpanded {
|
||||
itemBottomInset += itemLayout.expandedGrid.containerInsets.bottom
|
||||
}
|
||||
}
|
||||
|
||||
let _ = itemView.view.update(
|
||||
transition: itemTransition,
|
||||
component: AnyComponent(VideoChatParticipantVideoComponent(
|
||||
@ -600,7 +752,8 @@ final class VideoChatParticipantsComponent: Component {
|
||||
isPresentation: videoParticipant.isPresentation,
|
||||
isSpeaking: component.speakingParticipants.contains(videoParticipant.participant.peer.id),
|
||||
isExpanded: isItemExpanded,
|
||||
bottomInset: isItemExpanded ? 96.0 : 0.0,
|
||||
bottomInset: itemBottomInset,
|
||||
rootVideoLoadingEffectView: self.rootVideoLoadingEffectView,
|
||||
action: { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
@ -893,7 +1046,13 @@ final class VideoChatParticipantsComponent: Component {
|
||||
environment: {},
|
||||
containerSize: itemLayout.expandedGrid.itemContainerFrame().size
|
||||
)
|
||||
let expandedThumbnailsFrame = CGRect(origin: CGPoint(x: 0.0, y: expandedGridItemContainerFrame.height - expandedThumbnailsSize.height), size: expandedThumbnailsSize)
|
||||
var expandedThumbnailsFrame = CGRect(origin: CGPoint(x: 0.0, y: expandedGridItemContainerFrame.height - expandedThumbnailsSize.height), size: expandedThumbnailsSize)
|
||||
switch itemLayout.layoutType {
|
||||
case .vertical:
|
||||
break
|
||||
case .horizontal:
|
||||
expandedThumbnailsFrame.origin.y -= itemLayout.expandedGrid.containerInsets.bottom
|
||||
}
|
||||
if let expandedThumbnailsComponentView = expandedThumbnailsView.view {
|
||||
if expandedThumbnailsComponentView.superview == nil {
|
||||
self.expandedGridItemContainer.addSubview(expandedThumbnailsComponentView)
|
||||
@ -928,11 +1087,22 @@ final class VideoChatParticipantsComponent: Component {
|
||||
component: AnyComponent(VideoChatExpandedControlsComponent(
|
||||
theme: component.theme,
|
||||
strings: component.strings,
|
||||
isPinned: expandedVideoState.isMainParticipantPinned,
|
||||
backAction: { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.updateMainParticipant(nil)
|
||||
},
|
||||
pinAction: { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
guard let expandedVideoState = component.expandedVideoState else {
|
||||
return
|
||||
}
|
||||
|
||||
component.updateIsMainParticipantPinned(!expandedVideoState.isMainParticipantPinned)
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
@ -1011,6 +1181,28 @@ final class VideoChatParticipantsComponent: Component {
|
||||
|
||||
self.component = component
|
||||
|
||||
if !"".isEmpty {
|
||||
let rootVideoLoadingEffectView: VideoChatVideoLoadingEffectView
|
||||
if let current = self.rootVideoLoadingEffectView {
|
||||
rootVideoLoadingEffectView = current
|
||||
} else {
|
||||
rootVideoLoadingEffectView = VideoChatVideoLoadingEffectView(
|
||||
effectAlpha: 0.1,
|
||||
borderAlpha: 0.0,
|
||||
gradientWidth: 260.0,
|
||||
duration: 1.0,
|
||||
hasCustomBorder: false,
|
||||
playOnce: false
|
||||
)
|
||||
self.rootVideoLoadingEffectView = rootVideoLoadingEffectView
|
||||
self.insertSubview(rootVideoLoadingEffectView, at: 0)
|
||||
rootVideoLoadingEffectView.alpha = 0.0
|
||||
rootVideoLoadingEffectView.isUserInteractionEnabled = false
|
||||
}
|
||||
|
||||
rootVideoLoadingEffectView.update(size: availableSize, transition: transition)
|
||||
}
|
||||
|
||||
let measureListItemSize = self.measureListItemView.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(PeerListItemComponent(
|
||||
@ -1080,6 +1272,7 @@ final class VideoChatParticipantsComponent: Component {
|
||||
|
||||
let itemLayout = ItemLayout(
|
||||
containerSize: availableSize,
|
||||
layoutType: component.layoutType,
|
||||
sideInset: component.sideInset,
|
||||
collapsedContainerInsets: component.collapsedContainerInsets,
|
||||
expandedContainerInsets: component.expandedContainerInsets,
|
||||
@ -1097,7 +1290,7 @@ final class VideoChatParticipantsComponent: Component {
|
||||
cornerRadius: 10.0
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - itemLayout.sideInset * 2.0, height: itemLayout.list.contentHeight())
|
||||
containerSize: CGSize(width: itemLayout.list.containerSize.width, height: itemLayout.list.contentHeight())
|
||||
)
|
||||
let listItemsBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: listItemsBackgroundSize)
|
||||
if let listItemsBackgroundView = self.listItemsBackground.view {
|
||||
@ -1143,58 +1336,56 @@ final class VideoChatParticipantsComponent: Component {
|
||||
}
|
||||
(component.call as! PresentationGroupCallImpl).setRequestedVideoList(items: requestedVideo)
|
||||
|
||||
let scrollClippingFrame = CGRect(origin: CGPoint(x: itemLayout.sideInset, y: component.collapsedContainerInsets.top), size: CGSize(width: availableSize.width - itemLayout.sideInset * 2.0, height: availableSize.height - component.collapsedContainerInsets.top - component.collapsedContainerInsets.bottom))
|
||||
transition.setPosition(view: self.scrollViewClippingContainer, position: scrollClippingFrame.center)
|
||||
transition.setBounds(view: self.scrollViewClippingContainer, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size))
|
||||
transition.setFrame(view: self.scrollViewClippingContainer.cornersView, frame: scrollClippingFrame)
|
||||
transition.setPosition(view: self.scrollViewClippingContainer, position: itemLayout.scrollClippingFrame.center)
|
||||
transition.setBounds(view: self.scrollViewClippingContainer, bounds: CGRect(origin: CGPoint(x: itemLayout.scrollClippingFrame.minX - itemLayout.listFrame.minX, y: itemLayout.scrollClippingFrame.minY - itemLayout.listFrame.minY), size: itemLayout.scrollClippingFrame.size))
|
||||
transition.setFrame(view: self.scrollViewClippingContainer.cornersView, frame: itemLayout.scrollClippingFrame)
|
||||
self.scrollViewClippingContainer.update(params: SolidRoundedCornersContainer.Params(
|
||||
size: scrollClippingFrame.size,
|
||||
size: itemLayout.scrollClippingFrame.size,
|
||||
color: .black,
|
||||
cornerRadius: 10.0,
|
||||
smoothCorners: false
|
||||
), transition: transition)
|
||||
|
||||
if self.scrollViewClippingShadowView.image == nil {
|
||||
let height: CGFloat = 24.0
|
||||
let baseGradientAlpha: CGFloat = 1.0
|
||||
let numSteps = 8
|
||||
let firstStep = 0
|
||||
let firstLocation = 0.0
|
||||
let colors = (0 ..< numSteps).map { i -> UIColor in
|
||||
if i < firstStep {
|
||||
return UIColor(white: 1.0, alpha: 1.0)
|
||||
} else {
|
||||
let step: CGFloat = CGFloat(i - firstStep) / CGFloat(numSteps - firstStep - 1)
|
||||
let value: CGFloat = 1.0 - bezierPoint(0.42, 0.0, 0.58, 1.0, step)
|
||||
return UIColor(white: 0.0, alpha: baseGradientAlpha * value)
|
||||
}
|
||||
}
|
||||
let locations = (0 ..< numSteps).map { i -> CGFloat in
|
||||
if i < firstStep {
|
||||
return 0.0
|
||||
} else {
|
||||
let step: CGFloat = CGFloat(i - firstStep) / CGFloat(numSteps - firstStep - 1)
|
||||
return (firstLocation + (1.0 - firstLocation) * step)
|
||||
}
|
||||
}
|
||||
|
||||
self.scrollViewClippingShadowView.image = generateGradientImage(size: CGSize(width: 8.0, height: height), colors: colors.reversed(), locations: locations.reversed().map { 1.0 - $0 })!.withRenderingMode(.alwaysTemplate).stretchableImage(withLeftCapWidth: 0, topCapHeight: Int(height - 1.0))
|
||||
self.scrollViewClippingShadowView.tintColor = .black
|
||||
}
|
||||
let scrollViewClippingShadowHeight: CGFloat = 24.0
|
||||
let scrollViewClippingShadowOffset: CGFloat = 0.0
|
||||
transition.setFrame(view: self.scrollViewClippingShadowView, frame: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.maxY + scrollViewClippingShadowOffset - scrollViewClippingShadowHeight), size: CGSize(width: scrollClippingFrame.width, height: scrollViewClippingShadowHeight)))
|
||||
transition.setPosition(view: self.separateVideoScrollViewClippingContainer, position: itemLayout.separateVideoScrollClippingFrame.center)
|
||||
transition.setBounds(view: self.separateVideoScrollViewClippingContainer, bounds: CGRect(origin: CGPoint(x: itemLayout.separateVideoScrollClippingFrame.minX - itemLayout.separateVideoGridFrame.minX, y: itemLayout.separateVideoScrollClippingFrame.minY - itemLayout.separateVideoGridFrame.minY), size: itemLayout.separateVideoScrollClippingFrame.size))
|
||||
transition.setFrame(view: self.separateVideoScrollViewClippingContainer.cornersView, frame: itemLayout.separateVideoScrollClippingFrame)
|
||||
self.separateVideoScrollViewClippingContainer.update(params: SolidRoundedCornersContainer.Params(
|
||||
size: itemLayout.separateVideoScrollClippingFrame.size,
|
||||
color: .black,
|
||||
cornerRadius: 10.0,
|
||||
smoothCorners: false
|
||||
), transition: transition)
|
||||
|
||||
self.ignoreScrolling = true
|
||||
if self.scrollView.bounds.size != availableSize {
|
||||
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: availableSize))
|
||||
if self.scrollView.bounds.size != itemLayout.listFrame.size {
|
||||
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: itemLayout.listFrame.size))
|
||||
}
|
||||
let contentSize = CGSize(width: availableSize.width, height: itemLayout.contentHeight())
|
||||
let contentSize = CGSize(width: itemLayout.listFrame.width, height: itemLayout.contentHeight())
|
||||
if self.scrollView.contentSize != contentSize {
|
||||
self.scrollView.contentSize = contentSize
|
||||
}
|
||||
|
||||
if self.separateVideoScrollView.bounds.size != itemLayout.separateVideoGridFrame.size {
|
||||
transition.setFrame(view: self.separateVideoScrollView, frame: CGRect(origin: CGPoint(), size: itemLayout.separateVideoGridFrame.size))
|
||||
}
|
||||
let separateVideoContentSize = CGSize(width: itemLayout.separateVideoGridFrame.width, height: itemLayout.separateVideoGridContentHeight())
|
||||
if self.separateVideoScrollView.contentSize != separateVideoContentSize {
|
||||
self.separateVideoScrollView.contentSize = separateVideoContentSize
|
||||
}
|
||||
|
||||
self.ignoreScrolling = false
|
||||
|
||||
switch component.layoutType {
|
||||
case .vertical:
|
||||
if self.gridItemViewContainer.superview !== self.scrollView {
|
||||
self.scrollView.addSubview(self.gridItemViewContainer)
|
||||
}
|
||||
case .horizontal:
|
||||
if self.gridItemViewContainer.superview !== self.separateVideoScrollView {
|
||||
self.separateVideoScrollView.addSubview(self.gridItemViewContainer)
|
||||
}
|
||||
}
|
||||
|
||||
self.updateScrolling(transition: transition)
|
||||
|
||||
return availableSize
|
||||
|
@ -0,0 +1,99 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import TelegramPresentationData
|
||||
import ComponentDisplayAdapters
|
||||
|
||||
final class VideoChatPinStatusComponent: Component {
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let isPinned: Bool
|
||||
let action: () -> Void
|
||||
|
||||
init(
|
||||
theme: PresentationTheme,
|
||||
strings: PresentationStrings,
|
||||
isPinned: Bool,
|
||||
action: @escaping () -> Void
|
||||
) {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.isPinned = isPinned
|
||||
self.action = action
|
||||
}
|
||||
|
||||
static func ==(lhs: VideoChatPinStatusComponent, rhs: VideoChatPinStatusComponent) -> Bool {
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.strings !== rhs.strings {
|
||||
return false
|
||||
}
|
||||
if lhs.isPinned != rhs.isPinned {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private var pinNode: VoiceChatPinButtonNode?
|
||||
|
||||
private var component: VideoChatPinStatusComponent?
|
||||
private var isUpdating: Bool = false
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
}
|
||||
|
||||
@objc private func pinPressed() {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.action()
|
||||
}
|
||||
|
||||
func update(component: VideoChatPinStatusComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
self.isUpdating = true
|
||||
defer {
|
||||
self.isUpdating = false
|
||||
}
|
||||
|
||||
self.component = component
|
||||
|
||||
let pinNode: VoiceChatPinButtonNode
|
||||
if let current = self.pinNode {
|
||||
pinNode = current
|
||||
} else {
|
||||
pinNode = VoiceChatPinButtonNode(theme: component.theme, strings: component.strings)
|
||||
self.pinNode = pinNode
|
||||
self.addSubview(pinNode.view)
|
||||
pinNode.addTarget(self, action: #selector(self.pinPressed), forControlEvents: .touchUpInside)
|
||||
}
|
||||
let pinNodeSize = pinNode.update(size: availableSize, transition: transition.containedViewLayoutTransition)
|
||||
let pinNodeFrame = CGRect(origin: CGPoint(), size: pinNodeSize)
|
||||
transition.setFrame(view: pinNode.view, frame: pinNodeFrame)
|
||||
|
||||
pinNode.update(pinned: component.isPinned, animated: !transition.animation.isImmediate)
|
||||
|
||||
let size = pinNodeSize
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View()
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,130 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import HierarchyTrackingLayer
|
||||
|
||||
private let shadowImage: UIImage? = {
|
||||
UIImage(named: "Stories/PanelGradient")
|
||||
}()
|
||||
|
||||
final class VideoChatVideoLoadingEffectView: UIView {
|
||||
private let duration: Double
|
||||
private let hasCustomBorder: Bool
|
||||
private let playOnce: Bool
|
||||
|
||||
private let hierarchyTrackingLayer: HierarchyTrackingLayer
|
||||
|
||||
private let gradientWidth: CGFloat
|
||||
|
||||
let portalSource: PortalSourceView
|
||||
|
||||
private let backgroundView: UIImageView
|
||||
|
||||
private let borderGradientView: UIImageView
|
||||
private let borderContainerView: UIView
|
||||
let borderMaskLayer: SimpleShapeLayer
|
||||
|
||||
private var didPlayOnce = false
|
||||
|
||||
init(effectAlpha: CGFloat, borderAlpha: CGFloat, gradientWidth: CGFloat = 200.0, duration: Double, hasCustomBorder: Bool, playOnce: Bool) {
|
||||
self.portalSource = PortalSourceView()
|
||||
|
||||
self.hierarchyTrackingLayer = HierarchyTrackingLayer()
|
||||
|
||||
self.duration = duration
|
||||
self.hasCustomBorder = hasCustomBorder
|
||||
self.playOnce = playOnce
|
||||
|
||||
self.gradientWidth = gradientWidth
|
||||
self.backgroundView = UIImageView()
|
||||
|
||||
self.borderGradientView = UIImageView()
|
||||
self.borderContainerView = UIView()
|
||||
self.borderMaskLayer = SimpleShapeLayer()
|
||||
|
||||
super.init(frame: .zero)
|
||||
|
||||
self.portalSource.layer.addSublayer(self.hierarchyTrackingLayer)
|
||||
self.hierarchyTrackingLayer.didEnterHierarchy = { [weak self] in
|
||||
guard let self, self.bounds.width != 0.0 else {
|
||||
return
|
||||
}
|
||||
self.updateAnimations(size: self.bounds.size)
|
||||
}
|
||||
|
||||
let generateGradient: (CGFloat) -> UIImage? = { baseAlpha in
|
||||
return generateImage(CGSize(width: self.gradientWidth, height: 16.0), opaque: false, scale: 1.0, rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
let foregroundColor = UIColor(white: 1.0, alpha: min(1.0, baseAlpha * 4.0))
|
||||
|
||||
if let shadowImage {
|
||||
UIGraphicsPushContext(context)
|
||||
|
||||
for i in 0 ..< 2 {
|
||||
let shadowFrame = CGRect(origin: CGPoint(x: CGFloat(i) * (size.width * 0.5), y: 0.0), size: CGSize(width: size.width * 0.5, height: size.height))
|
||||
|
||||
context.saveGState()
|
||||
context.translateBy(x: shadowFrame.midX, y: shadowFrame.midY)
|
||||
context.rotate(by: CGFloat(i == 0 ? 1.0 : -1.0) * CGFloat.pi * 0.5)
|
||||
let adjustedRect = CGRect(origin: CGPoint(x: -shadowFrame.height * 0.5, y: -shadowFrame.width * 0.5), size: CGSize(width: shadowFrame.height, height: shadowFrame.width))
|
||||
|
||||
context.clip(to: adjustedRect, mask: shadowImage.cgImage!)
|
||||
context.setFillColor(foregroundColor.cgColor)
|
||||
context.fill(adjustedRect)
|
||||
|
||||
context.restoreGState()
|
||||
}
|
||||
|
||||
UIGraphicsPopContext()
|
||||
}
|
||||
})
|
||||
}
|
||||
self.backgroundView.image = generateGradient(effectAlpha)
|
||||
self.portalSource.addSubview(self.backgroundView)
|
||||
|
||||
self.borderGradientView.image = generateGradient(borderAlpha)
|
||||
self.borderContainerView.addSubview(self.borderGradientView)
|
||||
self.portalSource.addSubview(self.borderContainerView)
|
||||
self.borderContainerView.layer.mask = self.borderMaskLayer
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func updateAnimations(size: CGSize) {
|
||||
if self.backgroundView.layer.animation(forKey: "shimmer") != nil || (self.playOnce && self.didPlayOnce) {
|
||||
return
|
||||
}
|
||||
|
||||
let animation = self.backgroundView.layer.makeAnimation(from: 0.0 as NSNumber, to: (size.width + self.gradientWidth + size.width * 0.2) as NSNumber, keyPath: "position.x", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: self.duration, delay: 0.0, mediaTimingFunction: nil, removeOnCompletion: true, additive: true)
|
||||
animation.repeatCount = self.playOnce ? 1 : Float.infinity
|
||||
self.backgroundView.layer.add(animation, forKey: "shimmer")
|
||||
self.borderGradientView.layer.add(animation, forKey: "shimmer")
|
||||
|
||||
self.didPlayOnce = true
|
||||
}
|
||||
|
||||
func update(size: CGSize, transition: ComponentTransition) {
|
||||
if self.backgroundView.bounds.size != size {
|
||||
self.backgroundView.layer.removeAllAnimations()
|
||||
|
||||
if !self.hasCustomBorder {
|
||||
self.borderMaskLayer.fillColor = nil
|
||||
self.borderMaskLayer.strokeColor = UIColor.white.cgColor
|
||||
let lineWidth: CGFloat = 3.0
|
||||
self.borderMaskLayer.lineWidth = lineWidth
|
||||
self.borderMaskLayer.path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size).insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5), cornerRadius: 12.0).cgPath
|
||||
}
|
||||
}
|
||||
|
||||
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: -self.gradientWidth, y: 0.0), size: CGSize(width: self.gradientWidth, height: size.height)))
|
||||
|
||||
transition.setFrame(view: self.borderContainerView, frame: CGRect(origin: CGPoint(), size: size))
|
||||
transition.setFrame(view: self.borderGradientView, frame: CGRect(origin: CGPoint(x: -self.gradientWidth, y: 0.0), size: CGSize(width: self.gradientWidth, height: size.height)))
|
||||
|
||||
self.updateAnimations(size: size)
|
||||
}
|
||||
}
|
@ -24,18 +24,18 @@ private let backgroundCornerRadius: CGFloat = 11.0
|
||||
private let fadeColor = UIColor(rgb: 0x000000, alpha: 0.5)
|
||||
private let destructiveColor: UIColor = UIColor(rgb: 0xff3b30)
|
||||
|
||||
private class VoiceChatPinButtonNode: HighlightTrackingButtonNode {
|
||||
class VoiceChatPinButtonNode: HighlightTrackingButtonNode {
|
||||
private let pinButtonIconNode: VoiceChatPinNode
|
||||
private let pinButtonClippingnode: ASDisplayNode
|
||||
private let pinButtonTitleNode: ImmediateTextNode
|
||||
|
||||
init(presentationData: PresentationData) {
|
||||
init(theme: PresentationTheme, strings: PresentationStrings) {
|
||||
self.pinButtonIconNode = VoiceChatPinNode()
|
||||
self.pinButtonClippingnode = ASDisplayNode()
|
||||
self.pinButtonClippingnode.clipsToBounds = true
|
||||
|
||||
self.pinButtonTitleNode = ImmediateTextNode()
|
||||
self.pinButtonTitleNode.attributedText = NSAttributedString(string: presentationData.strings.VoiceChat_Unpin, font: Font.regular(17.0), textColor: .white)
|
||||
self.pinButtonTitleNode.attributedText = NSAttributedString(string: strings.VoiceChat_Unpin, font: Font.regular(17.0), textColor: .white)
|
||||
self.pinButtonTitleNode.alpha = 0.0
|
||||
|
||||
super.init()
|
||||
@ -209,7 +209,7 @@ final class VoiceChatMainStageNode: ASDisplayNode {
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
self.pinButtonNode = VoiceChatPinButtonNode(presentationData: presentationData)
|
||||
self.pinButtonNode = VoiceChatPinButtonNode(theme: presentationData.theme, strings: presentationData.strings)
|
||||
|
||||
self.backdropAvatarNode = ImageNode()
|
||||
self.backdropAvatarNode.contentMode = .scaleAspectFill
|
||||
|
Loading…
x
Reference in New Issue
Block a user