Android列表控制元件

劉強東發表於2018-07-15

在Android中可滑動的列表是常見的UI佈局效果. 所以熟練掌握列表控制元件是肯定的; RecyclerView是列表控制元件中最重要最複雜的所以將在另一篇文章中詳細講解, 本文不涉及;

主要控制元件

  • GridView (網格檢視)
  • GridLayout (網格佈局)
  • ListView (列表檢視)
  • Gallery (被廢棄, 由HorizontalScrollView以及ViewPager替代)
  • ExpandableListView (可擴充套件列表檢視)
  • Spinner (下拉檢視)

ListView

主要功能使列表複用. 可以處理大量的列表控制元件組合. 在android21以前屬於最常用控制元件之一. 之後引入RecyclerView控制元件. 其作用比ListView更加強大可定製性更高.

但是某些時候用ListView比RecyclerView更加方便. 而且官方並沒有說應該被廢棄.

Android列表控制元件

佈局屬性

在佈局檔案中可以使用的xml屬性

屬性 描述
android:entries 引用一個陣列資源來構成列表
android:divider 分割線(android:divider="@null" 可以去除分割線)
android:dividerHeight 分割線高度(即使設為0也會有1dp高度)
android:footerDividersEnabled 是否開啟腳部分割線
android:headerDividersEnabled 會否開啟頭部分割線

預覽item

 tools:listitem="@layout/demo_item"
複製程式碼

函式

頭佈局和腳佈局

新增布局

void addHeaderView (View v, 
                Object data, 
                boolean isSelectable)

void addHeaderView (View v)

void addFooterView (View v, 
                Object data, 
                boolean isSelectable)

void addFooterView (View v)
複製程式碼

刪除佈局

boolean removeHeaderView (View v)

boolean removeFooterView (View v)
複製程式碼

分割線

// 分割線是否啟用
boolean areHeaderDividersEnabled ()
boolean areFooterDividersEnabled () 

// 是否啟用分割線
void setHeaderDividersEnabled (boolean headerDividersEnabled)
void setFooterDividersEnabled (boolean footerDividersEnabled)

// 頭部和腳部的越界顯示圖片, 預設情況ListView是無法越界, 故預設沒有效果
void setOverscrollFooter (Drawable footer)
void setOverscrollHeader (Drawable header)

Drawable getOverscrollHeader ()
Drawable getOverscrollFooter ()
複製程式碼

得到佈局數量

int getFooterViewsCount ()

int getHeaderViewsCount ()
複製程式碼

條目選擇

void setSelection (int position)
複製程式碼

平滑滾動

smoothScroll代表平滑滾動, By代表相對距離移動, To只會滾動到指定位置後無變化

// 相對滾動位置
void smoothScrollToPosition (int position)

void smoothScrollByOffset (int offset)
複製程式碼

除此之外還繼承了AbsListView的滾動方法

// 取消快速滾動條(快速滾動時依舊有個無法拖動的小滾動條)
void setSmoothScrollbarEnabled (boolean enabled)
boolean isSmoothScrollbarEnabled ()

  
// 平滑滾動的同時限定了最大滾動範圍
void smoothScrollToPosition (int position, 
                int boundPosition) // 範圍單位px


// 滾動畫素單位, 並且可以控制滾動持續時間
void smoothScrollBy (int distance, 
                int duration)

// 指定的滾動位置會向上偏移一段距離 
void smoothScrollToPositionFromTop (int position, 
                int offset) // 偏移距離
  
// 增加控制滾動持續時間
void smoothScrollToPositionFromTop (int position, 
                int offset, 
                int duration)
複製程式碼

滾動

void scrollListBy (int y)
複製程式碼

介面卡

學習介面卡就要區分方法的作用:

  • 用於重寫的方法. 這類方法是給ListView來呼叫的(介面卡通過setAdapter()傳入ListView)

  • 用於呼叫的方法. 暴露給使用者來控制Item的

ListView採用MVC的架構, View和Data由一個Adapter控制. ListView使用的Adapter是介面ListAdapter. 使用setAdapter()方法設定.

最基礎的介面卡ListAdapter屬於介面. 需要實現的方法很多. 為了方便提供了繼承ListAdapter的抽象介面卡

Android列表控制元件

