天天看點

元件使用之ListView元件使用之ListView

元件使用之ListView

  清單在很多應用場景中都是非常重要的資訊展示方式,表型資料結構也是大部分使用者都能輕松接受并且看懂的一種。無論是聯系人清單,還是新聞标題清單,或者論壇主題清單,郵件清單,待辦事項清單等等;可以說有了清單就有了實作大部分使用者所需要的應用功能的基礎。

簡單實用的ListView

  安卓中的ListView元件就是個很常用的清單資料展示元件,它能靈活地實作各種各樣的清單效果,滿足大部分應用的資料展示和操作需求。在實踐中,它的布局控制能力以及View重用功能也極大地友善了開發者們的工作。

  ListView的使用方法有很多種,但都脫離不開它的基本工作方式,也就是ListView元件+Adapter的模式,每個ListView想要生效都必須綁定一個Adapter。

  舉例來說,使用ListView首先要在XML布局中寫一個ListView元件。

<ListView
    android:id="@+id/lvContent"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
</ListView>
           

  随後在對應的Activity或者Fragment代碼中為它設定Adapter。

  ListView要求使用任何繼承了BaseAdapter的類作為Adapter,而安卓系統自帶了一些Adapter友善使用,包括ArrayAdapter,SimpleAdapter,SimpleCursorAdapter。

  ArrayAdapter是最簡單的一種Adapter,它隻需要一個指定類型的List或者數組即可自動實作清單展示。

List<String> strList = new ArrayList<>();
String[] strArray = new String[];
ArrayAdapter<String> arrayAdapter;
public void setupList() {
    ListView listView = (ListView) findViewById(R.id.lvContent);
    // 通過List<String>清單建立ArrayAdapter,指定子項布局為R.layout.layout_rlv_item
    arrayAdapter = new ArrayAdapter<>(resContext, R.layout.layout_rlv_item, strList);
    // 通過String數組建立ArrayAdapter,指定子項布局為R.layout.layout_rlv_item
    arrayAdapter = new ArrayAdapter<>(resContext, R.layout.layout_rlv_item, strArray);
    // 通過List<String>清單建立ArrayAdapter,指定子項布局為R.layout.layout_rlv_item,指定文字展示的元件ID為tvItem
    arrayAdapter = new ArrayAdapter<>(resContext, R.layout.layout_rlv_item, R.id.tvItem, strList);
    // 通過String數組建立ArrayAdapter,指定子項布局為R.layout.layout_rlv_item,指定文字展示的元件ID為tvItem
    arrayAdapter = new ArrayAdapter<>(resContext, R.layout.layout_rlv_item, R.id.tvItem, strArray);
    // 必須将Adapter設定給ListView方可生效
    listView.setAdapter(arrayAdapter);
    // 資料變化後使用Adapter通知ListView進行重新整理
    arrayAdapter.notifyDataSetChanged();
}
           

  然後是SimpleAdapter,它比ArrayAdapter靈活,能應付較為複雜的清單展示需求。

List<Map<String,Object>> dataList = new ArrayList<>();
SimpleAdapter simpleAdapter;
public void setupList() {
    ListView listView = (ListView) findViewById(R.id.lvContent);
    // 每行資料中每個子項各自的Key
    String[] keys = new String[]{"name", "desc", "status"};
    // 每行中每個子項所用到的元件ID
    int[] views = new int[]{R.id.tvArg_1, R.id.tvArg_2, R.id.tvArg_3};
    simpleAdapter = new SimpleAdapter(resContext, dataList, R.layout.layout_rlv_item, keys, views);
    listView.setAdapter(simpleAdapter);
    simpleAdapter.notifyDataSetChanged();
}
           

  如上所示,dataList中儲存着每一行所需展示的資料,每行的資料以鍵值對的形式儲存,建立Adapter時要給出這些資料的Key,以String[]的形式;而最後一個參數是每個資料所對應的元件ID,SimpleAdapter會自動将讀取到的資料設定到指定的元件上。

  如果元件是Text View,則資料會被當做文本設定;如果元件是Image View,資料會被當做圖檔資源ID設定。

  使用SimpleAdapter已經可以做出基本的圖文并茂型的清單了,但它的設定比較死闆,必須按照一個固定的規則設定資料,對于清單項的控制也有所欠缺。

  至于SimpleCursorAdapter,這個Adapter是為了友善快捷地展示資料庫資料而設計的,但它限定了隻能使用Cursor來通路資料庫資料,是以局限性較大,使用場景不多。

