动态更新ViewPager?

336

我无法更新ViewPager中的内容。

在FragmentPagerAdapter类中,方法instantiateItem()和getItem()的正确使用方式是什么?

我之前只使用了getItem()来实例化和返回我的fragment:

@Override
public Fragment getItem(int position) {
    return new MyFragment(context, paramters);
}

这个方法很有效,但我无法更改内容。
所以我找到了这个:ViewPager PagerAdapter not updating the View

“我的方法是在instantiateItem()方法中为任何实例化的视图使用setTag()方法”

现在我想要实现instantiateItem()来做到这一点。但我不知道我必须返回什么(类型为Object),以及getItem(int position)之间的关系是什么?
我阅读了reference
  • public abstract Fragment getItem (int position)

    返回与指定位置相关联的Fragment。

  • public Object instantiateItem (ViewGroup container, int position)

    为给定的位置创建页面。适配器负责将视图添加到此处给定的容器中,尽管它只需确保在从finishUpdate(ViewGroup)返回时完成此操作。

    参数

    container 要显示该页的包含视图。 position 要实例化的页面位置。

    返回值

    返回表示新页面的对象。这不必是View,但可以是页面的其他容器。

但我仍然不理解。

这是我的代码。我正在使用support package v4。

ViewPagerTest

public class ViewPagerTest extends FragmentActivity {
    private ViewPager pager;
    private MyFragmentAdapter adapter; 

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.pager1);

        pager = (ViewPager)findViewById(R.id.slider);

        String[] data = {"page1", "page2", "page3", "page4", "page5", "page6"};

        adapter = new MyFragmentAdapter(getSupportFragmentManager(), 6, this, data);
        pager.setAdapter(adapter);

        ((Button)findViewById(R.id.button)).setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                reload();
            }
        });
    }

    private void reload() {
        String[] data = {"changed1", "changed2", "changed3", "changed4", "changed5", "changed6"};
        //adapter = new MyFragmentAdapter(getSupportFragmentManager(), 6, this, data);
        adapter.setData(data);
        adapter.notifyDataSetChanged();
        pager.invalidate();

        //pager.setCurrentItem(0);
    }
}

我的Fragment适配器

class MyFragmentAdapter extends FragmentPagerAdapter {
    private int slideCount;
    private Context context;
    private String[] data;

    public MyFragmentAdapter(FragmentManager fm, int slideCount, Context context, String[] data) {
        super(fm);
        this.slideCount = slideCount;
        this.context = context;
        this.data = data;
    }

    @Override
    public Fragment getItem(int position) {
        return new MyFragment(data[position], context);
    }

    @Override
    public int getCount() {
        return slideCount;
    }

    public void setData(String[] data) {
        this.data = data;
    }

    @Override
    public int getItemPosition(Object object) {
        return POSITION_NONE;
    }
}

我的Fragment

public final class MyFragment extends Fragment {
    private String text;

    public MyFragment(String text, Context context) {
        this.text = text;
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.slide, null);
        ((TextView)view.findViewById(R.id.text)).setText(text);

        return view;
    }
}

这里还有一个人遇到了类似的问题,但没有答案。 http://www.mail-archive.com/android-developers@googlegroups.com/msg200477.html


为了更好地理解涉及的组件,阅读我的有关带有单独后退导航历史记录的选项卡ViewPager的教程可能会有所帮助。它附带了GitHub上的工作示例和其他资源:https://medium.com/@nilan/separate-back-navigation-for-a-tabbed-view-pager-in-android-459859f607e4 - marktani
遇到了一些问题:http://stackoverflow.com/questions/40149039/viewpager-recyclerview-issue - albert
你可以在GitHub上查看这个示例。希望这个示例能对你有所帮助。 - Cabezas
好的简单解决方案:stackoverflow.com/a/35450575/2162226 - Gene Bo
Google最近推出了ViewPager2,它简化了动态Fragment/View的更新。您可以在https://github.com/googlesamples/android-viewpager2或此答案https://dev59.com/QGw05IYBdhLWcg3wZBAC#57393414上查看示例。 - Yves Delerm
21个回答

