UIView に elevation っぽさを付ける
この記事は Goodpatch Advent Calendar 2019 の23日目です.
iOS アプリの UI デザインで elevation について意識することは Android の場合と比較すると少ないと思います. ただし,Material Design 自体はプラットフォームを限定せずに適用可能な考え方であり,elevation は Material Components を使うかどうかを別としても UI をデザインするために有用です. ということで UIView で elevation を表現する方法について Material Components も参考にしながら実装してみました.
概要
- UIView の CALayer に影を付けて elevation っぽい表現をする
- Elevation の値に応じて影の radius や offset を設定する
- 影は ambient と key の2種類をつける
という方針で実装しました.
環境
- Xcode 11.0
- iOS Simulator 13.0
MDC の影を試す
Material Components for iOS の影がどのような挙動になっているのかデモを作って確認してみます.
UIView に影をつける
Material Components (MDC) を参考にしながら,UIView に影をつけていきます.
Radius を設定する
まずは UIView の layer に最も単純な形で影をつけてみます. とりあえず elevation の高さ分だけ shadowRadius をつけてみます.
class CardView: UIView {
func setShadowElevation(_ elevation: CGFloat) {
let cornerRadius: CGFloat = 4
self.layer.cornerRadius = cornerRadius
self.layer.shadowColor = UIColor.black.cgColor
self.layer.shadowRadius = elevation
self.layer.shadowOffset = .zero
self.layer.shadowOpacity = 0.33
}
}
UIView の方はカードの中心(よりやや下?)に点光源があるような影のつき方で,MDC のような強い elevation は感じられません.
Offset を変更する
続いて shadowOffset も設定してみます. とりあえず elevation の高さ分だけ Y 軸方向にずらします.
self.layer.shadowOffset = CGSize(width: 0, height: elevation)
だいぶそれらしくなりました. ぱっと見はどちらも区別がつかないかもしれません. ただし,よく見てみると,なんとなく MDC の方が「カード」らしい存在感があるようにも見えます. 特に elevation が小さいときが顕著です.
そこで elevation = 1
の両者を拡大して並べてみると,MDC の方は edge 付近の影が濃くなっていることが分かります.
Top | Bottom |
---|---|
MDC(左半分)の方が影の領域が広い. | MDC(左半分)は edge 付近の影がより濃い.薄くなっていく勾配も線形ではない. |
2つの影をつける
Material Design Guideline の Light and shadows を読むと,Material Design では ambient と key の2種類の影が必要なことが分かります.
その実装である MDCShadowLayer.m を読むと,ガイドラインどおり ambient と key の2種類の影が設定されていることが分かります. Ambient は elevation によって offset が変わらない影で,opacity もかなり薄くつけられています. また Radius の値は elevation の一次関数で決まっていますが,ambient と key でそれぞれに係数が調整されています.
Ambient Opacity | Key Opacity |
---|---|
0.08 | 0.26 |
Elevation | Ambient Radius | Key Radius | Key Offset |
---|---|---|---|
0 | -.– | -.– | -.– |
1 | 0.88 | 0.66 | 1.19 |
2 | 1.77 | 1.33 | 2.42 |
3 | 2.66 | 1.99 | 3.65 |
5 | 4.44 | 3.33 | 6.11 |
8 | 7.11 | 5.33 | 9.81 |
13 | 11.56 | 8.66 | 15.96 |
というわけで,ambient 用の CALayer を subLayer として追加します.
class CardView: UIView {
private weak var ambientShadowLayer: CALayer?
let maxElevation: CGFloat = 16
func setShadowElevation(_ elevation: CGFloat) {
let cornerRadius: CGFloat = 4
self.layer.cornerRadius = cornerRadius
self.layer.shadowColor = UIColor.black.cgColor
self.layer.shadowRadius = elevation
self.layer.shadowOffset = CGSize(width: 0, height: elevation)
self.layer.shadowOpacity = 0.33
let ambientShadowLayer = self.ambientShadowLayer ?? { (view: UIView) -> CALayer in
let superLayer = view.layer
let layer = CAShapeLayer()
layer.bounds = superLayer.bounds
layer.anchorPoint = .zero
let path = UIBezierPath(roundedRect: superLayer.bounds, cornerRadius: superLayer.cornerRadius)
layer.path = path.cgPath
layer.fillColor = view.backgroundColor?.cgColor
let maskPath = UIBezierPath(rect: superLayer.bounds.insetBy(dx: -maxElevation * 2, dy: -maxElevation * 4))
maskPath.append(path)
let mask = CAShapeLayer()
mask.path = maskPath.cgPath
mask.bounds = superLayer.bounds
mask.anchorPoint = .zero
mask.fillRule = .evenOdd
mask.fillColor = view.backgroundColor?.cgColor
layer.mask = mask
view.layer.addSublayer(layer)
return layer
}(self)
self.ambientShadowLayer = ambientShadowLayer
ambientShadowLayer.shadowColor = UIColor.black.cgColor
ambientShadowLayer.shadowRadius = elevation
ambientShadowLayer.shadowOffset = .zero
ambientShadowLayer.shadowOpacity = 0.11
}
}
レイヤーに設定した影が描画されるためにはレイヤーに塗りが必要ですが,レイヤーに塗りがあると元の UIView の描画を邪魔してしまうので mask を作って元の UIView 部分をくり抜いて透明化しています.
かなりそれらしくなりました. あとは opacity や offset の係数を調整して各々のいい感じに調整していけば良さそうです.
One More Thing
Ambient と key の2つの影を設定できるようになったので,それぞれに black 以外の色を指定することもできます. 試しに orange を設定してみると次のようになりました.
アプリの性格やコンテンツの内容に合わせて色付きの影(ただし,もっとさりげない色で)を入れてみるのも良さそうです.
まとめ・感想
UIView で Material Design のような elevation の表現を実装する方法をまとめました.
- Elevation によって影の radius と offset を設定した
- Ambient と key の2つの影を設定した
- Ambient は元の UIView のサブレイヤーとして追加した
- そのままだと UIView の描画を邪魔するのでマスクをかけた
コスパを重視するなら key シャドーだけでも良さそうですが,ambient が加わるだけでカードの存在感が増したり,表現の幅が広がったりして面白いです.