Android Pieから影を自由に描画しやすくなってた

# 「年末でめっちゃ忙しいんだわ、3行で頼む」人向け

* Android Pie (9.0) から hardware-accelerated な描画方法が若干増えた :clap: 
* 影の描画もお手軽にできるようになったので Neumorphism でもやってみましょ
* サンプルコードはこちら [android-neumorphism-demo](https://github.com/cubenoy22/android-neumorphism-demo)

# はじめに

冒頭でも書きましたが [公式ページ](https://developer.android.com/guide/topics/graphics/hardware-accel) によると Android Pie  から実はグラフィックス周りが若干強化されました。個人的に `setShadowLayer` がサポートされたところが大きいと感じていて、非マテリアルデザインで影を多用するアプリの実装がとても容易になりそうだ、と考えています。

# Neumorphism の描画を簡単に実装してみた

最近ちらほら見かける [Neumorphism](https://uxdesign.cc/neumorphism-in-user-interfaces-b47cef3bf3a6) が影を多用したUIということなのでまずは [こちらのサイト](https://neumorphism.io/) の模倣を早速実装してみました。サンプルコードは冒頭にリンクを用意してありますのでよかったらみてみてください。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/282588/3f15b6d4-6fa8-76e5-8e3d-24e0ef806f6b.png" width=400 />

## コードの解説とポイント

本当はボタンなどのコンポーネントも作りたかったのですが時間が確保できなかったため、最も基本的なViewのみ作りました。

### NeumorphismView

上にも貼った [uxdesign.cc のページ](https://uxdesign.cc/neumorphism-in-user-interfaces-b47cef3bf3a6) から画像を拝借 :pray: 
![Shape](https://miro.medium.com/max/1400/1*-1NT6L5KepZbD19Rw77-EQ.jpeg) 

上記表示を実現するためのパラメータとして下記を用意しています:

|名前|型|説明|
|---|---|---|
| bgColor |Int | 上図の Main Background 、fillColor と同じに設定するのが基本そう |
| fillColor | Int | 上図の Shape Background |
| size | Int | neumorphism.io での Size パラメータ |
| shadowOffset| Int | neumorphism.io での Distance パラメータ |
| roundCornerRadius | Int | neumorphism.io での Radius パラメータ、 cornerRadius としたかったのですが XML のカスタム属性を設定しようとしたところ、名前がコンフリクトしたので round をつけました |
| shadowRadius | Int | neumorphism.io での Blur パラメータ |
| lightShadowColor | Int | 上図 Light Shadow の色 |
| lightShadowColor | Int | 上図 Dark Shadow の色 |
| borderColor | Int | 上図 Optional Border の色 |
| lightShadowColor | Int | 上図 Light Shadow の色 |

これらを使って角丸矩形を描画するだけです:

```kotlin
override fun onDraw(canvas: Canvas?) {
    canvas ?: return
    canvas.drawRect(0f, 0f, width + 0f, height + 0f, backgroundPaint)

    val cornerRadius = roundCornerRadius + 0f
    canvas.drawRoundRect(rect, cornerRadius, cornerRadius, lightShadowPaint)
    canvas.drawRoundRect(rect, cornerRadius, cornerRadius, darkShadowPaint)
    canvas.drawRoundRect(rect, cornerRadius, cornerRadius, borderPaint)
}
```

影は `setShadowLayer` を使うだけですね:

```kotlin
private fun updateLightShadow() {
    lightShadowPaint.color = fillColor
    lightShadowPaint.setShadowLayer(shadowRadius + 0f, 0f -shadowOffset, 0f -shadowOffset, lightShadowColor)
    invalidate()
}
```

ViewModel と ViewBinding を活用できるようカスタム属性も定義したので、今回のように動的にパラメータを切り替えるのも簡単です:

```xml
<jp.cubenoy22.neumorphism.NeumorphismView
    android:id="@+id/neumorphismView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:size="@{ viewModel.size }"
    app:shadowOffset="@{ viewModel.distance }"
    app:shadowRadius="@{ viewModel.blur }"
    app:roundCornerRadius="@{ viewModel.radius }"
    />
```

## パフォーマンスについて

RecyclerView で NeumorphismView を敷き詰めてみました:

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/282588/074086fe-4679-76cb-267e-61a6952f4d86.png" width=400 />

程よいミドルレンジであろう Pixel 3a@Android 11 で試したところ、約30ぐらいのビューが存在している状態になりましたがスクロールしても特に負荷が上がった様子は見られませんでした :sparkles: 

また軽く既存の Android 向け Neumorphism のライブラリの実装を探ってみたのですがOreo (8.1) 以下をサポートするためにビューごとに Bitmap へ描画して表示するという手法が取られていましたが、ビューの数が多いとメモリを多く消費しそうな予感がしました ([*1](https://github.com/fornewid/neumorphism/blob/master/neumorphism/src/main/java/soup/neumorphism/internal/shape/FlatShape.kt), [*2](https://github.com/sshadkany/Android_neumorphic/blob/master/android_neumorphic/src/main/java/com/github/sshadkany/neo.java)) 。最近はRAMをたくさん積んでるから大丈夫なのでしょうか。

ちなみにビットマップ上でのぼかし(blur)についてもいろいろやり方がありますが、今回 [StackBlur](http://www.quasimondo.com/StackBlurForCanvas/StackBlurDemo.html) という軽量（らしい）アルゴリズムを使ってCPUで演算しているライブラリがちょこちょこあるのに気づけたのも面白い発見でした:

* https://github.com/fornewid/neumorphism/blob/master/neumorphism/src/main/java/soup/neumorphism/internal/blur/BlurProvider.kt
* https://github.com/wasabeef/Blurry/blob/main/blurry/src/main/java/jp/wasabeef/blurry/Blur.java


# 最後に

もっと世の中に Android Pie 以降のデバイスが普及してリッチな描画ができたら幸せになれそうと感じました。終わり。