PagerAdapter深度解析和實踐優化

目錄介紹

01.PagerAdapter簡單介紹
02.PagerAdapter抽象方法
03.PagerAdapter原理介紹
04.PagerAdapter緩存和銷燬
05.自定義PagerAdapter
06.PagerAdapter兩個子類
07.三種Adapter的總結
00.ViewPager相關

ViewPager懶加載:https://juejin.im/post/5d37bb...
這篇博客是接着上一篇繼續分析和實踐優化的。
01.PagerAdapter簡單介紹

使用場景
輪播圖:ViewPager+自定義PagerAdapter
fragment:TabLayout+ViewPager+FragmentPagerAdapter+Fragment
02.PagerAdapter抽象方法

子類繼承PagerAdapter需要實現方法說明
Object instantiateItem(ViewGroup container, int position)
一句話:要顯示的頁面或需要緩存的頁面,會調用這個方法進行佈局的初始化。
這個方法是ViewPager需要加載某個頁面時調用,container就是ViewPager自己,position頁面索引;
我們需要實現的是添加一個view到container中,然後返回一個跟這個view能夠關聯起來的對象,這個對象可以是view自身,也可以是其他對象(比如FragmentPagerAdapter返回的就是一個Fragment),關鍵是在isViewFromObject能夠將view和這個object關聯起來
void destroyItem(ViewGroup container, int position, Object object)
一句話:當ViewPager需要銷燬一個頁面時調用,我們需要將position對應的view從container中移除。
這時參數除了position就只有object,其實就是上面instantiateItem方法返回的對象,這時要通過object找到對應的View,然後將其移除掉,如果你的instantiateItem方法返回的就是View,這裏就直接強轉成View移除即可:container.removeView((View) object);如果不是,一般會自己創建一個List緩存view列表,然後根據position從List中找到對應的view移除;(當然你也可以不移除,內存泄漏)。
FragmentPagerAdapter的實現是:mCurTransaction.detach((Fragment)object),其實也就是將fragemnt的view從container中移除
isViewFromObject(View view, Object object)
一句話:這個方法用於判斷是否由對象生成界面,官方建議直接返回 return view == object;。
從名稱理解起來像是判斷view是否來自object,跟進一步解釋應該是上面instantiateItem方法中
向container中添加的view和方法返回的對象兩者之間一對一的關係;因爲在ViewPager內部有個方法叫infoForChild,
這個方法是通過view去找到對應頁面信息緩存類ItemInfo(內部調用了isViewFromObject),如果找不到,說明這個view是個野孩子,ViewPager會認爲不是Adapter提供的View,所以這個View不會顯示出來;
總結一下:isViewFromObject 方法是讓view和object(內部爲ItemInfo)一一對應起來
int getItemPosition(Object object)
改方法是判斷當前object對應的View是否需要更新,在調用notifyDataSetChanged時會間接觸發該方法,
如果返回POSITION_UNCHANGED表示該頁面不需要更新,如果返回POSITION_NONE則表示該頁面無效了,需要銷燬並觸發destroyItem方法(並且有可能調用instantiateItem重新初始化這個頁面)
02.PagerAdapter原理介紹

