错误的ViewPager用法(填坑):ViewPager2作了什么?

前言

思来想去仍是决定把ViewPager2写了,毕竟针对ViewPager已经写了3篇了,也不差这最后一哆嗦了。没看过以前3篇文章的,能够在这里自取:java

你的ViewPager八成用错了。android

错误的ViewPager用法(续),会产生内存泄漏?内存溢出?git

FragmentStatePagerAdapter在ViewPager中优化了什么github

结束今天的这一篇文章,也算是无心间成了一个小的系列了。api

正文

首先来讲ViewPager2已经稳定了,你们能够愉快的用起来了:缓存

dependencies {
    implementation "androidx.viewpager2:viewpager2:1.0.0"
}
复制代码

了解基本的api用法确定仍是官方API基础使用迁移ViewPager至ViewPager2ide

1、基本用法

毕竟有些同窗不喜欢看文绉绉的官方文档,那这里我就直接贴一下基本的用法,除了布局之外,就只有一个Adapter稍稍和ViewPager不一样:函数

private inner class ScreenSlidePagerAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) {
    override fun getItemCount(): Int = NUM_PAGES
    // new本身的Fragment
    override fun createFragment(position: Int): Fragment = ScreenSlidePageFragment() 
}
复制代码

很容易迁移,getItemCount()就是ViewPager里的getCount()createFragment()是ViewPager中的getItem()布局

基于这俩个方法,官方给予了额外的解释post

能够看到,官方明确提到:createFragment()须要提供new的实例,而不是复用的实例。这也算是官方层面对你的ViewPager八成用错了。的间接的佐证。

1.一、构造函数的不一样

能够发现ViewPager2里的Adapter的方法命名合理的多,createFragment(),很明显咱们应该在这个方法return咱们须要建立的Fragment。

不知道你们有没有注意到构造函数的不一样,ViewPager2的构造函数接受FragmentActivity或者Fragment,而再也不是FragmentManager。这也算是官方层面上告诉你们什么状况下用什么FragmentManager:

public FragmentStateAdapter(@NonNull FragmentActivity fragmentActivity) {
    this(fragmentActivity.getSupportFragmentManager(), fragmentActivity.getLifecycle());
}

public FragmentStateAdapter(@NonNull Fragment fragment) {
    this(fragment.getChildFragmentManager(), fragment.getLifecycle());
}
复制代码

固然,这也不是说就必定Fragment中就用FragmentStateAdapter(@NonNull Fragment fragment),Activity下就必定用FragmentStateAdapter(@NonNull FragmentActivity fragmentActivity)

官网有这么一句话:

大部分状况下,这么使用是更好的选择。

所以怎么使用并不绝对,若是你们充分理解ViewPager的设计和FragmentManager的设计,其实能够根据本身的需求选择使用哪一个构造函数。

更多代码,能够参考Google的demo

1.二、能够DiffUtil

你们应该也都知道,ViewPager2是基于RecycleView实现的,所以势必可使用DiffUtils。不过现实很骨感,使用DiffUtil还要额外重写2个方法:

对ViewPager理解比较深入的同窗,看到getItemId()应该会很熟悉,毕竟是ViewPager时代里动态更新Fragment的接口api。

并非说getItemId()只会在DiffUtil中生效,getItemId()和ViewPager中的效果相似。只要同一个position下getItemId()的return的Long不一样,就会触发从新createFragment()。虽然能够完成更新Fragment的效果,可是会带来体验的瑕疵:会“闪一下”。

简单贴一下Google的用法:

private val items = (1..9).map { longToItem(nextValue++) }.toMutableList()

object : FragmentStateAdapter(this) {
    override fun createFragment(position: Int): PageFragment {
        val itemId = items.itemId(position)
        val itemText = items.getItemById(itemId)
        return PageFragment.create(itemText)
    }
    override fun getItemCount(): Int = items.size
    // 主要在于这俩个方法
    override fun getItemId(position: Int): Long = items.itemId(position)
    override fun containsItem(itemId: Long): Boolean = items.contains(itemId)
}
复制代码

固然demo中,也提到了DiffUtil的用法:

/** using [DiffUtil] */
val idsOld = items.createIdSnapshot()
performChanges()
val idsNew = items.createIdSnapshot()
DiffUtil.calculateDiff(object : DiffUtil.Callback() {
    override fun getOldListSize(): Int = idsOld.size
    override fun getNewListSize(): Int = idsNew.size

    override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
        idsOld[oldItemPosition] == idsNew[newItemPosition]

    override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
        areItemsTheSame(oldItemPosition, newItemPosition)
}, true).dispatchUpdatesTo(viewPager.adapter!!)
复制代码

看到这里可能有小伙伴会问:notifyDataSetChanged()、和DiffUtils的区别是什么?

这个问题我没办法回答,由于答案就是notifyDataSetChanged()和DiffUtil的区别...DiffUtil的出现就是为了解决数据diff的问题,毕竟notifyDataSetChanged()是一股脑更新所有。

固然因为ViewPager2的特殊性,是否真正去new Fragment,还要基于getItemId()的实现。不过notifyDataSetChanged()会实打实的必定调用onCreateViewHolder();而DiffUtil则是由我们本身的实现控制。

这就是两者的区别。

1.三、配适TabLayout

