ViewPager禁止滑動和修改滑動速度

  1. 利用ViewPage的PagerTransformer定製頁面切換效果
  2. ViewPager動態添加刪除及刷新頁面
  3. ViewPager打造真正意義的無限輪播
  4. ViewPage 聯動效果自帶角標
  5. ViewPager禁止滑動和修改滑動速度

1. 簡介

實際開發中,我們有時候需要禁止 ViewPager 滑動,和改變 ViewPager 切換頁面時的滑動速率。下面總結了 禁止ViewPager滑動和通過Viewpager 的 scroller 修改滑動速度的實現。非常簡單。

2. ViewPager 禁止滑動

禁止 ViewPager 滑動,最簡單的方式就是攔截 ViewPager 觸摸滑動事件。如下代碼所示:

import android.content.Context;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;

public class NoScrollViewPager extends ViewPager {

    private boolean scrollable = true;

    public NoScrollViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public NoScrollViewPager(Context context) {
        super(context);
    }

    public void setScrollable(boolean scrollable) {
        this.scrollable = scrollable;
    }

    public boolean isScrollable() {
        return scrollable;
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        if (scrollable) {
            return super.onTouchEvent(ev);
        } else {
            return false;
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (scrollable) {
            return super.onInterceptTouchEvent(ev);
        } else {
            return false;
        }
    }
}

使用時,調用 setScrollable(false) 就可以禁止 ViewPager 滑動。

3. ViewPager 修改滑動速度

我們都知道,ViewPager 切換頁面有兩種方式:一是,手指觸摸滑動切換頁面;二是,調用方法 setCurrentItem(int) 切換頁面。用手指觸摸滑動切換頁面時,頁面的滑動與手指滑動速度有關,過渡動畫很明顯,體驗很好。調用方法切換頁面時,由於頁面切換速度較快,過渡動畫不明顯甚至沒有,往往是直接閃過去。當我們需要 ViewPager 自動切換頁面,例如自動輪播時,ViewPager 原有的切換速度有時候難以適應我們的需求,尤其在我們定製了 ViewPager 的切換效果時,就更加難以適應我們對交互體驗的需求了。這時我們就需要修改這種情況下頁面滑動速度了。如何修改頁面切換速度呢?咱們深入瞭解一下 ViewPager。

首先,我們看一下 setCurrentItem(int) 方法的源碼:

public void setCurrentItem(int item) {
    mPopulatePending = false;
    setCurrentItemInternal(item, !mFirstLayout, false);
}

調用了 setCurrentItemInternal(int, boolean, boolean) 方法,再看 setCurrentItemInternal(int, boolean, boolean) 方法;

void setCurrentItemInternal(int item, boolean smoothScroll, boolean always) {
	setCurrentItemInternal(item, smoothScroll, always, 0);
}

繼續往下看:

    void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) {
        if (mAdapter == null || mAdapter.getCount() <= 0) {
            setScrollingCacheEnabled(false);
            return;
        }
        if (!always && mCurItem == item && mItems.size() != 0) {
            setScrollingCacheEnabled(false);
            return;
        }

        if (item < 0) {
            item = 0;
        } else if (item >= mAdapter.getCount()) {
            item = mAdapter.getCount() - 1;
        }
        final int pageLimit = mOffscreenPageLimit;
        if (item > (mCurItem + pageLimit) || item < (mCurItem - pageLimit)) {
            // We are doing a jump by more than one page.  To avoid
            // glitches, we want to keep all current pages in the view
            // until the scroll ends.
            for (int i = 0; i < mItems.size(); i++) {
                mItems.get(i).scrolling = true;
            }
        }
        final boolean dispatchSelected = mCurItem != item;

        if (mFirstLayout) {
            // We don't have any idea how big we are yet and shouldn't have any pages either.
            // Just set things up and let the pending layout handle things.
            mCurItem = item;
            if (dispatchSelected) {
                dispatchOnPageSelected(item);
            }
            requestLayout();
        } else {
            populate(item);
            scrollToItem(item, smoothScroll, velocity, dispatchSelected);
        }
    }

整段代碼和滑動相關的大概只有這一行:

scrollToItem(item, smoothScroll, velocity, dispatchSelected);

我們看一下此方法:

    private void scrollToItem(int item, boolean smoothScroll, int velocity,
            boolean dispatchSelected) {
        final ItemInfo curInfo = infoForPosition(item);
        int destX = 0;
        if (curInfo != null) {
            final int width = getClientWidth();
            destX = (int) (width * Math.max(mFirstOffset,
                    Math.min(curInfo.offset, mLastOffset)));
        }
        if (smoothScroll) {
            smoothScrollTo(destX, 0, velocity);
            if (dispatchSelected) {
                dispatchOnPageSelected(item);
            }
        } else {
            if (dispatchSelected) {
                dispatchOnPageSelected(item);
            }
            completeScroll(false);
            scrollTo(destX, 0);
            pageScrolled(destX);
        }
    }

