错误的ViewPager用法(填坑):ViewPager2做了什么?
2020/7/20 23:03:45
本文主要是介绍错误的ViewPager用法(填坑):ViewPager2做了什么?,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
前言
思来想去还是决定把ViewPager2写了,毕竟针对ViewPager已经写了3篇了,也不差这最后一哆嗦了。没看过之前3篇文章的,可以在这里自取:
你的ViewPager八成用错了。
错误的ViewPager用法(续),会产生内存泄漏?内存溢出?
FragmentStatePagerAdapter在ViewPager中优化了什么
结束今天的这一篇文章,也算是无意间成了一个小的系列了。
正文
首先来说ViewPager2已经稳定了,大家可以愉快的用起来了:
dependencies { implementation "androidx.viewpager2:viewpager2:1.0.0" } 复制代码
了解基本的api用法肯定还是官方API、基础使用、迁移ViewPager至ViewPager2。
一、基本用法
毕竟有些同学不喜欢看文绉绉的官方文档,那这里我就直接贴一下基本的用法,除了布局以外,就只有一个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()
。
基于这俩个方法,官方给予了额外的解释:
![](/upload/202007/20/202007202303452917.png)
可以看到,官方明确提到:createFragment()
需要提供new的实例,而不是复用的实例。这也算是官方层面对你的ViewPager八成用错了。的间接的佐证。
1.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)
。
官网有这么一句话:
大部分情况下,这么使用是更好的选择。
![](/upload/202007/20/202007202303454850.png)
因此怎么使用并不绝对,如果大家充分理解ViewPager的设计和FragmentManager的设计,其实可以根据自己的需求选择使用哪个构造函数。
更多代码,可以参考Google的demo
1.2、可以DiffUtil
大家应该也都知道,ViewPager2是基于RecycleView实现的,因此势必可以使用DiffUtils。不过现实很骨感,使用DiffUtil还要额外重写2个方法:
![](/upload/202007/20/202007202303456159.png)
对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.3、配适TabLayout
这部分是一个“浑身难受”的点,由于ViewPager2的独特性,适配TabLayout需要费点脑筋。如果不能自己适配,可以使用Google的提供的适配方案:
TabLayoutMediator(tabLayout, viewPager) { tab, position -> tab.text = "你需要显示的Title" }.attach() 复制代码
TabLayoutMediator这个类,需要
com.google.android.material:material:1.1.0
及以上。
二、原理分析
首先,咱们看看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()
中
![](/upload/202007/20/202007202303457575.png)
- 第一个咱们已经看过了,只有在
onBindViewHolder()
调用时,当前bind的ViewHolder的id不是当前ViewHolder的itemId时,才会调用。(也就是只会出现重新notify的时候) - 第二是在gcFragment()时,而这个方法只有在!mHasStaleFragments、
shouldDelayFragmentTransactions()
都为false时才会调用。(也就是savestate的时候) - 因此常规情况下,只会在onViewRecycled()的时候被回调。
因此,ViewPager2的Fragment移除策略是完全基于RecycleView的(当然加载策略也是基于RecycleView,毕竟一起的开始是在onBindViewHolder()
方法中)。
尾声
ViewPager2整体来说并没有什么特殊的地方,毕竟民间基于RecycleView实现的ViewPager也是数不胜数。
到此也算是给自己的ViewPager系列文章画下句号了。
接下来差不多会基于官方的资料,结合我们自身的项目好好聊聊Jetpack。
我是一个应届生,最近和朋友们维护了一个公众号,内容是我们在从应届生过渡到开发这一路所踩过的坑,以及我们一步步学习的记录,如果感兴趣的朋友可以关注一下,一同加油~
![个人公众号:咸鱼正翻身](/upload/202007/20/202007202303458122.png)
这篇关于错误的ViewPager用法(填坑):ViewPager2做了什么?的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-01-18android.permission.read_media_video
- 2024-01-18android_getaddrinfo failed eai_nodata
- 2024-01-18androidmo
- 2024-01-15Android下三种离屏渲染技术
- 2024-01-09Android 蓝牙使用
- 2024-01-06Android对接华为AI - 文本识别
- 2023-11-15代码安全之代码混淆及加固(Android)
- 2023-11-10简述Android语音播报TTS
- 2023-11-06Android WiFi工具类
- 2023-07-22Android开发未来的出路