一個ListView布局的不斷演化


    剛出來工作,就負責一個APP的某塊功能的編寫,該功能就是類似微博那樣的界面。微博界面的編寫實際上是非常復雜的,雖然它只是一個ListView,但要想讓這個ListView滑得動,是的,在一些配置低的手機,隨便一個負載內容多的Item,都有可能導致OOM。。。如果只是簡單的為了實現了效果,可以選擇將所有內容都寫在xml文件,如果布局不好的話,就會出現嵌套過多的情況,同樣也會出現OOM的情況。。。就算不會出現OOM的情況,也能滑得動,也會面臨是否能夠滑得快。。。

     要想能滑得動,也能滑得快,就要動點腦筋了。

     一開始非常簡單的代碼就是這樣:

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    ViewHolder holder;
    if (convertView == null) {
        convertView = inflater.inflate(R.layout.list_item, null);
        holder = new ViewHolder();
        ……
        convertView.setTag(holder);
    } else {
        holder = (ViewHolder)convertView.getTag();
    }
}
/**
 * ViewHolder
 */
private static class ViewHolder {
    ImageView appIcon;
    TextView  appName;
    TextView  appInfo;
}

      這是谷歌推薦的方式,實際上也解決了很多性能上的問題。

      Android原生的ListView原本就做了相應的緩存機制,Recycler。

      Recycler的工作原理大致如下:

      假設屏幕最多能看到11個item,那么當第1個item滾出屏幕,這個item的View進入RecycleBin中,第12個要出現前,通過 getView從回收站(RecycleBin)中重用這個View,然后設置數據,而不必重新創建一個View。

      這樣即使有上萬個Item,inflate的次數最多就是n,也就是一個屏幕能夠容納的個數。

      大部分的情況都可以用這樣的代碼解決,但我覺得對於每個Adapter都要寫一個ViewHolder實在是太麻煩了,於是進一步將代碼改寫為這樣:

@Override
public View getView(int position, View convertView, ViewGroup parent) {
     if (convertView == null) {
        convertView = inflater.inflate(R.layout.list_item, null);
        }

        ImageView icon = (ImageView)CommonViewHolder.get(convertView, R.id.image_view);
}

     其中CommonViewHolder的代碼如下:

public class CommonViewHolder {

    /**
     * 用於獲取ItemView中的控件
     *
     * @param view ItemView
     * @param id   要獲取的控件的id
     * @param <T>  返回的控件的類型
     * @return 返回的控件
     */
    public static <T extends View> T get(View view, int id) {
        SparseArrayCompat<View> viewHolder = (SparseArrayCompat<View>) view.getTag();
        if (viewHolder == null) {
            viewHolder = new SparseArrayCompat<>();
            view.setTag(viewHolder);
        }
        View childView = viewHolder.get(id);
        if (childView == null) {
            childView = view.findViewById(id);
            viewHolder.put(id, childView);
        }
        return (T) childView;
    }
}

      實際上,ViewHolder就是通過setTag方法將相應的控件作為View的屬性保存起來,然后下次使用的時候就可以直接復用。

      既然明白了它的原理,就可以對它進行改造,CommonViewHolder是利用SparseArrayCompact存儲控件。SparseArrayCompact是SparseArrayCompact是SparseArray的兼容類,本質上就是類似Map的鍵值對容器,谷歌宣稱它的性能比Map要好,因為內在的算法已經優化了。

      這里的工作很簡單,同樣是利用setTag方法保存View中的控件,但是卻把findViewById這樣的工作放在了CommonViewHolder中,這樣就不用每次都要調用findViewById方法了。

      為了考慮通用,還使用泛型。

      代碼量瞬間就減少了很多,但這時就面臨了一個問題:Item項錯亂了。。。

      在快速滑動的時候,圖片加載錯了,仔細調試,就發現是Recycler機制出現了問題。當快速滑動的時候,原本應該開始加載圖片的控件已經滑出屏幕,然后我的圖片加載是異步的,所以圖片就會加載在后面的Item上。

      解決這個問題的方法同樣得利用setTag方法:

icon.setTag(imageUrl)

....
if(imageUrl.equals(icon.getTag()){
      ....
}

     通過setTag保存ImageView要加載的url信息,然后在下次加載的時候判斷是否相同。

     問題解決了,但看到getView方法中為了實現微博這樣承載大量信息的界面,不得不編寫大量的業務代碼,而且這樣復用性很差,因為微博詳情的界面和列表項的界面基本一樣,只是有一些不同而已。如果再寫一個,就有點傻逼了,但如果不寫,getView方法中肯定又要寫更多的判斷。

     為了解決復用的問題,就開始組件化。

     將微博界面拆成兩個部分:HeaderView和BodyView。HeaderView負責微博作者的基本信息,而BodyView就是微博內容。

     這樣,我只要在getView方法中這樣寫:

add(new HeaderView());
...
add(new BodyView());
...

     也就是說,我從一個靜態布局改成動態布局,這樣我在詳情那里也可以復用。

     到了這里原本也應該結束了,但我又想要為微博業務編寫測試,但Android中View和業務的代碼是各種糾纏,很難完全脫離View來測試業務。

     經過思考和查找資料,我找到了一種方式:ViewModel。

     編寫相應的ViewModel作為Controller,就可以將View和業務的代碼解綁:

public class ViewModel{
     private String text;
     ...
     public void setText(TextView view){
            view.setText(text);
     }
}

      這樣組件里面的代碼就更少了,它只要聲明好控件然后傳進來就行了,數據的獲取和綁定都在ViewModel這里。

      然后我們來寫一個簡單的測試:

ViewModel model = new ViewModel();
Button button = new Button(context);
button.setText("你好");
model.changeButtonText(button);
assertEquals("我好",  button.getText());

public void changeButtonText(Button button){
       button.setOnClickListener(new OnClickListener(){
               button.setText("我好");
       });
}

     利用ViewModel,我們可以方便的測試Android種的業務。

     這是到目前為止的思考和嘗試,實際上,我認為代碼還會不斷演化下去,現在已經開始出現MVVM的一些思想的應用,到了最后,沒准就會完全演化成MVVM模式。

     簡單的代碼,只要不斷思考,慢慢的,所能學到的東西就會變得越來越多,最后甚至超出我們的想象。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM