본문 바로가기
iOS/예제

[iOS 예제] Ripple Effect

by Sky Titan 2022. 6. 19.
728x90
 

Material Design

Build beautiful, usable products faster. Material Design is an adaptable system—backed by open-source code—that helps teams build high quality digital experiences.

material.io

Ripple Effect

  • 아래의 이미지와 같이 버튼이나 뷰를 클릭했을 때 마치 물결이 퍼지듯 원형 모향의 애니메이션이 퍼져나가면서 사용자에게 클릭을 인지시키는 효과
  • 위의 구글 material에서 pod으로 다운받아 사용해도 된다.

 

Code

  • UIView을 상속받은 protocol을 만들어서, 최대한 재사용이 가능하게끔 만듬
  • 원리는 아래와 같다.
    1. UIView의 touchesBegan에서 rippleLayer의 setRippleAt 메서드를 호출한다.
    2. 터치한 지점의 CGPoint를 기준으로 원형의 CAShapeLayer를 만들어서 추가한다.
    3. 추가한 circleLayer에 alpha애니메이션, scale 애니메이션을 넣는다.
  •  예제: https://github.com/Sky-Titan/RippleEffectExample
import UIKit

// MARK: RippledProtocol
protocol RippledProtocol: UIView {
    /// ripple 효과 색상
    var rippleColor: UIColor? { get set }
    /// ripple 효과의 alpha값
    var rippleAlpha: CGFloat { get set }
    var rippleLayer: RippleLayer { get }
    
    func touchesBeganForRipple(_ touches: Set<UITouch>, with event: UIEvent?)
}
extension RippledProtocol {
    var rippleLayer: RippleLayer {
        layer as! RippleLayer
    }
    var rippleColor: UIColor? {
        get {
            rippleLayer.rippleColor
        }
        
        set {
            rippleLayer.rippleColor = newValue
            setNeedsDisplay()
        }
    }
    var rippleAlpha: CGFloat {
        get {
            rippleLayer.rippleAlpha
        }
        
        set {
            rippleLayer.rippleAlpha = newValue
            setNeedsDisplay()
        }
    }
    
    func touchesBeganForRipple(_ touches: Set<UITouch>, with event: UIEvent?) {
        if let touch = touches.first {
            let point = touch.location(in: self)
            rippleLayer.setRippleAt(point)
        }
    }
}

// MARK: RippleLayer
class RippleLayer: CALayer {
    var rippleColor: UIColor? = .white
    var rippleAlpha: CGFloat = 0.3
    
    private var circleLayer: CAShapeLayer?
    
    fileprivate func setRippleAt(_ point: CGPoint) {
        self.circleLayer?.removeFromSuperlayer()
        
        let radius = radiusForRippleCircle(point)
        
        //ripple 효과를 보여줄 원형 layer
        let circle = CAShapeLayer()
        circle.anchorPoint = CGPoint(x: 0.5, y: 0.5)
        circle.position = CGPoint(x: point.x, y: point.y)
        circle.path = UIBezierPath(arcCenter: CGPoint(x: circle.frame.width / 2, y: circle.frame.height / 2), radius: radius, startAngle: 0, endAngle: .pi * 2, clockwise: true).cgPath
        circle.fillColor = rippleColor?.cgColor
        circle.opacity = 0
        
        self.masksToBounds = true
        self.addSublayer(circle)
        self.circleLayer = circle
        
        circle.add(rippleAnimation(), forKey: "ripple")
    }
    
    //터치한 위치를 기준으로 View의 상하좌우 간격 중 가장 긴 간격이 radius
    private func radiusForRippleCircle(_ point: CGPoint) -> CGFloat {
        let distanceX: CGFloat = max(self.frame.width - point.x, point.x)
        let distanceY: CGFloat = max(self.frame.height - point.y, point.y)
        return max(distanceX, distanceY)
    }
    
    //완성된 ripple Animation
    private func rippleAnimation() -> CAAnimationGroup {
        let animationGroup: CAAnimationGroup = CAAnimationGroup()
        animationGroup.duration = 0.45
        animationGroup.fillMode = .forwards
        
        let showingDuration: CGFloat = 0.3
        animationGroup.animations = [sizingAnimation(duration: showingDuration),
             showingAlphaAnimation(duration: showingDuration),
                                     hidingAlphaAnimation(beginTime: showingDuration, duration: 0.15)
        ]
        animationGroup.beginTime = CACurrentMediaTime()
        return animationGroup
    }
    
    //circle 등장 시 사이즈 확장 애니메이션
    private func sizingAnimation(duration: CGFloat) -> CABasicAnimation {
        let animation = CABasicAnimation(keyPath: "transform.scale")
        animation.fromValue = 0
        animation.toValue = 1
        animation.duration = duration
        animation.beginTime = 0
        animation.fillMode = .forwards
        return animation
    }
    
    //circle 등장 시 alpha 애니메이션
    private func showingAlphaAnimation(duration: CGFloat) -> CABasicAnimation {
        let animation = CABasicAnimation(keyPath: "opacity")
        animation.fromValue = 0
        animation.toValue = rippleAlpha
        animation.duration = duration
        animation.beginTime = 0
        animation.fillMode = .forwards
        return animation
    }
    
    //circle 사라질 시 alpha 애니메이션
    private func hidingAlphaAnimation(beginTime: CGFloat, duration: CGFloat) -> CABasicAnimation {
        let animation = CABasicAnimation(keyPath: "opacity")
        animation.fromValue = rippleAlpha
        animation.toValue = 0
        animation.duration = duration
        animation.beginTime = beginTime
        animation.fillMode = .forwards
        return animation
    }
}


// MARK: RippledButton
class RippledButton: UIButton, RippledProtocol {
    
    override class var layerClass: AnyClass {
        RippleLayer.self
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        self.touchesBeganForRipple(touches, with: event)
    }
}

 

728x90

댓글