iOS 变形播放/暂停动画

Posted

技术标签:

【中文标题】iOS 变形播放/暂停动画【英文标题】:iOS morphing play/pause animation 【发布时间】:2018-08-13 13:22:21 【问题描述】:

我想在我正在开发的应用程序中添加一个类似 YouTube 的变形播放/暂停动画。它看起来像动画here。我不知道该怎么做。我知道我需要CABasicAnimationCAShapeLayer。我知道我需要两个(或者更多?)UIBezierPath-s。一个是shapeLayer 的路径,另一个是动画的toValue。比如:

shape.path = pausePath().cgPath

let animation = CABasicAnimation(keyPath: "path")
animation.toValue = trainglePath().cgPath
animation.duration = 1
shape.add(animation, forKey: animation.keyPath)

但我不确定如何走这些路。我有以下问题:

    制作路径的方式重要吗?例如,从左到右绘制的线与从右到左绘制的线的动画效果会不同吗?我的意思是path.move(to: startPoint); path.addLine(to: endPoint1) 反对 path.move(to: endPoint1)path.addLine(to: startPoint) 两条路径就够了吗?一个用于开始按钮,一个用于暂停按钮?如果是 - 绘制它们以使它们“正确”动画的正确方法是什么?我不在这里要求代码(我不介意将代码作为答案),但对于初学者来说,一些一般性的解释就足够了。

【问题讨论】:

检查github.com/suzuki-0000/AnimatablePlayButton 【参考方案1】:

让路径动画正常工作的关键是起始路径和结束路径具有相同数量的控制点。如果你看那个动画,在我看来,最初的“播放”三角形被绘制为两个相互接触的封闭四边形,其中右四边形有两个点在一起,所以它看起来像一个三角形。然后动画将这些四边形的控制点分开,并将它们都变成暂停符号的矩形。

如果您将其绘制在方格纸上,那么创建前后控制点应该很容易。

考虑下面的(非常粗略的)插图。顶部显示了分成 2 个四边形的“游戏”三角形,并将右侧部分(黄色)显示为四边形,其右侧的点稍微分开以显示这个想法。在实践中,您会设置具有完全相同坐标的这两个控制点的四边形。

我用数字标记了每个四边形的控制点以显示绘制它们的顺序(对于每个四边形:moveTo 第一个点,lineTo 2nd/3rd/4th 点,最后是 lineTo 第一个点,以关闭多边形。 )

底部的插图显示了移动的点以给出暂停符号。

如果您创建 CABasicAnimation 的 CAShapeLayer 以控制点(如顶部插图)开始,并以控制点(如底部插图)结束,您应该得到一个与您链接的动画非常相似的动画。

我刚刚创建了一个演示程序,它可以创建我所描述的动画。这是它的样子:

我用黑色描边路径并用青色填充它们,这样更容易判断发生了什么。

(这比我的手绘完成了一点<grin>。)

您可以在this Github link 查看生成该动画的项目。

【讨论】:

赞成手绘颜色填充。但是,您忘记了透明的塑料三环活页夹盖。 打鼾。 (我在工作,我们这里的机器上没有图形软件。) 非常感谢您的解释。感谢它,我设法制作了动画。 我添加了一个具有不同笔触和填充颜色的示例动画,这样您就可以看到它在做什么。【参考方案2】:

这是一个基于CAShapeLayer 的小实现,它基本上克隆了链接中描述的行为。