介面卡的繼承關係

  • Adapter
    • ListAdapter
      • BaseAdapter
        • SimpleAdapter
        • ArrayAdapter
        • CursorAdapter
          • ResourceCursorADapter
          • SimpleCursorAdapter
      • WrapperListAdapter
        • HeaderViewListAdapter
    • SpinnerAdapter

以上講的介面卡適用於ListView和GridView以及Spinner.

ListAdapter

ListAdapter屬於介面, 一般情況並不直接使用, 因為沒必要重寫全部方法. 一般使用其子類.

// 是否啟用item. 如果fasle則不啟用. item處於無法選擇和點選的狀態
boolean isEnabled(int position); 

// 可以看到沒有position引數. 所以如果返回fasle則全部item都處於不啟用狀態
public boolean areAllItemsEnabled(); 
複製程式碼

繼承父類的方法

int getCount () // 決定ListView的Item數量

Object getItem (int position) // 得到item 資料. 這裡返回的值會在ListView中使用到

long getItemId (int position)  // 得到item 的 id. 這裡返回的值會在ListView中使用到

// 返回Item型別, 型別是否相同決定是否複用item
int getItemViewType (int position) 

// 返回Item檢視內容
View getView (int position,   // 位置
                View convertView, // 複用檢視 
                ViewGroup parent) // 父容器

// 返回Item型別數量
int getViewTypeCount () 
  
// id是否唯一
boolean hasStableIds () 

boolean isEmpty () // 是否為空

void registerDataSetObserver (DataSetObserver observer) // 註冊資料觀察者

void unregisterDataSetObserver (DataSetObserver observer) // 取消資料觀察者
複製程式碼

是否唯一

hasStableIds()這個方法是判斷id是否是有效. 返回true有效false無效.

  • 有效的情況下會通過getItemId()的返回id值來判斷item是否是相同
  • 無效的情況下會預設使用item的position來當作id

BaseAdapter

首先我講講最常用介面卡 BaseAdapter.

特點:

  • ListView支援高度自定義的Item

  • 需要自己重寫該介面卡來使用

重寫方法

        final String[] title = {"使用者", "首頁", "設定", "關於", "反饋"};

// 這是寫了個匿名類
        mListView.setAdapter(new BaseAdapter() {
            /**
             * 控制ListView的Item的數量
             * @return
             */
            @Override
            public int getCount() {
                return title.length;
            }

            /**
             * 控制ListView的某些方法返回的Object資料.
             * 例如ListView的getItemAtPosition()方法. 通過位置索引得到資料物件, 即該方法返回的Object物件
             *
             * @param position
             * @return
             */
            @Override
            public Object getItem(int position) {
                return null;
            }

            /**
             * 每次點選item都會回撥該方法. 同樣是為了ListView的getItemIdAtPosition()方法能夠得到item的id
             *
             * @param position
             * @return
             */
            @Override
            public long getItemId(int position) {
                return 0;
            }

            /**
             * 控制ListView的Item的檢視顯示
             *
             * @param position 當前顯示的檢視位置
             * @param convertView 快取的檢視. 用於複用item
             * @param parent 父容器佈局
             * @return
             */
            @Override
            public View getView(int position, View convertView, ViewGroup parent) {
              
                View view = View.inflate(MainActivity.this, R.layout.item_list, null);
                
                // 根據傳入的資料進行修改
                TextView text = ButterKnife.findById(view, R.id.text);
                text.setText(title[position]);
                
                return view;
            }
        });
複製程式碼

在主佈局中新增控制元件

<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.liangjingkanji.listview.MainActivity">

    <ListView
        android:id="@+id/list"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

</RelativeLayout>
複製程式碼

注意inflate item 檢視的時候是否啟用parent.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:orientation="vertical"
  android:layout_width="match_parent"
  android:layout_height="match_parent">
  
  <TextView
    android:id="@+id/text"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>
  
</LinearLayout>
複製程式碼

後面的佈局檔案我不會再寫出來了. 多餘程式碼影響閱讀性.

介紹下BaseAdapter相對於父類ListAdapter增加的方法. 這些方法都不是必須的

// 判斷介面卡是否存在item
boolean isEmpty () 

// **DropDownView**等方法是重寫的SpinnerAdapter的. 所以會在講解Spinner的時候詳細講. ListView和GridView用不到
View getDropDownView (int position, 
                View convertView, 
                ViewGroup parent)
 
