Fragment换装ViewPager2

作者:fundroid

链接:

https://juejin.cn/post/6948249854724276231

 

1

开启ViewPager2之旅

 

 

图片

 

距离ViewPager2正式版的发布已经一年多了,目前ViewPager早已停止更新,官方鼓励使用ViewPager2替代。ViewPager2底层基于RecyclerView实现,因此可以获得RecyclerView带来的诸多收益:

 

  • 抛弃传统的PagerAdapter,统一了Adapter的API。

     

  • 通过LinearLayoutManager可以实现类似抖音的纵向滑动。

     

  • 支持DiffUtil,可以实现局部刷新。

     

  • 支持RTL(right-to-left),对于一些有出海需求的APP非常有用。

     

  • 支持ItemDecorator。

 

2

ViewPager2 + Fragment

 

 

跟ViewPager一样,除了View以外,ViewPager2更多的是配合Fragment使用,这需要借助于FragmentStateAdapter。

 

 

接下来,本文简单介绍一下FragmentStateAdapter的使用及实现原理:

 

首先在gradle中引入ViewPager2:

 

 implementation 'androidx.viewpager2:viewpager2:1.1.0'
 

然后在xml中布局:

 

<androidx.viewpager2.widget.ViewPager2
  android:id="@+id/doppelgangerViewPager"
  android:layout_width="match_parent"
  android:layout_height="match_parent" />
 

FragmentStateAdapter

 

import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.viewpager2.adapter.FragmentStateAdapter

class DoppelgangerAdapter(activity: AppCompatActivity, val doppelgangerList: List<DoppelgangerItem>) :
    FragmentStateAdapter(activity) {

  override fun getItemCount(): Int {
    return doppelgangerList.size
  }

  override fun createFragment(position: Int): Fragment {
    return DoppelgangerFragment.getInstance(doppelgangerList[position])
  }
}
 

FragmentStateAdapter的API跟旧的Adapter很相似:

 

  • getItemCount:返回Item的数量。

     

  • createFragment:用来根据position创建fragment。

     

  • DoppelgangerFragment:创建的具体Fragment类型。

 

MainActivity

 

在Activity中为ViewPager2设置Adapter:

 

val doppelgangerAdapter = DoppelgangerAdapter(this, doppelgangerList) 
doppelgangerViewPager.adapter = doppelgangerAdapter

 

图片

 

3

揭秘FragmentStateAdapter的实现

 

 

因为ViewPager2继承自RecyclerView,因此可以推断出FragmentStateAdapter继承自RecyclerView.Adapter:

 

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

虽说是继承关系,但两者的API却不一致,RecyclerView.Adapter关注的是ViewHolder的复用,而在FragmentStateAdapter中Framgent是不会复用的,即有多少个item就应该创建多少个Fragment,那么这其中是如何转换的呢?

 

onCreateViewHolder

 

通过FragmentStateAdapter声明中的泛型可以知道,ViewPager2之所以能够在RecyclerView的基础上对外屏蔽对ViewHolder的使用,其内部是借助FragmentViewHolder实现的。

onCreateViewHolder中会创建一个FragmentViewHolder:

 

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

FragmentViewHolder的主要作用是通过FrameLayout为Fragment提供用作容器的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

 

@Override
public final void onBindViewHolder(final @NonNull FragmentViewHolder holder, int position) {
  ...
  ensureFragment(position);
  ...
  gcFragments();
}
 

ensureFragment(position),其内部会最终会调用createFragment创建当前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缓存创建的Fragment,供后面placeFramentInViewholder使用; gcFragments回收已经不再使用的的Fragment(对应的item已经删除),节省内存开销。

 

placeFragmentInViewHolder

 

  @Override
    public final void onViewAttachedToWindow(@NonNull final FragmentViewHolder holder) {
        placeFragmentInViewHolder(holder);
        gcFragments();
    }
 

onViewAttachToWindow的时候调用placeFragmentInViewHolder,将FragmentViewHolder的container与当前Fragment绑定。

 

void placeFragmentInViewHolder(@NonNull final FragmentViewHolder holder) {
    Fragment fragment = mFragments.get(holder.getItemId());
    if (fragment == null) {
        throw new IllegalStateException("Design assumption violated.");
    }
    FrameLayout container = holder.getContainer();
    View view = fragment.getView();

...
    if (fragment.isAdded() && view.getParent() != null) {
        if (view.getParent() != container) {
            addViewToContainer(view, container);
        }
        return;
    }
...
}
 
void addViewToContainer(@NonNull View v, @NonNull FrameLayout container) {
    ...

    if (container.getChildCount() > 0) {
        container.removeAllViews();
    }

    if (v.getParent() != null) {
        ((ViewGroup) v.getParent()).removeView(v);
    }

    container.addView(v);
}
 

通过上面源码分析可以知道,虽然Fragment没有被复用,但是通过复用了ViewHolder的container实现了Framgent的交替显示。

 

4

滑动监听

 

 

监听页面滑动是一个常见需求,ViewPager2的API也发生了变化,使用OnPageChangeCallback:

 

 

使用效果如下:

 

