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進行更新。