一、前言
上一篇文,Fragment 与 FragmentPagerAdapter (一) 描述了
Fragment的系统回收以及初始化
-
FragmentPagerAdapter
和FragmentStatePagerAdapter
的区别
这一篇主要还是来说说PagerAdapter
的基础分析,以及关于无限循环的ViewPager
的实战。
虽然这是第二篇,其实这篇反而偏基础点,多是讲PagerAdapter中各个接口的作用。
二、PagerAdapter 简单分析
用多了RecycleView
的人都知道,是有个BaseAdapter
与RecycleView
绑定。
ViewPager
是同样的原理,ViewPager
是一个ViewGroup
,缓存有多个子View(页面),而适配器类PagerAdapter
则负责为每个页面来提供数据,通过几个简单的接口,即可实现复杂功能而无须接触ViewPager里面繁而又繁的复杂逻辑计算代码。
ViewPager.setAdapter()
的时候,向PagerAdapter
注册一个观察者,即调用: mAdapter.setViewPagerObserver(mObserver);
当数据发生更新时,也就是调用 adapter.notifyDataSetChange()
,ViewPager
就会接受到通知,从而刷新界面使adapter重新生成数据(或者说提供一个新的界面)。
class ViewPager
public void setAdapter(@Nullable PagerAdapter adapter) {
if (mAdapter != null) {
mAdapter.setViewPagerObserver(null);
mAdapter.startUpdate(this);
for (int i = 0; i < mItems.size(); i++) {
final ItemInfo ii = mItems.get(i);
mAdapter.destroyItem(this, ii.position, ii.object);
}
mAdapter.finishUpdate(this);
...
}
final PagerAdapter oldAdapter = mAdapter;
mAdapter = adapter;
mExpectedAdapterCount = 0;
if (mAdapter != null) {
if (mObserver == null) {
mObserver = new PagerObserver();
}
mAdapter.setViewPagerObserver(mObserver);
...
}
...
}
class PagerAdapter {
private final DataSetObservable mObservable = new DataSetObservable();
private DataSetObserver mViewPagerObserver;
void setViewPagerObserver(DataSetObserver observer) {
synchronized (this) {
mViewPagerObserver = observer;
}
}
public void notifyDataSetChanged() {
synchronized (this) {
if (mViewPagerObserver != null) {
//ViewPager专用的观察者
mViewPagerObserver.onChanged();
}
}
mObservable.notifyChanged();
}
}
其实通过源码我们可以发现,PagerAdapter
虽然提供了注册观察者的模式,但却直接保留了ViewPager
的观察者,而不是通过注册
观察者的方式。当数据更新---notifyDataSetChanged()直接调用观察者的方法mViewPagerObserver.onChanged();
至于原因,我们可以猜测下:
- 首先是有使用
synchronized
关键字,那么就可以避免多线程的影响。 -
另外,我们又可以随时的使用 setAdapter()来切换,当切换时会执行mAdapter.setViewPagerObserver(null);
那么当又有其他地方调用mAdapter. notifyDataSetChanged()
那么就会引起???
=但是这两个方法都涉及了UI绘制,也就是只能在主线程调用。所以应该是不会有多线程的困扰的,所以猜不出原因。。
来自ViewPager的观察者的执行优先于其它观察者
另外谈下adapter.notifyDataSetChange()
无法刷新界面数据的问题。
这里就是mViewPagerObserver.onChanged()
的不作为了,看下源码就很清晰了
void dataSetChanged() {
// This method only gets called if our observer is attached, so mAdapter is non-null
...
for (int i = 0; i < mItems.size(); i++) {
final ItemInfo ii = mItems.get(i);
final int newPos = mAdapter.getItemPosition(ii.object);
//当getItemPosition()返回POSITION_UNCHANGED时,是不会做更新的!而默认返回POSITION_UNCHANGED,所以需要重写该方法返回POSITION_NONE,
if (newPos == PagerAdapter.POSITION_UNCHANGED) {
continue;
}
if (newPos == PagerAdapter.POSITION_NONE) {
mItems.remove(i);
i--;
...
}
...
}
即便重写了该方法,当使用的是FragmentPagerAdapter
,也要注意由于Fragment
的缓存,没有重新初始化Fragment
对象,数据缓存可能导致页面的不更新。
PagerAdapter中需要掌握的几个方法
先通过观察FragmentPagerAdapter的源码,我们会发现实现非常简单。就是继承PagerAdapter之后,实现了几个方法用于生成以及缓存Fragment!
1. instantiateItem()
/**
* Create the page for the given position. The adapter is responsible
* for adding the view to the container given here, although it only
* must ensure this is done by the time it returns from
* {@link #finishUpdate(ViewGroup)}.
*
* @param container The containing View in which the page will be shown.
* @param position The page position to be instantiated.
* @return Returns an Object representing the new page. This does not
* need to be a View, but can be some other container of the page.
*/
@NonNull
public Object instantiateItem(@NonNull ViewGroup container, int position) {
return instantiateItem((View) container, position);
}
instantiateItem()
顾名思义就是在ViewPager
的position
页面生成一个用于展示的界面,当然你也可以什么都不做,无非就是展示空白页罢了。
注意,这里有个参数ViewGroup container
这个container
,多研读下注释,可能还有部分人不理解这个是指代哪个View!
@param container The containing View in which the page will be shown
将会在改页面展示的View,也就是这个View会与Fragment绑定。
它有个返回值,会与position绑定,如FragmentPagerAdapter
返回生成的Fragment
对象。
也就是 position -- fragment -- container
就绑定一起了。
2. destroyItem()
/**
* Remove a page for the given position. The adapter is responsible
* for removing the view from its container, although it only must ensure
* this is done by the time it returns from {@link #finishUpdate(ViewGroup)}.
*
* @param container The containing View from which the page will be removed.
* @param position The page position to be removed.
* @param object The same object that was returned by
* {@link #instantiateItem(View, int)}.
*/
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {}
destroyItem()
这个就更简单了,销毁指定的位置的页面,当某个位置不在缓存范围内时,就会被移除。
如FragmentPagerAdapter
在此执行fragment.detach()
,没有销毁Fragment
对象。
3. setPrimaryItem()
/**
* Called to inform the adapter of which item is currently considered to
* be the "primary", that is the one show to the user as the current page.
* This method will not be invoked when the adapter contains no items.
*
* @param container The containing View from which the page will be removed.
* @param position The page position that is now the primary.
* @param object The same object that was returned by
* {@link #instantiateItem(View, int)}.
*/
public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
setPrimaryItem((View) container, position, object);
}
setPrimaryItem()
当滑动至某个页面时,该方法会被调用。
在FragmentPagerAdapter
中重写该方法使之执行了 fragment.setUserVisibleHint(true)
,让我们可以通过getUserVisibleHint()
来判断该Fragment
是否正在被展示从而执行某些骚操作
4. isViewFromObject()
/**
* Determines whether a page View is associated with a specific key object
* as returned by {@link #instantiateItem(ViewGroup, int)}. This method is
* required for a PagerAdapter to function properly.
*
* @param view Page View to check for association with object
* @param object Object to check for association with view
* @return true if view
is associated with the key object object
*/
public abstract boolean isViewFromObject(@NonNull View view, @NonNull Object object);
isViewFromObject()
上述几个方法中,都需要传入一个Object参数,其实就是instantiateItem()
所返回的Fragment。
例如,假设instantiateItem()
返回的不是Fragment
,那么在执行setPrimaryItem()
的时候,我们就无从下手去处理这个位置对应的Fragment
,无法执行(Fragment)object.setUserVisibleHint(true)
。因为Fragment是缓存在FragmentManager,而PagerAdaper本身是不做缓存的。除非我们自己缓存了每个position
对应的Fragment
,然后执行fragemntList.get(position).setUserVisibleHint(true)
.
说回这个isViewFromObject()
,对于ViewPager
来说,是这样使用的:
class ViewPager {
ItemInfo infoForChild(View child) {
for (int i = 0; i < mItems.size(); i++) {
ItemInfo ii = mItems.get(i);
if (mAdapter.isViewFromObject(child, ii.object)) {
return ii;
}
}
return null;
}
}
所以这个方法是用于遍历寻找到,某个页面(View)匹配哪一个Object,从而找到对应的ItemInfo
(ViewPager里面储存每个子页面所使用的数据结构)
所以若是直接instantiateItem()
返回一个View
的话,那么:
public boolean isViewFromObject(View view, Object object) {
return view == object;
}
所以若是直接instantiateItem()
返回一个Fragment
的话,那么:
public boolean isViewFromObject(View view, Object object) {
return ((Fragment)object).getView() == view;
}
5. getItem(int position)
还有个在FragmentPagerAdapter和FragmentStatePagerAdapter中定义的
public abstract Fragment getItem(int position);
,用于真正生成Fragment
实例的方法,然后被instantiateItem()
调用。毕竟在instantiateItem()中还涉及各种Fragment的缓存,不一定需要调用到getItem()。
三、无限循环实战
大部分上,使用FragmentPagerAdapter和FragmentStatePagerAdapter,可以分别满足我们对于少量或者较多页面的左右滑动切换的需求。
但有时候,需求又是多变的,比如上下滑动,无限左右滑动。
比如这个(或许可以理解成 小说阅读时的左右翻页):
- 有很多个不同的数据,通过左右滑动页面,展示不同的数据。
- 每个数据的类型格式相同
Talk is cheap, let me talk more!
思路一
在ViewGroup
添加3个全屏的View,编号1、2、3,使用View.setTranslationX()
方式将其中两个View(1和3),放在2两边,然后拦截滑动事件,当滑动时同时设置这三个view在X轴的偏移量,当滑动结束时,比如向左滑动,那么现在显示的为3,且3的x轴偏移量变为0,这时候,重新设置1和2的偏移量使之在3的两边
思路二
继承PagerAdapter,将数据大小,即 adapter.getCount()
返回 Int.MAX_VALUE
, viewPager.currentItem
初始化为 Int.MAX_VALUE/2
思路1的实现其实是考虑要很全面,坑可能不少。或许可以考虑用在上下滑的无限循环。当然现在是有了支持上下滑的ViewPager库的,另外androidx的Viewpager2库也可以直接支持。
所以本文接下来说说思路2.
实现思路二
首先为什么不直接使用FragmentPagerAdapter
和FragmentStatePagerAdapter
FragmentPagerAdapter
会根据缓存所有位置的Fragment
,那么这数量级就点大了!
FragmentStatePagerAdapter
在上一篇已经有提到了,初始化的时候,直接就OOM
了。
其实如果想一下,如果们这里不是使用Fragment来写,单纯的使用View来处理的话,由于每个View都是相同的,我们是必然要循环利用这些View的,想一下常用的RecycleView,没错了,加个缓存吧,使用List
保存回收的view,然后再用个ViewHolder优化下代码,大概就想下面这样了
class MyViewPagerAdapter extends PagerAdapter {
private LinkedList mViewCache = null;
@Override
public boolean isViewFromObject(View view, Object object) {
return view == object;
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
holder = null;
View convertView = null;
if (mViewCache.size() == 0) {
convertView = View.inflate(ExperimentActivity.this, R.layout.item_exper_viewpager, null);
holder = new ViewHolder();
holder.ivPic = (ImageView) convertView.findViewById(R.id.iv_title_pic);
holder.tvName = (TextView) convertView.findViewById(R.id.tv_exper_name);
convertView.setTag(holder);
} else {
convertView = mViewCache.removeFirst();
holder = (ViewHolder) convertView.getTag();
}
holder.tvName.setText("string");
container.addView(convertView);
return convertView;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
container.removeView((View) object);
mViewCache.add((View) object);
}
public static class ViewHolder {
public TextView tvName;
public ImageView ivPic;
}
}
同理,使用Fragment的形式,我们可以设置一个FragmentPool
来动态初始化fragment以及缓存fragment。
另外要注意的是,由于Fragment的事务处理都是异步的,所以记得使用commitNow
,因为如果不是立即提交,很可能滑动时出现空白页的情况。
//Fragment缓存池,每个被remove的Fragment的添加队列尾部,等待被重新利用
private val cacheFragments = ArrayDeque()
override fun instantiateItem(container: ViewGroup, position: Int): Any {
val tag = "DataFragment$position"
var fragment = mFragmentManager.findFragmentByTag(tag)
//ViewPager本身缓存的页面 或系统回收后恢复的缓存页面
if (fragment != null) {
return fragment
}
fragment = getItem(position)
fragment.setMenuVisibility(false)
fragment.userVisibleHint = false
mFragmentManager.beginTransaction().add(container.id, fragment, tag).commitNow()
return fragment
}
override fun destroyItem(container: ViewGroup, position: Int, fragment: Any) {
//从FragmentManager中移除
mFragmentManager.beginTransaction().remove(fragment as Fragment)
.commitNowAllowingStateLoss()
if (fragment is DataFragment) {
cacheFragments.add(fragment)
}
}
private fun getItem(position: Int): Fragment {
Log.i(TAG, "getItem: pos: $position")
var fragment = cacheFragments.poll()
if (fragment == null || fragment.isAdded || fragment.isStateSaved) {
Log.i(TAG, "新建fragment")
if (fragment != null) {
cacheFragments.add(fragment)
}
fragment = DataFragment()
}
getItemBundle(position)?.let {
fragment.arguments = it
}
return fragment
}
至于其他的几个方法,照搬FragmentPagerAdapter
就可以了!
另外,可以参照RecycleView
的写法,我们还可以扩展出一种MutiType
的形式,而不是仅支持一种Fragment。当然缓存池的结构也要相应的变一变。
写篇文章真的难。。三个星期前就写下这篇的三分之二了,今天才动手完结它。
我真的太难了,上辈子一定是数学高考最后一道答题。