ViewPager不為人知的秘密

ViewPager不為人知的秘密

ViewPager翻頁控制

關于控制ViewPager的翻頁,在網上已經有很多解決方法了,我們一個個來看看。

setScanScroll()

我們先來看一下具體實現(xiàn):

public class CustomViewPager extends ViewPager {  

    private boolean isCanScroll = true;  

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

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

    public void setScanScroll(boolean isCanScroll){  
        this.isCanScroll = isCanScroll;  
    }  

    @Override  
    public void scrollTo(int x, int y){  
        if (isCanScroll){  
            super.scrollTo(x, y);  
        }  
    } 
} 

通過控制isCanScroll變量,設置給scrollTo()方法,控制是否能滑動,看上去非常完美,實際上是最不靠譜的方法,因為你setScanScroll()調用之后狀態(tài)就無法再修改這個狀態(tài)了,甚至是setCurrentItem方法都不能調用了。

修改Touch事件

同樣,我們先來看看代碼:

public class NoScrollViewPager extends ViewPager {
    public NoScrollViewPager(Context context) {
        super(context);
    }

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

    @Override
    public boolean onTouchEvent(MotionEvent arg0) {
        return false;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent arg0) {
        return false;
    }
}

這代碼也很簡單,就是控制ViewPager的Touch事件,這個基本是萬能的,畢竟是從根源上入手的。你可以在onTouchEvent和onInterceptTouchEvent中做邏輯的判斷。

重寫ViewPager

前面兩種方法固然可以在一定程度上完成我們的要求,但是顯得略2.所以,我們來看這種方式。

首先我們要了解下ViewPager切頁的原理,經過一段時間的查找,我們找到了這個類:

    private int determineTargetPage(int currentPage, float pageOffset, int velocity, int deltaX) {
        int targetPage;
        if (Math.abs(deltaX) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) {
            targetPage = velocity > 0 ? currentPage : currentPage + 1;
        } else {
            final float truncator = currentPage >= mCurItem ? 0.4f : 0.6f;
            targetPage = (int) (currentPage + pageOffset + truncator);
        }

        if (mItems.size() > 0) {
            final ItemInfo firstItem = mItems.get(0);
            final ItemInfo lastItem = mItems.get(mItems.size() - 1);

            // Only let the user target pages we have items for
            targetPage = Math.max(firstItem.position, Math.min(targetPage, lastItem.position));
        }
        return targetPage;
    }

不用問我是怎么找到的,這是程序員的嗅覺。

這個方法會在切頁的時候重定向Page,那么我們只要在這個方法內重新定向到我們想要的Page就好了。

這是ViewPager的控制切頁邏輯。

下面我們繼續(xù)看,其實在ViewPager中,就給我們提供了一個重寫的方法——canScroll,看名字就知道了,這個方法是來控制是否能夠滑動的,我們來試下,我們先extends ViewPager,然后重寫這個方法:

    @Override
    protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
        boolean result = super.canScroll(v, checkV, dx, x, y);
        if (dx < 0 && (/*其它控制邏輯**/)) {
            return true;
        }
        return result;
    }

通過控制這個方法返回值,就可以真真實實的控制ViewPager的滑動了,你可以試一下,當然,肯定是可以的。

那是不是這樣就可以了呢?當然不是的,不然我怎么能繼續(xù)裝逼呢?

雖然在大部分時間,這個回調已經可以實現(xiàn)ViewPager的翻頁控制了,但是,如果你翻頁速度很快,你就會發(fā)現(xiàn),其實這個回調方法的執(zhí)行,是跟不上你的速度的。如果你翻頁很快,是可以跳過去的,如果你打log,你會發(fā)現(xiàn),canScroll雖然會一直回調,但是回調并不是實時的,所以會出現(xiàn)bug。這也是為什么我開始要解釋ViewPager翻頁原理的原因,真不是我要裝逼,而是為你留下的伏筆。

所以,最終的解決方案就是canScroll + determineTargetPage