ViewPager+PagerAdapter的合作關係:
ViewPager來控制一頁界面構造和銷燬的時機,使用回調來通知PagerAdapter具體做什麼,PagerAdapter只需要按照相應的步驟做。當然爲了使用得更好、提供更多的功能,又建議了使用View的回收工作和管理工作,同時提供當數據改變時的界面刷新工作。
instantiateItem(ViewGroup, int):
構造指定位置的頁面。adapter負責在這個方法中添加view到容器中,即使是在finishUpdate(ViewGroup)才保證完成的。在FragmentPagerAdapter和FragmentStatePagerAdapter中,都是返回一個構造的Fragment.
destroyItem(ViewGroup, populate, Object):
移除指定位置的頁面。adapter負責從容器中移除view,即是最後實在finishUpdate(ViewGroup)保證完成的。在FragmentPagerAdapter和FragmentStatePagerAdapter中,分別使用FragmentTransition.detach(Fragment)和FragmentTransition.remove(Fragment)來邏輯上銷燬Fragment.
finishUpdate(ViewGroup):
當頁面的顯示變化完成式調用。在這裏,你一定保證所有的頁面從容器中合理的添加或移除掉。
setPrimaryItem(ViewGroup, int, Object):
被ViewPager調用來通知adapter此時那個item應該被認爲是主要的頁面,這個頁面將在當前頁面展示給用戶。正是因爲這個方法,纔有在ViewPager中實現Fragment懶加載的機制。
isViewFromObject(View, Object):
指定當前頁面View是否和指定的key對象相關聯(這個key對象是在instantiateItem(ViewGroup, int)方法返回的)。這個方法需要PagerAdapter恰當的實現。即只要匹配好鍵值對即可。FragmentPagerAdapter和FragmentStatePagerAdapter的實現: return ((Fragment)object).getView() == view;.
雖然簡單或很少使用到的一些方法不想細究,不過還是一次性分析完爲好,如getPageTitle(int), getPageWidth(int), getItemPosition(Object)等。
getPageTitle(int): 返回每頁的標題,多用於關聯indicator
getPageWidth(int): 返回指定的頁面相對於ViewPager寬度的比例,範圍(0.f-1.f]。默認值爲1.f, 即佔滿整個屏幕。如果是0.5f, 那麼在初始狀態下,默認會出現前兩個頁面,而primary主頁面是在ViewPager的起始位置(通常是屏幕左側),直到最後一個頁面在屏幕右側,如果總共5個頁面,返回值爲0.2f, 那麼將一次性出現所有的頁面.
getItemPosition(Object):
用於數據刷新時的頁面處理方式。返回值包括三類:POSITION_UNCHANGED表示位置沒有變化,即在添加或移除一頁或多頁之後該位置的頁面保持不變,可以用於一個ViewPager中最後幾頁的添加或移除時,保持前幾頁仍然不變;POSITION_NONE,表示當前頁不再作爲ViewPager的一頁數據,將被銷燬,可以用於無視View緩存的刷新;根據傳過來的參數Object來判斷這個key所指定的新的位置
04.PagerAdapter緩存和銷燬

在ViewPager三種Adapter的子view創建和銷燬的方法添加相關的日誌代碼,如下:
@Override
public void destroyItem(ViewGroup container, int position, Object object) {

Log.d("yc", "destroyItem:" + position);
//...省略部分代碼

}

@Override
public Object instantiateItem(ViewGroup container, int position) {

Log.d("yc", "instantiateItem:" + position);
//...省略部分代碼

}
滑動ViewPager翻頁,觀察控制檯的輸出,三種Adapter針對不同界面、不同滑動方向的翻頁情況打印如下:

從圖中我們可以看到,三種Adapter在相同的情況下,ViewPager的子頁面銷燬和創建時機是一樣。通常所聽到的都是FragmentPagerAdapter會緩存所有的Fragment子項,而上圖中我們看到的是在滑動的過程中它的destroyItem方法被調用了,而在滑動回來時相對應的子項Fragment也確實調用instantiateItem方法。這樣看來根本就沒有緩存……
但是仔細對比了一下三個Adapter創建視圖的過程,發現上面推論有所欠缺。
因爲在使用Fragment作爲子視圖時,我們是通過getItem方法返回Fragment的,單純從這裏打印instantiateItem的調用不代表Fragment真的完全被重新創建了(重新創建代表需要重新add,即從頭走一遍生命週期,但是在這裏不能證明),也可以通過兩個FragmentAdapter中instantiateItem的實現證明(觀察getItem方法的調用條件),所以又在Fragment對應的兩種Adapter的getItem中添加相應的log代碼,如下:
@Override
public Fragment getItem(int position) {

Log.d("ccc", "getItem:" + position);
return fragmentList.get(position);

}
針對不同情況,控制檯輸出結果如下:

通過上圖我們可以看到,FragmentPagerAdapter在最後向右邊劃回來時並沒有調用getItem方法(getItem是創建一個新的Fragment),這也就說明了他沒有重新創建Fragment,證明了它會緩存所有Fragment,那麼它到底在哪裏做了緩存呢?具體看FragmentPagerAdapter分析……
05.自定義PagerAdapter

