From 58c8eaabe826c103e0f524f61662524d3f907142 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 5 Mar 2021 12:11:04 +0800 Subject: [PATCH] feat: add animation for progress bar value change --- Mastodon.xcodeproj/project.pbxproj | 16 +- Mastodon/Diffiable/Section/PollSection.swift | 2 +- .../Diffiable/Section/StatusSection.swift | 2 - .../View/Content/VoteProgressStripView.swift | 139 --------------- .../View/Control/StripProgressView.swift | 165 ++++++++++++++++++ .../PollOptionTableViewCell.swift | 4 +- 6 files changed, 180 insertions(+), 148 deletions(-) delete mode 100644 Mastodon/Scene/Share/View/Content/VoteProgressStripView.swift create mode 100644 Mastodon/Scene/Share/View/Control/StripProgressView.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 43969673..bac04cbb 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -129,7 +129,7 @@ DB59F0FE25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB59F0FD25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift */; }; DB59F10425EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB59F10325EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift */; }; DB59F10E25EF724F001F1DAB /* APIService+Poll.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */; }; - DB59F11825EFA35B001F1DAB /* VoteProgressStripView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB59F11725EFA35B001F1DAB /* VoteProgressStripView.swift */; }; + DB59F11825EFA35B001F1DAB /* StripProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB59F11725EFA35B001F1DAB /* StripProgressView.swift */; }; DB68586425E619B700F0A850 /* NSKeyValueObservation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68586325E619B700F0A850 /* NSKeyValueObservation.swift */; }; DB68A04A25E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68A04925E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift */; }; DB68A05D25E9055900CFDF14 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = DB68A05C25E9055900CFDF14 /* Settings.bundle */; }; @@ -358,7 +358,7 @@ DB59F0FD25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusProvider+UITableViewDelegate.swift"; sourceTree = ""; }; DB59F10325EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewCellHeightCacheableContainer.swift; sourceTree = ""; }; DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Poll.swift"; sourceTree = ""; }; - DB59F11725EFA35B001F1DAB /* VoteProgressStripView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoteProgressStripView.swift; sourceTree = ""; }; + DB59F11725EFA35B001F1DAB /* StripProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StripProgressView.swift; sourceTree = ""; }; DB68586325E619B700F0A850 /* NSKeyValueObservation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSKeyValueObservation.swift; sourceTree = ""; }; DB68A04925E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DarkContentStatusBarStyleNavigationController.swift; sourceTree = ""; }; DB68A05C25E9055900CFDF14 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = ""; }; @@ -527,7 +527,6 @@ isa = PBXGroup; children = ( 2D152A8B25C295CC009AA50C /* StatusView.swift */, - DB59F11725EFA35B001F1DAB /* VoteProgressStripView.swift */, ); path = Content; sourceTree = ""; @@ -684,6 +683,7 @@ 2D42FF8325C82245004A627A /* Button */, 2D42FF7C25C82207004A627A /* ToolBar */, DB9D6C1325E4F97A0051B173 /* Container */, + DBA9B90325F1D4420012E7B6 /* Control */, 2D152A8A25C295B8009AA50C /* Content */, DB1D187125EF5BBD003F1F23 /* TableView */, 2D7631A625C1533800929FB9 /* TableviewCell */, @@ -1119,6 +1119,14 @@ path = ViewModel; sourceTree = ""; }; + DBA9B90325F1D4420012E7B6 /* Control */ = { + isa = PBXGroup; + children = ( + DB59F11725EFA35B001F1DAB /* StripProgressView.swift */, + ); + path = Control; + sourceTree = ""; + }; DBE0821A25CD382900FD6BBD /* Register */ = { isa = PBXGroup; children = ( @@ -1523,7 +1531,7 @@ DBD9149025DF6D8D00903DFD /* APIService+Onboarding.swift in Sources */, DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */, 2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */, - DB59F11825EFA35B001F1DAB /* VoteProgressStripView.swift in Sources */, + DB59F11825EFA35B001F1DAB /* StripProgressView.swift in Sources */, DB59F10425EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift in Sources */, DBE0822425CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift in Sources */, 2DF75B9B25D0E27500694EC8 /* StatusProviderFacade.swift in Sources */, diff --git a/Mastodon/Diffiable/Section/PollSection.swift b/Mastodon/Diffiable/Section/PollSection.swift index eff868a7..54753a7a 100644 --- a/Mastodon/Diffiable/Section/PollSection.swift +++ b/Mastodon/Diffiable/Section/PollSection.swift @@ -76,7 +76,7 @@ extension PollSection { cell.optionPercentageLabel.isHidden = false cell.optionPercentageLabel.text = String(Int(100 * percentage)) + "%" cell.voteProgressStripView.tintColor = voted ? Asset.Colors.Background.Poll.highlight.color : Asset.Colors.Background.Poll.disabled.color - cell.voteProgressStripView.progress.send(CGFloat(percentage)) + cell.voteProgressStripView.setProgress(CGFloat(percentage), animated: true) } cell.voteState = state diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index a9c07202..d442a23e 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -302,8 +302,6 @@ extension StatusSection { return Double(option.votesCount?.intValue ?? 0) / Double(poll.votesCount.intValue) }() let voted = votedOptions.isEmpty ? true : votedOptions.contains(option) -// let voted = true -// let percentage: Double = Double.random(in: 0..<1) return .reveal(voted: voted, percentage: percentage) }() return PollItem.Attribute(selectState: selectState, voteState: voteState) diff --git a/Mastodon/Scene/Share/View/Content/VoteProgressStripView.swift b/Mastodon/Scene/Share/View/Content/VoteProgressStripView.swift deleted file mode 100644 index 51016da3..00000000 --- a/Mastodon/Scene/Share/View/Content/VoteProgressStripView.swift +++ /dev/null @@ -1,139 +0,0 @@ -// -// VoteProgressStripView.swift -// Mastodon -// -// Created by MainasuK Cirno on 2021-3-3. -// - -import UIKit -import Combine - -final class VoteProgressStripView: UIView { - - var disposeBag = Set() - - private lazy var stripLayer: CAShapeLayer = { - let shapeLayer = CAShapeLayer() - shapeLayer.lineCap = .round - shapeLayer.fillColor = tintColor.cgColor - shapeLayer.strokeColor = UIColor.clear.cgColor - return shapeLayer - }() - - let progressMaskLayer: CAShapeLayer = { - let shapeLayer = CAShapeLayer() - shapeLayer.fillColor = UIColor.red.cgColor - return shapeLayer - }() - - let progress = CurrentValueSubject(0.0) - - override init(frame: CGRect) { - super.init(frame: frame) - _init() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - _init() - } - -} - -extension VoteProgressStripView { - - private func _init() { - updateLayerPath() - - layer.addSublayer(stripLayer) - - progress - .receive(on: DispatchQueue.main) - .sink { [weak self] progress in - guard let self = self else { return } - UIView.animate(withDuration: 0.33) { - self.updateLayerPath() - } - } - .store(in: &disposeBag) - } - - override func layoutSubviews() { - super.layoutSubviews() - updateLayerPath() - } - -} - -extension VoteProgressStripView { - private func updateLayerPath() { - guard bounds != .zero else { return } - - stripLayer.frame = bounds - stripLayer.fillColor = tintColor.cgColor - - stripLayer.path = { - let path = UIBezierPath(roundedRect: bounds, cornerRadius: 0) - return path.cgPath - }() - - - progressMaskLayer.path = { - var rect = bounds - let newWidth = progress.value * rect.width - let widthChanged = rect.width - newWidth - rect.size.width = newWidth - switch UIApplication.shared.userInterfaceLayoutDirection { - case .rightToLeft: - rect.origin.x += widthChanged - default: - break - } - let path = UIBezierPath(rect: rect) - return path.cgPath - }() - stripLayer.mask = progressMaskLayer - } - -} - - -#if DEBUG -import SwiftUI - -struct VoteProgressStripView_Previews: PreviewProvider { - - static var previews: some View { - Group { - UIViewPreview() { - VoteProgressStripView() - } - .frame(width: 100, height: 44) - .padding() - .background(Color.black) - .previewLayout(.sizeThatFits) - UIViewPreview() { - let bar = VoteProgressStripView() - bar.tintColor = .white - bar.progress.value = 0.5 - return bar - } - .frame(width: 100, height: 44) - .padding() - .background(Color.black) - .previewLayout(.sizeThatFits) - UIViewPreview() { - let bar = VoteProgressStripView() - bar.tintColor = .white - bar.progress.value = 1.0 - return bar - } - .frame(width: 100, height: 44) - .padding() - .background(Color.black) - .previewLayout(.sizeThatFits) - } - } - -} -#endif diff --git a/Mastodon/Scene/Share/View/Control/StripProgressView.swift b/Mastodon/Scene/Share/View/Control/StripProgressView.swift new file mode 100644 index 00000000..ae3f8673 --- /dev/null +++ b/Mastodon/Scene/Share/View/Control/StripProgressView.swift @@ -0,0 +1,165 @@ +// +// StripProgressView.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-3-3. +// + +import os.log +import UIKit +import Combine + +private final class StripProgressLayer: CALayer { + + var tintColor: UIColor = .black + @NSManaged var progress: CGFloat + + override class func needsDisplay(forKey key: String) -> Bool { + switch key { + case "progress": + return true + default: + return super.needsDisplay(forKey: key) + } + } + + override func display() { + let progress = presentation()?.progress ?? self.progress + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: progress: %.2f", ((#file as NSString).lastPathComponent), #line, #function, progress) + + UIGraphicsBeginImageContextWithOptions(bounds.size, false, 0) + guard let context = UIGraphicsGetCurrentContext() else { + assertionFailure() + return + } + context.clear(bounds) + + var rect = bounds + let newWidth = CGFloat(progress) * rect.width + let widthChanged = rect.width - newWidth + rect.size.width = newWidth + switch UIApplication.shared.userInterfaceLayoutDirection { + case .rightToLeft: + rect.origin.x += widthChanged + default: + break + } + let path = UIBezierPath(rect: rect) + context.setFillColor(tintColor.cgColor) + context.addPath(path.cgPath) + context.fillPath() + + contents = UIGraphicsGetImageFromCurrentImageContext()?.cgImage + UIGraphicsEndImageContext() + } + +} + +final class StripProgressView: UIView { + + var disposeBag = Set() + + private let stripProgressLayer: StripProgressLayer = { + let layer = StripProgressLayer() + return layer + }() + + override var tintColor: UIColor! { + didSet { + stripProgressLayer.tintColor = tintColor + setNeedsDisplay() + } + } + + func setProgress(_ progress: CGFloat, animated: Bool) { + stripProgressLayer.removeAnimation(forKey: "progressAnimationKey") + if animated { + let animation = CABasicAnimation(keyPath: "progress") + animation.fromValue = stripProgressLayer.presentation()?.progress ?? stripProgressLayer.progress + animation.toValue = progress + animation.duration = 0.33 + animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) + animation.isRemovedOnCompletion = true + stripProgressLayer.add(animation, forKey: "progressAnimationKey") + stripProgressLayer.progress = progress + } else { + stripProgressLayer.progress = progress + stripProgressLayer.setNeedsDisplay() + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + _init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } + +} + +extension StripProgressView { + + private func _init() { + layer.addSublayer(stripProgressLayer) + updateLayerPath() + } + + override func layoutSubviews() { + super.layoutSubviews() + updateLayerPath() + } + +} + +extension StripProgressView { + private func updateLayerPath() { + guard bounds != .zero else { return } + + stripProgressLayer.frame = bounds + stripProgressLayer.tintColor = tintColor + stripProgressLayer.setNeedsDisplay() + } +} + +#if DEBUG +import SwiftUI + +struct VoteProgressStripView_Previews: PreviewProvider { + + static var previews: some View { + Group { + UIViewPreview() { + StripProgressView() + } + .frame(width: 100, height: 44) + .padding() + .background(Color.black) + .previewLayout(.sizeThatFits) + UIViewPreview() { + let bar = StripProgressView() + bar.tintColor = .white + bar.setProgress(0.5, animated: false) + return bar + } + .frame(width: 100, height: 44) + .padding() + .background(Color.black) + .previewLayout(.sizeThatFits) + UIViewPreview() { + let bar = StripProgressView() + bar.tintColor = .white + bar.setProgress(1.0, animated: false) + return bar + } + .frame(width: 100, height: 44) + .padding() + .background(Color.black) + .previewLayout(.sizeThatFits) + } + } + +} +#endif diff --git a/Mastodon/Scene/Share/View/TableviewCell/PollOptionTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/PollOptionTableViewCell.swift index 32f59964..455e5fb9 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/PollOptionTableViewCell.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/PollOptionTableViewCell.swift @@ -20,8 +20,8 @@ final class PollOptionTableViewCell: UITableViewCell { var voteState: PollItem.Attribute.VoteState? let roundedBackgroundView = UIView() - let voteProgressStripView: VoteProgressStripView = { - let view = VoteProgressStripView() + let voteProgressStripView: StripProgressView = { + let view = StripProgressView() view.tintColor = Asset.Colors.Background.Poll.highlight.color return view }()