boolean hasStableIds ()

void notifyDataSetChanged () // 資料如果發生變化通知ListView區域性更新

void notifyDataSetInvalidated () //資料如果發生變化通知ListView整個更新
複製程式碼

ArrayAdapter

ArrayAdapter是BaseAdapter的子類, 進行了進一步的封裝, 能夠快速實現最簡單的字串列表(同時限制了資料只能是單一的字串). 注意這不是抽象類. 可以直接建立物件.

特點:

  • 只需要構造方法就可以構造出一個ListView出來
  • 自定義很弱, ListView的資料只能是字串.

建立ArrayAdapter的時候需要指定泛型ArrayAdapter<T>. 泛型決定了構造方法能接受的資料型別

構造方法

ArrayAdapter (Context context, // 上下文
                int resource) // 佈局id. 只支援根佈局是TextView的佈局

ArrayAdapter (Context context, 
                int resource,  // 這個構造方法就支援任意佈局了
                int textViewResourceId)   // 指定一個Textview的id來設定資料. 

ArrayAdapter (Context context, 
                int resource, 
                T[] objects) // 直接在構造方法新增資料, 必須是字串的陣列

ArrayAdapter (Context context, 
                int resource, 
                int textViewResourceId, 
                T[] objects) // 同上

ArrayAdapter (Context context, 
                int resource, 
                List<T> objects)// 新增資料集合, 同樣必須是字串

ArrayAdapter (Context context, 
                int resource, 
                int textViewResourceId, // 同上
                List<T> objects) // 新增資料集合
複製程式碼

示例


String[] array = {"使用者", "首頁", "設定", "關於", "反饋"};

// 這裡的android.R.layout.simple_list_item_1是系統提供的TextView佈局檔案推薦直接拿來用. 可以點開看下原始碼.
listView.setAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, array));
複製程式碼

看上去是不是很方便就實現了ListView.

注意

  1. ArrayAdapter內部已經對ListView進行了Item複用

    Android列表控制元件
  2. 傳入的資料必須是字串. 原始碼中進行了判斷

    Android列表控制元件
  3. ArrayAdapter並沒有進行ViewHolder的複用.

方法

// 新增一個資料
void add (T object)

// 新增多個資料
void addAll (T... items)

// 新增集合
void addAll (Collection<? extends T> collection)

// 刪除全部資料
void clear ()
  
// 刪除指定資料
void remove (T object)

// 對應位置插入資料
void insert (T object, 
                int index)

// 預設為true. 所以每次你對資料修改的時候都會呼叫notifyOnChange方法. 多次呼叫影響效率(頻繁重新整理UI). 可以用該方法設定false.然後自己呼叫notifyChange等方法來更新Item的UI.
void setNotifyOnChange (boolean notifyOnChange)

// 這是一個靜態方法直接建立ListView. 簡單直接
ArrayAdapter<CharSequence> createFromResource (Context context, 
                int textArrayResId,  // 文字資料
                int textViewResId)  // 文字控制元件id

// 得到傳入的上下文
Context getContext ()

// 通過資料得到索引
int getPosition (T item)
複製程式碼

再介紹兩個用於重寫支援ListView的篩選Item功能的方法

// 過濾器. 例如聯絡人的聯想篩選
Filter getFilter ()

// 對資料使用標準的比較器排序操作
void sort (Comparator<? super T> comparator)
複製程式碼

SimpleAdapter

SimpleAdapter是這三種中最複雜的介面卡, 但是資料填充ListView的item很方便. 同樣非抽象類可以直接建立物件使用.

同樣先介紹構造方法

SimpleAdapter (Context context,  
                List<? extends Map<String, ?>> data,  // 資料. 每個Item對應一個Map集合
                int resource,  // Item的佈局
                String[] from, // Map集合是無序的. 所以需要一個key陣列來控制順序
                int[] to) // 資料填充到該陣列對應的View id
複製程式碼

示例

建立一個ListView作為資料

// List集合儲存Map集合代表資料
List<Map<String, Object>> list = new ArrayList<>();

Map<String, Object> map = new HashMap<>(); 
map.put("icon", R.mipmap.ic_launcher);
map.put("name", "設定");

