Update password setup animations

This commit is contained in:
Ali 2019-11-12 17:40:24 +04:00
parent acf59258e0
commit c08b3c6f81
3 changed files with 191 additions and 112 deletions

View File

@ -14,7 +14,7 @@ private final class ManagedAnimationState {
let frameCount: Int
let fps: Double
var startTime: Double?
var relativeTime: Double = 0.0
var frameIndex: Int?
private let renderContext: DrawingContext
@ -73,7 +73,8 @@ class ManagedAnimationNode: ASDisplayNode {
private let imageNode: ASImageNode
private let displayLink: CADisplayLink
private var state: ManagedAnimationState?
fileprivate var state: ManagedAnimationState?
fileprivate var trackStack: [ManagedAnimationItem] = []
init(size: CGSize) {
self.intrinsicSize = size
@ -110,30 +111,40 @@ class ManagedAnimationNode: ASDisplayNode {
}
}
private func updateAnimation() {
private func advanceState() {
guard !self.trackStack.isEmpty else {
return
}
let item = self.trackStack.removeFirst()
if let state = self.state, state.item.name == item.name {
self.state = ManagedAnimationState(displaySize: self.intrinsicSize, item: item, current: state)
} else {
self.state = ManagedAnimationState(displaySize: self.intrinsicSize, item: item, current: nil)
}
}
fileprivate func updateAnimation() {
if self.state == nil {
self.advanceState()
}
guard let state = self.state else {
return
}
let timestamp = CACurrentMediaTime()
var startTime: Double
if let current = state.startTime {
startTime = current
} else {
startTime = timestamp
state.startTime = startTime
}
let fps = state.fps
let frameRange = state.item.frames
let duration: Double = 0.3
var t = (timestamp - startTime) / duration
var t = state.relativeTime / duration
t = max(0.0, t)
t = min(1.0, t)
let frameOffset = Int(Double(frameRange.startFrame) * (1.0 - t) + Double(frameRange.startFrame) * t)
let lowerBound = min(frameRange.startFrame, state.frameCount - 1)
let upperBound = min(frameRange.endFrame, state.frameCount - 1)
let frameOffset = Int(Double(frameRange.startFrame) * (1.0 - t) + Double(frameRange.endFrame) * t)
let lowerBound: Int = 0
let upperBound = state.frameCount - 1
let frameIndex = max(lowerBound, min(upperBound, frameOffset))
if state.frameIndex != frameIndex {
@ -142,14 +153,19 @@ class ManagedAnimationNode: ASDisplayNode {
self.imageNode.image = image
}
}
var animationAdvancement: Double = 1.0 / 60.0
animationAdvancement *= Double(self.trackStack.count + 1)
state.relativeTime += animationAdvancement
if state.relativeTime >= duration {
self.advanceState()
}
}
func trackTo(item: ManagedAnimationItem, frameIndex: Int) {
if let state = self.state, state.item.name == item.name {
self.state = ManagedAnimationState(displaySize: self.intrinsicSize, item: item, current: state)
} else {
self.state = ManagedAnimationState(displaySize: self.intrinsicSize, item: item, current: nil)
}
func trackTo(item: ManagedAnimationItem) {
self.trackStack.append(item)
self.updateAnimation()
}
}
@ -222,62 +238,93 @@ enum ManagedMonkeyAnimationState: Equatable {
)*/
final class ManagedMonkeyAnimationNode: ManagedAnimationNode {
private var state: ManagedMonkeyAnimationState = .idle
private var monkeyState: ManagedMonkeyAnimationState = .idle
init() {
super.init(size: CGSize(width: 136.0, height: 136.0))
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyIdle", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 0)), frameIndex: 0)
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyIdle", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 0)))
}
func setState(_ state: ManagedMonkeyAnimationState) {
let previousState = self.state
self.state = state
func setState(_ monkeyState: ManagedMonkeyAnimationState) {
let previousState = self.monkeyState
self.monkeyState = monkeyState
func enqueueTracking(_ value: CGFloat) {
let lowerBound = 18
let upperBound = 160
let frameIndex = lowerBound + Int(value * CGFloat(upperBound - lowerBound))
if let state = self.state, state.item.name == "TwoFactorSetupMonkeyTracking" {
let item = ManagedAnimationItem(name: "TwoFactorSetupMonkeyTracking", frames: ManagedAnimationFrameRange(startFrame: state.frameIndex ?? 0, endFrame: frameIndex))
self.state = ManagedAnimationState(displaySize: self.intrinsicSize, item: item, current: state)
self.updateAnimation()
} else {
self.trackStack = self.trackStack.filter {
$0.name != "TwoFactorSetupMonkeyTracking"
}
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyTracking", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: frameIndex)))
}
}
func enqueueClearTracking() {
if let state = self.state, state.item.name == "TwoFactorSetupMonkeyTracking" {
let item = ManagedAnimationItem(name: "TwoFactorSetupMonkeyTracking", frames: ManagedAnimationFrameRange(startFrame: state.frameIndex ?? 0, endFrame: 0))
self.state = ManagedAnimationState(displaySize: self.intrinsicSize, item: item, current: state)
self.updateAnimation()
}
}
switch previousState {
case .idle:
switch state {
switch monkeyState {
case .idle:
break
case .eyesClosed:
break
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyClose", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 41)))
case .peeking:
break
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyCloseAndPeek", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 41)))
case let .tracking(value):
break
enqueueTracking(value)
}
case .eyesClosed:
switch state {
switch monkeyState {
case .idle:
break
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyClose", frames: ManagedAnimationFrameRange(startFrame: 41, endFrame: 0)))
case .eyesClosed:
break
case .peeking:
break
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyPeek", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 14)))
case let .tracking(value):
break
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyClose", frames: ManagedAnimationFrameRange(startFrame: 41, endFrame: 0)))
enqueueTracking(value)
}
case .peeking:
switch state {
switch monkeyState {
case .idle:
break
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyCloseAndPeek", frames: ManagedAnimationFrameRange(startFrame: 41, endFrame: 0)))
case .eyesClosed:
break
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyPeek", frames: ManagedAnimationFrameRange(startFrame: 14, endFrame: 0)))
case .peeking:
break
case let .tracking(value):
break
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyCloseAndPeek", frames: ManagedAnimationFrameRange(startFrame: 41, endFrame: 0)))
enqueueTracking(value)
}
case let .tracking(previousValue):
switch state {
case let .tracking(currentValue):
switch monkeyState {
case .idle:
break
enqueueClearTracking()
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyIdle", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 0)))
case .eyesClosed:
break
enqueueClearTracking()
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyClose", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 41)))
case .peeking:
break
enqueueClearTracking()
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyCloseAndPeek", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 41)))
case let .tracking(value):
break
if abs(currentValue - value) > CGFloat.ulpOfOne {
enqueueTracking(value)
}
}
}
}

View File

@ -20,8 +20,6 @@ public enum TwoFactorDataInputMode {
case passwordHint(password: String)
}
public final class TwoFactorDataInputScreen: ViewController {
private let context: AccountContext
private var presentationData: PresentationData
@ -471,7 +469,7 @@ private func generateTextHiddenImage(color: UIColor, on: Bool) -> UIImage? {
private final class TwoFactorDataInputTextNode: ASDisplayNode, UITextFieldDelegate {
private let theme: PresentationTheme
let mode: TwoFactorDataInputTextNodeType
private let focused: (TwoFactorDataInputTextNode) -> Void
private let focusUpdated: (TwoFactorDataInputTextNode, Bool) -> Void
private let next: (TwoFactorDataInputTextNode) -> Void
private let updated: (TwoFactorDataInputTextNode) -> Void
private let toggleTextHidden: (TwoFactorDataInputTextNode) -> Void
@ -481,6 +479,12 @@ private final class TwoFactorDataInputTextNode: ASDisplayNode, UITextFieldDelega
private let hideButtonNode: HighlightableButtonNode
private let clearButtonNode: HighlightableButtonNode
fileprivate var ignoreTextChanged: Bool = false
var isFocused: Bool {
return self.inputNode.textField.isFirstResponder
}
var text: String {
get {
return self.inputNode.textField.text ?? ""
@ -490,10 +494,10 @@ private final class TwoFactorDataInputTextNode: ASDisplayNode, UITextFieldDelega
}
}
init(theme: PresentationTheme, mode: TwoFactorDataInputTextNodeType, placeholder: String, focused: @escaping (TwoFactorDataInputTextNode) -> Void, next: @escaping (TwoFactorDataInputTextNode) -> Void, updated: @escaping (TwoFactorDataInputTextNode) -> Void, toggleTextHidden: @escaping (TwoFactorDataInputTextNode) -> Void) {
init(theme: PresentationTheme, mode: TwoFactorDataInputTextNodeType, placeholder: String, focusUpdated: @escaping (TwoFactorDataInputTextNode, Bool) -> Void, next: @escaping (TwoFactorDataInputTextNode) -> Void, updated: @escaping (TwoFactorDataInputTextNode) -> Void, toggleTextHidden: @escaping (TwoFactorDataInputTextNode) -> Void) {
self.theme = theme
self.mode = mode
self.focused = focused
self.focusUpdated = focusUpdated
self.next = next
self.updated = updated
self.toggleTextHidden = toggleTextHidden
@ -564,11 +568,21 @@ private final class TwoFactorDataInputTextNode: ASDisplayNode, UITextFieldDelega
func textFieldDidBeginEditing(_ textField: UITextField) {
let text = self.text
let isEmpty = text.isEmpty
self.focused(self)
if self.inputNode.textField.isSecureTextEntry {
let previousIgnoreTextChanged = self.ignoreTextChanged
self.ignoreTextChanged = true
self.inputNode.textField.text = ""
self.inputNode.textField.insertText(text + " ")
self.inputNode.textField.deleteBackward()
self.ignoreTextChanged = previousIgnoreTextChanged
}
self.focusUpdated(self, true)
}
func textFieldDidEndEditing(_ textField: UITextField) {
self.focusUpdated(self, false)
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
@ -581,13 +595,15 @@ private final class TwoFactorDataInputTextNode: ASDisplayNode, UITextFieldDelega
}
@objc private func textFieldChanged(_ textField: UITextField) {
switch self.mode {
case .password:
break
default:
self.clearButtonNode.isHidden = self.text.isEmpty
if !self.ignoreTextChanged {
switch self.mode {
case .password:
break
default:
self.clearButtonNode.isHidden = self.text.isEmpty
}
self.updated(self)
}
self.updated(self)
}
@objc private func hidePressed() {
@ -615,7 +631,19 @@ private final class TwoFactorDataInputTextNode: ASDisplayNode, UITextFieldDelega
func updateTextHidden(_ value: Bool) {
self.hideButtonNode.setImage(generateTextHiddenImage(color: self.theme.actionSheet.inputClearButtonColor, on: !value), for: [])
let text = self.inputNode.textField.text ?? ""
self.inputNode.textField.isSecureTextEntry = value
if value {
if self.inputNode.textField.isFirstResponder {
let previousIgnoreTextChanged = self.ignoreTextChanged
self.ignoreTextChanged = true
self.inputNode.textField.text = ""
self.inputNode.textField.becomeFirstResponder()
self.inputNode.textField.insertText(text + " ")
self.inputNode.textField.deleteBackward()
self.ignoreTextChanged = previousIgnoreTextChanged
}
}
}
}
@ -694,7 +722,7 @@ private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, UIS
var inputNodes: [TwoFactorDataInputTextNode] = []
var next: ((TwoFactorDataInputTextNode) -> Void)?
var focused: ((TwoFactorDataInputTextNode) -> Void)?
var focusUpdated: ((TwoFactorDataInputTextNode, Bool) -> Void)?
var updated: ((TwoFactorDataInputTextNode) -> Void)?
var toggleTextHidden: ((TwoFactorDataInputTextNode) -> Void)?
@ -707,8 +735,8 @@ private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, UIS
changeEmailActionText = ""
resendCodeActionText = ""
inputNodes = [
TwoFactorDataInputTextNode(theme: presentationData.theme, mode: .password(confirmation: false), placeholder: presentationData.strings.TwoFactorSetup_Password_PlaceholderPassword, focused: { node in
focused?(node)
TwoFactorDataInputTextNode(theme: presentationData.theme, mode: .password(confirmation: false), placeholder: presentationData.strings.TwoFactorSetup_Password_PlaceholderPassword, focusUpdated: { node, focused in
focusUpdated?(node, focused)
}, next: { node in
next?(node)
}, updated: { node in
@ -716,8 +744,8 @@ private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, UIS
}, toggleTextHidden: { node in
toggleTextHidden?(node)
}),
TwoFactorDataInputTextNode(theme: presentationData.theme, mode: .password(confirmation: true), placeholder: presentationData.strings.TwoFactorSetup_Password_PlaceholderConfirmPassword, focused: { node in
focused?(node)
TwoFactorDataInputTextNode(theme: presentationData.theme, mode: .password(confirmation: true), placeholder: presentationData.strings.TwoFactorSetup_Password_PlaceholderConfirmPassword, focusUpdated: { node, focused in
focusUpdated?(node, focused)
}, next: { node in
next?(node)
}, updated: { node in
@ -734,8 +762,8 @@ private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, UIS
changeEmailActionText = ""
resendCodeActionText = ""
inputNodes = [
TwoFactorDataInputTextNode(theme: presentationData.theme, mode: .email, placeholder: presentationData.strings.TwoFactorSetup_Email_Placeholder, focused: { node in
focused?(node)
TwoFactorDataInputTextNode(theme: presentationData.theme, mode: .email, placeholder: presentationData.strings.TwoFactorSetup_Email_Placeholder, focusUpdated: { node, focused in
focusUpdated?(node, focused)
}, next: { node in
next?(node)
}, updated: { node in
@ -761,8 +789,8 @@ private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, UIS
changeEmailActionText = presentationData.strings.TwoFactorSetup_EmailVerification_ChangeAction
resendCodeActionText = presentationData.strings.TwoFactorSetup_EmailVerification_ResendAction
inputNodes = [
TwoFactorDataInputTextNode(theme: presentationData.theme, mode: .code, placeholder: presentationData.strings.TwoFactorSetup_EmailVerification_Placeholder, focused: { node in
focused?(node)
TwoFactorDataInputTextNode(theme: presentationData.theme, mode: .code, placeholder: presentationData.strings.TwoFactorSetup_EmailVerification_Placeholder, focusUpdated: { node, focused in
focusUpdated?(node, focused)
}, next: { node in
next?(node)
}, updated: { node in
@ -781,8 +809,8 @@ private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, UIS
changeEmailActionText = ""
resendCodeActionText = ""
inputNodes = [
TwoFactorDataInputTextNode(theme: presentationData.theme, mode: .hint, placeholder: presentationData.strings.TwoFactorSetup_Hint_Placeholder, focused: { node in
focused?(node)
TwoFactorDataInputTextNode(theme: presentationData.theme, mode: .hint, placeholder: presentationData.strings.TwoFactorSetup_Hint_Placeholder, focusUpdated: { node, focused in
focusUpdated?(node, focused)
}, next: { node in
next?(node)
}, updated: { node in
@ -917,67 +945,64 @@ private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, UIS
}
}
}
focused = { [weak self] node in
DispatchQueue.main.async {
guard let strongSelf = self else {
return
}
}
}
var textHidden = true
let updateAnimations: () -> Void = { [weak self] in
guard let strongSelf = self else {
return
}
let hasText = strongSelf.inputNodes.contains(where: { !$0.text.isEmpty })
/*switch strongSelf.mode {
switch strongSelf.mode {
case .password:
if !hasText {
if strongSelf.animationNode.currentItemName == animationPeek.name {
strongSelf.animationNode.switchTo(animationHideOutro)
strongSelf.animationNode.switchTo(animationIdle)
if strongSelf.inputNodes[1].isFocused {
let textLength = strongSelf.inputNodes[1].text.count
let maxWidth = strongSelf.inputNodes[1].bounds.width
let textNode = ImmediateTextNode()
textNode.attributedText = NSAttributedString(string: strongSelf.inputNodes[1].text, font: Font.regular(17.0), textColor: .black)
let textSize = textNode.updateLayout(CGSize(width: 1000.0, height: 100.0))
let maxTextLength = 20
var trackingOffset = textSize.width / maxWidth
trackingOffset = max(0.0, min(1.0, trackingOffset))
strongSelf.monkeyNode?.setState(.tracking(trackingOffset))
} else if strongSelf.inputNodes[0].isFocused {
let hasText = !strongSelf.inputNodes[0].text.isEmpty
if !hasText {
strongSelf.monkeyNode?.setState(.idle)
} else if textHidden {
strongSelf.monkeyNode?.setState(.eyesClosed)
} else {
strongSelf.animationNode.switchTo(animationIdle)
}
} else if textHidden {
if strongSelf.animationNode.currentItemName == animationPeek.name {
strongSelf.animationNode.switchTo(animationHideNoIntro)
} else {
strongSelf.animationNode.switchTo(animationHide)
strongSelf.monkeyNode?.setState(.peeking)
}
} else {
if strongSelf.animationNode.currentItemName != animationPeek.name {
if strongSelf.animationNode.currentItemName == animationHide.name {
strongSelf.animationNode.switchTo(animationPeek, noOutro: true)
} else if strongSelf.animationNode.currentItemName == animationIdle.name {
strongSelf.animationNode.switchTo(animationHideNoOutro)
strongSelf.animationNode.switchTo(animationPeek)
} else {
strongSelf.animationNode.switchTo(animationPeek, noOutro: strongSelf.animationNode.currentItemName == animationHide.name)
}
}
strongSelf.monkeyNode?.setState(.idle)
}
case .emailAddress:
let textLength = strongSelf.inputNodes[0].text.count
let maxWidth = strongSelf.inputNodes[0].bounds.width
if textLength == 0 || maxWidth.isZero {
strongSelf.animationNode.trackTo(frameIndex: 0)
} else {
if strongSelf.inputNodes[0].isFocused {
let textLength = strongSelf.inputNodes[0].text.count
let maxWidth = strongSelf.inputNodes[0].bounds.width
let textNode = ImmediateTextNode()
textNode.attributedText = NSAttributedString(string: strongSelf.inputNodes[0].text, font: Font.regular(17.0), textColor: .black)
let textSize = textNode.updateLayout(CGSize(width: 1000.0, height: 100.0))
let maxTextLength = 20
let lowerBound = 14
let upperBound = 160
var trackingOffset = textSize.width / maxWidth
trackingOffset = max(0.0, min(1.0, trackingOffset))
let frameIndex = lowerBound + Int(trackingOffset * CGFloat(upperBound - lowerBound))
strongSelf.animationNode.trackTo(frameIndex: frameIndex)
strongSelf.monkeyNode?.setState(.tracking(trackingOffset))
} else {
strongSelf.monkeyNode?.setState(.idle)
}
default:
break
}*/
}
}
focusUpdated = { [weak self] node, _ in
DispatchQueue.main.async {
guard let strongSelf = self else {
return
}
updateAnimations()
}
}
updated = { [weak self] _ in
guard let strongSelf = self else {
@ -1068,7 +1093,13 @@ private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, UIS
let sideInset: CGFloat = 32.0
let buttonSideInset: CGFloat = 48.0
let iconSpacing: CGFloat = 2.0
let iconSpacing: CGFloat
switch self.mode {
case .passwordHint, .emailConfirmation:
iconSpacing = 6.0
default:
iconSpacing = 2.0
}
let titleSpacing: CGFloat = 19.0
let titleInputSpacing: CGFloat = 26.0
let textSpacing: CGFloat = 30.0
@ -1118,6 +1149,7 @@ private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, UIS
let iconFrame = CGRect(origin: CGPoint(x: floor((contentAreaSize.width - iconSize.width) / 2.0), y: contentVerticalOrigin), size: iconSize)
if let animatedStickerNode = self.animatedStickerNode {
animatedStickerNode.updateLayout(size: iconFrame.size)
transition.updateFrame(node: animatedStickerNode, frame: iconFrame)
} else if let monkeyNode = self.monkeyNode {
transition.updateFrame(node: monkeyNode, frame: iconFrame)

View File

@ -641,14 +641,14 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting
case .set:
break
case let .notSet(pendingEmail):
break
//intro = pendingEmail == nil
/*if pendingEmail == nil {
if pendingEmail == nil {
let controller = TwoFactorAuthSplashScreen(context: context, mode: .intro)
pushControllerImpl?(controller, true)
return
} else {
}*/
}
}
}
if intro {