查看原文
其他

ViewPager 刷新无效?

川峰 鸿洋 2019-04-05

点击上方“鸿洋”,选择“置顶公众号”

优质技术文第一时间送达


本文作者


作者:川峰

链接:

https://blog.csdn.net/lyabc123456/article/details/79797552

本文由作者授权发布。


1概述


最近在重构项目的时候有个地方想要做一个更换FragmentPagerAdapter中的Fragment的功能,按照通常使用ListView的习惯做法,如果你只是更新保存Fragment的List数据,然后调用adapter的notifyDataSetChanged()是不会起作用的(下面会分析原因)。 


搜索了下发现此问题普遍存在,多数是说先移除Fragment再notifyDataSetChanged(),因为FragmentPagerAdapter内部会缓存Fragment,但是经测试发现仅仅这样干是不行的。于是经过一番折腾,参考了各种方案之后我整理了一个可行的方案,本文做一个记录,以便后续参考,也方便各位道友参考。


2分析问题


下面来分析一下此问题的主要原因:


这可能是Android一个BUG, 与此问题相关的主要有两个方法:


  • getItemPosition()

  • instantiateItem()


搞清楚这两个方法的作用基本就知道如何解决了,先来看第一个方法 

这个是在PagerAdapter中的getItemPosition()源码的说明,从它的英文注释我们可以清楚的知道,这个方法的返回值的意思是:如果给定的item的position没有发生改变,那么就返回POSITION_UNCHANGED, 如果给定的item在adapter当中指定位置不再呈现了,那么就返回POSITION_NONE。默认返回的是POSITION_UNCHANGED。


OK, 导致这个问题的一个主要原因我们已经知道了,所以,默认我们是要重写这个方法的,不然总是返回POSITION_UNCHANGED,那当然是不会更新的了。


其实在使用viewpager包含普通view界面的时候我们应该会经常遇到这个问题的。那么, 这个问题的解决思路就有了: 我们就按照它要求的意思来实现当position发生变化的item我们都返回POSITION_NONE,而position没有发生变化的item我们就返回POSITION_UNCHANGED。


那怎么来实现呢,我们简单来想一下,首先我要记录更新之前的每个item对应的position,然后在更新Fragment列表数据之后,我们再把当前的每一个item的position跟之前的去比对一遍,这样我们就能知道到底哪个item的position发生了变化,哪个的position依然没变了。当然前提是比对的item是相同的item, 如果更新之后item都不存在了,那自然要返回POSITION_NONE了。 


好,我们这里就简单的思路设想一下,后面我会给出完整代码。


到这里包含普通view的viewpager的adapter刷新问题应该可以解决了,注意,这里很多人的暴力做法是在getItemPosition()当中直接返回POSITION_NONE,这样不是不可以,只不过这样做会默认把所有的view都重新销毁重建,那肯定不是我想要的理想的情况。


接下来再看另一个方法: 

这个是在FragmentPagerAdapter的源码当中的,可以看到在instantiateItem()方法的内部,它是这样做的:根据tag查找对应的Fragment, 如果找到,那么就通过当前的Transaction进行attach操作,这个fragment就会显示了,如果没有找到呢,就去getItem()从你的Fragment列表中获取一个然后Transaction进行add操作。


所以看到这里就恍然大悟了,为啥我list里面的fragment都换了新的了但就是不刷新呢,问题就在这里了,只要它能findFragmentByTag找得到那么就不会用你的列表中的fragment, 还是用之前的。


那么,到这里首先想到的就是,我们在更换或者删除列表中对应的Fragment时,同时也要将该Fragment从Transaction当中移除,这样就能够确保在刷新数据时adpater会从我们更新后的list中去获取fragment而不是用之前缓存的。


是不是这样?对不对?嗯,应该是没有问题的,好,想到这里那么我们就可以了,加上前面getItemPosition()的思路,应该是能够解决问题的了。假设你按照前面的思路完善了FragmentPagerAdapter的代码并准备测试(或者你可以直接往下拖查看完整的代码),你会悲剧的发现,在更换某一个fragment的时候是没有问题的,但是在删除某一个fragment时是会出现问题的,会发生crash! 抛出如下异常: 

哎,没办法,江湖就是如此险恶,到处都是坑。。 
那么究竟为什么发生crash呢,如果你查看该crash异常栈,我们可以在源码中搜素一下找到: 

没错,就是在高亮的这一行,如果你按照前面介绍的方法写好FragmentPagerAdapter 运行测试了,你就会发现抛出”Can’t change tag of fragment “的异常,我们可以发现上述的异常是在beginTransaction()之后进行add操作发生的,异常出现的判断条件是fragment.mTag != null &&!tag.equals(fragment.mTag),这里的tag就是add时传入的tag参数, 而mTag是要添加的frgament的tag, 这说明这个fragment之前被添加过,因为下面一行fragment.mTag = tag;我们知道只有添加过的fragment的mTag才不会为null。


