ViewPager2+Fragment操作筆記

ViewPager2+Fragment操作筆記

心情好

ViewPager2簡(jiǎn)介

ViewPager2官網(wǎng)介紹

ViewPager2官網(wǎng)Samples

距離ViewPager2正式版的發(fā)布已經(jīng)一年多了,目前ViewPager早已停止更新,官方鼓勵(lì)使用ViewPager2替代。 ViewPager2底層基于RecyclerView實(shí)現(xiàn),因此可以獲得RecyclerView帶來(lái)的諸多收益:

  • 拋棄傳統(tǒng)的PagerAdapter,統(tǒng)一了AdapterAPI;
  • 橫向、豎向布局都可以實(shí)現(xiàn)自由滑動(dòng);
  • 支持DiffUitl,可以實(shí)現(xiàn)局部刷新;
  • 支持RTL(right-to-left),對(duì)于一些有出海需求的APP非常有用;
  • 支持ItemDecorator,搭配PageTransformer實(shí)現(xiàn)炫酷的跳轉(zhuǎn)動(dòng)畫(huà);

ViewPager2更多的是配合Fragment的使用,這需要借助于FragmentStateAdapter。

他們偶爾會(huì)搭配TabLayout一起使用,相關(guān)代碼直接閱讀或者運(yùn)行 ViewPager2官網(wǎng)Samples 即可,這里不做重復(fù)的講解。

下面主要講一下在使用過(guò)程中遇到的問(wèn)題~!

實(shí)際操作效果

上滑吸頂+標(biāo)題頁(yè)面左右滑動(dòng)+橫滑和豎滑列表+標(biāo)題頁(yè)面數(shù)據(jù)和數(shù)量更新

上滑吸頂
CoordinatorLayout+AppBarLayout+CollapsingToolbarLayout
左右滑動(dòng)
ViewPager2+TabLayout+Fragment
橫滑和豎滑列表
RecycleView+NestedScrollableHost
標(biāo)題頁(yè)面數(shù)據(jù)和數(shù)量
TabLayoutMediator+聲明周期檢測(cè)+緩存優(yōu)化

viewpage2_tabLayout_nestedscroll.gif

RecycleView和Viewpage2的滑動(dòng)沖突

/**
 * Created by Tanzhenxing
 * Date: 2021/4/7 7:04 下午
 * Description:解決 [RecyclerView] 嵌套到 [androidx.viewpager2.widget.ViewPager2] 左右滑動(dòng)沖突
 * 目前只解決了左右滑動(dòng)沖突
 */
class RecyclerViewAtViewPager2 : RecyclerView {
    constructor(context: Context) : this(context, null)
    constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)

    var x1 = 0f
    var x2 = 0f
    override fun dispatchTouchEvent(event: MotionEvent?): Boolean {
        if(event!!.action == MotionEvent.ACTION_DOWN) {
            x1 = event.x
        } else if(event.action == MotionEvent.ACTION_MOVE) {
            x2 = event.x
        } else if (event.action == MotionEvent.ACTION_CANCEL
            || event.action == MotionEvent.ACTION_UP) {
            x2 = 0f
            x1 = 0f
        }
        val xOffset= x2-x1
        if (layoutManager is LinearLayoutManager) {
            val linearLayoutManager = layoutManager as LinearLayoutManager
            if (linearLayoutManager.orientation == HORIZONTAL) {
                if ((xOffset <= 0 && canScrollHorizontally(1))
                    || (xOffset >= 0 && canScrollHorizontally(-1))) {
                    this.parent?.requestDisallowInterceptTouchEvent(true)
                } else {
                    this.parent?.requestDisallowInterceptTouchEvent(false)
                }
            } else {
                // TODO: 2021/4/8 目前沒(méi)有實(shí)現(xiàn)上下滑動(dòng)和 [androidx.viewpager2.widget.ViewPager2]上下滑動(dòng)的沖突
            }
        } else {
            handleDefaultScroll()
        }

        return super.dispatchTouchEvent(event)
    }

    fun handleDefaultScroll() {
        val canScrollHorizontally = canScrollHorizontally(-1) || canScrollHorizontally(1)
        val canScrollVertically = canScrollVertically(-1) || canScrollVertically(1)
        if (canScrollHorizontally || canScrollVertically) {
            this.parent?.requestDisallowInterceptTouchEvent(true)
        } else {
            this.parent?.requestDisallowInterceptTouchEvent(false)
        }
    }
}

ViewPager2中Fragment的懶加載

懶加載

一般我們使用Fragment對(duì)頁(yè)面進(jìn)行數(shù)據(jù)懶加載的時(shí)候都是通過(guò)onHiddenChanged方法判斷顯示和隱藏,在第一次展現(xiàn)出來(lái)的時(shí)候再進(jìn)行接口調(diào)用。

 @Override
