|
|
|
@ -31,9 +31,9 @@
|
|
|
|
|
super.init(frame: .zero)
|
|
|
|
|
|
|
|
|
|
// init(arrangedSubviews:...) is not a designated initializer.
|
|
|
|
|
addArrangedSubview(dot1)
|
|
|
|
|
addArrangedSubview(dot2)
|
|
|
|
|
addArrangedSubview(dot3)
|
|
|
|
|
for dot in dots() {
|
|
|
|
|
addArrangedSubview(dot)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.axis = .horizontal
|
|
|
|
|
self.spacing = kDotMaxHSpacing
|
|
|
|
@ -45,12 +45,22 @@
|
|
|
|
|
return CGSize(width: TypingIndicatorView.kMaxRadiusPt * 3 + kDotMaxHSpacing * 2, height: TypingIndicatorView.kMaxRadiusPt)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func dots() -> [DotView] {
|
|
|
|
|
return [dot1, dot2, dot3]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@objc
|
|
|
|
|
public func startAnimation() {
|
|
|
|
|
for dot in dots() {
|
|
|
|
|
dot.startAnimation()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@objc
|
|
|
|
|
public func stopAnimation() {
|
|
|
|
|
for dot in dots() {
|
|
|
|
|
dot.stopAnimation()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private enum DotType {
|
|
|
|
@ -82,18 +92,86 @@
|
|
|
|
|
autoSetDimension(.width, toSize: kMaxRadiusPt)
|
|
|
|
|
autoSetDimension(.height, toSize: kMaxRadiusPt)
|
|
|
|
|
|
|
|
|
|
self.layer.addSublayer(shapeLayer)
|
|
|
|
|
|
|
|
|
|
updateLayer()
|
|
|
|
|
layer.addSublayer(shapeLayer)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func updateLayer() {
|
|
|
|
|
shapeLayer.fillColor = UIColor.ows_signalBlue.cgColor
|
|
|
|
|
|
|
|
|
|
let margin = (TypingIndicatorView.kMaxRadiusPt - TypingIndicatorView.kMinRadiusPt) * 0.5
|
|
|
|
|
let bezierPath = UIBezierPath(ovalIn: CGRect(x: margin, y: margin, width: TypingIndicatorView.kMinRadiusPt, height: TypingIndicatorView.kMinRadiusPt))
|
|
|
|
|
shapeLayer.path = bezierPath.cgPath
|
|
|
|
|
fileprivate func startAnimation() {
|
|
|
|
|
stopAnimation()
|
|
|
|
|
|
|
|
|
|
let timeIncrement: CFTimeInterval = 0.15
|
|
|
|
|
var colorValues = [CGColor]()
|
|
|
|
|
var pathValues = [CGPath]()
|
|
|
|
|
var keyTimes = [CFTimeInterval]()
|
|
|
|
|
var animationDuration: CFTimeInterval = 0
|
|
|
|
|
|
|
|
|
|
let addDotKeyFrame = { (keyFrameTime: CFTimeInterval, progress: CGFloat) in
|
|
|
|
|
let dotColor = UIColor(rgbHex: 0x636467).withAlphaComponent(CGFloatLerp(0.4, 1.0, progress))
|
|
|
|
|
colorValues.append(dotColor.cgColor)
|
|
|
|
|
let radius = CGFloatLerp(TypingIndicatorView.kMinRadiusPt, TypingIndicatorView.kMaxRadiusPt, progress)
|
|
|
|
|
let margin = (TypingIndicatorView.kMaxRadiusPt - radius) * 0.5
|
|
|
|
|
let bezierPath = UIBezierPath(ovalIn: CGRect(x: margin, y: margin, width: radius, height: radius))
|
|
|
|
|
pathValues.append(bezierPath.cgPath)
|
|
|
|
|
|
|
|
|
|
keyTimes.append(keyFrameTime)
|
|
|
|
|
animationDuration = max(animationDuration, keyFrameTime)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// All animations in the group apparently need to have the same number
|
|
|
|
|
// of keyframes, and use the same timing.
|
|
|
|
|
switch dotType {
|
|
|
|
|
case .dotType1:
|
|
|
|
|
addDotKeyFrame(0 * timeIncrement, 0.0)
|
|
|
|
|
addDotKeyFrame(1 * timeIncrement, 0.5)
|
|
|
|
|
addDotKeyFrame(2 * timeIncrement, 1.0)
|
|
|
|
|
addDotKeyFrame(3 * timeIncrement, 0.5)
|
|
|
|
|
addDotKeyFrame(4 * timeIncrement, 0.0)
|
|
|
|
|
addDotKeyFrame(5 * timeIncrement, 0.0)
|
|
|
|
|
addDotKeyFrame(6 * timeIncrement, 0.0)
|
|
|
|
|
addDotKeyFrame(10 * timeIncrement, 0.0)
|
|
|
|
|
break
|
|
|
|
|
case .dotType2:
|
|
|
|
|
addDotKeyFrame(0 * timeIncrement, 0.0)
|
|
|
|
|
addDotKeyFrame(1 * timeIncrement, 0.0)
|
|
|
|
|
addDotKeyFrame(2 * timeIncrement, 0.5)
|
|
|
|
|
addDotKeyFrame(3 * timeIncrement, 1.0)
|
|
|
|
|
addDotKeyFrame(4 * timeIncrement, 0.5)
|
|
|
|
|
addDotKeyFrame(5 * timeIncrement, 0.0)
|
|
|
|
|
addDotKeyFrame(6 * timeIncrement, 0.0)
|
|
|
|
|
addDotKeyFrame(10 * timeIncrement, 0.0)
|
|
|
|
|
break
|
|
|
|
|
case .dotType3:
|
|
|
|
|
addDotKeyFrame(0 * timeIncrement, 0.0)
|
|
|
|
|
addDotKeyFrame(1 * timeIncrement, 0.0)
|
|
|
|
|
addDotKeyFrame(2 * timeIncrement, 0.0)
|
|
|
|
|
addDotKeyFrame(3 * timeIncrement, 0.5)
|
|
|
|
|
addDotKeyFrame(4 * timeIncrement, 1.0)
|
|
|
|
|
addDotKeyFrame(5 * timeIncrement, 0.5)
|
|
|
|
|
addDotKeyFrame(6 * timeIncrement, 0.0)
|
|
|
|
|
addDotKeyFrame(10 * timeIncrement, 0.0)
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let makeAnimation: (String, [Any]) -> CAKeyframeAnimation = { (keyPath, values) in
|
|
|
|
|
let animation = CAKeyframeAnimation()
|
|
|
|
|
animation.keyPath = keyPath
|
|
|
|
|
animation.values = values
|
|
|
|
|
animation.duration = animationDuration
|
|
|
|
|
return animation
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let groupAnimation = CAAnimationGroup()
|
|
|
|
|
groupAnimation.animations = [
|
|
|
|
|
makeAnimation("fillColor", colorValues),
|
|
|
|
|
makeAnimation("path", pathValues)
|
|
|
|
|
]
|
|
|
|
|
groupAnimation.duration = animationDuration
|
|
|
|
|
groupAnimation.repeatCount = MAXFLOAT
|
|
|
|
|
|
|
|
|
|
shapeLayer.add(groupAnimation, forKey: UUID().uuidString)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fileprivate func stopAnimation() {
|
|
|
|
|
shapeLayer.removeAllAnimations()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|