安卓手勢觸摸處理(touch和scroll)

手勢滑動之玩轉onTouchEvent()與Scroller

版權聲明:本文轉自嚴振杰的博客:http://blog.yanzhenjie.com


看到好的文章就忍不住轉載 = =,當然還是想收藏了,可是csdn有沒有收藏功能,只好自己動手收藏啦。安卓觸摸手勢覺得十分重要,根據手勢觸摸可以配合寫出滿意的動畫,給用戶一種我在與機器進行交互的感覺。當然觸摸事件的分發機制也必懂啦,這個就不說啥了。不廢話了。趕緊收藏乾貨準備過冬。



智慧是對一切事物產生這些事物的原因的領悟。

——— 西塞羅

智慧是人類創造的源泉啊,有時候就在感慨爲什麼人類這麼強大,能創造出來智能手機操作系統。哈哈,題外話了。研究系統不現實,還是潛心研究智能系統軟件吧。智慧。

10月份工作太忙只寫了一篇博客,這個月多補幾篇吧。昨天和我一個超級要好的朋友聊起自定義view和手勢滑動,正好羣裏好多小夥伴總是問關於onTouchEvent()與Scroller的處理,所以就正好寫一篇這樣的博客,希望可以幫到需要的朋友。

今天的效果非常非常的簡單,所以只能說是入門級,重在理解其中的精髓,今天主要講兩個東西,一個是View#onTouchEvent(MotionEvent)方法,另一個是Scroller類,一般涉及到手勢操作的都離不開它倆。

下面先來預覽一下效果,源碼在文章末尾。

效果預覽

彈性效果 仿ViewPager彈性翻頁

原理分析與知識普及

不講道理的說,我們不是要做這兩個才分析,而是因爲分析了View#onTouchEvent(MotionEvent)Scroller才做出的這兩個,所以且聽我細細道來。

scrollTo(int, int)與scrollBy(int, int)

我們要發生滾動就的知道View的兩個方法:View#scrollTo(int, int)View#scrollBy(int, int),這兩個方法都是讓View來發生滾動的,他們有什麼區別呢?

  • View#scrollTo(int, int) 
    Viewcontent滾動到相對View初始位置的(x, y)處。

  • View#scrollBy(int, int) 
    Viewcontent滾動到相對於View當前位置的(x, y)處。

不知道你理解了木有?什麼,還沒理解?好那我們來一個sample,先來看看佈局:

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/content_scroll_method"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center">

    <Button        android:id="@+id/btn_scroll_to"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="scrollTo(int,int)" />
    <Button        android:id="@+id/btn_scroll_by"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="scrollBy(int,int)" /></LinearLayout>1234567891011121314151617181912345678910111213141516171819

這是Java代碼:

ViewGroup mContentRoot;@Overrideprotected void onCreate(Bundle savedInstanceState) {    super.onCreate(savedInstanceState);
    ...

    mContentRoot = (ViewGroup) findViewById(R.id.content_scroll_method);
    findViewById(R.id.btn_scroll_to).setOnClickListener(this);
    findViewById(R.id.btn_scroll_by).setOnClickListener(this);
}@Overrideprivate void onClick(View v) {    int id = v.getId();    switch (id) {        case R.id.btn_scroll_to: {
            mContentRoot.scrollTo(100, 100);            break;
        }        case R.id.btn_scroll_by: {
            mContentRoot.scrollBy(10, 20);            break;
        }
    }
}12345678910111213141516171819202122232425261234567891011121314151617181920212223242526

這個很好理解了,點擊scrollTo()按鈕的時候調用LayoutscrollTo(int, int)放,讓Layoutcontent滾動到相對Layout初始位置的(100, 100)處;點擊scrooBy()按鈕的時候調用LayoutscrollBy(int, int)Layoutcontent滾動到相對Layout當前位置的(10, 20)處,來看看效果吧:

scrooTo()與scrooBy()

我們發現點擊scrollTo()按鈕的時候,滾動了一下,然後再點就不動了,因爲此時Layoutcontent已經滾動到相對於它初始位置的(100,100)處了,所以再點它還是到這裏,所以再次點擊就看起來不動了。