首先,我們要重寫ViewPager,不用害怕,ViewPager沒有任何依賴,你可以把整個ViewPager的源代碼全部copy過來,而不需要修改一行代碼,除了包名。

然后,我們找到determineTargetPage這個方法,將targetPage修改下:

    private int determineTargetPage(int currentPage, float pageOffset, int velocity, int deltaX) {
        int targetPage;
        if (Math.abs(deltaX) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) {
            targetPage = velocity > 0 ? currentPage : currentPage + 1;
        } else {
            final float truncator = currentPage >= mCurItem ? 0.4f : 0.6f;
            targetPage = (int) (currentPage + pageOffset + truncator);
        }

        if (mItems.size() > 0) {
            final ItemInfo firstItem = mItems.get(0);
            final ItemInfo lastItem = mItems.get(mItems.size() - 1);

            // Only let the user target pages we have items for
            targetPage = Math.max(firstItem.position, Math.min(targetPage, lastItem.position));
        }

        targetPage = reDetermineTargetPage(targetPage);

        return targetPage;
    }

targetPage = reDetermineTargetPage(targetPage)這個就是我們加的代碼,通過reDetermineTargetPage方法,我們來修改ViewPager的targetPage,是不是很無恥的感覺,正常正常。

所以,我們要增加一個父類方法給我們后面繼承的ViewPager重寫:

    public int reDetermineTargetPage(int targetPage) {
        return targetPage;
    }

最后,我們在繼承的ViewPager中,重寫這兩個方法:

public class MyViewPager extends ViewPager {

    @Override
    protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
        boolean rt = super.canScroll(v, checkV, dx, x, y);
        if (dx < 0 && (/*其他邏輯控制**/)) {
            return true;
        }
        return rt;
    }

    @Override
    public int reDetermineTargetPage(int targetPage) {
        int rtn = targetPage;
        int currentPage = getCurrentItem();
        if (targetPage > currentPage && (/* 其他邏輯控制**/)) {
            rtn = currentPage;
        }
        return rtn;
    }
}

這樣我們就非常完美的實現(xiàn)了ViewPager的翻頁控制,在慢慢翻頁的時候,canScroll就可以幫我們控制了,當快速翻頁的時候reDetermineTargetPage給我們做了雙保險,即使你翻頁過去了,你也會被targetPage給帶回來。

ViewPager強制刷新UI

ViewPager不能動態(tài)刷新UI的原因主要是因為PagerAdapter中調用notifyDataSetChanged是會失效的。

通用解決方法

當ViewPager繪制完Item之后,ViewPager會把child標記為POSITION_UNCHANGED,這樣就不會在notifyDataSetChanged后更新這個View了。所以,要解決這個問題,我們只需要在:

    @Override
    public int getItemPosition(Object object) {
        return POSITION_NONE;
    }

當我們調用PagerAdapter的notifyDataSetChanged方法之后,系統(tǒng)會去Adapter的getItemPosition方法中遍歷所有的child,我們在上面的方法中改寫了返回值,全部返回為POSITION_NONE,表示child都沒有繪制過,這樣ViewPager就會去重繪了。

更加優(yōu)化一點的代碼如下:

    @Override
    public void notifyDataSetChanged() {
        mChildCount = getCount();
        super.notifyDataSetChanged();
    }

    @Override
    public int getItemPosition(Object object) {
        // 重寫getItemPosition,保證每次獲取時都強制重繪UI
        if (mChildCount > 0) {
            mChildCount--;
            return POSITION_NONE;
        }
        return super.getItemPosition(object);
    }

我們增加一個mChildCount來記錄子類的數(shù)量,在一定程度上減少重繪的次數(shù)。

因為重繪的時候,ViewPager會的Destory Item,增加了系統(tǒng)開銷。

更加優(yōu)化的方法

當我們只需要對ViewPager中的某些元素進行更新時,我們可以在instantiateItem方法調用時,用View.setTag方法加入標志,在需要更新View時,通過findViewWithTag的方法找到對應的View進行更新。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容