Android开发Android学习程序员

Android——RecyclerView入门学习之DiffUt

2016-10-23  本文已影响627人  英勇青铜5

学习资料:

十分感谢 :)

DiffUtilandroid.support:design:24.2.0中推出的一个RecyclerView的辅助工具类

作用:
Adapter中的数据发生变化时,用来比较Adpater中变化前和后两个数据集合,实现差异化更新。而两个集合的整个比较过程,以及针对数据变化该调用哪些notifyData方法,内部会自动进行处理


1.简单使用 <p>

使用的步骤:

  1. 创建一个DiffUtilCallback继承DiffUtil.Callback,重写里面的抽象方法
  2. 使用DiffUtil.calculateDiff(callback),计算差异结果,需要DiffUtilCallback,返回对象为DiffUtil.DiffResult
  3. 使用DiffResult对象,通过diffResult.dispatchUpdatesTo(adapter)方法,将DiffUtilAdapter关联起来

大致的思路就是这个样子,基本也是死套路


效果:

改变全部学科,并在加入一条

点击RecyclerView的一个item,将学科全部改变,并在postion2处,加入一个item


1.1 创建DiffUtilCallback

直接继承DiffUtil.Callback,之后Anddroid Studio便会自动提示必须要重写4个的方法

class DiffUtilCallback extends DiffUtil.Callback {

    private List<ResourceBean> oldList = new ArrayList<>();
    private List<ResourceBean> newList = new ArrayList<>();

    public void setNewList(List<ResourceBean> newList) {
        if (null != newList) {
            this.newList.clear();
            this.newList.addAll(newList);
        }
    }

    public void setOldList(List<ResourceBean> oldList) {
        if (null != oldList) {
            this.oldList.clear();
            this.oldList.addAll(oldList);
        }
    }

    /**
     * 数据变化之前,旧的数据集合size
     */
    @Override
    public int getOldListSize() {
        Log.e("oldList.size", "---->" + oldList.size());
        return oldList.size();
    }

    /**
     * 数据变化之后,新的数据集合size
     */
    @Override
    public int getNewListSize() {
        Log.e("newList.size", "---->" + newList.size());
        return newList.size();
    }

    /**
     * 根据position来对新旧集合中的Object进行判断
     */
    @Override
    public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
        ResourceBean oldBean = oldList.get(oldItemPosition);
        ResourceBean newBean = newList.get(newItemPosition);
        Log.e("areItemsTheSame", oldItemPosition + "--" + newItemPosition + "---->" + oldBean.getSubject().equals(newBean.getSubject()));
        return oldBean.getSubject().equals(newBean.getSubject());//比较科目
    }

    /**
     * 根据position来对新旧集合中的Object的内容进行判断
     */
    @Override
    public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
        ResourceBean oldBean = oldList.get(oldItemPosition);
        ResourceBean newBean = newList.get(newItemPosition);
        Log.e("areContentsTheSame", oldItemPosition + "--" + newItemPosition + "---->" + oldBean.getName().equals(newBean.getName()));
        return oldBean.getName().equals(newBean.getName());//比较学科
    }

    /**
     * 返回的特定的差异化结果
     */
    @Nullable
    @Override
    public Object getChangePayload(int oldItemPosition, int newItemPosition) {
        ResourceBean oldBean = oldList.get(oldItemPosition);
        ResourceBean newBean = newList.get(newItemPosition);
        Bundle diffBundle = new Bundle();
        if (!oldBean.getName().equals(newBean.getName())) {
            diffBundle.putSerializable("newBean",newBean);
        }
        return diffBundle;
    }
}

ResourceBean内只有两个String类型的subject,nameset,get方法,不再给出


1.2 Adapter代码

适配器就是一个一般写法的适配,代码比较简单

class PayloadAdapter extends RecyclerView.Adapter<PayloadAdapter.RecyclerHolder> {
    private Context mContext;
    private List<ResourceBean> dataList = new ArrayList<>();
    private onRecyclerItemClickerListener mListener;

