ViewPager源碼分析

目錄

  • PagerAdapter 介紹
  • ViwePager 緩存策略
  • ViewPager 布局處理
  • ViewPager 事件處理
  • 相關(guān)內(nèi)容

PagerAdapter 介紹

ViewPager使用非常簡單,看下面代碼片段

viewPager.setAdapter(new Adapter());

private class Adapter extends PagerAdapter {
    // container 其實就是ViewPager
    public Object instantiateItem(@NonNull ViewGroup container, int position) {
        View itemView = LayoutInflater.from(context).inflate(R.layout.item_pager, null);
        container.addView(itemView);
        return itemView;
    }
    // object  為 instantiateItem返回的對象
    @Override
    public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
        container.removeView((View) object);
    }
    // viewpager頁數(shù)
    @Override
    public int getCount() {
        return 10;
    }
    // 判斷view跟o是否存在對應關(guān)系,內(nèi)部其實是通過view找到對應的object的關(guān)聯(lián)關(guān)系(instantiateItem中返回的object)
    // 本例就返回view == o,因為instantiateItem方法直接返回的view
    @Override
    public boolean isViewFromObject(@NonNull View view, @NonNull Object o) {
        return view == o;
    }
}
  • 先看個對象ItemInfo,后面會用到它,它是ViewPager的一個內(nèi)部類,包含了一個頁面的基本信息,在調(diào)用Adapter的instantiateItem方法時,在ViewPager內(nèi)部就會創(chuàng)建這個對象,但它不包含view,結(jié)構(gòu)如下:
static class ItemInfo {
    Object object;      // 為adapter中instantiateItem方法返回的對象
    int position;       // 頁面position
    boolean scrolling;  // 是否正在滑動
    float widthFactor;  // 當前頁面寬度和ViewPager寬度的比例(默認是1,跟ViewPager寬度一致,可以通過重寫adapter的 getPageWidth(int position) 方法自定義內(nèi)容寬度)
    float offset;       // 當前頁面在所有已加載的頁面中的索引(用于頁面布局,后面會做詳細介紹)
}

\color{#2233EE}{解釋下PagerAdapter常用的4個方法}

Object instantiateItem(ViewGroup container, int position)

  • 這個方法是ViewPager需要加載某個頁面時調(diào)用,container就是ViewPager自己,position頁面索引;

  • 我們需要實現(xiàn)的是添加一個view到container中,然后返回一個跟這個view能夠關(guān)聯(lián)起來的對象,這個對象可以是view自身,也可以是其他對象(比如FragmentPagerAdapter返回的就是一個Fragment),關(guān)鍵是在isViewFromObject能夠?qū)iew和這個object關(guān)聯(lián)起來

void destroyItem(ViewGroup container, int position, Object object)

  • 當ViewPager需要銷毀一個頁面時調(diào)用,我們需要將position對應的view從container中移除;
    這時參數(shù)除了position就只有object,其實就是上面instantiateItem方法返回的對象,這時要通過object找到對應的View,然后將其移除掉,如果你的instantiateItem方法返回的就是View,這里就直接強轉(zhuǎn)成View移除即可:container.removeView((View) object);如果不是,一般會自己創(chuàng)建一個List緩存view列表,然后根據(jù)position從List中找到對應的view移除;(當然你也可以不移除,內(nèi)存泄漏)。
  • FragmentPagerAdapter的實現(xiàn)是:mCurTransaction.detach((Fragment)object),其實也就是將fragemnt的view從container中移除

isViewFromObject(View view, Object object)

  • 這個方法從名稱理解起來像是判斷view是否來自object,跟進一步解釋應該是上面instantiateItem方法中
    向container中添加的view和方法返回的對象兩者之間一對一的關(guān)系;因為在ViewPager內(nèi)部有個方法叫infoForChild,
    這個方法是通過view去找到對應頁面信息緩存類ItemInfo(內(nèi)部調(diào)用了isViewFromObject),如果找不到,說明這個view是個野孩子,ViewPager會認為不是Adapter提供的View,所以這個View不會顯示出來;

  • 總結(jié)一下:isViewFromObject 方法是讓view和object(內(nèi)部為ItemInfo)一一對應起來

int getItemPosition(Object object)

  • 改方法是判斷當前object對應的View是否需要更新,在調(diào)用notifyDataSetChanged時會間接觸發(fā)該方法,
    如果返回POSITION_UNCHANGED表示該頁面不需要更新,如果返回POSITION_NONE則表示該頁面無效了,需要銷毀并觸發(fā)destroyItem方法(并且有可能調(diào)用instantiateItem重新初始化這個頁面)