點擊scrollBy()按鈕的時候,發現Layoutcontent一直有在滾動,是因爲無論何時,content的相對位置與當前位置都是不同的,所以它總是會去到一個新的位置,所以再次點擊會一直滾動。

注意:這裏我們也發現scrollTo(int, int)scrollBy(int, int)傳入的值都是正數,經過我實驗得出,x傳入正數則向左移動,傳入負數則向右移動;y傳入正數則向上移動,傳入負數則向下移動,且這個xy的值是像素。這裏和Android座標系是相反的,不日我將新開一篇博客來專門講這個問題。

我們理解了View#scrollTo(int, int)View#scrollBy(int, int)後結合View#onTouchEvent(MotionEvent)就可以做很多事了。

View#onTouchEvent(MotionEvent)

對於View#onTouchEvent(MotionEvent)方法,它是當View接受到觸摸事件時被調用(暫不關心事件分發),第一我們從它可以拿到DOWNMOVEUPCANCEL幾個關鍵事件,第二我們可以拿到每個DOWN等事件發生時手指在屏幕上的位置和手指在View內的位置。基於此我們可以想到做很多事,假如我們在手指DOWN時記錄手指的xy,在MOVE時根據DOWN時的xy來計算手指滑動的距離,然後讓View發生一個移動,在手指UP/CANCEL時讓View回到最開始的位置,因此我們做了第一個效果,下面來做具體的代碼分析。

我們定義一個ScrollLayout,然後繼承自LinearLayout,在xml中引用,然後在ScrollLayout中放一個TextView,並讓內容居中:

<?xml version="1.0" encoding="utf-8"?><com.yanzhenjie.defineview.widget.ScrollLayout    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center">
    <TextView        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="按住我拖動試試" /></com.yanzhenjie.defineview.widget.ScrollLayout>12345678910111234567891011

佈局就是這樣的,根據上面的分析我們實現ScrollLayout的具體代碼,請看:

// 手指最後在View中的座標。private int mLastX;private int mLastY;// 手指按下時View的相對座標。private int mDownViewX;private int mDownViewY;@Overridepublic boolean onTouchEvent(MotionEvent event) {    // 第一步,記錄手指在view的座標。
    int x = (int) event.getRawX();    int y = (int) event.getRawY();    int action = event.getAction();    switch (action) {        case MotionEvent.ACTION_DOWN: {            // 記錄View相對於初始位置的滾動座標。
            mDownViewX = getScrollX();
            mDownViewY = getScrollY();            // 更新手指此時的座標。
            mLastX = x;
            mLastY = y;            return true;
        }        case MotionEvent.ACTION_MOVE: {            // 計算手指此時的座標和上次的座標滑動的距離。
            int dy = y - mLastY;            int dx = x - mLastX;            // 更新手指此時的座標。
            mLastX = x;
            mLastY = y;            // 滑動相對距離。
            scrollBy(-dx, -dy);            return true;
        }        case MotionEvent.ACTION_UP:        case MotionEvent.ACTION_CANCEL: {
            scrollTo(mDownViewX, mDownViewY);            return true;
        }
    }    return super.onTouchEvent(event);
}1234567891011121314151617181920212223242526272829303132333435363738394041424344454612345678910111213141516171819202122232425262728293031323334353637383940414243444546

那麼這裏再來說明兩個方法:

  • View#getScrollX() 
    獲取View相對於它初始位置X方向的滾動量。

  • View#getScrollY() 
    獲取View相對於它初始位置Y方向的滾動量。

根據我們上面的分析,這裏處理了四個事件,分別是:

  1. MotionEvent.ACTION_DOWN

  2. MotionEvent.ACTION_MOVE

  3. MotionEvent.ACTION_UP

  4. MotionEvent.ACTION_CANCEL

  • 第一步,因爲ACTION_DOWNACTION_MOVE中都需要記錄手指當前座標,所以一進入就記錄了event.getRawX()event.getRawY()

  • 第二步,ACTION_DOWN手指按下時被調用,在一次觸摸中只會被調用一次,在ACTION_DOWN的時候記錄了content相對於最開始滾動的座標getScrollX()getScrollY(),在我們我們手指鬆開時它滾動了多少getScrollX()和多少getScrollY(),那麼我們就調用scrollTo(int, int)滾動多少-getScrollX()和多少-getScrollY(),這樣它不就回到初始位置了嗎?同時記錄了手指此時的座標,用來在ACTION_MOVE的時候計算第一次ACTION_MOVE時的移動距離。

  • 第三步,ACTION_MOVE會在手指移動的時候調用,所以它會調用多次,所以每次需要計算與上次的手指座標的滑動距離,並且更新本次的手指座標,然後調用scrollBy(int, int)去滑動當前手指與上次手指的座標(當前View的位置)的距離。

  • 第四步,ACTION_UP在手指擡起時被調用,ACTION_CANCEL在手指滑動這個View的區域時被調用,此時我們調用scrollTo(int, int)回到最初的位置。

