上一篇文,描述了FragmentPagerAdapter
的一些基础使用。
这一篇主要还是来说说PagerAdapter
的基础原理,以及关于无限循环的ViewPager
的实战。
PagerAdapter 原理
用多了RecycleView
的人都知道,也是有个BaseAdapter
与RecycleView
绑定。
ViewPager
是同样的原理,ViewPager
是一个ViewGroup
,缓存有多个子View(页面),而适配器类PagerAdapter
为每个页面来提供数据,通过几个简单的接口,即可实现复杂功能而无须接触ViewPager里面繁而又繁的复杂逻辑计算代码。
ViewPager.setAdapter()
的时候,向Adapter
注册一个观察者,即调用: 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
的观察者,而不是通过注册
观察者的方式。当数据更新直接调用观察者的方法mViewPagerObserver.onChanged();
至于原因,我们可以猜测下:
首先是有使用synchronized
关键字,那么就可以避免多线程的影响。
另外,我们又可以随时的使用 setAdapter()来切换,当切换时会执行mAdapter.setViewPagerObserver(null);
那么当又有其他地方调用mAdapter. notifyDataSetChanged()
那么就会引起???
=但是这两个方法都涉及了UI绘制,也就是只能在主线程调用。所以应该是不会有多线程的困扰的,所以猜不出原因。。
也许原因只是,代码上比较直观,便于区分吧。
另外这里谈下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!
/**
* 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!
它有个返回值,会与position绑定,如FragmentPagerAdapter
返回生成的Fragment
对象。
也就是 position -- fragment -- container
就绑定一起了。
/**
* 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((View) container, position, object);
}
destroyItem()
这个就更简单了,销毁指定的位置的页面,当某个位置不在缓存范围内时,就会被移除。
如FragmentPagerAdapter
在此执行fragment.detach()
,没有销毁Fragment
对象。
/**
* 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
是否正在被展示从而执行某些骚操作
/**
* 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 <code>object</code>
* @param object Object to check for association with <code>view</code>
* @return true if <code>view</code> is associated with the key object <code>object</code>
*/
public abstract boolean isViewFromObject(@NonNull View view, @NonNull Object object);
isViewFromObject()
上述几个方法中,都有个Object参数,其实就是instantiateItem()
所返回的对象。例如,假设instantiateItem()
返回的不是Fragment
,那么在执行setPrimaryItem()
的时候,我们就无从下手去处理这个位置对应的Fragment
,无法执行执行(Fragment)object.setUserVisibleHint(true)
,除非我们自己缓存了每个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;
}
还有个FragmentPagerAdapter
中定义的
public abstract Fragment getItem(int position);
这个就没啥好说的了,真正用于生成Fragment
实例的方法,然后被instantiateItem()
调用。
无限循环实战
Talk is cheap, show me the code!
需求:
1.有很多个不同的数据,通过左右滑动页面,展示不同的数据。
- 每个数据的类型格式相同
或许可以理解成 小说阅读时的左右翻页。
思路一
在ViewGroup
添加3个全屏的View,编号123,使用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的实现其实是考虑要很全面,坑可能不少。或许可以考虑用在上下滑的无限循环。
所以本文接下来用的是思路2.
1.首先为什么不直接使用FragmentPagerAdapter
和FragmentStatePagerAdapter
FragmentPagerAdapter
会根据缓存所有位置的Fragment
,那么这数量级就点大了!
FragmentStatePagerAdapter
在上一篇已经有提到了,初始化的时候,直接就OOM
了。
其实如果想一下,如果们这里不是使用Fragment来写,单纯的使用View来处理的话,由于每个View都是相同的,我们是必然要循环利用这些View的,直接使用ArrayList<View>
保存view,那么在instantiateItem()中 return viewList(pos)
,其中要注意若view已经被addToParent
,那么需要先ViewGroup.removeView()
,再执行ViewGroup.addView()
.
public Object instantiateItem(ViewGroup container, int position) {
//对ViewPager页号求模取出View列表中要显示的项
position %= viewlist.size();
if (position<0){
position = viewlist.size()+position;
}
ImageView view = viewlist.get(position);
//如果View已经在之前添加到了一个父组件,则必须先remove,否则会抛出IllegalStateException。
ViewParent vp =view.getParent();
if (vp!=null){
ViewGroup parent = (ViewGroup)vp;
parent.removeView(view);
}
container.addView(view);
//add listeners here if necessary
return view;
}
同理,使用Fragment的形式,我们也可以使用ArrayList<Fragment>
直接初始化三个Fragment的形式进行循环利用。当然这里同样有 fragment.isAdd()
的问题,所以建议设置一个FragmentPool
来动态初始化fragment以及缓存fragment。
另外要注意的是,由于Fragment的事务处理都是异步的,所以记得使用commitNowAllowingStateLoss()
,因为如果不是立即提交,很可能滑动时出现空白页的情况。。
使用由于我们也有必要设置一个FragmentPool
其中不同的是,由于Fragment的事务处理都是异步的,那么我们就不可以直接采用
//Fragment缓存池,每个被remove的Fragment的添加队列尾部,等待被重新利用
private val cacheFragments = ArrayDeque<TodayDataFragment>()
override fun instantiateItem(container: ViewGroup, position: Int): Any {
val fragment = getItem(position)
fragment.setMenuVisibility(false)
fragment.userVisibleHint = false
Log.i(TAG, "Fragment添加, tag =${fragment.arguments?.getString(TodayDataFragment.PARAM_DATE)}")
mFragmentManager.beginTransaction().add(container.id, fragment).commitNow()
return fragment
}
override fun destroyItem(container: ViewGroup, position: Int, fragment: Any) {
//从FragmentManager中移除
mFragmentManager.beginTransaction().remove(fragment as Fragment).commitNowAllowingStateLoss()
mCurTransaction = null
if (fragment is TodayDataFragment) {
Log.i(TAG, "Fragment移除,并加入缓存池 tag =${fragment.arguments?.getString(TodayDataFragment.PARAM_DATE)}")
cacheFragments.add(fragment)
}
}
private fun getItem(position: Int): Fragment {
var fragment = cacheFragments.poll()
if(fragment == null || fragment.isAdded || fragment.isStateSaved) {
Log.i(TAG, "新建fragment")
if(fragment!=null) {
cacheFragments.add(fragment)
}
fragment = TodayDataFragment()
}
getItemBundle(position)?.let {
fragment.arguments = it
}
return fragment
}
至于其他的几个方法,照搬FragmentPagerAdapter
就可以了!
另外,可以参照RecycleView
的写法,我们还可以扩展出一种MutiType
的形式,而不是仅支持一种Fragment。当然缓存池的结构也要相应的变一变。
写篇文章真的难。。三个星期前就写下这篇的三分之二了,今天才动手完结它。
我真的太难了,上辈子一定是数学高考最后一道答题。
网友评论