Map<String, Object> map2 = new HashMap<>();
map2.put("icon", R.mipmap.ic_launcher);
map2.put("name", "關於");

list.add(map);
list.add(map2);

// String資料的值是Map集合中的鍵, 對應int陣列中的控制元件id. 將鍵對應的值填充到對應的id控制元件上
listView.setAdapter(new SimpleAdapter(this,list, R.layout.list_item, new String[]{"icon", "name"}, new int[]{R.id.icon, R.id.name}));
複製程式碼

檢視原始碼可以看出來其實SimpleAdapter只支援TextView和Checkable以及ImageView三種控制元件的屬性值設定, 如果非該三種將丟擲語法異常資訊.

Android列表控制元件

如果傳入ViewBinder介面可以在回撥方法內手動處理資料填充

SimpleAdapter.ViewBinder getViewBinder ()
void setViewBinder (SimpleAdapter.ViewBinder viewBinder)
複製程式碼

WrapperListAdapter

之前就提過這個介面卡和BaseAdapter一樣繼承自ListAdapter. 不過這是介面. 內部就一個方法:

// 返回介面卡物件. 等同於getAdapter
ListAdapter getWrappedAdapter ()
複製程式碼

HeaderViewListAdapter

支援頭佈局和腳佈局的ListAdapter

構造方法

// 可以看出主要就是加入兩個包含頭佈局和腳佈局的集合外加一個普通ListAdapter即可
HeaderViewListAdapter (ArrayList<ListView.FixedViewInfo> headerViewInfos, 
                ArrayList<ListView.FixedViewInfo> footerViewInfos, 
                ListAdapter adapter)
複製程式碼

其實ListView支援直接新增頭佈局和腳佈局, 這個之前提過. addHeaderViewaddFootView內部實現就是將原有介面卡包裹成HeaderViewListAdapter.

FixedViewInfo

public class FixedViewInfo {
  // 檢視
  public View view;
  // 資料
  public Object data;
  // 是否可選擇. Selectable狀態
  public boolean isSelectable;
}
複製程式碼

點選事件

// 普通點選事件
void setOnClickListener (View.OnClickListener l)

// item點選事件
void setOnItemClickListener (AdapterView.OnItemClickListener listener)

// item長按點選事件
void setOnItemLongClickListener (AdapterView.OnItemLongClickListener listener)

// item選擇事件
void setOnItemSelectedListener (AdapterView.OnItemSelectedListener listener)
複製程式碼

多條目

常常一個列表中不可能條目都是一模一樣的, 需要摻雜一些不同型別的Item.

下面介紹兩個方法getViewTypeCountgetItemViewType

預設情況下兩個方法實現

Android列表控制元件

我們通過重寫BaseAdapter中的這兩個方法實現多條目

Tip: 如果直接在getView方法中通過position判斷的話會導致檢視複用機制(convertView)無法正常執行. 會導致檢視錯亂.

記憶體優化

划動螢幕時ListView從螢幕消失的條目會被銷燬, 而新出現在螢幕的條目會被建立. 如果在大量的且顯示內容過多的條目快速划動會造成回收記憶體的速度趕不上建立條目物件的速度, 而造成oom記憶體溢位. 為了避免這種情況ListView需要進行記憶體複用優化.

複用條目

條目在每次被顯示在螢幕上的時候都會呼叫getView()方法, 如果每次在該方法中都建立一個View物件的情況下有大量的條目會導致OOM記憶體溢位, 所以使用的時候需要複用這個View物件, 而getView方法中已經快取了這個物件即convertView引數,只需要呼叫即可.

/**
* 返回條目顯示內容, 就是一個View物件

* @param position 條目所在位置
* @param convertView 條目複用物件
* @param parent
* @return
*/
@Override
public View getView(int position, View convertView, ViewGroup parent) {
    View view = null;
  
  // 如果convertView不為空則複用
    if (convertView == null) {
        view = View.inflate(MainActivity.this, R.layout.list_item, null);
    }else {
        view = convertView;
    }
  
    TextView text = (TextView) view.findViewById(R.id.text);
    text.setText("當前條目" + position);
    return view;
}
複製程式碼

ViewHolder

每次findViewById都會進行整個佈局的遍歷, 容易影響程式的執行效率, 所以可以建立ViewHolder類進行控制元件引用的儲存. 然後每個Item對該ViewHolder進行存取操作;

