LoginSignup
4
2

More than 3 years have passed since last update.

NeumorphismなUIButton

Last updated at Posted at 2020-05-30

gifは、影の大きさを調節する関数を実装する前のものです。

はじめに

NeumorphismなUIButtonを作ったときのことを記録に残しておきます。
index.gif

このButtonのしくみ

見てもらうとすぐわかるんですが、
image.png
クリックされていないときは、このボタンは

  • ボタンの外側の上と左に光があたっている。
  • ボタンの外側の下と右に影がついている。

というような特徴をもっています。
それだけでなく、

image.png

  • 押されているときはボタンの内側の上と左に影がついている。
  • 押されているときはボタンの内側の下と右に光があたっている。

というような特徴をもっています。

この計4つの特徴さえわかってしまえば、あとはそのとおりにUIButtonをいじっていくだけです。

実際のコード

実際のコードを公開します。

  • Colors.swift
  • PlainSquareButton.swift

という2つのファイルを使って実装しました。

Colors.swift
import Foundation
import UIKit

class Colors {

    static var plainColor = UIColor(hex: "ECF0F3")
}

extension UIColor {

    convenience init(hex: String, alpha: CGFloat = 1.0) {

        let v = Int("000000" + hex, radix: 16) ?? 0
        let r = CGFloat(v / Int(powf(256, 2)) % 256) / 255
        let g = CGFloat(v / Int(powf(256, 1)) % 256) / 255
        let b = CGFloat(v / Int(powf(256, 0)) % 256) / 255
        self.init(red: r, green: g, blue: b, alpha: min(max(alpha, 0), 1))
    }

    func brighter() -> UIColor {
        var hue: CGFloat = 0,
            saturation: CGFloat = 0,
            brightness: CGFloat = 0,
            alpha: CGFloat = 0
        if getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) {
            return UIColor(hue: hue, saturation: saturation * 0.85, brightness: brightness * 1.1, alpha: alpha)
        } else {
            return self
        }
    }

    func darker() -> UIColor {
        var hue: CGFloat = 0,
            saturation: CGFloat = 0,
            brightness: CGFloat = 0,
            alpha: CGFloat = 0
        if getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) {
            return UIColor(hue: hue, saturation: saturation * 1.25, brightness: brightness * 0.75, alpha: alpha)
        } else {
            return self
        }
    }
}
PlainSquareButton.swift
import Foundation
import UIKit

class PlainSquareButton: UIButton {

    private let highlightLayer = CALayer(),
                shadowLayer = CALayer()
    private let dentedHorizontalLayer = CAGradientLayer(),
                dentedVerticalLayer = CAGradientLayer()