靈活多變的BaseAdapter

  安卓自帶的Adapter總是不能滿足所有需求的,是以在清單展示需求複雜或者互動要求較高的時候,自定義Adapter才是主流選擇。

  BaseAdapter是ListView所使用的Adapter的抽象基類,檢視源代碼可知ArrayAdapter和SimpleAdapter等都是繼承了BaseAdapter的實作類。

  BaseAdapter又實作了兩個接口分别為ListAdapter和SpinnerAdapter,進一步追查會發現兩個接口的父級均為Adapter接口,它提供了Adapter的所有基本方法。

  使用BaseAdapter的方法是繼承它并實作所有接口方法,标準的實作如下

public class MyAdapter extends BaseAdapter {
    @Override
    public int getCount() {
        return ;
    }
    @Override
    public Object getItem(int position) {
        return null;
    }
    @Override
    public long getItemId(int position) {
        return ;
    }
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        return null;
    }
}
           

  四個方法都實作好了,一個Adapter就完成并可以投入使用了。

  • 方法getCount
    • 該方法用于傳回清單所需顯示的項目數量,一般而言是某個List的長度,也可以根據需求自行确定
    • 需要注意的是,直接傳回List.size()可能出錯,建議判空後再傳回
  • 方法getItem
    • 該方法用于按照位置索引傳回對應的資料項,傳回值為Object,具有通用性,在使用時需要強制轉換
    • 強制轉換一般不會出問題,但也可以不使用這個方法,直接通過資料清單獲得資料項
  • 方法getItemId
    • 該方法用于擷取清單項的唯一ID,一般情況下它是由AdapterView的getItemIdAtPosition方法調用,傳回某個位置的清單項的ID,友善使用
    • 實際使用中一般不會刻意去設定ID,直接傳回position即可應付大部分情況
  • 方法getView
    • 該方法是BaseAdapter的核心方法,每個清單項都是通過它建立并傳回的
    • 參數中的position是清單索引,convertView表示重用的清單項,parent表示父級元件,一般就是ListView本身
    • 如果直接在方法中建立子項并設定資料然後傳回,清單可以正常顯示,但性能非常不友好,尤其是當子項數量龐大或者子項本身比較複雜的時候。
    • 這是由于ListView的特點,每當清單滑動的時候它會檢測是否有新的項目被推倒螢幕上顯示,如果有就調用getView擷取,是以如果不進行重用優化則滑動清單會導緻大量的清單項被建立和丢棄,性能影響很大
    • 所謂的重用即是指将ListView推出顯示區域之外的清單項重新拿來使用,這樣就避免了大批量的項目建立和銷毀,getView将隻會建立有限個數的項目,每次有新項目需要顯示時都是就地修改重用項目
    • convertView參數就是用于這個目的,它會傳回ListView發現的被推出顯示區域的清單項,隻要檢查它是否為空即可完成重用
public View getView(int position, View convertView, ViewGroup parent) {
    if(convertView == null) {
        // 建立新的View
        convertView = LayoutInflater.from(resContext).inflate(R.layout.layout_rlv_item, null);
    } else {
        // 在此處将需要修改的元件擷取到
    }
    // 修改展示資料
    return convertView;
}
           

  上面的代碼簡單說明了如何重用,雖然一般而言隻要利用了convertView傳回重用項目這個特性的情況都可以稱為“重用”,但在實際項目中還是講究一個方式方法的,大部分情況都可以使用ViewHolder技巧來友善地重用項目。

  所謂ViewHolder技巧是指将清單項所有可以變動的元件都寫入一個ViewHolder類中,在建立新項目的同時建立該類的執行個體,設定各個元件并将該執行個體設定為convertView的Tag。