那问题肯定是跟tag有关了,我们回到instantiateItem()方法的源码,可以看到不管是add操作还是findFragmentByTag时的tag都是通过一个方法生成的:  makeFragmentName(), 都是这个方法生成的tag, 而这个方法生成tag的办法是getItemId()和viewId的组合, viewId应该就是我们的fragment的id了,而getItemId(): 

它默认实现就是简单的返回position,所以tag是由fragment的id+position组成的。 


那我们来分析一下,删除的时候为啥会出现”Can’t change tag of fragment “的异常,先画个简图: 
 假设初始时我们viewpager当中有4个Fragment分别是A B C D, 那么按照instantiateItem()源码中的tag生成方法,这四个fragment被add之后对应四个fragment中的mTag值应该分别就是:A0、B1、C2、D3(假设就用ABCD代表他们的fragment的id),好,现在我们把B对应的Fragment删除掉(注意此时我们已经按照前面已发现的解决方案实现了的代码): 

此时列表中只剩下A C D三个Fragment, 那么前面提到过,此时getItemPpsition()方法我们应该做的是A对应的Fragment返回POSITION_UNCHANGED, 因为A的位置没有发生变化,而B(已删除)、 C(移位) 、 D(移位) 三个我们应该返回POSITION_NONE,因此我们的adapter在刷新的时候刷新到第二个位置时会再首先去查找对应tag的Fragment: 


此时查找的tag是C1,然而找不到,因为C前面add的tag是C2,所以走else, 在else当中就会从我们的列表中去get第1个item,那取到的自然是C,然后对C进行add操作,这时又会生成C对应的tag传入add()方法,但是此时,注意了,生成C的tag的方法生成的结果是C1(fragment的id+当前position),分析到这里你可能发现了,前面我们的C是被add过的,所以之前C的mTag是C2,到了这里add操作时要变成C1了!


所以跟着源码走进去自然就符合前面“Can’t change tag of fragment “异常的判断条件fragment.mTag != null &&!tag.equals(fragment.mTag),我们的C之前的mTag不为空并且C1 != C2,所以中标了!


那么解决问题的方法,首先想到的是为每一个Fragment设置一个唯一的tag值,但是mTag在Fragment源码中是protected的,我们是不能改的。。。所以只能去改生成tag的方法makeFragmentName()了,但是一看这个方法又是private的,又不能改。。。。我MMP…好吧,再看,因为makeFragmentName()方法用到了getItemId()的返回值,而getItemId()我们是可以重写的,所以那去只能改getItemId()方法了:


@Override
public long getItemId(int position) {
   // return position;
   return 我们自定义的可以确定当前item的唯一值;
}


因为前面提到过getItemId()方法默认返回的是position,所以我们这个方法要修改一下,返回一个唯一的值,一个可以标志这个fragment的唯一值就可以了,这样在删除操作position发生变化之后,C的tag值经过makeFragmentName()生成的结果总是C+uniqueId, 所以应该不会有问题了。


好了,至此所有问题思路解决完毕,贴一下完善FragmentPagerAdapter的完整代码:

/**
* 加载显示Fragment的ViewPagerAdapter基类
* 提供可以刷新的方法
*
* @author Fly
* @e-mail 1285760616@qq.com
* @time 2018/3/22
*/

public class BaseFragmentPagerAdapter extends FragmentPagerAdapter {
   private List<BaseFragment> mFragmentList;
   private FragmentManager mFragmentManager;
   /**下面两个值用来保存Fragment的位置信息,用以判断该位置是否需要更新*/
   private SparseArray<String> mFragmentPositionMap;
   private SparseArray<String> mFragmentPositionMapAfterUpdate;
   public BaseFragmentPagerAdapter(FragmentManager fm, List<BaseFragment> fragments) {
       super(fm);
       mFragmentList = fragments;
       mFragmentManager = fm;
       mFragmentList = fragments;
       mFragmentPositionMap = new SparseArray<>();
       mFragmentPositionMapAfterUpdate = new SparseArray<>();
       setFragmentPositionMap();
       setFragmentPositionMapForUpdate();
   }
   /**
    * 保存更新之前的位置信息,用<hashCode, position>的键值对结构来保存
    */

   private void setFragmentPositionMap() {
       mFragmentPositionMap.clear();
       for (int i = 0; i < mFragmentList.size(); i++) {
           mFragmentPositionMap.put(Long.valueOf(getItemId(i)).intValue(), String.valueOf(i));
       }
   }
   /**
    * 保存更新之后的位置信息,用<hashCode, position>的键值对结构来保存
    */

