前言
問ViewPager為何物魏烫?
谷歌文檔有云'Layout manager that allows the user to flip left and right through pages of data.'
提供左右切換功能的布局控制器是也
既然是控制器涂屁,必定要Adapter相以輔助,繼續(xù)翻閱文檔,只見
有PagerAdapter审胚,正是'Base class providing the adapter to populate pages inside of a ViewPager'
填充ViewPager內(nèi)部頁面數(shù)據(jù)的基類適配器是也
路子都引出來了委造,自然沿著文檔和源碼步步深入,一探究竟
從使用說起 先談適配器
谷歌很直接
When you implement a PagerAdapter, you must override the following methods at minimum
:
要想實現(xiàn)PagerAdapter,必須覆寫以下四個方法:
1.instantiateItem(ViewGroup container, int position)
:為容器指定位置創(chuàng)建頁面
2.destroyItem(ViewGroup container, int position, Object object)
:銷毀容器指定位置頁面
3.getCount()
:返回容器內(nèi)有效頁面數(shù)量
4.isViewFromObject(View view, Object object)
:判斷頁面視圖與instantiateItem
返回元素是否為同一視圖
-
一小段示例
private class MyPagerAdapter extends PagerAdapter {
@Override
public int getCount() {
return dataList== null ? 0 : dataList.size();
}
@Override
public Object instantiateItem(ViewGroup container, int pos) {
MyPageItem item = new MyPageItem ();
container.addView(item.mRootView);
return item;
}
@Override
public void destroyItem(ViewGroup container, int pos, Object o) {
MyPageItem item = (MyPageItem) o;
container.removeView(item.mRootView);
}
@Override
public boolean isViewFromObject(View view, Object o) {
MyPageItem item = (MyPageItem) o;
return view == item.mRootView;
}
}
-
關(guān)于刷新的“坑”
ViewPager一個眾所周知的問題--數(shù)據(jù)源發(fā)生變化后調(diào)用 notifyDataSetChanged()
团滥,視圖并不會立即刷新
雖然是谷歌為節(jié)省資源開銷竿屹,為ViewPager承載大圖的特點專門設(shè)計,初次碰到也著實犯難灸姊。
從源碼看起拱燃,一步一問
首先,直奔ViewPager的適配器--PagerAdapter力惯,查看notifyDataSetChanged
方法
//PagerAdapter
public void notifyDataSetChanged() {
mObservable.notifyChanged();
}
//DataSetObservable
public void notifyChanged() {
synchronized(mObservers) {
for (int i = mObservers.size() - 1; i >= 0; i--) {
mObservers.get(i).onChanged();
}
}
}
好嘛碗誉,還是熟悉的觀察者模式,回調(diào)onChanged
方法
那ViewPager中是如何定義Observer類的呢父晶?
private class PagerObserver extends DataSetObserver {
@Override
public void onChanged() {
dataSetChanged();
}
@Override
public void onInvalidated() {
dataSetChanged();
}
}
矛頭都指向dataSetChanged()
哮缺,看來刷新玄機暗藏其中
這里暫時只留下解決刷新疑問的相關(guān)代碼
void dataSetChanged() {
··· ···
//遍歷容器中的元素
for (int i = 0; i < mItems.size(); i++) {
final ItemInfo ii = mItems.get(i);
// 返回元素相應(yīng)位置是否發(fā)生變化的標(biāo)志
// POSITION_UNCHANGED = -1;
// POSITION_NONE = -2;
final int newPos = mAdapter.getItemPosition(ii.object);
// 若返回POSITION_UNCHANGED,跳過
if (newPos == PagerAdapter.POSITION_UNCHANGED) {
continue;
}
if (newPos == PagerAdapter.POSITION_NONE) {
// 返回POSITION_NONE時移除元素并記錄標(biāo)志
// 這里對元素先移除甲喝,后重新加載
mItems.remove(i);
i--;
if (!isUpdating) {
mAdapter.startUpdate(this);
isUpdating = true;
}
mAdapter.destroyItem(this, ii.position, ii.object);
needPopulate = true;
if (mCurItem == ii.position) {
// Keep the current item in the valid range
newCurrItem = Math.max(0, Math.min(mCurItem, adapterCount - 1));
needPopulate = true;
}
continue;
}
··· ···
// 重新加載來了
setCurrentItemInternal(newCurrItem, false, true);
requestLayout();
}
看來尝苇,是否更新,由getItemPosition
的返回值決定啊埠胖,那getItemPosition
方法內(nèi)部又是怎樣實現(xiàn)的呢糠溜?
public int getItemPosition(Object object) {
return POSITION_UNCHANGED;
}
沒錯,默認返回POSITION_UNCHANGED
直撤,原來如此非竿!
那我們重寫getItemPosition
方法,讓其返回POSITION_NONE
刷新不就能生效了嘛谋竖?
答案是肯定的红柱。
但同時要注意,全部返回POSITION_NONE
意味著要刷新所有元素蓖乘,是灰常浪費資源的锤悄,畢竟谷歌這么設(shè)計也總有道理。
一個稍加優(yōu)化的思路是初始化時為頁面設(shè)置tag驱敲,在getItemPosition
方法中根據(jù)tag判斷僅更新當(dāng)前頁面視圖
@Override
public int getItemPosition(Object object) {
MyPageItem v = (MyPageItem) object;
if (v == null || v.mRoot == null){
return POSITION_UNCHANGED;
}
int position = (int) v.mRootView.getTag();
return mCurPageIndex == position ? POSITION_NONE : POSITION_UNCHANGED;
}
說回ViewPager
- 我們繼續(xù)一步一問铁蹈,提出幾個重要方法一探。
先看看instantiateItem
在ViewPager中的調(diào)用
ItemInfo addNewItem(int position, int index) {
ItemInfo ii = new ItemInfo();
ii.position = position;
ii.object = mAdapter.instantiateItem(this, position);
ii.widthFactor = mAdapter.getPageWidth(position);
if (index < 0 || index >= mItems.size()) {
mItems.add(ii);
} else {
mItems.add(index, ii);
}
return ii;
}
嗯众眨,添加新元素握牧,顧名可以思義。但是該方法在何處調(diào)用娩梨,返回值又怎么使用呢沿腰?
都是在
populate
中調(diào)用,看來是個很厲害的方法了狈定!
void populate(int newCurrentItem) {
ItemInfo oldCurInfo = null;
int focusDirection = View.FOCUS_FORWARD;
if (mCurItem != newCurrentItem) {
focusDirection = mCurItem < newCurrentItem ? View.FOCUS_RIGHT : View.FOCUS_LEFT;
// 獲取舊元素信息
oldCurInfo = infoForPosition(mCurItem);
// 更新當(dāng)前視圖index
mCurItem = newCurrentItem;
}
if (mAdapter == null) {
// 視圖位置重排
sortChildDrawingOrder();
return;
}
// 若滑動未停止則暫停populate操作防止出現(xiàn)問題
if (mPopulatePending) {
if (DEBUG) Log.i(TAG, "populate is pending, skipping for now...");
sortChildDrawingOrder();
return;
}
// 若視圖未依附于窗口則暫停populate操作
if (getWindowToken() == null) {
return;
}
mAdapter.startUpdate(this);
// mOffscreenPageLimit為設(shè)定的預(yù)加載數(shù)颂龙,具體下邊說
// 根據(jù)當(dāng)前視圖位置和預(yù)加載數(shù)計算填充位置的起始點和終結(jié)點
final int pageLimit = mOffscreenPageLimit;
final int startPos = Math.max(0, mCurItem - pageLimit);
final int N = mAdapter.getCount();
final int endPos = Math.min(N-1, mCurItem + pageLimit);
if (N != mExpectedAdapterCount) {
String resName;
try {
resName = getResources().getResourceName(getId());
} catch (Resources.NotFoundException e) {
resName = Integer.toHexString(getId());
}
throw new IllegalStateException("The application's PagerAdapter changed the adapter's" +
" contents without calling PagerAdapter#notifyDataSetChanged!" +
" Expected adapter item count: " + mExpectedAdapterCount + ", found: " + N +
" Pager id: " + resName +
" Pager class: " + getClass() +
" Problematic adapter: " + mAdapter.getClass());
}
// 在內(nèi)存中定位所需視圖元素习蓬,若不存在則重新添加
int curIndex = -1;
ItemInfo curItem = null;
for (curIndex = 0; curIndex < mItems.size(); curIndex++) {
final ItemInfo ii = mItems.get(curIndex);
if (ii.position >= mCurItem) {
if (ii.position == mCurItem) curItem = ii;
break;
}
}
if (curItem == null && N > 0) {
// 終于看到了addNewItem,若當(dāng)前需填充的元素不在內(nèi)存中則通過addNewItem調(diào)用instantiateItem加載
curItem = addNewItem(mCurItem, curIndex);
}
// Fill 3x the available width or up to the number of offscreen
// pages requested to either side, whichever is larger.
// If we have no current item we have no work to do.
if (curItem != null) {
float extraWidthLeft = 0.f;
// 當(dāng)前視圖左邊的元素
int itemIndex = curIndex - 1;
ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
final int clientWidth = getClientWidth();
// 計算左側(cè)預(yù)加載視圖寬度
final float leftWidthNeeded = clientWidth <= 0 ? 0 :
2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth;
// 遍歷當(dāng)前視圖左邊的所有元素
for (int pos = mCurItem - 1; pos >= 0; pos--) {
// 若該元素在預(yù)加載范圍外
if (extraWidthLeft >= leftWidthNeeded && pos < startPos) {
if (ii == null) {
break;
}
// 移除該頁面元素
if (pos == ii.position && !ii.scrolling) {
mItems.remove(itemIndex);
mAdapter.destroyItem(this, pos, ii.object);
if (DEBUG) {
Log.i(TAG, "populate() - destroyItem() with pos: " + pos +
" view: " + ((View) ii.object));
}
itemIndex--;
curIndex--;
ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
}
} else if (ii != null && pos == ii.position) {
// 若該左側(cè)元素在內(nèi)存中措嵌,則更新記錄
extraWidthLeft += ii.widthFactor;
itemIndex--;
ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
} else {
// 若該左側(cè)元素不在內(nèi)存中躲叼,則重新添加,再一次來到了addNewItem
ii = addNewItem(pos, itemIndex + 1);
extraWidthLeft += ii.widthFactor;
curIndex++;
ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
}
}
// 來到當(dāng)前視圖右側(cè)企巢,思路大致和左側(cè)相同
float extraWidthRight = curItem.widthFactor;
itemIndex = curIndex + 1;
if (extraWidthRight < 2.f) {
ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
final float rightWidthNeeded = clientWidth <= 0 ? 0 :
(float) getPaddingRight() / (float) clientWidth + 2.f;
for (int pos = mCurItem + 1; pos < N; pos++) {
if (extraWidthRight >= rightWidthNeeded && pos > endPos) {
if (ii == null) {
break;
}
if (pos == ii.position && !ii.scrolling) {
mItems.remove(itemIndex);
mAdapter.destroyItem(this, pos, ii.object);
if (DEBUG) {
Log.i(TAG, "populate() - destroyItem() with pos: " + pos +
" view: " + ((View) ii.object));
}
ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
}
} else if (ii != null && pos == ii.position) {
extraWidthRight += ii.widthFactor;
itemIndex++;
ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
} else {
ii = addNewItem(pos, itemIndex);
itemIndex++;
extraWidthRight += ii.widthFactor;
ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
}
}
}
// 計算頁面偏移量
calculatePageOffsets(curItem, curIndex, oldCurInfo);
}
if (DEBUG) {
Log.i(TAG, "Current page list:");
for (int i=0; i<mItems.size(); i++) {
Log.i(TAG, "#" + i + ": page " + mItems.get(i).position);
}
}
mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null);
mAdapter.finishUpdate(this);
// 遍歷子視圖枫慷,若寬度不合法則重繪
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
lp.childIndex = i;
if (!lp.isDecor && lp.widthFactor == 0.f) {
// 0 means requery the adapter for this, it doesn't have a valid width.
final ItemInfo ii = infoForChild(child);
if (ii != null) {
lp.widthFactor = ii.widthFactor;
lp.position = ii.position;
}
}
}
sortChildDrawingOrder();
if (hasFocus()) {
View currentFocused = findFocus();
ItemInfo ii = currentFocused != null ? infoForAnyChild(currentFocused) : null;
if (ii == null || ii.position != mCurItem) {
for (int i=0; i<getChildCount(); i++) {
View child = getChildAt(i);
ii = infoForChild(child);
if (ii != null && ii.position == mCurItem) {
if (child.requestFocus(focusDirection)) {
break;
}
}
}
}
}
}
方法較長,理解的甚為粗淺浪规,還望大神指點或听。
- 另外,作為支持“左右切換功能”的布局管理器笋婿,谷歌也為其配套提供了“預(yù)加載”機制誉裆,防止下一頁內(nèi)容加載不及時影響體驗。
設(shè)置預(yù)加載數(shù)量viewPager.setOffscreenPageLimit(2)
(括號內(nèi)數(shù)字代表當(dāng)前元素左右兩邊各需預(yù)加載的數(shù)量)
源碼內(nèi)部是這樣定義setOffscreenPageLimit
方法的缸濒,詳見注釋足丢。
// DEFAULT_OFFSCREEN_PAGES = 1 默認預(yù)加載數(shù)為1
public void setOffscreenPageLimit(int limit) {
// 若用戶設(shè)置的預(yù)加載數(shù)量小于1,則重置為默認值
if (limit < DEFAULT_OFFSCREEN_PAGES) {
Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to " +
DEFAULT_OFFSCREEN_PAGES);
limit = DEFAULT_OFFSCREEN_PAGES;
}
if (limit != mOffscreenPageLimit) {
// 設(shè)定預(yù)加載數(shù)绍填,填充頁面視圖
mOffscreenPageLimit = limit;
populate();
}
}
最后
ViewPager玄機深霎桅,本文僅僅窺一斑栖疑。往后會繼續(xù)叨叨ViewPager(二)讨永,理解越深用著越順。進擊遇革!