ListView浅析(一)——基本用法及常见问题总结

一、适用场景

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.jianshu.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根元素的布局参数也会起作用!!!
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()方法,所以这两种写法本质上没有任何区别。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容