Swift: анимация объекта по пути анимации - PullRequest
2 голосов
/ 18 июня 2020

Я хотел бы анимировать маленькую красную точку, вращающуюся по кругу, которая расширяется импульсным образом (go от маленького к большому, затем начинайте с малого). Кажется, что маленькая точка продолжает вращаться вокруг исходной формы и не принимает во внимание расширяющийся круг ... У меня это в коде:

// MARK: - Properties

  private lazy var containerView = UIView()

  let littleCircleRadius: CGFloat = 10

  private lazy var littleRedDot: CALayer = {
    let layer = CALayer()
    layer.backgroundColor = UIColor.red.cgColor
    let littleDotSize = CGSize(width: 10, height: 10)
    layer.frame = CGRect(x: containerView.bounds.center.x - littleDotSize.width / 2,
                         y: containerView.bounds.center.y - littleCircleRadius - littleDotSize.width/2 ,
                         width: littleDotSize.width,
                         height: littleDotSize.height)
    return layer
  }()

 private lazy var littleCircleLayer: CAShapeLayer = {
    let layer = CAShapeLayer()
    layer.lineWidth = 1.5
    layer.lineCap = .round
    layer.strokeColor = UIColor.black.cgColor
    layer.fillColor = UIColor.clear.cgColor
    return layer
  }()

// MARK: - Setup
 func setup() {
    view.addSubview(containerView)
    containerView.frame = CGRect(x: 40, y: 200, width: 300, height: 300)
    containerView.backgroundColor = UIColor.gray.withAlphaComponent(0.2)

    littleCircleLayer.path = makeArcPath(arcCenter: containerView.bounds.center, radius: 10)
    containerView.layer.addSublayer(littleCircleLayer)
    containerView.layer.addSublayer(littleRedDot)
}

// MARK: - Animations
func animate() {
    CATransaction.begin()
    CATransaction.setAnimationDuration(1.5)
    animateLittleRedDotRotation()
    animateCircleExpanding()
    CATransaction.commit()
}

func animateLittleRedDotRotation() {
    let anim = CAKeyframeAnimation(keyPath: "position")
    anim.duration = 1.5
    anim.rotationMode = .rotateAuto
    anim.repeatCount = Float.infinity
    anim.path = littleCircleLayer.path
    littleRedDot.add(anim, forKey: "rotate")
}

func animateCircleExpanding() {
    let maxCircle = makeArcPath(arcCenter: containerView.bounds.center, radius: 100)
    let circleExpandingAnim = CABasicAnimation(keyPath: "path")
    circleExpandingAnim.fromValue = littleCircleLayer.path
    circleExpandingAnim.toValue = maxCircle
    circleExpandingAnim.repeatCount = Float.infinity
    circleExpandingAnim.duration = 1.5
    littleCircleLayer.add(circleExpandingAnim, forKey: "pulseCircuitAnimation")
}

Это создает следующий эффект:

enter image description here

Однако я хотел бы добиться, чтобы маленькая точка вращалась вдоль пути расширяющегося круга (поскольку он анимируется от маленького круга к большему кругу), а не исходного пути маленького круга. Есть идеи?

1 Ответ

1 голос
/ 19 июня 2020

Использование CoreAnimation для анимации положения красной точки на основе пути предполагает, что путь не меняется. Теоретически вы можете определить спиральный путь, который отражает расширяющийся круг. Лично я бы просто использовал CADisplayLink, специальный таймер, оптимально разработанный для обновления экрана, и полностью отказался от вызовов CoreAnimation. Например,

func startDisplayLink() {
    let displayLink = CADisplayLink(target: self, selector: #selector(handleDisplayLink(_:)))
    displayLink.add(to: .main, forMode: .common)
}

@objc func handleDisplayLink(_ displayLink: CADisplayLink) {
    let percent = CGFloat(displayLink.timestamp).truncatingRemainder(dividingBy: duration) / duration
    let radius = ...
    let center = containerView.bounds.center
    circleLayer.path = makeArcPath(arcCenter: center, radius: radius)
    let angle = percent * .pi * 2
    let dotCenter = CGPoint(x: center.x + cos(angle) * radius, y: center.y + sin(angle) * radius)
    redDot.path = makeArcPath(arcCenter: dotCenter, radius: 5)
}

Это дает:

enter image description here


Полный пример:

class ViewController: UIViewController {

    private let radiusRange: ClosedRange<CGFloat> = 10...100
    private let duration: CGFloat = 1.5

    private lazy var containerView: UIView = {
        let containerView = UIView()
        containerView.translatesAutoresizingMaskIntoConstraints = false
        return containerView
    }()

    private lazy var redDot: CAShapeLayer = {
        let layer = CAShapeLayer()
        layer.fillColor = UIColor.red.cgColor
        return layer
    }()

    private lazy var circleLayer: CAShapeLayer = {
        let layer = CAShapeLayer()
        layer.lineWidth = 1.5
        layer.strokeColor = UIColor.black.cgColor
        layer.fillColor = UIColor.clear.cgColor
        return layer
    }()

    private weak var displayLink: CADisplayLink?

    override func viewDidLoad() {
        super.viewDidLoad()

        setup()
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        startDisplayLink()
    }

    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        stopDisplayLink()
    }
}

// MARK: Private utility methods

private extension ViewController {
    func setup() {
        addContainer()

        containerView.layer.addSublayer(circleLayer)
        containerView.layer.addSublayer(redDot)
    }

    func addContainer() {
        view.addSubview(containerView)

        NSLayoutConstraint.activate([
            containerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            containerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            containerView.topAnchor.constraint(equalTo: view.topAnchor),
            containerView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
    }

    func makeArcPath(arcCenter: CGPoint, radius: CGFloat) -> CGPath {
        UIBezierPath(arcCenter: arcCenter, radius: radius, startAngle: 0, endAngle: .pi * 2, clockwise: true).cgPath
    }
}

// MARK: - DisplayLink related methods

private extension ViewController {
    func startDisplayLink() {
        stopDisplayLink()  // stop existing display link, if any

        let displayLink = CADisplayLink(target: self, selector: #selector(handleDisplayLink(_:)))
        displayLink.add(to: .main, forMode: .common)
        self.displayLink = displayLink
    }

    func stopDisplayLink() {
        displayLink?.invalidate()
    }

    @objc func handleDisplayLink(_ displayLink: CADisplayLink) {
        let percent = CGFloat(displayLink.timestamp).truncatingRemainder(dividingBy: duration) / duration
        let radius = radiusRange.percent(percent)
        let center = containerView.bounds.center
        circleLayer.path = makeArcPath(arcCenter: center, radius: radius)
        let angle = percent * .pi * 2
        let dotCenter = CGPoint(x: center.x + cos(angle) * radius, y: center.y + sin(angle) * radius)
        redDot.path = makeArcPath(arcCenter: dotCenter, radius: 5)
    }
}

// MARK: - CGRect extension

extension CGRect {
    var center: CGPoint { return CGPoint(x: midX, y: midY) }
}

// MARK: - ClosedRange extension

extension ClosedRange where Bound: FloatingPoint {
    func percent(_ percent: Bound) -> Bound {
        (upperBound - lowerBound) * percent + lowerBound
    }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...