|
@@ -0,0 +1,308 @@
|
|
|
+//
|
|
|
+// TSImageProComparisonView.swift
|
|
|
+// TSLiveWallpaper
|
|
|
+//
|
|
|
+// Created by 100Years on 2025/6/15.
|
|
|
+//
|
|
|
+
|
|
|
+import UIKit
|
|
|
+
|
|
|
+class TSImageProComparisonView: UIView {
|
|
|
+ // MARK: - 属性
|
|
|
+ private let oldImageView = UIImageView()
|
|
|
+ private let newImageView = UIImageView()
|
|
|
+ private let lineView = UIView()
|
|
|
+ private var displayLink: CADisplayLink?
|
|
|
+ private var animationStartTime: CFTimeInterval = 0
|
|
|
+ var duration: TimeInterval = 3.0
|
|
|
+
|
|
|
+ // 动画方向枚举
|
|
|
+ enum AnimationDirection {
|
|
|
+ case leftToRight // 从左到右(默认)
|
|
|
+ case rightToLeft // 从右到左
|
|
|
+ case topToBottom // 从上到下
|
|
|
+ case bottomToTop // 从下到上
|
|
|
+ }
|
|
|
+
|
|
|
+ // 动画曲线枚举
|
|
|
+ enum AnimationCurve {
|
|
|
+ case linear // 线性
|
|
|
+ case easeIn // 缓入
|
|
|
+ case easeOut // 缓出
|
|
|
+ case easeInOut // 缓入缓出
|
|
|
+ }
|
|
|
+
|
|
|
+ // 线条样式枚举
|
|
|
+ enum LineStyle: Equatable {
|
|
|
+ case straight // 直线(默认)
|
|
|
+ case curved(curveWidth: CGFloat) // 弧线,可以指定弧线宽度
|
|
|
+ }
|
|
|
+
|
|
|
+ var direction: AnimationDirection = .leftToRight
|
|
|
+ var curve: AnimationCurve = .linear
|
|
|
+ var lineStyle: LineStyle = .straight
|
|
|
+ var lineWidth: CGFloat = 2.0
|
|
|
+ var lineColor: UIColor = .white
|
|
|
+
|
|
|
+ // 当前进度(0.0-1.0)
|
|
|
+ private var currentProgress: CGFloat = 0.0
|
|
|
+
|
|
|
+ // MARK: - 初始化
|
|
|
+ override init(frame: CGRect) {
|
|
|
+ super.init(frame: frame)
|
|
|
+ setupViews()
|
|
|
+ }
|
|
|
+
|
|
|
+ required init?(coder: NSCoder) {
|
|
|
+ super.init(coder: coder)
|
|
|
+ setupViews()
|
|
|
+ }
|
|
|
+
|
|
|
+ // MARK: - 视图设置
|
|
|
+ private func setupViews() {
|
|
|
+ // 旧图片视图(初始显示)
|
|
|
+ oldImageView.contentMode = .scaleAspectFill
|
|
|
+ oldImageView.clipsToBounds = true
|
|
|
+ addSubview(oldImageView)
|
|
|
+
|
|
|
+ // 新图片视图(初始隐藏)
|
|
|
+ newImageView.contentMode = .scaleAspectFill
|
|
|
+ newImageView.clipsToBounds = true
|
|
|
+ newImageView.alpha = 0
|
|
|
+ addSubview(newImageView)
|
|
|
+
|
|
|
+ // 分割线样式
|
|
|
+ lineView.backgroundColor = .clear
|
|
|
+ lineView.alpha = 0 // 初始隐藏
|
|
|
+ addSubview(lineView)
|
|
|
+ }
|
|
|
+
|
|
|
+ // MARK: - 布局
|
|
|
+ override func layoutSubviews() {
|
|
|
+ super.layoutSubviews()
|
|
|
+ oldImageView.frame = bounds
|
|
|
+ newImageView.frame = bounds
|
|
|
+ }
|
|
|
+
|
|
|
+ // MARK: - 公开方法
|
|
|
+ func configure(oldImage: UIImage?, newImage: UIImage?) {
|
|
|
+ oldImageView.image = oldImage
|
|
|
+ newImageView.image = newImage
|
|
|
+ reset()
|
|
|
+ }
|
|
|
+
|
|
|
+ func startAnimation(duration: TimeInterval = 3.0,
|
|
|
+ direction: AnimationDirection = .leftToRight,
|
|
|
+ curve: AnimationCurve = .linear) {
|
|
|
+ reset()
|
|
|
+ self.duration = duration
|
|
|
+ self.direction = direction
|
|
|
+ self.curve = curve
|
|
|
+
|
|
|
+ // 初始状态
|
|
|
+ oldImageView.alpha = 1
|
|
|
+ newImageView.alpha = 0
|
|
|
+ lineView.alpha = 1
|
|
|
+
|
|
|
+ // 启动动画
|
|
|
+ animationStartTime = CACurrentMediaTime()
|
|
|
+ displayLink = CADisplayLink(target: self, selector: #selector(updateAnimation))
|
|
|
+ displayLink?.add(to: .main, forMode: .common)
|
|
|
+ }
|
|
|
+
|
|
|
+ private func stopAnimation() {
|
|
|
+ displayLink?.invalidate()
|
|
|
+ displayLink = nil
|
|
|
+ }
|
|
|
+
|
|
|
+ func animationComplete() {
|
|
|
+ self.stopAnimation()
|
|
|
+ self.newImageView.alpha = 1
|
|
|
+ self.oldImageView.alpha = 0
|
|
|
+ self.lineView.alpha = 0
|
|
|
+ }
|
|
|
+
|
|
|
+ func reset() {
|
|
|
+ stopAnimation()
|
|
|
+ oldImageView.alpha = 1
|
|
|
+ newImageView.alpha = 0
|
|
|
+ lineView.alpha = 0
|
|
|
+ currentProgress = 0
|
|
|
+ lineView.frame = bounds // 重置为完整框架
|
|
|
+ lineView.layer.sublayers?.forEach { $0.removeFromSuperlayer() }
|
|
|
+ }
|
|
|
+
|
|
|
+ // MARK: - 动画更新
|
|
|
+ @objc private func updateAnimation() {
|
|
|
+ guard displayLink != nil else { return }
|
|
|
+
|
|
|
+ let elapsed = CACurrentMediaTime() - animationStartTime
|
|
|
+ let rawProgress = min(CGFloat(elapsed / duration), 1.0)
|
|
|
+
|
|
|
+ // 应用动画曲线
|
|
|
+ let progress: CGFloat
|
|
|
+ switch curve {
|
|
|
+ case .linear:
|
|
|
+ progress = rawProgress
|
|
|
+ case .easeIn:
|
|
|
+ progress = rawProgress * rawProgress // 缓入效果
|
|
|
+ case .easeOut:
|
|
|
+ progress = rawProgress * (2 - rawProgress)
|
|
|
+ case .easeInOut:
|
|
|
+ if rawProgress < 0.5 {
|
|
|
+ progress = 2 * rawProgress * rawProgress
|
|
|
+ } else {
|
|
|
+ progress = -1 + (4 - 2 * rawProgress) * rawProgress
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ currentProgress = progress
|
|
|
+ updateViews(with: progress)
|
|
|
+
|
|
|
+ if rawProgress >= 1.0 {
|
|
|
+ animationComplete()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func updateViews(with progress: CGFloat) {
|
|
|
+ // 更新遮罩和线条
|
|
|
+ updateMaskAndLine(with: progress)
|
|
|
+ newImageView.alpha = 1
|
|
|
+ }
|
|
|
+
|
|
|
+ private func updateMaskAndLine(with progress: CGFloat) {
|
|
|
+ // 清除之前的图层
|
|
|
+ lineView.layer.sublayers?.forEach { $0.removeFromSuperlayer() }
|
|
|
+ lineView.backgroundColor = .clear
|
|
|
+
|
|
|
+ // 创建遮罩层
|
|
|
+ let maskLayer = CAShapeLayer()
|
|
|
+
|
|
|
+ // 创建线条图层
|
|
|
+ let lineLayer = CAShapeLayer()
|
|
|
+ lineLayer.strokeColor = lineColor.cgColor
|
|
|
+ lineLayer.fillColor = UIColor.clear.cgColor
|
|
|
+ lineLayer.lineWidth = lineWidth
|
|
|
+
|
|
|
+ switch (direction, lineStyle) {
|
|
|
+ // 直线样式
|
|
|
+ case (_, .straight):
|
|
|
+ let path = UIBezierPath()
|
|
|
+
|
|
|
+ switch direction {
|
|
|
+ case .leftToRight:
|
|
|
+ let x = bounds.width * progress
|
|
|
+ path.move(to: CGPoint(x: x, y: 0))
|
|
|
+ path.addLine(to: CGPoint(x: x, y: bounds.height))
|
|
|
+
|
|
|
+ maskLayer.path = UIBezierPath(rect: CGRect(x: 0, y: 0, width: x, height: bounds.height)).cgPath
|
|
|
+
|
|
|
+ case .rightToLeft:
|
|
|
+ let x = bounds.width * (1 - progress)
|
|
|
+ path.move(to: CGPoint(x: x, y: 0))
|
|
|
+ path.addLine(to: CGPoint(x: x, y: bounds.height))
|
|
|
+
|
|
|
+ maskLayer.path = UIBezierPath(rect: CGRect(x: x, y: 0, width: bounds.width - x, height: bounds.height)).cgPath
|
|
|
+
|
|
|
+ case .topToBottom:
|
|
|
+ let y = bounds.height * progress
|
|
|
+ path.move(to: CGPoint(x: 0, y: y))
|
|
|
+ path.addLine(to: CGPoint(x: bounds.width, y: y))
|
|
|
+
|
|
|
+ maskLayer.path = UIBezierPath(rect: CGRect(x: 0, y: 0, width: bounds.width, height: y)).cgPath
|
|
|
+
|
|
|
+ case .bottomToTop:
|
|
|
+ let y = bounds.height * (1 - progress)
|
|
|
+ path.move(to: CGPoint(x: 0, y: y))
|
|
|
+ path.addLine(to: CGPoint(x: bounds.width, y: y))
|
|
|
+
|
|
|
+ maskLayer.path = UIBezierPath(rect: CGRect(x: 0, y: y, width: bounds.width, height: bounds.height - y)).cgPath
|
|
|
+ }
|
|
|
+
|
|
|
+ lineLayer.path = path.cgPath
|
|
|
+
|
|
|
+ // 弧线样式
|
|
|
+ case (.leftToRight, .curved(let curveWidth)):
|
|
|
+ let x = bounds.width * progress
|
|
|
+ let path = UIBezierPath()
|
|
|
+ path.move(to: CGPoint(x: x, y: 0))
|
|
|
+ path.addQuadCurve(to: CGPoint(x: x, y: bounds.height),
|
|
|
+ controlPoint: CGPoint(x: x + curveWidth, y: bounds.height / 2))
|
|
|
+
|
|
|
+ lineLayer.path = path.cgPath
|
|
|
+
|
|
|
+ // 弧形遮罩
|
|
|
+ let maskPath = UIBezierPath()
|
|
|
+ maskPath.move(to: .zero)
|
|
|
+ maskPath.addLine(to: CGPoint(x: x, y: 0))
|
|
|
+ maskPath.addQuadCurve(to: CGPoint(x: x, y: bounds.height),
|
|
|
+ controlPoint: CGPoint(x: x + curveWidth, y: bounds.height / 2))
|
|
|
+ maskPath.addLine(to: CGPoint(x: 0, y: bounds.height))
|
|
|
+ maskPath.close()
|
|
|
+ maskLayer.path = maskPath.cgPath
|
|
|
+
|
|
|
+ case (.rightToLeft, .curved(let curveWidth)):
|
|
|
+ let x = bounds.width * (1 - progress)
|
|
|
+ let path = UIBezierPath()
|
|
|
+ path.move(to: CGPoint(x: x, y: 0))
|
|
|
+ path.addQuadCurve(to: CGPoint(x: x, y: bounds.height),
|
|
|
+ controlPoint: CGPoint(x: x - curveWidth, y: bounds.height / 2))
|
|
|
+
|
|
|
+ lineLayer.path = path.cgPath
|
|
|
+
|
|
|
+ // 弧形遮罩
|
|
|
+ let maskPath = UIBezierPath()
|
|
|
+ maskPath.move(to: CGPoint(x: bounds.width, y: 0))
|
|
|
+ maskPath.addLine(to: CGPoint(x: x, y: 0))
|
|
|
+ maskPath.addQuadCurve(to: CGPoint(x: x, y: bounds.height),
|
|
|
+ controlPoint: CGPoint(x: x - curveWidth, y: bounds.height / 2))
|
|
|
+ maskPath.addLine(to: CGPoint(x: bounds.width, y: bounds.height))
|
|
|
+ maskPath.close()
|
|
|
+ maskLayer.path = maskPath.cgPath
|
|
|
+
|
|
|
+ case (.topToBottom, .curved(let curveWidth)):
|
|
|
+ let y = bounds.height * progress
|
|
|
+ let path = UIBezierPath()
|
|
|
+ path.move(to: CGPoint(x: 0, y: y))
|
|
|
+ path.addQuadCurve(to: CGPoint(x: bounds.width, y: y),
|
|
|
+ controlPoint: CGPoint(x: bounds.width / 2, y: y + curveWidth))
|
|
|
+
|
|
|
+ lineLayer.path = path.cgPath
|
|
|
+
|
|
|
+ // 弧形遮罩
|
|
|
+ let maskPath = UIBezierPath()
|
|
|
+ maskPath.move(to: .zero)
|
|
|
+ maskPath.addLine(to: CGPoint(x: 0, y: y))
|
|
|
+ maskPath.addQuadCurve(to: CGPoint(x: bounds.width, y: y),
|
|
|
+ controlPoint: CGPoint(x: bounds.width / 2, y: y + curveWidth))
|
|
|
+ maskPath.addLine(to: CGPoint(x: bounds.width, y: 0))
|
|
|
+ maskPath.close()
|
|
|
+ maskLayer.path = maskPath.cgPath
|
|
|
+
|
|
|
+ case (.bottomToTop, .curved(let curveWidth)):
|
|
|
+ let y = bounds.height * (1 - progress)
|
|
|
+ let path = UIBezierPath()
|
|
|
+ path.move(to: CGPoint(x: 0, y: y))
|
|
|
+ path.addQuadCurve(to: CGPoint(x: bounds.width, y: y),
|
|
|
+ controlPoint: CGPoint(x: bounds.width / 2, y: y - curveWidth))
|
|
|
+
|
|
|
+ lineLayer.path = path.cgPath
|
|
|
+
|
|
|
+ // 弧形遮罩
|
|
|
+ let maskPath = UIBezierPath()
|
|
|
+ maskPath.move(to: CGPoint(x: 0, y: bounds.height))
|
|
|
+ maskPath.addLine(to: CGPoint(x: 0, y: y))
|
|
|
+ maskPath.addQuadCurve(to: CGPoint(x: bounds.width, y: y),
|
|
|
+ controlPoint: CGPoint(x: bounds.width / 2, y: y - curveWidth))
|
|
|
+ maskPath.addLine(to: CGPoint(x: bounds.width, y: bounds.height))
|
|
|
+ maskPath.close()
|
|
|
+ maskLayer.path = maskPath.cgPath
|
|
|
+ }
|
|
|
+
|
|
|
+ // 应用遮罩
|
|
|
+ newImageView.layer.mask = maskLayer
|
|
|
+
|
|
|
+ // 应用线条
|
|
|
+ lineView.layer.addSublayer(lineLayer)
|
|
|
+ }
|
|
|
+}
|