關鍵方法setTag()

//在外面先定義,ViewHolder靜態類
static class ViewHolder
{
    public ImageView img;
    public TextView title;
    public TextView info;
}

//然後重寫getView
@Override
public View getView(int position, View convertView, ViewGroup parent) {
    ViewHolder holder;
    if(convertView == null)
    {
        holder = new ViewHolder();
      
        convertView = mInflater.inflate(R.layout.list_item, null);
      
        holder.img = (ImageView)item.findViewById(R.id.img)
        holder.title = (TextView)item.findViewById(R.id.title);
        holder.info = (TextView)item.findViewById(R.id.info);
      
        convertView.setTag(holder);
    }else
    {
        holder = (ViewHolder)convertView.getTag();
    }
        holder.img.setImageResource(R.drawable.ic_launcher);
        holder.title.setText("Hello");
        holder.info.setText("World");
    }
                                                                                                
    return convertView;
}
複製程式碼

RecyclerView都是強制使用ViewHolder;

選擇模式

ListView其實支援多種選擇模式(ChoiceModel)的設定

/**
 * 沒有選擇模式
 */
public static final int CHOICE_MODE_NONE = 0;
/**
 * 單選模式
 */
public static final int CHOICE_MODE_SINGLE = 1;
/**
 * 多選模式
 */
public static final int CHOICE_MODE_MULTIPLE = 2;
/**
 * The list allows multiple choices in a modal selection mode
 */
public static final int CHOICE_MODE_MULTIPLE_MODAL = 3;
複製程式碼

通過佈局屬性設定選擇模式

android:choiceMode
複製程式碼
Constant Value Description
multipleChoice 2 多選模式
multipleChoiceModal 3 遮蔽點選事件的多選模式
none 0 非選模式
singleChoice 1 單選模式

關於選擇模式的方法都在AbsListView抽象類中

boolean isItemChecked (int position)
// 判斷指定位置的item是否被選中

void setItemChecked (int position, 
                boolean value)
// 設定item選中

int getCheckedItemCount ()
// 被選中的item數量

long[] getCheckedItemIds ()
// 如果非非選模式並且(hasStableIds() == true) 才有效

int getCheckedItemPosition ()
// 選擇的位置索引(只在單選模式有效)

SparseBooleanArray getCheckedItemPositions ()
// 得到被選中的所有item的位置

void clearChoices ()
// 清理所有被選擇

int getChoiceMode ()
// 選擇模式

void setChoiceMode (int choiceMode)
// 設定選擇模式
    
void setMultiChoiceModeListener (AbsListView.MultiChoiceModeListener listener)
// 多選監聽器
複製程式碼

選擇模式生效必須滿足以下兩點:

  1. 設定選擇模式(預設情況是單選模式)

  2. Item沒有子控制元件攔截焦點

    android:focusable="false"
    複製程式碼

遮蔽點選事件的多選模式

該模式下要想進入多選模式有以下兩種方式:

  1. 長按item
  2. 通過函式呼叫setItemChecked

GridView

翻譯即"網格檢視"的意思. 和ListView的區別就是網格列表.

但是注意GridView並不是水平滑動佈局, 只不過是增加列數的ListView(即滿足列數限制就換行, 預設一行即ListView). 需要水平滑動佈局可以使用HorizontalScrollView以及RecyclerView的GridLayoutManager佈局

GridView相容ListView的所有介面卡.

佈局屬性

每個屬性都有對應的方法. 詳細介紹看方法.

屬性 描述
android:columnWidth 列寬(預設無效)需要配合拉伸模式使用
android:numColumns 列數(預設為1)
android:stretchMode 拉伸模式
android:horizontalSpacing 水平間距
android:verticalSpacing 垂直間距
android:gravity 對齊方式

示例

用法和ListView一樣. 支援之前講過的所有的ListAdapter介面卡;

mList.setAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, title));

mList.setNumColumns(2); // 設定列數. 預設1. 即和ListView沒區別
複製程式碼
Android列表控制元件

列寬和間距

GridView就像一個網格列表. 學會如何排列網格是非常重要的.

Android列表控制元件

上圖示註了GridView的列寬以及水平和垂直間距

網格寬度

// 格子寬度. 畫素單位
int getColumnWidth ()
void setColumnWidth (int columnWidth)
複製程式碼