在此方法中,velocity 表示速度,與速度有關的是 smoothScrollTo(destX, 0, velocity),我們只需關注 smoothScrollTo() 方法。

    void smoothScrollTo(int x, int y, int velocity) {
        if (getChildCount() == 0) {
            // Nothing to do.
            setScrollingCacheEnabled(false);
            return;
        }

        int sx;
        boolean wasScrolling = (mScroller != null) && !mScroller.isFinished();
        if (wasScrolling) {
            // We're in the middle of a previously initiated scrolling. Check to see
            // whether that scrolling has actually started (if we always call getStartX
            // we can get a stale value from the scroller if it hadn't yet had its first
            // computeScrollOffset call) to decide what is the current scrolling position.
            sx = mIsScrollStarted ? mScroller.getCurrX() : mScroller.getStartX();
            // And abort the current scrolling.
            mScroller.abortAnimation();
            setScrollingCacheEnabled(false);
        } else {
            sx = getScrollX();
        }
        int sy = getScrollY();
        int dx = x - sx;
        int dy = y - sy;
        if (dx == 0 && dy == 0) {
            completeScroll(false);
            populate();
            setScrollState(SCROLL_STATE_IDLE);
            return;
        }

        setScrollingCacheEnabled(true);
        setScrollState(SCROLL_STATE_SETTLING);

        final int width = getClientWidth();
        final int halfWidth = width / 2;
        final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dx) / width);
        final float distance = halfWidth + halfWidth
                * distanceInfluenceForSnapDuration(distanceRatio);

        int duration;
        velocity = Math.abs(velocity);
        if (velocity > 0) {
            duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
        } else {
            final float pageWidth = width * mAdapter.getPageWidth(mCurItem);
            final float pageDelta = (float) Math.abs(dx) / (pageWidth + mPageMargin);
            duration = (int) ((pageDelta + 1) * 100);
        }
        duration = Math.min(duration, MAX_SETTLE_DURATION);

        // Reset the "scroll started" flag. It will be flipped to true in all places
        // where we call computeScrollOffset().
        mIsScrollStarted = false;
        mScroller.startScroll(sx, sy, dx, dy, duration);
        ViewCompat.postInvalidateOnAnimation(this);
    }

這個方法就是關鍵了!我們知道 velocity 代表速度,找到 velocity,發現 velocity > 0 時,通過 velocity 和 滑動距離(這個應該是手指滑動的)計算出了一個 duration,與設定的最大時間間隔 MAX_SETTLE_DURATION 比較,取較小的,然後再參與滾動。我們向上找 velocity 的傳值,發現在 void setCurrentItemInternal(int item, boolean smoothScroll, boolean always) 方法中我們傳了 0,duration 會根據 將要滾動的距離 和 頁面佔用的寬度 計算出滾動的頁數,duration 爲 100*(滾動頁數 + 1) 毫秒,最後還是會取 duration 和 MAX_SETTLE_DURATION 的較小值。最終,duration 最大爲 MAX_SETTLE_DURATION ,MAX_SETTLE_DURATION 的值是 600。而 duration 最終傳入了 mScroller.startScroll(sx, sy, dx, dy, duration) 。兩個頁面切換時,滾動距離是固定的,duration 越小,速度越快;duration 越大,速度越慢。速度越慢,我越是有時間展示我們的頁面切換動畫,交互體驗就越好。

那麼,如何改變這個duration 呢?我們還要看 mScroller。發現,mScroller 是 ViewPager 的私有屬性,而且沒有提供對外的方法去修改這個 mScroller。沒有辦法,我們只能藉助於強大的反射機制,簡單粗暴但實用。

既然要改變 mScroller ,那麼就需要爲 mScroller 重新賦值,我們先繼承 Scroller 重寫 startScroll() 方法,使用自己的 duration,棄用上面代碼傳入的 duration 如下代碼所示:

import android.content.Context;
import android.view.animation.Interpolator;
import android.widget.Scroller;

public class FixedSpeedScroller extends Scroller {

    private static final int DURATION_DEF = 1500;
    private int mDuration = DURATION_DEF;
                                                                                                            
    public FixedSpeedScroller(Context context) {
        this(context, DURATION_DEF);
    }

    public FixedSpeedScroller(Context context, int duration) {
        this(context, null, duration);
    }
                                                                                                            
    public FixedSpeedScroller(Context context, Interpolator interpolator) {
        this(context, interpolator, DURATION_DEF);
    }
    public FixedSpeedScroller(Context context, Interpolator interpolator, int duration) {
        super(context, interpolator);
        this.mDuration = duration;
    }
                                                                                                            
    @Override
    public void startScroll(int startX, int startY, int dx, int dy, int duration) {
        // Ignore received duration, use fixed one instead
        super.startScroll(startX, startY, dx, dy, mDuration);
    }
                                                                                                            
    @Override
    public void startScroll(int startX, int startY, int dx, int dy) {
        // Ignore received duration, use fixed one instead
        this.startScroll(startX, startY, dx, dy, mDuration);
    }
                                                                                                            
    public void setDuration(int duration) {
        mDuration = duration;
    }

}

這樣的話,我們就可以自己自定義滑動的時間,控制滑動的速度。下一步就是如何修改 mScroller值了,上面提到,我們只能使用反射,那我們就用反射爲 mScroller 重新賦上我們想要的值,如下代碼:

try {
    Field field = ViewPager.class.getDeclaredField("mScroller");
    field.setAccessible(true);
    FixedSpeedScroller scroller = new FixedSpeedScroller(viewPager.getContext(), new AccelerateInterpolator());
    field.set(viewPager, scroller);
    scroller.setDuration(2000);
} catch (NoSuchFieldException e) {
    e.printStackTrace();
} catch (IllegalAccessException e) {
    e.printStackTrace();
}

4. 參考

·1> android之ViewPager修改滑動速度:
https://www.cnblogs.com/cmai/p/7705190.html

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