什么是View
View 是 Android 中所有控件的基類。
View的位置參數(shù)
View 的位置由它的四個(gè)頂點(diǎn)來(lái)決定, 分別對(duì)應(yīng) View 的四個(gè)屬性:top, left, right, bottom, 其中top是左上角的縱坐標(biāo), left 是左上角的橫坐標(biāo), right是右下角的橫坐標(biāo), bottom是右下角的縱坐標(biāo). 需要注意的是, 這些坐標(biāo)都是相對(duì)于View的父容器來(lái)說(shuō)的,這是一種相對(duì)坐標(biāo).
那么如何得到 View 的這四個(gè)參數(shù)呢?也很間,在View的源碼中它們對(duì)應(yīng)于 mLeft ,mRight ,mTop ,mBottom 這四個(gè)成員變量,獲取方式如下:
Left = getLeft();
Right = getRight();
Top = getTop();
Bottom = getBottom();
我們很容易得出 View 的寬高和坐標(biāo)的關(guān)系:
width = right - left;
height = bottom -top;
從 Android3.0開(kāi)始,View 增加了額外的幾個(gè)參數(shù); x, y, translationX 和 translationY,其中 x 和 y 是View左上角的坐標(biāo),而 translationX 和 translationY是View左上角相對(duì)于父容器的偏移量。這幾個(gè)參數(shù)也是相對(duì)于父容器的坐標(biāo),并且translationX和 translationY的默認(rèn)值是0,和View的四個(gè)基本的位置參數(shù)一樣,View也為它們提供了get/set方法
幾個(gè)參數(shù)的換算關(guān)系如下所示:
x = left + translationX;
y = top + translationY;
需要注意的是,View在平移的過(guò)程中,top 和 left 表示的是原始左上角的位置信息,其值并不會(huì)發(fā)生變化,此時(shí)發(fā)生改變的是 x , y ,translationX , translationY 這四個(gè)參數(shù)。
MotionEvent 和 TouchSlop
MotionEvent
在手指接觸屏幕所產(chǎn)生的一系列事件中,典型的事件類型有以下幾中:
- ACTION_DOWN 手指剛接觸屏幕;
- ACTION_MOVE 手指在屏幕上移動(dòng);
- ACTION_UP 手指從屏幕上松開(kāi)的一瞬間;
正常情況下 ,一次手指觸摸屏幕的行為會(huì)觸發(fā)一系列點(diǎn)擊事件,考慮如下幾中情況:
- 點(diǎn)擊屏幕后離開(kāi)松開(kāi),事件序列為DOWN --->UP;
- 點(diǎn)擊屏幕滑動(dòng)一會(huì)兒在松開(kāi),事件序列為DOWN--->MOVE--->....--->MOVE --->UP。
上述三種情況是典型的事件序列,同時(shí)通過(guò)MotionEvent
對(duì)象我們可以得到點(diǎn)擊事件發(fā)生的x和y的坐標(biāo)。為此,系統(tǒng)提供了兩組方法: getX/ getY 和 getRawX/getRawY。
它們的區(qū)別其實(shí)很簡(jiǎn)單,getX 、getY 返回的是 相對(duì)于當(dāng)前View 左上角的x 和 y的坐標(biāo), 而getRawX 、getRawY 返回的是相對(duì)于手機(jī)屏幕左上角的 x 和 y 坐標(biāo)。
1.3.2 TouchSlop
TouchSlop是系統(tǒng)所能識(shí)別出的被認(rèn)為是滑動(dòng)的最小距離,換個(gè)說(shuō)法,當(dāng)手指在屏幕上滑動(dòng)時(shí),如果兩次滑動(dòng)之間的距離小于這個(gè) 常量,那么系統(tǒng)就不認(rèn)為你是在進(jìn)行滑動(dòng)操作。
原因間之:滑動(dòng)的距離太短,系統(tǒng)不認(rèn)為它是滑動(dòng)的。這是一個(gè)常量,和設(shè)備有關(guān),在不同設(shè)備上這個(gè)值可能是不同的,
通過(guò)如下方式即可獲取這個(gè)常量
ViewConfiguration.get(getContext()).getScaledTouchSlop();
這個(gè)常量有什么意義呢? 當(dāng)我們?cè)谔幚砘瑒?dòng)時(shí),可以利用這個(gè)常量來(lái)做一些過(guò)濾, 比如當(dāng)兩次滑動(dòng)事件的滑動(dòng)距離小于這個(gè)值,我們就可以認(rèn)為未達(dá)到滑動(dòng)距離的臨界值,因此就可以認(rèn)為它們不是滑動(dòng),這樣做可以有更好的用戶體驗(yàn)。
如果細(xì)看的話,可以在源碼中找到這個(gè)常量的定義,在frameworks/base/core/res/res/values/config.xml
文件中。
如下代碼所示:這個(gè)"config_viewConfigurationTouchSlop"對(duì)應(yīng)的就這個(gè)常量的定義。
<!-- Base "touch slop" value used by ViewConfiguration as a movement threshold where scrolling should begin .-->
<dimen name ="config_viewConfigurationTouchSlop">8dp</dimen>
1.4 VelocityTracker、GestureDetector 和 Scroller
1.4.1 VelocityTracker
用于追蹤手指在滑動(dòng)過(guò)程中的速度, 包括水平和豎直方向上的速度。使用它時(shí),首先在View的onTouchEvent方法中追蹤當(dāng)前點(diǎn)擊事件的速度:
VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);
獲得滑動(dòng)速度:
速度 = (終點(diǎn)位置 - 起點(diǎn)位置)/時(shí)間段
velocityTracker.computeCurrentVelocity(1000);//1000毫秒
int xVelocity = (int) velocityTracker.getXVelocity();
int yVelocity = (int) velocityTracker.getYVelocity();
最后,當(dāng)不需要使用速度追蹤的時(shí)候,調(diào)用clear方法來(lái)重置并回收:
velocityTracker.clear();
velocityTracker.recycle();
1.4.2 GestureDetector
用于對(duì)用戶手勢(shì)進(jìn)行檢測(cè),輔助檢測(cè)用戶的單擊、滑動(dòng)、長(zhǎng)按、雙擊等行為。
使用過(guò)程:創(chuàng)建一個(gè)GestureDetector對(duì)象并實(shí)現(xiàn)OnGestureListener接口,再根據(jù)需要實(shí)現(xiàn)其中的方法,對(duì)用戶的行為做出怎樣的反應(yīng)。接著,在View的onTouchEvent方法中做如下實(shí)現(xiàn):
boolean consume = mGestureDetector.onTouchEvent(event);
return consume;
OnGestureListener 和OnDoubleTapListener中的方法常用的有:onSingleTapUp(單擊)、onFling(快速滑動(dòng))、oScroll(拖動(dòng))、onLongPress(長(zhǎng)按)、onDoubleTap(雙擊)。
值得注意的是在實(shí)際開(kāi)發(fā)中,可以在View的onTouchEvent方法中實(shí)現(xiàn)所需的監(jiān)聽(tīng),如果只監(jiān)聽(tīng)滑動(dòng)相關(guān)的,可以在onTouchEvent中實(shí)現(xiàn),如果監(jiān)聽(tīng)雙擊的話,可以使用GestureDetector。
1.4.3 Scroller
彈性滑動(dòng)對(duì)象,用于實(shí)現(xiàn)View的彈性滑動(dòng)。View的scrollTo/scrollBy進(jìn)行滑動(dòng)是瞬間完成的。Scroller本身是無(wú)法讓View滑動(dòng)的,它需要和View的computeScroll
方法配合使用才能共同完成這個(gè)功能。
例:
Scroller scroller = 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(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
2 View的滑動(dòng)
2.1 使用View.scrollTo/scrollBy
/**
* Set the scrolled position of your view. This will cause a call to
* {@link #onScrollChanged(int, int, int, int)} and the view will be
* invalidated.
*
* @param x the x position to scroll to
* @param y the y position to scroll to
*/
public void scrollTo(int x, int y) {
if (mScrollX != x || mScrollY != y) {
int oldX = mScrollX;
int oldY = mScrollY;
mScrollX = x;
mScrollY = y;
invalidateParentCaches();
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (!awakenScrollBars()) {
postInvalidateOnAnimation();
}
}
}
/**
* Move the scrolled position of your view. This will cause a call to
* {@link #onScrollChanged(int, int, int, int)} and the view will be
* invalidated.
*
* @param x the amount of pixels to scroll by horizontally
* @param y the amount of pixels to scroll by vertically
*/
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
從上面的源碼可以看出,scrollBy實(shí)際上也是調(diào)用了scrollTo方法,它實(shí)現(xiàn)了基于當(dāng)前位置的相對(duì)滑動(dòng),而scrollTo則實(shí)現(xiàn)了基于所傳遞參數(shù)的絕對(duì)滑動(dòng),這個(gè)不難理解。利用scrollTo和scrollBy來(lái)實(shí)現(xiàn)View的滑動(dòng),這不是一件困難的事,但是我們要明白滑動(dòng)過(guò)程中View內(nèi)部的兩個(gè)屬性mScrollX和mScrollY的改變規(guī)則,這兩個(gè)屬性可以通過(guò)getScrollX和getScrollY方法分別得到。這里先簡(jiǎn)要概況一下:在滑動(dòng)過(guò)程中,mScrollX的值總是等于View左邊緣和View內(nèi)容左邊緣在水平方向的距離,而mScrollY的值總是等于View上邊緣和View內(nèi)容上邊緣在豎直方向的距離。View邊緣是指View的位置,由四個(gè)頂點(diǎn)組成,而View內(nèi)容邊緣是指View中的內(nèi)容的邊緣,scrollTo和scrollBy只能改變View內(nèi)容的位置而不能改變View在布局中的位置。mScrollX和mScrollY的單位為像素,并且當(dāng)View左邊緣在View內(nèi)容左邊緣的右邊時(shí),mScrollX為正值,反之為負(fù)值;當(dāng)View上邊緣在View內(nèi)容上邊緣的下邊時(shí),mScrollY為正值,反之為負(fù)值。換句話說(shuō),如果從左向右滑動(dòng),那么mScrollX為負(fù)值,反之為正值;如果從上往下滑動(dòng),那么mScrollY為負(fù)值,反之為正值。
大家在理解這個(gè)問(wèn)題的時(shí)候,不妨這樣想象手機(jī)屏幕是一個(gè)中空的蓋板,蓋板下面是一個(gè)巨大的畫布,也就是我們想要顯示的視圖。當(dāng)把這個(gè)蓋板蓋在畫布上的某一處時(shí),透過(guò)中間空的矩形,我們看見(jiàn)了手機(jī)屏幕上顯示的視圖,而畫布上其他地方的視圖,則被蓋板蓋住了無(wú)法看見(jiàn)。我們的視圖與這個(gè)例子非常類似,我們沒(méi)有看見(jiàn)視圖,并不代表它就不存在,有可能只是在屏幕外面而已。當(dāng)調(diào)用scrollBy方法時(shí),可以想象為外面的蓋板在移動(dòng)
如果在ViewGroup中使用scrollTo、scrollBy方法,那么移動(dòng)的將是所有子View,但如果在View中使用,那么移動(dòng)的將是View的內(nèi)容,例如TextView,content就是它的文本;ImageView,content就是它的drawable對(duì)象。
相信通過(guò)上面的分析,讀者朋友應(yīng)該知道為什么不能在View中使用這兩個(gè)方法來(lái)拖動(dòng)這個(gè)View了。那么我們就該View所在的ViewGroup中來(lái)使用scrollBy方法,移動(dòng)它的子View,代碼如下所示。
((View) getParent()).scrollBy(offsetX, offsetY);
2.2 使用動(dòng)畫
通過(guò)動(dòng)畫我們能夠讓一個(gè)View進(jìn)行平移,而平移就是一種滑動(dòng)。使用動(dòng)畫來(lái)移動(dòng)View,主要是操作View的translationX和translationY屬性,既可以采用傳統(tǒng)的View動(dòng)畫,也可以采用屬性動(dòng)畫,如果采用屬性動(dòng)畫的話,為了能夠兼容3.0以下的版本,需要采用開(kāi)源動(dòng)畫庫(kù)nineoldandroids( http://nineoldandroids.com/ )。
采用View動(dòng)畫的代碼,如下所示。此動(dòng)畫可以在1000ms內(nèi)將一個(gè)View從原始位置向右下角移動(dòng)200個(gè)像素。
ObjectAnimator.ofFloat(targetView, "translationX", 0, 200).setDuration(1000).start();
2.3 改變布局參數(shù)
本節(jié)將介紹第三種實(shí)現(xiàn)View滑動(dòng)的方法,那就是改變布局參數(shù),即改變LayoutParams。這個(gè)比較好理解了,比如我們想把一個(gè)Button向右平移100px,我們只需要將這個(gè)Button的LayoutParams里的marginLeft參數(shù)的值增加100px即可,是不是很簡(jiǎn)單呢?還有一種情形,為了達(dá)到移動(dòng)Button的目的,我們可以在Button的左邊放置一個(gè)空的View,這個(gè)空View的默認(rèn)寬度為0,當(dāng)我們需要向右移動(dòng)Button時(shí),只需要重新設(shè)置空View的寬度即可,當(dāng)空View的寬度增大時(shí)(假設(shè)Button的父容器是水平方向的LinearLayout),Button就自動(dòng)被擠向右邊,即實(shí)現(xiàn)了向右平移的效果。如何重新設(shè)置一個(gè)View的Layout-Params呢?很簡(jiǎn)單,如下所示。
MarginLayoutParams params = (MarginLayoutParams)mButton1.getLayoutParams();
params.width += 100;
params.leftMargin += 100;
mButton1.requestLayout(); //或者mButton1.setLayoutParams(params);
2.4 各種滑動(dòng)方式的對(duì)比
- scrollTo/scrollBy這種方式,它是View提供的原生方法,其作用是專門用于View的滑動(dòng),它可以比較方便地實(shí)現(xiàn)滑動(dòng)效果并且不影響內(nèi)部元素的單擊事件。但是它的缺點(diǎn)也是很顯然的:只能滑動(dòng)View的內(nèi)容,并不能滑動(dòng)View本身。
- 通過(guò)動(dòng)畫來(lái)實(shí)現(xiàn)View的滑動(dòng),這要分情況。如果是Android 3.0以上并采用屬性動(dòng)畫,那么采用這種方式?jīng)]有明顯的缺點(diǎn);如果是使用View動(dòng)畫或者在Android 3.0以下使用屬性動(dòng)畫,均不能改變View本身的屬性。在實(shí)際使用中,如果動(dòng)畫元素不需要響應(yīng)用戶的交互,那么使用動(dòng)畫來(lái)做滑動(dòng)是比較合適的,否則就不太適合。但是動(dòng)畫有一個(gè)很明顯的優(yōu)點(diǎn),那就是一些復(fù)雜的效果必須要通過(guò)動(dòng)畫才能實(shí)現(xiàn)。
- 下改變布局這種方式,它除了使用起來(lái)麻煩點(diǎn)以外,也沒(méi)有明顯的缺點(diǎn),它的主要適用對(duì)象是一些具有交互性的View,因?yàn)檫@些View需要和用戶交互,直接通過(guò)動(dòng)畫去實(shí)現(xiàn)會(huì)有問(wèn)題,這在2.2節(jié)中已經(jīng)有所介紹, 所以這個(gè)時(shí)候我們可以使用直接改變布局參數(shù)的方式去實(shí)現(xiàn)。
下面我們實(shí)現(xiàn)一個(gè)跟手滑動(dòng)的效果,這是一個(gè)自定義View,拖動(dòng)它可以讓它在整個(gè)屏幕上隨意滑動(dòng)。這個(gè)View實(shí)現(xiàn)起來(lái)很簡(jiǎn)單,我們只要重寫它的onTouchEvent方法并處理AC-TION_MOVE事件,根據(jù)兩次滑動(dòng)之間的距離就可以實(shí)現(xiàn)它的滑動(dòng)了。為了實(shí)現(xiàn)全屏滑動(dòng),我們采用方式二:動(dòng)畫的方式。原因很簡(jiǎn)單,這個(gè)效果無(wú)法采用scrollTo來(lái)實(shí)現(xiàn)。另外,它還可以采用 方式三: 改變布局 來(lái)實(shí)現(xiàn),這里僅僅是為了演示,所以就選擇了動(dòng)畫的方式,代碼如下。
//方式二: 動(dòng)畫的方式(改變了translationX, translationY屬性)
public class TestButton extends TextView {
// 分別記錄上次滑動(dòng)的坐標(biāo)
private int mLastX = 0;
private int mLastY = 0;
public TestButton(Context context) {
this(context, null);
}
public TestButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
public TestButton(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
final int x = (int) event.getRawX();
final int y = (int) event.getRawY();
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE: {
final int deltaX = x - mLastX;
final int deltaY = y - mLastY;
final int translationX = (int) (super.getTranslationX() + deltaX);
final int translationY = (int) (super.getTranslationY() + deltaY);
super.setTranslationX(translationX);
super.setTranslationY(translationY);
break;
}
default:
break;
}
mLastX = x;
mLastY = y;
return true;
}
}
//方式三: 更改布局的方式
case MotionEvent.ACTION_MOVE: {
final int deltaX = x - mLastX;
final int deltaY = y - mLastY;
//final int translationX = (int) (super.getTranslationX() + deltaX);
//final int translationY = (int) (super.getTranslationY() + deltaY);
//super.setTranslationX(translationX);
//super.setTranslationY(translationY);
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams)getLayoutParams();
params.leftMargin += deltaX;
params.topMargin += deltaY;
requestLayout();
break;
}
通過(guò)上述代碼可以看出,這一全屏滑動(dòng)的效果實(shí)現(xiàn)起來(lái)相當(dāng)簡(jiǎn)單。首先我們通過(guò)getRawX和getRawY方法來(lái)獲取手指當(dāng)前的坐標(biāo),注意不能使用getX和getY方法,因?yàn)檫@個(gè)是要全屏滑動(dòng)的,所以需要獲取當(dāng)前點(diǎn)擊事件在屏幕中的坐標(biāo)而不是相對(duì)于View本身的坐標(biāo);其次,我們要得到兩次滑動(dòng)之間的位移,有了這個(gè)位移就可以移動(dòng)當(dāng)前的View.
3. 彈性滑動(dòng)
實(shí)現(xiàn)彈性滑動(dòng)的方法有很多,但是它們都有一個(gè)共同思想:將一次大的滑動(dòng)分成若干次小的滑動(dòng)并在一個(gè)時(shí)間段內(nèi)完成,彈性滑動(dòng)的具體實(shí)現(xiàn)方式有很多,比如通過(guò)Scroller、Handler#postDelayed以及Thread#sleep等,下面一一進(jìn)行介紹。
3.1 使用Scroller
Scroller的使用方法在1.4.3節(jié)中已經(jīng)進(jìn)行了介紹,下面我們來(lái)分析一下它的源碼,從而探究為什么它能實(shí)現(xiàn)View的彈性滑動(dòng)。
當(dāng)我們構(gòu)造一個(gè)Scroller對(duì)象并且調(diào)用它的startScroll方法時(shí),Scroller內(nèi)部其實(shí)什么也沒(méi)做,它只是保存了我們傳遞的幾個(gè)參數(shù),這幾個(gè)參數(shù)從startScroll的原型上就可以看出來(lái),如下所示。
/**
* Start scrolling by providing a starting point, the distance to travel,
* and the duration of the scroll.
*
* @param startX Starting horizontal scroll offset in pixels. Positive
* numbers will scroll the content to the left.
* @param startY Starting vertical scroll offset in pixels. Positive numbers
* will scroll the content up.
* @param dx Horizontal distance to travel. Positive numbers will scroll the
* content to the left.
* @param dy Vertical distance to travel. Positive numbers will scroll the
* content up.
* @param duration Duration of the scroll in milliseconds.
*/
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
mMode = SCROLL_MODE;
mFinished = false;
mDuration = duration;
mStartTime = AnimationUtils.currentAnimationTimeMillis();
mStartX = startX;
mStartY = startY;
mFinalX = startX + dx;
mFinalY = startY + dy;
mDeltaX = dx;
mDeltaY = dy;
mDurationReciprocal = 1.0f / (float) mDuration;
}
可以看到,僅僅調(diào)用startScroll方法是無(wú)法讓View滑動(dòng)的,因?yàn)樗鼉?nèi)部并沒(méi)有做滑動(dòng)相關(guān)的事,那么Scroller到底是如何讓View彈性滑動(dòng)的呢?答案就是startScroll方法下面的invalidate方法,雖然有點(diǎn)不可思議,但是的確是這樣的。invalidate方法會(huì)導(dǎo)致View重繪,在View的draw方法中又會(huì)去調(diào)用computeScroll方法,computeScroll方法在View中是一個(gè)空實(shí)現(xiàn),因此需要我們自己去實(shí)現(xiàn),上面的代碼已經(jīng)實(shí)現(xiàn)了computeScroll方法。正是因?yàn)檫@個(gè)computeScroll方法,View才能實(shí)現(xiàn)彈性滑動(dòng)。這看起來(lái)還是很抽象,其實(shí)這樣的:當(dāng)View重繪后會(huì)在draw方法中調(diào)用computeScroll,而computeScroll又會(huì)去向Scroller獲取當(dāng)前的scrollX和scrollY;然后通過(guò)scrollTo方法實(shí)現(xiàn)滑動(dòng);接著又調(diào)用postInvalidate方法來(lái)進(jìn)行第二次重繪,這一次重繪的過(guò)程和第一次重繪一樣,還是會(huì)導(dǎo)致computeScroll方法被調(diào)用;然后繼續(xù)向Scroller獲取當(dāng)前的scrollX和scrollY,并通過(guò)scrollTo方法滑動(dòng)到新的位置,如此反復(fù),直到整個(gè)滑動(dòng)過(guò)程結(jié)束。
我們?cè)倏匆幌耂croller的computeScrollOffset方法的實(shí)現(xiàn),如下所示。
/**
* Call this when you want to know the new location. If it returns true,
* the animation is not yet finished.
*/
public boolean computeScrollOffset() {
...
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
if (timePassed < mDuration) {
switch (mMode) {
case SCROLL_MODE:
final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
mCurrX = mStartX + Math.round(x * mDeltaX);
mCurrY = mStartY + Math.round(x * mDeltaY);
break;
...
}
}
...
return true;
}
是不是突然就明白了?這個(gè)方法會(huì)根據(jù)時(shí)間的流逝來(lái)計(jì)算出當(dāng)前的scrollX和scrollY的值。計(jì)算方法也很簡(jiǎn)單,大意就是根據(jù)時(shí)間流逝的百分比來(lái)算出scrollX和scrollY改變的百分比并計(jì)算出當(dāng)前的值,這個(gè)過(guò)程類似于動(dòng)畫中的插值器的概念,這里我們先不去深究這個(gè)具體過(guò)程。這個(gè)方法的返回值也很重要,它返回true表示滑動(dòng)還未結(jié)束,false則表示滑動(dòng)已經(jīng)結(jié)束,因此當(dāng)這個(gè)方法返回true時(shí),我們要繼續(xù)進(jìn)行View的滑動(dòng)。
通過(guò)上面的分析,我們應(yīng)該明白Scroller的工作原理了,這里做一下概括:Scroller本身并不能實(shí)現(xiàn)View的滑動(dòng),它需要配合View的computeScroll方法才能完成彈性滑動(dòng)的效果,它不斷地讓View重繪,而每一次重繪距滑動(dòng)起始時(shí)間會(huì)有一個(gè)時(shí)間間隔,通過(guò)這個(gè)時(shí)間間隔Scroller就可以得出View當(dāng)前的滑動(dòng)位置,知道了滑動(dòng)位置就可以通過(guò)scrollTo方法來(lái)完成View的滑動(dòng)。就這樣,View的每一次重繪都會(huì)導(dǎo)致View進(jìn)行小幅度的滑動(dòng),而多次的小幅度滑動(dòng)就組成了彈性滑動(dòng),這就是Scroller的工作機(jī)制。由此可見(jiàn),Scroller的設(shè)計(jì)思想是多么值得稱贊,整個(gè)過(guò)程中它對(duì)View沒(méi)有絲毫的引用,甚至在它內(nèi)部連計(jì)時(shí)器都沒(méi)有。
3.2 通過(guò)(ValueAnimator屬性動(dòng)畫核心類)
我們可以利用動(dòng)畫的特性來(lái)實(shí)現(xiàn)一些動(dòng)畫不能實(shí)現(xiàn)的效果。還拿scrollTo來(lái)說(shuō),我們也想模仿Scroller來(lái)實(shí)現(xiàn)View的彈性滑動(dòng),那么利用動(dòng)畫的特性,我們可以采用如下方式來(lái)實(shí)現(xiàn):
final int startX = 0;
final int deltaX = 100;
ValueAnimator animator = ValueAnimator.ofInt(0,
deltaX).setDuration(1000);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animator) {
mButton1.scrollTo(startX + (int)animator.getAnimatedValue(), 0);
}
});
animator.start();
在上述代碼中,我們的動(dòng)畫本質(zhì)上沒(méi)有作用于任何對(duì)象上,它只是在1000ms內(nèi)完成了整個(gè)動(dòng)畫過(guò)程。利用這個(gè)特性,我們就可以在動(dòng)畫的每一幀到來(lái)時(shí)獲取動(dòng)畫完成的比例,然后再根據(jù)這個(gè)比例計(jì)算出當(dāng)前View所要滑動(dòng)的距離。注意,這里的滑動(dòng)針對(duì)的是View的內(nèi)容而非View本身。可以發(fā)現(xiàn),這個(gè)方法的思想其實(shí)和Scroller比較類似,都是通過(guò)改變一個(gè)百分比配合scrollTo方法來(lái)完成View的滑動(dòng)。需要說(shuō)明一點(diǎn),采用這種方法除了能夠完成彈性滑動(dòng)以外,還可以實(shí)現(xiàn)其他動(dòng)畫效果,我們完全可以在onAnimationUpdate方法中加上我們想要的其他操作。
3.3 使用延時(shí)策略
本節(jié)介紹另外一種實(shí)現(xiàn)彈性滑動(dòng)的方法,那就是延時(shí)策略。它的核心思想是通過(guò)發(fā)送一系列延時(shí)消息從而達(dá)到一種漸近式的效果,具體來(lái)說(shuō)可以使用Handler或View的postDelayed方法,也可以使用線程的sleep方法。對(duì)于postDelayed方法來(lái)說(shuō),我們可以通過(guò)它來(lái)延時(shí)發(fā)送一個(gè)消息,然后在消息中來(lái)進(jìn)行View的滑動(dòng),如果接連不斷地發(fā)送這種延時(shí)消息,那么就可以實(shí)現(xiàn)彈性滑動(dòng)的效果。對(duì)于sleep方法來(lái)說(shuō),通過(guò)在while循環(huán)中不斷地滑動(dòng)View和sleep,就可以實(shí)現(xiàn)彈性滑動(dòng)的效果。
下面采用Handler來(lái)做個(gè)示例,其他方法請(qǐng)讀者自行去嘗試,思想都是類似的。下面的代碼在大約1000ms內(nèi)將View的內(nèi)容向左移動(dòng)了100像素,代碼比較簡(jiǎn)單,就不再詳細(xì)介紹了。之所以說(shuō)大約1000ms,是因?yàn)椴捎眠@種方式無(wú)法精確地定時(shí),原因是系統(tǒng)的消息調(diào)度也是需要時(shí)間的,并且所需時(shí)間不定。
private static final int DELAYED_TIME = 33;
@SuppressLint("HandlerLeak")
private Handler mHandler = new Handler() {
private static final int FRAME_COUNT = 30;
private static final int DELAY_X = 100; //the x position to scroll to
private int mCount = 0;
public void handleMessage(Message msg) {
mCount++;
if (mCount <= FRAME_COUNT) {
float fraction = mCount / (float) FRAME_COUNT;
int scrollX = (int) (fraction * DELAY_X);
mButton1.scrollTo(scrollX, 0);
mHandler.sendEmptyMessageDelayed(0, DELAYED_TIME);
}
};
};
4 View的事件分發(fā)機(jī)制
本節(jié)將介紹View的一個(gè)核心知識(shí)點(diǎn):事件分發(fā)機(jī)制。事件分發(fā)機(jī)制不僅僅是核心知識(shí)點(diǎn)更是難點(diǎn),不少初學(xué)者甚至中級(jí)開(kāi)發(fā)者面對(duì)這個(gè)問(wèn)題時(shí)都會(huì)覺(jué)得困惑。另外,View的另一大難題滑動(dòng)沖突,它的解決方法的理論基礎(chǔ)就是事件分發(fā)機(jī)制,因此掌握好View的事件分發(fā)機(jī)制是十分重要的。本節(jié)將深入介紹View的事件分發(fā)機(jī)制,在4.1節(jié)會(huì)對(duì)事件分發(fā)機(jī)制進(jìn)行概括性地介紹,而在4.2節(jié)將結(jié)合系統(tǒng)源碼去進(jìn)一步分析事件分發(fā)機(jī)制。
4.1 點(diǎn)擊事件的傳遞規(guī)則
在介紹點(diǎn)擊事件的傳遞規(guī)則之前,首先我們要明白這里要分析的對(duì)象就是MotionEvent,即點(diǎn)擊事件,關(guān)于MotionEvent在3.1節(jié)中已經(jīng)進(jìn)行了介紹。所謂點(diǎn)擊事件的事件分發(fā),其實(shí)就是對(duì)MotionEvent事件的分發(fā)過(guò)程,即當(dāng)一個(gè)MotionEvent產(chǎn)生了以后,系統(tǒng)需要把這個(gè)事件傳遞給一個(gè)具體的View,而這個(gè)傳遞的過(guò)程就是分發(fā)過(guò)程。點(diǎn)擊事件的分發(fā)過(guò)程由三個(gè)很重要的方法來(lái)共同完成:dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent,下面我們先介紹一下這幾個(gè)方法。
public boolean dispatchTouchEvent(MotionEvent ev)
用來(lái)進(jìn)行事件的分發(fā)。如果事件能夠傳遞給當(dāng)前View,那么此方法一定會(huì)被調(diào)用,返回結(jié)果受當(dāng)前View的onTouchEvent和下級(jí)View的dispatchTouchEvent方法的影響,表示是否消耗當(dāng)前事件。
public boolean onInterceptTouchEvent(MotionEvent event)
在上述方法內(nèi)部調(diào)用,用來(lái)判斷是否攔截某個(gè)事件,如果當(dāng)前View攔截了某個(gè)事件,那么在同一個(gè)事件序列當(dāng)中,此方法不會(huì)被再次調(diào)用,返回結(jié)果表示是否攔截當(dāng)前事件。
public boolean onTouchEvent(MotionEvent event)
在dispatchTouchEvent方法中調(diào)用,用來(lái)處理點(diǎn)擊事件,返回結(jié)果表示是否消耗當(dāng)前事件,如果不消耗,則在同一個(gè)事件序列中,當(dāng)前View無(wú)法再次接收到事件。
上述三個(gè)方法到底有什么區(qū)別呢?它們是什么關(guān)系呢?其實(shí)它們的關(guān)系可以用如下偽代碼表示:
public boolean dispatchTouchEvent(MotionEvent ev){
boolean consume = false;
if(onInterceptTouchEvent(ev)){
consume = onTouchEvent(ev);
}else{
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
通過(guò)上面的偽代碼,我們也可以大致了解點(diǎn)擊事件的傳遞規(guī)則:對(duì)于一個(gè)根ViewGroup來(lái)說(shuō),點(diǎn)擊事件產(chǎn)生后,首先會(huì)傳遞給它,這時(shí)它的dispatchTouchEvent就會(huì)被調(diào)用,如果這個(gè)ViewGroup的onInterceptTouchEvent方法返回true就表示它要攔截當(dāng)前事件,接著事件就會(huì)交給這個(gè)ViewGroup處理,即它的onTouchEvent方法就會(huì)被調(diào)用;如果這個(gè)ViewGroup的onInterceptTouchEvent方法返回false就表示它不攔截當(dāng)前事件,這時(shí)當(dāng)前事件就會(huì)繼續(xù)傳遞給它的子元素,接著子元素的dispatchTouchEvent方法就會(huì)被調(diào)用,如此反復(fù)直到事件被最終處理。
當(dāng)一個(gè)點(diǎn)擊事件產(chǎn)生后,它的傳遞過(guò)程遵循如下順序:Activity -> Window -> View,即事件總是先傳遞給Activity,Activity再傳遞給Window,最后Window再傳遞給頂級(jí)View。頂級(jí)View接收到事件后,就會(huì)按照事件分發(fā)機(jī)制去分發(fā)事件。考慮一種情況,如果一個(gè)View的onTouchEvent返回false,那么它的父容器的onTouchEvent將會(huì)被調(diào)用,依此類推。如果所有的元素都不處理這個(gè)事件,那么這個(gè)事件將會(huì)最終傳遞給Activity處理,即Activity的onTouchEvent方法會(huì)被調(diào)用。
關(guān)于事件傳遞的機(jī)制,這里給出一些結(jié)論,根據(jù)這些結(jié)論可以更好地理解整個(gè)傳遞機(jī)制,如下所示。
- 同一個(gè)事件序列是指從手指接觸屏幕的那一刻起,到手指離開(kāi)屏幕的那一刻結(jié)束,在這個(gè)過(guò)程中所產(chǎn)生的一系列事件,這個(gè)事件序列以down事件開(kāi)始,中間含有數(shù)量不定的move事件,最終以u(píng)p事件結(jié)束。
- 正常情況下,一個(gè)事件序列只能被一個(gè)View攔截且消耗。這一條的原因可以參考(3),因?yàn)橐坏┮粋€(gè)元素?cái)r截了某此事件,那么同一個(gè)事件序列內(nèi)的所有事件都會(huì)直接交給它處理,因此同一個(gè)事件序列中的事件不能分別由兩個(gè)View同時(shí)處理,但是通過(guò)特殊手段可以做到,比如一個(gè)View將本該自己處理的事件通過(guò)onTouchEvent強(qiáng)行傳遞給其他View處理。
- 某個(gè)View一旦決定攔截,那么這一個(gè)事件序列都只能由它來(lái)處理(如果事件序列能夠傳遞給它的話),并且它的onInterceptTouchEvent不會(huì)再被調(diào)用。這條也很好理解,就是說(shuō)當(dāng)一個(gè)View決定攔截一個(gè)事件后,那么系統(tǒng)會(huì)把同一個(gè)事件序列內(nèi)的其他方法都直接交給它來(lái)處理,因此就不用再調(diào)用這個(gè)View的onInterceptTouchEvent去詢問(wèn)它是否要攔截了。
- 某個(gè)View一旦開(kāi)始處理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回了false),那么同一事件序列中的其他事件都不會(huì)再交給它來(lái)處理,并且事件將重新交由它的父元素去處理,即父元素的onTouchEvent會(huì)被調(diào)用。意思就是事件一旦交給一個(gè)View處理,那么它就必須消耗掉,否則同一事件序列中剩下的事件就不再交給它來(lái)處理了,這就好比上級(jí)交給程序員一件事,如果這件事沒(méi)有處理好,短期內(nèi)上級(jí)就不敢再把事情交給這個(gè)程序員做了,二者是類似的道理。
- 如果View不消耗除ACTION_DOWN以外的其他事件,那么這個(gè)點(diǎn)擊事件會(huì)消失,此時(shí)父元素的onTouchEvent并不會(huì)被調(diào)用,并且當(dāng)前View可以持續(xù)收到后續(xù)的事件,最終這些消失的點(diǎn)擊事件會(huì)傳遞給Activity處理。
- ViewGroup默認(rèn)不攔截任何事件。Android源碼中ViewGroup的onInterceptTouchEvent方法默認(rèn)返回false。
- View沒(méi)有onInterceptTouchEvent方法,一旦有點(diǎn)擊事件傳遞給它,那么它的onTouchEvent方法就會(huì)被調(diào)用。
- View的onTouchEvent默認(rèn)都會(huì)消耗事件(返回true),除非它是不可點(diǎn)擊的(clickable 和longClickable同時(shí)為false)。View的longClickable屬性默認(rèn)都為false,clickable屬性要分情況,比如Button的clickable屬性默認(rèn)為true,而TextView的clickable屬性默認(rèn)為false。9. View的enable屬性不影響onTouchEvent的默認(rèn)返回值。哪怕一個(gè)View是disable狀態(tài)的,只要它的clickable或者longClickable有一個(gè)為true,那么它的onTouchEvent就返回true。
- onClick會(huì)發(fā)生的前提是當(dāng)前View是可點(diǎn)擊的,并且它收到了down和up的事件。
- 事件傳遞過(guò)程是由外向內(nèi)的,即事件總是先傳遞給父元素,然后再由父元素分發(fā)給子View,通過(guò)requestDisallowInterceptTouchEvent方法可以在子元素中干預(yù)父元素的事件分發(fā)過(guò)程,但是ACTION_DOWN事件除外。
4.2 事件分發(fā)的源碼解(略)
5 View的滑動(dòng)沖突
本節(jié)是View體系的核心章節(jié),前面4節(jié)均是為本節(jié)服務(wù)的,通過(guò)本節(jié)的學(xué)習(xí),滑動(dòng)沖突將不再是個(gè)問(wèn)題。
5.1 常見(jiàn)的滑動(dòng)沖突場(chǎng)景
常見(jiàn)的滑動(dòng)沖突場(chǎng)景可以簡(jiǎn)單分為如下三種(詳情請(qǐng)參看圖3-4):
- 場(chǎng)景1——外部滑動(dòng)方向和內(nèi)部滑動(dòng)方向不一致;
- 場(chǎng)景2——外部滑動(dòng)方向和內(nèi)部滑動(dòng)方向一致;
- 場(chǎng)景3——上面兩種情況的嵌套。
先說(shuō)場(chǎng)景1,主要是將ViewPager和Fragment配合使用所組成的頁(yè)面滑動(dòng)效果,主流應(yīng)用幾乎都會(huì)使用這個(gè)效果。在這種效果中,可以通過(guò)左右滑動(dòng)來(lái)切換頁(yè)面,而每個(gè)頁(yè)面內(nèi)部往往又是一個(gè)ListView。本來(lái)這種情況下是有滑動(dòng)沖突的,但是ViewPager內(nèi)部處理了這種滑動(dòng)沖突,因此采用ViewPager時(shí)我們無(wú)須關(guān)注這個(gè)問(wèn)題,如果我們采用的不是ViewPager而是ScrollView等,那就必須手動(dòng)處理滑動(dòng)沖突了,否則造成的后果就是內(nèi)外兩層只能有一層能夠滑動(dòng),這是因?yàn)閮烧咧g的滑動(dòng)事件有沖突。
再說(shuō)場(chǎng)景2,這種情況就稍微復(fù)雜一些,當(dāng)內(nèi)外兩層都在同一個(gè)方向可以滑動(dòng)的時(shí)候,顯然存在邏輯問(wèn)題。因?yàn)楫?dāng)手指開(kāi)始滑動(dòng)的時(shí)候,系統(tǒng)無(wú)法知道用戶到底是想讓哪一層滑動(dòng),所以當(dāng)手指滑動(dòng)的時(shí)候就會(huì)出現(xiàn)問(wèn)題,要么只有一層能滑動(dòng),要么就是內(nèi)外兩層都滑動(dòng)得很卡頓。在實(shí)際的開(kāi)發(fā)中,這種場(chǎng)景主要是指內(nèi)外兩層同時(shí)能上下滑動(dòng)或者內(nèi)外兩層同時(shí)能左右滑動(dòng)。
最后說(shuō)下場(chǎng)景3,場(chǎng)景3是場(chǎng)景1和場(chǎng)景2兩種情況的嵌套,因此場(chǎng)景3的滑動(dòng)沖突看起來(lái)就更加復(fù)雜了。比如在許多應(yīng)用中會(huì)有這么一個(gè)效果:內(nèi)層有一個(gè)場(chǎng)景1中的滑動(dòng)效果,然后外層又有一個(gè)場(chǎng)景2中的滑動(dòng)效果。具體說(shuō)就是,外部有一個(gè)Slide-Menu效果,然后內(nèi)部有一個(gè)ViewPager,ViewPager的每一個(gè)頁(yè)面中又是一個(gè)ListView。雖然說(shuō)場(chǎng)景3的滑動(dòng)沖突看起來(lái)更復(fù)雜,但是它是幾個(gè)單一的滑動(dòng)沖突的疊加,因此只需要分別處理內(nèi)層和中層、中層和外層之間的滑動(dòng)沖突即可,而具體的處理方法其實(shí)是和場(chǎng)景1、場(chǎng)景2相同的。
從本質(zhì)上來(lái)說(shuō),這三種滑動(dòng)沖突場(chǎng)景的復(fù)雜度其實(shí)是相同的,因?yàn)樗鼈兊膮^(qū)別僅僅是滑動(dòng)策略的不同,至于解決滑動(dòng)沖突的方法,它們幾個(gè)是通用的.
5.2 滑動(dòng)沖突的處理規(guī)則
一般來(lái)說(shuō),不管滑動(dòng)沖突多么復(fù)雜,它都有既定的規(guī)則,根據(jù)這些規(guī)則我們就可以選擇合適的方法去處理。如圖5-1所示,對(duì)于場(chǎng)景1,它的處理規(guī)則是:當(dāng)用戶左右滑動(dòng)時(shí),需要讓外部的View攔截點(diǎn)擊事件,當(dāng)用戶上下滑動(dòng)時(shí),需要讓內(nèi)部View攔截點(diǎn)擊事件。這個(gè)時(shí)候我們就可以根據(jù)它們的特征來(lái)解決滑動(dòng)沖突,具體來(lái)說(shuō)是:根據(jù)滑動(dòng)是水平滑動(dòng)還是豎直滑動(dòng)來(lái)判斷到底由誰(shuí)來(lái)攔截事件,如圖5-2所示,
根據(jù)滑動(dòng)過(guò)程中兩個(gè)點(diǎn)之間的坐標(biāo)就可以得出到底是水平滑動(dòng)還是豎直滑動(dòng)。如何根據(jù)坐標(biāo)來(lái)得到滑動(dòng)的方向呢?這個(gè)很簡(jiǎn)單,有很多可以參考,比如可以依據(jù)滑動(dòng)路徑和水平方向所形成的夾角,也可以依據(jù)水平方向和豎直方向上的距離差來(lái)判斷,某些特殊時(shí)候還可以依據(jù)水平和豎直方向的速度差來(lái)做判斷。這里我們可以通過(guò)水平和豎直方向的距離差來(lái)判斷,比如豎直方向滑動(dòng)的距離大就判斷為豎直滑動(dòng),否則判斷為水平滑動(dòng)。根據(jù)這個(gè)規(guī)則就可以進(jìn)行下一步的解決方法制定了。對(duì)于場(chǎng)景2來(lái)說(shuō),比較特殊,它無(wú)法根據(jù)滑動(dòng)的角度、距離差以及速度差來(lái)做判斷,但是這個(gè)時(shí)候一般都能在業(yè)務(wù)上找到突破點(diǎn),比如業(yè)務(wù)上有規(guī)定:當(dāng)處于某種狀態(tài)時(shí)需要外部View響應(yīng)用戶的滑動(dòng),而處于另外一種狀態(tài)時(shí)則需要內(nèi)部View來(lái)響應(yīng)View的滑動(dòng),根據(jù)這種業(yè)務(wù)上的需求我們也能得出相應(yīng)的處理規(guī)則,有了處理規(guī)則同樣可以進(jìn)行下一步處理。這種場(chǎng)景通過(guò)文字描述可能比較抽象,在下一節(jié)會(huì)通過(guò)實(shí)際的例子來(lái)演示這種情況的解決方案,那時(shí)就容易理解了,這里先有這個(gè)概念即可。
對(duì)于場(chǎng)景3來(lái)說(shuō),它的滑動(dòng)規(guī)則就更復(fù)雜了,和場(chǎng)景2一樣,它也無(wú)法直接根據(jù)滑動(dòng)的角度、距離差以及速度差來(lái)做判斷,同樣還是只能從業(yè)務(wù)上找到突破點(diǎn),具體方法和場(chǎng)景2一樣,都是從業(yè)務(wù)的需求上得出相應(yīng)的處理規(guī)則,在下一節(jié)將會(huì)通過(guò)實(shí)際的例子來(lái)演示這種情況的解決方案。
5.3 滑動(dòng)沖突的解決方式
描述了三種典型的滑動(dòng)沖突場(chǎng)景,在本節(jié)將會(huì)一一分析各種場(chǎng)景并給出具體的解決方法。首先我們要分析第一種滑動(dòng)沖突場(chǎng)景,這也是最簡(jiǎn)單、最典型的一種滑動(dòng)沖突,因?yàn)樗幕瑒?dòng)規(guī)則比較簡(jiǎn)單,不管多復(fù)雜的滑動(dòng)沖突,它們之間的區(qū)別僅僅是滑動(dòng)規(guī)則不同而已。拋開(kāi)滑動(dòng)規(guī)則不說(shuō),我們需要找到一種不依賴具體的滑動(dòng)規(guī)則的通用的解決方法,在這里,我們就根據(jù)場(chǎng)景1的情況來(lái)得出通用的解決方案,然后場(chǎng)景2和場(chǎng)景3我們只需要修改有關(guān)滑動(dòng)規(guī)則的邏輯即可。
1.外部攔截法
所謂外部攔截法是指點(diǎn)擊事情都先經(jīng)過(guò)父容器的攔截處理,如果父容器需要此事件就攔截,如果不需要此事件就不攔截,這樣就可以解決滑動(dòng)沖突的問(wèn)題,這種方法比較符合點(diǎn)擊事件的分發(fā)機(jī)制。外部攔截法需要重寫父容器ViewGroup的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_MOVE:{
if(父容器需要當(dāng)前點(diǎn)擊事件){
intercepted = true;
}
break;
}
default:
break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
上述代碼是外部攔截法的典型邏輯,針對(duì)不同的滑動(dòng)沖突,只需要修改父容器需要當(dāng)前點(diǎn)擊事件這個(gè)條件即可,其他均不需做修改并且也不能修改。這里對(duì)上述代碼再描述一下,在onInterceptTouchEvent方法中,首先是ACTION_DOWN這個(gè)事件,父容器必須返回false,即不攔截ACTION_DOWN事件,這是因?yàn)橐坏└溉萜鲾r截了ACTION_DOWN,那么后續(xù)的AC-TION_MOVE和ACTION_UP事件都會(huì)直接交由父容器處理,這個(gè)時(shí)候事件沒(méi)法再傳遞給子元素了;其次是ACTION_MOVE事件,這個(gè)事件可以根據(jù)需要來(lái)決定是否攔截,如果父容器需要攔截就返回true,否則返回false;最后是ACTION_UP事件,這里必須要返回false,因?yàn)锳CTION_UP事件本身沒(méi)有太多意義。
上述代碼是外部攔截法的典型邏輯,針對(duì)不同的滑動(dòng)沖突,只需要修改父容器需要當(dāng)前點(diǎn)擊事件這個(gè)條件即可,其他均不需做修改并且也不能修改。這里對(duì)上述代碼再描述一下,在onIn-terceptTouchEvent方法中,首先是ACTION_DOWN這個(gè)事件,父容器必須返回false,即不攔截ACTION_DOWN事件,這是因?yàn)橐坏└溉萜鲾r截了ACTION_DOWN,那么后續(xù)的AC-TION_MOVE和ACTION_UP事件都會(huì)直接交由父容器處理,這個(gè)時(shí)候事件沒(méi)法再傳遞給子元素了;其次是ACTION_MOVE事件,這個(gè)事件可以根據(jù)需要來(lái)決定是否攔截,如果父容器需要攔截就返回true,否則返回false;最后是ACTION_UP事件,這里必須要返回false,因?yàn)锳CTION_UP事件本身沒(méi)有太多意義。
2.內(nèi)部攔截法
內(nèi)部攔截法是指父容器不攔截任何事件,所有的事件都傳遞給子元素,如果子元素需要此事件就直接消耗掉,否則就交由父容器進(jìn)行處理,這種方法和Android中的事件分發(fā)機(jī)制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作,使用起來(lái)較外部攔截法稍顯復(fù)雜。它的偽代碼如下,我們需要重寫子元素的dispatchTouchEvent方法:
public boolean dispathTouchEvent(MotionEvent event){
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:{
parent.requestDisallowInterceptTouchEvent(true);
break;
}
case MotionEvent.ACTION_MOVE:{
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if(父容器需要此類點(diǎn)擊事件){
parent.requestDisallowInterceptTouchEvent(false)
}
break;
}
case MotionEvent.ACTION_UP:{
break;
}
default:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
除了子元素需要做處理以外,父元素也要默認(rèn)攔截除了ACTION_DOWN以外的其他事件,這樣當(dāng)子元素調(diào)用parent.requestDisallowInterceptTouchEvent(false)方法時(shí),父元素才能繼續(xù)攔截所需的事件。
為什么父容器不能攔截ACTION_DOWN事件呢?那是因?yàn)锳CTION_DOWN事件并不受FLAG_DISALLOW_INTER-CEPT這個(gè)標(biāo)記位的控制,所以一旦父容器攔截ACTION_DOWN事件,那么所有的事件都無(wú)法傳遞到子元素中去,這樣內(nèi)部攔截就無(wú)法起作用了。父元素所做的修改如下所示。
public boolean onInterceptTouchEvent(MotionEvent event){
if(event.getAction() == MotionEvent.ACTION_DOWN){
return false;
}else {
return true;
}
}
因?yàn)閮?nèi)部攔截法沒(méi)有外部攔截法簡(jiǎn)單易用,所以推薦采用外部攔截法來(lái)解決常見(jiàn)的滑動(dòng)沖突.
參考書目