Android RecyclerView与ListView局部刷新

Android ListView与RecyclerView局部刷新


一、ListView

之前写过一篇关于ListView局部刷新的博客,这部分对其进行完善
,之前的链接为:Android模拟ListView点击下载和局部刷新

平时在写ListView的时候需要更改某些数据,这种情况我们一般会调用
notifyDataSetChanged()方法进行刷新,调用notifydatasetchange其实会导致adpter的getView方法被多次调用(画面上能显示多少就会被调用多少次),并且在有获取网络图片的情况下会可能造成大量闪动或卡顿,极大的影响用户体验(图片重新加载并闪动在ImageLoader框架中会出现,在glide框架中没有出现)。

所以我们需要做单行刷新来进行优化

这个是Google官方给出的解决方案:

private void updateSingleRow(ListView listView, long id) {  

        if (listView != null) {  
            int start = listView.getFirstVisiblePosition();  
            for (int i = start, j = listView.getLastVisiblePosition(); i <= j; i++)  
                if (id == ((Messages) listView.getItemAtPosition(i)).getId()) {  
                    View view = listView.getChildAt(i - start);  
                    getView(i, view, listView);  
                    break;  
                }  
        }  
    }

对于这个方法可以参考这个博客:android ListView 单条刷新方法实践及原理解析

可以看出来谷歌的方案是通过listview的getView方法将单行的所有内容都刷新一遍,但是这样如果是有加载网络图片的话可能也会造成闪动重新加载,所以我们需要单独刷新某个item中的某个控件来实现局部刷新

所以我们在Adapter中添加一个局部刷新的方法

/**
     * 局部刷新
     *
     * @param mListView
     * @param posi
     */
    public void updateSingleRow(ListView mListView, int posi) {
        if (mListView != null) {
            //获取第一个显示的item
            int visiblePos = mListView.getFirstVisiblePosition();
            //计算出当前选中的position和第一个的差,也就是当前在屏幕中的item位置
            int offset = posi - visiblePos;
            int lenth = mListView.getChildCount();
            // 只有在可见区域才更新,因为不在可见区域得不到Tag,会出现空指针,所以这是必须有的一个步骤
            if ((offset < 0) || (offset >= lenth)) return;
            View convertView = mListView.getChildAt(offset);
            ViewHolder viewHolder = (ViewHolder) convertView.getTag();
            //以下是处理需要处理的控件方法。。。。。
        }
    }

举个例子,简单的模拟在列表中点击下载并更新列表的demo
1.首先定义一个Bean对象
这里有一个图片url供Glide加载

public class Game {

    private String gameName;
    private long gameTotalSize;
    private long gameDownSize;
    private String gameUrl;
    private int state;

    public Game(String gameName, long gameTotalSize) {
        this.gameName = gameName;
        this.gameTotalSize = gameTotalSize;
        gameUrl = "http://dynamic-image.yesky.com/600x-/uploadImages/upload/20140912/upload/201409/smkxwzdt1n1jpg.jpg";
        this.gameDownSize = 0;
    }

    public String getGameUrl() {
        return gameUrl;
    }

    public String getGameName() {
        return gameName;
    }

    public long getGameTotalSize() {
        return gameTotalSize;
    }

    public long getGameDownSize() {
        return gameDownSize;
    }

    public void setGameDownSize(long gameDownSize) {
        this.gameDownSize = gameDownSize;
    }

    public int getState() {
        return state;
    }

    public void setState(int state) {
        this.state = state;
    }
}

2.实现一个下载的模拟器(单例类)

public class DownLoadManager {

    public interface DownCallBack{
        void update(int posi,DownFile downFile);
    }

    private DownCallBack downCallBack;

    public void setDownCallBack(DownCallBack downCallBack) {
        this.downCallBack = downCallBack;
    }

    /**
     * 下载文件类,不过在正常的项目中应该有一个ID或者url,
     * 用来判断文件,在这个demo中就用列表的position来判断了
     */
    public static class DownFile {
        public long total;
        public long downSize;
        public int downState;

        public DownFile(long total, long downSize, int downState) {
            this.total = total;
            this.downSize = downSize;
            this.downState = downState;
        }
    }

    public static final int DOWN_WATE = 0X02;
    public static final int DOWN_PAUST = 0X03;
    public static final int DOWN_FINISH = 0X04;
    public static final int DOWN_DOWNLOADING = 0X05;

