
View的事件體系
View的基礎(chǔ)知識(shí)
View的位置參數(shù)
一個(gè)View的位置主要由四個(gè)頂點(diǎn)構(gòu)成, 或者可以就是兩個(gè)點(diǎn)就可以確定. 分別為左上點(diǎn),右下角每個(gè)點(diǎn)都對(duì)應(yīng)x,y兩個(gè)屬性. 因?yàn)槟J(rèn)都是矩形, 所以兩個(gè)點(diǎn)就可以確定.
一個(gè)View的大小可以利用四個(gè)屬性可知. 分別對(duì)應(yīng)getLeft(),getRight(),getTop(),getBottom系統(tǒng)提供的函數(shù).
- 一個(gè)控件的寬: getRight() - getLeft()
- 一個(gè)控件的高: getTop() - getBottom()
在Android3.0中, View增加了幾個(gè)屬性:x , y, translationX, translationY
-
x,y: 表示View的左上角坐標(biāo)點(diǎn)(最終坐標(biāo)點(diǎn)). -
translationX,translationY: 表示View的左上點(diǎn)相對(duì)于父容器的偏移量(默認(rèn)是0).
而這些參數(shù)的換算關(guān)系為:
x = left + translationX;
y = top + translationY;
MotionEvent和TouchSlop
MotionEvent是指手指在接觸屏幕之后產(chǎn)生的一系列事件
最常見事件類型是ACTION_DOWN,ACTION_MOVE,ACTION_UP
一次事件可以有不同的持續(xù)時(shí)間, 和不同的事件類型. 例如
- 按下抬起 : DOWN –> UP
- 按下移動(dòng)抬起 : DOWN -> MOVE -> MOVE -> … ->UP
- ….
而在移動(dòng)時(shí)可以根據(jù)MotionEvent提供的參數(shù)獲對(duì)應(yīng)的xy取值.
*getX/getY: 返回相對(duì)于當(dāng)前View左上角的x,y坐標(biāo).
getRawX/getRawY: 返回的是針對(duì)整個(gè)屏幕的左上角的x,y坐標(biāo).
TouchSlop是系統(tǒng)可以識(shí)別的最小滑動(dòng)距離單位
只有手指兩次滑動(dòng)大于這個(gè)TouchSlop,系統(tǒng)才認(rèn)為是滑動(dòng).
ViewConfiguration.get(getContent).getSealedTouchSlop()可以獲得這個(gè)系統(tǒng)值默認(rèn)8dp.
用途: 在自定義的時(shí)候, 可以參考系統(tǒng)的默認(rèn)值, 來作為實(shí)際的滑動(dòng)定義.
VelocityTracker GestureDetector
VelocityTracker 速度追蹤
用于追蹤手指在滑動(dòng)過程中的速度,包括水平和數(shù)值方向的速度
使用方式: 在View的OnTouchEvent方法中:
//獲得速度追蹤對(duì)象
VelocityTracker velocity = VelocityTracker.obtain();
velocity.addMovement(event);
//計(jì)算速度 并獲取計(jì)算值
velocity.computeCurrentVelocity(1000); //設(shè)定一個(gè)時(shí)間間隔值
float xVelocity = velocity.getXVelocity();
float yVelocity = velocity.getYVelocity();
必須要先計(jì)算并設(shè)定計(jì)算速度的時(shí)間單元值,才可以獲得速率.
公式: 速度 = (終點(diǎn)位置 - 起點(diǎn)位置) / 時(shí)間間隔值
可以看到, 計(jì)算的速度是根據(jù)我們自己添加的時(shí)間間隔值計(jì)算的. 并且速度可以為負(fù)值,如果向左滑動(dòng).
當(dāng)不需要的時(shí)候, 調(diào)用clear()重置并回收內(nèi)存.
velocity.clear();
velocity.recycle();
GestureDetector 手勢(shì)檢測(cè)
用于輔助檢測(cè)用戶的單擊, 滑動(dòng), 長(zhǎng)按, 雙擊等行為.
使用如下
創(chuàng)建GestureDetector對(duì)象并實(shí)現(xiàn)OnGestureDetector接口.
GestureDetector mGestureDetector = new GestureDetector(this);
// 解決長(zhǎng)按屏幕后無法拖動(dòng)的現(xiàn)象
mGestureDetector.setIsLongpressEnabled(false);
然后接管目標(biāo)View的onTouchEvent()方法. 在onTouchEvent()方法中
boolean consume = mGestureDetector.onTouchEvent(event);
return consume;
然后根據(jù)需求可以選擇性的實(shí)現(xiàn)OnGestureListener和OnDoubleTapListener接口
接口的方法說明:
| 方法名 | 描述 | 所屬接口 |
|---|---|---|
onDown |
按下 | OnGestureListener |
onShowPress |
按下 但是未松開或者拖動(dòng).強(qiáng)調(diào)狀態(tài) | OnGestureListener |
onSingleTapUp |
抬起 表示單擊行為, 雙擊中也會(huì)觸發(fā) | OnGestureListener |
onScroll |
按下并拖動(dòng) 拖動(dòng)行為 | OnGestureListener |
onLongPress |
長(zhǎng)按 | OnGestureListener |
onFling |
按下屏幕病快速滑動(dòng)后松開 | OnGestureListener |
onDoubleTap |
雙擊,兩次連續(xù)單擊組成, 與onSingleTapConfirmed無法共存 | OnDoubleTapListener |
onSingleTapConfirmed |
嚴(yán)格意義上的單擊 雙擊中的單擊無法觸發(fā) | OnDoubleTapListener |
onDoubleTapEvent |
表示發(fā)生了行為 | OnDoubleTapListener |
實(shí)際開發(fā)中:根據(jù)喜好來使用. 即使不使用GestureDetector輔助手勢(shì)檢測(cè)類,一樣可以實(shí)現(xiàn).
建議: 如果要監(jiān)聽雙擊這種行為就是用此類.
Scroller 彈性滑動(dòng)對(duì)象
用于實(shí)現(xiàn)View的彈性滑動(dòng).
在開發(fā)中, 當(dāng)需要把View從一個(gè)點(diǎn)移動(dòng)到另一個(gè)點(diǎn)的時(shí)候. 如果使用scrollTo/scrollBy進(jìn)行滑動(dòng)時(shí), 都是瞬間完成. 沒有過度動(dòng)畫, 給用戶感覺很生硬. 使用Scroller 可以實(shí)現(xiàn)有過渡的滑動(dòng).Scroller本身無法讓View彈性滑動(dòng), 需要和View的computerScroll進(jìn)行配合使用.
下面會(huì)說到
View的滑動(dòng)
實(shí)現(xiàn)滑動(dòng)的方式有三種:
- 通過View本身的
scrollTo/scrollBy方法實(shí)現(xiàn)滑動(dòng) - 通過動(dòng)畫給View施加平移效果來實(shí)現(xiàn)動(dòng)畫
- 通過改變View的
LayoutParams使View重新布局實(shí)現(xiàn)滑動(dòng)
scrollTo/scrollBy
首先要明確一點(diǎn): 這兩個(gè)方法只能改變View的內(nèi)容位置,而不能改變View本身在布局中的位置
而且方法中都是以像素值來進(jìn)行移動(dòng)的.
- scrollTo: 針對(duì)當(dāng)前View的絕對(duì)位置進(jìn)行移動(dòng).
- scrollBy: 根據(jù)當(dāng)前View的內(nèi)容值進(jìn)行相對(duì)位置移動(dòng).
看一下scrollBy的源碼調(diào)用
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
其實(shí)本質(zhì)上scrollBy調(diào)用了scrollTo方法
而mScrollX/mScrollY是什么? 這個(gè)就是當(dāng)前View的內(nèi)容 與這個(gè)View實(shí)際布局位置(原始位置)的差值.
而當(dāng)前View內(nèi)容這個(gè)東西就是讓用戶看到的效果發(fā)生改變. 但是如果這個(gè)View可以被點(diǎn)擊. 那么能觸發(fā)點(diǎn)擊的位置是View的實(shí)際所在布局位置. 而不是View的內(nèi)容顯示的位置.
使用動(dòng)畫
使用動(dòng)畫來對(duì)View進(jìn)行移動(dòng),主要就是操作View的translationX/translationY屬性
可以使用普通動(dòng)畫和屬性動(dòng)畫.
普通動(dòng)畫是對(duì)View進(jìn)行影像的移動(dòng). 可以通過設(shè)置fillAfter=true,來讓影像在動(dòng)畫結(jié)束時(shí)候保留最終結(jié)果.而不是還原到起始位置.
而屬性動(dòng)畫會(huì)對(duì)真實(shí)位置也進(jìn)行改變.
ObjectAnimator.ofFloat(tagerView,"translationX",0,100).setDuration(100).start()
改變布局參數(shù)"
這個(gè)比較簡(jiǎn)單, 獲得View的LayoutParams參數(shù).進(jìn)行修改,改好之后再賦值回去.
MarginLayoutParams params = (MarginLayoutParams)mTextView.getLayoutParams();
params.width += 100;
params.leftMargin += 100;
mTextView.requestLayout();
//或者mTextview.setLayoutParams(params);
關(guān)于這三種方式的簡(jiǎn)單總結(jié)
- scrollTo/scrollBy: 操作簡(jiǎn)單, 適合對(duì)View內(nèi)容的滑動(dòng)
- 動(dòng)畫: 操作簡(jiǎn)單,主要適用于沒有交互的View和實(shí)現(xiàn)復(fù)雜的動(dòng)畫效果.
- 修改布局參數(shù): 操作稍微復(fù)雜,適用于有交互的View.
彈性滑動(dòng)
使用Scroller
一個(gè)簡(jiǎn)單的使用方法如下:
Scroller mScroller = new Scroller(mContent);
// 封裝一個(gè)方法, 接收要移動(dòng)到的目標(biāo)點(diǎn) x和y
private void smoothScrollTo(int destX, int destY){
int scrollX = getScrollX();
int deltaX = destX - scrollX;
// 1000ms內(nèi)逐漸滑向destX
mScroller.startScroll(scrollX, 0, deltaX, 0, 1000);
invalidate();
}
//復(fù)寫View的computeScroll方法
public void computeScroll(){
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate()
}
}
源碼中Scroller類中startScroll()方法,其實(shí)沒有實(shí)際操作什么,只是保存了調(diào)用方法時(shí),傳遞的幾個(gè)參數(shù). 如: 開始結(jié)束點(diǎn),時(shí)間等. 那動(dòng)畫究竟是怎么實(shí)現(xiàn)的? 復(fù)寫的computeScroll()又有什么用?
流程順序這樣的: 當(dāng)調(diào)用了startScroll()系統(tǒng)只是保存了一些信息, 但是下面調(diào)用invalidate(). 這個(gè)方法都知道是會(huì)導(dǎo)致View的重繪, 在View的draw()方法中又會(huì)去調(diào)用computeScroll()方法,本身computeScroll()是一個(gè)空實(shí)現(xiàn),但是這里進(jìn)行了復(fù)寫. 而這個(gè)方法我們復(fù)寫的時(shí)候調(diào)用了scrollTo()方法! ok這樣View就會(huì)真正的移動(dòng)了! 但是還有一點(diǎn)這次滾動(dòng)只是整個(gè)滾動(dòng)事件的一個(gè)小部分,后續(xù)的怎么觸發(fā)的? 就是下面又調(diào)用了postInvalidate(), 又會(huì)重新繪制重新調(diào)用computeScroll()這個(gè)復(fù)寫過的空實(shí)現(xiàn)方法.
而Scroller類中的computeScrollOffset()可以直接返回這個(gè)滾動(dòng)的動(dòng)作是否全部完成. 源碼實(shí)現(xiàn)思路就是根據(jù)時(shí)間的流逝的百分比來計(jì)算出當(dāng)前ScrollX和ScrollY的值.
// 核心代碼 x就是時(shí)間流逝的百分比
mCurrX = mStartX + Math.round(x * mDeltaX);
mCurrY = mStartY + Math.round(x * mDeltaY);
小結(jié)Scroller的工作原理:
Scroller本身不可以實(shí)現(xiàn)滑動(dòng), 需要和View的ComputeScroll()配合使用來完成彈性滑動(dòng). 通過不斷的在computeScroll()調(diào)用View的重繪方法. 每次繪制時(shí)候的當(dāng)前時(shí)間與開始時(shí)間的時(shí)間差與設(shè)定的執(zhí)行動(dòng)畫時(shí)間的百分比,算出每一次需要scroll到的坐標(biāo)點(diǎn), 然后通過調(diào)用scrollTo()來實(shí)現(xiàn)每一次的小滾動(dòng)效果. 通過一連串的滾動(dòng)達(dá)到了平滑的效果. 這就是Scroller工作機(jī)制. 完全實(shí)現(xiàn)了解耦操作. 這個(gè)過程沒有任何一處對(duì)View進(jìn)行引用,甚至連內(nèi)部計(jì)時(shí)器都沒有.
補(bǔ)充:
1、top、left、right、bottom的值,是在view的onLayout的時(shí)候確定。
2、scrollView只在繪制的時(shí)候onLayout,在滾動(dòng)的時(shí)候不會(huì)再次出發(fā)onLayout,所以對(duì)于子View的top、left、right、bottom是沒有影響的、
3、listView自身有回收機(jī)制,所以在滾動(dòng)的時(shí)候需要時(shí)刻去檢測(cè)item是否已經(jīng)滾動(dòng)出了屏幕,這樣就需要重新測(cè)量子view的位置,所以就直接影響了item的top、left、right、bottom。
通過動(dòng)畫
可以直接使用ObjectAnimator.ofFloat(tagerView,"translationX",0,100).setDuration(100).start()
也可以利用動(dòng)畫的特性, 實(shí)現(xiàn)與Scroller原理近似的方法.
final int startX = 100;
final int endX = 200;
ValueAnimator valueAnimator = ValueAnimator.ofInt(0, 1).setDuration(1000);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int offset = (int) animation.getAnimatedFraction();
mTextview.scrollTo(startX+offset, 0);
}
});
讓系統(tǒng)算出每個(gè)時(shí)間片我們需要移動(dòng)的距離, 并回調(diào)給我們.讓我們自己實(shí)現(xiàn). 如果是一組動(dòng)畫在相同的時(shí)間執(zhí)行的絕對(duì)值相同我們就可以在onAnimationUpdate()一起進(jìn)行調(diào)用.
使用延時(shí)策略
核心思想就是通過發(fā)送一些列延時(shí)消息從而達(dá)到一種漸進(jìn)的效果.
可以使用: Handler, View的postDelayed()方法, 或者線程的sleep()
參看文章
《Android 開發(fā)藝術(shù)探索》書集
《Android 開發(fā)藝術(shù)探索》 03-View的事件體系