【個人開発】ダイアル錠をアプリで再現してみた【Flutter】【ListWheelScrollView】

こんにちは、海岸蒼です。

本記事は、iOS,Android向けのモバイルアプリとして、
ダイアル錠を再現してみたのでそのアプリの紹介と、
Flutter,ListWheelScrollViewのノウハウの共有を記載します。

読んでみて、また実際にアプリをダウンロードして触ってみていただけますと嬉しいです。

<h2>作ったアプリの紹介</h2>

**Brute Forceというアプリになります。**
<img width="1000" alt="TwitterPR.001.jpeg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1694109/037767c9-2f41-2da5-bd05-bcdaf667fd74.jpeg">

iOS : https://apps.apple.com/jp/app/brute-force-puzzle/id1586258328
Android : https://play.google.com/store/apps/details?id=com.zerokaraapp.bruteforce

ダイアルを回して正しい番号に揃え、鍵を開けるゲームです。
ダイアルを全通り試し、"Brute Force = 総当たり"で鍵を開けることを目指します。

これだけだとアプリの最小要件に引っ掛かると思ったので、

<img width="500" alt="Android_ja.002.jpeg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1694109/5cac2b80-e586-e1fe-dd73-b56ae180d881.jpeg">

ダイアル数を増やしたバージョンを作ったり、

<img width="500" alt="Android_ja.003.jpeg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1694109/442e852e-c964-3cca-aed9-8149f913c0cf.jpeg">

縦向きのダイアルを作ったりしました。

あと、課金で最難関のモードが解放されるように追加したりと細々と機能を追加しています。

<h2>作ったきっかけ</h2>

iOS,Android のアプリを開発するにあたって、
**Flutter**という統合開発環境を使用しています。

https://flutter.dev/

iOS,Androidの両方のアプリを同時に作れ、
公式のドキュメントも英語が多いですが充実しているのが魅力です。

このFlutter、公式が画面を構成する部品等を紹介するWidget of the Weekという動画を出しています。

今回のアプリのアイデアを思いついたのは、
ListWheelScrollViewというスロットのように回転する画面部品の紹介を見た時です。

（この回です）

https://www.youtube.com/watch?v=dUhmWAz4C7Y

これを見てふと思い出したのが、ダイアル錠でした。

これを再現してみたら、無限プチプチ（梱包材のプチプチを無限に潰せるようにしたおもちゃ）のような形で、おもしろいのでは？
と考えたのがきっかけです。

アプリストアで調べてみて似たようなアプリがなかったので、
即、作ることに決めました。

<h2>技術的に困った点</h2>

今回のアプリでは、ダイアルの部分をFlutterのWidgetを埋め込む形で再現しています。

具体的には、
0,1,2,...,9をListにし、
ListWheelScrollViewという、
3DでスクロールするListViewを使用しています。

このListWheelScrollViewで困った点を2点、紹介したいと思います。

:::note warn
ここからFlutterを触ったことある方向けの内容になります。
興味がなければ下のまとめまで進んでください。
:::

<h3>横並びのListWheelScrollView</h3>

1つ目はListWheelScrollViewをPropertyで**横並びにできない**ことです。

普通のListViewではPropertyにscrollDirectionというのがあり、
これでAxis.horizontalと指定することで横並びになります。
しかし、ListWheelScrollViewにはそもそもscrollDirectionのpropertyがありません。
そもそも横並びにすることを想定されていなかったのです。

対応策は、stackoverflowにありました。

https://stackoverflow.com/questions/52578569/how-to-make-listwheelscrollview-horizontal

RotatedBoxという、Widgetを回転させるWidgetがあります。
このRotatedBoxを駆使して横並びにする、というのが対応策です。

具体的には、まずListWheelScrollView全体を90度回転させています。
このままだと中身の数字も90度回転してしまうので、
中身の数字は中身の数字でRotatedBoxで-90度回転させ、回転を打ち消しています。

以上が、ListWheelScrollViewを横並びにする方法です。

<h3>スクロールの振る舞い</h3>

2つ目は実際に触ってみてわかる、スクロールの振る舞いについてです。

普通にListWheelScrollViewを使うとスクロールの振る舞いについて困った点が2つありました。

1つ目は、**ListViewが中途半端な位置（例えば1と2の間）で止まってしまう**点です。
ダイアル錠なので、中途半端な位置で止まってしまうのは好ましくありません。

対応策は、physicsというPropetyに**FixedExtentScrollPhysics**を追加することでした。

physicsはスクロールの振る舞いを管理するPropertyです。
FixedExtentScrollPhysicsはまさに中途半端な位置で止まらない振る舞いを定義したもののため、
これを追加するだけで解決しました。

2つ目は**フリング(サッと指をスワイプする動作)をすると、スロットのようにダイアルが回ってしまう**点です。
普通のListViewであれば、この動作は正常です。
一番下のアイテムを見たい時に指を動かした分しかスクロールしなかったら不便で仕方がないからです。

ただ、今回のダイアル錠は違います。
ちょっと勢いよくスワイプしただけで回転してしまうとストレスでしかありません。
指を動かした分だけ動くように制御してあげなければいけませんでした。

対応策は、**FixedExtentScrollPhysicsを継承したクラスを作成し、フリングさせなくする**ことです。

作成したScrollPhysicsをここに載せておきます。

```dart
import 'package:flutter/widgets.dart';

class MyFixedExtentScrollPhysics extends FixedExtentScrollPhysics{
  const MyFixedExtentScrollPhysics({ ScrollPhysics? parent }) : super(parent: parent);

  @override
  MyFixedExtentScrollPhysics applyTo(ScrollPhysics? ancestor) {
    return MyFixedExtentScrollPhysics(parent: buildParent(ancestor));
  }

  @override
  double get minFlingDistance => 10000;
}

```

ポイントは最後の**minFlingDistance**の部分です。
この部分でフリング判定する指の移動距離の最小のピクセル数を定義しています。
わざと１００００というアプリ上だとありえない値を定義することによって、
絶対にフリングさせなくしています。

この自作したScrollPhysicsをListWheelScrollView のphysicsに追加することで、
ダイアル錠らしい振る舞いを再現しています。

この辺りはぜひアプリをダウンロードして実際に触って見て欲しいです。

<h2>まとめ</h2>

今回はダイアル錠を再現したアプリの紹介と、
ListWheelScrollViewを使うにあたって技術的に困った点を紹介し、解決策を述べました。

このアプリで、
*「Flutterでこんな面白いことできるんだ」*
と思っていただければこの上なく嬉しいです。

最後に、もう一度ダウンロードリンクを紹介して、この記事を終わります。
読んでいただき、ありがとうございました。

iOS : https://apps.apple.com/jp/app/brute-force-puzzle/id1586258328
Android : https://play.google.com/store/apps/details?id=com.zerokaraapp.bruteforce