我們來看看效果:

這裏寫圖片描述

嗯效果已經實現了,但是我們發現和開頭演示的效果有點出入,就是手指鬆開時View一下子就回去了而不是平滑的回到最初的位置,因此我們需要用到Scroller

Scroller

Scroller是手指滑動中比較重要的一個輔助類,可以輔助我們完成一些動畫參數的計算等,下面把它的幾個重要的方法做個簡單解釋。

  • Scroller#startScroll(int startX, int startY, int dx, int dy)

  • Scroller#startScroll(int startX, int startY, int dx, int dy, int duration) 
    這倆方法幾乎是一樣的,用來標記一個View想要從哪裏移動到哪裏。 
    startX,x方向從哪裏開始移動。 
    startY,y方向從哪裏開始移動。 
    dx,x方向移動多遠。 
    dy,y方向移動多遠。 
    duration,這個移動操作需要多少時間執行完,默認是250毫秒。

當然光這個方法是不夠的,它只是標記一個位置和時間,那麼怎麼計算呢?

  • Scroller#computeScrollOffset() 
    這個方法用來計算當前你想知道的一個新位置,Scroller會自動根據標記時的座標、時間、當前位置計算出一個新位置,記錄到內部,我們可以通過Scroller#getCurrX()Scroller#getCurrY()獲取的新的位置。

    要知道的是,它計算出的新位置是一個閉區間[x, y],而且會在你調用startScroll傳入的時間內漸漸從你指定的int startXint startY移動int dxint dy的距離,所以我們每次調用Scroller#computeScrollOffset()後再調用ViewscrollTo(int, int)然後傳入Scroller#getCurrX()Scroller#getCurrY()就可以得到一個漸漸移動的效果。

    同時這個方法有一個返回值是boolean類型的,內部是用一個boolean來記錄是否完成的,在調用Scroller#startScroll)時會把這個boolean參數置爲false。內部邏輯是先判斷startScroll()動畫是否還在繼續,如果沒有完成則計算最新位置,計算最新位置前會對duration做判斷,第一如果時間沒到,則真正的計算位置,並且返回true,第二如果時間到了,把記錄是否繼續的boolean成員變量標記完成,並直接賦值最新位置爲最終目的位置,並且返回true;如果startScroll()已經完成則直接返回false。我們判斷Scroller#computeScrollOffset()是true時說明還沒完成,此時拿到Scroller#getCurrX()Scroller#getCurrY()做一個滾動,待會代碼中可以看到這個邏輯。

  • Scroller#getCurrX()

  • Scroller#getCurrY() 
    這兩個方法就是拿到通過Scroller#computeScrollOffset()計算出的新的位置,上面也解釋過了。

  • Scroller.isFinished() 
    上次的動畫是否完成。

  • Scroller.abortAnimation() 
    取消上次的動畫。

這裏要強調的是Scroller.isFinished()和一般是配套使用的,一般咋ACTION_DWON的時候判斷是否完成,如果沒有完成咋取消動畫。

基於此,我們完善上面的效果,讓它平滑滾動,所以我們來完善一下。

View#onTouchEvent(MotionEvent)與Scroller結合完善動畫