626
当使用FragmentPagerAdapter或FragmentStatePagerAdapter时,最好只处理getItem()方法,而不接触instantiateItem()方法。 PagerAdapter上的instantiateItem()-destroyItem()-isViewFromObject()接口是一个更低级别的接口,FragmentPagerAdapter使用它来实现更简单的getItem()接口。
在深入讨论之前,我应该澄清:
如果您想切换正在显示的实际片段,则需要避免使用FragmentPagerAdapter并使用FragmentStatePagerAdapter。
早期版本的答案错误地使用FragmentPagerAdapter作为示例 - 这将不起作用,因为FragmentPagerAdapter仅在首次显示后才会销毁片段。
我不推荐您链接的帖子中提供的setTag()和findViewWithTag()解决方法。正如您发现的那样,使用setTag()和findViewWithTag()与片段不兼容,因此不是很好匹配。
正确的解决方案是覆盖getItemPosition()方法。当调用notifyDataSetChanged()方法时,ViewPager会调用其适配器中所有项目的getItemPosition()方法,以查看它们是否需要移动到不同的位置或删除。
默认情况下,getItemPosition()方法返回POSITION_UNCHANGED,这意味着“该对象位于适当的位置,不要销毁或删除它。”返回POSITION_NONE则通过表明“我不再显示该对象”来解决问题。因此,它的效果是从适配器中删除并重新创建每个项目。
这是一个完全合法的解决方案!这个修复程序使notifyDataSetChanged()方法的行为类似于没有视图回收的常规适配器。如果您实现了此修复程序并且性能令人满意,则可以顺利进行。工作完成。
如果需要更好的性能,可以使用更精细的getItemPosition()方法实现。以下是基于字符串列表创建片段的分页器的示例:
ViewPager pager = /* get my ViewPager */;
// assume this actually has stuff in it
final ArrayList<String> titles = new ArrayList<String>();

FragmentManager fm = getSupportFragmentManager();
pager.setAdapter(new FragmentStatePagerAdapter(fm) {
    public int getCount() {
        return titles.size();
    }

    public Fragment getItem(int position) {
        MyFragment fragment = new MyFragment();
        fragment.setTitle(titles.get(position));
        return fragment;
    }

    public int getItemPosition(Object item) {
        MyFragment fragment = (MyFragment)item;
        String title = fragment.getTitle();
        int position = titles.indexOf(title);

        if (position >= 0) {
            return position;
        } else {
            return POSITION_NONE;
        }
    }
});

使用此实现方式,只有显示新标题的片段才会被显示出来。任何仍然在列表中显示标题的片段将移动到它们在列表中的新位置,不再在列表中的标题对应的片段将被销毁。

如果片段尚未重新创建,但仍需要进行更新怎么办?最好由片段本身处理对处于活动状态的片段的更新。毕竟,这就是具有片段自身控制器的优势所在。片段可以在onCreate()方法中向另一个对象添加监听器或观察器,然后在onDestroy()方法中将其删除,从而管理更新。你不必像为ListView或其他AdapterView类型的适配器一样将所有的更新代码放在getItem()方法中。

最后一件事 - 仅因为FragmentPagerAdapter不会销毁片段并不意味着在FragmentPagerAdapter中完全没有用处。你仍然可以使用该回调函数按顺序重排ViewPager中的片段。但它永远不会彻底从FragmentManager中删除它们。


4
还是不行,它仍未更新任何内容... 我贴出了我的代码。 - User
68
好的,问题已经找到了 - 你需要使用 FragmentStatePagerAdapter 而不是 FragmentPagerAdapter。FragmentPagerAdapter 从 FragmentManager 中永远不会移除片段,而只会分离和附加它们。查看文档以获取更多信息 - 通常,你只想为永久片段使用 FragmentPagerAdapter。我已经编辑了我的示例以进行更正。 - Bill Phillips
12
没错,将适配器的类更改为FragmentStatePagerAdapter解决了我所有的问题。谢谢! - User
1
如果你为此提交一个独立的问题,darkravedev,我认为你会得到更好的反馈。 - Bill Phillips
2
即使我在使用FragmentStatePagerAdapter时返回POSITION_NONE,我仍然可以看到它删除了我的片段并创建了新实例。但是,新实例接收到一个保存旧片段状态的savedInstanceState Bundle。如何完全销毁片段,以便在再次创建时Bundle为null? - Singed
显示剩余21条评论

149

不要从getItemPosition()返回POSITION_NONE并导致完全重建视图,而是应该这样做:

//call this method to update fragments in ViewPager dynamically
public void update(UpdateData xyzData) {
    this.updateData = xyzData;
    notifyDataSetChanged();
}

@Override
public int getItemPosition(Object object) {
    if (object instanceof UpdateableFragment) {
        ((UpdateableFragment) object).update(updateData);
    }
    //don't return POSITION_NONE, avoid fragment recreation. 
    return super.getItemPosition(object);
}