class PlayPauseButton: UIControl 

    // public function can be called from out interface
    func setPlaying(_ playing: Bool) 
        self.playing = playing
        animateLayer()
    

    private (set) var playing: Bool = false
    private let leftLayer: CAShapeLayer
    private let rightLayer: CAShapeLayer

    override init(frame: CGRect) 
        leftLayer = CAShapeLayer()
        rightLayer = CAShapeLayer()
        super.init(frame: frame)

        backgroundColor = UIColor.white
        setupLayers()
    

    required init?(coder aDecoder: NSCoder) 
        fatalError("Not implemented")
    

    private func setupLayers() 
        layer.addSublayer(leftLayer)
        layer.addSublayer(rightLayer)

        leftLayer.fillColor = UIColor.black.cgColor
        rightLayer.fillColor = UIColor.black.cgColor
        addTarget(self,
                  action: #selector(pressed),
                  for: .touchUpInside)
    

    @objc private func pressed() 
        setPlaying(!playing)
    

    private func animateLayer() 
        let fromLeftPath = leftLayer.path
        let toLeftPath = leftPath()
        leftLayer.path = toLeftPath

        let fromRightPath = rightLayer.path
        let toRightPath = rightPath()
        rightLayer.path = toRightPath

        let leftPathAnimation = pathAnimation(fromPath: fromLeftPath,
                                              toPath: toLeftPath)
        let rightPathAnimation = pathAnimation(fromPath: fromRightPath,
                                               toPath: toRightPath)

        leftLayer.add(leftPathAnimation,
                      forKey: nil)
        rightLayer.add(rightPathAnimation,
                       forKey: nil)
    

    private func pathAnimation(fromPath: CGPath?,
                               toPath: CGPath) -> CAAnimation 
        let animation = CABasicAnimation(keyPath: "path")
        animation.duration = 0.33
        animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
        animation.fromValue = fromPath
        animation.toValue = toPath
        return animation
    

    override func layoutSubviews() 
        leftLayer.frame = leftLayerFrame
        rightLayer.frame = rightLayerFrame

        leftLayer.path = leftPath()
        rightLayer.path = rightPath()
    

    private let pauseButtonLineSpacing: CGFloat = 10

    private var leftLayerFrame: CGRect 
        return CGRect(x: 0,
                      y: 0,
                      width: bounds.width * 0.5,
                      height: bounds.height)
    

    private var rightLayerFrame: CGRect 
        return leftLayerFrame.offsetBy(dx: bounds.width * 0.5,
                                       dy: 0)
    

    private func leftPath() -> CGPath 
        if playing 
            let bound = leftLayer.bounds
                                 .insetBy(dx: pauseButtonLineSpacing,
                                          dy: 0)
                                 .offsetBy(dx: -pauseButtonLineSpacing,
                                           dy: 0)

            return UIBezierPath(rect: bound).cgPath
        

        return leftLayerPausedPath()
    

    private func rightPath() -> CGPath 
        if playing 
            let bound = rightLayer.bounds
                                 .insetBy(dx: pauseButtonLineSpacing,
                                          dy: 0)
                                .offsetBy(dx: pauseButtonLineSpacing,
                                          dy: 0)
            return UIBezierPath(rect: bound).cgPath
        

        return rightLayerPausedPath()
    

    private func leftLayerPausedPath() -> CGPath 
        let y1 = leftLayerFrame.width * 0.5
        let y2 = leftLayerFrame.height - leftLayerFrame.width * 0.5

        let path = UIBezierPath()
        path.move(to:CGPoint(x: 0, y: 0))
        path.addLine(to: CGPoint(x: leftLayerFrame.width,
                                 y: y1))
        path.addLine(to: CGPoint(x: leftLayerFrame.width,
                                 y: y2))
        path.addLine(to: CGPoint(x: 0,
                                 y: leftLayerFrame.height))
        path.close()

        return path.cgPath
    

    private func rightLayerPausedPath() -> CGPath 
        let y1 = rightLayerFrame.width * 0.5
        let y2 = rightLayerFrame.height - leftLayerFrame.width * 0.5
        let path = UIBezierPath()

        path.move(to:CGPoint(x: 0, y: y1))
        path.addLine(to: CGPoint(x: rightLayerFrame.width,
                                 y: rightLayerFrame.height * 0.5))
        path.addLine(to: CGPoint(x: rightLayerFrame.width,
                                 y: rightLayerFrame.height * 0.5))
        path.addLine(to: CGPoint(x: 0,
                                 y: y2))
        path.close()
        return path.cgPath
    

代码应该非常简单。这是你如何使用它,

let playButton = PlayPauseButton(frame: CGRect(x: 0,
                                               y: 0,
                                               width: 200,
                                               height: 200))
view.addSubview(playButton)

您不需要处理来自外部的任何事情。按钮本身处理动画。您可以使用目标/操作来更改检测按钮点击。此外,您可以使用公共方法从外部添加动画状态,

playButton.setPlaying(true) // animate to playing
playButton.setPlaying(false) // animate to paused state

这是它的外观,

【讨论】:

哇,这是对问题的完整答案! (已投票)。 你应该提供一些关于你做了什么以及它如何/为什么工作的解释(或者我想你可以参考我的答案,因为我介绍了解释部分,你介绍了实现部分.) 非常感谢您的代码,它让我想到了必须使用 CAShapeLayers 和两个动画而不是一个,但我接受了另一个答案,因为我遵循了它的解释并发现它非常清楚。我不能同时接受这两个,所以只是投票赞成你的答案。 我很惊讶让 leftPath() 和 rightPath() 函数返回矩形有效。为了使路径动画从头到尾正确动画,开始路径和结束路径都需要具有相同数量的控制点,以相同的顺序绘制。我想,在内部,UIBezierPath(rect:) 方法从左上角开始,移动到右上角,右下角,左下角,然后回到左上角,就像你的 rightLayerPausedPath()lefttLayerPausedPath() 方法一样.

以上是关于iOS 变形播放/暂停动画的主要内容,如果未能解决你的问题,请参考以下文章

css 您可以通过更改其动画播放状态属性来“暂停”和“播放”CSS动画。将其设置为“暂停”会停止播放动画

动画暂停不起作用

悬停时播放 CSS 动画,悬停时暂停

IOS - 暂停和取消暂停当前播放的音乐

使用带有 pylab/matplotlib 嵌入的 Tkinter 播放、暂停、停止功能的彩色绘图动画:无法更新图形/画布?

将鼠标悬停在css上时如何暂停幻灯片自动播放?