    public PayloadAdapter(RecyclerView recyclerView) {
        this.mContext = recyclerView.getContext();
    }


    /**
     * 增加点击监听
     */
    public void setItemListener(onRecyclerItemClickerListener mListener) {
        this.mListener = mListener;
    }

    /**
     * 设置数据源
     */
    public void setData(List<ResourceBean> dataList) {
        if (null != dataList) {
            this.dataList.clear();
            this.dataList.addAll(dataList);
        }
    }

    public List<ResourceBean> getDataList() {
        return dataList;
    }

    @Override
    public RecyclerHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(mContext).inflate(R.layout.id_rv_item_layout, parent, false);
        return new RecyclerHolder(view);
    }

    @Override
    public void onBindViewHolder(RecyclerHolder holder, int position) {
        ResourceBean resourceBean = dataList.get(position);
        holder.textView.setText(resourceBean.getSubject() + "--->" + resourceBean.getName());
        holder.textView.setOnClickListener(getOnClickListener(position));
    }

    @Override
    public void onBindViewHolder(RecyclerHolder holder, int position, List<Object> payloads) {
        super.onBindViewHolder(holder, position, payloads);
        if (payloads.isEmpty()) {
            Log.e("isEmpty", "---->" + payloads.isEmpty());
            onBindViewHolder(holder, position);
        }else {
            Log.e("isEmpty", "---->no null");
            Bundle bundle = (Bundle) payloads.get(0);
            ResourceBean resourceBean = (ResourceBean) bundle.get("newBean");
            if (null == resourceBean) return;
            // Log.e("change", "---->" + change);
            holder.textView.setText(resourceBean.getSubject() + "--->" + resourceBean.getName());
        }
    }


    private View.OnClickListener getOnClickListener(final int position) {
        return new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (null != mListener && null != v) {
                    mListener.onRecyclerItemClick(v, dataList.get(position), position);
                }
            }
        };
    }

    @Override
    public int getItemCount() {
        return dataList.size();
    }

    class RecyclerHolder extends RecyclerView.ViewHolder {
        TextView textView;

        private RecyclerHolder(View itemView) {
            super(itemView);
            textView = (TextView) itemView.findViewById(R.id.tv__id_item_layout);
        }
    }

    /**
     * 点击监听回调接口
     */
    public interface onRecyclerItemClickerListener {
        void onRecyclerItemClick(View view, Object data, int position);
    }
}

加入了一个onBindViewHolder(RecyclerHolder holder, int position, List<Object> payloads)方法


1.3 与Adapter进行关联 <p>

