Infinito Nirone 7

白羽の矢を刺すスタイル

MotionLayout の MotionScene で touchAnchorId に指定した View のクリックをハンドルしたい

MotionLayout は ConstraintLayout 2.0.0 から導入された、スワイプジェスチャやクリックジェスチャで View をキーフレームアニメーションさせるときに使うレイアウトです。キーフレームアニメーションは<MotionScene>をルートとする XML リソースで記述でき、ジェスチャ判定を割り当てる View の指定と、アニメーションの開始・終了時点での View のプロパティを宣言すると、MotionLayout がよしなにアニメーションを作ってくれます。

<MotionScene
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:motion="http://schemas.android.com/apk/res-auto">
  <!-- constraintSetStart が開始時点の状態、constraintSetEnd が終了時点の状態 -->
  <Transition
    motion:constraintSetStart="@id/start"
    motion:constraintSetEnd="@id/end"
    motion:duration="500">
    <!-- スワイプジェスチャで動かすときの設定 -->
    <!-- touchAnchorId を使ってどの View にジェスチャ判定をさせるか指定する -->
    <OnSwipe
      motion:dragDirection="dragUp"
      motion:touchAnchorId="@id/swipe_gesture_anchor"
      motion:touchAnchorSide="bottom"
      motion:maxVelocity="1"
      motion:maxAcceleration="1"
      motion:moveWhenScrollAtTop="true"/>
  </Transition>
  <ConstraintSet android:id="@+id/start">
    <!-- ... -->
  </ConstraintSet>
  <ConstraintSet android:id="@+id/end">
    <!-- ... -->
  </ConstraintSet>
</MotionScene>

このとき、<OnSwipe>touchAnchorIdに指定した View でクリック判定をして何かしら処理をしようと思うといくつかの壁にぶち当たります。 touchAnchorId に指定した View で onTouchEvent を拾ってみると、MotionEvent.ACTION_DOWN は onTouchEvent に流れてくるものの、MotionEvent.ACTION_UP は onTouchEvent に流れてきません。 一方で、touchAnchorIdに指定した View に OnClickListener を設定すると、今度は MotionLayout 側に onTouchEvent が流れていきません(ジェスチャによるアニメーションが動かなくなる)。 MotionScene にはもう一つ<OnClick>もありますが、こちらはクリック時にキーフレームアニメーションを開始するためのもので、汎用的に何かしらの処理をトリガーするためのものではありません。

苦肉の策として次のようなコードを書いてみましたが、もうちょっといいやり方無いかな…

/* クリック判定を少し遅らせ、MotionLayout によるキーフレームアニメーションが始まらなければそのまま実行する */

val motionLayout: MotionLayout = // ...
val anchorView: View = // ...
val handler: Handler = // ...
val delayedClickTask: () -> Unit = {
  // ...
}

anchorView.setOnTouchListener { v, event ->
  handler.postDelayed(delayedClickTask, 300L)
  return@setOnTouchListener false
}
motionLayout.setTransitionListener(object : MotionLayout.TransitionListener {
  override fun onTransitionChange(p0: MotionLayout?, p1: Int, p2: Int, p3: Float) {
    handler.removeCallbacks(delayedClickTask)
    // ...
  }
  override fun onTransitionCompleted(p0: MotionLayout?, p1: Int) {
    // ...
  }
})