ドラッグ可能かつクリック可能なViewを作る
リサーチ・アンド・イノベーションの高田(tfandkusu)です。Androidエンジニアをやっています。昔から独自のUI部品やユニークなアニメーションを作ることにはこだわっていましたが、今回作ったドラッグ可能かつクリック可能なViewもなかなかの自信作なので紹介しようと思います。
FINE演出
CODEの買い物登録では、まずレシートを撮影して頂きますが、撮影されたレシート画像はOCR(光学的文字認識)処理され、日付、合計金額、店舗電話番号が読み取られます。CODEではサードパーティー製のレシートOCRライブラリを使用してOCR処理を行っています。クラウドの力は借りずにリソースの限られたスマホ端末内で完結して処理が行われますが、認識精度と処理速度の両面で高い完成度を持ったライブラリだと思います。とは言っても、レシートが遠すぎたり手ぶれしていたりピンボケしていたりすると読めません。
適切な撮影に誘導する仕組みとして、日付、合計金額、店舗電話番号のすべてが読み取れるとブタさんのキャラクターが褒めてくれる演出を作成しました。社内では「FINE演出」と呼ばれています。
クリックできる
FINE演出は左上のXボタンクリックで閉じることができます。閉じると小さい閉じたFINE演出になります。撮影状態やレシートの記載を確認できるようにするためです。そして閉じたFINE演出をクリックすると再び大きなFINE演出にできます。
ドラッグできる
しかし閉じたFINE演出でもレシートの表示に被ります。そこでドラッグで移動可能にしました。さらにそれによって読み取り結果が合っているかを該当箇所の隣にドラッグして確認することもできます。
ドラッグ可能かつクリック可能なViewの作り方
それではドラッグ可能かつクリック可能なViewの作り方を紹介します。それを実現できるライブラリが無いか探してみたのですが、見つけることができませんでした。なのでAndroid SDKの機能を組み合わせて実現しました。
まずレイアウトファイルはタッチイベントをキャッチする担当Viewと閉じたFINE演出に分けます。
<!-- 略 --> <FrameLayout android:id="@+id/fine_view_close_parent" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1"> <!-- 閉じたFINE演出(ドラッグ可能かつクリック可能) --> <include android:id="@+id/fine_view_close" layout="@layout/view_receipt_confirm_fine_close" /> <!-- タッチイベントキャッチ担当(見えない) --> <View android:id="@+id/fine_close_touch_catcher" android:layout_width="match_parent" android:layout_height="match_parent" android:visibility="invisible" /> </FrameLayout> <!-- 略 -->
下記のコードが閉じたFINE演出find_view_closeをクリック可能かつドラッグ可能にするコードです。ドラッグおよびクリックのイベントはすべてタッチイベントキャッチ担当fine_close_touch_catcherが担当します。取得したタッチイベントを処理した結果をfind_view_closeに反映させています。移動ならばViewの位置を基準の位置からずらすためのtranslationX、translationYプロパティを更新し、クリックならばperformClickメソッドを呼び出します。クリックおよびドラッグの判定はすべてAndroid SDK標準のGestureDetectorにお任せして、何ピクセル以上移動したらクリックでは無くてドラック操作になるといった複雑なタッチイベント判定処理は独自に書きません。また、タッチしたときのリップルエフェクトにもGestureDetectorのonShowPressコールバックメソッドを使って対応しています。
/** * 閉じたFINE演出の移動 */ private fun setUpDragCloseView() { // View取得の方法はView Binding // 閉じたFINE演出高さ // 高さは可変長ではないのでここで取得。幅はこのタイミングだとテキスト適用前のデフォルトの幅が取得されてしまう val h = binding.fineViewClose.height // 可動範囲の大きさ val parentWidth = binding.fineCloseTouchCatcher.width val parentHeight = binding.fineCloseTouchCatcher.height // ドラッグで移動する val gestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() { override fun onDown(e: MotionEvent): Boolean { // 閉じたFINE演出の上で起きたイベントか確認する val x = binding.fineViewClose.x val y = binding.fineViewClose.y // 閉じたFINE演出幅 val w = binding.fineViewClose.width // ここでtrueを返却すると、onScroll、onSingleTapUpイベントが呼ばれるようになる return x <= e.x && y <= e.y && e.x <= x + w && e.y <= y + h } override fun onShowPress(e: MotionEvent) { // タップ反応エフェクトを表示すべき時に呼ばれる // 閉じたFINE演出の上で起きたイベントか確認する val x = binding.fineViewClose.x val y = binding.fineViewClose.y // 閉じたFINE演出幅 val w = binding.fineViewClose.width if (x <= e.x && y <= e.y && e.x <= x + w && e.y <= y + h) { // リップルエフェクトの中心点を設定する binding.fineViewClose.drawableHotspotChanged( e.x - binding.fineViewClose.x, e.y - binding.fineViewClose.y ) // タップされたときのエフェクトを表示する binding.fineViewClose.isPressed = true } } override fun onScroll( e1: MotionEvent, e2: MotionEvent, distanceX: Float, distanceY: Float ): Boolean { // タップされたときのエフェクトを解除して binding.fineViewClose.isPressed = false // 移動する binding.fineViewClose.translationX -= distanceX binding.fineViewClose.translationY -= distanceY // はみ出さないようにする // まずはみ出しピクセル数を計算 val x = binding.fineViewClose.x val y = binding.fineViewClose.y // 閉じたFINE演出幅 val w = binding.fineViewClose.width val leftOver = 0 - x val topOver = 0 - y val rightOver = x + w - parentWidth val bottomOver = y + h - parentHeight // はみ出していたら戻す if (leftOver > 0) binding.fineViewClose.translationX += leftOver if (topOver > 0) binding.fineViewClose.translationY += topOver if (rightOver > 0) binding.fineViewClose.translationX -= rightOver if (bottomOver > 0) binding.fineViewClose.translationY -= bottomOver return true } override fun onSingleTapUp(e: MotionEvent): Boolean { // タップされたときのイベント処理 // タップされたときのエフェクトを解除して binding.fineViewClose.isPressed = false // 閉じたFINE演出をクリックする binding.fineViewClose.performClick() return true } }) // ロングタップは無効化 gestureDetector.setIsLongpressEnabled(false) // GestureDetectorにタッチイベントを送る binding.fineCloseTouchCatcher.setOnTouchListener { _, event -> gestureDetector.onTouchEvent(event) } }
最後に
前回の記事ではAndroidエンジニアを募集中と締めましたが、おかげさまで採用に成功しました。現在はエンジニア採用を行っていませんが、再び採用を開始することになりましたら、最近リニューアルされたコーポレートサイトや転職サイトに加えてこのブログでも紹介します。