   private void setFragmentPositionMapForUpdate() {
       mFragmentPositionMapAfterUpdate.clear();
       for (int i = 0; i < mFragmentList.size(); i++) {
           mFragmentPositionMapAfterUpdate.put(Long.valueOf(getItemId(i)).intValue(),  String.valueOf(i));
       }
   }
  /**
   * 在此方法中找到需要更新的位置返回POSITION_NONE,否则返回POSITION_UNCHANGED即可
   */

   @Override
   public int getItemPosition(Object object)
{
       int hashCode = object.hashCode();
       //查找object在更新后的列表中的位置
       String position = mFragmentPositionMapAfterUpdate.get(hashCode);
       //更新后的列表中不存在该object的位置了
       if (position == null) {
           return POSITION_NONE;
       } else {
           //如果更新后的列表中存在该object的位置, 查找该object之前的位置并判断位置是否发生了变化
           int size = mFragmentPositionMap.size();
           for (int i = 0; i < size ; i++) {
               int key = mFragmentPositionMap.keyAt(i);
               if (key == hashCode) {
                   String index = mFragmentPositionMap.get(key);
                   if (position.equals(index)) {
                       //位置没变依然返回POSITION_UNCHANGED
                       return POSITION_UNCHANGED;
                   } else {
                       //位置变了
                       return POSITION_NONE;
                   }
               }
           }
       }
       return POSITION_UNCHANGED;
   }
   /**
    * 将指定的Fragment替换/更新为新的Fragment
    * @param oldFragment 旧Fragment
    * @param newFragment 新Fragment
    */

   public void replaceFragment(BaseFragment oldFragment, BaseFragment newFragment) {
       int position = mFragmentList.indexOf(oldFragment);
       if (position == -1) {
           return;
       }
       //从Transaction移除旧的Fragment
       removeFragmentInternal(oldFragment);
       //替换List中对应的Fragment
       mFragmentList.set(position, newFragment);
       //刷新Adapter
       notifyItemChanged();
   }
   /**
    * 将指定位置的Fragment替换/更新为新的Fragment,同{@link #replaceFragment(BaseFragment oldFragment, BaseFragment newFragment)}
    * @param position    旧Fragment的位置
    * @param newFragment 新Fragment
    */

   public void replaceFragment(int position, BaseFragment newFragment) {
       BaseFragment oldFragment = mFragmentList.get(position);
       removeFragmentInternal(oldFragment);
       mFragmentList.set(position, newFragment);
       notifyItemChanged();
   }
   /**
    * 移除指定的Fragment
    * @param fragment 目标Fragment
    */

   public void removeFragment(BaseFragment fragment) {
       //先从List中移除
       mFragmentList.remove(fragment);
       //然后从Transaction移除
       removeFragmentInternal(fragment);
       //最后刷新Adapter
       notifyItemChanged();
   }
   /**
    * 移除指定位置的Fragment,同 {@link #removeFragment(BaseFragment fragment)}
    * @param position
    */

   public void removeFragment(int position) {
       BaseFragment fragment = mFragmentList.get(position);
       //然后从List中移除
       mFragmentList.remove(fragment);
       //先从Transaction移除
       removeFragmentInternal(fragment);
       //最后刷新Adapter
       notifyItemChanged();
   }
   /**
    * 添加Fragment
    * @param fragment 目标Fragment
    */

   public void addFragment(BaseFragment fragment) {
       mFragmentList.add(fragment);
       notifyItemChanged();
   }
   /**
    * 在指定位置插入一个Fragment
    * @param position 插入位置
    * @param fragment 目标Fragment
    */

   public void insertFragment(int position, BaseFragment fragment) {
       mFragmentList.add(position, fragment);
       notifyItemChanged();
   }
   private void notifyItemChanged() {
       //刷新之前重新收集位置信息
       setFragmentPositionMapForUpdate();
       notifyDataSetChanged();
       setFragmentPositionMap();
   }
   /**
    * 从Transaction移除Fragment
    * @param fragment 目标Fragment
    */

   private void removeFragmentInternal(BaseFragment fragment) {
       FragmentTransaction transaction = mFragmentManager.beginTransaction();
       transaction.remove(fragment);
       transaction.commitNow();
   }
   /**
    * 此方法不用position做返回值即可破解fragment tag异常的错误
    */

   @Override
   public long getItemId(int position)
{
       // 获取当前数据的hashCode,其实这里不用hashCode用自定义的可以关联当前Item对象的唯一值也可以,只要不是直接返回position
       return mFragmentList.get(position).hashCode();
   }
   @Override
   public Fragment getItem(int position)
{
       return mFragmentList.get(position);
   }
   @Override
   public int getCount()
{
       return mFragmentList.size();
   }
   public List<BaseFragment> getFragments() {
       return mFragmentList;
   }
}


