layout: post
date: 2016-01-08
title: Android開發(fā)藝術(shù)探索-第三章-View的事件體系
categories: blog
tags: [Activity,Android,View,MotionEvent,TouchSlop]
category: Android
description:
本文首發(fā)于個人博客KuTear,轉(zhuǎn)載引用請注明原出處.謝謝!
另外,更多文章分享請查看博客KuTear
3.1 View的基礎(chǔ)知識
-
位置參數(shù)
top狐粱、left、right缀拭、bottom,在3.0之后增加了x杈绸、y辆布、translationX丘侠、translationY.這里的所有參數(shù)都是相對其父布局來說的.
下面是具體的含義表示其中參數(shù)的關(guān)系為
x = left + translationX y = top + translationY
-
MontionEvent和TouchSlop
MontionEvent代表著觸摸事件封裝的數(shù)據(jù),包括常用的Action和位置參數(shù)等.如上面圖示,注意函數(shù)getRaw*()是相對與屏幕的.
TouchSlop表示滑動的最小常量.是常量(int),不是具體的類.獲取方式為:ViewConfiguration.get(getContext()).getScaledTouchSlop()
-
VelocityTracker,GestureDetector和Scroller
VelocityTracker用于追蹤手指在滑動過程中的速度危彩,包括水平和垂直方向上的速度攒磨。
速度計算公式:速度 = (終點位置 - 起點位置) / 時間段
速度可能為負值,例如當手指從屏幕右邊往左邊滑動的時候汤徽。此外娩缰,速度是單位時間內(nèi)移動的像素數(shù),單位時間不一定是1秒鐘谒府,可以使用方法
computeCurrentVelocity(xxx)指定單位時間是多少拼坎,單位是ms。例如通過computeCurrentVelocity(1000)來獲取速度完疫,手指在1s中
滑動了100個像素泰鸡,那么速度是100,即100(像素/1000ms)壳鹤。如果computeCurrentVelocity(100)來獲取速度盛龄,在100ms內(nèi)手指只是滑動了
10個像素,那么速度是10芳誓,即10(像素/100ms)余舶。
VelocityTracker的使用方式://初始化 VelocityTracker mVelocityTracker = VelocityTracker.obtain(); //在onTouchEvent方法中 mVelocityTracker.addMovement(event); //獲取速度 mVelocityTracker.computeCurrentVelocity(1000); float xVelocity = mVelocityTracker.getXVelocity(); //重置和回收 mVelocityTracker.clear(); //一般在MotionEvent.ACTION_UP的時候調(diào)用 mVelocityTracker.recycle(); //一般在onDetachedFromWindow中調(diào)用
GestureDetector用于輔助檢測用戶的單擊、滑動锹淌、長按匿值、雙擊等行為。GestureDetector的使用比較簡單葛圃,主要也是輔助檢測常見的觸屏事件千扔。
作者建議:如果只是監(jiān)聽滑動相關(guān)的事件在onTouchEvent中實現(xiàn)憎妙;如果要監(jiān)聽雙擊這種行為的話,那么就使用GestureDetector曲楚。//自定義的View,實現(xiàn)相關(guān)接口(onGestureListener,onDoubleTabListener) GestureDetector mGestureDetector = new GestureDetector(this/*context*/,listener/*onGestureListener*/); //function onTouchEvent(...)或onTouchListener的onTouch(...)中,直接返回 return mGestureDetector.onTouchEvent(event)
更多使用參見[參考2]
3.2 View的滑動
-
layout
public void layout (int l, int t, int r, int b)
參數(shù)都是相對與父布局.
@Override public boolean onTouchEvent(MotionEvent event) { int rawX = (int) (event.getRawX()); //相對與屏幕的坐標 int rawY = (int) (event.getRawY()); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: // 記錄觸摸點坐標 lastX = rawX; lastY = rawY; break; case MotionEvent.ACTION_MOVE: // 計算偏移量 int offsetX = rawX - lastX; int offsetY = rawY - lastY; // 在當前l(fā)eft厘唾、top、right龙誊、bottom的基礎(chǔ)上加上偏移量 layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY); // 重新設(shè)置初始坐標 lastX = rawX; lastY = rawY; break; } return true; }
-
offsetLeftAndRight和offsetTopAndBottom
使用方法同上幾乎一致
//直接在onTouchEvent中調(diào)用,替換上面的layout(...)部分 offsetLeftAndRight(offestX); offsetTopAndBottom(offestY);
-
LayoutParams
這個方式在平時開發(fā)中應(yīng)該使用的比較多.使用也是很簡單,就是修改params的某些參數(shù)
//ViewGroup.MarginLayoutParams layoutParams = // (ViewGroup.MarginLayoutParams) getLayoutParams(); //LinearLayout.LayoutParams extends ViewGroup.MarginLayoutParams, //幾乎所有的LayoutParms都是繼承至 //ViewGroup.MarginLayoutParams, //所以ViewGroup.MarginLayoutParams是通用的... LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) getLayoutParams(); layoutParams.leftMargin = getLeft() + offsetX; layoutParams.topMargin = getTop() + offsetY; setLayoutParams(layoutParams); //requestLayout();效果和上面這一句一樣
-
動畫
動畫部分在Android群英傳-第七章 Android動畫機制與使用技巧中已經(jīng)有比較詳細的說明,在這里就不做說明.
-
ViewDragHelper
ViewDragHelper的使用過程其實也是比較簡單的,主要用戶控制部分都在Callback中.CallBack中的函數(shù)比較多
下面是一個簡單的栗子:
//初始化 mDragHelper = ViewDragHelper.create(this/*要處理的ViewGroup*/, 1.0f/*敏感度*/, new DragHelperCallback()/*前面說的Callback*/); //復(fù)寫一些函數(shù),代碼幾乎固定 @Override public boolean onInterceptTouchEvent(MotionEvent ev) { final int action = MotionEventCompat.getActionMasked(ev); if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { mDragHelper.cancel(); return false; } return mDragHelper.shouldInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent ev) { mDragHelper.processTouchEvent(ev); return true; }
這里沒有詳細寫出CallBack的代碼,可以在這里查看.
-
ScrollTo和ScrollBy
根據(jù)函數(shù)名稱就知道這兩個函數(shù)的區(qū)別,To是到具體的點,by只是與當前的偏移.
這兩個函數(shù)不是針對view本身,而是針對其內(nèi)容,具體來說就是ViewGroup調(diào)用這兩函數(shù),是其內(nèi)部的view在移動,view調(diào)用是其內(nèi)容在動(TextView-->文本,ImageView-->圖像)
另一方面就是他的參數(shù)不同與其他,正數(shù)X往左,正數(shù)Y往上.原因查看這里,
如果想要移動View,就需要在她的parent上調(diào)用這函數(shù),下面是個栗子//替換上文onTouchEvent中的layout(...) ((ViewGroup) getParent()).scrollBy(-offsetX, -offsetY);
-
Scroller
在以前都不知道有這個類,哎,基礎(chǔ)不夠誒.下面一個栗子說明
//初始化,還可以使用插值器 Scroller mScroller = new Scroller(mContext,interpolator/*插值器,可以不用*/); //View的computescroll() @Override public void computeScroll() { super.computeScroll(); // 判斷Scroller是否執(zhí)行完畢 if (mScroller.computeScrollOffset()) { ((View) getParent()).scrollTo( mScroller.getCurrX(), mScroller.getCurrY()); // 通過重繪來不斷調(diào)用computeScroll invalidate();//很重要 } } //啟動 mScroller.startScroll(startX,startY,dX,dY,duration);
本質(zhì)上Scroller不能移動View,在我看來她同屬性動畫中的ValueAnimator是一樣的,因為他們都只是按照某種插值器產(chǎn)生數(shù)值,需要自己把數(shù)值同移動
相聯(lián)系.
3.3 View的事件分發(fā)機制
-
事件分發(fā)過程的三個重要方法
-
dispatchTouchEvent
函數(shù)原型
public boolean dispatchTouchEvent(MotionEvent ev)
主要的功能是負責事件的分發(fā).
返回值:
true: 表示向下分發(fā)中斷
false: 表示繼續(xù)向下分發(fā) -
onInterceptTouchEvent
函數(shù)原型
public boolean onInterceptTouchEvent(MotionEvent event)
主要功能是負責事件的攔截
返回值:
true:攔截,事件交由自己(View/ViewGroup)的onTouchEvent(...)處理
false:不攔截,事件繼續(xù)向下分發(fā). -
onTouchEvent
函數(shù)原型
public boolean onTouchEvent(MotionEvent event)
主要功能是處理觸摸事件
返回值:
true:表示消費了這個事件.
false:表示沒有消費該事件,返回到上級處理.如果一直得不到處理,最終反饋到Activity的onTouchEvent(...)
-
-
函數(shù)之間的邏輯關(guān)系
-
以上三個函數(shù)的偽代碼
類似于遞歸調(diào)用的方式
public boolean dispatchTouchEvent(MotionEvent ev) { boolean consume = false; if (onInterceptTouchEvent(ev)) { consume = onTouchEvent(ev); } else { consume = child.dispatchTouchEvent(ev); } return consume; }
-
函數(shù)與監(jiān)聽接口
在通常情況下,我們?yōu)锽utton等組件設(shè)置了onClickListener接口,有時也會設(shè)置onTouchListener接口,但在什么時候接口中的方法才會執(zhí)行呢?如果設(shè)置了onTouchListener接口監(jiān)聽,會對View(ViewGroup)的onTouchEvent有一定的影響.如果設(shè)置了onTouchListener,她的onTouch的返回值會影響view中onTouchEvent的調(diào)用與否,onTouch返回值的含義與onTouchEvent一樣,表示是否消費了該事件.onTouch會先于onTouchEvent執(zhí)行.偽代碼為
//true表示消費掉 if(!listener.onTouch(ev)){ onTouchEvent(ev); }
對于onClickListener接口,他內(nèi)部方法onCLick的調(diào)用是在onTouchEvent中(根據(jù)上面就知道如果在onTouchListener的onTouch中返回true,onclick就不會再執(zhí)行了),其內(nèi)部部分代碼如下.
//View#onTouchEvent(...) if (mPerformClick == null) { mPerformClick = new PerformClick(); } if (!post(mPerformClick)) { performClick(); } //點擊事件的處理者 private final class PerformClick implements Runnable { @Override public void run() { performClick(); } } //點擊調(diào)用onClick函數(shù) public boolean performClick() { //ListenerInfo封裝了各種監(jiān)聽 final ListenerInfo li = mListenerInfo; if (...) { //調(diào)用部分 li.mOnClickListener.onClick(this); result = true; } ... return result; }
根據(jù)上面的描述,知道調(diào)用順序為onTouchListener#onTouch,返回值決定是否繼續(xù)執(zhí)行view的onTouchEvent,最后在onTouchEvent中執(zhí)行onClickListener的onClick方法.
-
-
分發(fā)過程
-
Activity分發(fā)
觸摸事件最先到達Activity,所以首先會在Activity中分發(fā)
//Activity#dispatchTouchEvent() public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } //分發(fā)到Window. if (getWindow().superDispatchTouchEvent(ev)) { //true表示不再向下分發(fā) return true; } return onTouchEvent(ev); }
在getWindow()中返回mWindow,最終在函數(shù)attach(...)中發(fā)現(xiàn)
mWindow = new PhoneWindow(this);
PhoneWindow不在SDK中,在在線源碼(Android源碼)網(wǎng)站上可以找到相關(guān)的代碼
public boolean superDispatchTouchEvent(MotionEvent event ) { //DecorView extends FrameLayout // DecorView#superDispatchTouchEvent(ev) // public boolean superDispatchTouchEvent(MotionEvent event) { // //來到了ViewGroup // return super.dispatchTouchEvent(event); // } return mDecorView.superDispatchTouchEvent(event); }
由此就把事件分發(fā)到了ViewGroup,接下來就是在VieGroup中分發(fā).
-
-
View分發(fā)
函數(shù)dispatchTouchEvent(...)中的部分代碼
... if (onFilterTouchEventForSecurity(event)) { //noinspection SimplifiableIfStatement ListenerInfo li = mListenerInfo; if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { result = true; } // result==true,函數(shù)onTouchEvent(...)就執(zhí)行不到了,而影想result的主要就是 //li.mOnTouchListener.onTouch(this, // event)的返回值,返回true, //表示事件被處理了,自然不需要在調(diào)用onTouchEvent(...)來重新處理 // 前面說過onClick(...)是在onTouchEvent(...)中調(diào)用的.即優(yōu)先級小于onTouch() if (!result && onTouchEvent(event)) { result = true; } } ...
函數(shù)onTouchEvent(...)主要就是處理事件,前面已經(jīng)說過onClick的執(zhí)行過程了.這里就不說了.
-
ViewGroup分發(fā)
函數(shù)dispatchTouchEvent(...)中的部分代碼
// Check for interception. final boolean intercepted; // 事件為ACTION_DOWN或者mFirstTouchTarget不為null //(即已經(jīng)找到能夠接收touch事件的目標組件)時if成立 if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { //判斷disallowIntercept(禁止攔截)標志位 //因為在其他地方可能調(diào)用了 //requestDisallowInterceptTouchEvent(boolean disallowIntercept) //從而禁止執(zhí)行是否需要攔截的判斷 //(有點拗口~其實看requestDisallowInterceptTouchEvent()方法名就可明白) final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; //補充:根據(jù)下面的代碼可以發(fā)現(xiàn), disallowIntercept 的值等于函數(shù) //requestDisallowInterceptTouchEvent的參數(shù). if (!disallowIntercept) { intercepted = onInterceptTouchEvent(ev); ev.setAction(action); // restore action in case it was changed } else { intercepted = false; } } else { // There are no touch targets and this action is not an initial down // so this view group continues to intercept touches. intercepted = true; }
注意上文代碼中的注釋部分,這里看一下部分requesrDisallowInterceptTouchEvent(...)的部分源碼
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { //更具這里可以看出,當disallowIntercept=true時, //(mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0 成立, //這就意味著上面一段代碼中的disallowIntercept=true; if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) { // We're already in this state, assume our ancestors are too return; } ... }
由此可見VIewGroup只會在ACTION=ACTION_DOWN或者mFirstTouchTarget != null時才判斷是否攔截事件,因為一個事件序列(DOWN->MOVE->...->UP)只能有一個View處理.但是mFirstTouchTarget != null表示什么呢?
當事件被ViewGroup的子元素成功處理了(子View的onTouchEvent/onTouch返回了true??),mFirstTouchTarget被賦值指向子元素(即!=null)
函數(shù)dispatchTouchEvent(...)的部分實現(xiàn).
final View[] children = mChildren; for (int i = childrenCount - 1; i >= 0; i--) { final int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i; final View child = (preorderedList == null) ? children[childIndex] : preorderedList.get(childIndex); // If there is a view that has accessibility focus we want it // to get the event first and if not handled we will perform a // normal dispatch. We may do a double iteration but this is // safer given the timeframe. if (childWithAccessibilityFocus != null) { if (childWithAccessibilityFocus != child) { continue; } childWithAccessibilityFocus = null; i = childrenCount - 1; } if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); continue; } newTouchTarget = getTouchTarget(child); if (newTouchTarget != null) { // Child is already receiving touch within its bounds. // Give it the new pointer in addition to the ones it is handling. // 找到接收Touch事件的子View!!!!!!!即為newTouchTarget. newTouchTarget.pointerIdBits |= idBitsToAssign; break; } resetCancelNextUpFlag(child); //注意這個方法,再后面再看看..根據(jù)源碼, //可以知道它返回的是子View(child)的dispatchTouchEvent(...) //當child==null,返回super.dispatchTouchEvent(...), //即View的dispatchTouchEvent(...) if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { // Child wants to receive touch within its bounds. mLastTouchDownTime = ev.getDownTime(); if (preorderedList != null) { // childIndex points into presorted list, find original index for (int j = 0; j < childrenCount; j++) { if (children[childIndex] == mChildren[j]) { mLastTouchDownIndex = j; break; } } } else { mLastTouchDownIndex = childIndex; } mLastTouchDownX = ev.getX(); mLastTouchDownY = ev.getY(); //找到了事件的處理者,終止循環(huán) newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true; break; } // The accessibility focus didn't handle the event, so clear // the flag and do a normal dispatch to all children. ev.setTargetAccessibilityFocus(false); }
同樣是dispatchTouchEvent(...)的部分代碼
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
//這里說明沒有子View處理該事件,只得有View的dispatchTouchEvent(...)來處理.
//關(guān)于該函數(shù)的部分源碼在后面介紹.
handled = dispatchTransformedTouchEvent(ev, canceled, null/*child*/,
TouchTarget.ALL_POINTER_IDS);
} else {
...
}
函數(shù)addTouchTarget(...)的具體實現(xiàn).
private TouchTarget addTouchTarget(View child, int pointerIdBits) {
TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
函數(shù)dispatchTransformedTouchEvent(...)的部分實現(xiàn).
....
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
....
return handled.
3.4 View的滑動沖突
-
常見的滑動沖突的場景:
- 外部滑動方向和內(nèi)部滑動方向不一致抚垃,例如viewpager中包含listview;
- 外部滑動方向和內(nèi)部滑動方向一致趟大,例如viewpager的單頁中存在可以滑動的bannerview鹤树;
- 上面兩種情況的嵌套,例如viewpager的單個頁面中包含了bannerview和listview逊朽。
-
滑動沖突處理規(guī)則
可以根據(jù)滑動距離和水平方向形成的夾角罕伯;或者根絕水平和豎直方向滑動的距離差;或者兩個方向上的速度差等
-
解決方式
-
外部攔截法:點擊事件都先經(jīng)過父容器的攔截處理叽讳,如果父容器需要此事件就攔截追他,如果不需要就不攔截。該方法需要重寫父容器的onInterceptTouchEvent方法岛蚤,在內(nèi)部做相應(yīng)的攔截即可邑狸,其他均不需要做修改。偽代碼如下:
public boolean onInterceptTouchEvent(MotionEvent event) { boolean intercepted = false; int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: { intercepted = false; break; } case MotionEvent.ACTION_MOVE: { int deltaX = x - mLastXIntercept; int deltaY = y - mLastYIntercept; if (父容器需要攔截當前點擊事件的條件涤妒,例如:Math.abs(deltaX) > Math.abs(deltaY)) { intercepted = true; } else { intercepted = false; } break; } case MotionEvent.ACTION_UP: { intercepted = false; break; } default: break; } mLastXIntercept = x; mLastYIntercept = y; return intercepted; }
-
內(nèi)部攔截法:父容器不攔截任何事件单雾,所有的事件都傳遞給子元素,如果子元素需要此事件就直接消耗掉她紫,否則就交給父容器來處理硅堆。這種方法和Android中的事件分發(fā)機制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作贿讹。
public boolean dispatchTouchEvent(MotionEvent event) { int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: { getParent().requestDisallowInterceptTouchEvent(true); break; } case MotionEvent.ACTION_MOVE: { int deltaX = x - mLastX; int deltaY = y - mLastY; if (當前view需要攔截當前點擊事件的條件硬萍,例如:Math.abs(deltaX) > Math.abs(deltaY)) { getParent().requestDisallowInterceptTouchEvent(false); } break; } case MotionEvent.ACTION_UP: { break; } default: break; } mLastX = x; mLastY = y; return super.dispatchTouchEvent(event); }
父View的onInterceptTouchEvent(...)偽代碼
public boolean onInterceptTouchEvent(MotionEvent ev){ if(ev.getAction() == MotionEvent.ACTION_DOWN){ retuen false; }else{ retuen true; } }
內(nèi)部攔截法過程說明,父類在ACTION_DOWN時不攔截,子類在ACTION_DOWN時攔截,這時mFirstTouchTarget!=null, disallowIntercept = true,這意味著父類的onInterceptTouchEvent(...)不會再被執(zhí)行,并且一個事件序列只有一個View來處理,則所有的后續(xù)ACTION_MOVE都會傳到子View,當在子View中判斷到某個事件應(yīng)該由父View處理,只需重置disallowIntercept=false即可,即調(diào)用函數(shù)requestDisallowInterceptTouchEvent(false),這時事件就到父View的onTouchEvent(...)處理的(因為onInterceptionTouchEvent在非ACTION_DOWN時都返回true).如果父類沒有在設(shè)置requestDisallowInterceptTouchEvent(true)的話,這個事件就會一直都在父View中做處理了.(注:為個人理解,若有不對,望其指出)
-