您的片段应实现UpdateableFragment接口:

public class SomeFragment extends Fragment implements
    UpdateableFragment{

    @Override
    public void update(UpdateData xyzData) {
         // this method will be called for every fragment in viewpager
         // so check if update is for this fragment
         if(forMe(xyzData)) {
           // do whatever you want to update your UI
         }
    }
}

和接口相关的内容:

接口:

public interface UpdateableFragment {
   public void update(UpdateData xyzData);
}

你的数据类:

public class UpdateData {
    //whatever you want here
}

1
这是否意味着您的MainActivity也需要实现该接口?请在这里帮助我。在我的当前实现中,我正在使用我的MainActivity类作为片段之间的通信器,因此我对这个解决方案感到困惑。 - Rakeeb Rajbhandari
1
好的,我已经在另一个地方回答了这个问题,并提供了完整的实现,其中我正在动态更新“Fragment”,请看:https://dev59.com/q3jZa4cB1Zd3GeqPkfg7#20089116 - M-Wajeeh
4
在过去2.5天的工作生活中,我看到的所有东西中,这是唯一对我有效的东西。各种感谢的话,比如“arigato”、“thanks”、“spaciba”、“sukran”等。 - Orkun
2
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Sebastián Guerrero
在同一个视图页中,它会对另一个片段产生一些影响!有人注意到了吗?我正在使用FragmentStatePagerAdapter。当我执行上述操作时,另一个片段的数据为null。 - Rethinavel
显示剩余13条评论

29

对于那些仍然面临我之前遇到的问题的人,当我有一个包含7个片段的ViewPager时。默认情况下,这些片段从API服务加载英文内容,但问题在于我想从设置活动更改语言,并在完成设置活动后,希望ViewPager在主活动中刷新片段以匹配用户的语言选择并加载阿拉伯语内容。

1- 必须像上面提到的那样使用FragmentStatePagerAdapter。

2- 在MainActivity中,我重写了onResume方法并执行了以下操作


(Note: The translated text may appear longer than the original text due to differences in sentence structure and grammar between languages.)
if (!(mPagerAdapter == null)) {
    mPagerAdapter.notifyDataSetChanged();
}

我在mPagerAdapter中重写了getItemPosition()方法,并将其返回值设置为POSITION_NONE

@Override
public int getItemPosition(Object object) {
    return POSITION_NONE;
}

运作得像魔力一样


1
除了一个情况外都可以正常工作。当您删除viewPager中的最后一个项目时,它不会从列表中移除。 - Vlado Pandžić
我正在动态地将片段添加到viewpager的当前片段的左侧和右侧。这个方法有效了!谢谢。 - h8pathak

17

