Androidはワンツーパンチ 三歩進んで二歩下がる

プログラミングやどうでもいい話

DrawerLayout内でマルチタッチ処理を行うとArrayIndexOutOfBoundsExceptionが発生する

「自分用メモ」です。こればっか言ってますな😁バッドノウハウを自分用メモですと言ってる感が。先人の知恵以外何も無いし😓
恥ずかしい気分モードですが、気を取り直して書きます。

support-v4:22.2.1のお話です。
PhotoViewという、画像をピンチイン・ピンチアウトでズームイン・アウトできるライブラリを使ってみました。実機でピンチアウトしてみた時画像は拡大されずクラッシュしてしまいました。
ログを見たところ、ArrayIndexOutOfBoundsExceptionが発生してました。

E/AndroidRuntime﹕ FATAL EXCEPTION: main
    Process: jp.co.xxxx, PID: 29695
    java.lang.ArrayIndexOutOfBoundsException: length=1; index=1
            at android.support.v4.widget.ViewDragHelper.shouldInterceptTouchEvent(ViewDragHelper.java:1014)
            at android.support.v4.widget.DrawerLayout.onInterceptTouchEvent(DrawerLayout.java:1140)
            at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:1960)
            at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2405)
            at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2106)
            at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2405)
            at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2106)
            at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2405)
            at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2106)
            at com.android.internal.policy.impl.PhoneWindow$DecorView.superDispatchTouchEvent(PhoneWindow.java:2369)
            at com.android.internal.policy.impl.PhoneWindow.superDispatchTouchEvent(PhoneWindow.java:1719)
            at android.app.Activity.dispatchTouchEvent(Activity.java:2742)
            at com.android.internal.policy.impl.PhoneWindow$DecorView.dispatchTouchEvent(PhoneWindow.java:2330)
            at android.view.View.dispatchPointerEvent(View.java:8666)
            at android.view.ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl.java:4123)
            at android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:3989)
            at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:3544)
            at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:3597)
            at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:3563)
            at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:3680)
            at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:3571)
            at android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:3737)
            at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:3544)
            at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:3597)
            at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:3563)
            at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:3571)
            at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:3544)
            at android.view.ViewRootImpl.deliverInputEvent(ViewRootImpl.java:5807)
            at android.view.ViewRootImpl.doProcessInputEvents(ViewRootImpl.java:5781)
            at android.view.ViewRootImpl.enqueueInputEvent(ViewRootImpl.java:5752)
            at android.view.ViewRootImpl$WindowInputEventReceiver.onInputEvent(ViewRootImpl.java:5897)
            at android.view.InputEventReceiver.dispatchInputEvent(InputEventReceiver.java:185)
            at android.view.InputEventReceiver.nativeConsumeBatchedInputEvents(Native Method)
            at android.view.InputEventReceiver.consumeBatchedInputEvents(InputEventReceiver.java:176)
            at android.view.ViewRootImpl.doConsumeBatchedInput(ViewRootImpl.java:5868)
            at android.view.ViewRootImpl$ConsumeBatchedInputRunnable.run(ViewRootImpl.java:5920)
            at android.view.Choreographer$CallbackRecord.run(Choreographer.java:767)
            at android.view.Choreographer.doCallbacks(Choreographer.java:580)
            at android.view.Choreographer.doFrame(Choreographer.java:548)
            at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:753)
            at android.os.Handler.handleCallback(Handler.java:739)
            at android.os.Handler.dispatchMessage(Handler.java:95)
            at android.os.Looper.loop(Looper.java:135)
            at android.app.ActivityThread.main(ActivityThread.java:5254)
            at java.lang.reflect.Method.invoke(Native Method)
            at java.lang.reflect.Method.invoke(Method.java:372)
            at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:903)
            at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:698)

at android.support.v4.widget.DrawerLayout.onInterceptTouchEvent(DrawerLayout.java:1140)

とあるように、DrawerLayoutを使っており、中でPhotoViewを使用しているFragmentを表示しています。

DrawerLayout | Android Developers


ググりましたところ、DrawerLayoutのバグのようです。

Issue 60464 - android - DrawerLayout throwing ArrayIndexOutOfBoundsException - Android Open Source Project - Issue Tracker - Google Project Hosting

上のissuesの5番目に原因と対処方法を書いてくれています。

原因:
これはマルチタッチを処理する時だけに起こる問題で、DrawerLayoutの中のViewがrequestDisallowInterceptTouchEvent(true)を呼ぶことで
DrawerLayoutのonInterceptTouchEventがcallされなくなることが絡んでいます。
まずユーザーの1本めの指(pointerId = 0)が画面に触れると配列の一番目にタッチ情報を保存しますが、
requestDisallowInterceptTouchEvent(true)を実行します。
この結果onInterceptTouchEventが呼び出されず、2本目の指(pointerId = 1)が触れてもタッチ情報を二番目の配列に保存しないままとなってしまいます。
次に指を離す時 ACTION_MOVEイベント中で配列にアクセスしますが、2番めの指を離すとpointerId = 1(←これが添字になっている)の情報が入っているはずが配列のサイズは1なので、ArrayIndexOutOfBoundsExceptionが投げられます。

対策:
上記issuesの5番目の内容ですが、DrawerLayoutの継承クラスを作成しrequestDisallowInterceptTouchEventとdispatchTouchEventをOverrideします。

public class HackyDrawerLayout extends DrawerLayout {

    public HackyDrawerLayout(Context context) {
        super(context);
    }

    public HackyDrawerLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public HackyDrawerLayout(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    private boolean mIsDisallowIntercept = false;

    @Override
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
        // keep the info about if the innerViews do requestDisallowInterceptTouchEvent
        mIsDisallowIntercept = disallowIntercept;
        super.requestDisallowInterceptTouchEvent(disallowIntercept);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        // the incorrect array size will only happen in the multi-touch scenario.
        if (ev.getPointerCount() > 1 && mIsDisallowIntercept) {
            requestDisallowInterceptTouchEvent(false);
            boolean handled = super.dispatchTouchEvent(ev);
            requestDisallowInterceptTouchEvent(true);
            return handled;
        } else {
            return super.dispatchTouchEvent(ev);
        }
    }
}

もちろんDrawerLayoutを定義しているxmlも修正しておきます。


こちらの参考サイト様にも同じ説明がありました。www.arthurwang.net


以上です。