var doppelgangerPageChangeCallback = object : ViewPager2.OnPageChangeCallback() {
  override fun onPageSelected(position: Int) {
    Toast.makeText(this@MainActivity, "Selected position: ${position}", 
      Toast.LENGTH_SHORT).show()
  }
}
 

OnPageChangeCallback同样也有三个方法:

 

1、onPageScrolled: 当前页面开始滑动时。

 

2、onPageSelected: 当页面被选中时。

 

3、onPageScrollStateChanged: 当前页面滑动状态变动时。

 

图片

 

5

纵向滑动

 

 

设置纵向滑动很简单,一行代码搞定。

 

doppelgangerViewPager.orientation = ViewPager2.ORIENTATION_VERTICAL

 

图片

 

源码也很简单。

 

/**
* Sets the orientation of the ViewPager2.
*
* @param orientation {@link #ORIENTATION_HORIZONTAL} or {@link #ORIENTATION_VERTICAL}
*/
public void setOrientation(@Orientation int orientation) {
  mLayoutManager.setOrientation(orientation);
  mAccessibilityProvider.onSetOrientation();
}

 

6

TabLayout

 

配合TabLayout的使用也是一个常见需求,TabLayout需要引入material库。

 

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

然后在xml中声明:

 

<com.google.android.material.tabs.TabLayout
  android:id="@+id/tabLayout"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:background="@color/colorPrimary"
  app:tabMode="scrollable"
  app:tabTextColor="@android:color/white" />
 

TabsLayoutMediator

 

要关联TabLayout和ViewPager2需要借助TabLayoutMediator:

 

public TabLayoutMediator(
  @NonNull TabLayout tabLayout,
  @NonNull ViewPager2 viewPager,
  @NonNull TabConfigurationStrategy tabConfigurationStrategy) {
  this(tabLayout, viewPager, true, tabConfigurationStrategy);
}
 

其中,TabConfigurationStrategy定义如下:根据position配置当前tab。

 

/**
* A callback interface that must be implemented to set the text and styling of newly created
* tabs.
*/
public interface TabConfigurationStrategy {
  /**
   * Called to configure the tab for the page at the specified position. Typically calls {@link
   * TabLayout.Tab#setText(CharSequence)}, but any form of styling can be applied.
   *
   * @param tab The Tab which should be configured to represent the title of the item at the given
   *     position in the data set.
   * @param position The position of the item within the adapter's data set.
   */
  void onConfigureTab(@NonNull TabLayout.Tab tab, int position);
}
 

在MainActivity中具体使用如下:

 

TabLayoutMediator(tabLayout, doppelgangerViewPager) { tab, position ->
  //To get the first name of doppelganger celebrities
  tab.text = doppelgangerList[position].title
}.attach()
 

attach方法很关键,经过前面一系列配置后最终需要通过它关联两个组件。

 

加入TabLayout后的最终效果如下:

 

图片

 

7

DiffUtil 局部更新

 

 

RecyclerView基于DiffUtil可以实现局部更新,如今,FragmentStateAdapter也可以对Fragment实现局部更新。

 

首先定义DiffUtil.Callback:

 

class PagerDiffUtil(private val oldList: List<DoppelgangerItem>, private val newList: List<DoppelgangerItem>) : DiffUtil.Callback() {

    enum class PayloadKey {
        VALUE
    }

    override fun getOldListSize() = oldList.size

    override fun getNewListSize() = newList.size

    override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        return oldList[oldItemPosition].id == newList[newItemPosition].id
    }

    override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        return oldList[oldItemPosition].value == newList[newItemPosition].value
    }

    override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
        return listOf(PayloadKey.VALUE)
    }
}
 

然后在Adapter中使用DiffUtil更新数据:

 
class DoppelgangerAdapter(private val activity: FragmentActivity) : FragmentStateAdapter(activity) {

    private val items: ArrayList<DoppelgangerItem> = arrayListOf()


    override fun createFragment(position: Int): Fragment {
        return DoppelgangerFragment.getInstance(doppelgangerList[position])
    }

    override fun getItemCount() = items.size

    override fun getItemId(position: Int): Long {
        return items[position].id.toLong()
    }

    override fun containsItem(itemId: Long): Boolean {
        return items.any { it.id.toLong() == itemId }
    }

    fun setItems(newItems: List<PagerItem>) {
        //不借助DiffUtil更新数据
        //items.clear()
        //items.addAll(newItems)
        //notifyDataSetChanged()

        //使用DiffUtil更新数据
        val callback = PagerDiffUtil(items, newItems)
        val diff = DiffUtil.calculateDiff(callback)
        items.clear()
        items.addAll(newItems)
        diff.dispatchUpdatesTo(this)
    }
}

 

8

总结

 

本文主要介绍了ViewPager2配合Fragment的使用方法以及FragmentStateAdapter的实现原理,顺带介绍了TabLayout、OnPageChangeCallback、DiffUtil等常见功能的用法。


ViewPager2的使用非常简单,在性能以及使用体验等各方面都要优于传统的ViewPager,没尝试的小伙伴抓紧用起来吧~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值