From 37ae4ef3604c660647681ee3f73428debe4ad04f Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Thu, 1 Nov 2018 13:53:58 -0400 Subject: [PATCH] Add typing indicator animation. --- .../Cells/TypingIndicatorCell.swift | 1 - Signal/src/views/TypingIndicatorView.swift | 102 +++++++++++++++--- 2 files changed, 90 insertions(+), 13 deletions(-) diff --git a/Signal/src/ViewControllers/ConversationView/Cells/TypingIndicatorCell.swift b/Signal/src/ViewControllers/ConversationView/Cells/TypingIndicatorCell.swift index 670d10261..27bac0dae 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/TypingIndicatorCell.swift +++ b/Signal/src/ViewControllers/ConversationView/Cells/TypingIndicatorCell.swift @@ -56,7 +56,6 @@ public class TypingIndicatorCell: ConversationViewCell { bubbleView.bubbleColor = conversationStyle.bubbleColor(isIncoming: true) typingIndicatorView.startAnimation() - typingIndicatorView.addBackgroundView(withBackgroundColor: UIColor.red) viewConstraints.append(contentsOf: [ bubbleView.autoPinEdge(toSuperviewEdge: .leading, withInset: conversationStyle.gutterLeading), diff --git a/Signal/src/views/TypingIndicatorView.swift b/Signal/src/views/TypingIndicatorView.swift index f78cd65df..2f14ac43d 100644 --- a/Signal/src/views/TypingIndicatorView.swift +++ b/Signal/src/views/TypingIndicatorView.swift @@ -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() } } }