View Groupイベント配信メカニズムソースコード解析(一)View編


本編はAndroid 9に基づく.0のソースコードはビューのイベント配布メカニズムを分析し、ソースコードはhttps://github.com/Oaman/Forward.

Viewイベント処理メカニズムソース分析


レイアウトファイルは次のとおりです.ルートViewはLinearLayoutで、内部にはButtonとImageViewの2つのviewが配置されています.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/rootView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/buttonView"
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:text="button" />

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:layout_marginTop="10dp"
        android:background="@color/colorPrimaryDark"
        android:text="image" />

</LinearLayout>

私たちはMainActivityでこのButtonに対してクリックリスニングとTouchイベントを設定します.
		button.setOnClickListener(new View.OnClickListener() {
     
            @Override
            public void onClick(View v) {
     
                Log.e("normally", "button onClick");
            }
        });

        button.setOnTouchListener(new View.OnTouchListener() {
     
            @Override
            public boolean onTouch(View v, MotionEvent event) {
     
                Log.e("normally", "button onTouch: " + getAction(event.getAction()) + "--clickable:" + button.isClickable());

                return false;
            }
        });

運転後にボタンをクリックすると次の結果が印刷されます.
/com.oman.dispatch E/normally: button onTouch:  DOWN--clickable:true
/com.oman.dispatch E/normally: button onTouch:  MOVE--clickable:true
/com.oman.dispatch E/normally: button onTouch:  UP--clickable:true
/com.oman.dispatch E/normally: button onClick

ここではまずonTouchメソッドを実行し、それからonClickメソッドを実行することがわかります.私はクリックしたときにわざとスライドして、MOVEの動作を印刷しました.これでDOWN、MOVE、UPの3つの動作が、それぞれ印刷されました.
このsetOnTouchListenerメソッドには戻り値があります.ここではデフォルトのfalseです.戻り値をtrueに変更してみましょう.印刷結果を見てみましょう.
/com.oman.dispatch E/normally: button onTouch:  DOWN--clickable:true
/com.oman.dispatch E/normally: button onTouch:  MOVE--clickable:true
/com.oman.dispatch E/normally: button onTouch:  UP--clickable:true

onTouchメソッドのみが実行され、onClickメソッドが実行されていないことがわかります.これはなぜですか.次に、ソースコードの観点からなぜですか.
まず、どのコントロールに触れても、このコントロールのdispatchTouchEventメソッドが呼び出されることを知っておく必要があります.では、このメソッドのソースコードを見てみましょう.ButtonはTextViewから継承され、TextViewはViewから継承されています.私たちはViewでこのメソッドを見つけました.dispatchTouchEventメソッドのソースコードを見てみましょう.
	public boolean dispatchTouchEvent(MotionEvent event) {
     
        ...
        boolean result = false;
        ...
        if (onFilterTouchEventForSecurity(event)) {
     
            ...
            ListenerInfo li = mListenerInfo;
            // 1 
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
     
                result = true;
            }
			// 2
            if (!result && onTouchEvent(event)) {
     
                result = true;
            }
        }
        // 3
        return result;
    }