在创建Adapter的Activity中,完整代码:

 class DiffUtil2Activity extends AppCompatActivity {
    private RecyclerView rv;
    private PayloadAdapter mAdapter;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_diff_util2);
        init();
    }

    private void init() {
        rv = (RecyclerView) findViewById(R.id.rv_diff2_util_activity);
        //布局管理器
        LinearLayoutManager manager = new LinearLayoutManager(this);
        manager.setOrientation(LinearLayoutManager.VERTICAL);
        rv.setLayoutManager(manager);
        //设置ItemDecoration
        rv.addItemDecoration(new DividerItemDecoration(this, LinearLayoutManager.VERTICAL));
        //适配器
        mAdapter = new PayloadAdapter(rv);
        rv.setAdapter(mAdapter);
        //添加数据
        addData();
    }

    private void addData() {
        DiffUtilCallback callback = new DiffUtilCallback();
        callback.setOldList(mAdapter.getDataList());
        List<ResourceBean> oldList = new ArrayList<>();
        for (int i = 'A'; i < 'D'; i++) {
            for (int j = 0; j < 15; j++) {
                ResourceBean resourceBean = new ResourceBean();
                resourceBean.setSubject("学科" + (char) i);
                resourceBean.setName("资料" + j);
                oldList.add(resourceBean);
            }
        }
        callback.setNewList(oldList);
        DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(callback);//子线程计算差异
        diffResult.dispatchUpdatesTo(mAdapter);
        //记得将新数据替换adapter中旧的集合数据
        mAdapter.setData(oldList);
        //设置点击事件
        mAdapter.setItemListener(new PayloadAdapter.onRecyclerItemClickerListener() {
            @Override
            public void onRecyclerItemClick(View view, Object data, int position) {
                changeData();
            }
        });
    }

    private void changeData() {
        DiffUtilCallback callback = new DiffUtilCallback();
        callback.setOldList(mAdapter.getDataList());
        List<ResourceBean> newList = new ArrayList<>();
        for (int i = 'A'; i < 'D'; i++) {
            for (int j = 0; j < 15; j++) {
                ResourceBean resourceBean = new ResourceBean();
                resourceBean.setSubject("学科" + (char) i);
                resourceBean.setName("吃饭课" + j);
                newList.add(resourceBean);
            }
        }
        ResourceBean r = new ResourceBean();
        r.setSubject("体育课");
        r.setName("---->666666");
        newList.add(4, r);
        callback.setNewList(newList);
        //这是主线程。下面有案例放在了子线程中
        DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(callback);//子线程计算差异 
        diffResult.dispatchUpdatesTo(mAdapter);
        mAdapter.setData(newList);
    }
     @Override
    protected void onDestroy() {
        super.onDestroy();
        if (null != rv) rv.setAdapter(null);
    }
}

而关联操作关键的代码也就2

DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(callback);
diffResult.dispatchUpdatesTo(adapter);//与Adapter进行关联

这时计算两个List的过程是放在UI主线程到了实际开发中,数据量往往会比较大,计算差异化这一步,最好做些处理放在一个子线程中

在上一篇RecyclerView.Adapter学习的1.5小节一系列的notifyData方法中,移除或者增加同类型对象的item时,说会感觉卡顿,使用DiffUtil感觉好多了,可能心理作用。。。

如果只是想做个,感觉没必要使用DiffUtilRecycelrView.Adapter提供的局部刷新感觉蛮好用的了。有一个特别适合使用的场景便是下拉刷新,不仅有动画,效率也有提高,尤其是下拉刷新操作后,Adapter内集合数据并没有发生改变,不需要进行重新绘制RecyclerView


2. DiffUtil <p>

DiffUtil.DiffResultDiffUtil.CallbackDiffUtil的两个内部类。DiffUtil对外提供的public方法只有2

方法1:

 /**
  * Calculates the list of update operations that can covert one list into the other one.
  *
  * @param cb The callback that acts as a gateway to the backing list data
  *
  * @return A DiffResult that contains the information about the edit sequence to convert the old list into the new list.
  */
public static DiffResult calculateDiff(Callback cb) {
    return calculateDiff(cb, true);
}

这个方法用于计算newListoldList的差异,在内部计算出后的操作便是将两个列表转换成为另一个新的集合,但转换过程并不用再操心的

在方法内,调用了两个参数的calculateDiff(cb, true),便是方法2


方法2:

/**
 * Calculates the list of update operations that can covert one list into the other one.
 * <p>
 * If your old and new lists are sorted by the same constraint and items never move (swap positions), you can disable move detection which takes <code>O(N^2)</code> time where N is the number of added, moved, removed items.
 *
 * @param cb The callback that acts as a gateway to the backing list data
 * @param detectMoves True if DiffUtil should try to detect moved items, false otherwise.
 *
 * @return A DiffResult that contains the information about the edit sequence to convert the old list into the new list.
 */
