【Jetpack Compose】Pickerを自作した話

# Pickerが無い…

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440643/b2bc9e40-6a03-fb6e-b9f8-8e0b6b931a67.png" width="200">


iOSではドラムロールとも呼ばれますが、主に数字など特定の範囲かつ連続する値からユーザーが選択するUIコンポーネントです。Androidでは `NumberPicker`　として View 時代から多くの開発者が慣れ親しんでいると思います。

https://developer.android.com/reference/android/widget/NumberPicker

ところが近年 Android で主流になりつつある UI フレームワーク Jetpack Compose では、この `NumberPicker` に相当するコンポーネントが標準で用意されていません。そもそも Jetpack Compose のデザインシステムである Material(3) Design に定義すらされていません（特定用途の **Picker はありますが）。

https://m3.material.io/components/time-pickers/overview

https://m3.material.io/components/date-pickers/overview


# 本当に必要か？

標準で用意されていないので自作しよう、と話を進めるのは早計かもしれません。

### Pickerの不在
[Accompanist](https://github.com/google/accompanist) が全盛だったような Jetpack Compose の黎明期であれば、単純にまだ実装されていない説もありますが、流石に現在でも未実装ですので今後も追加は無いと見ていいでしょう。そして公式が用意していない以上、それなりの理由があると考えるのが自然です。

### 用途の確認と代替の検討

Picker の用途として比較的多いのが日付・時刻の選択かと思います。特にiOSではこの用途でドラムロールが多用されてる印象で、Androidも見た目を合わせるなら `NumberPicker` を選択せざるを得ません。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440643/457cf0da-274f-6b01-5411-570482945ac1.png" width="300">


ところで Android（Material Design）に限定すれば日付・時刻を選択する専用のUIコンポーネント DatePicker, TimePicker が用意されています。カレンダーやアナログ時計のアナロジーでより直感的に操作できるため、ユーザー体験も良さそうです。Material Design 的には「日付・時刻の専用コンポーネントを用意したのだから、従来の `NumberPicker` は不要」という設計なのでしょう。しかし実際のアプリ開発ではiOSも含めたデザイン設計が重要であり、「iOSと同じで」と言われたら `NumberPicker` 相当の実装が必要でしょう。


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440643/b05bb4cf-bd5d-43fd-620d-5fc753871ae9.png" width="300">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440643/6fd2ba0f-1e64-7bb6-4cf9-478d872627a8.png" width="276">

### 用途・目的の再確認

どうしても `NumberPicker` が Jetpack Compose でも必要な場合を仮定して、他のフォームUIとの用途・目的を差別化しておきます。

| | `TextField` | `NumberPicker` | `DropdownMenu` |  
|:----:|:-----:|:----:|:-----:|  
| イメージ | ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440643/a18402b2-4f5a-7878-8656-30dcbb16d331.png) | ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440643/d2a67699-c760-5af7-e55d-e489d0a63124.png) | ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440643/3ad2261b-0db0-16e1-be62-e499f3237b8b.png)|  
| 入力値の範囲 | 大 | 中 | 小〜中 |  
| 入力値の連続性 | なし | 強 | 弱 |  
| 使用例 | 電話番号、郵便番号 | 日付、時刻 | 数量、種別 |  

# Pickerを自作する

3rdパーティー製であれば Compose 対応の `NumberPicker` は多数存在していますが、以下の条件をすべて満たす実装がなかなか見つけられず自作に踏み切りました。

- 基本的な選択機能
- `NumberPicker`と同等のスクロール操作感・Snap/Flingアニメーション
- Material3 のカラースキーム対応
- ラベル部分のカスタム可能性
- NestedScrollと競合しない

https://github.com/Seo-4d696b75/compose-m3-picker


![sample_picker.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440643/e60fb0b4-4f55-077a-5682-d2d9d1f29c64.gif)



## 実装の紹介

実装の詳細はソースコードを直接見てもらうとして、ここでは概略的な説明にとどめておきます。

| 機能 | 実装方法 |  
|:-----:|:---------|
| ラベル表示 | 効率性を重視して`LazyLayout`を採用し、表示するラベルを必要なときだけ compostion & レイアウトします。|  
| スクロール | ジェスチャーの検出は`Modifier.pointerInput()`に任せますが、スクロール位置の管理＆描画への反映は独自の状態クラスで行います。|  
| snap,flingアニメーション | `SnapFlingBehavior`を利用。アニメーション後のスナップ位置を指定する`SnapLayoutInfoProvider`を自前で実装する必要があります。|  


<details>
<summary>余談：ラベル部分のPager実装を見送った理由</summary>

上下にスクロールできるラベル部分は`VerticalPager`で十分な気がしますが、縦スクロール可能な領域内に picker を配置したとき思わぬ不都合がありました。というのも Pager は NestedScroll に対応しているため、Pager実装の picker を操作していると全体の縦スクロールが動いてしまう場合があります。また TopBar の collapsing も動いたりします。

![sample_picker_pager.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440643/2ce24ad8-9613-098b-abbc-b9efe0ba2ef2.gif)


通常の Pager であれば期待通りの挙動ですが、pickerはコンテナ要素ではなく単一のフォーム要素であり、他の縦スクロールと連動するのは不自然です。`Modifier.scrollable()`でスクロールが実装されている以上この問題は不可避（多分）のため Pager 実装は見送りました。

</details>