我今天遇到了这个问题并最终解决了它,所以我写下了我学到的东西,希望对那些刚接触Android的ViewPager的人有所帮助,同时也作为我的更新记录。我在API 17中使用了FragmentStatePagerAdapter,目前只有2个片段。我认为肯定有些地方是不正确的,请指出来,谢谢。 输入图像描述

  1. 序列化数据必须加载到内存中。这可以使用CursorLoader/AsyncTask/Thread来完成。是否自动加载取决于你的代码。如果你正在使用CursorLoader,则会自动加载,因为已注册数据观察器。

  2. 在调用viewpager.setAdapter(pageradapter)之后,适配器的getCount()会不断被调用来构建片段。因此,如果正在加载数据,getCount()可能返回0,因此无需为没有数据显示创建虚拟片段。

  3. 数据加载完毕后,适配器将不会自动构建片段,因为getCount()仍然为0,因此我们可以将实际加载的数据数量设置为返回的getCount(),然后调用适配器的notifyDataSetChanged()ViewPager开始按照内存中的数据创建片段(仅前两个片段)。在返回notifyDataSetChanged()之前完成。然后ViewPager就有了你需要的正确片段。

  4. 如果数据库和内存中的数据都更新(写入到硬盘),或只有内存中的数据被更新(写回),或只有数据库中的数据被更新。在后两种情况下,如果没有自动从数据库加载数据到内存中(如上所述)。 ViewPager和页面适配器仅处理内存中的数据。

  • 因此,当内存中的数据被更新时,我们只需要调用适配器的notifyDataSetChanged()方法。由于片段已经被创建,所以在notifyDataSetChanged()返回之前,适配器的onItemPosition()方法将被调用。在getItemPosition()方法中不需要执行任何操作。然后数据被更新。


  • 我发现在更新数据(内存中)后,不需要做任何事情,只需调用notifyDataSetChanged(),然后就会自动更新,因为我使用的片段(带有图表视图)会自动刷新。如果不是这样的话,比如一个静态片段,我将不得不调用onItemPosition()并返回POSITION_NONE。对此表示抱歉。 - oscarthecat
    有人知道制作那个图表所用的工具是什么吗? - JuiCe

    13

    在您的代码中,在调用notifyDataSetChanged()后,请尝试对ViewPager使用destroyDrawingCache()


    3
    我也一样。非常好用,谢谢你。我尤其苦恼,因为我在我的视图翻页器中没有使用“片段”。所以感谢你! - Seth

    11

    在尝试了上述所有解决方案并尝试了其他类似问题的许多解决方案(如这个这个这个),都未能解决这个问题并使ViewPager销毁旧的Fragment并使用新的Fragment填充pager时,我已经找到了解决方法:

    1)ViewPager类扩展为FragmentPagerAdapter,如下所示:

     public class myPagerAdapter extends FragmentPagerAdapter {
    

    2) 创建一个项目 ViewPager ,它保存以下内容的titlefragment

    public class PagerItem {
    private String mTitle;
    private Fragment mFragment;
    
    
    public PagerItem(String mTitle, Fragment mFragment) {
        this.mTitle = mTitle;
        this.mFragment = mFragment;
    }
    public String getTitle() {
        return mTitle;
    }
    public Fragment getFragment() {
        return mFragment;
    }
    public void setTitle(String mTitle) {
        this.mTitle = mTitle;
    }
    
    public void setFragment(Fragment mFragment) {
        this.mFragment = mFragment;
    }
    
    }
    

    3) 修改 ViewPager 的构造函数,将我的FragmentManager实例传入并存储在我的class中,代码如下:

    private FragmentManager mFragmentManager;
    private ArrayList<PagerItem> mPagerItems;
    
    public MyPagerAdapter(FragmentManager fragmentManager, ArrayList<PagerItem> pagerItems) {
        super(fragmentManager);
        mFragmentManager = fragmentManager;
        mPagerItems = pagerItems;
    }
    

    4) 创建一个方法,通过直接从fragmentManager中删除所有先前的fragment,重新设置adapter数据为新数据,以便再次从新列表设置新的fragment

    public void setPagerItems(ArrayList<PagerItem> pagerItems) {
        if (mPagerItems != null)
            for (int i = 0; i < mPagerItems.size(); i++) {
                mFragmentManager.beginTransaction().remove(mPagerItems.get(i).getFragment()).commit();
            }
        mPagerItems = pagerItems;
    }
    

    5) 不要在容器ActivityFragment中重新初始化适配器,使用方法setPagerItems将新数据设置为以下内容:

    ArrayList<PagerItem> pagerItems = new ArrayList<PagerItem>();
        pagerItems.add(new PagerItem("Fragment1", new MyFragment1()));
        pagerItems.add(new PagerItem("Fragment2", new MyFragment2()));
    
        mPagerAdapter.setPagerItems(pagerItems);
        mPagerAdapter.notifyDataSetChanged();
    

    我希望它能够有所帮助。


    你能帮我解决这个问题吗?http://stackoverflow.com/questions/40149039/viewpager-recyclerview-issue - albert
    你能帮我解决这个问题吗?https://dev59.com/L1gR5IYBdhLWcg3wO7bX#41548210?noredirect=1#comment70327157_41548210 - viper

    9
    由于某些原因,我尝试了所有的答案都不起作用,所以我不得不在我的FragmentStatePagerAdapter中覆盖restoreState方法而不调用super。代码如下:
    private class MyAdapter extends FragmentStatePagerAdapter {
    
        // [Rest of implementation]
    
        @Override
        public void restoreState(Parcelable state, ClassLoader loader) {}
    
    }
    

    1
    在尝试了SO答案中的所有其他方法后,这是对我有效的方法。我正在finish()一个Activity,该Activity返回到托管PagerAdapter的Activity。在这种情况下,我希望重新创建所有片段,因为我刚刚完成的Activity可能已更改用于绘制片段的状态。 - JohnnyLambada
    哇!它运行了!它运行了!太棒了:D 在我的情况下,我需要删除ViewPager当前项目对象。但是,在尝试了所有上述方法/步骤后,当前片段没有更新。 - Rahul Khurana
    restoreState 在后续版本的 FragmentStatePagerAdapter 中被标记为 final。 - Jeffrey Blattman
    是的,这个答案已经有6年了。Pager的东西自那时以来已经得到了改进。 - hordurh

    3
    我稍微修改了Bill Phillips提供的解决方案以适应我的需求。
    private class PagerAdapter extends FragmentStatePagerAdapter{
        Bundle oBundle;
        FragmentManager oFragmentManager;
        ArrayList<Fragment> oPooledFragments;
    
    public PagerAdapter(FragmentManager fm) {
        super(fm);
        oFragmentManager=fm;
    
        }
    @Override
    public int getItemPosition(Object object) {
    
        Fragment oFragment=(Fragment)object;
        oPooledFragments=new ArrayList<>(oFragmentManager.getFragments());
        if(oPooledFragments.contains(oFragment))
            return POSITION_NONE;
        else
            return POSITION_UNCHANGED;
        } 
    }
    

    使getItemPosition()仅对那些当前在FragmentManager中的片段返回POSITION_NONE。(注意,此FragmentStatePager和与之关联的ViewPager包含在片段中而不是活动中)


    2

    使用 ViewPager2FragmentStateAdapter

    ViewPager2 支持动态更新数据。

    文档中有一个重要的提示,说明如何使其正常工作:

    注意: DiffUtil 实用程序类依赖于通过 ID 识别项。 如果您正在使用 ViewPager2 对可变集合进行分页,还必须重写 getItemId()containsItem() (强调我的)

    基于 ViewPager2 文档 和 Android 的 Github 示例项目,我们需要执行以下几个步骤:

    1. 设置FragmentStateAdapter并覆盖以下方法:getItemCountcreateFragmentgetItemIdcontainsItem(注意:ViewPager2不支持FragmentStatePagerAdapter

    2. 将适配器附加到ViewPager2

    3. 使用DiffUtilViewPager2分派列表更新(可以不使用DiffUtil,如示例项目中所示)


    例子:

    private val items: List<Int>
        get() = viewModel.items
    private val viewPager: ViewPager2 = binding.viewPager
    
    private val adapter = object : FragmentStateAdapter(this@Fragment) {
        override fun getItemCount() = items.size
        override fun createFragment(position: Int): Fragment = MyFragment()
        override fun getItemId(position: Int): Long = items[position].id
        override fun containsItem(itemId: Long): Boolean = items.any { it.id == itemId }
    }
    viewPager.adapter = adapter
        
    private fun onDataChanged() {
        DiffUtil
            .calculateDiff(object : DiffUtil.Callback() {
                override fun getOldListSize(): Int = viewPager.adapter.itemCount
                override fun getNewListSize(): Int = viewModel.items.size
                override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
                    viewPager.adapter?.getItemId(oldItemPosition) == viewModel.items[newItemPosition].id
    
                override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
                    areItemsTheSame(oldItemPosition, newItemPosition)
            }, false)
            .dispatchUpdatesTo(viewPager.adapter!!)
    }
    

    问题是在Java中为什么要用Kotlin来解决? - Teocci
    @Teocci很好,对于使用最新包解决问题的人来说非常容易。 - emen

    2
    我遇到了类似的问题,但不想依赖于现有的解决方案(硬编码标签名称等),并且我无法使M-WaJeEh的解决方案对我起作用。这是我的解决方案:
    我在一个数组中保留getItem中创建的片段的引用。只要活动没有因配置更改、缺乏内存或其他原因而被销毁(-->当返回到活动时,片段返回到它们的最后状态,而不会再次调用“getItem”,因此不会更新数组),这个方法就能正常工作。
    为了避免这个问题,我实现了instantiateItem(ViewGroup、int)并在那里更新我的数组,像这样:
            @Override
        public Object instantiateItem(ViewGroup container, int position) {
            Object o = super.instantiateItem(container, position);
            if(o instanceof FragmentX){
                myFragments[0] = (FragmentX)o;
            }else if(o instanceof FragmentY){
                myFragments[1] = (FragmentY)o;
            }else if(o instanceof FragmentZ){
                myFragments[2] = (FragmentZ)o;
            }
            return o;
        }
    

    所以,一方面我很高兴我找到了适合自己的解决方案,并想与你分享,但我也想问问是否有人尝试过类似的方法,是否有任何理由不这样做?目前对我来说非常有效...


    我在https://dev59.com/wWgv5IYBdhLWcg3wXPkt#23427215中做了相关的事情,尽管你可能还应该在`public void destroyItem(ViewGroup container, int position, Object object)中从myFragments`中删除该项,以免获取到过时的片段引用。 - qix

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接