學(xué)習(xí)內(nèi)容
- View 基礎(chǔ)
- 滑動
- 事件分發(fā)機制
- 滑動沖突
1. View 基礎(chǔ)知識
-
View 定義
- View 是 Android 種所有控件的基類,是一種界面層的控件的一種抽象,代表了一個控件
- ViewGroup 繼承 View,其內(nèi)部包含了許多個控件,即一組 View
- ViewGroup 內(nèi)部是可以有子 View 的,而這個子 View 同樣還可以是 ViewGroup
-
View 位置參數(shù)
- Android 種,坐標系的 x 軸和 y 軸的正方向分別是右和下。
- View 的位置由其四個頂點決定,分別對應(yīng)四個屬性:top(左上角縱坐標)、left(左上角橫坐標)、right(右下角橫坐標)、bottom(右下角縱坐標),這些坐標相對于父容器來說的。
- Android 3.0 以后,加入 x、y、translationX、translationY,其中 x、y是 View 左上角的坐標,而 translationX、translationY 是 View 左上角相對于父容器的偏移量
-
MotionEvent 和 TouchSlop
- MotionEvent 是手指接觸屏幕后所產(chǎn)生的一系列事件。
- 一般通過 MotionEvent 對象可以得到點擊事件發(fā)生的 x 和 y 坐標
- getX / getY:相對坐標
- getRawX / getRawY:絕對坐標
- TouchSlop 指系統(tǒng)能識別出的被認為是滑動的最小距離,通過
ViewConfiguration.get(getContext()).getScaledTouchSlop()方法來獲取。
-
VelocityTracker、GestureDetector 和 Scroller
-
VelocityTracker
- 速度追蹤,用于追蹤手指在滑動過程中的速度
- 使用
VelocityTracker velocityTracker = VelociityTracker.obtain(); velocityTracker.addMovement(event); //獲取速度之前按必須先計算速度,速度指一段時間內(nèi)手指滑過的像素數(shù) //速度 = (終點位置 - 起點位置)/ 時間段 velocityTracker.computeCurrentVelocity(1000); int xVelocity = (int)velocityTracker.getXVelocity(); int yVelocity = (int)velocityTracker.getYVelocity(); //不再需要使用的時候,重置并回收內(nèi)存 velocityTracker.clear(); velocityTracker.recycler();
-
GestureDetector
手勢檢測,用于輔助檢測用戶的單擊、滑動、長按、雙擊等行為。
-
使用
//創(chuàng)建 GestureDetector 對象并實現(xiàn) 指定接口如 OnGestureListener 、 OnDoubleTapListener GestureDetector mGestureDetector = new GestureDetector(this); mGestureDetector.setIsLongpressEnabled(flase); //接著接管目標 View 的 onTouchEvent 方法 boolean consume = mGestureDetector.onTouchEvent(event); return consume; 建議:如果只是監(jiān)聽滑動相關(guān),建議自己在 onTouchEvent 中實現(xiàn),如果要監(jiān)聽雙擊這種行為的話,那么就使用 GestureDetector
-
Scroller
彈性滑動對象,用于實現(xiàn) View 的彈性滑動
-
使用
Scroller mScroller = new Scroller(mContext); //緩慢滾動到指定位置 private void smoothScrollTo(int destX,int destY){ int scrollX = getScrollX(); int delta = destX - scrollX; //1000ms 內(nèi)滑向 destX,效果就是緩慢滑動 mScroller.startScroll(scrollX,0,delta,0,1000); invalidate(); } @Override public void computeScroll(){ if(mScroll.computeScrollOffset()){ scrollTo(mScroller.getCurrX(),mScroller.getCurrY()); postInvalidate(); } }
-
View 的滑動
-
使用 ScrollTo / ScrollBy
- 只能改變 View 內(nèi)容的位置而不能改變 View 在布局中的位置
- mScrolllX 的值總是等于 View 左邊緣和 View 內(nèi)容左邊緣在水平方向的距離;mScrollY 的值總是等于 View 上邊緣和 View 內(nèi)容上邊緣在豎直方向的距離,二者單位均為像素。
- 當(dāng) View 左邊緣在 View 內(nèi)容左邊緣的右邊時,mScrollX 為正值
- 當(dāng) View 上邊緣在 View 內(nèi)容上邊緣的下邊時,mScrollY 為正值
-
使用動畫
- translationX / translationY 屬性
- View 動畫
- 以上兩種,只是移動 View 的影像,不能改變真正的位置
- 屬性動畫
- 可以改變 View 的參數(shù)
-
改變布局參數(shù)
改變 LayoutParams
-
舉例:
//將一個 Button 右平移100px ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams)mTbn.getLayoutParams(); params.leftMargin += 100; mBtn.requestLayout(); //或者 mBtn.setLayoutParams(params);
-
小結(jié)
- ScrollTo / ScrollBy:操作簡單,適合對 View 內(nèi)容的滑動
- 動畫(View 動畫):操作簡單,主要適用于沒有交互的 View 和實現(xiàn)復(fù)雜的動畫效果
- 改變布局參數(shù):操作稍微復(fù)雜,適用于有交互的 View
3. 彈性滑動
具體思想:將一次大的滑動分成若干次小的滑動,并在一個時間段內(nèi)完成。
-
實現(xiàn)方式
-
使用 Scrolller
當(dāng) View 重繪后會在 draw 方法中調(diào)用 computeScroll,而 compiteScroll 又會去向 Scroller 獲取當(dāng)前的 scrollX 和 scrollY;然后通過 scrollTo 方法實現(xiàn)滑動;接著又調(diào)用 postInvalidate 方法來進行第二次重繪,這一次重繪的過程和第一次重繪一樣,還是會導(dǎo)致 computeScroll 方法被調(diào)用;后續(xù)同上,如此反復(fù),直到整個滑動過程結(jié)束。
通過動畫
-
使用延時策略
核心思想:通過發(fā)送一系列延時消息從而達到一種漸進式的效果
-
方法:
使用 Handler或 View 的 postDelayed 方法,也可以使用線程的 sleep 方法,對于 postDelayed 方法來說,通過其延時發(fā)送消息,然后在消息中進行 View 的滑動。接連不斷地發(fā)送這種延時消息,以此大導(dǎo)彈性滑動的效果
-
4. View 的事件分發(fā)機制
-
核心的三個方法
-
dispatchTouchEvent(MotionEvent ev)用來進行事件的分發(fā)。返回結(jié)果受當(dāng)前 View 的 onTouchEvent 和 下級 View 的 dispatchTouchEvent 方法的影響,表示是否消耗該事件。
-
onInterceptTouchEvent(MotionEvent ev)在上述方法中調(diào)用,用來判斷是否攔截某個事件,如果當(dāng)前View 攔截了某個事件,那么在同一個時間序列中,此方法不會再次調(diào)用,返回結(jié)果表示是否攔截當(dāng)前事件
-
onTouchEvent(MotionEvent ev)在 dispatchTouchEvent 方法中調(diào)用,用來處理點擊事件,返回結(jié)果表示是否消耗事件,如果不消耗,則同一個事件序列中,當(dāng)前 View 無法再次接收到事件
-
三者關(guān)系偽代碼表示
public boolean dispatchTouchEvent(MotionEvent ev){ boolean consume = false; if(onInterceptTouchEvent(ev){ consume = onTouchEvent(ev); }else { consume = child.dispatchTouchEvent(ev); } return consume; }
-
-
事件的傳遞規(guī)則
對于一個根 ViewGroup 來說,點擊事件產(chǎn)生后,首先傳遞給它,此時它的 dispatchTouchEvent 會被調(diào)用,如果這個 ViewGroup 的 onInterceptTouchEvent 方法返回 true 就表示它要攔截當(dāng)前事件,接著事件就會交給這個 ViewGroup 處理,即調(diào)用它的 onTouchEvent 方法。如果 onInterceptTouchEvent 方法 返回 false,表示不攔截當(dāng)前事件,這時該事件傳遞給它的子元素,接著子元素的 dispatchTouchEvent 方法被調(diào)用,如此反復(fù)。
-
傳遞過程遵循如下順序:
Activity -> Window ( PhoneWindow )-> View (DecorView)
當(dāng)一個 View 的 onTouchEvent 返回 false,那么會調(diào)用其父容器的 onTouchEvent ,依此類推。如果所有的元素都不處理這個事件,那么這個事件將會最終傳遞給 Activity 處理。
-
一些結(jié)論
- 同一個時間序列指 從手指接觸屏幕的那一刻起,到手指離開屏幕的那一刻結(jié)束。(down -> [move]* -> up )
- 正常情況下,一個事件序列只能被一個 View 攔截并消耗
- 某個 View 一旦決定攔截,那么這一個事件序列都只能由它來處理,并且 onInterceptTouchEvent 不會再被調(diào)用
- 某個 View 一旦開始處理事件,如果它不消耗 ACTION_DOWN( onTouchEvent 返回了 false),那么同一事件序列中其他事件都不會再交給它來處理,事件將重新交給他的父元素處理,即父元素的 onTouchEvent 會被調(diào)用。
- 如果某個 View 不消耗除 ACTION_DOWN 以外的其他事件,那么這個點擊事件會消失,此時父元素的 onTouchEvent 并不會被調(diào)用,并且當(dāng)前 View 可以收到后續(xù)事件,最終這些消失的點擊事件會傳遞給 Activity 處理
- ViewGroup 默認不攔截任何事件,ViewGroup 的 onInterceptTouchEvent 方法默認返回 false
- View 沒有 onInterceptTouchEvent 方法,一旦有事件傳遞給它,那么它的 onTouchEvent 方法會被調(diào)用
- View 的 onTouchEvent 方法默認消耗事件(返回 true ),除非他是不可點擊的(clickable 和 longClickable 同時為 false)。View 的 longClickable 屬性默認都為 false,clickable 屬性分情況,Button 默認為 true,TextView 默認為false。
- View 的 enable 屬性不影響 o'nTouchEvent 的默認返回值
- onClick 會發(fā)生的前提是當(dāng)前 View 是可點擊的,并且它收到了 down 和 up 的事件
- 時間傳遞過程是由外向內(nèi)的,即事件總是先傳遞給父元素,然后再由父元素分發(fā)給子 View,通過 requestDisallowInterceptTouchEvent 方法可以在子元素中干預(yù)父元素的事件分發(fā)過程,但是 ACTION_DOWN 事件除外。
5. View 的滑動沖突
-
常見的滑動沖突場景
- 外部滑動方向和內(nèi)部滑動方向不一致
- 外部滑動方向和內(nèi)部滑動方向一致
- 上面兩種情況的嵌套
-
滑動沖突處理規(guī)則
- 滑動方向有明顯差異時:根據(jù)特征(水平滑動還是豎直滑動)來決定讓誰來攔截事件
- 滑動方向無法辨別:根據(jù)業(yè)務(wù)需求來決定讓誰來攔截事件
滑動沖突的解決方式
-
外部攔截法(推薦)
指點擊事件都先經(jīng)過父容器的攔截處理,如果父容器需要此事件就攔截,否則不攔截。
需要重寫父容器的 onInterceptTouchEvent 方法,在內(nèi)部做相應(yīng)的攔截
-
典型偽代碼
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { boolean intercepted = false; int x = (int) ev.getX(); int y = (int) ev.getY(); switch (ev.getAction()){ //對于 ACTION_DOWN 事件,父容器必須返回false,即不攔截,一旦攔截,那么后續(xù)的 MOVE、UP 事件都會直接交由父容器處理。沒法傳遞給子元素 case MotionEvent.ACTION_DOWN: intercepted = false; break; //MOVE 事件根據(jù)需求來決定是否攔截,父容器需要則返回true,否則返回false case MotionEvent.ACTION_MOVE: if (/*父容器需要當(dāng)前點擊事件*/){ intercepted = true; }else { intercepted = false; } break; //必須返回 false,因為 UP 事件沒太多意義 case MotionEvent.ACTION_UP: intercepted = false; break; default: break; } mLastXIntercept = x; mLastYIntercept = y; return intercepted; }
-
內(nèi)部攔截法
指 父容器不攔截任何事件,所有的事件都傳遞給子元素,如果子元素需要此事件就直接消耗掉,否則交由父容器進行處理。
需要 requestDisallowInterceptTouchEvent 方法配合工作。
-
典型偽代碼
//子元素 @Override public boolean dispatchTouchEvent(MotionEvent ev) { int x = (int) ev.getX(); int y = (int) ev.getY(); switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: getParent().requestDisallowInterceptTouchEvent(true); break; case MotionEvent.ACTION_MOVE: int deltaX = x - mLastX; int deltaY = y - mLastY; if (/*父容器需要此類點擊事件*/){ getParent().requestDisallowInterceptTouchEvent(false); } break; case MotionEvent.ACTION_UP: break; default: break; } mLastX = x; mLastY = y; return super.dispatchTouchEvent(ev); } //父元素 //父元素默認攔截除了 ACTION_DOWN 外的事件,原因是 ACTION_DOWN 不受 requestDisallowInterceptTouchEvent() 方法的控制 @Override public boolean onInterceptTouchEvent(MotionEvent ev) { int action = ev.getAction(); if (action == MotionEvent.ACTION_DOWN){ return false; }else { return true; } }