动画中的锚点 — anchorPoint

在layer中有一个叫做anchorPoint的属性,在苹果的官方文档中是这样定义的:

“You specify the value for this property using the unit coordinate space. The default value of this property is (0.5, 0.5), which represents the center of the layer’s bounds rectangle. All geometric manipulations to the view occur about the specified point. For example, applying a rotation transform to a layer with the default anchor point causes the layer to rotate around its center. Changing the anchor point to a different location would cause the layer to rotate around that new point.”

意为:你需要以单位坐标系的形式为这个属性赋值,它的默认值是(0.5,0.5),也就是这个图层的中心点。在这个视图上的所有图形变换都与这个点有关。例如,将旋转变换应用于默认锚点的图层上,图层围绕其中心旋转。而将锚点更改为另一个点则将导致图层围绕那个点旋转。

这段话不难理解,但是实际使用anchorPoint时,会出现一些苹果在文档中没有提到的情况。

anchorPoint的坐标系

在iOS上与macOS上采取了不同的坐标系,iOS中(0,0)在左上角,(1,1)在右下角,而macOS中(0,0)在左下角,(1,1)在右上角,这一点苹果并没有在anchorPoint的注释中做区分,而是统一解释为macOS的情况,不确定是不是忘了加。

修改anchorPoint会导致frame变化

我们先来看一段代码

    var btn: UIButton = UIButton()
    
    var viewA: UIView = UIView()
    
    override func viewDidLoad() {
        setupUI()
    }
    
    func setupUI(){
        view.backgroundColor = .white
        
        btn.backgroundColor = .green
        btn.addTarget(self, action: #selector(popOrDismissView), for: .touchUpInside)
        btn.frame = CGRect(x: 100, y: 100, width: 40, height: 40)
        view.addSubview(btn)
        
        viewA.backgroundColor = .red
        viewA.layer.anchorPoint = CGPoint(x: 0, y: 0)
        viewA.frame = CGRect(x: 140, y: 140, width: 100, height: 100)

        viewA.alpha = 0
    }
    
    @objc func popOrDismissView(){
        if viewA.superview == nil {
            view.addSubview(viewA)
            viewA.transform = CGAffineTransform(scaleX: 0.5, y: 0.5)
            UIView.animate(withDuration: 0.25) {
                self.viewA.transform = CGAffineTransform(scaleX: 1, y: 1)
                self.viewA.alpha = 1
            } completion: { (completed) in
                
            }
        }else{
            UIView.animate(withDuration: 0.25) {
                self.viewA.transform = CGAffineTransform(scaleX: 0.5, y: 0.5)
                self.viewA.alpha = 0
            } completion: { (completed) in
                self.viewA.removeFromSuperview()
            }
        }
    }

在这段代码的效果是让红色视图viewA从绿色按钮btn的右下角位置弹出,这并没有什么问题,然而如果在这里将anchorPoint的赋值移到frame赋值之后则会发现,viewA的弹出位置改变了,打断点后发现它的frame变成了

(190 190; 100 100)

发现是origin改变了。

联系anchorPoint的变化观察origin的变化

anchorPoint: (0.5, 0.5) -> (0, 0)
origin:      (140, 140) -> (190, 190)

也就是说origin移动到了原本anchorPoint所在的位置,即原视图的中心点。

对于这个现象的解释是,修改anchorPoint的值后,frame会自动调整,使anchorPoint在该视图的父视图中的位置保持不变。

那么是什么让anchorPoint相对于父视图的位置保持不变,这就引申出了另一个属性position,以下是苹果对position的解释:

The value of this property is specified in points and is always specified relative to the value in the anchorPoint property. For new standalone layers, the default position is set to (0.0, 0.0). Changing the frame property also updates the value in this property.

意为: 这个属性的值是以points为单位,而它总是与anchorPoint的值相关联,对于一个新的独立图层而言,position的默认值是(0.0, 0.0)。改变frame也会更新这个属性的值

文档中没有提到position与anchorPoint的关系,经过实验后发现position代表anchorPoint在该视图的父视图中的位置。

比较关键的一点是position并不会随anchorPoint的值自动变化,相对的origin是会随anchorPoint变化的。 因此origin向右下角移动而保证了anchorPoint还是在position上。

如何使用anchorPoint

了解原理后我们就有两种方法来保证在anchorPoint改变后令原视图的frame不改变了。

  • 提前记录原本的frame值,在anchorPoint改变后还原frame。
  • 计算anchorPoint在父视图中的位置,还原position的值

代码如下:

//extension UIView

    func setAnchorPoint(_ anchorPoint: CGPoint){
        let tempFrame = self.frame
        self.layer.anchorPoint = anchorPoint
        self.frame = tempFrame
    }
    
    ///根据anchorPoint与默认值(中心点)的差值计算原本position的位置
    func correctPosition(){
        self.layer.position = CGPoint(x: self.frame.origin.x + (self.layer.anchorPoint.x - 0.5) * bounds.size.width, y: self.frame.origin.y + (self.layer.anchorPoint.x - 0.5) * bounds.size.height)
    }

在使用frame布局的情况下这两种方法都可以用,但如果使用的是snapKit、masonry,或者其他autolayout的方式布局,在layout之后frame会被自动更新,因此只能在每次layoutIfneeded后加上correctPosition()。

本篇到此为止,希望这对你有帮助,如果有错误或是有需要补充的地方,望告知。


种一棵树最好的时间是在十年前,而后是现在。

Loading Disqus comments...
Table of Contents