Android 關於Scroller、View的滑動的知識點梳理

概述

Scroller類是Android中專門用來處理滾動的類。比如,我們使用最多的ViewPager、ListView等控件,其內部都是通過Scroller類實現。我們知道實現一個View的滑動有很多種方法,下面列舉一下:

  • layout
  • offsetLeftAndRight()與offsetTopAndBottom()
  • layoutParams
  • scrollTo與scrollBy
  • scroller
  • 屬性動畫
  • ViewDragHelp

大概就是以上總結的這麼多,本篇文章重點是前5中方法的知識點,由於屬性動畫(Property Animation)和ViewDragHelp實際的知識點也比較多,將會單獨整理。


代碼實現

首先自定義一個簡單的View,在佈局中引用,代碼如下:

public class CustomView extends View {

    private Paint mPaint;
    private int lastX;
    private int lastY;

    public CustomView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);

        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(getResources().getColor(R.color.colorAccent));
        mPaint.setStyle(Paint.Style.FILL);
    }

    @Override
    public void draw(Canvas canvas) {
        super.draw(canvas);

        canvas.drawCircle(100, 100, 100, mPaint);
    }
    

    @Override
    public boolean onTouchEvent(MotionEvent event) {

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

        switch (event.getAction()) {

            case MotionEvent.ACTION_DOWN:

                lastX = x;
                lastY = y;
                break;

            case MotionEvent.ACTION_MOVE:

                int offsetX = x - lastX;
                int offsetY = y - lastY;
                
                break;
        }

        return true;
    }
}

在構造方法中,初始化Paint畫筆,對畫筆進行相關的設置,在onDraw()方法中畫了一個簡單的圓,最後重寫了onTouchEvent方法,然後定義了兩個成員變量lastX、lastY,用來記錄上個移動點座標,局部變量x、y用來記錄當前座標,offsetX、offsetY表示X軸Y軸偏移量。

關於在View中獲取座標信息可參考之前寫過的一篇文章Android座標系、View座標系以及獲取座標等知識點梳理

Layout方法實現

case MotionEvent.ACTION_MOVE:

     int offsetX = x - lastX;
     int offsetY = y - lastY;
     layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() +offsetY);
     break;

運行效果:

在這裏插入圖片描述

調用layout()方法,在手指一動的過程中據變化的座標值不斷地重繪根View。

offsetLeftAndRight()與offsetTopAndBottom()方法實現

case MotionEvent.ACTION_MOVE:

     int offsetX = x - lastX;
     int offsetY = y - lastY;
     offsetLeftAndRight(offsetX);
     offsetTopAndBottom(offsetY);
     break;

offsetLeftAndRight(offsetX):參數View在X軸偏移量。該方法只對View在X軸方向上進行移動
offsetTopAndBottom(offsetY):參數View在Y軸偏移量。該方法只對View在Y軸方向上進行移動

layoutParams方法實現

case MotionEvent.ACTION_MOVE:

     int offsetX = x - lastX;
     int offsetY = y - lastY;
     LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) getLayoutParams();
     params.leftMargin = getLeft() + offsetX;
     params.topMargin = getTop() + offsetY;
     setLayoutParams(params);
     break;

注:通過getLayoutParams獲取LayoutParams的時候,一定要根據View所在的父佈局類型來設置對應的類型,例如:父佈局若是LinearLayout,那麼就對應LinearLayout.LayoutParams,若RelativeLayout,那麼就對應RelativeLayout.LayoutParams。

上述方法實現起來問題也不大,但需要知道View的父佈局佈局類型,我們也可以利用ViewGroup.MarginLayoutParams來更加簡便的實現此功能,在這裏我們不需要關注父佈局的佈局類型。

case MotionEvent.ACTION_MOVE:

     int offsetX = x - lastX;
     int offsetY = y - lastY;
     ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) getLayoutParams();
     params.leftMargin = getLeft() + offsetX;
     params.topMargin = getTop() + offsetY;
     setLayoutParams(params);
     break;

scrollTo與scrollBy方法實現

相信大家對這兩個方法都不陌生,沒錯它們就是Android系統中向我們提供的用來改變View位置的方法。

scrollTo(int x,int y)
scrollBy(int dx,int dy)

這兩個方法到底有什麼區別呢?從字面意思也能猜出個大概。

  • scrollTo(int x,int y):將View移動到具體的座標(x,y)處。
  • scrollBy(int dx,int dy):在當前座標的基礎上,X軸增量dx,Y軸增量dy,需要注意的是這個增量分正負,正負僅代表方向,與數值大小無關,與高中學的向量差不多。

在LinearLayout標籤下,寫兩個TextView先感受一下,當點擊TextView自身的時候,第一個TextView調用scrollTo,第二個TextView調用scrollBy。

@Override
    public void onClick(View v) {
        switch (v.getId()) {
        
            case R.id.tv_scroll_to:
            
                mTvScrollTo.scrollTo(20, 20);
                break;

            case R.id.tv_scroll_by:

                mTvScrollBy.scrollBy(20, 20);
                break;
        }
    }

運行結果

在這裏插入圖片描述

