View的滑動

本文內容參考于《android開發藝術探索》,作為分析總結。

1、一些基礎

1.1、理解View的位置參數

看下面這張圖:


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 為正,反之為負。

mScrollX /mScrollY
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方法并沒有執行移動,只是對數據進行了存儲:

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方法。主要看源碼的這一部分:

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等等。

END
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,327評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,996評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,316評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,406評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,128評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,524評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,576評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,759評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,310評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,065評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,249評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,821評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,479評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,909評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,140評論 1 290
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,984評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,228評論 2 375

推薦閱讀更多精彩內容

  • 目前移動設備流行,我們要在如此小的屏幕上盡可能給用戶展現更多的內容,就需要在應用上通過滑動來顯示和隱藏部分內容,V...
    shenhuniurou閱讀 980評論 2 1
  • 開發中,為了增加更多炫麗的效果,我們經常在應用中添加滑動效果,今天就來分析一下 View 中滑動效果的實現原理以及...
    任教主來也閱讀 3,025評論 0 14
  • View體系之View的滑動 本文原創,轉載請注明出處。歡迎關注我的 簡書 ,關注我的專題 Android Cla...
    MeloDev閱讀 2,129評論 1 13
  • 什么是View View 是 Android 中所有控件的基類。 View的位置參數 View 的位置由它的四個頂...
    acc8226閱讀 1,202評論 0 7
  • View的滑動 View的滑動實現基本有三種思路,一種是通過ScrollTo和ScrollBy,一種是通過動畫給V...
    Cooke_閱讀 1,426評論 0 0