Android 7.0新工具 DiffUtil類

一、概述

DiffUtil是一個(gè)查找集合變化的工具類,是搭配RecyclerView一起使用的,它用來比較兩個(gè)數(shù)據(jù)集,尋找出舊數(shù)據(jù)和新數(shù)據(jù)集的最小變化量,有了它以后在RecyclerView刷新時(shí),我們?cè)谝膊挥脽o腦的adapter.notifyDataSetChanged()。

我們先來看效果圖:


效果圖1

可以看到,當(dāng)我們點(diǎn)擊按鈕的時(shí)候,這個(gè)RecyclerView所顯示的集合發(fā)生了改變,有的元素被增加了(8.Jason),也有的元素被移動(dòng)了(3.Rose),甚至是被修改了(2.Fndroid)。RecyclerView對(duì)于每個(gè)Item的動(dòng)畫是以不同方式刷新的:

  • notifyItemInserted
  • notifyItemChanged
  • notifyItemMoved
  • notifyItemRemoved

而對(duì)于連續(xù)的幾個(gè)Item的刷新,可以調(diào)用:

  • notifyItemRangeChanged
  • notifyItemRangeInserted
  • notifyItemRangeRemoved

而由于集合發(fā)生變化的時(shí)候,只可以調(diào)用notifyDataSetChanged方法進(jìn)行整個(gè)界面的刷新,并不能根據(jù)集合的變化為每一個(gè)變化的元素添加動(dòng)畫。所以這里就有了DiffUtil來解決這個(gè)問題。

DiffUtil的作用,就是找出集合中每一個(gè)Item發(fā)生的變化,然后對(duì)每個(gè)變化給予對(duì)應(yīng)的刷新。

這個(gè)DiffUtil使用的是Eugene Myers的差別算法,這個(gè)算法本身不能檢查到元素的移動(dòng),也就是移動(dòng)只能被算作先刪除、再增加,而DiffUtil是在算法的結(jié)果后再進(jìn)行一次移動(dòng)檢查。假設(shè)在不檢測(cè)元素移動(dòng)的情況下,算法的時(shí)間復(fù)雜度為O(N + D2),而檢測(cè)元素移動(dòng)則復(fù)雜度為O(N2)。所以,如果集合本身就已經(jīng)排好序,可以不進(jìn)行移動(dòng)的檢測(cè)提升效率。

二、如何使用

首先對(duì)于每個(gè)Item,數(shù)據(jù)是一個(gè)Student對(duì)象:

class Student {
    private String name;
    private int num;

    public Student(String name, int num) {
        this.name = name;
        this.num = num;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getNum() {
        return num;
    }

    public void setNum(int num) {
        this.num = num;
    }
}

接著我們定義布局(省略)和適配器:

class MyAdapter extends RecyclerView.Adapter {
        private ArrayList<Student> data;

        ArrayList<Student> getData() {
            return data;
        }

        void setData(ArrayList<Student> data) {
            this.data = new ArrayList<>(data);
        }

        @Override
        public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            View itemView = LayoutInflater.from(RecyclerViewActivity.this).inflate(R.layout.itemview, null);
            return new MyViewHolder(itemView);
        }

        @Override
        public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
            MyViewHolder myViewHolder = (MyViewHolder) holder;
            Student student = data.get(position);
            myViewHolder.tv.setText(student.getNum() + "." + student.getName());
        }

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

        class MyViewHolder extends RecyclerView.ViewHolder {
            TextView tv;

            MyViewHolder(View itemView) {
                super(itemView);
                tv = (TextView) itemView.findViewById(R.id.item_tv);
            }
        }
    }

初始化數(shù)據(jù)集合:

private void initData() {
        students = new ArrayList<>();
        Student s1 = new Student("John", 1);
        Student s2 = new Student("Curry", 2);
        Student s3 = new Student("Rose", 3);
        Student s4 = new Student("Dante", 4);
        Student s5 = new Student("Lunar", 5);
        students.add(s1);
        students.add(s2);
        students.add(s3);
        students.add(s4);
        students.add(s5);
    }

