CAShapeLayer, CABasicAnimation을 이용하여 버튼 누를 시 원이 그려지는 애니메이션 + 애니메이션이 완료되었을 때 원하는 작업 수행하기

CAShapeLayer: 좌표 공간에서 벡터 기반의 경로를 그릴 수 있습니다. 그린 경로를 이용하여 여러가지 작업을 할 수 있으며, 이를 애니메이션을 이용해 표현 할 수 있습니다.

 

CABasicAnimation: 레이어를 대상으로 단일 키프레임 애니메이션 기능을 제공해주는 객체 입니다.

 

누를 시 감싸는 애니메이션 생성

  • 커스텀 버튼 생성
class LongPressGestureButton: UIButton {
}

애니메이션을 넣을 CAShapeLayer 생성

class LongPressGestureButton: UIButton {
	let sliceLayer = CAShapeLayer()
}

애니메이션 생성. UIBezierPath를 이용해 어떤식으로 그릴건지 정의. 그 후 CABasicAnimation 생성 한 후 CAShapeLayer 객체에 add.

 

CABasicAnimation의 keypath는 CAShapeLayer의 스타일 프로퍼티,CALayer의 Modifying the Layer's Appearance등 다양한 값을 애니메이션에 적용 할 수 있다. 공식문서에서 Animatable이 붙어 있다면 애니메이션으로 적용 가능합니다.

 

key값으로 사용한 strokeEnd는 그릴 경로의 위치값입니다. 0.0 ~ 1.0 사이의 값이어야 하고, 1.0이면 모두 그리는 것입니다. 따라서 BezierPath를 그리는 애니메이션을 만들 수 있는 거죠. fromValue는 시작할 값, toValue는 종료할 값입니다. fromValue가 0이고 endValue가 1, duration이 2 이니 BezierPath를 그리는 애니메이션을 처음부터 끝까지 2초동안 실행하게 됩니다.

@objc func startAnimation() {
		//애니메이션 적용시킬 선 생성
        let path = UIBezierPath(arcCenter: center, radius: frame.width/2+2, startAngle: startEngle, endAngle: endAngle, clockwise: true)
        sliceLayer.path = path.cgPath
        sliceLayer.fillColor = nil
        sliceLayer.strokeColor = UIColor.black.cgColor
        sliceLayer.lineWidth = 4
        layer.addSublayer(sliceLayer)
        
        //애니메이션 생성 및 적용
        let animation = CABasicAnimation(keyPath: "strokeEnd")
        animation.fromValue = 0
        animation.toValue = 1
        animation.duration = 2
        animation.delegate = self
        sliceLayer.add(animation, forKey: animation.keyPath)
    }

이제 버튼을 누르고 있을 때 해당 애니메이션이 실행되도록합니다.

addTarget(self, action: #selector(startAnimation), for: .touchDown)

손가락을 떼었을 때 애니메이션 초기화

손가락을 떼었을때, 버튼에서 그리고 있던 선을 제거되있는 상태여야 합니다. 일단 그리는 애니메이션을 제거합니다.

@objc func stopAnimation() {
        sliceLayer.removeAllAnimations()
    }

그리고 startAnimation에서 하나를 추가합니다.

@objc func startAnimation() {
		//애니메이션 적용시킬 선 생성
        let path = UIBezierPath(arcCenter: center, radius: frame.width/2+2, startAngle: startEngle, endAngle: endAngle, clockwise: true)
        sliceLayer.path = path.cgPath
        sliceLayer.fillColor = nil
        sliceLayer.strokeColor = UIColor.black.cgColor
        sliceLayer.lineWidth = 4
        sliceLayer.strokeEnd = 0 //추가
        layer.addSublayer(sliceLayer)
        
        //애니메이션 생성 및 적용
        let animation = CABasicAnimation(keyPath: "strokeEnd")
        animation.fromValue = 0
        animation.toValue = 1
        animation.duration = 2
        sliceLayer.add(animation, forKey: animation.keyPath)
    }

strokeEnd는 그리는 경로가 중지될 때, 어느 위치까지 경로를 그리느냐를 정합니다. stopAnimation()에서 애니메이션을 제거했으니, 경로를 0으로 설정해서 선이 그려지지 않도록 해야 합니다. 

 

이 동작은 버튼에서 손을 떼면 동작되야 하니 touchUpInside를 적용하도록 합니다.

addTarget(self, action: #selector(stopAnimation), for: .touchUpInside)

 

애니메이션이 완료되면 원하는 동작 수행

CAAnimationDelegate를 채택해줍니다. 

class LongPressGestureButton: UIButton, CAAnimationDelegate {
.
.
.

@objc func startAnimation() {
		//애니메이션 적용시킬 선 생성
        let path = UIBezierPath(arcCenter: center, radius: frame.width/2+2, startAngle: startEngle, endAngle: endAngle, clockwise: true)
        sliceLayer.path = path.cgPath
        sliceLayer.fillColor = nil
        sliceLayer.strokeColor = UIColor.black.cgColor
        sliceLayer.lineWidth = 4
        sliceLayer.strokeEnd = 0 //추가
        layer.addSublayer(sliceLayer)
        
        //애니메이션 생성 및 적용
        let animation = CABasicAnimation(keyPath: "strokeEnd")
        animation.fromValue = 0
        animation.toValue = 1
        animation.duration = 2
        animation.delegate = self
        sliceLayer.add(animation, forKey: animation.keyPath)
    }
    
    }

CAAnimationDelegate의 animationDidStop는 애니메이션이 중지되거나 완료되면 호출되는 함수 입니다. flag의 값이 true이면 완료된 것이고 false는 완료되지 못하고 중지된 것입니다. 저는 애니메이션이 완료 되면 어떤 작업을 수행하는거니 flag의 값이 true이면 동작하도록 합니다. 

func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
        if flag {
            self.delegate?.animationComplete()
        }
    }

원하는 작업을 수행하는 작업을 클로저로 넘겨도 되고, delegate로 해도 되고, notifycenter로 해도 되지만 저는 delegate 패턴을 이용해서 구현했습니다.