在Android開發(fā)的過程中泪电,自定義控件一直是我們繞不開的話題拄丰。而在這個話題中事件分發(fā)機制也是其中的重點和疑點,特別是當我們處理控件嵌套滑動事件時捅暴,正確的處理各個控件間事件分發(fā)攔截狀態(tài)婉陷,可以實現(xiàn)更炫酷的控件動畫效果帚称。
一、事件分發(fā)機制介紹
關于Android事件分發(fā)秽澳,我們主要分ViewGroup和View兩個事件處理部分進行介紹闯睹,主要研究在處理事件過程中關注最多的三個方法dispatchTouchEvent
、onInterceptTouchEvent
担神、onTouchEvent
楼吃,在ViewGroup和View對三個方法的支持如下圖所示:
事件種類 | ViewGroup | View |
---|---|---|
dispatchTouchEvent | 有 | 有 |
onInterceptTouchEvent | 有 | 無 |
onTouchEvent | 有 | 有 |
在Android中,當用戶觸摸界面時系統(tǒng)會把產(chǎn)生一系列的MotionEvent
妄讯,通過ViewGroup 的dispatchTouchEvent
方法開始向下分發(fā)事件孩锡,在dispatchTouchEvent
方法中,會調(diào)用onInterceptTouchEvent
方法亥贸,如果該方法返回true躬窜,表明當前控件攔截了該事件,此后事件交由該控件處理并不再調(diào)用該控件的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;
}
先說結(jié)論再細分析:
- 事件是由其父視圖向子視圖傳遞仍劈,如圖為A->B->C
- 如果當前控件需要攔截該事件,則在
onInterceptTouchEvent
方法中返回true寡壮,但真正決定是否處理事件是在onTouchEvent
方法中贩疙,也就是說如果此時onTouchEvent
方法返回了false讹弯,則此控件也表示不處理該事件,交由父控件的onTouchEvent
方法來判斷處理这溅。如圖:當事件由A分發(fā)至B组民,B在其onInterceptTouchEvent
方法中返回true表示要攔截該事件,此時事件將不會再傳給C悲靴,但在B的onTouchEvent
方法中返回了false臭胜,表示不處理該事件,則事件以此向上傳遞交由A控件的onTouchEvent
方法處理癞尚。即onInterceptTouchEvent
負責對事件進行攔截耸三,攔截成功后交給最先遇到onTouchEvent
返回true的那個view進行處理。- 一旦控件確定處理該事件浇揩,則后續(xù)事件序列也會交由該控件處理仪壮,同時該控件的
onInterceptTouchEvent
方法將不再調(diào)用。- 由于View沒有
onInterceptTouchEvent
方法胳徽,在其dispatchTouchEvent
方法中調(diào)用onTouchEvent
方法處理事件积锅,如果返回false則表示事件不作處理。同時其ACTION_MOVE养盗、ACTION_UP不會得到響應缚陷。- View的
OnTouchListener
優(yōu)先于onTouchEvent
方法執(zhí)行,如果OnTouchListener
方法返回true爪瓜,那么View的dispatchTouchEvent
方法就返回true蹬跃。而后則onTouchEvent
方法得不到執(zhí)行,同時因為onClick
方法在onTouchEvent
方法的ACTION_UP中調(diào)用铆铆,onClick
方法也得不到執(zhí)行蝶缀。
情況一、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時柄慰,都沒有攔截和處理事件,則事件再次向上傳遞調(diào)用B和A的onTouchEvent
方法税娜。
看下打印的結(jié)果:
情況二坐搔、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
之后將不再向上傳遞,隨后事件將不再調(diào)用其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);
}
二介汹、實現(xiàn)側(cè)滑刪除效果
運用上面的知識學習,我們來實現(xiàn)一下簡單的側(cè)滑刪除效果吧~
其核心代碼主要在于對事件的攔截和處理上:
@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
中處理是否攔截該事件体斩。如果手指是向左滑動起便,則表示用戶在進行側(cè)滑刪除操作辙喂,則攔截該事件,需要注意的是操刀,一旦攔截了該事件乒验,之后事件將不調(diào)用該控件的onInterceptTouchEvent
方法俄认,所以我們將具體的處理邏輯放在onTouchEvent
方法中往毡,該方法返回true表示處理該事件蒙揣,此后事件都由dispatchTouchEvent
方法交由onTouchEvent
方法處理。在onTouchEvent
方法中調(diào)用scrollBy
方法實現(xiàn)控件左右滑動开瞭,從而實現(xiàn)類似側(cè)滑刪除效果懒震。
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
invalidate();
}else {
isMoving = false;
}
}
為使滑動效果更自然,用Scroller
在手指抬起的時候控制控件打開或者閉合嗤详,Scroller
的使用也很簡單个扰,抬起時調(diào)用其startScroll
方法并刷新界面,在控件computeScroll
方法中判斷是否滑動完畢并刷新界面葱色,在invalidate
方法中會調(diào)用computeScroll
從而直到滑動結(jié)束锨匆。
好了,總的實現(xiàn)就這么多,希望可以加深對事件分發(fā)機制的理解~