public final void onHiddenChanged(boolean hidden) {
  super.onHiddenChanged(hidden);
  if (!hidden) {
    onUserVisible();
  } else {
    onUserGone();
  }
}

但在ViewPager2中,FragmentsetUserVisibleHintonHiddenChanged方法都是不執(zhí)行的。

  • ViewPager展現(xiàn)第一個(gè)頁(yè)面,然后切后臺(tái)的日志:
04-17 16:45:10.992 D/tanzhenxing:11(22006): onCreate
04-17 16:45:10.992 D/tanzhenxing:11(22006): onCreateView:
04-17 16:45:11.004 D/tanzhenxing:11(22006): onActivityCreated
04-17 16:45:11.004 D/tanzhenxing:11(22006): onViewStateRestored: 184310198
04-17 16:45:11.004 D/tanzhenxing:11(22006): onStart
04-17 16:45:11.004 D/tanzhenxing:11(22006): onResume
04-17 16:45:18.739 D/tanzhenxing:11(22006): onPause
04-17 16:45:18.779 D/tanzhenxing:11(22006): onStop

然后在切回前臺(tái)的日志:

04-17 16:53:40.749 D/tanzhenxing:11(22006): onStart
04-17 16:53:40.752 D/tanzhenxing:11(22006): onResume
  • ViewPager展現(xiàn)第一個(gè)頁(yè)面,然后手動(dòng)滑動(dòng)到第二個(gè)頁(yè)面日志:
04-17 16:54:44.168 D/tanzhenxing:12(22006): onCreate
04-17 16:54:44.168 D/tanzhenxing:12(22006): onCreateView:
04-17 16:54:44.178 D/tanzhenxing:12(22006): onActivityCreated
04-17 16:54:44.178 D/tanzhenxing:12(22006): onViewStateRestored: 47009644
04-17 16:54:44.178 D/tanzhenxing:12(22006): onStart
04-17 16:54:44.553 D/tanzhenxing:11(22006): onPause
04-17 16:54:44.554 D/tanzhenxing:12(22006): onResume

這樣看起來(lái)我們是可以利用Fragment聲明周期中的onStartonResume方法做懶加載。

預(yù)加載

只要講數(shù)據(jù)請(qǐng)求寫(xiě)在 onCreateView 或者onStart中就能進(jìn)行接口的離屏請(qǐng)求。

FragmentStateAdapter

ViewPager2繼承自RecyclerView,大概率FragmentStateAdapter繼承自RecyclerView.Adapter

public abstract class FragmentStateAdapter extends 
  RecyclerView.Adapter<FragmentViewHolder> implements StatefulAdapter {
}

onCreateViewHolder

onCreateViewHolderRecycleVeiw用于創(chuàng)建ViewHolder的方法:

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

FragmentViewHolder的主要作用是通過(guò)FrameLayoutFragment提供用作容器的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);
}

onBindViewHolder

onBindViewHolderRecycleVeiw用于數(shù)據(jù)綁定的方法:

@Override
public final void onBindViewHolder(final @NonNull FragmentViewHolder holder, int position) {
    /**部分代碼省略 */
      ensureFragment(position);
    /**部分代碼省略 */
    gcFragments();
}

ensureFragment(position),其內(nèi)部會(huì)最終回調(diào)用createFragment創(chuàng)建當(dāng)前Fragment

private void ensureFragment(int position) {
  long itemId = getItemId(position);
  if (!mFragments.containsKey(itemId)) {
    // TODO(133419201): check if a Fragment provided here is a new Fragment
    Fragment newFragment = createFragment(position);
    newFragment.setInitialSavedState(mSavedStates.get(itemId));
    mFragments.put(itemId, newFragment);
  }
}

mFragments緩存創(chuàng)建的Fragment,供后面placeFramentInViewholder使用; gcFragments回收已經(jīng)不再使用的的Fragment對(duì)應(yīng)的item已經(jīng)刪除),節(jié)省內(nèi)存開(kāi)銷(xiāo)。

onViewAttachedToWindow

onViewAttachedToWindowViewHolder出現(xiàn)在頁(yè)面中回調(diào)。

@Override
public final void onViewAttachedToWindow(@NonNull final FragmentViewHolder holder) {
    //將FragmentViewHolder的container與當(dāng)前Fragment綁定
      placeFragmentInViewHolder(holder);
    gcFragments();
}

FragmentStateAdapter使用

  • Fragment對(duì)象容器;
  • 生產(chǎn)fragment識(shí)別的id;
