[Android]Viewの更新による初期化を抑止した


はじめに

AndroidのViewをユーザ操作で移動させた後、Viewの更新によって初期位置に戻ってしまうという現象に遭遇したため解決した方法を記載していきます。
強引な方法であるような気はしているので、よりよい方法があればコメントいただければと思います。

環境

言語:Kotlin
動作端末:Android 9.0

概要

カスタムビューをタッチイベントで移動できるようにした。
他のビューを操作することでビューの再構築が走ることで初期位置に戻る現象が発生しました。

対処前


この現象に対して、カスタムビューのonLayoutをOverrideしてViewを再び構築することで初期位置に戻ることを回避しました。

対処後

View更新の仕組

まずはAndroidのView更新の仕組から見ていきます。
Activity、Fragmentと同様にViewにもライフサイクルが存在します。

引用:The Life Cycle of a View in Android
Viewのサイズと位置を決めるのには2つのプロセス(View#layout, View#measure)があります。
View#layout, View#measureは親ビューが子ビューに対して呼び出します。
View#layout, View#measureが呼ばれるとView#onMeasure, View#onLayoutが呼ばれ、
さらに子ビューに対して、、、とリカーシブルに呼ばれていくようです。

View#onMeasure, View#onLayoutの中での処理は次のようになっています。
onMeasure:子ビューのView#measureを呼んで子ビューのサイズを測定し、自ビューのサイズを測定する
onLayout:子ビューのView#layoutを呼んで子ビューの位置を決める

公式ではこの辺に書かれています。

対策

ユーザのタッチイベントでカスタムビューを移動する処理

    override fun onTouch(v: View?, event: MotionEvent?): Boolean {
        when (event!!.action) {
            MotionEvent.ACTION_DOWN -> {
                mLastX = event.rawX.toInt()
                mLastY = event.rawY.toInt()
            }

            MotionEvent.ACTION_MOVE -> {
                if (event.pointerCount == 1) {
                    val x = event.rawX.toInt()
                    val y = event.rawY.toInt()
                    val deltaX = x - mLastX
                    val deltaY = y - mLastY
                    mLastX = x
                    mLastY = y
                    if (abs(deltaX) >= 5 || abs(deltaY) >= 5) {
                        mIsMoving = true     // ユーザ操作による移動かどうかの判定
                        mDx += deltaPoint[0] // X軸方向の初期位置からの移動を管理
                        mDy += deltaPoint[1] // Y軸方向の初期位置からの移動を管理

                        val dx = left + deltaPoint[0]
                        val dy = top + deltaPoint[1]
                        layout(dx, dy, dx + width, dy + height) // Viewを更新
                    }
                }
            }
        }
        return false
    }

ACTION_DOWNで取得した座標とACTION_MOVEで取得した座標の差分を移動量として計算しています。

カスタムビュー内において初期位置への移動を抑止する処理

    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        super.onLayout(changed, l, t, r, b)
        if (mX != x && mY != y && !mIsMoving) { // ユーザの操作中(mIsMoving)は再構築を行わない
            val dx = left + mDx
            val dy = top + mDy
            layout(dx, dy, dx + width, dy + height)
        }
        mIsMoving = false
        mX = x
        mY = y
    }

最初、自ビューのonLayoutではx, yに初期位置の座標が入ってきます。
そこで初期化前の座標(mX, mY)と比較して、x, yの値が初期化前の値と異なる場合には、
初期位置からの移動量(mDx, mDy)をもとにlayoutを再構築します。
mX, mYはViewの更新が走る前の処理(タッチイベントなど)のタイミングであらかじめ保存しておきます。
ロジック上は初期位置に移動して、初期化前の位置にもう一度移動させているイメージです。

最後に

ソースはこちらです。
冒頭にも記載しましたが、強引な方法であるような気はしているので、よりよい方法があればコメントいただければと思います。

参考

以下、参考にさせていただきました。ありがとうございます。
公式
The Life Cycle of a View in Android
[Android] ImageView をドラッグする
onMeasureとonLayoutについて理解する