Swift

【マテリアルデザイン】SwiftでRippleButtonを作成する

RippleButtonはボタンをタップしたときに波紋が広がるアニメーションの付いたボタンです。

この記事では、RippleButtonを実装する方法を紹介します。

RippleButton

RippleButtonのコード

UIButtonを継承して以下のように作成しました。

Storyboardやコード内でUIButtonを使用するのと同じように使用できます。

import UIKit

class RippleButton: UIButton {
    // ハイライトを無効化
    override var isHighlighted: Bool {
        didSet { if isHighlighted { isHighlighted = false } }
    }
    
    let rippleView = UIView()
    let rippleBackgroundView = UIView()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        initialize()
    }
    
    required init?(coder aDecorder: NSCoder) {
        super.init(coder: aDecorder)
        initialize()
    }
    
    private func initialize() {
        isExclusiveTouch = true // 同時タップを禁止する
        setupRipple()
        addTarget(self, action: #selector(touchDragEnter(_:forEvent:)), for: .touchDragEnter)
        addTarget(self, action: #selector(touchDragExit(_:)), for: .touchDragExit)
    }
    
    private func setupRipple() {
        rippleBackgroundView.alpha = 0
        rippleBackgroundView.clipsToBounds = true
        addSubview(rippleBackgroundView)
        // AutoLayout
        rippleBackgroundView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            rippleBackgroundView.leadingAnchor.constraint(equalTo: leadingAnchor),
            rippleBackgroundView.trailingAnchor.constraint(equalTo: trailingAnchor),
            rippleBackgroundView.topAnchor.constraint(equalTo: topAnchor),
            rippleBackgroundView.bottomAnchor.constraint(equalTo: bottomAnchor)
        ])
        
        setupRippleView()
    }
    
    private func setupRippleView() {
        rippleView.backgroundColor = UIColor(white: 1, alpha: 0.2)
        // Rippleが広がったときに、画面全体を覆う大きさを設定
        let screenSize = UIScreen.main.bounds
        let radius = (pow(screenSize.width, 2) + pow(screenSize.height, 2)).squareRoot()
        let diameter = 2 * radius
        rippleView.bounds.size = CGSize(width: diameter, height: diameter)
        rippleView.layer.cornerRadius = radius
        rippleView.clipsToBounds = true
        rippleBackgroundView.addSubview(rippleView)
    }
    
    override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
        let location = touch.location(in: self)
        animateRipple(location)
        return super.beginTracking(touch, with: event)
    }
    
    override func cancelTracking(with event: UIEvent?) {
        super.cancelTracking(with: event)
        animateToNormal()
    }
    
    override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
        super.endTracking(touch, with: event)
        animateToNormal()
    }
    
    @objc
    private func touchDragEnter(_ sender: UIButton, forEvent event: UIEvent) {
        let touches = event.touches(for: sender)
        if let location = touches?.first?.location(in: sender) {
            animateRipple(location)
        }
    }
    
    @objc
    private func touchDragExit(_ sender: UIButton) {
        animateToNormal()
    }
    
    private func animateRipple(_ point: CGPoint) {
        // Rippleの位置を設定
        rippleView.center = point
        // タップ直後のripple直径を44に設定
        let scale = 44 / max(rippleView.bounds.width, rippleView.bounds.height)
        rippleView.transform = CGAffineTransform(scaleX: scale, y: scale)
        
        UIView.animate(
            withDuration: 0.1,
            delay: 0,
            options: .allowUserInteraction,
            animations: { self.rippleBackgroundView.alpha = 1 },
            completion: nil
        )
        UIView.animate(
            withDuration: 1.0,
            delay: 0,
            options: [.curveEaseOut, .allowUserInteraction],
            animations: { self.rippleView.transform = .identity },
            completion: nil
        )
    }
    
    private func animateToNormal() {
        UIView.animate(
            withDuration: 0.7,
            delay: 0,
            options: .allowUserInteraction,
            animations: { self.rippleBackgroundView.alpha = 0 },
            completion: nil
        )
    }
}

Rippleの動きに細かな用件がある場合は調整が必要ですが、ライブラリーを使用するよりも自前で作成した方が調整しやすいかと思います。

-Swift

Copyright© てくてくライフ , 2020 All Rights Reserved.