    required init?(coder: NSCoder) {

        super.init(coder: coder)
        self.layer.cornerRadius = 5.0
        self.backgroundColor = Colors.plainColor
        self.putHighlight()
        self.putShadow()
        self.addTarget(self, action: #selector(self.onPushed), for: .touchDown)
        self.addTarget(self, action: #selector(self.onReleased), for: .touchUpInside)
    }

    private func putHighlight() {

        self.highlightLayer.masksToBounds = false
        self.highlightLayer.frame = self.bounds
        self.highlightLayer.backgroundColor = Colors.plainColor.cgColor
        self.highlightLayer.shadowColor = Colors.plainColor.brighter().cgColor
        self.highlightLayer.cornerRadius = 5.0
        self.highlightLayer.shadowOpacity = 0.75
        self.highlightLayer.shadowOffset = CGSize(width: -6, height: -6)
        self.highlightLayer.shadowRadius = 5.0
        self.layer.addSublayer(self.highlightLayer)
    }

    private func putShadow() {

        self.shadowLayer.masksToBounds = false
        self.shadowLayer.frame = self.bounds
        self.shadowLayer.backgroundColor = Colors.plainColor.cgColor
        self.shadowLayer.shadowColor = Colors.plainColor.darker().cgColor
        self.shadowLayer.cornerRadius = 5.0
        self.shadowLayer.shadowOpacity = 0.65
        self.shadowLayer.shadowOffset = CGSize(width: 6, height: 6)
        self.shadowLayer.shadowRadius = 5.0
        self.layer.addSublayer(self.shadowLayer)
    }

    @objc dynamic func onPushed() {

        self.highlightLayer.removeFromSuperlayer()
        self.shadowLayer.removeFromSuperlayer()
        self.putDentedVerticalLayer()
        self.putDentedHorizontalLayer()
    }

    private func putDentedVerticalLayer() {

        self.dentedVerticalLayer.cornerRadius = 5.0
        self.dentedVerticalLayer.frame = self.bounds
        self.dentedVerticalLayer.colors = [
            Colors.plainColor.darker().cgColor,
            Colors.plainColor.cgColor,
            Colors.plainColor.cgColor,
            Colors.plainColor.brighter().cgColor
        ]
        self.dentedVerticalLayer.locations = [
            0,
            0.15,
            0.85,
            1
        ]
        self.dentedVerticalLayer.opacity = 1
        self.layer.insertSublayer(self.dentedVerticalLayer, at: 0)
    }

    private func putDentedHorizontalLayer() {

        self.dentedHorizontalLayer.cornerRadius = 5.0
        self.dentedHorizontalLayer.frame = self.bounds
        self.dentedHorizontalLayer.colors = [
            Colors.plainColor.darker().cgColor,
            Colors.plainColor.cgColor,
            Colors.plainColor.cgColor,
            Colors.plainColor.brighter().cgColor
        ]

        self.dentedHorizontalLayer.locations = self.getProperWidthLocations(size: self.bounds.size)
        self.dentedHorizontalLayer.startPoint = CGPoint(x: 0, y: 0)
        self.dentedHorizontalLayer.endPoint = CGPoint(x: 1, y: 0)
        self.dentedHorizontalLayer.opacity = 0.5
        self.layer.insertSublayer(self.dentedHorizontalLayer, at: 1)
    }

    @objc dynamic func onReleased() {

        self.dentedVerticalLayer.removeFromSuperlayer()
        self.dentedHorizontalLayer.removeFromSuperlayer()
        self.putHighlight()
        self.putShadow()
    }

    private func getProperWidthLocations(size: CGSize) -> [NSNumber] {
        if (size.width >= size.height*2 &&
            size.width < size.height*3) {
            return [
                0,
                0.075,
                0.925,
                1
            ]
        } else if (size.width >= size.height*3 &&
            size.width < size.height*4) {
            return [
                0,
                0.05,
                0.95,
                1
            ]
        } else if (size.width >= size.height*4 &&
            size.width < size.height*5) {
            return [
                0,
                0.0325,
                0.9675,
                1
            ]
        } else {
            return [
                0,
                0.15,
                0.85,
                1
            ]
        }
    }

    private func getProperHeightLocations(size: CGSize) -> [NSNumber] {
        if (size.width*2 >= size.height &&
            size.width*3 < size.height) {
            return [
                0,
                0.075,
                0.925,
                1
            ]
        } else if (size.width*3 >= size.height &&
            size.width*4 < size.height) {
            return [
                0,
                0.05,
                0.95,
                1
            ]
        } else if (size.width*4 >= size.height &&
            size.width*5 < size.height) {
            return [
                0,
                0.0325,
                0.9675,
                1
            ]
        } else {
            return [
                0,
                0.15,
                0.85,
                1
            ]
        }
    }
}

解説

まず、イニシャライザでputHighlight()というメソッドと、putShadow()というメソッドを呼び出します。それぞれのメソッドで光と影をボタンの外側につけます。

putHighlight()では、plainColorを少し明るくしたものを、ボタンから(-6, -6)くらいの位置まで届くように表示しています。
逆にputShadow()では、plainColorを少し暗くしたものを、(6, 6)くらいの位置まで届くように表示しています。
すると、とりあえずこの画像の状態が完成します。

image.png

凹ませた状態を作るのがちょっと厄介で、
まずCAGradientLayerを使って、始点の方に光、終点の方に影があたるようにグラデーションを設定します。
これを縦と横につけるのですが、locationsの数値を一緒にしていると、影の長さが縦と横で合わなくなってしまうので、ざっくりと合うようにgetProperHeightLocations()のようなメソッドを用意しておきました。
そして作成されたレイヤを貼り付けることで、凹んだ状態を再現できます。
表示されていると困る方のレイヤがremoveしてしまいましょう。

image.png

これで、gifのようなボタンが再現できると思います。

おわり

けっこう楽しいので、この調子で色々な部品を作ってみようと思います。

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2