接著實(shí)例化Adapter并設(shè)置給RecyclerView:

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_recycler_view);
        initData();
        recyclerView = (RecyclerView) findViewById(R.id.rv);
        recyclerView.setLayoutManager(new LinearLayoutManager(this));
        adapter = new MyAdapter();
        adapter.setData(students);
        recyclerView.setAdapter(adapter);
    }

這些內(nèi)容都不是本篇的內(nèi)容,但是,需要注意到的一個(gè)地方是Adapter的定義:

class MyAdapter extends RecyclerView.Adapter {
        private ArrayList<Student> data;

        ArrayList<Student> getData() {
            return data;
        }

        void setData(ArrayList<Student> data) {
            this.data = new ArrayList<>(data);
        }

        // 省略部分代碼
         ......  
    }
注:這里的setData方法并不是直接將ArrayList的引用保存,而是重新的建立一個(gè)ArrayList,先記著,后面會(huì)解釋為什么要這樣做

當(dāng)鼠標(biāo)按下時(shí),修改ArrayList的內(nèi)容:

public void change(View view) {
       students.set(1, new Student("Fndroid", 2));
       students.add(new Student("Jason", 8));
       Student s2 = students.get(2);
       students.remove(2);
       students.add(s2);

       ArrayList<Student> old_students = adapter.getData();
       DiffUtil.DiffResult result = DiffUtil.calculateDiff(new MyCallback(old_students, students), true);
       adapter.setData(students);
       result.dispatchUpdatesTo(adapter);
   }

2-6行是對(duì)集合進(jìn)行修改,第8行先獲取到adapter中的集合為舊的數(shù)據(jù)。

重點(diǎn)看第9行調(diào)用DiffUtil.calculateDiff方法來計(jì)算集合的差別,這里要傳入一個(gè)CallBack接口的實(shí)現(xiàn)類(用于指定計(jì)算的規(guī)則)并且把新舊數(shù)據(jù)都傳遞給這個(gè)接口的實(shí)現(xiàn)類,最后還有一個(gè)boolean類型的參數(shù),這個(gè)參數(shù)指定是否需要進(jìn)行Move的檢測(cè),如果不需要,如果有Item移動(dòng)了,會(huì)被認(rèn)為是先remove,然后insert。這里指定為true,所以就有了動(dòng)圖顯示的移動(dòng)效果。

第10行重新將新的數(shù)據(jù)設(shè)置給Adapter。

第11行調(diào)用第9行得到的DiffResult對(duì)象的dispatchUpdatesTo方法通知RecyclerView刷新對(duì)應(yīng)發(fā)生變化的Item。

這里回到上面說的setData方法,因?yàn)槲覀冊(cè)谶@里要區(qū)分兩個(gè)集合,如果在setData方法中直接保存引用,那么在2-6行的修改就直接修改了Adapter中的集合了(Java知識(shí))。

如果設(shè)置不檢查Item的移動(dòng),效果如下:


效果圖2

接著我們看看CallBack接口的實(shí)現(xiàn)類如何定義:

private class MyCallback extends DiffUtil.Callback {
       private ArrayList<Student> old_students, new_students;

       MyCallback(ArrayList<Student> data, ArrayList<Student> students) {
           this.old_students = data;
           this.new_students = students;
       }

       @Override
       public int getOldListSize() {
           return old_students.size();
       }

       @Override
       public int getNewListSize() {
           return new_students.size();
       }

       // 判斷Item是否已經(jīng)存在
       @Override
       public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
           return old_students.get(oldItemPosition).getNum() == new_students.get(newItemPosition).getNum();
       }

       // 如果Item已經(jīng)存在則會(huì)調(diào)用此方法,判斷Item的內(nèi)容是否一致
       @Override
       public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
           return old_students.get(oldItemPosition).getName().equals(new_students.get(newItemPosition).getName());
       }
   }

這里根據(jù)學(xué)號(hào)判斷是否同一個(gè)Item,根據(jù)姓名判斷這個(gè)Item是否有被修改。

實(shí)際上,這個(gè)Callback抽象類還有一個(gè)方法getChangePayload(),這個(gè)方法的作用是我們可以通過這個(gè)方法告訴Adapter對(duì)這個(gè)Item進(jìn)行局部的更新而不是整個(gè)更新。