Tip: 如果在item的佈局中設定了固定大小, 會導致裁剪效果.

拉伸模式

拉伸模式我認為是GridView最重要也是最難理解的地方. 注意理解我的分析

// 設定拉伸模式
void setStretchMode (int stretchMode)
複製程式碼

支援四種拉伸模式

  1. NO_STRETCH

    不拉伸, 尺寸自己控制. 該模式下必須設定網格寬度否則什麼都不顯示

  2. STRETCH_COLUMN_WIDTH (拉伸列寬)

    預設模式, 列寬由螢幕拉伸決定. 所以之前介紹的setColumnWidth無效. 網格內容會根據螢幕的大小來比例縮放控制

  3. STRETCH_SPACING (拉伸間距)

    該模式必須制定列寬否則不顯示, 同時自己指定的間距(包括水平間距和垂直間距)無效 Android列表控制元件

  4. STRETCH_SPACING_UNIFORM

    此模式下列寬和間距都是有效設定值, 並且水平方向最左邊也會有間距.

    但是由於都是有效值所以無法做到螢幕均布的效果

    Android列表控制元件

除了STRETCH_COLUMN_WIDTH其他模式都需要指定網格寬度(setColumnWidth).

總結:

  1. 拉伸間距或者列寬就無法設定其值.
  2. 如果列寬和間距都非拉伸模式就無法均布
  3. 如果不是拉伸列寬的情況下就必須制定列寬值否則不顯示內容.

間距

// 設定水平間隔
int getHorizontalSpacing ()
void setHorizontalSpacing (int horizontalSpacing)

// 設定垂直間隔
void setVerticalSpacing (int verticalSpacing)
int getVerticalSpacing ()
複製程式碼

列數

void setNumColumns (int numColumns)
int getNumColumns ()
複製程式碼

對齊方式

int getGravity ()
void setGravity (int gravity)
複製程式碼

方法介紹

// 返回介面卡
ListAdapter getAdapter ()
複製程式碼

列表巢狀

這裡介紹GridView和ListView之間或者兩種相同列表的相互巢狀.

列表巢狀的問題分為兩種情況:

  1. 顯示不完整
  2. 被巢狀的列表無法滑動

第一種

如果存在列表巢狀了一個高度為wrap_content|match_parent的列表時會發現被巢狀的列表無法完全顯示, 但是如果固定的高度就不會發生這種情況, 但是很多資料都並不是固定的而是通過資料的數量動態載入.

通過自定義onMeasure方法給被巢狀的ListView一個無限的高度最大值

	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2,
				MeasureSpec.AT_MOST);
		super.onMeasure(widthMeasureSpec, expandSpec);
	}
複製程式碼

integer的最大值是32位, 之所以右移兩位是因為在MeasureSpec中前兩位表示模式

// 移位位數 30 private static final int MODE_SHIFT = 30;

// int 型佔 32 位,左移 30 位,該屬性表示掩碼值,用來與 size 和 mode 進行 "&" 運算,獲取對應值。
private static final int MODE_MASK = 0x3 << MODE_SHIFT;

// 左移 30 位,其值為 00...(此處省略 30 個0)
public static final int UNSPECIFIED = 0 << MODE_SHIFT;

// 左移 30 位,其值為 01...(此處省略 30 個0)
public static final int EXACTLY = 1 << MODE_SHIFT;

// 左移 30 位,其值為 10...(此處省略 30 個0)
public static final int AT_MOST = 2 << MODE_SHIFT;

如果不想重寫就通過ListView的Item數量來動態的設定ListView的固定高度

private void setListViewHeight(ListView listView) {

    ListAdapter listAdapter = listView.getAdapter();

    if (listAdapter == null) {    
            return;
    }

    int totalHeight = 0;

    for (int i = 0; i < listAdapter.getCount(); i++) {

            View listItem = listAdapter.getView(i, null, listView);
            listItem.measure(0, 0);
            totalHeight += listItem.getMeasuredHeight();
    }


    ViewGroup.LayoutParams params = listView.getLayoutParams();
    params.height = totalHeight
                    + (listView.getDividerHeight() * (listAdapter.getCount() - 1));

    listView.setLayoutParams(params);

}
複製程式碼

或者你可以在getView方法中設定每個View物件的layoutParams

