View的事件體系總結

一、基礎知識

1、View的座標系

View的座標系統是相對於父控件的,如下圖:
圖片來自GcsSloop

  getTop();       //獲取子View左上角距父View頂部的距離
  getLeft();      //獲取子View左上角距父View左側的距離
  getBottom();    //獲取子View右下角距父View頂部的距離
  getRight();     //獲取子View右下角距父View左側的距離
getX()、getTranslationX()

Android3.0後增加了:

x、y :	表示View左上角座標。用getX()、 getY()獲得
translationX、translationY : 	表示View的左上角相對於父容器的偏移量,
	通過 getTranslationX()、getTranslationY()獲得。 默認爲0

其中:

其中:
x = getLeft() + translationX ;
y = getTop() + translationY ;

2、MotionEvent

表示觸摸屏幕產生的一系列事件。常用的有如下三種:

  • ACTION_DOWN : 手指剛開始觸摸屏幕,事件的起始位置。
  • ACTION_MOVE :手指在屏幕上移動。
  • ACTION_UP :手指離開屏幕的瞬間觸發。

從事件開始到結束任意時間內,都可以通過 MotionEvent 內部的 getX/getY和getRawX/getRayY獲得相應座標,兩種方式的區別如下圖:
在這裏插入圖片描述
兩種方式的含義:

    event.getX();       //觸摸點相對於其所在組件座標系的座標
    event.getY();

    event.getRawX();    //觸摸點相對於屏幕默認座標系的座標
    event.getRawY();

3、VelocityTracker、GestureDetector、Scroller

①、VelocityTracker速度追蹤

用法如下:

        VelocityTracker velocityTracker = VelocityTracker.obtain();
        velocityTracker.addMovement(event);
        
        //1000ms內速度
        velocityTracker.computeCurrentVelocity(1000);
        //x軸方向速度
        int xVelocty = (int) velocityTracker.getXVelocity();
        //y方向速度
        int yVelocty = (int) velocityTracker.getYVelocity();
        
        //釋放
        velocityTracker.clear();
        velocityTracker.recycle();

速度的單位是: 像素/毫秒(px/ms),eg:100像素/每毫秒

②、GestureDetector 手勢檢測

包含一下方法:

  • onDown:觸摸到屏幕
  • onShowPress:
  • onSingleTapUp:單擊
  • onScroll:手指滾動
  • onLongPress:長按
  • onFling: 手指離開,頁面滑動
③、Scroller

彈性滑動對象,用於實現view的彈性滑動。

二、View的滑動

1、scrollTo/scrollBy

    public void scrollTo(int x, int y) {
        if (mScrollX != x || mScrollY != y) {
            int oldX = mScrollX;
            int oldY = mScrollY;
            mScrollX = x;
            mScrollY = y;
            invalidateParentCaches();
            onScrollChanged(mScrollX, mScrollY, oldX, oldY);
            if (!awakenScrollBars()) {
                postInvalidateOnAnimation();
            }
        }
    }
    public void scrollBy(int x, int y) {
        scrollTo(mScrollX + x, mScrollY + y);
    }

可以看到scrollBy也是調用scrollTo方法實現移動。

特點:
  • 無論是scrollTo還是scrollBy都無法改變view的位置,移動的是view的內部位置。
  • scrollTo屬於絕對滑動,移動的位置是相對於View的。即:無論移動多少次,位置都是在第一次移動的位置。
  • scrollBy屬於相對滑動,移動的位置是相對自己的。即:每次點擊移動,都會相對自己的位置再次移動。
  • 移動的距離scrollX和scrollY正負和Android座標系相反。即x移動正100,view的內容向左移動100(不是向右),y移動負100,view內容向下移動100(不是向上)。

2、使用動畫實現view的滑動

★ 使用屬性動畫可以實現view的滑動。

view動畫,不能真正改變動畫的位置。即位置改變了,但是view的事件還留在原來的位置

nineoldandroids動畫兼容庫

3、使用LayoutParams改變位置參數。

可用通過改變view的margin屬性,或者改變父view的padding屬性。實現view的滑動

三、彈性滑動

1、使用scroller
2、使用動畫
3、使用延時策略

四、View的事件分發機制

view的事件分發機制指的是從手指按下屏幕開始,事件從屏幕傳遞到指定view的一系列過程。

1、點擊事件的傳遞規則

View的事件分發其實是對MotionEvent事件的分發過程。

而事件的分發過程由三個很重要的方法共同完成:dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent。

  • dispatchTouchEvent(MotionEvent event) :用來分發事件。返回結果受當前view的onTouchEvent和下級View的dispatchTouchEvent方法影響。
  • onInterceptTouchEvent(MotionEvent ev) :用來攔截事件。
  • onTouchEvent(MotionEvent event) :在dispatchTouchEvent方法中調用,表示是否消耗當前事件
三者之間的關係

