乐趣区

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…
邮箱:yangchong211@163.com
阿里云博客: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…

退出移动版