第二種

上面介紹的兩種方法只是針對ListView被巢狀時不顯示的問題. 但是如果ListView裡面巢狀的要是一個可滑動的ListView就需要另外解決了.

其實Google已經考慮到這種問題了, 提供方法可以直接生效

ViewCompat.setNestedScrollingEnabled(mList,true); // api21以上可以直接使用View而不是ViewCompat
複製程式碼
Android列表控制元件

關於NestedScrollView以及ScrollView中巢狀ListView或GridView不會出現第二種情況, 但是也會出現顯示不完全. 同樣解決方法.

對於RecyclerView中巢狀GridView和ListView第二種方法的解決辦法就失效了.

ExpandableListView

ExpandableListView 是支援分組展開的ListView.

主要分為兩部分:組和子列表

Attributes

指示器圖示

android:groupIndicator

android:childIndicator
複製程式碼

指示器間隔

android:indicatorEnd	
android:indicatorLeft	
android:indicatorRight	
android:indicatorStart

android:childIndicatorEnd	
android:childIndicatorLeft	
android:childIndicatorRight	
android:childIndicatorStart
複製程式碼

指示器圖示會和你的getView()檢視內容重疊. 建議給item設定一個padding

子列表分割線

android:childDivider
複製程式碼

分割線可以是圖片或者顏色. 但是無論如何都是一個高度為1dp的全屏寬度的分割線. 且必須介面卡isChildSelectable()方法返回true才會顯示.

去除預設的指示器和分割線

android:divider="@null"
android:groupIndicator="@null"
複製程式碼

ExpandableListAdapter

Android列表控制元件

ExpandaleListAdapter屬於介面. 一般情況直接使用其子類.

abstract boolean	areAllItemsEnabled()
  
// 該方法用於介面卡返回檢視資料
abstract Object	getChild(int groupPosition, int childPosition)

  
// 子列表item id
abstract long	getChildId(int groupPosition, int childPosition)

// 子列表item 檢視
abstract View	getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent)

// 子列表
abstract int	getChildrenCount(int groupPosition)


abstract long	getCombinedChildId(long groupId, long childId)


abstract long	getCombinedGroupId(long groupId)

// 組物件
abstract Object	getGroup(int groupPosition)

// 組數量
abstract int	getGroupCount()

// 組id
abstract long	getGroupId(int groupPosition)

// 返回組檢視
abstract View	getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent)

// id是否穩定, 該方法等同於ListView
abstract boolean	hasStableIds()

// 子列表是否可選
abstract boolean	isChildSelectable(int groupPosition, int childPosition)

abstract boolean	isEmpty()

// 組摺疊回撥
abstract void	onGroupCollapsed(int groupPosition)

// 組展開回撥
abstract void	onGroupExpanded(int groupPosition)
複製程式碼

BaseExpandableListAdapter

類似BaseAdapter, 需要實現的方法上面已經介紹過了. 下面直接示例;

public class CustomExpandableListAdapter extends BaseExpandableListAdapter {

    private  Context mContext;
    private  List<String> mGroupData;
    private  List<ArrayList<String>> mChildData;

    public CustomExpandableListAdapter(Context context, List<String> groupData, List<ArrayList<String>> childData) {
        mContext = context;
        mGroupData = groupData;
        mChildData = childData;
    }

    @Override public int getGroupCount() {
        return mGroupData.size();
    }

    @Override public int getChildrenCount(int i) {
        return mChildData.size();
    }

    @Override public Object getGroup(int i) {
        return mGroupData.get(i);
    }

    @Override public Object getChild(int i, int i1) {
        return mChildData.get(i).get(i1);
    }

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

    @Override public long getChildId(int i, int i1) {
        return i+i1;
    }

    @Override public boolean hasStableIds() {
        return true;
    }

    @Override public View getGroupView(int i, boolean b, View view, ViewGroup viewGroup) {

        if(view == null) {
        }

        View groupView = LayoutInflater.from(mContext).inflate(R.layout.item_group_text, viewGroup, false);
        TextView tvTitle = ButterKnife.findById(groupView, R.id.tv_title);
        tvTitle.setText(mGroupData.get(i));
        return groupView;
    }

