一、適用場景
ListViewListview是一個很重要的組件,它以列表的形式根據數據的長自適應展示具體內容,用戶可以自由的定義listview每一列的布局;由于屏幕尺寸的限制,不能一次性展現所有條目,用戶需要上下滾動查看所有條目。滾出顯示區域的條目將被回收并在下一個條目可見時復用。(下面是一張很經典的圖片)
可以看到,在這個屏幕中可以顯示7條Item。當Item 1滑出屏幕外之后,就會進入到一個緩沖區(Recycler)中以便新的條目可見時(屏幕底部又滑出了新的Item)進行復用。
??一個ListView通常有兩個職責,一是將數據填充到特定布局,二是處理用戶的選擇點擊事件;一個ListView的創建需要創建3個元素,(1)ListView中的每一列的View布局,(2)填入View數據或者圖片,(3)連接View與ListView的適配器,下面我們就來具體聊聊ListView中的那些事。
二、適配器
什么是適配器?適配器(Adapter)是一個連接數據與AdapterView(ListView就是一個典型的AdapterView)的橋梁,實現數據與AdapterView的分離設置,使的AdapterView與數據的綁定更加方便,下面是Android提供的幾個常見的Adapter。
ArrayAdapter<T> 用來綁定一個數組,支持泛型操作
SimpleAdapter 用來綁定在xml中定義的控件對應的數據
BaseAdapter 通用的基礎適配器
1.ArrayAdapter
我們先來看看他的繼承結構:
java.lang.Object
? android.widget.BaseAdapter
? android.widget.ArrayAdapter<T>
可以看到ArrayAdapter是繼承自BaseAdapter這個大boss的,谷歌官網中對其的說明為:
A concrete BaseAdapter that is backed by an array of arbitrary objects. By default this class expects
that the provided resource id references a single TextView. If you want to use a more complex layout, use
the constructors that also takes a field id. That field id should reference a TextView in the larger
layout resource.
一種可以被任意對象填充的實類BaseAdapter。默認的,這個類期望提供的資源ID指向一個單一的TextView,如果你想用一個
更加復雜layout,用這個構造器也只是持有一個ID指向更大的Layout資源中的一個TextView。
也就是說,ArrayAdapter(數組適配器)一般用于顯示一行文本信息,因此更多的指向的是一個簡單的TextView。我們比較常見的構造方法是:
ArrayAdapter (Context context, int resource, List<T> objects)
context Context: The current context.
resource int: The resource ID for a layout file containing a TextView to use when instantiating views.
objects List: The objects to represent in the ListView.
從官方對該方法的說明中可以看出,resource是“一個包含TextView的Layout”,實際上我們在使用的時候更多的直接就是一個TextView,
更加復雜的布局直接繼承BaseAdapter(這個后面會講)
objects是我們早ListView中藥呈現的東西,這里可以傳入泛型類,那么就可以傳入很多我們自定義的Bean數據,非常的方便(具體看下邊實例)
我們先看一段網上的代碼,非常的簡單:
public class ArrayListActivity extends Activity {
private ListView listView;
private String[] adapterData;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.array_list_layout);
listView = (ListView) findViewById(R.id.array_list);
/* 我們要在listView上面顯示的數據,放到一個數組中 */
adapterData = new String[] { "Afghanistan", "Albania", "Algeria",
"American Samoa", "Andorra", "Angola", "Anguilla",
"Antarctica", "Antigua and Barbuda", "Argentina", "Armenia",
"Aruba", "Australia", "Austria", "Azerbaijan", "Bahrain",
"Bangladesh", "Barbados", "Belarus", "Belgium", "Belize",
"Benin", "Bermuda", "Bhutan", "Bolivia",
"Bosnia and Herzegovina", "Botswana", "Bouvet Island" };
/* 下面就是對適配器進行配置了 */
ArrayAdapter<String> arrayAdapter = new ArrayAdapter<String>( ArrayListActivity.this, android.R.layout.simple_list_item_1, adapterData);
/* 設置ListView的Adapter */
listView.setAdapter(arrayAdapter);
}
}
這應該是我們在開發中遇到的最簡單的ListView使用了,這里我們用到的適配器是ArrayAdapter<String>( ArrayListActivity.this, android.R.layout.simple_list_item_1, adapterData);
由于泛型中傳入的是String類型,因此后面我們傳入的數據(adapterData)是一個String類型的數組,這個要對的上號。
另外android.R.layout.simple_list_item_1是一個Android SDK中自帶的布局,非常簡單,就是一行TextVeiew文本。
上述例子中我們只是簡單的呈現了一個靜態數組,下面我們可以對這塊代碼加以修改,以動態的添自定義的數組:
首先我們自定義一個Bean類:
public class Restaurant {
private String name="";
private String address="";
public String getName() {
return(name);
}
public void setName(String name) {
this.name=name;
}
public String getAddress() {
return(address);
}
public void setAddress(String address) {
this.address=address;
}
@Override
public String toString() {
return("名稱:"+getName()+";"+" 地址:"+getAddress());
}
}
然后我們對上面一段代碼中ArrayAdapter<String> arrayAdapter = new ArrayAdapter<String>(
ArrayListActivity.this, android.R.layout.simple_list_item_1, adapterData);
做出如下修改:
ArrayAdapter<Restaurant> arrayAdapter = new ArrayAdapter<>(this,android.R.layout.simple_list_item_1,restaurantList);
public class MainActivity extends AppCompatActivity implements View.OnClickListener{
private Button save_btn;
private EditText name_edt;
private EditText adress_edt;
private ListView listView;
private List<Restaurant> restaurantList;
private ArrayAdapter<Restaurant> arrayAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
}
public void initView(){
name_edt = (EditText) findViewById(R.id.name_edt);
adress_edt = (EditText) findViewById(R.id.adress_edt);
listView = (ListView)findViewById(R.id.list);
save_btn = (Button)findViewById(R.id.save_btn);
save_btn.setOnClickListener(this);
restaurantList = new ArrayList<>();
arrayAdapter = new ArrayAdapter<>(this,android.R.layout.simple_list_item_1,restaurantList);
listView.setAdapter(arrayAdapter);
}
@Override
public void onClick(View v) {
switch(v.getId()){
case R.id.save_btn:
Restaurant restaurantData = new Restaurant();
restaurantData.setName(name_edt.getText().toString());
restaurantData.setAddress(adress_edt.getText().toString());
arrayAdapter.add(restaurantData); //每個增加的條目都會添加到適配器里面
break;
}
}
}
布局文件如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.example.dell.arrayadpaterdome.MainActivity">
<ListView
android:layout_width="match_parent"
android:layout_height="300dp"
android:id="@+id/list">
</ListView>
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/name_edt"/>
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/adress_edt"/>
<Button
android:text="添加"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/save_btn" />
</LinearLayout>
我們在xml布局中定義了兩個EditText,用于獲取我們輸入的數據,分別是飯店的名字和地址。這里要注意的是adapter.add(r)這個方法,
類似于ArrayList(動態數組)中的.add()方法該方法用于在ArrayAdapter中動態的添加一條條數據,
下面我們來仔細分析一下上面的代碼:我們定義一個實體類來存放我們所需要讀取的數據,并在MainActivity中定義了一個動態數組List<Restaurant> restaurantList = new ArrayList<>();表示restaurantList這個數組中只能存放實體類Restaurant的對象,這里需要注意的是我們在寫實體類的時候一定要在最后復寫toString()方法并在其中return我們需要返回的數據(原因待會講)。
??之后我們定義了一個ArrayAdapter<Restaurant> arrayAdapter = new ArrayAdapter<>(this,android.R.layout.simple_list_item_1,restaurantList);
其中泛型表明這個arrayAdapter中的數據只能是Restaurant這個實體類的對象,這也就是為什么我們定義restaurantList的原因。(this,android.R.layout.simple_list_item_1,restaurantList)這三個參數分別表示:this——當前的上下文(可以暫時粗略的理解為當前類) ; android.R.layout.simple_list_item_1——Android sdk中提供的一個族簡單的用于適配ArrayAdapter的布局,就是一個TextView,我們可以點開源碼看一下(本段末);最后一個參數restaurantList就是我們需要在ListView中呈現的數據內容。
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2006 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@android:id/text1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceListItemSmall"
android:gravity="center_vertical"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:minHeight="?android:attr/listPreferredItemHeightSmall" />
這里要著重說的是最后一個參數,注意他返回的是Restaurant實體類的對象,對象,對象!??!那么我們不可能把一個對象展現在ListView中吧?那ListView中展示的是什么呢?我們點開arrayAdapter = new ArrayAdapter<>(this,android.R.layout.simple_list_item_1,restaurantList);的源碼,可以看到:
@Override
public @NonNull View getView(int position, @Nullable View convertView,
@NonNull ViewGroup parent) {
return createViewFromResource(mInflater, position, convertView, parent, mResource);
}
private @NonNull View createViewFromResource(@NonNull LayoutInflater inflater, int position,
@Nullable View convertView, @NonNull ViewGroup parent, int resource) {
final View view;
final TextView text;
if (convertView == null) {
view = inflater.inflate(resource, parent, false);
} else {
view = convertView;
}
try {
if (mFieldId == 0) {
// If no custom field is assigned, assume the whole resource is a TextView
text = (TextView) view;
} else {
// Otherwise, find the TextView field within the layout
text = (TextView) view.findViewById(mFieldId);
if (text == null) {
throw new RuntimeException("Failed to find view with ID "
+ mContext.getResources().getResourceName(mFieldId)
+ " in item layout");
}
}
} catch (ClassCastException e) {
Log.e("ArrayAdapter", "You must supply a resource ID for a TextView");
throw new IllegalStateException(
"ArrayAdapter requires the resource ID to be a TextView", e);
}
final T item = getItem(position);
if (item instanceof CharSequence) {
text.setText((CharSequence) item);
} else {
text.setText(item.toString());
}
return view;
}
由于ArrayAdapter是繼承自BaseAdapter的,所以他會復寫getView(int position, @Nullable View convertView,@NonNull ViewGroup parent)方法,該方法最終return view;這個view就是我們每一行要呈現的東西。我們重點看下面這幾句:
final T item = getItem(position);
if (item instanceof CharSequence) {
text.setText((CharSequence) item);
} else {
text.setText(item.toString());
}
return view;
這里T就是我們自定義的Restaurant實體類,可以看出,在這里他做了一個判斷,判斷這個實體類的對象是不是CharSequence類的實例,如果是的話,就直接返回;如果不是的話,就調用對象的toString()方法再返回(這就是為什么我們一定要在實體類中復寫toString()方法返回我們想要的數據了)隨便輸入幾個名稱地址可以看到結果了:
可能有些童鞋比較喜歡搞事情,如果我不復寫toString()方法會怎么樣呢?我們可以試試,注釋掉實體類中的toString方法,同樣的輸入內容,結果如下:
花擦?com.example.dell.arraydapterdome.Restaurant@20659989?這是什么鬼?原來,在JAVA中,所有的類都繼承自Object類,如果我們沒有復寫toString()這個方法,那么text.setText(item.toString())這句代碼就會強行調用Object類的toString()方法,這是返回一個由類名(對象是該類的一個實例)、at 標記符“@”和此對象哈希碼的無符號十六進制表示組成的字符串,換句話說,此時該方法返回一個字符串,他由:getClass().getName() + '@' + Integer.toHexString(hashCode())
組成,而仔細看上面那句代碼:com.example.dell.arraydapterdome.Restaurant就是我們實體類的包名+類名,后面緊跟著@符號以及一個16進制的哈希數值。
2.SimpleAdapter
java.lang.Object
? android.widget.BaseAdapter
? android.widget.SimpleAdapter
同樣,我們可以在繼承結構中看到他是BaseAdapter的子類。
An easy adapter to map static data to views defined in an XML file. You can specify the data backing
the list as an ArrayList of Maps. Each entry in the ArrayList corresponds to one row in the list. The Maps
contain the data for each row. You also specify an XML file that defines the views used to display the row,
and a mapping from keys in the Map to specific views. Binding data to views occurs in two phases. First, if
a SimpleAdapter.ViewBinder is available, setViewValue(android.view.View, Object, String) is invoked. If the
returned value is true, binding has occurred. If the returned value is false, the following views are then
tried in order:
*A view that implements Checkable (e.g. CheckBox). The expected bind value is a boolean.
*TextView. The expected bind value is a string and setViewText(TextView, String) is invoked.
*ImageView. The expected bind value is a resource id or a string and setViewImage(ImageView, int) or
setViewImage(ImageView, String) is invoked.
If no appropriate binding can be found, an IllegalStateException is thrown.
翻譯一下就是:
一個將靜態數據映射到xml文件中定義好的視圖中的簡單適配器。你可以指定由Map組成的List類型(如ArrayList)的數據。
在ArrayList中每個條目對應List中的一行。Map包含了每一行的數據,你也可以指定一個xml文件用于展示每一行的視圖,并且
根據Map的key映射值到指定的視圖.綁定數據到視圖分兩個階段,首先,如果設置了SimpleAdapter.ViewBinder,那么這個設置
的ViewBinder的setViewValue(android.view.View, Object, String)將被調用。如果setViewValue的返回值是true,則表示
綁定已經完成,將不再調用系統默認的綁定實現。如果返回值為false,視圖將按以下順序綁定數據:
*如果View實現了Checkable(例如CheckBox),期望綁定值是一個布爾類型。
*TextView.期望綁定值是一個字符串類型,通過調用setViewText(TextView, String)綁定。
*ImageView,期望綁定值是一個資源id或者一個字符串,通過調用setViewImage(ImageView, int) 或 setViewImage(ImageView, String)綁定數據。
如果沒有一個合適的綁定發生將會拋出IllegalStateException。
接下來我們看看他的構造函數:
SimpleAdapter(Context context, List<? extends Map<String, ?>> data, int resource, String[] from, int[] to)
context Context: The context where the View associated with this SimpleAdapter is running
data List: A List of Maps. Each entry in the List corresponds to one row in the list. The Maps contain the data for each row, and should include all the entries specified in "from"
resource int: Resource identifier of a view layout that defines the views for this list item. The layout file should include at least those named views defined in "to"
from String: A list of column names that will be added to the Map associated with each item.
to int: 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.
翻譯一下就是:
context 當前上下文
data 一個Map組成的List集合。在列表中的每個條目對應列表中的一行,每一個Map包含了每一行的數據,并且應該包含所有在from中指定的鍵(key)。
resource int類型值,用于指定列表項Item(每一行)布局文件資源,布局文件應該至少包含那些在to中定義了的View。(實際上這個就是那個Layout的Id值).
from 一個將被添加到Map映射上的鍵名(key值)
to 將綁定數據的視圖的ID,跟from參數對應,這些應該全是TextView
關于這個SimpleAdapter由于用的時候有諸多的限制,所以平時用的機會并不多。因此在此不做過多的說明,僅僅給出一個最簡單的例子,配合上面文旦的說明大家一看就明白了,直接上代碼:
public class SimpleAdapterActivity extends AppCompatActivity {
private ListView listView;
private SimpleAdapter adapter;
private String [] myicon = new String[]{"圖標1","圖標2","圖標3","圖標4","圖標5"};
private String [] myicon_tx = new String[]{"圖1","圖2","圖3","圖4","圖5"};
private String [] mynum = new String[]{"1","2","3","4","5"};
private int[] myPic = {R.mipmap.ic_launcher,
R.mipmap.ic_launcher,
R.mipmap.ic_launcher,
R.mipmap.ic_launcher,
R.mipmap.ic_launcher};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.simple_adapter_layout);
listView = (ListView)findViewById(R.id.listView);
ArrayList <HashMap<String,Object>> arrayList = new ArrayList<>();
for(int i=0;i<myicon.length;i++){
HashMap<String,Object> item = new HashMap<>();
item.put("pic",myPic[i]);
item.put("icon",myicon[i]);
item.put("icon_tx",myicon_tx[i]);
item.put("num","——序號為:"+mynum[i]);
arrayList.add(item);
}
//重點來了
adapter = new SimpleAdapter(this,
arrayList,
R.layout.simple_item_layout,
new String[]{"pic","icon","icon_tx","num"},
new int[]{R.id.imageView1,R.id.textView1,R.id.textView2,R.id.textView3}
);
listView.setAdapter(adapter);
}
}
布局文件(這里我們定義了兩個布局文件):
simple_adapter_layout:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/listView"/>
</LinearLayout>
simple_item_layout:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/textView1"
android:textSize="20dp"
android:text="text1"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/imageView1"
android:src="@mipmap/ic_launcher"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/textView2"
android:textSize="20dp"
android:text="text2"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/textView3"
android:textSize="20dp"
android:text="text3"/>
</LinearLayout>
</LinearLayout>
解釋一下,這里兩個布局文件,一個是主界面的布局(simple_adapter_layout),這里我們只添加了一個ListView。而另一個布局simple_item_layout則是每一條Item的布局,從這里可以看出SimpleAdapter多了很多自由的空間,我們可以有限制的自定義每一條Item的布局。為什么說他是有限制的呢?因為注意看上面的代碼就會發現,最關鍵的一段代碼中:
adapter = new SimpleAdapter(this,
arrayList,
R.layout.simple_item_layout,
new String[]{"pic","icon","icon_tx","num"},
new int[]{R.id.imageView1,R.id.textView1,R.id.textView2,R.id.textView3}
);
我們仍然需要在構建SimpleAdapter時注意形式,new String[]和new int兩個數組長度需保持一致(即每一個數據和Item的View中的控件Id數量保持一致,并在內容上意義對應), new String[]{"pic","icon","icon_tx","num"},代表的是上面HashMap<String,Object> item = new HashMap<>();中item的鍵(key)值,而每一個鍵又對應最上面的一組數組值(String [] myicon,String [] myicon_tx,String [] mynum,int[] myPic),這里的數值對應比較繞,各位同學看的時候要注意點,當然,這里只是舉了SimpleAdapter最簡單的例子,我們還可以定義更為復雜的ItemView布局,但是那樣沒有意義,因為此時為了更加靈活的自定義ItemVeiw,我們還是用到下一節的最終大Boss——BaseAdapter!??!
最終效果:
3、BaseAdapter
我們使用ListView,最終都然不開BaseAdapter,那么我們來看看BaseAdapter使用的一般步驟:
1.定義主界面(包含ListView)的xml布局
2.根據需要定義ListView每行(ItemView)所實現的xml布局
3.定義一個Adapter類繼承BaseAdapter,重寫里面的方法。
4.定義一個HashMap構成的列表,將數據以鍵值對的方式存放在里面。
5.構造一個繼承自BaseAdpater的Adpater對象,設置適配器。
6.將Adapter綁定到上ListView上(通過setAdpater方法)。
第一步:定義一個BaseAdpater,并重寫里邊的方法(主要是重寫下邊的四個方法),下面是一般寫法:
class MyAdapter extends BaseAdapter {
private LayoutInflater inflater;//得到一個LayoutInfalter對象用來導入布局
private ArrayList<HashMap<String,String>> contentList;
private Context context;
//構造函數,傳入的context是ListView所在界面的上下文
public MyAdapter(Context context,ArrayList<HashMap<String, Object>> contentList) {
this.context = context;
this.contentList = contentList;
inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
@Override
public int getCount() {
return contentList.size();
}//這個方法返回了在適配器中所代表的數據集合的條目數
@Override
public Object getItem(int position) {
return contentList.get(position);
}//這個方法返回了數據集合中與指定索引position對應的數據項
@Override
public long getItemId(int position) {
return position;
}//這個方法返回了在列表中與指定索引對應的行id
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View view = inflater.inflate(R.layout.itemlayout,null);
//很多時候我們在這里設置一個null就OK了,好像也沒有什么問題,但是這里邊大有學問,待會著重要講.
return view;//返回的就是我們要呈現的ItemView,即每一條Item的布局.
}
}
在此我們有必要了解一下系統繪制ListView的原理:
??當系統開始繪制ListView的時候,首先調用getCount()方法。得到它的返回值,即ListView的長度,根據這個長度,系統調用getView()方法,根據這個長度逐一繪制ListView的每一行。(如果讓getCount()返回1,那么只顯示一行)。
??getItem()和getItemId()則在需要處理和取得Adapter中的數據時調用。
??那么getView()如何使用呢?如果有10000行數據,就繪制10000次?這肯定會極大的消耗資源,導致ListView滑動非常的慢,那應該怎么做呢?可以使用BaseAdapter進行優化ListView的顯示。
??以下將使用3種重寫方法來說明getView()的使用:
第一種重寫getView()的方法:
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View itemView = mInflater.inflate(R.layout.item,null); //這里姑且用null
ImageView img = (ImageView)item.findViewById(R.id.ItemImage);
TextView title = (TextView)item.findViewById(R.id.ItemTitle);
TextView test = (TextView)item.findViewById(R.id.ItemText);
Button btn = (Button) item.findViewById(R.id.ItemBottom);
img.setImageResource((Integer) listItem.get(position).get("ItemImage"));
title.setText((String) listItem.get(position).get("ItemTitle"));
test.setText((String) listItem.get(position).get("ItemText"));
return itemView;
}
這個方法返回了指定索引對應的數據項的視圖,但是這種方法每次getView()都要findViewById和重新繪制一個View,當列表項數據量很大的時候會嚴重影響性能,造成下拉很慢,所以數據量大的時候不推薦用這種方式。
第二種重寫getView()的方法,使用convertView作為緩存進行優化:
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if(convertView == null){
convertView = mInflater.inflate(R.layout.item, null);
}//檢測有沒有可以重用的View,沒有就重新繪制
ImageView img = (ImageView)convertView.findViewById(R.id.ItemImage);
TextView title = (TextView)convertView.findViewById(R.id.ItemTitle);
TextView test = (TextView)convertView.findViewById(R.id.ItemText);
Button btn = (Button) convertView.findViewById(R.id.ItemBottom);
img.setImageResource((Integer) listItem.get(position).get("ItemImage"));
title.setText((String) listItem.get(position).get("ItemTitle"));
test.setText((String) listItem.get(position).get("ItemText"));
return convertView;
}
在方法getView(int position, View convertView, ViewGroup parent)中,第二個參數convertView的含義:是代表系統最近回收的View。若整屏能顯示9個Item,第一次打開帶ListView的控件時,因為并沒有回收的View,調用getView時,參數convertView的值會為null;當我們滑動ListView時,由于有ItemView劃出了屏幕而被回收,此時convertView將不是null,而是最近回收的View(剛劃出屏幕的那個Item的View)的引用。這里順便說一下,第三個參數parent是我們的ItemView的父布局ListView的引用(The parent that this view will eventually be attached to),這個待會要用到。
這里借用網上的一張圖來說明:假如我們的ListView狀態如圖,此時系統繪制的只有position:4到positon12這9個Item.若按箭頭方法滑動,即將回收position12,以及繪制position3(position4已經露出來了,也就是已經觸發了繪制)。
??Android系統繪制Item的View和回收Item的View時有個規則:該Item只要顯示出一點點就觸發繪制,但必須等該Item完全隱藏之后才觸發回收。
??而接下來將顯示position=3的Item(注意此時position=4已經漏了出來,也就是說已經觸發了繪制),系統調用getView方法時,第二個參數convertView的值將是position=12的View的引用(最近回收的一個Item的View,當position=3的Item漏出來的時候position=12恰好剛剛完全回收)。
第三種重寫getView()的方法,通過convertView+ViewHolder來實現緩存進而進行優化:
convertView緩存了View,ViewHolder相當于更加具體的緩存:View里的組件,即把View和View的組件一并進行緩存,那么重用View的時候就不用再重繪View和View的組件(findViewById)。這種方法就既減少了重繪View,又減少了findViewById的次數,所以這種方法是最能節省資源的,所以非常推薦大家使用通過convertView+ViewHolder來重寫getView()。
static class ViewHolder{
public ImageView img;
public TextView title;
public TextView text;
public Button btn;
}//聲明一個外部靜態類
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder ;
if(convertView == null){
holder = new ViewHolder();
convertView = mInflater.inflate(R.layout.item, null);
holder.img = (ImageView)convertView.findViewById(R.id.ItemImage);
holder.title = (TextView)convertView.findViewById(R.id.ItemTitle);
holder.text = (TextView)convertView.findViewById(R.id.ItemText);
holder.btn = (Button) convertView.findViewById(R.id.ItemBottom);
convertView.setTag(holder);
}else {
holder = (ViewHolder)convertView.getTag();
}
holder.img.setImageResource((Integer) listItem.get(position).get("ItemImage"));
holder.title.setText((String) listItem.get(position).get("ItemTitle"));
holder.text.setText((String) listItem.get(position).get("ItemText"));
return convertView;
}//這個方法返回了指定索引對應的數據項的視圖
view的setTag和getTag方法其實很簡單,在實際編寫代碼的時候一個view不僅僅是為了顯示一些字符串、圖片,有時我們還需要他們攜帶一些其他的數據以便我們對該view的識別或者其他操作。于是android 的設計者們就創造了setTag(Object)方法來存放一些數據和view綁定,我們可以理解為這個是view 的一個唯一標示,也可以理解為view 作為一個容器存放了一些數據。而這些數據我們也可以通過getTag() 方法來取出來。這里我們可以看看這兩個方法的源碼:
/**
* Returns this view's tag.
*
* @return the Object stored in this view as a tag, or {@code null} if not
* set
*
* @see #setTag(Object)
* @see #getTag(int)
*/
@ViewDebug.ExportedProperty
public Object getTag() {
return mTag;
}
/**
* Sets the tag associated with this view. A tag can be used to mark
* a view in its hierarchy and does not have to be unique within the
* hierarchy. Tags can also be used to store data within a view without
* resorting to another data structure.
*
* @param tag an Object to tag the view with
*
* @see #getTag()
* @see #setTag(int, Object)
*/
public void setTag(final Object tag) {
mTag = tag;
}
可以看出,我們通過setTag()設置的東西,到最后又通過getTag()原封不動的取了出來。具體到上面第三種復寫getView()方法的代碼中,我們在首次創建頁面時(此時還沒有滑動,convertview為空),通過holder.img = (ImageView)convertView.findViewById(R.id.ItemImage); holder.title = (TextView)convertView.findViewById(R.id.ItemTitle); holder.text = (TextView)convertView.findViewById(R.id.ItemText); holder.btn = (Button) convertView.findViewById(R.id.ItemBottom);
將ViewHolder中的四個對象與Item的View中控件一一綁定,然后將這個已經持有ItemView中四個控件引用的holder對象通過setTag()方法設置給convertView,然后當我們滑動ListView的時候,此時會出現ItemView的復用,convertView不為空,這個時候我們就通過getTag()方法把上面已與那四個控件綁定好的holder取出來,并給這四個控件設置相應的數據內容,這樣就巧妙的避免了每次出現新的ItemView都會頻繁的findViewById。
??OK,講了這么多了,我們按照第三種方法完整的實現一遍:
(注:由于這個實現起來比較簡單,這里直接復用了http://www.lxweimin.com/p/4e8e4fd13cf7這篇博客中的代碼)
1.定義主xml的布局
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
android:background="#FFFFFF"
android:orientation="vertical" >
<ListView
android:id="@+id/listView1"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
2.根據需要,定義ListView每行所實現的xml布局(item布局)
item.xml:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:layout_alignParentRight="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/ItemImage"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="按鈕"
android:id="@+id/ItemBottom"
android:focusable="false"
android:layout_toLeftOf="@+id/ItemImage" />
<TextView android:id="@+id/ItemTitle"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:textSize="20sp"/>
<TextView android:id="@+id/ItemText"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:layout_below="@+id/ItemTitle"/>
</RelativeLayout>
3.定義一個Adapter類繼承BaseAdapter,重寫里面的方法。
(利用convertView+ViewHolder來重寫getView())
package com.example.dell.listviewtest;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.HashMap;
/**
* Created by dell on 2016/10/18.
*/
class MyAdapter extends BaseAdapter {
private LayoutInflater mInflater;//得到一個LayoutInfalter對象用來導入布局
private ArrayList<HashMap<String, Object>> listItem;
public MyAdapter(Context context, ArrayList<HashMap<String, Object>> listItem) {
this.mInflater = LayoutInflater.from(context);
this.listItem = listItem;
}//聲明構造函數
@Override
public int getCount() {
return listItem.size();
}//這個方法返回了在適配器中所代表的數據集合的條目數
@Override
public Object getItem(int position) {
return listItem.get(position);
}//這個方法返回了數據集合中與指定索引position對應的數據項
@Override
public long getItemId(int position) {
return position;
}//這個方法返回了在列表中與指定索引對應的行id
//利用convertView+ViewHolder來重寫getView()
private static class ViewHolder {
ImageView img;
TextView title;
TextView text;
Button btn;
}//聲明一個外部靜態類
@Override
public View getView(final int position, View convertView, final ViewGroup parent) {
ViewHolder holder ;
if(convertView == null) {
holder = new ViewHolder();
convertView = mInflater.inflate(R.layout.item, null);
holder.img = (ImageView)convertView.findViewById(R.id.ItemImage);
holder.title = (TextView)convertView.findViewById(R.id.ItemTitle);
holder.text = (TextView)convertView.findViewById(R.id.ItemText);
holder.btn = (Button) convertView.findViewById(R.id.ItemBottom);
convertView.setTag(holder);
} else {
holder = (ViewHolder)convertView.getTag();
}
holder.img.setImageResource((Integer) listItem.get(position).get("ItemImage"));
holder.title.setText((String) listItem.get(position).get("ItemTitle"));
holder.text.setText((String) listItem.get(position).get("ItemText"));
holder.btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
System.out.println("你點擊了選項"+position);//bottom會覆蓋item的焦點,所以要在xml里面配置android:focusable="false"
}
});
return convertView;
}//這個方法返回了指定索引對應的數據項的視圖
}
4.在MainActivity里:
定義一個HashMap構成的列表,將數據以鍵值對的方式存放在里面。
構造Adapter對象,設置適配器。
將LsitView綁定到Adapter上。
MainActivity.java
package com.example.dell.listviewtest;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;
import java.util.ArrayList;
import java.util.HashMap;
public class MainActivity extends AppCompatActivity {
private ListView listview;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
listview = (ListView) findViewById(R.id.listView1);
/*定義一個以HashMap為內容的動態數組*/
ArrayList<HashMap<String, Object>> listItem = new ArrayList<HashMap<String, Object>>();/*在數組中存放數據*/
for (int i = 0; i < 100; i++) {
HashMap<String, Object> map = new HashMap<String, Object>();
map.put("ItemImage", R.mipmap.ic_launcher);//加入圖片
map.put("ItemTitle", "第" + i + "行");
map.put("ItemText", "這是第" + i + "行");
listItem.add(map);
}
MyAdapter adapter = new MyAdapter(this, listItem);
listview.setAdapter(adapter);//為ListView綁定適配器
listview.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> arg0, View arg1, int arg2, long arg3) {
System.out.println("你點擊了第" + arg2 + "行");//設置系統輸出點擊的行
}
});
}
}
實現效果如下:
三、ListView常見問題
1、LayoutInflater.inflate中xml根元素的布局參數不起作用的問題
一般的ListView教程講到上面第三點就沒有然后了,因為在用的過程中以上面講的知識點也夠用了。但是對于ListView送一些問題你真正有了解過嗎?下面我們來深入討論ListView使用過程中的一些常見問題。
??由于我們很容易公式化預設的低嗎,所以有時會忽略優雅的細節。在上面的例子中我們經常能看到View view = inflater.inflate(R.layout.itemlayout,null);
這句代碼,并且一開始我們也強調,這里姑且用null。一般情況下我們都感覺用null會后沒有什么影響,但是你真的知道null是什么意思嗎?
??要說到這個問題,我們不得不從LayoutInflater說起。我們一般用LayoutInflater比較多情況就是:
①.ListView的Adapter的getView()方法中基本都會出現,使用inflate方法去加載一個布局,用于ListView的每個Item的布局
private LayoutInflater inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
@Override
public View getView(int position, View convertView, ViewGroup viewGroup) {
if(convertView == null){
convertView = inflater.inflate(R.layout.itemlayout,viewGroup,false);
}
}
return convertView;
②.在Fragment的onCreateView()中,使用inflate方法去加載一個布局,用于ViewPager的每一頁的的布局
@Override
public View onCreateView(LayoutInflater inflater,final ViewGroup container, Bundle savedInstanceState) {
LayoutView = inflater.inflate(R.layout.fragmentlayout,container,false);
...
return LayoutView;
}
Ok,在這里我們先說ListView中的情況。其實
View view = inflater.inflate(R.layout.itemlayout,null);
這句代碼在Androdid項目中幾乎無處不在,包括郭霖郭大神的《第一行代碼》中在講解相關的內容時也是這么用的。然而這個時候IDE會給出警告:
Avoid passing null as the view root (needed to resolve layout parameters on the inflated layout's root element).
When inflating a layout, avoid passing in null as the parent view, since otherwise any layout parameters on the root of the inflated layout will be ignored.
這段警告的意思說的很明確了,如果第二個參數傳入null,那么ItemView(在上面是R.layout.itemlayout)的根布局Layout(也就是最外層的布局,如整個頁面是包含在一個LinearLayout中,那么根布局就是他)的所有屬性包括layout_width、layout_height、background等等。
??我們可以做一個小小的實驗,就拿上面那個我們復制過來的BaseAdpater的代碼來說事。我們原本這里ItemView的布局是這樣的:
item.xml:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:layout_alignParentRight="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/ItemImage"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="按鈕"
android:id="@+id/ItemBottom"
android:focusable="false"
android:layout_toLeftOf="@+id/ItemImage" />
<TextView android:id="@+id/ItemTitle"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:textSize="20sp"/>
<TextView android:id="@+id/ItemText"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:layout_below="@+id/ItemTitle"/>
</RelativeLayout>
現在我們的需求變了,我們讓每個Item的高度都變為200dp:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="200dp">
但是此時我們getView()方法中inflate()方法如此:
convertView = mInflater.inflate(R.layout.item, null);
運行結果如下:
嗯?ItemView的高度并沒有發生任何變化!??!
如果我們將inflate()方法稍加修改:
convertView = mInflater.inflate(R.layout.item, parent,false);
Ok,這個時候我們看到已經達成了我們想要的結果(雖然有點丑)為什么參數按照這個規則傳遞就不會出問題呢?前面我們也有提到getView(final int position, View convertView, final ViewGroup parent)的第三個參數的意義就是指父布局(也就是整體的ListView的引用),而在convertView = mInflater.inflate(R.layout.item, parent,false);中,R.layout.item表示ItemView,parent表示父布局,false表示這里不將ItemView添加進父布局。而這三個參數的整體意義代表:將ItemView的XML文件的根布局參數(Layout_width,Layout_height等)添加進視圖中。
??這里我們對infalate的幾種參數形式做一說明,首先我們記住一些結論性的東西:
一、首先看帶三個參數的infalte方法:
public View inflate (int resource, ViewGroup root, boolean attachToRoot)
1、如果root不為null,且attachToRoot為TRUE,則會在加載的布局文件的最外層再嵌套一層root布局,這時候xml根元素的布局參數當然會起作用。
2、如果root不為null,且attachToRoot為false,則不會在加載的布局文件的最外層再嵌套一層root布局,這個root只會用于為要加載的xml的根view生成布局參數,這時候xml根元素的布局參數也會起作用?。。?br>
3、如果root為null,則attachToRoot無論為true還是false都沒意義!即xml根元素的布局參數依然不會起作用!
二、再看帶兩個參數的inflate方法:
public View inflate(int resource, ViewGroup root)
1、當root不為null時,相當于上面帶三個參數的inflate方法的第2種情況
2、當root為null時,相當于上面帶三個參數的inflate方法的第3種情況
下面我們從源碼的角度解析這個問題:
/**
* Inflate a new view hierarchy from the specified XML node. Throws
* {@link InflateException} if there is an error.
* <p>
* <em><strong>Important</strong></em> For performance
* reasons, view inflation relies heavily on pre-processing of XML files
* that is done at build time. Therefore, it is not currently possible to
* use LayoutInflater with an XmlPullParser over a plain XML file at runtime.
*
* @param parser XML dom node containing the description of the view
* hierarchy.
* @param root Optional view to be the parent of the generated hierarchy (if
* <em>attachToRoot</em> is true), or else simply an object that
* provides a set of LayoutParams values for root of the returned
* hierarchy (if <em>attachToRoot</em> is false.)
* @param attachToRoot Whether the inflated hierarchy should be attached to
* the root parameter? If false, root is only used to create the
* correct subclass of LayoutParams for the root view in the XML.
* @return The root View of the inflated hierarchy. If root was supplied and
* attachToRoot is true, this is root; otherwise it is the root of
* the inflated XML file.
*/
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
final Context inflaterContext = mContext;
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
View result = root;
try {
// Look for the root node.
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}
if (type != XmlPullParser.START_TAG) {
throw new InflateException(parser.getPositionDescription()
+ ": No start tag found!");
}
final String name = parser.getName();
if (DEBUG) {
System.out.println("**************************");
System.out.println("Creating root view: "
+ name);
System.out.println("**************************");
}
if (TAG_MERGE.equals(name)) {
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// Temp is the root view that was found in the xml
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
if (DEBUG) {
System.out.println("Creating params from root: " +
root);
}
// Create layout params that match root, if supplied
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}
if (DEBUG) {
System.out.println("-----> start inflating children");
}
// Inflate all children under temp against its context.
rInflateChildren(parser, temp, attrs, true);
if (DEBUG) {
System.out.println("-----> done inflating children");
}
// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
if (root != null && attachToRoot) {
root.addView(temp, params);
}
// Decide whether to return the root that was passed in or the
// top view found in xml.
if (root == null || !attachToRoot) {
result = temp;
}
}
} catch (XmlPullParserException e) {
final InflateException ie = new InflateException(e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (Exception e) {
final InflateException ie = new InflateException(parser.getPositionDescription()
+ ": " + e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} finally {
// Don't retain static reference on context.
mConstructorArgs[0] = lastContext;
mConstructorArgs[1] = null;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
return result;
}
}
首先我們來看看View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)這幾個參數的意義:
parser: XML dom node containing the description of the view hierarchy.說白了就是ItemView的視圖ID
root: Optional view to be the parent of the generated hierarchy (if <em>attachToRoot</em> is true), or else simply an object that provides a set of LayoutParams values for root of the returned hierarchy (if <em>attachToRoot</em> is false.)
??如果attachToRoot為true,他是一個可選擇的view,并作為層次視圖(ItemView)的父視圖;如果attachToRoot為false,他僅僅作為一個為返回的層次視圖(ItemView)的根視圖提供一組LayoutParams值的對象。
??嗯,翻譯成人話就是,如果后面的attachToRoot參數為true,那么他就是ItemView的父視圖(ListView)。如果后面的attachToRoot參數為false,他就是一個給ItemView提供提供父視圖LayoutParams值的對象.也就是說,不管是true還是false,都需要ViewGroup(父視圖,第二個參數傳入的東西)的LayoutParams值來正確的測量與放置layout文件(第一個參數,或者ItemView)所產生的View對象。這個不理解的話就先記住他是父視圖(ListView)的引用就行了,待會我們分析源碼。
attachToRoot :Whether the inflated hierarchy should be attached to the root parameter? If false, root is only used to create the correct subclass of LayoutParams for the root view in the XML.
??被填充的層是否應該附在root參數內部?如果是false,root參數只適用于為XML根元素View創建正確的LayoutParams的子類。
??翻譯成人話就是:前面已經說過,不管是true還是false,都需要ViewGroup(父視圖ListView,第二個參數傳入的東西)的LayoutParams值來正確的測量與放置layout文件(第一個參數,或者ItemView)所產生的View對象!??!這個參數true與false的區別在于:如果attachToRoot是true的話,那第一個參數的layout文件就會被填充并附加在第二個參數所指定的ViewGroup內。方法返回兩者結合后的View,根元素是第二個參數ViewGroup;如果是false的話,第一個參數所指定的layout文件會被(自己xml文件中視圖的LayoutParams參數填)充并作為View返回,此時這個View的根元素就是layout文件(ItemView)的根元素。此時這個ItemView會被系統以其他的方式添加進ListView。
接下來我們來看源碼,我們重點看這幾句:
......
// Temp is the root view that was found in the xml
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
if (DEBUG) {
System.out.println("Creating params from root: " +
root);
}
// Create layout params that match root, if supplied
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}
......
// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
if (root != null && attachToRoot) {
root.addView(temp, params);
}
// Decide whether to return the root that was passed in or the
// top view found in xml.
if (root == null || !attachToRoot) {
result = temp;
}
......
首先,final View temp = createViewFromTag(root, name, inflaterContext, attrs);
這句看注釋我們可以知道:Temp is the root view that was found in the xml(temp是我們在xml中發現的根View),當然這句話說得極具迷惑性,實際上這里的temp就是ItemView的視圖,注釋里所說的root view實際上指的是itemView的xml布局里定義的頂層(最外層)view。
??然后在root != null(第二個參數ViewGroup不為null)這個if語句中,我們看這句:params = root.generateLayoutParams(attrs);
然后我們看注釋:Create layout params that match root, if supplied。創建匹配root的params(可理解為布局參數)。需要注意的一點是,這里的root不同于上一句代碼注釋中的root view,他指的就是第二個參數,即ViewGroup root,這句代碼實際上就是在根據子View的xml布局參數生成一個可以匹配父布局(可以暫時直接理解為ListView)的布局參數。上面我們已經強調過兩遍:不管是true還是false,都需要ViewGroup的LayoutParams來正確的測量與放置子layout文件所產生的View對象。
??OK,現在我們接著看:if (!attachToRoot) { temp.setLayoutParams(params); }
當attachToRoot為false的時候,Set the layout params for temp if we are not attaching. (If we are, we use addView, below)如果我們不把子布局放進父布局,就給給temp(子View)設置我們剛剛生成的那個匹配父布局的params參數(如果要把子View添加進父布局,我們用addView這個方法,在下邊)。
??那我們接著看attachToRoot為true的情況:if (root != null && attachToRoot) { root.addView(temp, params); }
此時直接調用root.addView(temp, params)方法,將temp以params參數添加進root.
??那么如果ViewGroup為null會怎么樣呢?我們看最下面一句:if (root == null || !attachToRoot) { result = temp; }
這里的result就是最后我們要返回的View,此時將temp直接返回,并沒有進行任何params參數的設置,這也難怪convertView = mInflater.inflate(R.layout.item,null);這種寫法會出現根布局失效的原因了。
至此,inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)的幾種情況算是真相大白了,至于inflate(int resource, ViewGroup root)兩個參數的情況可以參照本段最開始的結論性的內容,道理都是一樣的。
2、關于 inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)中attachToRoot為true時崩潰問題
(1).在ListView中
在講第四點時,我們為了說明布局參數不起作用的問題,先后將inflate的參數做了一下改變:
從convertView = mInflater.inflate(R.layout.itemlayout,null);
變到了convertView = mInflater.inflate(R.layout.item, parent,false);
那么喜歡搞事情的同學就說了,如果我們把最后一個參數變為tuue之后(convertView = mInflater.inflate(R.layout.item, parent,true);
)會怎么樣呢?可能上面也有講,這是將子View添加到父布局中的意思,但是真的這么簡單嗎?我們可以試一試:
??還是3中給出的例子,只不過我們將inflate的第三個參數改為true,運行之后......只見屏幕華麗的一閃,花擦?閃退了?。?!
我們看Logcat中的提示:
點擊藍色的提示行,代碼定位到了
這一行,也就是說正是我們剛剛修改的這行代碼引起的錯誤。仔細看看錯誤具體內容為:addView(View, LayoutParams) is not supported in AdapterView.也就是說AdapterView不支持addView(View, LayoutParams)方法。我們點擊at android.widget.AdapterView.addView(AdapterView.java:487)進入AdapterView源碼中可以看到下面的代碼:
上面也有提到過,在inflate的源碼中當attachToRoot為true的時候會執行下面代碼:
這里,root代表的是第二個參數也就是ViewGroup,在這里表示的是ListView,ListVeiew繼承自AdpaterVeiw,因此會直接調用AdapterView類中的addView(View, LayoutParams)方法,上圖已經展示,該方法在AdpaterVeiw已經被禁,調用即會拋出addView(View, LayoutParams) is not supported in AdapterView.異常。
??Ok,閃退的原因我們已經找到,但是我們不禁要問,AdapterView為什么要把addView()方法禁掉?原來這根AdapterView本身的特點有關,AdapterView雖然繼承自GroupView,但卻不同于GroupView:
AdapterView是一種包含多項相同格式資源的列表,其本質上是一個容器,他其中的列表項內容由Adapter提供。而他最大的特點就是:
①將前端顯示和后端數據分離.
②內容不能通過ListView.add的形式添加列表項,需指定一個Adapter對象,通過它獲得顯示數據。
③AdapterView相當于MVC框架中的V(視圖);Adapter相當于MVC框架中的C(控制器);數據源相當于MVC框架中的M(模型)
??在我們寫好整個BaseAdapter的時候,必須要通過listView.setAdapter(MyAdapter);的形式將這個繼承自BaseAdapter的MyAdapter添加進listView,而在這一步中,AdpaterView會調用自己內部獨有的循環加載View的方式(有興趣的讀者可以自行閱讀源碼),因此AdapterView肯定要禁掉addView()的那一系列方法。因此我們在ListView、GridView這類AdapterView的子類中通過inflate加載子視圖的時候,如果將attachToRoot設為true的時候,實際上已經在源碼中調用了addView()方法,自然會掛掉。
(2)、在fragment中
上文有提到過,inflate方法我們用到的比較多的地方就是ListView的getView()方法中;以及fragment的onCreateView()中。
??如果我們這里講inflate()的第三個參數設為true,也會產生奔潰的問題,但這一次奔潰的原因卻不經相同,畢竟fragment不是AdapterView,而奔潰的異常是:IllegalStateException!!我們來看看下面兩段代碼:
FragmentManager fragmentManager = getSupportFragmentManager();
Fragment fragment = fragmentManager.findFragmentById(R.id.root_viewGroup);
if (fragment == null) {
fragment = new MainFragment();
fragmentManager.beginTransaction().add(R.id.root_viewGroup, fragment).commit();
}
上面代碼中root_viewGroup就是Activity中用于放置Fragment的容器,它會作為inflate()方法中的第二個參數被傳入onCreateView()中。它也是你在inflate()方法中傳入的ViewGroup。FragmentManager會在.commit()的時候調用內部機制將Fragment的View添加到ViewGroup中。如果我們將最后一個參數設為true,那么就像上面那樣在源碼中再調用一次addView(),這樣就出現了重復添加,因而出現錯誤就不足為奇了。
public View onCreateView(LayoutInflater inflater, ViewGroup parentViewGroup, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_layout, parentViewGroup, false);
…
return view;
}
如果我們不需在onCreateView()中將View添加進ViewGroup,為什么還要傳入ViewGroup呢?為什么inflate()方法必須要傳入根ViewGroup?為什么不直接傳null進去呢?
??原因是即使不需要馬上將新填充的View添加進ViewGroup,我們還是需要這個父元素的LayoutParams來在將來添加時決定View的size和position。
(3).什么時候inflate()第三個參數可以設為true?
經過上面兩個例子,很多童鞋對inflate()的第三個參數估計產生了恐懼心理,那么我們就開看看什么情況下可以為true:答案就是當我們不負責將layout文件的View添加進ViewGroup時設置attachToRoot參數為false;當我們手動動態添加一個View到另一父布局的時候可以(注意是可以)設為true,舉個栗子:
Button button = (Button) inflater.inflate(R.layout.custom_button, mLinearLayout, false);
mLinearLayout.addView(button);
Button button = (Button) inflater.inflate(R.layout.custom_button, mLinearLayout, true);
在這兩段等效的代碼中,R.layout.custom_button使我們自定義的一個Button控件,mLinearLayout是我們定義的一個LinearLayout,這個時候我們可以將attachToRoot設為true,也就是將Button添加進LinearLayout,這個過程中沒有任何類似于FragmentManager或者AdapterView內置的自動添加視圖的機制,所以這樣做沒有任何問題。
??當然我們也可以設為false,我們告訴LayoutInflater我們不暫時還想將View添加到根元素ViewGroup中,意思是我們一會兒再添加。在這個例子中,一會兒再添加就是在inflate()后調用addView()方法手動添加。
??當然,通過上面源碼的分析可知,設為true的時候再源碼內部會自動調用addVew()方法,所以這兩種寫法本質上沒有任何區別。