在Android開發的過程中,自定義控件一直是我們繞不開的話題。而在這個話題中事件分發機制也是其中的重點和疑點,特別是當我們處理控件嵌套滑動事件時,正確的處理各個控件間事件分發攔截狀態,可以實現更炫酷的控件動畫效果。
一、事件分發機制介紹
關於Android事件分發,我們主要分ViewGroup和View兩個事件處理部分進行介紹,主要研究在處理事件過程中關注最多的三個方法dispatchTouchEvent
、onInterceptTouchEvent
、onTouchEvent
,在ViewGroup和View對三個方法的支持如下圖所示:
事件種類 | ViewGroup | View |
---|---|---|
dispatchTouchEvent | 有 | 有 |
onInterceptTouchEvent | 有 | 無 |
onTouchEvent | 有 | 有 |
在Android中,當用戶觸摸界面時系統會把產生一系列的MotionEvent
,通過ViewGroup 的dispatchTouchEvent
方法開始向下分發事件,在dispatchTouchEvent
方法中,會調用onInterceptTouchEvent
方法,如果該方法返回true,表明當前控件攔截了該事件,此後事件交由該控件處理並不再調用該控件的onInterceptTouchEvent
方法。最後交由該控件的onTouchEvent
方法對事件進行處理。如果當前控件在onInterceptTouchEvent
方法中返回false,表示不攔截該控件,之後交由其子控件進行判斷是否對事件進行攔截處理。可以用如下僞代碼來對其進行處理:
public boolean dispatchTouchEvent(MotionEvent event) {
boolean consume = false;
if (onInterceptTouchEvent(event)) {
consume = onTouchEvent(event);
} else {
consume = child.dispatchTouchEvent(event);
}
return consume;
}
先說結論再細分析:
- 事件是由其父視圖向子視圖傳遞,如圖爲A->B->C
- 如果當前控件需要攔截該事件,則在
onInterceptTouchEvent
方法中返回true,但真正決定是否處理事件是在onTouchEvent
方法中,也就是說如果此時onTouchEvent
方法返回了false,則此控件也表示不處理該事件,交由父控件的onTouchEvent
方法來判斷處理。如圖:當事件由A分發至B,B在其onInterceptTouchEvent
方法中返回true表示要攔截該事件,此時事件將不會再傳給C,但在B的onTouchEvent
方法中返回了false,表示不處理該事件,則事件以此向上傳遞交由A控件的onTouchEvent
方法處理。即onInterceptTouchEvent
負責對事件進行攔截,攔截成功後交給最先遇到onTouchEvent
返回true的那個view進行處理。- 一旦控件確定處理該事件,則後續事件序列也會交由該控件處理,同時該控件的
onInterceptTouchEvent
方法將不再調用。- 由於View沒有
onInterceptTouchEvent
方法,在其dispatchTouchEvent
方法中調用onTouchEvent
方法處理事件,如果返回false則表示事件不作處理。同時其ACTION_MOVE、ACTION_UP不會得到響應。- View的
OnTouchListener
優先於onTouchEvent
方法執行,如果OnTouchListener
方法返回true,那麼View的dispatchTouchEvent
方法就返回true。而後則onTouchEvent
方法得不到執行,同時因爲onClick
方法在onTouchEvent
方法的ACTION_UP中調用,onClick
方法也得不到執行。
情況一、A\B\C onInterceptTouchEvent
onTouchEvent
均返回false
事件種類 | A(ViewGroup) | B(ViewGroup) | C(View) |
---|---|---|---|
onInterceptTouchEvent | false | false | 無 |
onTouchEvent | false | false | false |
當A、B、C同時返回false時,事件傳遞爲A(onInterceptTouchEvent) –>B(onInterceptTouchEvent) –>C(onTouchEvent)–>B(onTouchEvent) –>A(onTouchEvent),也就是事件從A傳至C時,都沒有攔截和處理事件,則事件再次向上傳遞調用B和A的onTouchEvent
方法。
看下打印的結果:
情況二、B onInterceptTouchEvent
方法返回true
事件種類 | A(ViewGroup) | B(ViewGroup) | C(View) |
---|---|---|---|
onInterceptTouchEvent | false | true | 無 |
onTouchEvent | false | false | false |
當BonInterceptTouchEvent
返回true時表示攔截了事件,C控件就無法響應該事件。
情況三、B onInterceptTouchEvent
、 onTouchEvent
方法返回true
事件種類 | A(ViewGroup) | B(ViewGroup) | C(View) |
---|---|---|---|
onInterceptTouchEvent | false | true | 無 |
onTouchEvent | false | true | false |
當BonInterceptTouchEvent
、onTouchEvent
返回true時表示攔截處理了事件,C控件就無法響應該事件,同時事件在B的onTouchEvent
之後將不再向上傳遞,隨後事件將不再調用其onInterceptTouchEvent
方法。
情況四、C onTouchEvent
方法返回true
事件種類 | A(ViewGroup) | B(ViewGroup) | C(View) |
---|---|---|---|
onInterceptTouchEvent | false | false | 無 |
onTouchEvent | false | false | true |
當ConTouchEvent
返回true時表示處理了該事件,之後事件就交由C控件處理,同時事件在C的onTouchEvent
之後將不再向上傳遞。
情況五、A onInterceptTouchEvent
方法返回true
事件種類 | A(ViewGroup) | B(ViewGroup) | C(View) |
---|---|---|---|
onInterceptTouchEvent | true | false | 無 |
onTouchEvent | false | false | false |
當AonInterceptTouchEvent
返回true時表示攔截了事件,之後事件就交由A的onTouchEvent
方法處理,B、C就無法響應該事件。如果AonTouchEvent
方法返回false,其ACTION_MOVE、ACTION_UP事件不會得到響應。
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e(TAG, "A --- onTouchEvent");
switch (event.getAction()){
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "A --- onTouchEvent :ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "A --- onTouchEvent :ACTION_UP");
break;
}
return false;//super.onTouchEvent(event);
}
二、實現側滑刪除效果
運用上面的知識學習,我們來實現一下簡單的側滑刪除效果吧~
其核心代碼主要在於對事件的攔截和處理上:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// boolean intercepter = false;
Log.e("TAG", "onInterceptTouchEvent: "+ev.getAction());
boolean intercepter = false;
if (isMoving)
intercepter = true;
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
downX = (int) ev.getX();
downY = (int) ev.getY();
if (mVelocityTracker == null)
mVelocityTracker = VelocityTracker.obtain();
mVelocityTracker.clear();
break;
case MotionEvent.ACTION_MOVE:
moveX = (int) ev.getX();
moveY = (int) ev.getY();
Log.e("TAG", "getScrollX: "+getScrollX() );
if (Math.abs(moveX - downX) > 0){
intercepter = true;
//Log.e("TAG","onInterceptTouchEvent: ");
//scrollBy(moveX - downX,0);
}else {
intercepter = false;
}
downX = moveX;
downY = moveY;
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
intercepter = false;
break;
}
//scrollBy(45,0);
return intercepter;//
//super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
Log.e("TAG", "onTouchEvent: "+ev.getAction() );
mVelocityTracker.addMovement(ev);
switch (ev.getAction()){
case MotionEvent.ACTION_MOVE:
moveX = (int) ev.getX();
moveY = (int) ev.getY();
mVelocityTracker.computeCurrentVelocity(1000);
Log.e("TAG", "getScrollX: "+getScrollX() );
if (getScrollX()+downX - moveX>=0 && getScrollX()+downX - moveX <= view1.getMeasuredWidth()){
scrollBy(downX - moveX,0);
}
isMoving = true;
downX = moveX;
downY = moveY;
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
Log.e("TAG1", "getXVelocity: "+mVelocityTracker.getXVelocity() );
Log.e("TAG1", "getYVelocity: "+mVelocityTracker.getYVelocity() );
//
if (getScrollX()>=view1.getMeasuredWidth()/2 || mVelocityTracker.getXVelocity() < -ViewConfiguration.get(getContext()).getScaledMinimumFlingVelocity()){
//scrollTo(view1.getMeasuredWidth(),0);
open();
}else {
//scrollTo(0,0);
close();
}
mVelocityTracker.clear();
mVelocityTracker.recycle();
mVelocityTracker = null;
break;
}
return true;//super.onTouchEvent(ev);
}
這裏整個父佈局繼承自ViewGroup
,在onMeasure
中測量子控件大小:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
measureChildren(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(getMeasuredWidth(), getMeasuredHeight());
}
在onFinishInflate
方法中獲取各個子控件:
@Override
protected void onFinishInflate() {
super.onFinishInflate();
view = getChildAt(0);
view1 = getChildAt(1);
if (mScroller == null)
mScroller = new Scroller(getContext());
view.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View mViewm, MotionEvent mMotionEventm) {
if (mMotionEventm.getAction() == MotionEvent.ACTION_UP
&& isOpen){
close();
}
if (mMotionEventm.getAction() == MotionEvent.ACTION_DOWN){
if (mOnChangeMenuListener!=null){
mOnChangeMenuListener.onStartTouch();
}
}
return false;
}
});
}
並在onLayout
方法中佈局子控件:
@Override
protected void onLayout(boolean mBm, int mIm, int mIm1, int mIm2, int mIm3) {
if (getChildCount()!=2){
throw new IllegalArgumentException("必須包含兩個子控件");
}
Log.e("TAG", "onLayout:getWidth "+view.getWidth() );
view.layout(0,0,view.getMeasuredWidth(),view.getMeasuredHeight());
view1.layout(view.getMeasuredWidth(),0,view.getMeasuredWidth()+view1.getMeasuredWidth(),view1.getMeasuredHeight());
}
重點在對onInterceptTouchEvent
和onTouchEvent
方法的處理,我們在onInterceptTouchEvent
中處理是否攔截該事件。如果手指是向左滑動,則表示用戶在進行側滑刪除操作,則攔截該事件,需要注意的是,一旦攔截了該事件,之後事件將不調用該控件的onInterceptTouchEvent
方法,所以我們將具體的處理邏輯放在onTouchEvent
方法中,該方法返回true表示處理該事件,此後事件都由dispatchTouchEvent
方法交由onTouchEvent
方法處理。在onTouchEvent
方法中調用scrollBy
方法實現控件左右滑動,從而實現類似側滑刪除效果。
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
invalidate();
}else {
isMoving = false;
}
}
爲使滑動效果更自然,用Scroller
在手指擡起的時候控制控件打開或者閉合,Scroller
的使用也很簡單,擡起時調用其startScroll
方法並刷新界面,在控件computeScroll
方法中判斷是否滑動完畢並刷新界面,在invalidate
方法中會調用computeScroll
從而直到滑動結束。
好了,總的實現就這麼多,希望可以加深對事件分發機制的理解~