這是什麼鬼?不是說好的View移動呢?童話裏都是騙人的,好了不扯淡了。
首先需要明白的,scrollTo(int x,int y)方法是移動到一個具體的座標(x,y),當移動到此點後,再調用此方法,就不會在起作用了。而scrollBy(int dx,int dy)函數每次在當前的基礎上,在X軸方向移動dx個像素,在Y軸方向上移動dy個像素。然後我們需要知道的事情是,對於單個的View,例如TextView、ImageView分別移動的是TextView的文本內容和ImageView的drawable對象。

總結:
對於單個View移動的是View內容,TextView內容文本內容、ImageView的內容是drawable對象
對於ViewGroup移動的也是內部的內容,這裏“內部的內容”指的是所有的子View

既然我們明白了我們不應該直接用View去調用scrollTo和scrollBy,而是用View所在的父佈局去調用,簡單的修改一下代碼:

@Override
    public void onClick(View v) {
        switch (v.getId()) {

            case R.id.tv_scroll_to:

                mLinearScroll.scrollTo(20, 20);
                break;

            case R.id.tv_scroll_by:

                mLinearScroll.scrollBy(20, 20);
                break;
        }
    }

效果如下:
在這裏插入圖片描述

上面的移動文本內容的問題已經解決了,再來解釋一下爲什麼沒有按照我們的意願去移動,而是相反方向去移動。

移動前:
在這裏插入圖片描述
移動後:
在這裏插入圖片描述
看到上面兩幅圖,我們想象一種場景來感受一下Android中視圖移動的知識。上面的底層背景(畫布)就相當於天空,而手機屏幕的可視區域就相當於我們眼睛透過望遠鏡看到的可視區域,我們能不能說我們沒看見的東西就是不存在的?只有看到的纔是存在的?因爲我們站在西安城牆上遙望港珠澳大橋,沒能看見港珠澳大橋,所以港珠澳大橋不存在,這明顯不對,畫布上左下角的按鈕雖然沒有在屏幕中,可是確實存在,在這裏我們移動的是手機屏幕而不是畫布,同樣我們不能移動天空來更新我們的可視區域,那麼我們也可以移動望遠鏡來更新可視區域的內容。
通過分析,我們調用mLinearScroll.scrollBy(20, 20),實際上是在不斷的移動手機,手機相對於TextView分別向X軸、Y軸正方向(向右、向下)移動20像素,TextView相對於手機向左、向上移動。兩者的相對運動方向是相反的。解決以上的問題很簡單,直接上代碼。

@Override
    public void onClick(View v) {
        switch (v.getId()) {

            case R.id.tv_scroll_to:

                mLinearScroll.scrollTo(-20, -20);
                break;

            case R.id.tv_scroll_by:

                mLinearScroll.scrollBy(-20, -20);
                break;
        }
    }

效果如下:
在這裏插入圖片描述
完美!!!終於解決了上面的所有問題。

一般在自定義中的寫法是:
((View)getParent()).scrollBy(-offsetX,-offsetY)

scroller實現

前面,我們雖然實現了View的移動,但是細心觀察的你有沒有發現,這個移動是不連續的、跳躍式的,爲解決這個問題就出現了Scroller這個類。Scroller類是Android中專門用來處理滾動的類,通過Scroller類可以實現平滑的移動效果。

實現原理:scrollTo和scrollBy的效果跳躍式的產生一段距離,那麼可以這樣去理解Scroller,它是先將這段距離細分至無限小再去移動,我們視覺上完全看不到“跳躍感”,因此看上去像是平滑移動效果,在這裏完全可以用極限思想去理解Scroller的實現原理。

一般情況下,使用Scroller有3個步驟:

  1. 初始化Scroller:mScroller=new Scroller(context);
  2. 重寫computeScroll()方法
  3. 開啓滑動startScroll()

開啓滑動有2個構造方法:

  1. public void startScroll(int staryX,int startY,int dx,int dy);
  2. public void startScroll(int staryX,int startY,int dx,int dy,int duration);

可以看出兩個方法的區別就是有無設置移動時間。

在這裏直接上代碼

//第一步:在自定義View兩個參數的構造方法中初始化Scroller
mScroller=new Scroller(context);

//第二步:重寫computeScroll()方法
    @Override
    public void computeScroll() {
        super.computeScroll();
        /*
         * computeScrollOffset方法源碼註釋:Call this when you want to know the new location.
         * If it returns true,the animation is not yet finished.
         */

        if (mScroller.computeScrollOffset()) {

            ((View) getParent()).scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            invalidate();
        }
    }

//第三步:開啓滑動過程
 public void startSmoothScroll() {
     mScroller.startScroll(0, 0, -400, -400, 3000);
     invalidate();
    }

效果如下
在這裏插入圖片描述
在Activity中調用開啓滑動過程方法,此處邏輯:
invalidate()—>onDraw()—>computeScroll()
首先開啓模擬滑動過程,然後調用invalidate(),強制重繪會調用onDraw()方法,在onDraw()方法中會調用computeScroll()方法,然後會在computeScroll()方法與onDraw()方法中形成了循環,直至computeScroll()方法中的mScroller.computeScrollOffset()返回值爲false,循環結束。重寫computeScroll()方法,並且在if語句中,就是不斷的獲取當前的滾動值,一段一段地去模擬出平滑移動的過程的邏輯。

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