    private ExecutorService executorService;
    private SparseArray<DownFile> downFileSparseArray;
    private SparseArray<DownTask> downTaskSparseArray;
    private static volatile DownLoadManager singleton;
    private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            int posi = msg.arg1;
            DownLoadManager.DownFile downFile = (DownLoadManager.DownFile) msg.obj;
            if (downCallBack!=null){
                downCallBack.update(posi,downFile);
            }
        }
    };

    private DownLoadManager() {
        executorService = Executors.newFixedThreadPool(3);
        downFileSparseArray = new SparseArray<>();
        downTaskSparseArray = new SparseArray<>();
    }

    public static DownLoadManager getInstance() {
        if (singleton == null) {
            synchronized (DownLoadManager.class) {
                if (singleton == null) {
                    singleton = new DownLoadManager();
                }
            }
        }
        return singleton;
    }

    public void start(int posi, DownFile downFile) {
        if (downFile.downState == DOWN_WATE||downFile.downState == DOWN_FINISH){
            return;
        }
        //首先设置为排队中的状态
        downFile.downState = DOWN_WATE;
        update(posi, downFile);
        downFileSparseArray.put(posi, downFile);
        DownTask downTask = new DownTask(posi);
        downTaskSparseArray.put(posi, downTask);
        executorService.submit(downTask);
    }

    public void pause(int posi, DownFile downFile) {
        downTaskSparseArray.get(posi).stop();
        downTaskSparseArray.remove(posi);
        downFile.downState = DOWN_PAUST;
        update(posi, downFile);
    }

    public void update(int posi, DownFile downFile) {
        Message msg = handler.obtainMessage();
        msg.obj = downFile;
        msg.arg1 = posi;
        msg.sendToTarget();
    }

    public void stopAll(){
        for (int i = 0;i<downTaskSparseArray.size();i++) {
            downTaskSparseArray.valueAt(i).stop();
        }
        downTaskSparseArray.clear();
    }


    private class DownTask implements Runnable {

        private int posi;
        private boolean isWorking;
        private DownFile downFile;

        public DownTask(int posi) {
            this.posi = posi;
            isWorking = true;
            downTaskSparseArray.put(posi, this);
        }

        public void stop() {
            this.isWorking = false;
        }

        @Override
        public void run() {

            //一旦成功进入到线程里就变为下载中状态
            downFile = downFileSparseArray.get(posi);
            downFile.downState = DOWN_DOWNLOADING;
            while (isWorking) {
                update(posi, downFile);
                if (downFile.downSize < downFile.total) {
                    ++downFile.downSize;
                } else {
                    downFile.downState = DOWN_FINISH;
                    downFileSparseArray.remove(posi);
                    downTaskSparseArray.remove(posi);
                    isWorking = false;
                    break;
                }
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    downFile.downState = DOWN_PAUST;
                    downFileSparseArray.remove(posi);
                    downTaskSparseArray.remove(posi);
                    isWorking = false;
                }
            }
        }
    }

}

3.创建adapter

public class LvAdapter extends BaseAdapter implements DownLoadManager.DownCallBack{

    private Context context;
    private List<Game> games;
    private LayoutInflater layoutInflater;
    private ListView listView;
    private DownLoadManager downLoadManager;

    public LvAdapter(Context context, ListView listView, List<Game> games) {
        this.context = context;
        this.games = games;
        this.listView = listView;
        layoutInflater = LayoutInflater.from(context);
        downLoadManager = DownLoadManager.getInstance();
        downLoadManager.setDownCallBack(this);
    }

    @Override
    public int getCount() {
        return games.size();
    }

    @Override
    public Object getItem(int position) {
        return games.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {

        ViewHolder viewHolder;
        if (convertView == null) {
            convertView = layoutInflater.inflate(R.layout.item, parent, false);
            viewHolder = new ViewHolder(convertView);
            convertView.setTag(viewHolder);
        } else {
            viewHolder = (ViewHolder) convertView.getTag();
        }

        setItemView(viewHolder, games.get(position), position);

        return convertView;
    }

    private void setItemView(ViewHolder viewHolder, final Game game, final int position) {

        viewHolder.itemProgress.setMax((int) game.getGameTotalSize());
        viewHolder.itemName.setText(game.getGameName());
        viewHolder.itemSize.setText(game.getGameTotalSize() + "");
        //加载图片
        Glide.with(context).load(game.getGameUrl()).placeholder(R.mipmap.ic_launcher).crossFade(2000).into(viewHolder.itemImg);
        viewHolder.itemDownBtn.setText(getGameState(game));

        if (game.getGameDownSize() > 0 && game.getGameDownSize() < game.getGameTotalSize()) {
            viewHolder.itemProgress.setVisibility(View.VISIBLE);
            viewHolder.itemProgress.setProgress((int) game.getGameDownSize());
        } else {
            viewHolder.itemProgress.setVisibility(View.INVISIBLE);
        }

        viewHolder.itemDownBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (game.getState() == DownLoadManager.DOWN_DOWNLOADING)
                    downLoadManager.pause(position, new DownLoadManager.DownFile(game.getGameTotalSize(), game.getGameDownSize(), game.getState()));
                else
                    downLoadManager.start(position, new DownLoadManager.DownFile(game.getGameTotalSize(), game.getGameDownSize(), game.getState()));
            }
        });

    }

    private String getGameState(Game game) {
        switch (game.getState()) {
            case DownLoadManager.DOWN_DOWNLOADING:
                return "下载中";
            case DownLoadManager.DOWN_FINISH:
                return "已完成";
            case DownLoadManager.DOWN_PAUST:
                return "暂停";
            case DownLoadManager.DOWN_WATE:
                return "等待中";
        }
        return "下载";
    }

    /**
     * 局部刷新
     *
     * @param mListView
     * @param posi
     */
    public void updateSingleRow(ListView mListView, int posi) {
        if (mListView != null) {
            //获取第一个显示的item
            int visiblePos = mListView.getFirstVisiblePosition();
            //计算出当前选中的position和第一个的差,也就是当前在屏幕中的item位置
            int offset = posi - visiblePos;
            int lenth = mListView.getChildCount();
            // 只有在可见区域才更新,因为不在可见区域得不到Tag,会出现空指针,所以这是必须有的一个步骤
            if ((offset < 0) || (offset >= lenth)) return;
            View convertView = mListView.getChildAt(offset);
            ViewHolder viewHolder = (ViewHolder) convertView.getTag();
            //以下是处理需要处理的控件
            System.out.println("posi = " + posi);
            Game game = games.get(posi);
            if (game.getGameDownSize() > 0 && game.getGameDownSize() < game.getGameTotalSize()) {
                viewHolder.itemProgress.setVisibility(View.VISIBLE);
                viewHolder.itemProgress.setProgress((int) game.getGameDownSize());
            } else {
                viewHolder.itemProgress.setVisibility(View.INVISIBLE);
            }

            viewHolder.itemDownBtn.setText(getGameState(game));
        }
    }

    @Override
    public void update(int posi, DownLoadManager.DownFile downFile) {
        Game game = games.get(posi);
        game.setGameDownSize(downFile.downSize);
        game.setState(downFile.downState);
//            notifyDataSetChanged();
        updateSingleRow(listView, posi);
    }

    private class ViewHolder {

        TextView itemName, itemSize;
        Button itemDownBtn;
        ProgressBar itemProgress;
        ImageView itemImg;

        public ViewHolder(View convertView) {
            itemImg = (ImageView) convertView.findViewById(R.id.itemImg);
            itemName = (TextView) convertView.findViewById(R.id.itemName);
            itemSize = (TextView) convertView.findViewById(R.id.itemSize);
            itemDownBtn = (Button) convertView.findViewById(R.id.itemDownBtn);
            itemProgress = (ProgressBar) convertView.findViewById(R.id.itemProgress);
        }
    }

}