比如,引導頁使用ViewPager,這個時候動態管理的Adapter,可以每次都會創建新view,銷燬舊View。節省內存消耗性能。可以說下面這種用的最多……
/**

  • <pre>
  • @author yangchong
  • blog : https://github.com/yangchong211
  • time : 2016/3/18
  • desc : 動態管理的Adapter。概念參照{@link android.support.v4.app.FragmentPagerAdapter}
  • 每次都會創建新view,銷燬舊View。節省內存消耗性能
  • revise: 比如使用場景是啓動引導頁
  • </pre>

*/
public abstract class AbsDynamicPagerAdapter extends PagerAdapter {

@Override
public boolean isViewFromObject(@NonNull View arg0, @NonNull Object arg1) {
    return arg0==arg1;
}

@Override
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
    container.removeView((View) object);
}

@Override
public int getItemPosition(@NonNull Object object) {
    return super.getItemPosition(object);
}

@NonNull
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
    View itemView = getView(container,position);
    container.addView(itemView);
    return itemView;
}

/**
 * 創建view
 * @param container                    container
 * @param position                    索引
 * @return
 */
public abstract View getView(ViewGroup container, int position);

}
比如,常見有無限輪播圖,可以自動輪播,大家應該用的特別多。這個時候可以優化自定義輪播圖的PagerAdapter,創建集合用來存儲view,再次用的時候先取集合,沒有就創建。而不是頻繁創建視圖。
/**

  • <pre>
  • @author yangchong
  • blog : https://github.com/yangchong211
  • time : 2016/3/18
  • desc : AbsLoopPagerAdapter
  • revise: 如果是自動輪播圖的話就用這一個
  • </pre>

*/
public abstract class AbsLoopPagerAdapter extends PagerAdapter {

private BannerView mViewPager;
/**
 * 用來存放View的集合
 */
private ArrayList<View> mViewList = new ArrayList<>();
/**
 * 刷新全部
 */
@Override
public void notifyDataSetChanged() {
    mViewList.clear();
    initPosition();
    super.notifyDataSetChanged();
}

/**
 * 獲取item索引
 *
 * POSITION_UNCHANGED表示位置沒有變化,即在添加或移除一頁或多頁之後該位置的頁面保持不變,
 * 可以用於一個ViewPager中最後幾頁的添加或移除時,保持前幾頁仍然不變;
 *
 * POSITION_NONE,表示當前頁不再作爲ViewPager的一頁數據,將被銷燬,可以用於無視View緩存的刷新;
 * 根據傳過來的參數Object來判斷這個key所指定的新的位置
 * @param object                        objcet
 * @return
 */
@Override
public int getItemPosition(@NonNull Object object) {
    return POSITION_NONE;
}

/**
 * 註冊數據觀察者監聽
 * @param observer                      observer
 */
@Override
public void registerDataSetObserver(@NonNull DataSetObserver observer) {
    super.registerDataSetObserver(observer);
    initPosition();
}

private void initPosition(){
    if (getRealCount()>1){
        if (mViewPager.getViewPager().getCurrentItem() == 0&&getRealCount()>0){
            int half = Integer.MAX_VALUE/2;
            int start = half - half%getRealCount();
            setCurrent(start);
        }
    }
}

/**
 * 設置位置,利用反射實現
 * @param index                         索引
 */
@TargetApi(Build.VERSION_CODES.KITKAT)
private void setCurrent(int index){
    try {
        Field field = ViewPager.class.getDeclaredField("mCurItem");
        field.setAccessible(true);
        field.set(mViewPager.getViewPager(),index);
    } catch (NoSuchFieldException | IllegalAccessException e) {
        e.printStackTrace();
    }
}

public AbsLoopPagerAdapter(BannerView viewPager){
    this.mViewPager = viewPager;
}

@Override
public boolean isViewFromObject(@NonNull View arg0, @NonNull Object arg1) {
    return arg0==arg1;
}

/**
 * 如果頁面不是當前顯示的頁面也不是要緩存的頁面,會調用這個方法,將頁面銷燬。
 * @param container                     container
 * @param position                      索引
 * @param object                        object
 */
@Override
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
    container.removeView((View) object);
    Log.d("PagerAdapter","銷燬的方法");
}