private Scroller mScroller;private int mLastX;private int mLastY; public ScrollLayout(Context context) {    this(context, null, 0);
}public ScrollLayout(Context context, AttributeSet attrs) {    this(context, attrs, 0);
}public ScrollLayout(Context context, AttributeSet attrs, int defStyleAttr) {    super(context, attrs, defStyleAttr);
    mScroller = new Scroller(context);
}@Overridepublic boolean onTouchEvent(MotionEvent event) {    int x = (int) event.getRawX();    int y = (int) event.getRawY();    int action = event.getAction();    switch (action) {        case MotionEvent.ACTION_DOWN: {            if (!mScroller.isFinished()) { // 如果上次的調用沒有執行完就取消。
                mScroller.abortAnimation();
            }
            mLastX = x;
            mLastY = y;            return true;
        }        case MotionEvent.ACTION_MOVE: {            int dy = y - mLastY;            int dx = x - mLastX;

            mLastX = x;
            mLastY = y;

            scrollBy(-dx, -dy);            return true;
        }        case MotionEvent.ACTION_UP:        case MotionEvent.ACTION_CANCEL: {            // XY都從滑動的距離回去,最後一個參數是多少毫秒內執行完這個動作。
            mScroller.startScroll(getScrollX(), getScrollY(), -getScrollX(), -getScrollY(), 1000);
            invalidate();            return true;
        }
    }    return super.onTouchEvent(event);
}/**
 * 這個方法在調用了invalidate()後被回調。
 */@Overridepublic void computeScroll() {    if (mScroller.computeScrollOffset()) { // 計算新位置,並判斷上一個滾動是否完成。
        scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
        invalidate();// 再次調用computeScroll。
    }
}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263
  • 第一步,在構造方法中初始化Scroller

  • 第二步,在ACTION_DOWN時去掉最開始記錄的content的初始位置,下面講爲什麼。並且判斷Scroller的動畫是否完成,沒有完成則取消。

  • 第三步,在ACTION_MOVE的時候調用滾動,讓View跟着手指走。

  • 第四步,在ACTION_UPACTION_CANCEL時讓View平滑滾動到最初位置。 
    根據上面Scroller的分析,這裏可以調用Scroller#startScroll(startX, startY, dx, dy, duration)記錄開始位置,和滑動的距離以及指定動畫完成的時間。

  1. (startX, startY)傳入當前content的相對與最開始滾動的位置(getScrollX(), getScrollY())

  2. (dx, dy)要傳入要平滑滑動的距離,那麼傳什麼呢?既然它滾動了(getScrollX(), getScrollY()),那麼我們就讓它滾這麼多的距離回去不久行了?所以我們傳入(-getScrollX(), -getScrollY())

  3. duration滾動時間,我們傳個800毫秒,1000毫秒的都可以,默認是250毫秒。

第五步,調用invalidate()/postInvalidate()刷新View,最底層View會調用一系列方法,這裏我們重寫其中computeScroll()方法。
  1. 我們看到invalidate()postInvalidate()invalidate()在當前線程調用,也就是主線程,這裏我們使用invalidate()postInvalidate()一般在子線程需要刷新View時調用。

  2. computeScroll()方法是用來計算滾動的,我們平滑滾動時不就是要它麼。

第六步,根據上面Scroller的分析,在computeScroll()中此時調用Scroller.computeScrollOffset()再好不過了,計算出一個新的相對位置,然後調用scrollTo(int, int)滑動過去。第七步,在computeScroll()中scrollTo(int, int)後調用invalidate()computeScroll刷新視圖,呈現出一個動畫的效果。

彈性效果

View#onTouchEvent(MotionEvent)與Scroller再升級

View#onTouchEvent(MotionEvent)Scroller結合再升級,這一節是基於上一節的,如果你沒看上一節,那麼最好看完再看這個,不然非常可能看不懂。下面我們來完成文中開頭的第二個效果,一個模擬ViewPager翻頁且加彈性動畫的效果。

上面的自定義ScrollLayout是繼承LinearLayout的,下面我們新建一個ScrollPager的繼承ViewGroup,來完成目標:

public class ScrollPager extends ViewGroup {
    public ScrollPager(Context context) {        this(context, null, 0);
    }    public ScrollPager(Context context, AttributeSet attrs) {        this(context, attrs, 0);
    }    public ScrollPager(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);
    }
}1234567891011121312345678910111213

然後我們把佈局寫好,放三個Layout,高度爲100dp,寬度都爲match_parent

