學(xué)習(xí)內(nèi)容
- View 基礎(chǔ)
- 滑動(dòng)
- 事件分發(fā)機(jī)制
- 滑動(dòng)沖突
1. View 基礎(chǔ)知識(shí)
-
View 定義
- View 是 Android 種所有控件的基類,是一種界面層的控件的一種抽象冲秽,代表了一個(gè)控件
- ViewGroup 繼承 View,其內(nèi)部包含了許多個(gè)控件,即一組 View
- ViewGroup 內(nèi)部是可以有子 View 的,而這個(gè)子 View 同樣還可以是 ViewGroup
-
View 位置參數(shù)
- Android 種蒸痹,坐標(biāo)系的 x 軸和 y 軸的正方向分別是右和下。
- View 的位置由其四個(gè)頂點(diǎn)決定呛哟,分別對(duì)應(yīng)四個(gè)屬性:top(左上角縱坐標(biāo))叠荠、left(左上角橫坐標(biāo))、right(右下角橫坐標(biāo))扫责、bottom(右下角縱坐標(biāo))榛鼎,這些坐標(biāo)相對(duì)于父容器來說的。
- Android 3.0 以后,加入 x者娱、y抡笼、translationX、translationY黄鳍,其中 x推姻、y是 View 左上角的坐標(biāo),而 translationX际起、translationY 是 View 左上角相對(duì)于父容器的偏移量
-
MotionEvent 和 TouchSlop
- MotionEvent 是手指接觸屏幕后所產(chǎn)生的一系列事件拾碌。
- 一般通過 MotionEvent 對(duì)象可以得到點(diǎn)擊事件發(fā)生的 x 和 y 坐標(biāo)
- getX / getY:相對(duì)坐標(biāo)
- getRawX / getRawY:絕對(duì)坐標(biāo)
- TouchSlop 指系統(tǒng)能識(shí)別出的被認(rèn)為是滑動(dòng)的最小距離,通過
ViewConfiguration.get(getContext()).getScaledTouchSlop()
方法來獲取街望。
-
VelocityTracker、GestureDetector 和 Scroller
-
VelocityTracker
- 速度追蹤弟跑,用于追蹤手指在滑動(dòng)過程中的速度
- 使用
VelocityTracker velocityTracker = VelociityTracker.obtain(); velocityTracker.addMovement(event); //獲取速度之前按必須先計(jì)算速度灾前,速度指一段時(shí)間內(nèi)手指滑過的像素?cái)?shù) //速度 = (終點(diǎn)位置 - 起點(diǎn)位置)/ 時(shí)間段 velocityTracker.computeCurrentVelocity(1000); int xVelocity = (int)velocityTracker.getXVelocity(); int yVelocity = (int)velocityTracker.getYVelocity(); //不再需要使用的時(shí)候,重置并回收內(nèi)存 velocityTracker.clear(); velocityTracker.recycler();
-
GestureDetector
手勢(shì)檢測(cè)孟辑,用于輔助檢測(cè)用戶的單擊哎甲、滑動(dòng)、長(zhǎng)按饲嗽、雙擊等行為炭玫。
-
使用
//創(chuàng)建 GestureDetector 對(duì)象并實(shí)現(xiàn) 指定接口如 OnGestureListener 、 OnDoubleTapListener GestureDetector mGestureDetector = new GestureDetector(this); mGestureDetector.setIsLongpressEnabled(flase); //接著接管目標(biāo) View 的 onTouchEvent 方法 boolean consume = mGestureDetector.onTouchEvent(event); return consume;
建議:如果只是監(jiān)聽滑動(dòng)相關(guān)貌虾,建議自己在 onTouchEvent 中實(shí)現(xiàn)吞加,如果要監(jiān)聽雙擊這種行為的話,那么就使用 GestureDetector
-
Scroller
彈性滑動(dòng)對(duì)象尽狠,用于實(shí)現(xiàn) View 的彈性滑動(dòng)
-
使用
Scroller mScroller = new Scroller(mContext); //緩慢滾動(dòng)到指定位置 private void smoothScrollTo(int destX,int destY){ int scrollX = getScrollX(); int delta = destX - scrollX; //1000ms 內(nèi)滑向 destX衔憨,效果就是緩慢滑動(dòng) mScroller.startScroll(scrollX,0,delta,0,1000); invalidate(); } @Override public void computeScroll(){ if(mScroll.computeScrollOffset()){ scrollTo(mScroller.getCurrX(),mScroller.getCurrY()); postInvalidate(); } }
-
View 的滑動(dòng)
-
使用 ScrollTo / ScrollBy
- 只能改變 View 內(nèi)容的位置而不能改變 View 在布局中的位置
- mScrolllX 的值總是等于 View 左邊緣和 View 內(nèi)容左邊緣在水平方向的距離;mScrollY 的值總是等于 View 上邊緣和 View 內(nèi)容上邊緣在豎直方向的距離袄膏,二者單位均為像素践图。
- 當(dāng) View 左邊緣在 View 內(nèi)容左邊緣的右邊時(shí),mScrollX 為正值
- 當(dāng) View 上邊緣在 View 內(nèi)容上邊緣的下邊時(shí)沉馆,mScrollY 為正值
-
使用動(dòng)畫
- translationX / translationY 屬性
- View 動(dòng)畫
- 以上兩種码党,只是移動(dòng) View 的影像,不能改變真正的位置
- 屬性動(dòng)畫
- 可以改變 View 的參數(shù)
-
改變布局參數(shù)
改變 LayoutParams
-
舉例:
//將一個(gè) Button 右平移100px ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams)mTbn.getLayoutParams(); params.leftMargin += 100; mBtn.requestLayout(); //或者 mBtn.setLayoutParams(params);
-
小結(jié)
- ScrollTo / ScrollBy:操作簡(jiǎn)單斥黑,適合對(duì) View 內(nèi)容的滑動(dòng)
- 動(dòng)畫(View 動(dòng)畫):操作簡(jiǎn)單揖盘,主要適用于沒有交互的 View 和實(shí)現(xiàn)復(fù)雜的動(dòng)畫效果
- 改變布局參數(shù):操作稍微復(fù)雜,適用于有交互的 View
3. 彈性滑動(dòng)
具體思想:將一次大的滑動(dòng)分成若干次小的滑動(dòng)心赶,并在一個(gè)時(shí)間段內(nèi)完成扣讼。
-
實(shí)現(xiàn)方式
-
使用 Scrolller
當(dāng) View 重繪后會(huì)在 draw 方法中調(diào)用 computeScroll,而 compiteScroll 又會(huì)去向 Scroller 獲取當(dāng)前的 scrollX 和 scrollY缨叫;然后通過 scrollTo 方法實(shí)現(xiàn)滑動(dòng)椭符;接著又調(diào)用 postInvalidate 方法來進(jìn)行第二次重繪荔燎,這一次重繪的過程和第一次重繪一樣,還是會(huì)導(dǎo)致 computeScroll 方法被調(diào)用销钝;后續(xù)同上有咨,如此反復(fù),直到整個(gè)滑動(dòng)過程結(jié)束蒸健。
通過動(dòng)畫
-
使用延時(shí)策略
核心思想:通過發(fā)送一系列延時(shí)消息從而達(dá)到一種漸進(jìn)式的效果
-
方法:
使用 Handler或 View 的 postDelayed 方法座享,也可以使用線程的 sleep 方法,對(duì)于 postDelayed 方法來說似忧,通過其延時(shí)發(fā)送消息渣叛,然后在消息中進(jìn)行 View 的滑動(dòng)。接連不斷地發(fā)送這種延時(shí)消息盯捌,以此大導(dǎo)彈性滑動(dòng)的效果
-
4. View 的事件分發(fā)機(jī)制
-
核心的三個(gè)方法
-
dispatchTouchEvent(MotionEvent ev)
用來進(jìn)行事件的分發(fā)淳衙。返回結(jié)果受當(dāng)前 View 的 onTouchEvent 和 下級(jí) View 的 dispatchTouchEvent 方法的影響,表示是否消耗該事件饺著。
-
onInterceptTouchEvent(MotionEvent ev)
在上述方法中調(diào)用箫攀,用來判斷是否攔截某個(gè)事件,如果當(dāng)前View 攔截了某個(gè)事件幼衰,那么在同一個(gè)時(shí)間序列中靴跛,此方法不會(huì)再次調(diào)用,返回結(jié)果表示是否攔截當(dāng)前事件
-
onTouchEvent(MotionEvent ev)
在 dispatchTouchEvent 方法中調(diào)用渡嚣,用來處理點(diǎn)擊事件梢睛,返回結(jié)果表示是否消耗事件,如果不消耗严拒,則同一個(gè)事件序列中扬绪,當(dāng)前 View 無(wú)法再次接收到事件
-
三者關(guān)系偽代碼表示
public boolean dispatchTouchEvent(MotionEvent ev){ boolean consume = false; if(onInterceptTouchEvent(ev){ consume = onTouchEvent(ev); }else { consume = child.dispatchTouchEvent(ev); } return consume; }
-
-
事件的傳遞規(guī)則
對(duì)于一個(gè)根 ViewGroup 來說,點(diǎn)擊事件產(chǎn)生后裤唠,首先傳遞給它挤牛,此時(shí)它的 dispatchTouchEvent 會(huì)被調(diào)用,如果這個(gè) ViewGroup 的 onInterceptTouchEvent 方法返回 true 就表示它要攔截當(dāng)前事件种蘸,接著事件就會(huì)交給這個(gè) ViewGroup 處理墓赴,即調(diào)用它的 onTouchEvent 方法。如果 onInterceptTouchEvent 方法 返回 false航瞭,表示不攔截當(dāng)前事件诫硕,這時(shí)該事件傳遞給它的子元素,接著子元素的 dispatchTouchEvent 方法被調(diào)用刊侯,如此反復(fù)章办。
-
傳遞過程遵循如下順序:
Activity -> Window ( PhoneWindow )-> View (DecorView)
當(dāng)一個(gè) View 的 onTouchEvent 返回 false,那么會(huì)調(diào)用其父容器的 onTouchEvent ,依此類推藕届。如果所有的元素都不處理這個(gè)事件挪蹭,那么這個(gè)事件將會(huì)最終傳遞給 Activity 處理。
-
一些結(jié)論
- 同一個(gè)時(shí)間序列指 從手指接觸屏幕的那一刻起休偶,到手指離開屏幕的那一刻結(jié)束梁厉。(down -> [move]* -> up )
- 正常情況下,一個(gè)事件序列只能被一個(gè) View 攔截并消耗
- 某個(gè) View 一旦決定攔截踏兜,那么這一個(gè)事件序列都只能由它來處理词顾,并且 onInterceptTouchEvent 不會(huì)再被調(diào)用
- 某個(gè) View 一旦開始處理事件,如果它不消耗 ACTION_DOWN( onTouchEvent 返回了 false)碱妆,那么同一事件序列中其他事件都不會(huì)再交給它來處理肉盹,事件將重新交給他的父元素處理,即父元素的 onTouchEvent 會(huì)被調(diào)用山橄。
- 如果某個(gè) View 不消耗除 ACTION_DOWN 以外的其他事件垮媒,那么這個(gè)點(diǎn)擊事件會(huì)消失,此時(shí)父元素的 onTouchEvent 并不會(huì)被調(diào)用航棱,并且當(dāng)前 View 可以收到后續(xù)事件,最終這些消失的點(diǎn)擊事件會(huì)傳遞給 Activity 處理
- ViewGroup 默認(rèn)不攔截任何事件萌衬,ViewGroup 的 onInterceptTouchEvent 方法默認(rèn)返回 false
- View 沒有 onInterceptTouchEvent 方法饮醇,一旦有事件傳遞給它,那么它的 onTouchEvent 方法會(huì)被調(diào)用
- View 的 onTouchEvent 方法默認(rèn)消耗事件(返回 true )秕豫,除非他是不可點(diǎn)擊的(clickable 和 longClickable 同時(shí)為 false)朴艰。View 的 longClickable 屬性默認(rèn)都為 false,clickable 屬性分情況混移,Button 默認(rèn)為 true祠墅,TextView 默認(rèn)為false。
- View 的 enable 屬性不影響 o'nTouchEvent 的默認(rèn)返回值
- onClick 會(huì)發(fā)生的前提是當(dāng)前 View 是可點(diǎn)擊的歌径,并且它收到了 down 和 up 的事件
- 時(shí)間傳遞過程是由外向內(nèi)的毁嗦,即事件總是先傳遞給父元素,然后再由父元素分發(fā)給子 View回铛,通過 requestDisallowInterceptTouchEvent 方法可以在子元素中干預(yù)父元素的事件分發(fā)過程狗准,但是 ACTION_DOWN 事件除外。
5. View 的滑動(dòng)沖突
-
常見的滑動(dòng)沖突場(chǎng)景
- 外部滑動(dòng)方向和內(nèi)部滑動(dòng)方向不一致
- 外部滑動(dòng)方向和內(nèi)部滑動(dòng)方向一致
- 上面兩種情況的嵌套
-
滑動(dòng)沖突處理規(guī)則
- 滑動(dòng)方向有明顯差異時(shí):根據(jù)特征(水平滑動(dòng)還是豎直滑動(dòng))來決定讓誰(shuí)來攔截事件
- 滑動(dòng)方向無(wú)法辨別:根據(jù)業(yè)務(wù)需求來決定讓誰(shuí)來攔截事件
滑動(dòng)沖突的解決方式
-
外部攔截法(推薦)
指點(diǎn)擊事件都先經(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()){ //對(duì)于 ACTION_DOWN 事件捞附,父容器必須返回false,即不攔截,一旦攔截鸟召,那么后續(xù)的 MOVE胆绊、UP 事件都會(huì)直接交由父容器處理。沒法傳遞給子元素 case MotionEvent.ACTION_DOWN: intercepted = false; break; //MOVE 事件根據(jù)需求來決定是否攔截药版,父容器需要?jiǎng)t返回true辑舷,否則返回false case MotionEvent.ACTION_MOVE: if (/*父容器需要當(dāng)前點(diǎn)擊事件*/){ intercepted = true; }else { intercepted = false; } break; //必須返回 false,因?yàn)?UP 事件沒太多意義 case MotionEvent.ACTION_UP: intercepted = false; break; default: break; } mLastXIntercept = x; mLastYIntercept = y; return intercepted; }
-
內(nèi)部攔截法
指 父容器不攔截任何事件槽片,所有的事件都傳遞給子元素何缓,如果子元素需要此事件就直接消耗掉,否則交由父容器進(jìn)行處理还栓。
需要 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 (/*父容器需要此類點(diǎn)擊事件*/){ getParent().requestDisallowInterceptTouchEvent(false); } break; case MotionEvent.ACTION_UP: break; default: break; } mLastX = x; mLastY = y; return super.dispatchTouchEvent(ev); } //父元素 //父元素默認(rèn)攔截除了 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; } }