ViewPager2+Fragment Operation note

ViewPager2 profile

ViewPager2

ViewPager2 website Samples

It has been more than a year since ViewPager2 was officially released. Currently, ViewPager has stopped updating and is officially encouraged to use ViewPager2 instead. The bottom layer of ViewPager2 is realized based on RecyclerView, so many benefits brought by RecyclerView can be obtained:

  • untraditionalPagerAdapterAnd unifiedAdaptertheAPI;
  • Horizontal and vertical layout can achieve free sliding;
  • supportDiffUitl, can achieve local refresh;
  • supportRTL(Right-to-left), which is very useful for some apps that need to go abroad;
  • supportItemDecorator, tie-inPageTransformerRealize cool jump animation;

ViewPager2 is used more in conjunction with fragments, with the help of a FragmentStateAdapter.

They are occasionally used together with TabLayout, and the relevant code can be directly read or run Samples on ViewPager2 website, which will not be repeated here.

The following is mainly about the problems encountered in the process of use ~!

Actual operation effect

Up slide top + title page slide left and right + horizontal and vertical slide list + title page data and quantity update

Slide the top

CoordinatorLayout+AppBarLayout+CollapsingToolbarLayout

Sliding around

ViewPager2+TabLayout+Fragment

Horizontal and vertical slide lists

RecycleView+NestedScrollableHost

Title page data and quantity

TabLayoutMediator+ Declaration cycle detection + cache optimization

RecycleView and Viewpage2 sliding conflict

/** * Created by Tanzhenxing * Date: 2021/4/7 7:04 * Description: this afternoon to solve [RecyclerView] nested [androidx. Viewpager2. Widget. Viewpager2] about sliding around * currently only has solved the conflict conflict * /
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 no slide up and down and [androidx. Viewpager2. Widget. Viewpager2] slide up and down the conflict}}else {
            handleDefaultScroll()
        }

        return super.dispatchTouchEvent(event)
    }

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

Lazy loading of fragments in ViewPager2

Lazy loading

Generally, the onHiddenChanged method is used to determine whether to display or hide the data when the Fragment is used for lazy loading of the page. The interface is called when the Fragment is displayed for the first time.

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

But in ViewPager2, the Fragment setUserVisibleHint and onHiddenChanged methods are not executed.

  • ViewPagerDisplay the first page, then cut the background log:
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
Copy the code

Then before cutting back to the foreground log:

04-17 16:53:40.749 D/tanzhenxing:11(22006): onStart
04-17 16:53:40.752 D/tanzhenxing:11(22006): onResume
Copy the code
  • ViewPagerDisplay the first page, then manually swipe to the second page log:
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
Copy the code

So it looks like we can use the onStart and onResume methods in the Fragment declaration cycle for lazy loading.

preload

Just write the data request in onCreateView or onStart and you can make an off-screen request to the interface.

FragmentStateAdapter

ViewPager2 inherited from RecyclerView, high probability FragmentStateAdapter inherited from RecyclerView.Adapter:

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

onCreateViewHolder

CreateViewHolder onCreateViewHolder onCreateViewHolder is a RecycleVeiw method to create a ViewHolder:

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

The FragmentViewHolder’s main role is to provide a container for the Fragment to use as a container via FrameLayout:

@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);
}
Copy the code

onBindViewHolder

OnBindViewHolder is a RecycleVeiw method for data binding:

@Override
public final void onBindViewHolder(final @NonNull FragmentViewHolder holder, int position) {
    /** ** ** /
	  ensureFragment(position);
    /** ** ** /
  	gcFragments();
}
Copy the code

EnsureFragment (Position), which will eventually call createFragment to create the current 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 FragmentFragment newFragment = createFragment(position); newFragment.setInitialSavedState(mSavedStates.get(itemId)); mFragments.put(itemId, newFragment); }}Copy the code

MFragments cache fragments created for use by placeFramentInViewholder; GcFragments recover fragments that are no longer used (the corresponding item has been deleted) to save memory.

onViewAttachedToWindow

OnViewAttachedToWindow is the ViewHolder callback that appears on the page.

@Override public final void onViewAttachedToWindow(@NonNull final FragmentViewHolder holder) { / / will FragmentViewHolder container with current fragments binding placeFragmentInViewHolder (holder); gcFragments(); }Copy the code

FragmentStateAdapter use

  • FragmentObject container;
  • productionfragmentIdentification of theid;
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
    }

    /** * Generate unique id from data ** When calling [notifyDataSetChanged], * * will throw new IllegalStateException("Fragment already Added ") */
    override fun getItemId(position: Int): Long {
        return data[position].toLong()
    }

    /** * Determines whether */ is added to the fragment corresponding to the current ID
    override fun containsItem(itemId: Long): Boolean {
        data.forEach {
            if (it.toLong() == itemId) {
                return true}}return false
    }
    override fun getItemCount(a): Int {
        return data.size
    }
}
Copy the code

In the

Obtaining the Fragment instance

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

Source code analysis:

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) {
        returnposition; }}Copy the code

Add “f” + holder.getitemID () TAG to Fragment.

Exception handling

  • A crash encountered during initialization;
Fragment HomeFragment{b793d14 (e67290fe-7ab1-4b2b-b98c-4e08d146644c)} has not been attached yet.
com.xxx.xxx.xxx.adapter.HomeFragmentStateAdapter.<init>(SourceFile:29)
Copy the code

If you encounter problems during development, you need to judge the Fragment state isAdded() when constructing a FragmentStateAdapter.

  • Crash encountered while updating data:
Fragment already added
Copy the code

Override the getItemId method that returns a value related to the data rather than the index of the data in the list. Because it represents the uniqueness of the Fragment, whether it can be reused.

ViewPager2 slide listen

public abstract static class OnPageChangeCallback {
    // When the current page starts to slide
    public void onPageScrolled(int position, float positionOffset,@Px int positionOffsetPixels) {}// When the page is selected
    public void onPageSelected(int position) {}// When the current page sliding state changes
    public void onPageScrollStateChanged(@ScrollState int state) {}}Copy the code

Implementation:

var pageChangeCallback = object : ViewPager2.OnPageChangeCallback() {
  override fun onPageSelected(position: Int) {
    // Postion must be greater than 0}}Copy the code

TabLayout+TabLayoutMediator

Easy to implement TAB and ViewPager slide or jump association.

implementation 'com. Google. Android. Material: material: 1.2.0'
Copy the code

It is recommended that the material version number be around 1.0.0, otherwise implementing custom TAB layout widths presents some problems.

Samples: ViewPager2 official website

DiffUtil is locally updated

DiffUtil and its difference algorithm

conclusion

This paper mainly introduces the use method of ViewPager2 with Fragment and the problems to be paid attention to in the use process, along with TabLayout, OnPageChangeCallback, DiffUtil and so on.

The article here is all about the end, if there are other need to exchange can leave a message oh ~! ~!