    @Override public View getChildView(int i, int i1, boolean b, View view, ViewGroup viewGroup) {
        View childView = LayoutInflater.from(mContext).inflate(R.layout.item_group_text, viewGroup, false);
        TextView tvTitle = ButterKnife.findById(childView, R.id.tv_title);
        tvTitle.setText(mChildData.get(i).get(i1));
        return childView;
    }

    @Override public boolean isChildSelectable(int i, int i1) {
        return true;
    }
}
複製程式碼

SimpleExpandableListAdapter

SimpleExpandableListAdapter和SimpleAdapter差不多不屬於抽象類, 只需要使用構造方法建立例項即可.

SimpleExpandableListAdapter (Context context, 
                List<? extends Map<String, ?>> groupData, // 資料 
                int groupLayout,  // 組檢視佈局
                String[] groupFrom, // 資料鍵
                int[] groupTo,   // 佈局控制元件id
                List<? extends List<? extends Map<String, ?>>> childData, 
                int childLayout,  // 子列表檢視
                String[] childFrom, 
                int[] childTo)

SimpleExpandableListAdapter (Context context, 
                List<? extends Map<String, ?>> groupData, 
                int expandedGroupLayout, // 租展開佈局
                int collapsedGroupLayout, // 組摺疊佈局
                String[] groupFrom, 
                int[] groupTo, 
                List<? extends List<? extends Map<String, ?>>> childData, 
                int childLayout, 
                String[] childFrom, 
                int[] childTo)

SimpleExpandableListAdapter (Context context, 
                List<? extends Map<String, ?>> groupData, 
                int expandedGroupLayout, 
                int collapsedGroupLayout, 
                String[] groupFrom, 
                int[] groupTo, 
                List<? extends List<? extends Map<String, ?>>> childData, 
                int childLayout, 
                int lastChildLayout, // 租最後一個子列表的檢視
                String[] childFrom, 
                int[] childTo)
複製程式碼

從構造方法可以看到SimpleExpandableListAdapter還支援兩種固定的多型別佈局. 不過需要注意的是多型別佈局的groupTo/childTo控制元件id還是必須包括在內的.

監聽器

子列表點選事件

void setOnChildClickListener (ExpandableListView.OnChildClickListener onChildClickListener)
複製程式碼

組點選事件

void setOnGroupClickListener (ExpandableListView.OnGroupClickListener onGroupClickListener)
複製程式碼

組收縮和展開事件

void setOnGroupCollapseListener (ExpandableListView.OnGroupCollapseListener onGroupCollapseListener)

void setOnGroupExpandListener (ExpandableListView.OnGroupExpandListener onGroupExpandListener)
複製程式碼

取消組的摺疊和收縮只需要在組點選事件的回撥中返回true即可

        mExpand.setOnGroupClickListener(new ExpandableListView.OnGroupClickListener() {
            @Override
            public boolean onGroupClick(ExpandableListView expandableListView, View view, int i,
                    long l) {
                return true;
            }
        });
複製程式碼

Spinner

雖然Spinner是容器佈局不過並不支援子控制元件.因為其繼承了AdapterView;

簡單實現

佈局中建立控制元件

<Spinner
         android:id="@+id/spinner"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:entries="@array/entries"
         />
複製程式碼

values/strings 建立陣列實體

<resources>
    <string-array name="entries">
        <item>Mercury</item>
        <item>Venus</item>
        <item>Earth</item>
        <item>Mars</item>
        <item>Jupiter</item>
        <item>Saturn</item>
        <item>Uranus</item>
        <item>Neptune</item>
    </string-array>
</resources>
複製程式碼
Android列表控制元件

Attributes

// 水平和垂直偏移. 只有垂直是有效的
android:dropDownHorizontalOffset

android:dropDownVerticalOffset

android:dropDownWidth // 下拉彈窗寬度

android:dropDownSelector // 下拉顏色選擇器

android:gravity 

android:popupBackground // 下拉彈窗背景顏色

android:spinnerMode // 對話方塊/下拉彈窗

android:prompt // 對話方塊模式的標題, 注意需要引用string
複製程式碼

選擇監聽

mSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
  @Override
  public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {

  }

  @Override public void onNothingSelected(AdapterView<?> parent) {

  }
});
複製程式碼

如果只是想使用下拉選單可以看看ListPopupWindow; 單純的下拉選單, 提供依附功能和自定義寬高;

相關文章