ViewPager緩存策略

\color{#2233EE}{mOffscreenPageLimit} 是ViewPager的一個變量,表示ViewPager左右兩邊分別最大緩存的頁面數(shù)量,可以通過ViewPager.setOffscreenPageLimit(int limit)方法設置,緩存頁面的相關(guān)計算(創(chuàng)建,銷毀)由populate函數(shù)完成,后面會詳細說明

初始化緩存(mOffscreenPageLimit == 1)

  • 當初始化時,當前顯示頁面是第0頁;mOffscreenPageLimit為1,所以預加載頁面為第1頁,再往后的頁面就不需要加載了(這里的2, 3, 4頁)
image

中間頁面緩存(mOffscreenPageLimit == 1)

  • 當向右滑動到第2頁時,左右分別需要緩存一頁,第0頁就需要銷毀掉,第3頁需要預加載,第4頁不需要加載
image

ViewPager相關(guān)方法

ViewPager.setAdapter方法

銷毀舊的Adapter數(shù)據(jù),用新的Adaper更新UI

  1. 清除舊的Adapter,對已加載的item調(diào)用destroyItem,
  2. 將自身滾動到初始位置this.scrollTo(0, 0)
  3. 設置PagerObserver: mAdapter.setViewPagerObserver(mObserver);
  4. 調(diào)用populate()方法計算并初始化View(這個方法后面會詳細介紹)
  5. 如果設置了OnAdapterChangeListener,進行回調(diào)

ViewPager.populate(int newCurrentItem)

該方法是ViewPager非常重要的方法,主要根據(jù)參數(shù)newCurrentItem和mOffscreenPageLimit計算出需要初始化的頁面和需要銷毀頁面,然后通過調(diào)用Adapter的instantiateItem和destroyItem兩個方法初始化新頁面和銷毀不需要的頁面!

  1. 根據(jù)newCurrentItem和mOffscreenPageLimit計算要加載的page頁面,計算出startPos和endPos
  2. 根據(jù)startPos和endPos初始化頁面ItemInfo,先從緩存里面獲取,如果沒有就調(diào)用addNewItem方法,實際調(diào)用mAdapter.instantiateItem
  3. 將不需要的ItemInfo移除: mItems.remove(itemIndex),并調(diào)用mAdapter.destroyItem方法
  4. 設置LayoutParams參數(shù)(包括position和widthFactor),根據(jù)position排序待繪制的View列表:mDrawingOrderedChildren,重寫了getChildDrawingOrder方法
  5. 最后一步獲取當前顯示View的焦點:currView.requestFocus(View.FOCUS_FORWARD)

ViewPager.dataSetChanged()

當調(diào)用Adapter的notifyDataSetChanged時,會觸發(fā)這個方法,該方法會重新計算當前頁面的position,
移除需要銷毀的頁面的ItemInfo對象,然后再調(diào)用populate方法刷新頁面

  • 循環(huán)mItems(每個page對應的ItemInfo對象),調(diào)用int newPos = mAdapter.getItemPosition方法
  • 當newPos等于PagerAdapter.POSITION_UNCHANGED表示當前頁面不需要更新,不用銷毀,當newPos等于PagerAdapter.POSITION_NONE時,需要更新,移除item,調(diào)用mAdapter.destroyItem
  • 循環(huán)完成后,最后計算出顯示頁面的newCurrItem,調(diào)用setCurrentItemInternal(newCurrItem, false, true)方法更新UI(實際調(diào)用populate方法重新計算頁面信息)

ViewPager.scrollToItem(int item, boolean smoothScroll, int velocity, boolean dispatchSelected)

  • 滑動到指定頁面,內(nèi)部會觸發(fā)OnPageChangeListener

ViewPager.calculatePageOffsets(ItemInfo curItem, int curIndex, ItemInfo oldCurInfo)

  • 這個方法主要用于計算每個頁面對應ItemInfo的offset變量,這個變量用于記錄當前view在所有緩存View中(包含當前顯示頁)的索引,用于布局的時候計算該View應該放在哪個位置
  • 在populate方法中更新完頁面數(shù)據(jù)后,會調(diào)用該方法計算所有頁面的offset

到目前為止,ViewPager和Adapter相關(guān)調(diào)用關(guān)系差不多分析完了,下面看ViewPager內(nèi)部對頁面的布局,滑動事件監(jiān)聽相關(guān)操作!

ViewPager 布局處理

  • ViewPager將子View分為兩種,一種是Decor View用于裝飾ViewPager,它需要占用一些空間;另一種是普通的子View,也就是Adapter創(chuàng)建的View。
  • ViewPager布局處理主要是兩個方法onMeasure和onLayout

onMeasure(int widthMeasureSpec, int heightMeasureSpec)

該方法主要測量上述兩種子View,第一種Decor View就不多說了,用得很少,但要注意的是它會占用一部分ViewPager空間,剩下的留給普通子View;

主要說下普通子View測量,還記得上面提到的ItemInfo中widthFactor變量,它是通過Adapter.getPageWidth方法得來的(可以重寫),決定了頁面View的寬度,所以這里測量就用到了它;還有一點就是在測量之前會調(diào)用populate方法初始化需要顯示的頁面,然后再測量,看下面代碼片段

// Make sure we have created all fragments that we need to have shown.
// 計算并初始化需要顯示的頁面
mInLayout = true;
populate();
mInLayout = false;
...
if (lp == null || !lp.isDecor) {
    final int widthSpec = MeasureSpec.makeMeasureSpec(
            (int) (childWidthSize * lp.widthFactor), MeasureSpec.EXACTLY);
    child.measure(widthSpec, mChildHeightMeasureSpec);
}

這里用到了\color{#2233EE}{lp.widthFactor},就是ItemInfo.widthFactor,是在populate方法中設置給LayoutParams的,測量就到這里,比較簡單,下面看下onLayout方法;

onLayout(boolean changed, int l, int t, int r, int b)

跟onMeasure方法差不多

第一步先layout DecorView,它會占用一定空間,計算出四個padding值(paddingLeft、paddingTop、paddingRight、paddingBottom) 提供給后面layout普通子View使用;

第二步就是利用第一步得出的padding值得到一個可用的空間對子View進行布局,這里就涉及到對多個子View橫向排列順序的問題,這里就根據(jù)ItemInfo中的offset值來決定的,通過offset計算每個子View的Left值,關(guān)鍵代碼如下:

// 計算childLeft值
int loff = (int) (childWidth * ii.offset);
int childLeft = paddingLeft + loff;

// paddingTop就是上面說的第一步計算出來的padding值
int childTop = paddingTop;
...
// 布局子View
child.layout(childLeft, childTop, childLeft + child.getMeasuredWidth(), childTop + child.getMeasuredHeight());
整體來說ViewPager的布局流程也是非常簡單的,下面看事件處理

ViewPager 事件處理

  • ViewPager事件處理內(nèi)容不多,主要就是左右翻頁滑動的事件攔截,滑動事件又只需要攔截橫向滑動。
  • 事件攔截處理相關(guān)的幾個方法:dispatchTouchEvent,onInterceptTouchEvent,onTouchEvent;ViewPager這里只重寫了onInterceptTouchEvent和onTouchEvent

ViewPager.onInterceptTouchEvent

該方法主要計算是否攔截滑動事件變量:mIsBeingDragged,滿足下面2個主要條件才攔截:

  1. 當xDiff * 0.5f > yDiff,簡單說就是X軸上滑動的距離要大于Y軸上的2倍才攔截
  2. 并且canScroll(View v, boolean checkV, int dx, int x, int y)方法返回false,該方法是判斷子View能不能橫向滑動,如果子View能滑動ViewPager就不攔截滑動

ViewPager.onTouchEvent

該方法主要是通過上面計算出的mIsBeingDragged變量,判斷是否需要滑動操作,下面看下在不同MotionEvent中處理的內(nèi)容:

  • ACTION_MOVE:如果mIsBeingDragged = fasle,這里會重新計算,這里的判斷條件是xDiff > yDiff就能滑動了(跟onInterceptTouchEvent中處理不一樣,不知道為什么?),然后調(diào)用performDrag方法完成具體滑動操作
  • ACTION_UP:調(diào)用setCurrentItemInternal滑動到最終的頁面
  • ACTION_CANCEL:跟ACTION_UP差不多,調(diào)用scrollToItem完事
  • 其他滑動Event事件就不描述了,很簡單

相關(guān)內(nèi)容

未完待續(xù)...

以上基本就差不多了,剩下的看代碼吧

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容