好了,现在这个类可以用来实现Fragment列表中的Fragment替换、删除、添加等操作了,并且可以实时刷新adapter, 你可以试验一下。


3测试

测试代码: 

Activity代码

public class TestActivity extends FragmentActivity implements View.OnClickListener {
   List<Fragment> mFragmentList;
   ViewPager mViewPager;
   public BaseFragmentPagerAdapter mAdapter;
   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_test);
       mViewPager = findViewById(R.id.vp);
       findViewById(R.id.btn_change).setOnClickListener(this);
       mFragmentList = new ArrayList<>();
       mFragmentList.add(getFg("AAA"));
       mFragmentList.add(getFg("BBB"));
       mFragmentList.add(getFg("CCC"));
       mFragmentList.add(getFg("DDD"));
       mAdapter = new BaseFragmentPagerAdapter(getSupportFragmentManager(), mFragmentList);
       mViewPager.setAdapter(mAdapter);
   }
   private TestFragment getFg(String a){
       TestFragment fragment = new TestFragment();
       fragment.setTest(a);
       return fragment;
   }
   @Override
   public void onClick(View view) {
       TestFragment eee = getFg("EEE");
       //新增
       mAdapter.addFragment(eee);
       //插入
       mAdapter.insertFragment(1, eee);
       //删除
       mAdapter.removeFragment(1);
       //删除
       mAdapter.removeFragment(mFragmentList.get(1));
       //替换
       mAdapter.replaceFragment(1, eee);
       //替换
       mAdapter.replaceFragment(mFragmentList.get(0), eee);
   }
}


用到的TestFragment:

public class TestFragment extends Fragment {
   private final static String TAG = TestFragment.class.getSimpleName();
   private String test;
   public View mContentView;
   public void setTest(String test) {
       this.test = test;
   }
   @Override
   public void onCreate(@Nullable Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       Log.e(TAG, "onCreate:  test = "+test);
   }
   @Nullable
   @Override
   public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
       mContentView = inflater.inflate(R.layout.layout_fg, null);
       Log.e(TAG, "onCreateView: test = "+test);
       return mContentView;
   }
   @Override
   public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
       Log.e(TAG, "onViewCreated: test = "+test);
       TextView testText = mContentView.findViewById(R.id.tv_test);
       testText.setText(test);
   }
   @Override
   public void onActivityCreated(@Nullable Bundle savedInstanceState) {
       super.onActivityCreated(savedInstanceState);
       Log.e(TAG, "onActivityCreated: test = "+test);
   }
   @Override
   public void onDestroy() {
       super.onDestroy();
       Log.e(TAG, "onDestroy:  test = "+test);
   }
}


布局文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
             android:orientation="vertical"
   >

   <android.support.v4.view.ViewPager
       android:id="@+id/vp"
       android:layout_width="match_parent"
       android:layout_height="0dp"
       android:layout_weight="1"
       android:layout_gravity="center"
       />

   <Button
       android:id="@+id/btn_change"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_gravity="center_horizontal"
       android:text="btn_change"
       />

</LinearLayout>


布局文件很简单就是一个viewpager+一个button, 然后我们在Activity当中点击这个button对vp的adapter所使用的fragment列表进行操作,并观察变化。


注意,封装的Adapter类提供了新增、插入、删除、替换几种方法的重载,可以通过指定的位置或者fragment进行操作,在onClick()中测试时,注释其他的情况,只测试一种情况即可。


另外,我们在TestFragment中的生命周期方法中添加了Log日志,以便观察结果。


运行代码测试你会发现,当替换掉列表中的一个Fragment时,左右两边的Fragment生命周期是不会被调用的。这符合我们的预期,因为替换时左右两边的Fragment在viewpager中的位置没有发生变化,所以它们不会被销毁重建。


当你删除或者插入一个Fragment时,当前Fragment后面的Fragment会走重新创建view的生命周期方法,而当前Fragment前面的Fragment不会,这也符合我们的预期,但为啥后面的会重建,而前面的不会?别忘了我们使用的viewpager是有默认预加载当前页面左右两边的view的特性的,所以这个也属于正常的现象,如果viewpager预加载给你造成了困扰,我们可以通过其它方式来避免,当然这是另外的话题了。


参考: 
我在寻找解决方案的过程中参考了这个:

https://stackoverflow.com/questions/10396321/remove-fragment-page-from-viewpager-in-android#


推荐阅读

实现黑客帝国数字雨 | 自定义View

阿里春招Android面经



如果你想要跟大家分享你的文章,欢迎投稿~


图书信息已经收集完毕,今天就会交给出版社了,申请了14个好友,有个没搜索到~~谢谢大家支持。

┏(^0^)┛明天见!

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存