public View getView(int position, View convertView, ViewGroup parent) {
    ViewHolder holder;
    if(convertView == null) {
        // 建立新的View
        convertView = LayoutInflater.from(resContext).inflate(R.layout.layout_rlv_item, null);
        holder = new ViewHolder();
        holder.arg1 = (TextView) findViewById(R.id.tvArg_1);
        holder.arg2 = (TextView) findViewById(R.id.tvArg_2);
        holder.arg3 = (TextView) findViewById(R.id.tvArg_3);
        convertView.setTag(holder);
    } else {
        // 在此處将需要修改的元件擷取到
        holder = (ViewHolder) convertView.getTag();
    }
    // 修改展示資料
    return convertView;
}

class ViewHolder {
    TextView arg1;
    TextView arg2;
    TextView arg3;
}
           

  如上就是一個例子,使用ViewHolder能很好地兼顧重用以及便利性,此外還有很多不同的重用方法,但都脫離不了“将需要變化的元件緩存下來”這樣的基本思路。

進一步使用ListView

  ListView乍一看像是一個專門用來展示清單的元件,但通過介紹Base Adapter的自定義可以看到,它的靈活性很高,并不一定隻能用于清單,隻要适合需求,ListView也能作為一般的滑動型元件使用。

  舉個簡單的例子,如果應用的某個頁面需求是一個可以滑動的元件,元件内分成多個不同的子產品,每個子產品有自己的資料以及擷取資料的管道,如果直覺考慮那一定是使用ScrollView元件,将各個元件在内部碼好,按需擷取資料并展示。

  這樣的做法當然是可行的,可一旦需求變化或者子產品數量變多,内部邏輯複雜化,把這些子產品都擠在一個Fragment或者Activity中并不好,即便是使用MVP模式也不一定能避開大量的前端邏輯代碼聚集。

  ListView可以在此提供另一種解決方案,那便是将子產品都寫成自定義的元件,或者寫一個自定義抽象架構将子產品統一抽象出來;接着在Fragment或者Activity中建立一個清單對象,裡面放上所有需要的子產品,然後将這個清單作為輸入來自定義一個Adapter并交由ListView來展示。

  這樣一來雖然重用是不可能了,但因為子產品本身就是緩存好的,也不會對性能有太大影響,要控制子產品的顯示與否可以直接設定标志位,按照position設定convertView的Visibility,getView方法中按索引獲得子產品,直接通知子產品本身進行更新即可。

// 元件View抽象基類
public abstract class ComponentView {
    protected Context context;
    protected View contentView;

    public ComponentView(Context resContext) {
        context = resContext;
    }

    public void initView(View origin) { // 從現有View初始化
        contentView = origin;
        loadViews();
    }

    protected abstract void loadViews();

    protected abstract void loadMembers();

    public abstract void updateViews(); // 重新整理界面元件

    public View getContentView() {
        return contentView;
    }
}

List<ComponentView> viewList = new ArrayList<>();

public class ComponentListAdapter extends BaseAdapter {

    @Override
    public int getCount() {
        return viewList==null?:viewList.size();
    }

    @Override
    public Object getItem(int i) {
        return viewList==null?null:viewList.get(i);
    }

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

    @Override
    public View getView(int i, View view, ViewGroup viewGroup) {
        ComponentView current = (ComponentView) getItem(i);
        if(current != null) {
            current.updateViews();
            return current.getContentView();
        }
        return null;
    }
}
           

  于是子產品的代碼不需要由Fragment或者Activity來維護了,每個子產品自己維護自己的資料,有通信需求則使用Handler來實作,這樣的方法比使用ScrollView要靈活友善的得多。

  ListView的每行項目本身也是一般的元件,是以它可以和屬性動畫或者值動畫配合使用來實作比較好看的清單動畫,包括滑動删除,長按拖動,動畫進入等。

  總結起來,ListView看似清單勝似清單,它能做到多少事全看設計人員的想象力有多強。