一、 AdapterView 及其子類
AdapterView 是一組重要的組件,它的主要作用是通過列表的形式顯示數據。
AdapterView 本身是一個抽象類,常用的 ListView(列表)、Spinner(下拉列表)、Gallery(縮略圖),GridView(網格圖)都是 AdapterView 的子類。子類的用法相似,知識在顯示上略有不同。
AdapterView 的子類繼承關系
下面是 AdapterView 的類定義(成員變量及方法省略)
/** * An AdapterView is a view whose children are determined by an {@link Adapter}.
* AdapterView是一個視圖,其子視圖由{@link Adapter}確定 * See {@link ListView}, {@link GridView}, {@link Spinner} and {@link Gallery} for commonly used subclasses of AdapterView.
* 提示AdapterView 的常用子類 */ public abstract class AdapterView<T extends Adapter> extends ViewGroup { }
AdapterView 具有如下特征:
1. AdapterView 繼承了 ViewGroup ,說明它本質是容器。
2. AdapterView 可以包含多個 ”列表項“ (即子視圖),子視圖由與之關聯的 Adapter 確定,以合適的方式顯示出來。
二、AdapterView 的子類 ListView
以垂直列表的形式顯示所有的列表項,並且能夠根據數據的長度自適應顯示。下面就是使用 AdapterView 實現的效果
ListView 的使用大致上可以分為四個步驟:添加 ListView 組件、存儲數據、設置列表項item的布局文件、加載數據/資源進行顯示、添加監聽。下面講解幾個 ListView 的使用方法:
1. 當整個Activity中只有一個ListView組件時,可以使用ListActivity。
ListActivity類繼承於Activity類,默認綁定了一個ListView組件,並提供一些與ListView處理相關的操作。
ListActivity類常用的方法為getListView(),該方法返回綁定的ListView組件。
一旦在程序中獲得了ListView之后,接下來就需要為ListView設置它要顯示的列表項了。在這一點上,ListView顯示出了AdapterView的特征:通過setAdapter(Adapter)方法為之提供Adapter,並由Adapter提供列表項即可。
2. 最簡單的方法——通過 android:entried 調用加載數組資源
a. 在布局文件中添加 ListView 組件 ( ListView 的 id 號為 android:list )
使用特定 id 號的好處:在 listView 中可以直接通過 getListView() 方法得到 ListView 實例
<ListView android:id="@+id/android:list" android:layout_width="match_parent" android:layout_height="wrap_content" />
b. 在 values 文件夾中添加或設置 string.xml 文件用來存儲數據(為了體現滾輪效果,可以適量增加 item 數目)
<string-array name="fruit_array"> <item>Apple</item> <item>Banana</item> <item>Orange</item> <item>Watermelon</item> <item>Pear</item> <item>Grape</item> <item>Pineapple</item> <item>Strawberry</item> <item>Cherry</item> <item>Mango</item> </string-array>
c. 通過 android:entried 調用加載數組資源
<ListView android:id="@+id/android:list" android:layout_width="match_parent" android:layout_height="wrap_content" android:entries="@array/fruit_array" />
由於這個方法只用於顯示文本類型的數據,不經常使用,添加監聽的部分在這里就不展開講解了
3. AdapterView + Adapter、
使用數組資源創建 ListView 是一種非常簡單的方式,但是這種 ListView 能定制的內容很少。如果想豐富 ListView 的外觀、內容,添加組件的行為,就需要把 ListView 當作 AdapterView 來使用,通過 Adapter 自定義每一個 列表項的 外觀、內容、行為動作等。
AdapterView + Adapter 的工作原理
類似於 MVC 框架,數據源(Model)存放數據,利用控制器(Controller)將數據顯示在視圖(View)上。
ListView 相當於 V(View),用於顯示視圖;Adapter 相當於 控制器(Controller)
當需要數據時,ListView 會從 Adapter 中取出數據進行顯示。
3-1 基於ArrayAdapter 使用 ListView
a. 添加 ListView 組件(同上)
b. 存儲數據。存儲數據的方式可以多種多樣的,可以將數據存儲在 string.xml 文件中,可以將其存儲為 string[] 數組形式,也可以將其存儲為 List 列表形式。
c. 設置列表項item的布局文件。
item 中包含多種組件,例如 ImageView,Button 等,我們可以用 ArrayList 進行加載,將數據資源存儲在一個類中,接着創建一個類繼承 ArrayAdapter ,在 getView() 方法中將數據資源與 item 布局中的組件聯系在一起即可,這在《第一行代碼》有非常詳細的講解,在這篇博客中,我使用 SimpleAdapter 進行加載,會在下面進行講解。
該 LIstView 只顯示最簡單的當行文本內容,因此選擇一個android 自帶的簡單布局
android.R.layout.simple_list_item_1
d. 利用 adapter 將數據顯示在 ListView 上。也就是實例化 ArrayAdapter 類,在 ListView 中添加 Adapter 兩個步驟。
實例化 ArrayAdapter 類
ArrayAdapter 有很多的構造方法,例如
public ArrayAdapter(@NonNull Context context, @LayoutRes int resource) { this(context, resource, 0, new ArrayList<>()); } public ArrayAdapter(@NonNull Context context, @LayoutRes int resource, @IdRes int textViewResourceId) { this(context, resource, textViewResourceId, new ArrayList<>()); } public ArrayAdapter(@NonNull Context context, @LayoutRes int resource, @NonNull T[] objects) { this(context, resource, 0, Arrays.asList(objects)); } public ArrayAdapter(@NonNull Context context, @LayoutRes int resource, @IdRes int textViewResourceId, @NonNull T[] objects) { this(context, resource, textViewResourceId, Arrays.asList(objects)); } public ArrayAdapter(@NonNull Context context, @LayoutRes int resource, @NonNull List<T> objects) { this(context, resource, 0, objects); }
public ArrayAdapter(@NonNull Context context, @LayoutRes int resource, @IdRes int textViewResourceId, @NonNull List<T> objects) { this(context, resource, textViewResourceId, objects, false); }
我們可以發現,上面五個構造方法都是調用最后一個構造方法,而最后構造方法調用了另一個方法,這個方法是私有的。我們來詳細看看這個構造方法
/** * Constructor * @param context 當前上下文。 * @param resource 子項布局id:布局文件的資源ID,其中包含在實例化視圖時要使用的布局。 * @param textViewResourceId 布局資源中要填充的TextView的ID * @param objects 要在ListView中表示的對象 */ private ArrayAdapter(@NonNull Context context, @LayoutRes int resource, @IdRes int textViewResourceId, @NonNull List<T> objects, boolean objsFromResources) { mContext = context; mInflater = LayoutInflater.from(context); mResource = mDropDownResource = resource; mObjects = objects; mObjectsFromResources = objsFromResources; mFieldId = textViewResourceId; }
在上面的構造方法中,我們最經常使用的是第三個
/** * Constructor * @param context 當前上下文。 * @param resource 子項布局id:布局文件的資源ID,其中包含在實例化視圖時要使用的布局。 * @param objects 要在ListView中表示的對象 */
public ArrayAdapter(@NonNull Context context, @LayoutRes int resource, @NonNull T[] objects) {
this(context, resource, 0, Arrays.asList(objects));
}
可以看到,在這個方法中,使用 泛型 來表示傳進來的要在 ListView 中顯示的對象,着說明你可以傳入許多種數據類型,但是最終會會使用 Arrays.asList(Objects)) 進行類型轉換,返回 MutableList
d. 添加監聽——這里只添加 列表項的點擊事件。使用
總的代碼如下:
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main);
//獲取 ListView 控件 ListView listView = (ListView)findViewById(R.id.list_view);
//獲取數據 final String[] fruitArray = getResources().getStringArray(R.array.fruit_array);
// 為 ListView 添加控制器 ArrayAdapter ArrayAdapter adapter = new ArrayAdapter(this, android.R.layout.simple_list_item_1,fruitArray); listView.setAdapter(adapter);
//為 ListView 的列表項添加鼠標點擊事件 listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { /** * @param adapterView 發生單擊事件的列表項 ListView * @param view 被單擊控件 view * @param i 在列表項中的位置 position * @param l 被單擊列表項的行ID */ @Override public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) { String Tag = "onItemClick======"; Log.d(Tag,"position="+i); Log.d(Tag,"行 ID"+l); Toast.makeText(MainActivity.this,fruitArray[i],Toast.LENGTH_SHORT); } }); } }
3-2 基於 SimpleAdapter 使用 ListView
SimpleAdapter的擴展性最好,可以定義各種各樣的布局出來,可以放上ImageView(圖片),還可以放上Button(按鈕),CheckBox(復選框)等等
a. 添加 ListView 組件(同上)
b. 設置列表項 item 的布局文件
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:id="@+id/image" android:layout_weight="4" android:layout_width="0dp" android:layout_height="150dp" android:layout_gravity="left" android:layout_marginLeft="1dp" android:padding="7dp"/> <LinearLayout android:layout_weight="6" android:layout_width="0dp" android:layout_height="wrap_content" android:orientation="vertical" android:layout_gravity="center" android:layout_marginLeft="5dp"> <TextView android:id="@+id/title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="left"/> <TextView android:id="@+id/info" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="left"/> <Button android:id="@+id/button" android:layout_width="30dp" android:layout_height="30dp" android:background="@drawable/btn" android:layout_gravity="right" android:layout_marginRight="30dp" android:padding="10dp" android:focusable="false"/> <!--android:focusable="false"--> </LinearLayout> </LinearLayout>
效果:
c. 存儲數據。
同樣,我們通過分析 SimplaeAdapter 構造方法來決定數據類型。SimpleAdapter 只有一個構造方法
/** * Constructor * * @param context 運行與此SimpleAdapter關聯的View的上下文,即放置 ListView 的上下文環境 * @param data 數據為一個列表 list ,list 中的數據以 Map 類型存儲列表中的每個條目對應於列表中的一行。 列表中的每個條目對應於列表中的一行。 * @param resource 列表項的布局文件 * @param from A list of column names that will be added to the Map associated with each item. * @param to The views that should display column in the "from" parameter. These should all be * TextViews. The first N views in this list are given the values of the first N columns * in the from parameter. */ public SimpleAdapter(Context context, List<? extends Map<String, ?>> data, @LayoutRes int resource, String[] from, @IdRes int[] to) { mData = data; mResource = mDropDownResource = resource; mFrom = from; mTo = to; mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); }
可以知道,數據存儲在 List 中,List 中每一項數據對應 ListView 的一行,以 Map 的形式進行存儲。 from 和 to 數組分別代表Map中 key 值和 Item 布局文件中組件的 ID 號,呈一一對應關系。
public List<HashMap<String,Object>> getData(){ List<HashMap<String,Object>> list = new ArrayList<>(); HashMap<String,Object> map; map= new HashMap<>(); map.put("image",R.drawable.i1); map.put("title","開心的哆啦A夢"); map.put("info","cute cute cute cute cute cute cute cute cute cute "); list.add(map); map = new HashMap<>(); map.put("image",R.drawable.i2); map.put("title","貪吃的哆啦A夢"); map.put("info","卡哇伊 卡哇伊 卡哇伊 卡哇伊 卡哇伊 卡哇伊 卡哇伊 "); list.add(map); map = new HashMap<>(); map.put("image",R.drawable.i3); map.put("title","大哭的哆啦A夢"); map.put("info","cute cute cute cute cute cute cute cute cute cute "); list.add(map); return list; }
d. 利用 adapter 加載數據/資源進行顯示
//初始化列表數據 final List<HashMap<String,Object>> list = getData(); //為ListView 添加 Adapter String[] mapKeyArray = {"image","title","info"}; int[] layoutIdArray = {R.id.image,R.id.title,R.id.info}; SimpleAdapter adapter = new SimpleAdapter(MainActivity.this,list,R.layout.listview_item, mapKeyArray , layoutIdArray );
e. 添加監聽
如果沒有重寫 Adapter 的 getView() 方法,給按鈕單獨添加監聽是比較麻煩的,所以我們可以使用 BaseAdapter 再給按鈕添加監聽,這里給 Item 列表項添加監聽。添加的方法和使用 ArrayAdapter 一樣。所以這里就不展開了。
但是需要注意的是,由於 列表項中添加的 Button 按鈕,按鈕組件會獲取焦點,所以需要設置 button 的 focusable 為 false。可以直接設置 button 的屬性,也可以用java 方法進行設置。例如上面共參考的 item 布局文件就已經設置了。
3-3 基於 BaseAdapter 使用 ListView
添加 ListView 組件,存放數據,設置列表項的布局文件都和 SimpleAdapter 中的操作相同
d. 創建一個 Adapter 繼承 BaseAdapter,並實現抽象方法。
BaseAdapter 有 4 個抽象方法
int getCount(); //返回的是數據源對象的個數,即列表項數 Object getItem(int var1); //返回指定位置position上的列表項 long getItemId(int var1); //返回指定位置處的行ID View getView(int var1, View var2, ViewGroup var3); //返回列表項對應的視圖
繼承 BaseAdapter 時需要去實現這 4 個抽象方法,這幾個抽象方法都是 Adapter 接口中定義的方法。
以理解為adapter先由getCount確定數量,然后循環執行getView()方法將條目一個一個繪制出來。
必須重寫的方法是getCount和getView方法。
前三個方法基本不需要過多修改,
public int getCount() { return mData.size(); } public Object getItem(int position) { return mData.get(position); } public long getItemId(int position) { return position; }
由於新建了一個 java 文件,所以建議把 上下文 context,和 存儲數據的列表 list 傳過來
public class BabyAdapter extends BaseAdapter { Context context; List<HashMap<String,Object>> list ; public BabyAdapter(Context context,List list){ super(); this.context = context; this.list = list; } @Override public int getCount() { return list.size(); } @Override public Object getItem(int i) { return list.get(i); } @Override public long getItemId(int i) { return i; } @Override public View getView(final int i, View convertView, ViewGroup viewGroup) { /** 在不重寫getView 方法的情況下,每次調用 getView 時都要重新實例化子項 item 的布局文件,然后通過 findViewById 重新尋找 View 組件並繪畫 着會導致兩個問題,1. 重復加載布局文件;2. 重復多次尋找 View 組件 解決:使用 convertView 作為 View 緩存,形成 adapter 的itemView 重用機制,減少重繪 view 的次數 形參 view 中緩存了itemView 的布局文件
*/ View view; ViewHolder holder; if(convertView == null){
// LayoutInflater 用於加載布局的系統服務,實例化與Layout XML文件對應的View對象
// 不能直接使用, 需要通過getLayoutInflater( )方法或getSystemService( )方法來獲得與當前Context綁定的 LayoutInflater實例。
LayoutInflater factory = LayoutInflater.from(context);
// reSource:View 的layout 的ID
// root: 生成的view 對象的父控件。若提供了 root(!null),則返回 root 作為根結點,否則,返回 view 對象的根布局作為根布局,
view = factory.inflate(R.layout.listview_item,null); // 2. 使用 ViewHolder 實現 View 組件的緩存 // 重用 View 時就不用通過 findViewById 重新尋找 view 組件,同時減少 view 組件重繪的次數 holder = new ViewHolder(); holder.image =(ImageView)view.findViewById(R.id.image); holder.title = (TextView)view.findViewById(R.id.title); holder.info = (TextView)view.findViewById(R.id.info); holder.button = (Button)view.findViewById(R.id.button); view.setTag(holder); }else{ view = convertView; holder = (ViewHolder)view.getTag(); } // 2. 使用 ViewHolder 實現 View 組件的緩存 // 重用 View 時就不用通過 findViewById 重新尋找 view 組件,同時減少 view 組件重繪的次數 HashMap<String,Object> map = list.get(i); holder.image.setImageResource((int)map.get("image")); holder.title.setText((String)map.get("title")); holder.info.setText((String)map.get("info")); // 給按鈕添加監聽 holder.button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { new AlertDialog.Builder(context) .setTitle(list.get(i).get("title").toString()) .setMessage(list.get(i).get("info").toString()) .setPositiveButton("確定", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { } }).show(); } }); return view; } class ViewHolder{ ImageView image; TextView title ; TextView info; Button button; } }
其中,converView 指 列表項視圖,當該 item 從未再屏幕上出現過時,convertView 為空,一旦出現過,convertView 中就緩存了View 對象,下一次就可以直接取得 列表項中視圖對象使用而不用再加載。
三、ListView 的子項 Item 緩存原理
1. 假設:屏幕只能顯示5個Item,那么ListView只會創建(5+1)個Item的視圖;當第1個Item完全離開屏幕后才會回收至緩存從而復用(用於顯示第6個Item)
2. 假設:屏幕只能顯示5個Item,那么ListView只會創建(5+1)個Item的視圖;當第1個Item完全離開屏幕后才會回收至緩存從而復用(用於顯示第7個Item)