本文內容參考于《android開發藝術探索》,作為分析總結。
1、一些基礎
1.1、理解View的位置參數
看下面這張圖:
那么:
width = right(getRight) - left(getLeft)
height = bottom(getBottom) - top(getTop)
比較注意的是:activity的生命周期中view是在繪制狀態,所以getRight、getWidth...這些方法獲取的是0,具體解決辦法可自行研究。而且這幾個參數都是相對于父容器的。
從Android3.0開始,View增加了x、y、translationX、translationY。其中x和y是View左上角的坐標,translationX和translationY是View左上角相對于父容器的偏移量,默認值為0。這幾個參數也是相對于父容器。那么:
x=left+translationX;
y=top+translationY;
需要注意的是,View平移過程中,top、left表示的是原始左上角的位置信息,其值并不會發生改變,此時發生改變的是x、y、translationX、translationY。
1.2、MotionEvent和TouchSlop
1. MotionEvent
典型的幾種事件類型:
- ACTION_DOWN--剛接觸屏幕
- ACTION_MOVE--屏幕上移動
- ACTION_UP--屏幕上松開的一瞬間
典型的一系列事件:
- 點擊屏幕后離開松開,事件序列:DOWN->UP
- 點擊屏幕滑動一會再松開,事件序列:DOWN->MOVE->...->MOVE->UP
在序列事件中可以通過MotionEvent對象得到點擊事件發生的x和y坐標。例如:
event.getX();
event.getRawX();
這和View的getX是不同的,再者就是event.getX()獲取的是相當于當前View左上角的x坐標,event.getRawX()返回的是相對于手機屏幕左上角的x坐標。
2. TouchSlop
TouchSlop是系統所能夠識別出的被認為是滑動的最小距離,換句話說,當手指在屏幕上滑動時,如果兩次滑動之間的距離小于這個常量,那么系統就不認為你是在進行滑動操作。獲取這個常量方式:
ViewConfiguration.get(this).getScaledDoubleTapSlop();
1.3、VelocityTracker、GestureDetector和Scroller
1. VelocityTracker
速度追蹤,用于追蹤手指在滑動工程中的速度,包括水平和豎直方向的速度(有正負之分,即速度是有方向的):
private VelocityTracker mVelocityTracker = null;
@Override
public boolean onTouchEvent(MotionEvent event) {
int index = event.getActionIndex();
int pointerId = event.getPointerId(index);
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
if (mVelocityTracker == null) {
//獲取VelocityTracker對象
mVelocityTracker = VelocityTracker.obtain();
} else {
mVelocityTracker.clear();//清除加入的event
}
//加入event
mVelocityTracker.addMovement(event);
break;
case MotionEvent.ACTION_MOVE:
mVelocityTracker.addMovement(event);
//1000代表是1s,即單位時間
mVelocityTracker.computeCurrentVelocity(1000);
//獲取x、y速度
Loger.e("ACTION_MOVE:" + VelocityTrackerCompat.getXVelocity(mVelocityTracker,
pointerId) + "-" + VelocityTrackerCompat.getYVelocity(mVelocityTracker,
pointerId));
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
//回收
mVelocityTracker.recycle();
break;
}
return true;
}
使用較為簡單,不復雜。
2. GestureDetector
手勢檢測,用于輔助檢測用戶的單擊、滑動、長按、雙擊等行為。使用較為簡單,可參考官方文檔:https://developer.android.com/training/gestures/detector.html
這里說幾個比較常用的:
- onSingleTapUp,單擊
- onFiling,快速滑動
- onScroll,拖動
- onLongPress,長按
- onDoubleTap,雙擊
但是實際開發中,GestureDetector并非必須用,可以自己在View的OnTouchEvent方法中實現所需的監聽。比如監聽滑動相關的,建議在onTouchEvent中實現,像雙擊這種可以使用GestureDetector。
3. Scroller
彈性滑動對象,用于實現View的彈性滑動。View的scrollTo/scrollBy滑動時瞬間完成,效果不好。所就要用Scroller來實現有過度效果的滑動。Scroller本身無法讓View彈性滑動,它需要和View的computeScroll方法配合使用才能共同完成這個功能。下面是一個通用模板:
private Scroller mScroller = new Scroller(mContext);
private void scroll(int x, int y) {
int scrollX = getScrollX();
int delta = x - scrollX;
//1000ms的移動
mScroller.startScroll(scrollX, 0, delta, 0, 1000);
invalidate();
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
2、View的滑動
一般View的滑動有下面三種方式:通過View本身提供的scrollTo/scrollBy方法,通過動畫給View施加平移效果來實現滑動,通過改變View的LayoutParams使得View重新布局從而實現滑動。
2.1、使用scrollTo/scrollBy
首先需要名曲的是,scrollTo/scrollBy滑動是對view的內容進行移動,而非view本身。比如對于ViewGroup來講,里面的組件都歸宿于他的內容。
下面是scrollTo/scrollBy源碼:
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();
}
}
}
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
源碼中可以看到scrollBy內部調用的也是scrollTo方法。里面有mScrollX 和mScrollY 參數,這兩個參數的意思是view的邊和view的內容的距離。mScrollX 值總是等于view左邊緣和view內容左邊緣在水平方向的距離,mScrollY的值總是等于View上邊緣和View內容邊緣在豎直的距離。這個值是有正負之分的,比如view左邊緣在view內容左邊緣右邊時,那么mScrollX 為正,反之為負。
2.2、使用動畫
使用動畫主要是操作View的translationX和translationY屬性。可以使用傳統View動畫,也可以采用屬性動畫(支持3.0以上,3.0以下采用開源動畫庫nineoldandroids:
http://nineoldandroids.com/)
比如采用屬性動畫完成1s內從原始位置向右平移100像素:
ObjectAnimator.ofFloat(mBtn, "translationX", 0, 100)
.setDuration(1000)
.start();
建議使用屬性動畫,動畫有些bug,比如移動后不能點擊,屬性動畫沒有這個問題。
2.3、改變布局參數
這個就很簡單了。比如,一個button要右移,改變LayoutParams里的marginLeft即可,或者左邊放一個空的view也可以。
2.4、各種滑動方式對比
- scrollTo/scrollBy,內容滑動,簡單
- 動畫,復雜的動畫效果,簡單
- 改變布局參數,復雜
3、彈性滑動
彈性滑動原理是吧一次大的滑動分成若干次小的滑動并在一個時間內完成。實現方式比如:通過Scroller、Handler和postDelayed、Thread和sleep等。
3.1、使用Scroller
首先要注意的是,Scroller的滑動時內容的滑動。在1.3的Scroller介紹中有一個典型使用:
private Scroller mScroller = new Scroller(mContext);
private void scroll(int x, int y) {
int scrollX = getScrollX();
int delta = x - scrollX;
//1000ms的移動
mScroller.startScroll(scrollX, 0, delta, 0, 1000);
invalidate();
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
這里解釋下為什么能夠實現彈性滑動。Scroller調用startSroll開始移動,但是實際上startScroll方法并沒有執行移動,只是對數據進行了存儲:
傳入的這幾個參數,startX和startY表示滑動的起點,dx和dy表示的是要滑動的距離,而duration表示是滑動時間。這里的滑動是指View內容的滑動而非View本身位置的改變。再者就是能夠滑動的原因是因為:invalidate這個方法,這個方法調用會導致View重繪,重繪調用view的draw方法,draw方法又會調用computeScroll方法。這時候因為重新了computeScroll方法,方法中又會去向scroller獲取當前的scrollX和scrollY,然后通過scrollTo方法實現滑動;接著又調用postInvalidate方法進行下次重繪;然后又走了上面的流程。invalidate和postInvalidate放的區別在于前者是UI線程中調用,后者是非線程中調用。
再來說下scroller的computeScrollOffset方法。主要看源碼的這一部分:
這個方法的意思主要是采用時間流逝計算當前的scrollX和scrollY。這樣就可以做到彈性滑動。
3.2、通過動畫
動畫本身就是漸進過程,所以天然彈性效果。比如下面這個代碼:
ObjectAnimator.ofFloat(view, "translationX", 0, 100)
.setDuration(1000)
.start();
這個可以讓一個View在100s內向右移動100像素。
那么如果想模仿Scroller來實現View的彈性滑動呢?這里也可以利用動畫的特性來實現:
final int startX = 0;
final int deltaX = 100;
ValueAnimator animator = ValueAnimator.ofInt(0, 1).setDuration(100);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float fraction = animation.getAnimatedFraction();
mBtn.scrollTo(startX + (int) (deltaX * fraction), 0);
}
});
其實你會發現,這個代碼本質上并沒有作用于任何對象。這里是根據動畫完成的漸變獲取View所要滑動的距離。實際上,這里的滑動針對的還是View的內容而非View本身。
3.3、使用延時策略
延時策略的核心是發送一系列延時消息,比如Handler、View的postDelayed、線程的sleep方法。Handler主要是通過調用sendEmptyMessageDelayed方法實現,一直這樣調用本身即可。對于postDelayed方法,可以通過其來發送一個延時消息,然后再消息中進行View的滑動,如果連續不斷地發送這種延時消息,那么就可以實現彈性滑動的效果。對于sleep方法來說,通過在while循環中不斷地滑動View和sleep。
代碼這里就不在貼出,原理和上面兩種方式相同。
其實這三種方法的核心思想都是把一個完整的移動分割成一個一個小的移動,這樣的話就需要借助有時間流的工具,比如computeScroll、ValueAnimator、Handler等等。