public static DiffResult calculateDiff(Callback cb, boolean detectMoves) {
   ...

   //做过修改的Eugene W. Myers’s difference算法
   final Snake snake = diffPartial(cb, range.oldListStart, range.oldListEnd, range.newListStart, range.newListEnd, forward, backward, max);
   
   ...
   
   return new DiffResult(cb, snakes, forward, backward, detectMoves);

具体哪种场景下设置为false,目前还不了解,有知道的同学请留言告诉一声

在这个方法中,calculateDiff()算是整个DiffUtil的核心操作,计算差异化操作,里面涉及到的算法以后再进行学习

最后返回了一个DiffResult对象,这个对象内部封装着进行了差异化比较操作后的转换的数据集合。这个对象内部涉及到的转换操作,暂时也不打算进行学习,等对于DiffUtil的使用相对熟练一些后,再进行学习


2.1 DiffUtil.Callback <p>

在这个抽象类中,一共有5个抽象方法:

方法 作用
public abstract int getOldListSize() 新集合的size
public abstract int getNewListSize() 旧集合的size
public abstract boolean areItemsTheSame() 判断两个集合中Object是否相同
public abstract boolean areContentsTheSame() 检查两个item的对象内容是否相同
public Object getChangePayload() @Nullable,非必需重写的方法,得到封装新旧集合中两个item负载对象,默认返回null

前两个方法很好理解,后面3个方法需要根据源码中的注释来学习


2.1.1 areItemsTheSame()判断Item中Objecct是否相同 <p>

源码:

/**
 * Called by the DiffUtil to decide whether two object represent the same Item.
 * 进行比较新旧集合中在oldItemPosition与newItemPosition两个对象是否相同
 *
 * For example, if your items have unique ids, this method should check their id equality.
 *例如,当集合中的对象有唯一的标记ids时,就比较两个对象的ids
 *
 * @param oldItemPosition The position of the item in the old list
 * 对象在旧的集合中的position
 * @param newItemPosition The position of the item in the new list
 * 对象在新的集合中的position
 * @return True if the two items represent the same object or false if they are different.
 * 返回比较结果
 */
 public abstract boolean areItemsTheSame(int oldItemPosition, int newItemPosition);

这个方法的返回结果会对areContentsTheSame()方法有影响


2.1.2 areContentsTheSame()判断Objecct中的内容是否相同<p>

源码:

 /**
  * Called by the DiffUtil when it wants to check whether two items have the same data.
  * 比较新旧集合对象中的内容是否一样
  * DiffUtil uses this information to detect if the contents of an item has changed.
  * DiffUtil利用这个方法的返回值来检测一个对象是否发生变化
  * 
  * DiffUtil uses this method to check equality instead of {@link Object#equals(Object)}
  * DiffUtil使用这个方法代替equals()来检测是否相等
  * so that you can change its behavior depending on your UI.
  * 可以根据UI需求来改变返回值
  *
  * For example, if you are using DiffUtil with a {@link android.support.v7.widget.RecyclerView.Adapter RecyclerView.Adapter}, you should return whether the items' visual representations are the same.
  * 例如,在配合RecyclerView.Adapter使用时,需要返回RecycelrView的item视觉效果是否一致
  *这里视觉效果应该指的就是动画吧
  *
  * This method is called only if {@link #areItemsTheSame(int, int)} returns{@code true} for these items.
  *这个方法只有在areItemsTheSame()方法返回ture时才会被调用
  * @param oldItemPosition The position of the item in the old list
  * @param newItemPosition The position of the item in the new list which replaces the oldItem
  * @return True if the contents of the items are the same or false if they are different.
  */
public abstract boolean areContentsTheSame(int oldItemPosition, int newItemPosition);

只有当areContentsTheSame()方法返回true时,这个方法才会被掉y用。不难理解,只有听一个类型的对象才可以比较的意义


2.1.3 getChangePayload()得到封装差异化信息的对象 <p>

源码:

 /**
  * When {@link #areItemsTheSame(int, int)} returns {@code true} for two items and {@link #areContentsTheSame(int, int)} returns false for them, DiffUtil calls this method to get a payload about the change.
  * 当areItemsTheSame()返回true并且areContentsTheSame返回false,DiffUtil便会调用这个方法将两个item的差异封装在一个负载对象中
  *
  * <p>
  * For example, if you are using DiffUtil with {@link RecyclerView}, you can return the particular field that changed in the item and your{@link android.support.v7.widget.RecyclerView.ItemAnimator ItemAnimator} can use that information to run the correct animation.
  * 例如,当配合RecyclerView使用DiffUtil时,可以将新的item改变的特定属性返回,还可以使用返回的差异属性利用RecyclerView.ItemAnimator使用改变的动画
  *
  * <p>
  * Default implementation returns {@code null}.
  *默认返回为null
  * @param oldItemPosition The position of the item in the old list
  * @param newItemPosition The position of the item in the new list
  *
  * @return A payload object that represents the change between the two items.
  */
  @Nullable
  public Object getChangePayload(int oldItemPosition, int newItemPosition) {
        return null;
  }

使用这个方法,有特定的条件:
areItemsTheSame()返回true,areContentsTheSame返回false

这个方法还要重写RecyclerView.AdapteronBindViewHolder(RecyclerHolder holder, int position, List<Object> payloads)

DiffUtil将返回的封装差异化的对象存储在一个List集合中,集合payloads对象不会为null,但可能会为empty,也就是只有集合对象,而集合中却是空的。在onBindViewHolder()需要加判断payloads.isEmpty(),若为empty就调用两个参数的onBindViewHolder()


简单使用:

DiffUtilCallback中对应:

    /**
     * 返回的特定的差异化结果
     */
    @Nullable
    @Override
    public Object getChangePayload(int oldItemPosition, int newItemPosition) {
        ResourceBean oldBean = oldList.get(oldItemPosition);
        ResourceBean newBean = newList.get(newItemPosition);
        Bundle diffBundle = new Bundle();
        if (!oldBean.getName().equals(newBean.getName())) {
            diffBundle.putSerializable("newBean",newBean);
        }
        return diffBundle;
    }```

**在Adapter中对应:**
```java
    @Override
    public void onBindViewHolder(RecyclerHolder holder, int position, List<Object> payloads) {
        super.onBindViewHolder(holder, position, payloads);
        if (payloads.isEmpty()) {
            Log.e("isEmpty", "---->" + payloads.isEmpty());
            onBindViewHolder(holder, position);
        }else {
            Log.e("isEmpty", "---->no null");
            Bundle bundle = (Bundle) payloads.get(0);
            ResourceBean resourceBean = (ResourceBean) bundle.get("newBean");
            if (null == resourceBean) return;
            // Log.e("change", "---->" + change);
            holder.textView.setText(resourceBean.getSubject() + "--->" + resourceBean.getName());
        }
    }

注意要先判断payloads中是否含有差异化数据


子线程模拟网络请求,完整的Activity代码:

public class DiffUtilActivity extends AppCompatActivity {

    private RecyclerView rv;
    private PayloadAdapter mAdapter;
    private StaticHandler mHandler = null;
    private static final int ADD_DATA = 2 << 5;
    private static final int CHANGE_DATA = 2 << 6;
    private List<ResourceBean> oldList;
    private List<ResourceBean> newList;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_diff_util);
        init();
    }

    private void init() {
        mHandler = new StaticHandler(DiffUtilActivity.this);
        rv = (RecyclerView) findViewById(R.id.rv_diff_util_activity);
        //布局管理器
        LinearLayoutManager manager = new LinearLayoutManager(this);
        manager.setOrientation(LinearLayoutManager.VERTICAL);
        rv.setLayoutManager(manager);
        //设置ItemDecoration
        rv.addItemDecoration(new DividerItemDecoration(this, LinearLayoutManager.VERTICAL));
        //适配器
        mAdapter = new PayloadAdapter(rv);
        rv.setAdapter(mAdapter);
        //设置点击事件
        mAdapter.setItemListener(new PayloadAdapter.onRecyclerItemClickerListener() {
            @Override
            public void onRecyclerItemClick(View view, Object data, int position) {
                changeData();
            }
        });
        //添加数据
        addData();
    }

    public static class StaticHandler extends Handler {
        private final WeakReference<DiffUtilActivity> mWeakReference;

        private StaticHandler(DiffUtilActivity activity) {
            mWeakReference = new WeakReference<>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            DiffUtilActivity activity = mWeakReference.get();
            if (null != activity) {
                PayloadAdapter adapter = activity.mAdapter;
                if (msg.what == ADD_DATA) {
                    List<ResourceBean> list = activity.oldList;
                    DiffUtil.DiffResult diffResult = (DiffUtil.DiffResult) msg.obj;
                    diffResult.dispatchUpdatesTo(adapter);
                    adapter.setData(list);
//                 adapter.notifyDataSetChanged(); //由上面代替
                } else if (msg.what == CHANGE_DATA) {
                    List<ResourceBean> newData = activity.newList;
                    DiffUtil.DiffResult diffResult = (DiffUtil.DiffResult) msg.obj;
                    diffResult.dispatchUpdatesTo(adapter);
                    adapter.setData(newData);
                }
            }
        }
    }


    /**
     * 点击之后,更改数据
     */
    private void changeData() {

        new Thread(new Runnable() {
            @Override
            public void run() {
                Message message = mHandler.obtainMessage();
                DiffUtilCallback callback = new DiffUtilCallback();
                callback.setOldList(mAdapter.getDataList());
                newList = new ArrayList<>();
                //模拟下拉刷新数据没有变化
//                for (int i = 'A'; i < 'D'; i++) {
//                    for (int j = 0; j < 15; j++) {
//                        ResourceBean resourceBean = new ResourceBean();
//                        resourceBean.setSubject("学科" + (char) i);
//                        resourceBean.setName("资料" + j);
//                        newList.add(resourceBean);
//                    }
//                }
                for (int i = 'A'; i < 'D'; i++) {
                    for (int j = 0; j < 15; j++) {
                        ResourceBean resourceBean = new ResourceBean();
                        resourceBean.setSubject("学科" + (char) i);
                        resourceBean.setName("吃饭课" + j);
                        newList.add(resourceBean);
                    }
                }
                ResourceBean r = new ResourceBean();
                r.setSubject("体育课");
                r.setName("---->666666");
                newList.add(4, r);
                callback.setNewList(newList);
                DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(callback);//子线程计算差异
                message.what = CHANGE_DATA;
                message.obj = diffResult;
                try {
                    TimeUnit.MILLISECONDS.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                mHandler.sendMessage(message);
            }
        }).start();
    }

    /**
     * 第一次加入数据
     */
    private void addData() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                Message message = mHandler.obtainMessage();
                DiffUtilCallback callback = new DiffUtilCallback();
                callback.setOldList(mAdapter.getDataList());
                oldList = new ArrayList<>();
                for (int i = 'A'; i < 'D'; i++) {
                    for (int j = 0; j < 15; j++) {
                        ResourceBean resourceBean = new ResourceBean();
                        resourceBean.setSubject("学科" + (char) i);
                        resourceBean.setName("资料" + j);
                        oldList.add(resourceBean);
                    }
                }
                try {
                    TimeUnit.MILLISECONDS.sleep(500);//模拟网络耗时
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                callback.setNewList(oldList);
                DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(callback);//子线程计算差异
                message.what = ADD_DATA;
                message.obj = diffResult;
                mHandler.sendMessage(message);
            }
        }).start();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (null != rv) rv.setAdapter(null);
    }
}

补充:

2016 12月8号 00:16 发现之前写的有严重错误,已经修改。改代码一直到1:02,越看越感觉代码烂,困了,先睡觉,以后对DiffUtil再熟悉一点再来修改


3.最后 <p>

这篇学习记录得稀里糊涂的,希望不会坑到看博客的同学

本人很菜,有错误请指出

共勉 :)

上一篇下一篇

猜你喜欢

热点阅读