<?xml version="1.0" encoding="utf-8"?><com.yanzhenjie.defineview.widget.ScrollPager
xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <LinearLayout        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:gravity="center"
        android:orientation="vertical">

        <TextView            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="第一頁" />

    </LinearLayout>

    <LinearLayout        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:gravity="center"
        android:orientation="vertical">

        <TextView            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="第二頁" />

    </LinearLayout>

    <LinearLayout        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:gravity="center"
        android:orientation="vertical">

        <TextView            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="第三頁" />

    </LinearLayout></com.yanzhenjie.defineview.widget.ScrollPager>123456789101112131415161718192021222324252627282930313233343536373839404142434445123456789101112131415161718192021222324252627282930313233343536373839404142434445

佈局蠻簡單了,就是一個ViewGroup中三個高度爲100dp,寬度都爲match_parentLinearLayout,寬度爲match_parent是爲了佔滿一屏的寬。然後每個LinearLayout中一個TextView,分別爲第一頁、第二頁、第三頁。

分析一下,ViewPager首先要每一屏一個Layout/View,加上繼承ViewGroup必須要重寫ViewGroup#onLayout()ViewGroup#onLayout()是用來佈局子View的,也就是在它裏面決定哪個View放在哪裏。

爲了新建的ScrollPager中的View橫向鋪開,所以我們接着實現ScrollPager#onLayout(),但是要想佈局子View,就得知道子View的寬高,所以先要測量寬高,因此還得重寫ScrollPager#onMeasure方法測量View大小,因此我們有了下面的代碼:

@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    super.onMeasure(widthMeasureSpec, heightMeasureSpec);    int childCount = getChildCount();    // 在Layout 子view之前測量子view大小,在layout的時候才能調用getMeasuredWidth()和getMeasuredHeight()。
    for (int i = 0; i < childCount; i++) {
        View childView = getChildAt(i);
        measureChild(childView, widthMeasureSpec, heightMeasureSpec);
    }
}@Overrideprotected void onLayout(boolean changed, int l, int t, int r, int b) {    if (changed) {        int childCount = getChildCount();        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);            int childW = childView.getMeasuredWidth();            // 把所有子view放在水平方向,依次排開。
            // left:  0, w, 2w, 3w..
            // top:   0...
            // right: w, 2w, 3w...
            // topL   h...
            childView.layout(i * childW, 0, childW * i + childW, childView.getMeasuredHeight());
        }
    }
}1234567891011121314151617181920212223242526272812345678910111213141516171819202122232425262728

onMeasure()沒神馬好解釋的,就是挨個測量子View的大小,如果細節不懂可以自行搜索。那麼onLayout()中調用子ViewView#layout()方法把子View佈局到ScrollPager上,並且依次橫向排開。

然後我們把’onTouchEvent()’中的滑動處理一下:

// 手指每次移動時需要更新xy,記錄上次手指所處的座標。private float mLastX;@Overridepublic boolean onTouchEvent(MotionEvent event) {    float x = event.getRawX();    int action = event.getAction();    switch (action) {        case MotionEvent.ACTION_DOWN:
            mLastX = x;            return true;        case MotionEvent.ACTION_MOVE:            int dxMove = (int) (mLastX - x);
            scrollBy(dxMove, 0);
            mLastX = x;            return true;        case MotionEvent.ACTION_UP:        case MotionEvent.ACTION_CANCEL: {            // 鬆開時處理慣性滑動。
            break;
        }
    }    return super.onTouchEvent(event);
}12345678910111213141516171819202122232425261234567891011121314151617181920212223242526

這裏我們只是沒有處理ACTION_UPACTION_CANCEL事件,我們來運行一把看看:

簡陋的Pager效果

哦喲,出來了,可是沒有像ViewPager那樣鬆開時自動動切換到某一頁,所以我們還要處理ACTION_UPACTION_CANCEL事件。

要想有鬆開時平滑滑動到某一頁,我們分析一下,肯定是需要Scroller的,然後還要重寫View#computeScroll()方法,下面是完成的代碼:

