mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-23 14:45:21 +00:00
Grouped music files
This commit is contained in:
@@ -14,6 +14,7 @@ import TelegramStringFormatting
|
||||
import RadialStatusNode
|
||||
import SemanticStatusNode
|
||||
import FileMediaResourceStatus
|
||||
import CheckNode
|
||||
|
||||
private struct FetchControls {
|
||||
let fetch: () -> Void
|
||||
@@ -21,6 +22,10 @@ private struct FetchControls {
|
||||
}
|
||||
|
||||
final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
private var selectionBackgroundNode: ASDisplayNode?
|
||||
private var selectionNode: FileMessageSelectionNode?
|
||||
private var cutoutNode: ASDisplayNode?
|
||||
|
||||
private let titleNode: TextNode
|
||||
private let descriptionNode: TextNode
|
||||
private let descriptionMeasuringNode: TextNode
|
||||
@@ -35,7 +40,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
private var iconNode: TransformImageNode?
|
||||
private var statusNode: SemanticStatusNode?
|
||||
private var playbackAudioLevelView: VoiceBlobView?
|
||||
private var streamingStatusNode: RadialStatusNode?
|
||||
private var streamingStatusNode: SemanticStatusNode?
|
||||
private var tapRecognizer: UITapGestureRecognizer?
|
||||
|
||||
private let statusDisposable = MetaDisposable()
|
||||
@@ -76,6 +81,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
private var actualFetchStatus: MediaResourceStatus?
|
||||
private let fetchDisposable = MetaDisposable()
|
||||
|
||||
var toggleSelection: (Bool) -> Void = { _ in }
|
||||
var activateLocalContent: () -> Void = { }
|
||||
var requestUpdateLayout: (Bool) -> Void = { _ in }
|
||||
|
||||
@@ -86,8 +92,6 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
private var progressFrame: CGRect?
|
||||
private var streamingCacheStatusFrame: CGRect?
|
||||
private var fileIconImage: UIImage?
|
||||
private var cloudFetchIconImage: UIImage?
|
||||
private var cloudFetchedIconImage: UIImage?
|
||||
|
||||
override init() {
|
||||
self.titleNode = TextNode()
|
||||
@@ -204,7 +208,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ chatLocation: ChatLocation, _ attributes: ChatMessageEntryAttributes, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ isRecentActions: Bool, _ forcedResourceStatus: FileMediaResourceStatus?, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool) -> Void))) {
|
||||
func asyncLayout() -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ chatLocation: ChatLocation, _ attributes: ChatMessageEntryAttributes, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ isRecentActions: Bool, _ forcedResourceStatus: FileMediaResourceStatus?, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool) -> Void))) {
|
||||
let currentFile = self.file
|
||||
|
||||
let titleAsyncLayout = TextNode.asyncLayout(self.titleNode)
|
||||
@@ -214,7 +218,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
|
||||
let currentMessage = self.message
|
||||
|
||||
return { context, presentationData, message, chatLocation, attributes, file, automaticDownload, incoming, isRecentActions, forcedResourceStatus, dateAndStatusType, constrainedSize in
|
||||
return { context, presentationData, message, chatLocation, attributes, file, automaticDownload, incoming, isRecentActions, forcedResourceStatus, dateAndStatusType, messageSelection, constrainedSize in
|
||||
return (CGFloat.greatestFiniteMagnitude, { constrainedSize in
|
||||
let titleFont = Font.regular(floor(presentationData.fontSize.baseDisplaySize * 16.0 / 17.0))
|
||||
let descriptionFont = Font.regular(floor(presentationData.fontSize.baseDisplaySize * 13.0 / 17.0))
|
||||
@@ -415,15 +419,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
textConstrainedSize.width -= 80.0
|
||||
}
|
||||
|
||||
let streamingProgressDiameter: CGFloat = 28.0
|
||||
var hasStreamingProgress = false
|
||||
if isAudio && !isVoice {
|
||||
hasStreamingProgress = true
|
||||
|
||||
if hasStreamingProgress {
|
||||
textConstrainedSize.width -= streamingProgressDiameter + 4.0
|
||||
}
|
||||
}
|
||||
let streamingProgressDiameter: CGFloat = 20.0
|
||||
|
||||
let (titleLayout, titleApply) = titleAsyncLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: hasThumbnail ? 2 : 1, truncationType: .middle, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
let (descriptionLayout, descriptionApply) = descriptionAsyncLayout(TextNodeLayoutArguments(attributedString: descriptionString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
@@ -461,15 +457,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
if let statusSize = statusSize {
|
||||
minLayoutWidth = max(minLayoutWidth, statusSize.width)
|
||||
}
|
||||
|
||||
var cloudFetchIconImage: UIImage?
|
||||
var cloudFetchedIconImage: UIImage?
|
||||
if hasStreamingProgress {
|
||||
minLayoutWidth += streamingProgressDiameter + 4.0
|
||||
cloudFetchIconImage = incoming ? PresentationResourcesChat.chatBubbleFileCloudFetchIncomingIcon(presentationData.theme.theme) : PresentationResourcesChat.chatBubbleFileCloudFetchOutgoingIcon(presentationData.theme.theme)
|
||||
cloudFetchedIconImage = incoming ? PresentationResourcesChat.chatBubbleFileCloudFetchedIncomingIcon(presentationData.theme.theme) : PresentationResourcesChat.chatBubbleFileCloudFetchedOutgoingIcon(presentationData.theme.theme)
|
||||
}
|
||||
|
||||
|
||||
let fileIconImage: UIImage?
|
||||
if hasThumbnail {
|
||||
fileIconImage = nil
|
||||
@@ -551,10 +539,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
if isAudio && !isVoice {
|
||||
streamingCacheStatusFrame = CGRect(origin: CGPoint(x: boundingWidth - streamingProgressDiameter + 1.0, y: 8.0), size: CGSize(width: streamingProgressDiameter, height: streamingProgressDiameter))
|
||||
if hasStreamingProgress {
|
||||
fittedLayoutSize.width += streamingProgressDiameter + 6.0
|
||||
}
|
||||
streamingCacheStatusFrame = CGRect(origin: CGPoint(x: progressFrame.maxX - streamingProgressDiameter + 2.0, y: progressFrame.maxY - streamingProgressDiameter + 2.0), size: CGSize(width: streamingProgressDiameter, height: streamingProgressDiameter))
|
||||
fittedLayoutSize.width = max(fittedLayoutSize.width, boundingWidth + 2.0)
|
||||
} else {
|
||||
streamingCacheStatusFrame = CGRect()
|
||||
@@ -688,7 +673,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
strongSelf.waveformNode.displaysAsynchronously = !presentationData.isPreview
|
||||
strongSelf.statusNode?.displaysAsynchronously = !presentationData.isPreview
|
||||
strongSelf.statusNode?.frame = progressFrame
|
||||
@@ -696,8 +681,6 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
strongSelf.progressFrame = progressFrame
|
||||
strongSelf.streamingCacheStatusFrame = streamingCacheStatusFrame
|
||||
strongSelf.fileIconImage = fileIconImage
|
||||
strongSelf.cloudFetchIconImage = cloudFetchIconImage
|
||||
strongSelf.cloudFetchedIconImage = cloudFetchedIconImage
|
||||
|
||||
if let updatedFetchControls = updatedFetchControls {
|
||||
let _ = strongSelf.fetchControls.swap(updatedFetchControls)
|
||||
@@ -706,7 +689,64 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
strongSelf.updateStatus(animated: !synchronousLoads)
|
||||
let isAnimated = !synchronousLoads
|
||||
let transition: ContainedViewLayoutTransition = isAnimated ? .animated(duration: 0.2, curve: .spring) : .immediate
|
||||
if let selection = messageSelection {
|
||||
if let streamingStatusNode = strongSelf.streamingStatusNode {
|
||||
transition.updateAlpha(node: streamingStatusNode, alpha: 0.0)
|
||||
transition.updateTransformScale(node: streamingStatusNode, scale: 0.2)
|
||||
}
|
||||
let selectionFrame = CGRect(origin: CGPoint(), size: fittedLayoutSize)
|
||||
if let selectionNode = strongSelf.selectionNode {
|
||||
selectionNode.frame = selectionFrame
|
||||
selectionNode.updateSelected(selection, animated: isAnimated)
|
||||
} else {
|
||||
let selectionNode = FileMessageSelectionNode(theme: presentationData.theme.theme, incoming: incoming, toggle: { [weak self] value in
|
||||
self?.toggleSelection(value)
|
||||
})
|
||||
strongSelf.selectionNode = selectionNode
|
||||
strongSelf.addSubnode(selectionNode)
|
||||
selectionNode.frame = selectionFrame
|
||||
selectionNode.updateSelected(selection, animated: false)
|
||||
if isAnimated {
|
||||
selectionNode.animateIn()
|
||||
}
|
||||
}
|
||||
|
||||
let selectionBackgroundFrame = CGRect(origin: CGPoint(x: -8.0, y: -9.0), size: CGSize(width: fittedLayoutSize.width + 16.0, height: fittedLayoutSize.height + 6.0))
|
||||
if let selectionBackgroundNode = strongSelf.selectionBackgroundNode {
|
||||
selectionBackgroundNode.frame = selectionBackgroundFrame
|
||||
selectionBackgroundNode.isHidden = !selection
|
||||
} else {
|
||||
let selectionBackgroundNode = ASDisplayNode()
|
||||
selectionBackgroundNode.backgroundColor = messageTheme.accentControlColor.withAlphaComponent(0.08)
|
||||
selectionBackgroundNode.frame = selectionBackgroundFrame
|
||||
selectionBackgroundNode.isHidden = !selection
|
||||
strongSelf.selectionBackgroundNode = selectionBackgroundNode
|
||||
strongSelf.insertSubnode(selectionBackgroundNode, at: 0)
|
||||
}
|
||||
} else {
|
||||
if let streamingStatusNode = strongSelf.streamingStatusNode {
|
||||
transition.updateAlpha(node: streamingStatusNode, alpha: 1.0)
|
||||
transition.updateTransformScale(node: streamingStatusNode, scale: 1.0)
|
||||
}
|
||||
if let selectionNode = strongSelf.selectionNode {
|
||||
strongSelf.selectionNode = nil
|
||||
if isAnimated {
|
||||
selectionNode.animateOut(completion: { [weak selectionNode] in
|
||||
selectionNode?.removeFromSupernode()
|
||||
})
|
||||
} else {
|
||||
selectionNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
if let selectionBackgroundNode = strongSelf.selectionBackgroundNode {
|
||||
strongSelf.selectionBackgroundNode = nil
|
||||
selectionBackgroundNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
|
||||
strongSelf.updateStatus(animated: isAnimated)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -751,7 +791,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
let state: SemanticStatusNodeState
|
||||
var streamingState: RadialStatusNodeState = .none
|
||||
var streamingState: SemanticStatusNodeState = .none
|
||||
|
||||
let isSending = message.flags.isSending
|
||||
|
||||
@@ -795,35 +835,19 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
if isAudio && !isVoice && !isSending {
|
||||
let streamingStatusForegroundColor: UIColor = messageTheme.accentControlColor
|
||||
let streamingStatusBackgroundColor: UIColor = messageTheme.mediaInactiveControlColor
|
||||
switch resourceStatus.fetchStatus {
|
||||
case let .Fetching(_, progress):
|
||||
let adjustedProgress = max(progress, 0.027)
|
||||
streamingState = .cloudProgress(color: streamingStatusForegroundColor, strokeBackgroundColor: streamingStatusBackgroundColor, lineWidth: 2.0, value: CGFloat(adjustedProgress))
|
||||
streamingState = .progress(value: CGFloat(adjustedProgress), cancelEnabled: true, appearance: .init(inset: 1.0, lineWidth: 2.0))
|
||||
case .Local:
|
||||
if let cloudFetchedIconImage = self.cloudFetchedIconImage {
|
||||
streamingState = .customIcon(cloudFetchedIconImage)
|
||||
} else {
|
||||
streamingState = .none
|
||||
}
|
||||
streamingState = .none
|
||||
case .Remote:
|
||||
if let cloudFetchIconImage = self.cloudFetchIconImage {
|
||||
streamingState = .customIcon(cloudFetchIconImage)
|
||||
} else {
|
||||
streamingState = .none
|
||||
}
|
||||
streamingState = .download
|
||||
}
|
||||
} else {
|
||||
streamingState = .none
|
||||
}
|
||||
|
||||
let statusForegroundColor: UIColor
|
||||
if self.iconNode != nil {
|
||||
statusForegroundColor = presentationData.theme.theme.chat.message.mediaOverlayControlColors.foregroundColor
|
||||
} else {
|
||||
statusForegroundColor = incoming ? presentationData.theme.theme.chat.message.incoming.mediaControlInnerBackgroundColor : presentationData.theme.theme.chat.message.outgoing.mediaControlInnerBackgroundColor
|
||||
}
|
||||
|
||||
switch resourceStatus.mediaStatus {
|
||||
case var .fetchStatus(fetchStatus):
|
||||
if self.message?.forwardInfo != nil {
|
||||
@@ -903,7 +927,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
self.playbackAudioLevelView?.setColor(presentationData.theme.theme.chat.inputPanel.actionControlFillColor)
|
||||
|
||||
if streamingState != .none && self.streamingStatusNode == nil {
|
||||
let streamingStatusNode = RadialStatusNode(backgroundNodeColor: .clear)
|
||||
let streamingStatusNode = SemanticStatusNode(backgroundNodeColor: backgroundNodeColor, foregroundNodeColor: foregroundNodeColor)
|
||||
self.streamingStatusNode = streamingStatusNode
|
||||
streamingStatusNode.frame = streamingCacheStatusFrame
|
||||
self.addSubnode(streamingStatusNode)
|
||||
@@ -944,6 +968,36 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
if streamingState == .none && self.selectionNode == nil {
|
||||
if let cutoutNode = self.cutoutNode {
|
||||
self.cutoutNode = nil
|
||||
if animated {
|
||||
cutoutNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false) { [weak cutoutNode] _ in
|
||||
cutoutNode?.removeFromSupernode()
|
||||
}
|
||||
} else {
|
||||
cutoutNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
} else if let statusNode = self.statusNode {
|
||||
if let _ = self.cutoutNode {
|
||||
} else {
|
||||
let cutoutNode = ASImageNode()
|
||||
cutoutNode.displaysAsynchronously = false
|
||||
cutoutNode.displayWithoutProcessing = true
|
||||
cutoutNode.image = generateFilledCircleImage(diameter: 23.0, color: messageTheme.bubble.withWallpaper.fill)
|
||||
|
||||
self.cutoutNode = cutoutNode
|
||||
self.insertSubnode(cutoutNode, aboveSubnode: statusNode)
|
||||
|
||||
cutoutNode.frame = streamingCacheStatusFrame.insetBy(dx: -1.5, dy: -1.5)
|
||||
|
||||
if animated {
|
||||
cutoutNode.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let (expandedString, compactString, font) = downloadingStrings {
|
||||
self.fetchingTextNode.attributedText = NSAttributedString(string: expandedString, font: font, textColor: messageTheme.fileDurationColor)
|
||||
self.fetchingCompactTextNode.attributedText = NSAttributedString(string: compactString, font: font, textColor: messageTheme.fileDurationColor)
|
||||
@@ -975,12 +1029,12 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
self.fetchingCompactTextNode.frame = CGRect(origin: self.descriptionNode.frame.origin, size: fetchingCompactSize)
|
||||
}
|
||||
|
||||
static func asyncLayout(_ node: ChatMessageInteractiveFileNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ chatLocation: ChatLocation, _ attributes: ChatMessageEntryAttributes, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ isRecentActions: Bool, _ forcedResourceStatus: FileMediaResourceStatus?, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool) -> ChatMessageInteractiveFileNode))) {
|
||||
static func asyncLayout(_ node: ChatMessageInteractiveFileNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ chatLocation: ChatLocation, _ attributes: ChatMessageEntryAttributes, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ isRecentActions: Bool, _ forcedResourceStatus: FileMediaResourceStatus?, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool) -> ChatMessageInteractiveFileNode))) {
|
||||
let currentAsyncLayout = node?.asyncLayout()
|
||||
|
||||
return { context, presentationData, message, chatLocation, attributes, file, automaticDownload, incoming, isRecentActions, forcedResourceStatus, dateAndStatusType, constrainedSize in
|
||||
return { context, presentationData, message, chatLocation, attributes, file, automaticDownload, incoming, isRecentActions, forcedResourceStatus, dateAndStatusType, messageSelection, constrainedSize in
|
||||
var fileNode: ChatMessageInteractiveFileNode
|
||||
var fileLayout: (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ chatLocation: ChatLocation, _ attributes: ChatMessageEntryAttributes, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ isRecentActions: Bool, _ forcedResourceStatus: FileMediaResourceStatus?, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool) -> Void)))
|
||||
var fileLayout: (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ chatLocation: ChatLocation, _ attributes: ChatMessageEntryAttributes, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ isRecentActions: Bool, _ forcedResourceStatus: FileMediaResourceStatus?, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool) -> Void)))
|
||||
|
||||
if let node = node, let currentAsyncLayout = currentAsyncLayout {
|
||||
fileNode = node
|
||||
@@ -990,7 +1044,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
fileLayout = fileNode.asyncLayout()
|
||||
}
|
||||
|
||||
let (initialWidth, continueLayout) = fileLayout(context, presentationData, message, chatLocation, attributes, file, automaticDownload, incoming, isRecentActions, forcedResourceStatus, dateAndStatusType, constrainedSize)
|
||||
let (initialWidth, continueLayout) = fileLayout(context, presentationData, message, chatLocation, attributes, file, automaticDownload, incoming, isRecentActions, forcedResourceStatus, dateAndStatusType, messageSelection, constrainedSize)
|
||||
|
||||
return (initialWidth, { constrainedSize in
|
||||
let (finalWidth, finalLayout) = continueLayout(constrainedSize)
|
||||
@@ -1053,3 +1107,60 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
final class FileMessageSelectionNode: ASDisplayNode {
|
||||
private let toggle: (Bool) -> Void
|
||||
|
||||
private var selected = false
|
||||
private let checkNode: CheckNode
|
||||
|
||||
public init(theme: PresentationTheme, incoming: Bool, toggle: @escaping (Bool) -> Void) {
|
||||
self.toggle = toggle
|
||||
self.checkNode = CheckNode(strokeColor: incoming ? theme.chat.message.incoming.mediaPlaceholderColor : theme.chat.message.outgoing.mediaPlaceholderColor, fillColor: theme.list.itemCheckColors.fillColor, foregroundColor: theme.list.itemCheckColors.foregroundColor, style: .compact)
|
||||
self.checkNode.isUserInteractionEnabled = false
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.checkNode)
|
||||
}
|
||||
|
||||
override public func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
||||
}
|
||||
|
||||
public func animateIn() {
|
||||
self.checkNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
self.checkNode.layer.animateScale(from: 0.2, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
}
|
||||
|
||||
public func animateOut(completion: @escaping () -> Void) {
|
||||
self.checkNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
||||
self.checkNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
|
||||
completion()
|
||||
})
|
||||
}
|
||||
|
||||
public func updateSelected(_ selected: Bool, animated: Bool) {
|
||||
if self.selected != selected {
|
||||
self.selected = selected
|
||||
self.checkNode.setIsChecked(selected, animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
self.toggle(!self.selected)
|
||||
}
|
||||
}
|
||||
|
||||
override public func layout() {
|
||||
super.layout()
|
||||
|
||||
let checkSize = CGSize(width: 30.0, height: 30.0)
|
||||
self.checkNode.frame = CGRect(origin: CGPoint(x: 23.0, y: 17.0), size: checkSize)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user