这部分是一个“浑身难受”的点,因为ViewPager2的独特性,适配TabLayout须要费点脑筋。若是不能本身适配,可使用Google的提供的适配方案:

TabLayoutMediator(tabLayout, viewPager) { tab, position ->
    tab.text = "你须要显示的Title"
}.attach()
复制代码

TabLayoutMediator这个类,须要com.google.android.material:material:1.1.0及以上。

2、原理分析

首先,我们看看FragmentStateAdapter:

public abstract class FragmentStateAdapter extends RecyclerView.Adapter<FragmentViewHolder> implements StatefulAdapter 复制代码

很直接的继承RecyclerView.Adapter,以此ViewPager2的机制是不会脱离RecycleView的。所以接下来,我们看一看onCreateViewHolder()onBindViewHolder()

onCreateViewHolder()没啥好说,就是生成一个父布局,这里直接贴代码:

public final FragmentViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
    return FragmentViewHolder.create(parent);
}

public final class FragmentViewHolder extends ViewHolder {
    private FragmentViewHolder(@NonNull FrameLayout container) {
        super(container);
    }

    @NonNull static FragmentViewHolder create(@NonNull ViewGroup parent) {
        FrameLayout container = new FrameLayout(parent.getContext());
        container.setLayoutParams(
                new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                        ViewGroup.LayoutParams.MATCH_PARENT));
        container.setId(ViewCompat.generateViewId());
        container.setSaveEnabled(false);
        return new FragmentViewHolder(container);
    }

    @NonNull FrameLayout getContainer() {
        return (FrameLayout) itemView;
    }
}
复制代码

重点内容在onBindViewHolder()中:

final LongSparseArray<Fragment> mFragments = new LongSparseArray<>();

public final void onBindViewHolder(final @NonNull FragmentViewHolder holder, int position) {
    final long itemId = holder.getItemId();
    final int viewHolderId = holder.getContainer().getId();
    final Long boundItemId = itemForViewHolder(viewHolderId); 
    // 判断在onBindViewHolder()的时候,是否须要removeFragment()
    if (boundItemId != null && boundItemId != itemId) {
        removeFragment(boundItemId);
        mItemIdToViewHolder.remove(boundItemId);
    }

    mItemIdToViewHolder.put(itemId, viewHolderId); 
    // 判断是否回调createFragment()
    ensureFragment(position);
    // 省略部分代码
    // 特殊状况下remove到引用
    gcFragments();
}

private void ensureFragment(int position) {
    // 基于getItemId()的return,判断mFragemtns中是否有缓存
    long itemId = getItemId(position);
    if (!mFragments.containsKey(itemId)) {
        Fragment newFragment = createFragment(position);
        newFragment.setInitialSavedState(mSavedStates.get(itemId));
        mFragments.put(itemId, newFragment);
    }
}
复制代码

onBindViewHolder()里边的流程仍是比较直接的,和ViewPager很想。总结一句话:借用RecycleView的bind时机,基因而否有缓存,决定是否须要从新new。

这里和ViewPager不一样的是移除缓存的策略,也就是上面我们看到的removeFragments():

private void removeFragment(long itemId) {
    Fragment fragment = mFragments.get(itemId);
    // 省略判空
    // remove掉View
    if (fragment.getView() != null) {
        ViewParent viewParent = fragment.getView().getParent();
        if (viewParent != null) {
            ((FrameLayout) viewParent).removeAllViews();
        }
    }
    // remove掉state
    if (!containsItem(itemId)) {
        mSavedStates.remove(itemId);
    }
    // 若是没有被add,直接remove这个Fragemnt
    if (!fragment.isAdded()) {
        mFragments.remove(itemId);
        return;
    }
    // 若是已经add了,而且containsItem(itemId)仍是true,那么就存一下state而后remove
    if (fragment.isAdded() && containsItem(itemId)) {
        mSavedStates.put(itemId, mFragmentManager.saveFragmentInstanceState(fragment));
    }
    mFragmentManager.beginTransaction().remove(fragment).commitNow();
    mFragments.remove(itemId);
}
复制代码

remove方法一共有三处会被调用,一个是在onBindViewHolder()

  • 第一个我们已经看过了,只有在onBindViewHolder()调用时,当前bind的ViewHolder的id不是当前ViewHolder的itemId时,才会调用。(也就是只会出现从新notify的时候)
  • 第二是在gcFragment()时,而这个方法只有在!mHasStaleFragments、shouldDelayFragmentTransactions()都为false时才会调用。(也就是savestate的时候)
  • 所以常规状况下,只会在onViewRecycled()的时候被回调。

所以,ViewPager2的Fragment移除策略是彻底基于RecycleView的(固然加载策略也是基于RecycleView,毕竟一块儿的开始是在onBindViewHolder()方法中)。

尾声

ViewPager2总体来讲并无什么特殊的地方,毕竟民间基于RecycleView实现的ViewPager也是数不胜数。

到此也算是给本身的ViewPager系列文章画下句号了。

接下来差很少会基于官方的资料结合咱们自身的项目好好聊聊Jetpack

我是一个应届生,最近和朋友们维护了一个公众号,内容是咱们在从应届生过渡到开发这一路所踩过的坑,以及咱们一步步学习的记录,若是感兴趣的朋友能够关注一下,一同加油~

我的公众号:咸鱼正翻身
相关文章
相关标签/搜索