viewgroup的事件分發可以用下面僞代碼表示三者之間的關係:

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {

        boolean consume = false;
        //當前view是否攔截
        if (onInterceptTouchEvent(event)){
        	//攔截後,則調用自己的onTouchEvent,
        	//如果onTouchEvent消耗事件則返回true,否則false,交由父控件處理
            consume = onTouchEvent(event);
        }else {
        	//如果不攔截,則獲得子view是否消耗
            consume = child.dispatchTouchEvent(event);
        }
        return consume;
    }

2、事件分發源碼

當我們點擊屏幕產生事件時,最先接收事件的是Activity。所以事件先從Activity的dispatchTouchEvent開始分發。

1、Activity事件分發

Activity中 dispatchTouchEvent 方法源碼如下:

    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

從上面源碼可以看到,activity事件分發受到Widow的superDispatchTouchEvent方法影響。
在這裏插入圖片描述
可以看到Window是一個抽象方法。註釋方法裏面說它有一個子類PhoneWindow。

可以全局搜索PhoneWindow。找到PhoneWindow的位置,在com.android.internal.policy包中

PhoneWindow

查看PhoneWindow的superDispatchTouchEvent方法:

    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

可以看到該方法的返回值又受到 mDecor 中的方法影響。
查看mDecor 聲明的地方

    // This is the top-level view of the window, containing the window decor.
    private DecorView mDecor;

從註釋中可以看到,DecorView 是PhoneWindow的頂層視圖。

DecorView

在這裏插入圖片描述
可以看到DecorView 繼承FrameLayout。DecorView 的superDispatchTouchEvent方法源碼如下:

    public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }

因爲DecorView 繼承自FrameLayout,所以這裏DecorView 調用ViewGroup的dispatchTouchEvent將事件向下傳遞分發。

這個時候我們的事件已經傳遞到了DecorView 了。 傳遞順序如下:
Activity --> PhoneWindow --> DecorView

事件是怎麼從DecorView傳遞到我們自己的Layout中的?

Activity & setContentView()

在Activity中我們通過 setContentView()來加載我們的佈局。源碼如下:

    public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }

可以看到,它會調用PhoneWindow的setContentView()方法來加載我們的佈局文件。

PhoneWindow & setContentView()
    @Override
    public void setContentView(int layoutResID) {
        if (mContentParent == null) {
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }
		//加載佈局
        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                    getContext());
            transitionTo(newScene);
        } else {
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }

1、當 mContentParent爲空時,會執行 installDecor()方法。因爲mContentParent是在installDecor()方法中賦值的,所以一定會先執行installDecor()方法來初始化。

2、當mContentParent不爲空,則移除mContentParent內部的view,將佈局文件添加到mContentParent中。

PhoneWindow & installDecor()
    private void installDecor() {
        mForceDecorInstall = false;
        if (mDecor == null) {
            mDecor = generateDecor(-1);
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
            }
        } else {
            mDecor.setWindow(this);
        }
        if (mContentParent == null) {
            mContentParent = generateLayout(mDecor);
           ...
        }
    }

可以看到,在installDecor中會初始化mDecormContentParent
mContentParent = generateLayout(mDecor);

PhoneWindow & generateLayout(mDecor)

從方法名就可以看出來了,這個方法是在mDecor 中生成一個layout佈局。

    protected ViewGroup generateLayout(DecorView decor) {
    	...//省略資源加載
        mDecor.startChanging();
        //layoutResource 在上面加載過了,省略
        //mDecor 加載layoutResource佈局
        mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
         //通過findViewById找到contentParent 
         // int ID_ANDROID_CONTENT = com.android.internal.R.id.content;
         ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
        mDecor.finishChanging();
        return contentParent;
    }

佈局文件如下:
在這裏插入圖片描述
在上面的方法中,DecorView會加載layoutResource佈局文件,layoutResource如上圖,通過findviewbyid找到contentParent 控件,也就是上圖紅框代表的FrameLayout。

而我們加載的佈局文件就是放在紅框的contentParent 中。

用一張圖來展示他們之間的層級關係如下:

在這裏插入圖片描述
這個時候我們的事件就傳遞到了ContentParent中了,然後再由ContentParent傳遞到我們佈局文件的最外層View即根View

findViewById(id)

這裏面既然用到了findViewById(id)那我們不妨看一下findViewById的源碼:

Activity & findViewById
    @Nullable
    public View findViewById(@IdRes int id) {
        return getWindow().findViewById(id);
    }
Window & findViewById
    @Nullable
    public View findViewById(@IdRes int id) {
        return getDecorView().findViewById(id);
    }

我的findViewById其實也是在DecorView中查找控件id的

事件從Activity到根View傳遞順序:

Activity -> PhoneWindow -> DecorView -> ContentParent -> 根View

3、根View對點擊事件的分發

①、ViewGroup事件分發

如果根View是ViewGroup,則會調用ViewGroup 的 dispatchTouchEvent方法,

