Android Scroller分析

(1)Android view的直角座標系

需要注意的是,view的直角座標系和數學的直角座標系不同,view的x軸方向和數學的直接座標系一致,但是y軸方向卻相反。

(2)基本方法

在介紹Scroller之前,我們先來看一下view移動的基礎方法。

  • getScrollX()getScrollY()

獲取x軸方向和y軸方向偏移量,如view直角座標系所示,view的左上角在原點處,也就是說,初始狀態下 getScrollX()getScrollY()的值爲0,往左移動時偏移量x爲負數,往右移動時偏移量x爲正數,往上移動時偏移量y爲負數,往下移動時偏移量y爲正數。

  • scrollTo(x, y) (無滾動特效,所以叫移動)

view的內容移動到指定位置。

這裏需要注意的是,不是移動view,而是移動view的內容。

這個方法往往結合onTouchEvent一起使用,我們看以下代碼

public class TestView extends View {

    private float mLastX = 0;
    private float mLastY = 0;

    private Paint mPaint;

    public TestView(Context context) {
        super(context);
        init();
    }

    public TestView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init();
    }

    private void init(){
        mPaint = new Paint();
        mPaint.setTextSize(80);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        canvas.drawText("我是中國人!", 0, 100, mPaint);

    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        float x = event.getX();
        float y = event.getY();

        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                //標誌着第一個手指按下
                mLastX = x;//獲取按下時x座標值
                mLastY = y;//獲取按下時y座標值
                break;
            case MotionEvent.ACTION_MOVE:
                //按住一點手指開始移動
                float move_x = mLastX - x;//計算當前已經移動的x軸方向的距離
                float move_y = mLastY - y;//計算當前已經移動的y軸方向的距離
                float oldScollX = getScrollX();//計算之前已經偏移的x軸方向的距離
                float oldScollY  = getScrollY();//計算之前已經偏移的y軸方向的距離

                scrollTo((int) move_x, (int) move_y);

                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                //表示手勢被取消了,不再接受後續事件
                scrollTo(0, 0);
                break;

        }
        return true;
    }
}

我們自定義一個view,view僅僅繪製一個文本,這個文本就是view的內容,代碼的邏輯是:移動view,在移動的過程中view的內容也隨之移動,當結束移動時,view的內容位置恢復。

圖片效果:

  • scrollBy(x, y) (無滾動特效,所以叫移動)

我們先來看一下scrollTo,假如我們去掉觸摸時間的處理

public class TestView extends View {

    private Paint mPaint;

    public TestView(Context context) {
        super(context);
        init();
    }

    public TestView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init();
    }

    private void init(){
        mPaint = new Paint();
        mPaint.setTextSize(80);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        canvas.drawText("我是中國人!", 0, 100, mPaint);

    }
}

這樣的話文本的位置是:


現在我們使用代碼控制view內容的位置

    tv_text = findViewById(R.id.tv_text);
    tv_text.scrollTo(-100, -100);

移動之後的效果如下:

當我們多次使用scrollTo

    tv_text = findViewById(R.id.tv_text);
    tv_text.scrollTo(-100, -100);
    tv_text.scrollTo(-100, -100);
    tv_text.scrollTo(-100, -100);
    tv_text.scrollTo(-100, -100);
    tv_text.scrollTo(-100, -100);

效果如下:

我們發現不管我們使用多少次scrollTo,view的移動都是以最開始的位置開始的。

scrollBy(x, y)可以完美解決這個問題,我們看一下源碼就知道它的具體作用了:

