620 lines
21 KiB
Swift
620 lines
21 KiB
Swift
//
|
|
// SOSAnimationViews.swift
|
|
// QuickLocation
|
|
//
|
|
|
|
import UIKit
|
|
|
|
final class SOSPulseButtonView: UIView {
|
|
|
|
var onTap: (() -> Void)?
|
|
|
|
private let buttonImageView = UIImageView(image: UIImage(named: "SOS/sos_main_button"))
|
|
private let titleImageView = UIImageView(image: UIImage(named: "SOS/sos_title"))
|
|
private let handImageView = UIImageView(image: UIImage(named: "SOS/sos_tap_hand"))
|
|
private let tapButton = UIButton(type: .custom)
|
|
private let rippleLayers = [CAShapeLayer(), CAShapeLayer()]
|
|
private var shouldAnimate = true
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
backgroundColor = .clear
|
|
isAccessibilityElement = false
|
|
|
|
rippleLayers.forEach {
|
|
$0.fillColor = UIColor.clear.cgColor
|
|
$0.strokeColor = UIColor(hexStr: "#FF7378").cgColor
|
|
$0.lineWidth = 1.5
|
|
$0.opacity = 0
|
|
layer.addSublayer($0)
|
|
}
|
|
|
|
buttonImageView.contentMode = .scaleAspectFit
|
|
addSubview(buttonImageView)
|
|
|
|
titleImageView.contentMode = .scaleAspectFit
|
|
addSubview(titleImageView)
|
|
|
|
handImageView.contentMode = .scaleAspectFit
|
|
addSubview(handImageView)
|
|
|
|
tapButton.accessibilityLabel = "发送SOS"
|
|
tapButton.addTarget(self, action: #selector(handleTap), for: .touchUpInside)
|
|
addSubview(tapButton)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override var intrinsicContentSize: CGSize {
|
|
CGSize(width: 345, height: 345)
|
|
}
|
|
|
|
override func layoutSubviews() {
|
|
super.layoutSubviews()
|
|
|
|
let side = min(bounds.width, bounds.height)
|
|
let center = CGPoint(x: bounds.midX, y: bounds.midY)
|
|
let buttonSide = side * 0.74
|
|
buttonImageView.frame = CGRect(
|
|
x: center.x - buttonSide / 2,
|
|
y: center.y - buttonSide / 2,
|
|
width: buttonSide,
|
|
height: buttonSide
|
|
)
|
|
|
|
let rippleCenter = buttonImageView.center
|
|
let rippleStartRadius = buttonImageView.frame.width / 2
|
|
rippleLayers.forEach { rippleLayer in
|
|
rippleLayer.frame = bounds
|
|
rippleLayer.path = UIBezierPath(
|
|
arcCenter: rippleCenter,
|
|
radius: rippleStartRadius,
|
|
startAngle: 0,
|
|
endAngle: .pi * 2,
|
|
clockwise: true
|
|
).cgPath
|
|
}
|
|
|
|
let titleWidth = buttonSide * 0.49
|
|
let titleAspectRatio = titleImageView.image.map { $0.size.height / $0.size.width } ?? 0.32
|
|
let titleHeight = titleWidth * titleAspectRatio
|
|
let titleCenterY = handImageView.isHidden
|
|
? center.y
|
|
: center.y - buttonSide * 0.13
|
|
titleImageView.frame = CGRect(
|
|
x: center.x - titleWidth / 2,
|
|
y: titleCenterY - titleHeight / 2,
|
|
width: titleWidth,
|
|
height: titleHeight
|
|
)
|
|
|
|
let handSize = CGSize(width: side * 0.09, height: side * 0.14)
|
|
handImageView.frame = CGRect(
|
|
x: center.x - handSize.width / 2,
|
|
y: center.y + buttonSide * 0.13,
|
|
width: handSize.width,
|
|
height: handSize.height
|
|
)
|
|
tapButton.frame = bounds
|
|
}
|
|
|
|
override func didMoveToWindow() {
|
|
super.didMoveToWindow()
|
|
window == nil ? stopAnimations() : startAnimationsIfNeeded()
|
|
}
|
|
|
|
func setShowsHand(_ showsHand: Bool) {
|
|
handImageView.isHidden = !showsHand
|
|
setNeedsLayout()
|
|
if showsHand {
|
|
startAnimationsIfNeeded()
|
|
} else {
|
|
handImageView.layer.removeAnimation(forKey: "sos.hand.tap")
|
|
}
|
|
}
|
|
|
|
func setAnimationEnabled(_ enabled: Bool) {
|
|
shouldAnimate = enabled
|
|
enabled ? startAnimationsIfNeeded() : stopAnimations()
|
|
}
|
|
|
|
private func startAnimationsIfNeeded() {
|
|
guard shouldAnimate, window != nil, !UIAccessibility.isReduceMotionEnabled else {
|
|
stopAnimations()
|
|
return
|
|
}
|
|
|
|
let travelDuration = 1.35
|
|
let staggerDuration = travelDuration * 0.85
|
|
let cycleDuration = staggerDuration * Double(rippleLayers.count)
|
|
let travelProgress = NSNumber(value: travelDuration / cycleDuration)
|
|
let baseTime = CACurrentMediaTime()
|
|
for (index, rippleLayer) in rippleLayers.enumerated() {
|
|
guard rippleLayer.animation(forKey: "sos.ripple") == nil else { continue }
|
|
|
|
let rippleScale = CAKeyframeAnimation(keyPath: "transform.scale")
|
|
rippleScale.values = [1.0, 1.42, 1.42]
|
|
rippleScale.keyTimes = [0, travelProgress, 1]
|
|
|
|
let opacity = CAKeyframeAnimation(keyPath: "opacity")
|
|
opacity.values = [0.58, 0, 0]
|
|
opacity.keyTimes = [0, travelProgress, 1]
|
|
|
|
let rippleGroup = CAAnimationGroup()
|
|
rippleGroup.animations = [rippleScale, opacity]
|
|
rippleGroup.duration = cycleDuration
|
|
rippleGroup.beginTime = baseTime + Double(index) * staggerDuration
|
|
rippleGroup.repeatCount = Float.infinity
|
|
rippleGroup.isRemovedOnCompletion = false
|
|
rippleLayer.add(rippleGroup, forKey: "sos.ripple")
|
|
}
|
|
|
|
guard !handImageView.isHidden,
|
|
handImageView.layer.animation(forKey: "sos.hand.tap") == nil else { return }
|
|
|
|
let translation = CAKeyframeAnimation(keyPath: "transform.translation.y")
|
|
translation.values = [0, 5, 5, 0]
|
|
translation.keyTimes = [0, 0.35, 0.58, 1]
|
|
|
|
let handScale = CAKeyframeAnimation(keyPath: "transform.scale")
|
|
handScale.values = [1, 0.9, 0.9, 1]
|
|
handScale.keyTimes = [0, 0.35, 0.58, 1]
|
|
|
|
let handGroup = CAAnimationGroup()
|
|
handGroup.animations = [translation, handScale]
|
|
handGroup.duration = 1.15
|
|
handGroup.repeatCount = Float.infinity
|
|
handImageView.layer.add(handGroup, forKey: "sos.hand.tap")
|
|
}
|
|
|
|
private func stopAnimations() {
|
|
rippleLayers.forEach { $0.removeAnimation(forKey: "sos.ripple") }
|
|
handImageView.layer.removeAnimation(forKey: "sos.hand.tap")
|
|
}
|
|
|
|
@objc private func handleTap() {
|
|
onTap?()
|
|
}
|
|
}
|
|
|
|
final class SOSCountdownRingView: UIView {
|
|
|
|
var onCompleted: (() -> Void)?
|
|
|
|
private let trackLayer = CAShapeLayer()
|
|
private let progressMaskLayer = CAShapeLayer()
|
|
private let progressGradientLayer = CAGradientLayer()
|
|
private let rippleLayers = [CAShapeLayer(), CAShapeLayer()]
|
|
private let digitStack = UIStackView()
|
|
private let alarmContainer = UIView()
|
|
private let alarmRaysView = UIImageView(image: UIImage(named: "SOS/sos_alarm_rays"))
|
|
private let alarmIconView = UIImageView(image: UIImage(named: "SOS/sos_alarm_icon"))
|
|
|
|
private var countdownTimer: Timer?
|
|
private var deadline: Date?
|
|
private var duration: TimeInterval = 10
|
|
private var lastDisplayedValue = -1
|
|
private var completionDelivered = false
|
|
private var progress: CGFloat = 1
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
backgroundColor = .clear
|
|
|
|
rippleLayers.forEach {
|
|
$0.fillColor = UIColor.clear.cgColor
|
|
$0.strokeColor = UIColor(hexStr: "#F4B5EE").cgColor
|
|
$0.lineWidth = 1.5
|
|
$0.opacity = 0
|
|
layer.addSublayer($0)
|
|
}
|
|
|
|
trackLayer.fillColor = UIColor.clear.cgColor
|
|
trackLayer.strokeColor = UIColor(hexStr: "#EEE8FF").cgColor
|
|
trackLayer.lineCap = .butt
|
|
layer.addSublayer(trackLayer)
|
|
|
|
progressGradientLayer.type = .conic
|
|
progressGradientLayer.colors = [
|
|
UIColor(hexStr: "#C9B7FF").cgColor,
|
|
UIColor(hexStr: "#9C5AFF").cgColor,
|
|
UIColor(hexStr: "#D72DD6").cgColor,
|
|
UIColor(hexStr: "#C9B7FF").cgColor
|
|
]
|
|
progressGradientLayer.locations = [0, 0.38, 0.76, 1]
|
|
progressGradientLayer.startPoint = CGPoint(x: 0.5, y: 0.5)
|
|
progressGradientLayer.endPoint = CGPoint(x: 0.5, y: 0)
|
|
progressGradientLayer.mask = progressMaskLayer
|
|
layer.addSublayer(progressGradientLayer)
|
|
|
|
digitStack.axis = .horizontal
|
|
digitStack.alignment = .center
|
|
digitStack.distribution = .fill
|
|
digitStack.spacing = 2
|
|
addSubview(digitStack)
|
|
|
|
alarmIconView.contentMode = .scaleAspectFit
|
|
alarmRaysView.contentMode = .scaleAspectFit
|
|
alarmContainer.addSubview(alarmIconView)
|
|
alarmContainer.addSubview(alarmRaysView)
|
|
addSubview(alarmContainer)
|
|
|
|
render(progress: 1, value: 10)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override var intrinsicContentSize: CGSize {
|
|
CGSize(width: 345, height: 345)
|
|
}
|
|
|
|
override func layoutSubviews() {
|
|
super.layoutSubviews()
|
|
|
|
let side = min(bounds.width, bounds.height)
|
|
let center = CGPoint(x: bounds.midX, y: bounds.midY)
|
|
let ringDiameter = side * 0.64
|
|
let ringRadius = ringDiameter / 2
|
|
let lineWidth = max(18, side * 0.064)
|
|
rippleLayers.forEach { rippleLayer in
|
|
rippleLayer.frame = bounds
|
|
rippleLayer.path = UIBezierPath(
|
|
arcCenter: center,
|
|
radius: ringRadius + lineWidth / 2,
|
|
startAngle: 0,
|
|
endAngle: .pi * 2,
|
|
clockwise: true
|
|
).cgPath
|
|
}
|
|
|
|
let ringPath = UIBezierPath(
|
|
arcCenter: center,
|
|
radius: ringRadius,
|
|
startAngle: -.pi / 2,
|
|
endAngle: .pi * 1.5,
|
|
clockwise: true
|
|
)
|
|
|
|
trackLayer.frame = bounds
|
|
trackLayer.path = ringPath.cgPath
|
|
trackLayer.lineWidth = lineWidth
|
|
|
|
progressMaskLayer.frame = bounds
|
|
progressMaskLayer.path = ringPath.cgPath
|
|
progressMaskLayer.fillColor = UIColor.clear.cgColor
|
|
progressMaskLayer.strokeColor = UIColor.black.cgColor
|
|
progressMaskLayer.lineWidth = lineWidth
|
|
progressMaskLayer.lineCap = .butt
|
|
progressGradientLayer.frame = bounds
|
|
|
|
layoutDigitStack(at: center)
|
|
|
|
let alarmSide = side * 0.15
|
|
let alarmSize = CGSize(width: alarmSide, height: alarmSide)
|
|
alarmContainer.bounds = CGRect(origin: .zero, size: alarmSize)
|
|
alarmIconView.frame = alarmContainer.bounds
|
|
alarmRaysView.frame = alarmContainer.bounds
|
|
updateAlarmPosition(progress: progress)
|
|
}
|
|
|
|
func start(duration: TimeInterval = 10) {
|
|
stop(reset: false)
|
|
self.duration = max(0.1, duration)
|
|
deadline = Date().addingTimeInterval(self.duration)
|
|
completionDelivered = false
|
|
lastDisplayedValue = -1
|
|
render(progress: 1, value: Int(ceil(self.duration)))
|
|
startAlarmAnimation()
|
|
|
|
let timer = Timer(timeInterval: 1.0 / 30.0, repeats: true) { [weak self] _ in
|
|
self?.updateCountdown()
|
|
}
|
|
RunLoop.main.add(timer, forMode: .common)
|
|
countdownTimer = timer
|
|
}
|
|
|
|
func stop(reset: Bool = true) {
|
|
countdownTimer?.invalidate()
|
|
countdownTimer = nil
|
|
deadline = nil
|
|
rippleLayers.forEach { $0.removeAnimation(forKey: "sos.countdown.ripple") }
|
|
alarmRaysView.layer.removeAnimation(forKey: "sos.alarm.rays")
|
|
alarmIconView.layer.removeAnimation(forKey: "sos.alarm.icon")
|
|
if reset {
|
|
completionDelivered = false
|
|
lastDisplayedValue = -1
|
|
render(progress: 1, value: 10)
|
|
}
|
|
}
|
|
|
|
private func updateCountdown() {
|
|
guard let deadline else { return }
|
|
let remaining = max(0, deadline.timeIntervalSinceNow)
|
|
let progress = CGFloat(min(1, remaining / duration))
|
|
let value = max(0, min(10, Int(ceil(remaining))))
|
|
render(progress: progress, value: value)
|
|
|
|
guard remaining <= 0, !completionDelivered else { return }
|
|
completionDelivered = true
|
|
countdownTimer?.invalidate()
|
|
countdownTimer = nil
|
|
self.deadline = nil
|
|
rippleLayers.forEach { $0.removeAnimation(forKey: "sos.countdown.ripple") }
|
|
alarmRaysView.layer.removeAnimation(forKey: "sos.alarm.rays")
|
|
alarmIconView.layer.removeAnimation(forKey: "sos.alarm.icon")
|
|
onCompleted?()
|
|
}
|
|
|
|
private func render(progress: CGFloat, value: Int) {
|
|
self.progress = progress
|
|
CATransaction.begin()
|
|
CATransaction.setDisableActions(true)
|
|
progressMaskLayer.strokeStart = 1 - progress
|
|
progressMaskLayer.strokeEnd = 1
|
|
CATransaction.commit()
|
|
updateAlarmPosition(progress: progress)
|
|
|
|
guard value != lastDisplayedValue else { return }
|
|
lastDisplayedValue = value
|
|
digitStack.arrangedSubviews.forEach {
|
|
digitStack.removeArrangedSubview($0)
|
|
$0.removeFromSuperview()
|
|
}
|
|
String(value).forEach { character in
|
|
let imageView = UIImageView(image: UIImage(named: "SOS/sos_countdown_\(character)"))
|
|
imageView.contentMode = .scaleAspectFit
|
|
imageView.widthAnchor.constraint(equalToConstant: 42).isActive = true
|
|
imageView.heightAnchor.constraint(equalToConstant: 58).isActive = true
|
|
digitStack.addArrangedSubview(imageView)
|
|
}
|
|
layoutDigitStack(at: CGPoint(x: bounds.midX, y: bounds.midY))
|
|
bringSubviewToFront(digitStack)
|
|
}
|
|
|
|
private func updateAlarmPosition(progress: CGFloat) {
|
|
guard bounds.width > 0, bounds.height > 0 else { return }
|
|
let side = min(bounds.width, bounds.height)
|
|
let radius = side * 0.64 / 2
|
|
let angle = -.pi / 2 + .pi * 2 * (1 - progress)
|
|
alarmContainer.center = CGPoint(
|
|
x: bounds.midX + cos(angle) * radius,
|
|
y: bounds.midY + sin(angle) * radius - 5
|
|
)
|
|
bringSubviewToFront(alarmContainer)
|
|
}
|
|
|
|
private func layoutDigitStack(at center: CGPoint) {
|
|
let digitWidth: CGFloat = 42
|
|
let digitHeight: CGFloat = 58
|
|
let count = digitStack.arrangedSubviews.count
|
|
let spacingWidth = CGFloat(max(0, count - 1)) * digitStack.spacing
|
|
digitStack.bounds = CGRect(
|
|
x: 0,
|
|
y: 0,
|
|
width: CGFloat(count) * digitWidth + spacingWidth,
|
|
height: digitHeight
|
|
)
|
|
digitStack.center = center
|
|
digitStack.setNeedsLayout()
|
|
digitStack.layoutIfNeeded()
|
|
}
|
|
|
|
private func startAlarmAnimation() {
|
|
guard !UIAccessibility.isReduceMotionEnabled else { return }
|
|
|
|
let travelDuration = 1.35
|
|
let staggerDuration = travelDuration * 0.85
|
|
let cycleDuration = staggerDuration * Double(rippleLayers.count)
|
|
let travelProgress = NSNumber(value: travelDuration / cycleDuration)
|
|
let baseTime = CACurrentMediaTime()
|
|
for (index, rippleLayer) in rippleLayers.enumerated() {
|
|
let rippleScale = CAKeyframeAnimation(keyPath: "transform.scale")
|
|
rippleScale.values = [1.0, 1.55, 1.55]
|
|
rippleScale.keyTimes = [0, travelProgress, 1]
|
|
|
|
let rippleOpacity = CAKeyframeAnimation(keyPath: "opacity")
|
|
rippleOpacity.values = [0.58, 0, 0]
|
|
rippleOpacity.keyTimes = [0, travelProgress, 1]
|
|
|
|
let rippleGroup = CAAnimationGroup()
|
|
rippleGroup.animations = [rippleScale, rippleOpacity]
|
|
rippleGroup.duration = cycleDuration
|
|
rippleGroup.beginTime = baseTime + Double(index) * staggerDuration
|
|
rippleGroup.repeatCount = .infinity
|
|
rippleGroup.isRemovedOnCompletion = false
|
|
rippleLayer.add(rippleGroup, forKey: "sos.countdown.ripple")
|
|
}
|
|
|
|
let opacity = CAKeyframeAnimation(keyPath: "opacity")
|
|
opacity.values = [0.25, 1, 0.25]
|
|
opacity.keyTimes = [0, 0.5, 1]
|
|
|
|
let scale = CAKeyframeAnimation(keyPath: "transform.scale")
|
|
scale.values = [0.88, 1.08, 0.88]
|
|
scale.keyTimes = [0, 0.5, 1]
|
|
|
|
let group = CAAnimationGroup()
|
|
group.animations = [opacity, scale]
|
|
group.duration = 0.62
|
|
group.repeatCount = .infinity
|
|
alarmRaysView.layer.add(group, forKey: "sos.alarm.rays")
|
|
|
|
let rotation = CAKeyframeAnimation(keyPath: "transform.rotation.z")
|
|
rotation.values = [-CGFloat.pi / 24, CGFloat.pi / 24, -CGFloat.pi / 24]
|
|
rotation.keyTimes = [0, 0.5, 1]
|
|
|
|
let iconGroup = CAAnimationGroup()
|
|
iconGroup.animations = [rotation]
|
|
iconGroup.duration = 0.62
|
|
iconGroup.repeatCount = .infinity
|
|
iconGroup.isRemovedOnCompletion = false
|
|
alarmIconView.layer.add(iconGroup, forKey: "sos.alarm.icon")
|
|
}
|
|
|
|
deinit {
|
|
countdownTimer?.invalidate()
|
|
}
|
|
}
|
|
|
|
final class SOSSlideCancelView: UIView {
|
|
|
|
enum Style {
|
|
case practiceCountdown
|
|
case countdown
|
|
case active
|
|
}
|
|
|
|
var onCompleted: (() -> Void)?
|
|
|
|
private let gradientLayer = CAGradientLayer()
|
|
private let titleLabel = UILabel()
|
|
private let thumbView = UIImageView(image: UIImage(named: "SOS/sos_slider_arrow"))
|
|
private var style: Style = .countdown
|
|
private var startX: CGFloat = 3
|
|
private var isDragging = false
|
|
private var isLocked = false
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
layer.insertSublayer(gradientLayer, at: 0)
|
|
clipsToBounds = true
|
|
|
|
titleLabel.text = "滑动取消sos"
|
|
titleLabel.textColor = .white
|
|
titleLabel.font = .systemFont(ofSize: 18, weight: .semibold)
|
|
titleLabel.textAlignment = .center
|
|
addSubview(titleLabel)
|
|
|
|
thumbView.contentMode = .scaleAspectFit
|
|
thumbView.isUserInteractionEnabled = true
|
|
addSubview(thumbView)
|
|
|
|
let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
|
|
thumbView.addGestureRecognizer(pan)
|
|
isAccessibilityElement = true
|
|
accessibilityLabel = "滑动取消SOS"
|
|
accessibilityTraits = .adjustable
|
|
|
|
configure(style: .countdown)
|
|
}
|
|
|
|
convenience init(thumbImageName: String) {
|
|
self.init(frame: .zero)
|
|
thumbView.image = UIImage(named: thumbImageName)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override func layoutSubviews() {
|
|
super.layoutSubviews()
|
|
layer.cornerRadius = bounds.height / 2
|
|
gradientLayer.frame = bounds
|
|
gradientLayer.cornerRadius = bounds.height / 2
|
|
titleLabel.frame = bounds
|
|
|
|
let thumbSide = max(0, bounds.height - 10)
|
|
thumbView.frame = CGRect(x: 10, y: 5, width: thumbSide, height: thumbSide)
|
|
}
|
|
|
|
func configure(style: Style) {
|
|
self.style = style
|
|
switch style {
|
|
case .practiceCountdown:
|
|
gradientLayer.colors = [
|
|
UIColor(hexStr: "#08ADEF").cgColor,
|
|
UIColor(hexStr: "#43C7F8").cgColor
|
|
]
|
|
case .countdown, .active:
|
|
gradientLayer.colors = [
|
|
UIColor(hexStr: "#293445").cgColor,
|
|
UIColor(hexStr: "#293445").cgColor
|
|
]
|
|
}
|
|
gradientLayer.startPoint = CGPoint(x: 0, y: 0.5)
|
|
gradientLayer.endPoint = CGPoint(x: 1, y: 0.5)
|
|
reset(animated: false)
|
|
}
|
|
|
|
func setInteractionEnabled(_ enabled: Bool) {
|
|
isLocked = !enabled
|
|
alpha = enabled ? 1 : 0.65
|
|
thumbView.isUserInteractionEnabled = enabled
|
|
}
|
|
|
|
func reset(animated: Bool = true) {
|
|
isLocked = false
|
|
isDragging = false
|
|
thumbView.isUserInteractionEnabled = true
|
|
alpha = 1
|
|
let changes = {
|
|
self.thumbView.frame.origin.x = 2
|
|
}
|
|
if animated {
|
|
UIView.animate(withDuration: 0.25, delay: 0, options: [.curveEaseOut], animations: changes)
|
|
} else {
|
|
changes()
|
|
}
|
|
}
|
|
|
|
@objc private func handlePan(_ gesture: UIPanGestureRecognizer) {
|
|
guard !isLocked else { return }
|
|
let maxX = max(2, bounds.width - thumbView.bounds.width - 2)
|
|
|
|
switch gesture.state {
|
|
case .began:
|
|
isDragging = true
|
|
startX = thumbView.frame.minX
|
|
case .changed:
|
|
let translation = gesture.translation(in: self)
|
|
thumbView.frame.origin.x = min(maxX, max(2, startX + translation.x))
|
|
case .ended, .cancelled, .failed:
|
|
isDragging = false
|
|
if thumbView.frame.minX >= maxX - 4 {
|
|
isLocked = true
|
|
thumbView.isUserInteractionEnabled = false
|
|
onCompleted?()
|
|
} else {
|
|
reset()
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
final class SOSGradientButton: UIButton {
|
|
|
|
private let gradientLayer = CAGradientLayer()
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
gradientLayer.colors = [
|
|
UIColor(hexStr: "#05AEF2").cgColor,
|
|
UIColor(hexStr: "#42C5F7").cgColor
|
|
]
|
|
gradientLayer.startPoint = CGPoint(x: 0, y: 0.5)
|
|
gradientLayer.endPoint = CGPoint(x: 1, y: 0.5)
|
|
layer.insertSublayer(gradientLayer, at: 0)
|
|
setTitleColor(.white, for: .normal)
|
|
titleLabel?.font = .systemFont(ofSize: 18, weight: .bold)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override func layoutSubviews() {
|
|
super.layoutSubviews()
|
|
gradientLayer.frame = bounds
|
|
layer.cornerRadius = bounds.height / 2
|
|
gradientLayer.cornerRadius = bounds.height / 2
|
|
clipsToBounds = true
|
|
}
|
|
}
|