private Scroller mScroller;// 手指每次移動時需要更新xy,記錄上次手指所處的座標。private float mLastX;public ScrollPager(Context context) {    this(context, null, 0);
}public ScrollPager(Context context, AttributeSet attrs) {    this(context, attrs, 0);
}public ScrollPager(Context context, AttributeSet attrs, int defStyleAttr) {    super(context, attrs, defStyleAttr);
    mScroller = new Scroller(context);
}@Overridepublic boolean onTouchEvent(MotionEvent event) {    float x = event.getRawX();    int action = event.getAction();    switch (action) {        case MotionEvent.ACTION_DOWN:            if (!mScroller.isFinished()) { // 如果上次的調用沒有執行完就取消。
                mScroller.abortAnimation();
            }
            mLastX = x;            return true;        case MotionEvent.ACTION_MOVE:            int dxMove = (int) (mLastX - x);
            scrollBy(dxMove, 0);
            mLastX = x;            return true;        case MotionEvent.ACTION_UP:        case MotionEvent.ACTION_CANCEL: {            int sonIndex = (getScrollX() + getWidth() / 2) / getWidth();            // 如果滑動超過最後一頁,就退回到最後一頁。
            int childCount = getChildCount();            if (sonIndex >= childCount)
                sonIndex = childCount - 1;            // 現在滑動的相對距離。
            int dx = sonIndex * getWidth() - getScrollX();            // Y方向不變,X方向到目的地。
            mScroller.startScroll(getScrollX(), 0, dx, 0, 500);
            invalidate();            break;
        }
    }    return super.onTouchEvent(event);
}@Overridepublic void computeScroll() {    if (mScroller.computeScrollOffset()) {
        scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
        invalidate();
    }
}@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    super.onMeasure(widthMeasureSpec, heightMeasureSpec);    int childCount = getChildCount();    // 在Layout 子view之前測量子view大小,在onLayout的時候才能調用getMeasuredWidth()和getMeasuredHeight()。
    for (int i = 0; i < childCount; i++) {
        View childView = getChildAt(i);
        measureChild(childView, widthMeasureSpec, heightMeasureSpec);
    }
}@Overrideprotected void onLayout(boolean changed, int l, int t, int r, int b) {    if (changed) {        int childCount = getChildCount();        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);            int childW = childView.getMeasuredWidth();            // 把所有子view放在水平方向,依次排開。
            // left:  0, w, 2w, 3w..
            // top:   0...
            // right: w, 2w, 3w...
            // topL   h...
            childView.layout(i * childW, 0, childW * i + childW, childView.getMeasuredHeight());
        }
    }
}1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909112345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091

這裏需要解釋的只有這一段代碼:

case MotionEvent.ACTION_UP:case MotionEvent.ACTION_CANCEL: {    int sonIndex = (getScrollX() + getWidth() / 2) / getWidth();    // 如果滑動頁面超過當前頁面數,那麼把屏index定爲最大頁面數的index。
    int childCount = getChildCount();    if (sonIndex >= childCount)
        sonIndex = childCount - 1;    // 現在滑動的相對距離。
    int dx = sonIndex * getWidth() - getScrollX();    // Y方向不變,X方向到目的地。
    mScroller.startScroll(getScrollX(), 0, dx, 0, 500);
    invalidate();    break;
}1234567891011121314151612345678910111213141516

當手指鬆開的時候怎麼平滑過度到某一頁呢?

  • 先來看int sonIndex = (getScrollX() + getWidth() / 2) / getWidth();,這句話的意思是拿到從最開始滑動到當前位置的距離 加上 Layout一半的Layout寬 除以Layout寬,得到的結果是在屏幕上顯示的較多區域的這一屏的子View的index。

    是什麼意思呢?,舉個例子來說,當前向左滑動了一屏,那麼getScrollX()的距離和getWidth的寬度就是相等的,因爲滑動了一屏的距離,這個時候如果直接用getScrollX()/getWidth()那麼得到的結果是1沒有問題。

    如果現在從0屏開始滑,滑了小半屏,此時的getScrollX() < getWidth(),那麼計算出的int必將是0,假如我滑了大半屏,此時計算出的結果又是0,但是根據慣性和四捨五入,我們滑動大半屏的時候,應該跑到下一屏,所以我們在getScrollX()/getWidth()之前給getScrollX()加了getWidth()/2的距離,這樣不滿一屏的將會自動補滿一屏。

  • 然後int dx = sonIndex * getWidth() - getScrollX();,目標位置的距離sonIndex * getWidth()減掉已經滑動的距離getScrollX()得出的現在要滑動的相對距離。

此時運行一把,我們將得到正確的效果:

仿ViewPager彈性翻頁


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