class MyFragmentStateAdapter(val data: List<Int>, fragment: Fragment) : FragmentStateAdapter(fragment){
    private val fragments = mutableMapOf<Int, Fragment>()
    override fun createFragment(position: Int): Fragment {
        val value = data[position]
        val fragment = fragments[value]
        if (fragment != null) {
            return fragment
        }
        val cardFragment =
            NestedAllRecyclerViewFragment.create(value)
        fragments[value] = cardFragment
        return cardFragment
    }

    /**
     * 根據(jù)數(shù)據(jù)生成唯一id
     *
     * 如果不重寫(xiě),那么在調(diào)用[notifyDataSetChanged]更新的時(shí)候
     *
     * 會(huì)拋出```new IllegalStateException("Fragment already added")```異常
     */
    override fun getItemId(position: Int): Long {
        return data[position].toLong()
    }

    /**
     * 用來(lái)判斷當(dāng)前id對(duì)應(yīng)的fragment是否添加過(guò)
     */
    override fun containsItem(itemId: Long): Boolean {
        data.forEach {
            if (it.toLong() == itemId) {
                return true
            }
        }
        return false
    }
    override fun getItemCount(): Int {
        return data.size
    }
}

在進(jìn)行

獲取Fragment實(shí)例

fun getCurrentFragment(position: Int): Fragment? =
        fragment.childFragmentManager.findFragmentByTag("f$position")

源碼分析:

public abstract class FragmentStateAdapter extends RecyclerView.Adapter<FragmentViewHolder>
    implements StatefulAdapter {
    /**部分代碼省略 */
    void placeFragmentInViewHolder(@NonNull
    final FragmentViewHolder holder) {
        /**部分代碼省略 */
        if (!shouldDelayFragmentTransactions()) {
            scheduleViewAttach(fragment, container);
            mFragmentManager.beginTransaction()
                            .add(fragment, "f" + holder.getItemId())
                            .setMaxLifecycle(fragment, STARTED).commitNow();
            mFragmentMaxLifecycleEnforcer.updateFragmentMaxLifecycle(false);
        }

        /**部分代碼省略 */
    }

    /**部分代碼省略 */
    @Override
    public long getItemId(int position) {
        return position;
    }
}

Fragment添加的TAG"f" + holder.getItemId()。

異常處理

  • 初始化時(shí)遇到的崩潰;
Fragment HomeFragment{b793d14 (e67290fe-7ab1-4b2b-b98c-4e08d146644c)} has not been attached yet.
com.xxx.xxx.xxx.adapter.HomeFragmentStateAdapter.<init>(SourceFile:29)

在開(kāi)發(fā)過(guò)程中遇到問(wèn)題,需要在構(gòu)造FragmentStateAdapter的時(shí)候?qū)?code>Fragment的狀態(tài)做判斷isAdded()。

  • 更新數(shù)據(jù)的時(shí)候遇到的崩潰:
Fragment already added

重寫(xiě)getItemId方法,該方法返回的值與數(shù)據(jù)有關(guān)而不是與數(shù)據(jù)在列表中的索引有關(guān)。因?yàn)樗碇?code>Fragment的唯一性,是否可以復(fù)用。

ViewPager2滑動(dòng)監(jiān)聽(tīng)

public abstract static class OnPageChangeCallback {
    //當(dāng)前頁(yè)面開(kāi)始滑動(dòng)時(shí)
    public void onPageScrolled(int position, float positionOffset,@Px int positionOffsetPixels) {
    }
    //當(dāng)頁(yè)面被選中時(shí)
    public void onPageSelected(int position) {
    }
    //當(dāng)前頁(yè)面滑動(dòng)狀態(tài)變動(dòng)時(shí)
    public void onPageScrollStateChanged(@ScrollState int state) {
    }
}

實(shí)現(xiàn):

var pageChangeCallback = object : ViewPager2.OnPageChangeCallback() {
  override fun onPageSelected(position: Int) {
    //需要注意的是postion需要做大于0的判斷
  }
}

TabLayout+TabLayoutMediator

方便實(shí)現(xiàn)TABViewPager滑動(dòng)或跳轉(zhuǎn)的關(guān)聯(lián)。

implementation 'com.google.android.material:material:1.2.0'

建議material的版本號(hào)大約1.0.0,否則實(shí)現(xiàn)TAB的自定義布局寬度展現(xiàn)些問(wèn)題。

使用:ViewPager2官網(wǎng)Samples

DiffUtil 局部更新

DiffUtil和它的差量算法

總結(jié)

本文主要介紹了ViewPager2配合Fragment的使用方法以及在使用過(guò)程中需要注意的問(wèn)題,順帶提到了TabLayout、OnPageChangeCallbackDiffUtil等。

文章到這里就全部講述完啦,若有其他需要交流的可以留言哦~!~!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容