/**
 *  要顯示的頁面或需要緩存的頁面,會調用這個方法進行佈局的初始化。
 * @param container                     container
 * @param position                      索引
 * @return
 */
@NonNull
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
    int realPosition = position%getRealCount();
    View itemView = findViewByPosition(container,realPosition);
    container.addView(itemView);
    Log.d("PagerAdapter","創建的方法");
    return itemView;
}

/**
 * 這個是避免重複創建,如果集合中有,則取集合中的
 * @param container                     container
 * @param position                      索引
 * @return
 */
private View findViewByPosition(ViewGroup container, int position){
    for (View view : mViewList) {
        if (((int)view.getTag()) == position&&view.getParent()==null){
            return view;
        }
    }
    View view = getView(container,position);
    view.setTag(position);
    mViewList.add(view);
    return view;
}


@Deprecated
@Override
public final int getCount() {
    //設置最大輪播圖數量 ,如果是1那麼就是1,不輪播;如果大於1則設置一個最大值,可以輪播
    //return getRealCount();
    return getRealCount()<=1?getRealCount(): Integer.MAX_VALUE;
}

/**
 * 獲取輪播圖數量
 * @return                          數量
 */
public abstract int getRealCount();

/**
 * 創建view
 * @param container                 viewGroup
 * @param position                  索引
 * @return
 */
public abstract View getView(ViewGroup container, int position);

}
還有一種場景,靜態輪播圖,也就是不會自動輪播,但是手指可以滑動,並且滑動到第一張不能往左滑動,滑動到最後一張不能向右滑動。這種場景,view添加進去就不管了,View就常在呢!
/**

  • <pre>
  • @author yangchong
  • blog : https://github.com/yangchong211
  • time : 2016/3/18
  • desc : 靜態存儲的Adapter,概念參照{@link android.support.v4.app.FragmentStatePagerAdapter}
  • view添加進去就不管了,View長在,內存不再
  • revise: 如果是靜態輪播圖就用這個
  • </pre>

*/
public abstract class AbsStaticPagerAdapter extends PagerAdapter {

private ArrayList<View> mViewList = new ArrayList<>();

@Override
public boolean isViewFromObject(@NonNull View arg0, @NonNull Object arg1) {
    return arg0==arg1;
}

@Override
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
    container.removeView((View) object);
    Log.d("PagerAdapter","銷燬的方法");
}

@Override
public void notifyDataSetChanged() {
    mViewList.clear();
    super.notifyDataSetChanged();
}

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

@NonNull
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
    View itemView = findViewByPosition(container,position);
    container.addView(itemView);
    onBind(itemView,position);
    Log.d("PagerAdapter","創建的方法");
    return itemView;
}

private View findViewByPosition(ViewGroup container, int position){
    for (View view : mViewList) {
        if (((int)view.getTag()) == position&&view.getParent()==null){
            return view;
        }
    }
    View view = getView(container,position);
    view.setTag(position);
    mViewList.add(view);
    return view;
}


public void onBind(View view, int position){}

public abstract View getView(ViewGroup container, int position);

}
這三種不同的使用場景,我們應該都見到過,那麼自定義adpater的時候能否再優化一下,ok,上面的方案剛好合適。如果有不同的想法,歡迎提出……該源代碼的開源地址:https://github.com/yangchong2...
06.PagerAdapter兩個子類

PagerAdapter 的兩個直接子類 FragmentPagerAdapter 和 FragmentStatePagerAdapter 。而我們常常會在 ViewPager 和 Fragment 結合使用的時候來使用這兩個適配器。
6.1 FragmentPagerAdapter

