zoukankan      html  css  js  c++  java
  • ##解决 ViewPager 调用 notifyDataSetChanged()无刷新:原理、解决办法##

    一、原理

    转自:http://www.cnblogs.com/maoyu417/p/3740209.html

    转载 http://www.67tgb.com/?p=624

    最近项目结束,搞了一次代码分享。其中一位同学分享了一下自己在解决问题过程中的一些心得体会,感觉受益匪浅。整理出来,分享给大家。

      建议使用自己编译的android os和虚拟机,这样就可以调试android系统中的任何组件。简单说来,深入android源码,去寻找解决问题的答案。这事儿说起来简单,实际做起来还是有些难度的。我也曾经尝试着去看过,没看一会儿就晕了。

      所以还是有针对性的去看源码,效率会高一些。

      废话不多说,先看第一个示例。 

    Viewpager在调用notifyDataSetChanged()时,界面无刷新。

      相信很多做过Viewpager的同学肯定遇到过这个问题,这个是bug还是android就是如此设计的,我们不做讨论。总之,它确实影响我们功能的实现了。

      可能不少同学选择为Viewpager重新设置一遍适配器adapter,达到刷新的目的。但是这种方法在大多数情况下,是有问题的。

    追踪源代码

      为什么调用数据更新的方法,Viewpager却没有更新呢,我们跟进该方法的源代码看一下。

      首先查看适配器调用的super.notifyDataSetChanged(),该方法调到抽象基类PagerAdapter.notifyDataSetChanged()中:

        /**
         * This method should be called by the application if the data backing this adapter has changed
         * and associated views should update.
         */
        public void notifyDataSetChanged() {
            mObservable.notifyChanged();
        }

    注释里说到,当附加在适配器上的数据发生变化时,应该调用该方法刷新数据。该方法调用了一个mObservable .notifyChanged();

      我们继续跟进这个方法,进入DataSetObservable类中,发现这样一段代码:

      /**
         * Invokes {@link DataSetObserver#onChanged} on each observer.
         * Called when the contents of the data set have changed.  The recipient
         * will obtain the new contents the next time it queries the data set.
         */
        public void notifyChanged() {
            synchronized(mObservers ) {
                // since onChanged() is implemented by the app, it could do anything, including
                // removing itself from {@link mObservers} - and that could cause problems if
                // an iterator is used on the ArrayList {@link mObservers}.
                // to avoid such problems, just march thru the list in the reverse order.
                for (int i = mObservers .size() - 1; i >= 0; i--) {
                    mObservers.get(i).onChanged();
                }
            }
        }
     

       这都不是重点,重点我们来看这个mObservers的类型是一个抽象类DataSetObserver,里面只有两个未实现的方法,都有谁使用了这个抽象类呢,快捷键 ctrl + alt + H ,在众多的调用者当中,我们发现了Viewpager的身影

        进入viewpager,我们终于找到了viewpager中控制数据变更的重点方法dataSetChanged ,这个方法如下:

          void dataSetChanged () {
            // This method only gets called if our observer is attached, so mAdapter is non-null.
     
            boolean needPopulate = mItems .size() < mOffscreenPageLimit * 2 + 1 &&
                    mItems.size() < mAdapter.getCount();
            int newCurrItem = mCurItem ;
     
            boolean isUpdating = false;
            for (int i = 0; i < mItems.size(); i++) {
                final ItemInfo ii = mItems .get(i);
                final int newPos = mAdapter.getItemPosition(ii.object );
     
                if (newPos == PagerAdapter.POSITION_UNCHANGED ) {
                    continue;
                }
     
                if (newPos == PagerAdapter.POSITION_NONE) {
                    mItems.remove(i);
                    i--;
     
                    if (!isUpdating) {
                        mAdapter.startUpdate( this);
                        isUpdating = true;
                    }
     
                    mAdapter.destroyItem( this, ii.position , ii.object);
                    needPopulate = true;
     
                    if (mCurItem == ii.position ) {
                        // Keep the current item in the valid range
                        newCurrItem = Math. max(0, Math.min(mCurItem, mAdapter.getCount() - 1));
                        needPopulate = true;
                    }
                    continue;
                }
     
                if (ii.position != newPos) {
                    if (ii.position == mCurItem ) {
                        // Our current item changed position. Follow it.
                        newCurrItem = newPos;
                    }
     
                    ii. position = newPos;
                    needPopulate = true;
                }
            }
     
            if (isUpdating) {
                mAdapter.finishUpdate( this);
            }
     
            Collections. sort(mItems, COMPARATOR);
     
            if (needPopulate) {
                // Reset our known page widths; populate will recompute them.
                final int childCount = getChildCount();
                for (int i = 0; i < childCount; i++) {
                    final View child = getChildAt(i);
                    final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                    if (!lp.isDecor ) {
                        lp. widthFactor = 0.f;
                    }
                }
     
                setCurrentItemInternal(newCurrItem, false, true);
                requestLayout();
            }
        }
     

      重点看这样一行代码:

     final int newPos = mAdapter.getItemPosition(ii.object );
     
       if (newPos == PagerAdapter.POSITION_UNCHANGED ) {
             continue ;
       }
     

      官方对getItemPosition()的解释是:

      Called when the host view is attempting to determine if an item’s position has changed. Returns POSITION_UNCHANGED if the position of the given item has not changed orPOSITION_NONE if the item is no longer present in the adapter.

    The default implementation assumes that items will never change position and always returns POSITION_UNCHANGED.

      意思是如果item的位置如果没有发生变化,则返回POSITION_UNCHANGED。如果返回了POSITION_NONE,表示该位置的item已经不存在了。默认的实现是假设item的位置永远不会发生变化,而返回POSITION_UNCHANGED

    解决方案:

      所以我们可以尝试着修改适配器的写法,覆盖getItemPosition()方法,当调用notifyDataSetChanged时,让getItemPosition方法人为的返回POSITION_NONE,从而达到强迫viewpager重绘所有item的目的。

      具体代码如下:

    class SearchAdapter extends PagerAdapter {
        
         private int mChildCount = 0;
     
         @Override
         public void notifyDataSetChanged() {         
               mChildCount = getCount();
               super.notifyDataSetChanged();
         }
     
         @Override
         public int getItemPosition(Object object)   {          
               if ( mChildCount > 0) {
               mChildCount --;
               return POSITION_NONE;
               }
               return super.getItemPosition(object);
         }
    }



    二、解决办法
    转自:http://www.aiuxian.com/article/p-2786024.html

    Google在Android 3.0SDK中推出的ViewPager控件很大程度上满足了开发者开发页面左右移动切换的功能,使用非常方便。但是使用中发现,在删除或者修改数据的时候,PagerAdapter无法像BaseAdapter那样仅通过notifyDataSetChanged方法通知刷新View。
    最基本的方法
    针对于child view比较简单的情况(例如仅有TextView、ImageView等,没有ListView等展示数据的情况),可以在自己的Adapter中加入代码:

    [java] view plain copy print ? 在CODE上查看代码片 派生到我的代码片
    1. @Override
    2. public int getItemPosition(Object object) {
    3. return POSITION_NONE;
    4. }
     
    这样既可达到一般情况下要求的效果。 
    存在的问题: 
    这不是PagerAdapter中的Bug,通常情况下,调用notifyDataSetChanged方法会让ViewPager通过Adapter的getItemPosition方法查询一遍所有child view,这种情况下,所有child view位置均为POSITION_NONE,表示所有的child view都不存在,ViewPager会调用destroyItem方法销毁,并且重新生成,加大系统开销,并在一些复杂情况下导致逻辑问题。特别是对于只是希望更新child view内容的时候,造成了完全不必要的开销。 
    更有效地方法: 
    更为靠谱的方法是因地制宜,根据自己的需求来实现notifyDataSetChanged的功能,比如,在仅需要对某个View内容进行更新时,在instantiateItem()时,用View.setTag方法加入标志,在需要更新信息时,通过findViewWithTag的方法找到对应的View进行更新即可。 

    使用ViewPager做滑动切换图片的效果时,如果图片是从网络下载的,那么再子线程中下载完图片时我们会使用handler通知UI线程,然后UI线程就可以调用mViewPager.getAdapter().notifyDataSetChanged()进行页面的刷新,但是viewpager不同于listview,你会发现单纯的调用notifyDataSetChanged()并不能刷新页面。先说说Viewpager的刷新过程:在每次调用notifyDataSetChanged()时,都会激活getItemPosition(Object object)方法,该方法会遍历viewpager的所有item(据我debug的结果,只有当前页和其左右加起来共3页被遍历了,待确定),为每个item返回一个状态值(POSITION_NONE/POSITION_UNCHANGED),如果是none,那么该item会被destroyItem(ViewGroup container, int position, Object object)方法remove掉,然后重新加载,如果是unchanged,就不会重新加载,默认是unchanged,所以我国我们不重写getItemPosition(Object object),就无法看到刷新效果。解决方法有两种
    第一种网上比较容易查找到:重写PagerAdapter的getItemPosition(Object object)方法,使其返回POSITION_NONE

    Java代码 复制代码 链接地址
    1. @Override
    2. public int getItemPosition(Object object) {
    3. return POSITION_NONE;
    4. }
     

    这种方法的弊端大家都很容易看出来,我不需要刷新的项目也被重新加载了,浪费系统资源; 
    第二种更合理,当然相对前一种要再多做点事:思路是在instantiateItem时给每个view加上tag,然后在需要刷新页面时通过View.getTag()来判断是否是我们想要刷新的页面,只给当前页面返回POSITION_NONE。 
    Java代码 复制代码 链接地址
    1. /**
    2. * DispImgAdapter.java
    3. */
    4. @Override
    5. public Object instantiateItem(ViewGroup container, int position) {
    6. iv = new ImageView(mContext);
    7. iv.setTag(position); // Add tag
    8. try {
    9. Bitmap bm = cacheImg2(position);
    10. iv.setImageBitmap(bm);
    11. catch (OutOfMemoryError e) {
    12. e.printStackTrace();
    13. }
    14. ((ViewPager)container).addView(iv);
    15. return iv;
    16. }
    17. @Override
    18. public int getItemPosition(Object object) {
    19. View view = (View)object;
    20. int currentPage = ((DispImgActivity)mContext).getCurrentPagerIdx(); // Get current page index
    21. if(currentPage == (Integer)view.getTag()){
    22. return POSITION_NONE;
    23. }else{
    24. return POSITION_UNCHANGED;
    25. }
    26. // return POSITION_NONE;
    27. }
     

    关键的currentPageIdx则需要在Activity中获取,如果你的Adapter是Activity的内部类,那么只要把index写成全局变量就可以在adapter中使用了,如果是单独的两个类,那么你就自己提供一个接口,将index传给Adapter便是。 
    Java代码 复制代码 链接地址
    1. /**
    2. * DispImgActivity.java
    3. */
    4. // Get current page index
    5. mViewPager.setOnPageChangeListener(new OnPageChangeListener() {
    6. @Override
    7. public void onPageScrolled(int i, float f, int j) {
    8. }
    9. @Override
    10. public void onPageSelected(int position) {
    11. DispImgActivity.this.position = position;
    12. }
    13. @Override
    14. public void onPageScrollStateChanged(int i) {
    15. }
    16. });
    17. // Return current index to Adapter
    18. public int getCurrentPagerIdx() {
    19. return position;
    20. }
     
    PS:我的项目中还加入了图片下载进度条的功能,当我用第二种方法时,在一些比较极端的情况下会有一点问题,
    假设所有图片都需要从网上下载,在极快速滑动页面时,发现偶尔会出现异步下载到的图片并没有被刷新显示,在滑过几页重新回到该页时图片才被刷新了,
    这里涉及到的关键问题是【ViewPager的预加载机制+图片异步下载+getItemPosition中对Tag的判断】,我认为是这几种机制结合后再快速切换页面时造成的问题,
    由于项目工期的限制,没有去探索更完美的解决方法,反正图片也不是很多,我就采用了第一种方法来做,可以完美的实现我的功能。


     
  • 相关阅读:
    vscode task 与 linux shell编程小记
    Python并行计算专题
    分类任务评价指标(Accuracy / Precision / Recall / F1 / ROC / AUC)
    JAVA笔记 10.04
    MySQL5.7忘记密码 Linux
    MYSQL 获取最近多少天时间列表
    Volatile 关键字的原理和实现
    安装正常APP时被小米提醒是诈骗APP不允许安装的问题
    【瞎口胡】网络流基础
    题解 Luogu P4774 [NOI2018]屠龙勇士
  • 原文地址:https://www.cnblogs.com/cheneasternsun/p/6017012.html
Copyright © 2011-2022 走看看