先要知道這個(gè)payload是什么?payload是一個(gè)用來描述Item變化的對(duì)象,也就是我們的Item發(fā)生了哪些變化,這些變化就封裝成一個(gè)payload,所以我們一般可以用Bundle來充當(dāng)。

接著,getChangePayload()方法是在areItemsTheSame()返回true,而areContentsTheSame()返回false時(shí)被回調(diào)的,也就是一個(gè)Item的內(nèi)容發(fā)生了變化,而這個(gè)變化有可能是局部的(例如微博的點(diǎn)贊,我們只需要刷新圖標(biāo)而不是整個(gè)Item)。所以可以在getChangePayload()中封裝一個(gè)Object來告訴RecyclerView進(jìn)行局部的刷新。

假設(shè)上例中學(xué)號(hào)和姓名用不同的TextView顯示,當(dāng)我們修改了一個(gè)學(xué)號(hào)對(duì)應(yīng)的姓名時(shí),局部刷新姓名即可(這里例子可能顯得比較多余,但是如果一個(gè)Item很復(fù)雜,用處就比較大了):

先是重寫Callback中的該方法:

@Nullable
        @Override
        public Object getChangePayload(int oldItemPosition, int newItemPosition) {
            Student newStudent = newStudents.get(newItemPosition);
            Bundle diffBundle = new Bundle();
            diffBundle.putString(NAME_KEY, newStudent.getName());
            return diffBundle;
        }

返回的這個(gè)對(duì)象會(huì)在什么地方收到呢?實(shí)際上在RecyclerView.Adapter中有兩個(gè)onBindViewHolder方法,一個(gè)是我們必須要重寫的,而另一個(gè)的第三個(gè)參數(shù)就是一個(gè)payload的列表:

   @Override
   public void onBindViewHolder(RecyclerView.ViewHolder holder, int position, List payloads) {}

所以我們只需在Adapter中重寫這個(gè)方法,如果List為空,執(zhí)行原來的onBindViewHolder進(jìn)行整個(gè)Item的更新,否則根據(jù)payloads的內(nèi)容進(jìn)行局部刷新:

@Override
        public void onBindViewHolder(RecyclerView.ViewHolder holder, int position, List payloads) {
            if (payloads.isEmpty()) {
                onBindViewHolder(holder, position);
            } else {
                MyViewHolder myViewHolder = (MyViewHolder) holder;
                Bundle bundle = (Bundle) payloads.get(0);
                if (bundle.getString(NAME_KEY) != null) {
                    myViewHolder.name.setText(bundle.getString(NAME_KEY));
                    myViewHolder.name.setTextColor(Color.BLUE);
                }
            }
        }

這里的payloads不會(huì)為null,所以直接判斷是否為空即可

這里注意:如果RecyclerView中加載了大量數(shù)據(jù),那么算法可能不會(huì)馬上完成,要注意ANR的問題,可以開啟單獨(dú)的線程進(jìn)行計(jì)算
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,157評(píng)論 25 708
  • 內(nèi)容抽屜菜單ListViewWebViewSwitchButton按鈕點(diǎn)贊按鈕進(jìn)度條TabLayout圖標(biāo)下拉刷新...
    皇小弟閱讀 47,166評(píng)論 22 665
  • 原諒 即使你真的受了傷, 那你也學(xué)會(huì)原諒, 他無心讓你流淚, 她無心讓你無助, 只是他們從不懂得, 如何去愛你, ...
    甜蜜蜜_b52a閱讀 231評(píng)論 0 0
  • 文字:雪人 圖片:視頻截圖 這個(gè)視頻是兒子同學(xué)的爸爸在家長群里發(fā)的,看完后很受感動(dòng),思緒萬千,讓我聯(lián)想到了我們的孩...
    霧都花兒閱讀 1,188評(píng)論 0 1
  • 前文再續(xù),書接上一回!在昨天的收盤點(diǎn)評(píng)《生蠔說:防風(fēng),防雨,防短期踏空》中,我們講到:“ 1,短期內(nèi),由于連續(xù)...
    果園生蠔閱讀 1,553評(píng)論 1 0

友情鏈接更多精彩內(nèi)容