FragmentPagerAdapter 它將每一個頁面表示爲一個 Fragment,並且每一個 Fragment 都將會保存到 FragmentManager 當中。而且,當用戶沒可能再次回到頁面的時候,FragmentManager 纔會將這個 Fragment 銷燬。
FragmentPagerAdapter:對於不再需要的 fragment,選擇調用 onDetach() 方法,僅銷燬視圖,並不會銷燬 fragment 實例。
使用 FragmentPagerAdapter 需要實現兩個方法:
public Fragment getItem(int position) 返回的是對應的 Fragment 實例,一般我們在使用時,會通過構造傳入一個要顯示的 Fragment 的集合,我們只要在這裏把對應的 Fragment 返回就行了。
public int getCount() 這個上面介紹過了返回的是頁面的個數,我們只要返回傳入集合的長度就行了。
使用起來是非常簡單的,FragmentStatePagerAdapter 的使用也和上面一樣,那兩者到底有什麼區別呢?
錯誤說法
超出範圍的Fragment會被銷燬。所以之前,我一直認爲的是,FragmentPagerAdapter中通常最多會保留3個Fragment, 超出左右兩側的Fragment將被銷燬,滑動到時又會被重新構造。
PagerAdapter的實現類,使用將一直保留在FragmentManager中的Fragment來代表每一頁,直到用戶返回上一頁。
當用於典型地使用多靜態化的Fragment時,FragmentPagerAdapter無疑是最好使用的,例如一組tabs. 每個用戶訪問過的頁面的Fragment都將會保留在內存中,即使它的視圖層在不可見時已經被銷燬。這可能導致使用比較大數量的內存,因爲Fragment實例持有任意數量的狀態。如果使用大數據的頁面,考慮使用FragmentStatePagerAdapter.
從上面可以看出,即使是超出可視範圍和緩存範圍之外的Fragment,它的視圖將會被銷燬,但是它的實例將會保留在內存中,所以每一頁的Fragment至始至終都只需要構造一次而已。通常是在主頁中使用FragmentPagerAdapter, 但是超出範圍的Fragment的視圖會被銷燬,我們也可以在Fragment中緩存View來避免狀態的丟失,也可以使用另外的機制,如緩存View的狀態。
@Override
public Object instantiateItem(ViewGroup container, int position) {

if (mCurTransaction == null) {
    mCurTransaction = mFragmentManager.beginTransaction();
}

final long itemId = getItemId(position);

// Do we already have this fragment?
String name = makeFragmentName(container.getId(), itemId);
Fragment fragment = mFragmentManager.findFragmentByTag(name);
if (fragment != null) {
    if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
    mCurTransaction.attach(fragment);
} else {
    fragment = getItem(position);
    if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment);
    mCurTransaction.add(container.getId(), fragment,
            makeFragmentName(container.getId(), itemId));
}
if (fragment != mCurrentPrimaryItem) {
    fragment.setMenuVisibility(false);
    fragment.setUserVisibleHint(false);
}

return fragment;

}

@Override
public void destroyItem(ViewGroup container, int position, Object object) {

if (mCurTransaction == null) {
    mCurTransaction = mFragmentManager.beginTransaction();
}
if (DEBUG) Log.v(TAG, "Detaching item #" + getItemId(position) + ": f=" + object
        + " v=" + ((Fragment)object).getView());
mCurTransaction.detach((Fragment)object);

}
從上面源碼可以得出結論
當被銷燬時,Fragment並沒有從FragmentTransition中移除,而是調用了FragmentTransition.detach(Fragment)方法,這樣銷燬了Fragment的視圖,但是沒有移除Fragment本身。
detach:對應執行的是Fragment生命週期中onPause()-onDestroyView()的方法,此時並沒有執行onDestroy和onDetach方法。所以在恢復時只需要attach方法即可(可以在FragmentPagerAdapter的instantiateItem方法中看到調用,對應源碼下面給出),attach方法對應的是執行Fragment生命週期中onCreateView()-onResume()。
6.2 FragmentStatePagerAdapter

