錯誤的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

我是一個應屆生,最近和朋友們維護了一個公衆號,內容是咱們在從應屆生過渡到開發這一路所踩過的坑,以及咱們一步步學習的記錄,若是感興趣的朋友能夠關注一下,一同加油~

我的公衆號:鹹魚正翻身
相關文章
相關標籤/搜索