注釈1には4つの条件があり、この4つの条件がtrueの場合dispatchTouchEventはtrueを返し、イベントが処理されたことを表します.そうしないと、注釈2のonTouchEventメソッドが実行されます.次に、この4つの条件を見てみましょう.
  • 最初と2番目はmOnTouchListenerで空ではありませんが、setOnTouchListenerを設定するときにすでに設定されています.ここでListener Infoは、実際には複数のlistenerを持つオブジェクト
    public void setOnTouchListener(OnTouchListener l) {
           
        getListenerInfo().mOnTouchListener = l;
    }
    
  • です.
  • の3番目の条件は(mViewFlags & ENABLE_MASK)== ENABLEで、現在のコントロールがクリック可能かどうかを判断し、Buttonのデフォルトはクリック可能であるため、ここではtrueとする.
  • 第4条件li.mOnTouchListener.onTouch(this,event)メソッドは、実はここが登録したonTouchイベントをコールバックするonTouchメソッドであり、戻り値がtrueであれば全体的にtrueを返し、戻り値がfalseであればonTouchEvent(event)メソッドを実行し続ける.

  • ここまで言うと、先に印刷した結果、まずdispatchTouchEventの中のonTouchメソッドを実行し、onTouchの戻り値がfalseであればonTouchEventを実行してonClickメソッドを印刷し、onClickメソッドがonTouchEventメソッドで実行されていることを示します.trueを返すと後のonTouchEventメソッドは実行されず、onClickは実行されません.

    View.onTouchEvent


    onTouchEventメソッドといえば、ソースコードを見てみましょう.
    	public boolean onTouchEvent(MotionEvent event) {
         
            final float x = event.getX();
            final float y = event.getY();
            final int viewFlags = mViewFlags;
            final int action = event.getAction();
    		...
    		// 1
            if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
         
                switch (action) {
         
                    case MotionEvent.ACTION_UP:
                       
                        boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
         
                            
                            if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
         
                                // This is a tap, so remove the longpress check
                                removeLongPressCallback();
    
                                // Only perform take click actions if we were in the pressed state
                                if (!focusTaken) {
         
                                    // Use a Runnable and post this rather than calling
                                    // performClick directly. This lets other visual state
                                    // of the view update before click actions start.
                                    if (mPerformClick == null) {
         
                                        mPerformClick = new PerformClick();
                                    }
                                    if (!post(mPerformClick)) {
         
                                    	// 2
                                        performClickInternal();
                                    }
                                }
                            }
    
                            ...
                        }
                        mIgnoreNextUpEvent = false;
                        break;
    
                    case MotionEvent.ACTION_DOWN:
                        ...
                        break;
    
                    case MotionEvent.ACTION_CANCEL:
                        ...
                        break;
    
                    case MotionEvent.ACTION_MOVE:
                        ...
                        break;
                }
    
                return true;
            }
    
            return false;
        }
    

    コメント1では、コントロールがクリック可能であればswitch判定に入り、指を上げたときのACTION_UPイベントは、様々な判断を経て注釈2に入るperformClickInternal()メソッドで、中のソースコードを見てみましょう.
    	private boolean performClickInternal() {
         
            return performClick();
        }
    	
    	public boolean performClick() {
         
            ...
            final boolean result;
            final ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnClickListener != null) {
         
                playSoundEffect(SoundEffectConstants.CLICK);
                // 1
                li.mOnClickListener.onClick(this);
                result = true;
            } else {
         
                result = false;
            }
            return result;
        }
    

    上は最終的にコメント1に実行され、mOnClickListenerがnullでない限りonClickメソッドが実行されますが、mOnClickListenerはどこで値を付与しているのでしょうか.実はonClickリスニングを設定すると自動的に値が付与されます.コードは以下の通りです.
    	public void setOnClickListener(@Nullable OnClickListener l) {
         
            if (!isClickable()) {
         
                setClickable(true);
            }
            getListenerInfo().mOnClickListener = l;
        }
    

    今から見れば全体の考え方がはっきりしているのではないでしょうか.ここで注意しなければならないのは、コントロールがクリック可能な状態でdispatchTouchEventメソッドを実行している場合、onTouchEventメソッドに入る前にonTouchの戻り値にかかわらず、最後の戻り値はtrueで、信じない場合は、上のonTouchEventソースコードを見て、switchに入ってから最後に戻るtrueです.

    ImageViewのテスト状況


    では、ButtonをImageViewに変更すると(ここではsetOnTouchListenerだけを設定し、setOnClickListenerは設定していません)、印刷結果を見てみましょう
    	imageView.setOnTouchListener(new View.OnTouchListener() {
         
            @Override
            public boolean onTouch(View v, MotionEvent event) {
         
                Log.e("normally", "image onTouch: " + getAction(event.getAction()) + "--clickable:" + imageView.isClickable());
                return false;
            }
        });
    

    印刷結果は次のとおりです.
    /com.oman.dispatch E/normally: image onTouch:  DOWN--clickable:false
    

    ACTIONが実行されているだけでDOWN動作は、ImageViewがデフォルトでクリック不可であるため、onTouchEventメソッドの実行時にswitch判定に入ることなく、1つの押下動作のみが実行され、最終的にイベントimageViewが処理されず、親コントロールに処理される.onTouchの戻り値をtrueに変更すると、次のような結果が印刷されます.
    /com.oman.dispatch E/normally: image onTouch:  DOWN--clickable:false
    /com.oman.dispatch E/normally: image onTouch:  MOVE--clickable:false
    /com.oman.dispatch E/normally: image onTouch:  UP--clickable:false
    

    なぜかというと、Touchイベントの階層伝達という重要な知識点があるので、dispatchTouchEventメソッドが実行されると、前のactionの戻り値がtrueの場合にのみ、次のactionがトリガーされます.
    ImageViewにonClickとonTouchリスニングを同時に登録すると(戻り値はfalse)、
    	imageView.setOnTouchListener(new View.OnTouchListener() {
         
            @Override
            public boolean onTouch(View v, MotionEvent event) {
         
                Log.e("normally", "image onTouch: " + getAction(event.getAction()) + "--clickable:" + imageView.isClickable());
                return false;
            }
            });
        imageView.setOnClickListener(new View.OnClickListener() {
         
            @Override
            public void onClick(View v) {
         
                Log.e("normally", "image onClick");
            }
        });
    

    imageViewをクリックして次のように印刷します.
    /com.oman.dispatch E/normally: image onTouch:  DOWN--clickable:true
    /com.oman.dispatch E/normally: image onTouch:  MOVE--clickable:true
    /com.oman.dispatch E/normally: image onTouch:  UP--clickable:true
    /com.oman.dispatch E/normally: image onClick
    

    onTouchの戻り値がtrueの場合、印刷結果は次のようになります.
    /com.oman.dispatch E/normally: image onTouch:  DOWN--clickable:true
    /com.oman.dispatch E/normally: image onTouch:  MOVE--clickable:true
    /com.oman.dispatch E/normally: image onTouch:  UP--clickable:true
    

    これはsetOnClickListenerメソッドを設定したためです.ソースコードは次のとおりです.
    	public void setOnClickListener(@Nullable OnClickListener l) {
         
            if (!isClickable()) {
         
                // 1
                setClickable(true);
            }
            getListenerInfo().mOnClickListener = l;
        }
    

    ソースコードの注釈1でImageViewをCLICKABLE状態に設定しているのを見て、onTouchがfalseに戻ってもonTouchEventのswitchに入ることができて、あなたが見た結果を印刷しました.onTouchの戻り値をtrueに設定すると、onTouchEventメソッドは実行されないので、onClickメソッドは印刷されません.

    小結


    以上はViewのイベント配信と処理ですが、次編ではView Groupのイベント配信とスライド競合の解決を分析し、View Groupのイベント配信とスライド競合の表示をクリックします.