FragmentStatePagerAdapter:會銷燬不再需要的 fragment,噹噹前事務提交以後,會徹底的將 fragmeng 從當前 Activity 的FragmentManager 中移除,state 標明,銷燬時,會將其 onSaveInstanceState(Bundle outState) 中的 bundle 信息保存下來,當用戶切換回來,可以通過該 bundle 恢復生成新的 fragment,也就是說,你可以在 onSaveInstanceState(Bundle outState) 方法中保存一些數據,在 onCreate 中進行恢復創建。
使用 FragmentStatePagerAdapter 更省內存,但是銷燬後新建也是需要時間的。一般情況下,如果你是製作主頁面,就 3、4 個 Tab,那麼可以選擇使用 FragmentPagerAdapter,如果你是用於 ViewPager 展示數量特別多的條目時,那麼建議使用 FragmentStatePagerAdapter。
PagerAdapter的實現類,使用Fragment來管理每一頁。這個類也會管理保存和恢復Fragment的狀態。
當使用一個大數量頁面時,FragmentStatePagerAdapter將更加有用,工作機制類似於ListView. 當每頁不再可見時,整個Fragment將會被銷燬,只保留Fragment的狀態。相對於FragmentPagerAdapter, 這個將允許頁面持有更少的內存。
@Override
public Object instantiateItem(ViewGroup container, int position) {

// If we already have this item instantiated, there is nothing
// to do.  This can happen when we are restoring the entire pager
// from its saved state, where the fragment manager has already
// taken care of restoring the fragments we previously had instantiated.
if (mFragments.size() > position) {
    Fragment f = mFragments.get(position);
    if (f != null) {
        return f;
    }
}

if (mCurTransaction == null) {
    mCurTransaction = mFragmentManager.beginTransaction();
}

Fragment fragment = getItem(position);
if (DEBUG) Log.v(TAG, "Adding item #" + position + ": f=" + fragment);
if (mSavedState.size() > position) {
    Fragment.SavedState fss = mSavedState.get(position);
    if (fss != null) {
        fragment.setInitialSavedState(fss);
    }
}
while (mFragments.size() <= position) {
    mFragments.add(null);
}
fragment.setMenuVisibility(false);
fragment.setUserVisibleHint(false);
mFragments.set(position, fragment);
mCurTransaction.add(container.getId(), fragment);

return fragment;

}

@Override
public void destroyItem(ViewGroup container, int position, Object object) {

Fragment fragment = (Fragment) object;

if (mCurTransaction == null) {
    mCurTransaction = mFragmentManager.beginTransaction();
}
if (DEBUG) Log.v(TAG, "Removing item #" + position + ": f=" + object
        + " v=" + ((Fragment)object).getView());
while (mSavedState.size() <= position) {
    mSavedState.add(null);
}
mSavedState.set(position, fragment.isAdded()
        ? mFragmentManager.saveFragmentInstanceState(fragment) : null);
mFragments.set(position, null);

mCurTransaction.remove(fragment);

}
從源碼可以看出,當銷燬Fragment時,緩存了Fragment的狀態,並移除了Fragment的引用。而在構造時,顯示判斷是否已經在構造,如果是則直接返回該Fragment, 如果不是,則重新構造一個新的Fragment, 並且如果已經緩存了狀態,則將改狀態傳入Fragment用於恢復狀態。
07.三種Adapter的總結

三種Adapter的緩存策略
PagerAdapter:緩存三個,通過重寫instantiateItem和destroyItem達到創建和銷燬view的目的。
FragmentPagerAdapter:內部通過FragmentManager來持久化每一個Fragment,在destroyItem方法調用時只是detach對應的Fragment,並沒有真正移除!
FragmentPagerStateAdapter:內部通過FragmentManager來管理每一個Fragment,在destroyItem方法,調用時移除對應的Fragment。
三個Adapter使用場景分析
PagerAdapter:當所要展示的視圖比較簡單時適用
FragmentPagerAdapter:當所要展示的視圖是Fragment,並且數量比較少時適用
FragmentStatePagerAdapter:當所要展示的視圖是Fragment,並且數量比較多時適用
其他介紹

01.關於博客彙總鏈接

1.技術博客彙總
2.開源項目彙總
3.生活博客彙總
4.喜馬拉雅音頻彙總
5.其他彙總
02.關於我的博客

github:https://github.com/yangchong211
知乎:https://www.zhihu.com/people/...
簡書:http://www.jianshu.com/u/b7b2...
csdn:http://my.csdn.net/m0_37700275
喜馬拉雅聽書:http://www.ximalaya.com/zhubo...
開源中國:https://my.oschina.net/zbj161...
泡在網上的日子:http://www.jcodecraeer.com/me...
郵箱:[email protected]
阿里雲博客:https://yq.aliyun.com/users/a... 239.headeruserinfo.3.dT4bcV
segmentfault頭條:https://segmentfault.com/u/xi...
掘金:https://juejin.im/user/593943...
狀態管理器項目地址:https://github.com/yangchong2...

自定義PagerAdapter輪播圖案例:https://github.com/yangchong2...

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