feat: add animation for progress bar value change

This commit is contained in:
CMK 2021-03-05 12:11:04 +08:00
parent 06aac878c8
commit 58c8eaabe8
6 changed files with 180 additions and 148 deletions

View File

@ -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 = "<group>"; };
DB59F10325EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewCellHeightCacheableContainer.swift; sourceTree = "<group>"; };
DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Poll.swift"; sourceTree = "<group>"; };
DB59F11725EFA35B001F1DAB /* VoteProgressStripView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoteProgressStripView.swift; sourceTree = "<group>"; };
DB59F11725EFA35B001F1DAB /* StripProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StripProgressView.swift; sourceTree = "<group>"; };
DB68586325E619B700F0A850 /* NSKeyValueObservation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSKeyValueObservation.swift; sourceTree = "<group>"; };
DB68A04925E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DarkContentStatusBarStyleNavigationController.swift; sourceTree = "<group>"; };
DB68A05C25E9055900CFDF14 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = "<group>"; };
@ -527,7 +527,6 @@
isa = PBXGroup;
children = (
2D152A8B25C295CC009AA50C /* StatusView.swift */,
DB59F11725EFA35B001F1DAB /* VoteProgressStripView.swift */,
);
path = Content;
sourceTree = "<group>";
@ -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 = "<group>";
};
DBA9B90325F1D4420012E7B6 /* Control */ = {
isa = PBXGroup;
children = (
DB59F11725EFA35B001F1DAB /* StripProgressView.swift */,
);
path = Control;
sourceTree = "<group>";
};
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 */,

View File

@ -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

View File

@ -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)

View File

@ -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<AnyCancellable>()
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<CGFloat, Never>(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

View File

@ -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<AnyCancellable>()
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

View File

@ -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
}()