/**
 * 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(x, y)使每次移動都是以當前位置開始。

    tv_text = findViewById(R.id.tv_text);
    tv_text.scrollBy(-100, -100);
    tv_text.scrollBy(-100, -100);
    tv_text.scrollBy(-100, -100);
    tv_text.scrollBy(-100, -100);
    tv_text.scrollBy(-100, -100);
    tv_text.scrollBy(-100, -100);
    tv_text.scrollBy(-100, -100);

效果如下:

(3)Scroller滑動輔助類的基本方法

Scroller本身不會去移動view,它只是一個移動計算輔助類,用於跟蹤控件滑動的軌跡,只相當於一個滾動軌跡記錄工具,最終還是通過View的scrollTo、scrollBy方法完成View的移動的。

  • getCurrX()

獲取mScroller當前水平滾動的位置

  • getCurrY

獲取mScroller當前豎直滾動的位置

  • getFinalX

獲取mScroller最終停止的水平位置

  • getFinalY

獲取mScroller最終停止的豎直位置

  • startScroll()

開始滾動動畫:
startX:滾動的x方向起始點
startY:滾動的y方向起始點
dx:x方向的偏移量
dy:y方向的偏移量
duration:滾動所消耗的時間,默認爲250毫秒

startScroll(int startX, int startY, int dx, int dy)
startScroll(int startX, int startY, int dx, int dy, int duration)
  • computeScrollOffset()

判斷滾動動畫是否結束:
true:滾動尚未完成
false:滾動已經完成

(4)基本代碼實現
public class TestView extends View {

    private float mDownX = 0;
    private float mDonwY = 0;
    private Paint mPaint;

    private Scroller mScroller;

    public TestView(Context context) {
        super(context);
        init(context);
    }

    public TestView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init(context);
    }

    private void init(Context mContext){
        mPaint = new Paint();
        mPaint.setTextSize(80);

        mScroller = new Scroller(mContext);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        canvas.drawText("我是中國人!", 0, 100, mPaint);

    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        float x = event.getX();
        float y = event.getY();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                //標誌着第一個手指按下
                mDownX = x;//獲取按下時x座標值
                mDonwY = y;//獲取按下時y座標值
                break;
            case MotionEvent.ACTION_MOVE:
                //按住一點手指開始移動
                float move_x = mDownX - x;//計算當前已經移動的x軸方向的距離
                float move_y = mDonwY - y;//計算當前已經移動的y軸方向的距離
                float oldScollX = getScrollX();//計算之前已經偏移的x軸方向的距離
                float oldScollY  = getScrollY();//計算之前已經偏移的y軸方向的距離

                //開始滾動動畫
                //第一個參數:x軸開始位置
                //第二個參數:y軸開始位置
                //第三個參數:x軸偏移量
                //第四個參數:y軸偏移量
                mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), (int) move_x, (int) move_y, 3000);

                invalidate();//目的是重繪view,是的執行computeScroll方法

                break;

        }
        return true;
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        if(mScroller.computeScrollOffset()){//判斷滾動是否完成,true說明滾動尚未完成,false說明滾動已經完成
            scrollTo(mScroller.getCurrX(),mScroller.getCurrY());//將view直接移動到當前滾動的位置
            invalidate();//觸發view重繪
        }
    }
}

效果如下:

看到這三秒鐘的滾動動畫了吧,默認情況下就是這個效果,默認情況下Scroller使用的插值器是ViscousFluidInterpolator,從字面意義上看是一個粘性流體插值器。

(5)構造方法
//默認插值器是ViscousFluidInterpolator
Scroller mScroller = new Scroller(mContext);

//指定一個插值器
Scroller mScroller = new Scroller(mContext, new AccelerateDecelerateInterpolator());

//指定一個插值器,第三個參數表示是否開啓“飛輪”效果,也就是多次滾動時速度疊加
Scroller mScroller = new Scroller(mContext, new AccelerateDecelerateInterpolator(), false);
(6)插值器

Scroller其實就是在scrollTo(x, y)scrollBy(x, y)的基礎上添加滾動效果,滾動效果是一個動畫,當我們new一個Scroller對象時,就已經指定了一個插值器,下面來說明一下各種插值器:

  • ViscousFluidInterpolator

這是一個默認插值器,當構造Scroller時,如果不傳遞插值器或者插值器爲null時,系統默認使用ViscousFluidInterpolator插值器。

  • AccelerateDecelerateInterpolator

在動畫開始與結束的時候速率改變比較慢,在中間的時候速率較快。

  • AccelerateInterpolator

在動畫開始的地方速率改變比較慢,然後開始加速。

  • AnticipateInterpolator

開始的時候向後然後向前甩。

如圖所示

  • AnticipateOvershootInterpolator

開始的時候向後然後向前甩一定值後返回最後的值。

如圖所示:

  • BounceInterpolator

反彈插值器。

如圖所示:


  • CycleInterpolator

動畫循環播放特定的次數,速率改變沿着正弦曲線。

  • DecelerateInterpolator

在動畫開始的地方快然後慢。

  • LinearInterpolator

以常量速率改變。

  • OvershootInterpolator

向前甩一定值後再回到原來位置.

  • PathInterpolator

路徑插值器,我們可以按照自己想要的軌跡滾動。

PathInterpolator(Path path)
PathInterpolator(float controlX, float controlY)
PathInterpolator(float controlX1, float controlY1, float controlX2, float controlY2)

如果學習Path使用的話,這篇博客是個不錯的選擇Android開發之Path詳解

  • FastOutLinearInInterpolator

MaterialDesign基於貝塞爾曲線的插補器效果:依次慢慢快。

  • FastOutSlowInInterpolator

基於貝塞爾曲線的插補器效果:依次慢快慢

  • LinearOutSlowInInterpolator

基於貝塞爾曲線的插補器效果:依次快慢慢

以上的插值器運用比較廣泛,在Scroller中設置一個插值器可以優化滾動的效果。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章