dispatchTouchEvent 攔截部分源碼如下:

            // Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                 //子view是否調用requestDisallowInterceptTouchEvent()
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                intercepted = true;
            }

默認ViewGroup是不會攔截事件分發的。
可以看到子View可以調用requestDisallowInterceptTouchEvent來影響父view是否攔截。

1、ViewGroup不攔截事件
  • 如果viewgroup不攔截事件的話,viewgroup會遍歷所有子view,並調用dispatchTransformedTouchEvent方法,把事件分發給子view。
    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;

        // Canceling motions is a special case.  We don't need to perform any transformations
        // or filtering.  The important part is the action, not the contents.
        final int oldAction = event.getAction();
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
            event.setAction(MotionEvent.ACTION_CANCEL);
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }
        ...//省略部分情況判斷
	}

可以看到,當子view不爲空時,如果child是ViewGroup則會再次執行ViewGroup的dispatchTouchEvent。如果子View爲空則執行View的dispatchTouchEvent

view的dispatchTouchEvent方法放到下面講。

2、ViewGroup攔截事件

如果ViewGroup攔截分發事件,則執行自己的OnTouchEvent()方法。而ViewGroup沒有專門實現自己的OnTouchEvent方法的邏輯,仍然使用的是view的OnTouchEvent邏輯。view的OnTouchEvent方法下面講。

②View的事件分發

上面說viewgroup的事件分發的時候,在ViewGroup的dispatchTouchEvent方法中,不攔截的話最終會執行view的dispatchTouchEvent方法。
view的dispatchTouchEvent部分源碼如下:

            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

            if (!result && onTouchEvent(event)) {
                result = true;
            }

1、可以看到,當view設置了setOnTouchListener的時候,mOnTouchListener不爲null,此時view的dispatchTouchEvent方法的返回值受mOnTouchListener.onTouch()方法影響。
如果在onTouch()方法中返回true,則view的dispatchTouchEvent方法返回值就爲true。而如果view上面還有ViewGroup,則ViewGroup的dispatchTouchEvent方法也就返回true,則不再繼續分發事件。
2、如果沒有設置setOnTouchListener或者mOnTouchListener.onTouch()方法返回false,則執行View的onTouchEvent(event)方法

View的onTouchEvent方法
onTouchEvent部分源碼如下:

public boolean onTouchEvent(MotionEvent event) {
	...
    if (mTouchDelegate != null) {
        if (mTouchDelegate.onTouchEvent(event)) {
            return true;
        }
    }
    ...
        switch (action) {
            case MotionEvent.ACTION_UP:
            	...
                if (mPerformClick == null) {
                    mPerformClick = new PerformClick();
                }
                if (!post(mPerformClick)) {
                    performClick();
                }
                ...
}

1、如果給view設置setTouchDelegate()此時onTouchEvent方法返回值受mTouchDelegate.onTouchEvent(event)方法影響。
2、在MotionEvent.ACTION_UP的時候,會執行performClick()方法,即點擊事件的方法。源碼如下:

    public boolean performClick() {
        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
        return result;
    }

當我們給View設置點擊事件的時候,則在此執行mOnClickListener.onClick()方法。

到這裏一個事件從Activity的dispatchTouchEvent方法開始分發,一直到View的onClick()方法響應的整個過程已經分析完了。

View事件優先級總結

dispatchTouchEvent -> onTouch -> onTouchEvent -> onClick

五、View的滑動衝突解決方式

1、外部攔截法

在外部佈局的onInterceptTouchEvent 方法中ACTION_MOVE事件中判斷是否攔截子view的事件,並 在ACTION_UP和ACTION_DOWN中釋放攔截。
僞代碼如下:

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercept = false;
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                intercept = false;
                break;
            case MotionEvent.ACTION_MOVE:
                if (父容器需要當前事件){
                    //攔截
                    intercept = true;
                }else {
                    intercept = false;
                }
                break;
            case MotionEvent.ACTION_UP:
                intercept = false;
                break;
        }

        return intercept;
    }

2、內部攔截法
指父容器不攔截任何事件,所有的事件都交給子view處理,如果子view需要就消耗掉,否則交給父容器處理。需要配合requestDisallowInterceptTouchEvent使用。

子元素的dispatchTouchEvent方法如下:

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //屏蔽父容器事件
                parent.requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                if (父容器需要當前事件){
                    //交給父容器處理
                    parent.requestDisallowInterceptTouchEvent(false);
                }
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return super.dispatchTouchEvent(ev);
    }

父容器需要將ACTION_DOWN的攔截事件接觸,不然在需要父容器接收的時候,父容器也沒有地方接收。

父元素修改如下:

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.getAction() ==  MotionEvent.ACTION_DOWN){
            return false;
        }else {
            return true;
        }
    }
相比較內部攔截法,外部攔截更加方便,只需要在一個view內做攔截就行了

參考:Android開發藝術探索。
安卓中的座標系

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章