基本代码就是这些。
下面是notifyDataSetChanged后刷新的效果:由于不断的刷新界面中所有的item,所以在下载的时候点击按钮没有任何的反应,只有在刷新的间隙时间里点击才可以执行下载

这里写图片描述

可以看到,这里我点击了好多回没有效果,偶尔会成功,这简直没法用了~

局部刷新的效果:

这里写图片描述

一、RecyclerView

看到RecyclerView局部刷新可能大家会说,RecyclerView 中不是有notifyItemChanged(position)么,没错,的确是用这个方法可以刷新一个item,但是这个方法也是有个坑,同样会刷新item中所有东西,并且在不断刷新的时候会执行刷新Item的动画,导致滑动的时候会错开一下,还可能会造成图片重新加载或者不必要的消耗。

下面讲解一下我的方法:
一般我们刷新某个item的方法为

notifyItemChanged(position);

所以我们就从这里入手,
首先查看源码


这里写图片描述
这里写图片描述

这个方法里执行了notifyItemRangeChanged方法,继续跟踪


这里写图片描述
这里写图片描述

发现这里执行了另一个重载方法

最后一个参数为null?继续跟进


这里写图片描述
这里写图片描述

发现这里的参数为Object payload,然后我看到了notifyItemChanged的另一个重载方法,这里也有一个Object payload参数,看一下源码:


这里写图片描述
这里写图片描述

payload 的解释为:如果为null,则刷新item全部内容
那言外之意就是不为空就可以局部刷新了~!
继续从payload跟踪,发现在RecyclerView中有另一个onBindViewHolder的方法,多了一个参数,payload!!!这个不就是我要找的么~~!

看一下源码:


这里写图片描述
这里写图片描述

发现它调用了另一个重载方法,而另一个重载方法就是我们在写adapter中抽象的方法,那我们就可以直接从这里入手了。

1.重写onBindViewHolder(VH holder, int position, List<Object> payloads)这个方法

@Override
    public void onBindViewHolder(MyViewHolder holder, final int position, List<Object> payloads) {
        super.onBindViewHolder(holder, position, payloads);
        if (payloads.isEmpty()){  
        //全部刷新
        }else {
        //局部刷新
        }
    }

2.执行两个参数的notifyItemChanged,第二个参数随便什么都行,只要不让它为空就OK~,这样就可以实现只刷新item中某个控件了!!!

这个是用一般的刷新item方法的效果图:

这里写图片描述

虽然录制的效果不是特别好,但是可以看见明显的浮动错位现象

这个是用这个方法刷新的效果图:

这里写图片描述

这样就恢复正常了

RecyclerView adapter默认的使用我就不在这里多说了。主要是局部刷新。

今天的博客就说到这里,有什么增加的日后再补充~~

源码链接:https://github.com/asd7364645/PartialRefreshDemo

希望喜欢的小伙伴给个屎蛋(star)~~


我是Alex-蜡笔小刘,一个android小白~!
有失误或者错误希望有大神能纠正!也希望与其他“小白”共同进步

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容