Android—ListView 的各种使用方法
一、 AdapterView 及其子类
AdapterView 是一组重要的组件,它的主要作用是通过列表的形式显示数据。
AdapterView 本身是一个抽象类,常用的 ListView(列表)、Spinner(下拉列表)、Gallery(缩略图),GridView(网格图)都是 AdapterView 的子类。子类的用法相似,知识在显示上略有不同。
AdapterView 的子类继承关系
下面是 AdapterView 的类定义(成员变量及方法省略)
/** * An AdapterView is a view whose children are determined by an {@link Adapter}.
* AdapterView是一个视图,其子视图由{@link Adapter}确定 * See {@link ListView}, {@link GridView}, {@link Spinner} and {@link Gallery} for commonly used subclasses of AdapterView.
* 提示AdapterView 的常用子类 */ public abstract class AdapterView<T extends Adapter> extends ViewGroup { }
AdapterView 具有如下特征:
1. AdapterView 继承了 ViewGroup ,说明它本质是容器。
2. AdapterView 可以包含多个 ”列表项“ (即子视图),子视图由与之关联的 Adapter 确定,以合适的方式显示出来。
二、AdapterView 的子类 ListView
以垂直列表的形式显示所有的列表项,并且能够根据数据的长度自适应显示。下面就是使用 AdapterView 实现的效果
ListView 的使用大致上可以分为四个步骤:添加 ListView 组件、存储数据、设置列表项item的布局文件、加载数据/资源进行显示、添加监听。下面讲解几个 ListView 的使用方法:
1. 当整个Activity中只有一个ListView组件时,可以使用ListActivity。
ListActivity类继承于Activity类,默认绑定了一个ListView组件,并提供一些与ListView处理相关的操作。
ListActivity类常用的方法为getListView(),该方法返回绑定的ListView组件。
一旦在程序中获得了ListView之后,接下来就需要为ListView设置它要显示的列表项了。在这一点上,ListView显示出了AdapterView的特征:通过setAdapter(Adapter)方法为之提供Adapter,并由Adapter提供列表项即可。
2. 最简单的方法——通过 android:entried 调用加载数组资源
a. 在布局文件中添加 ListView 组件 ( ListView 的 id 号为 android:list )
使用特定 id 号的好处:在 listView 中可以直接通过 getListView() 方法得到 ListView 实例
<ListView android:id="@+id/android:list" android:layout_width="match_parent" android:layout_height="wrap_content" />
b. 在 values 文件夹中添加或设置 string.xml 文件用来存储数据(为了体现滚轮效果,可以适量增加 item 数目)
<string-array name="fruit_array"> <item>Apple</item> <item>Banana</item> <item>Orange</item> <item>Watermelon</item> <item>Pear</item> <item>Grape</item> <item>Pineapple</item> <item>Strawberry</item> <item>Cherry</item> <item>Mango</item> </string-array>
c. 通过 android:entried 调用加载数组资源
<ListView android:id="@+id/android:list" android:layout_width="match_parent" android:layout_height="wrap_content" android:entries="@array/fruit_array" />
由于这个方法只用于显示文本类型的数据,不经常使用,添加监听的部分在这里就不展开讲解了
3. AdapterView + Adapter、
使用数组资源创建 ListView 是一种非常简单的方式,但是这种 ListView 能定制的内容很少。如果想丰富 ListView 的外观、内容,添加组件的行为,就需要把 ListView 当作 AdapterView 来使用,通过 Adapter 自定义每一个 列表项的 外观、内容、行为动作等。
AdapterView + Adapter 的工作原理
类似于 MVC 框架,数据源(Model)存放数据,利用控制器(Controller)将数据显示在视图(View)上。
ListView 相当于 V(View),用于显示视图;Adapter 相当于 控制器(Controller)
当需要数据时,ListView 会从 Adapter 中取出数据进行显示。
3-1 基于ArrayAdapter 使用 ListView
a. 添加 ListView 组件(同上)
b. 存储数据。存储数据的方式可以多种多样的,可以将数据存储在 string.xml 文件中,可以将其存储为 string[] 数组形式,也可以将其存储为 List 列表形式。
c. 设置列表项item的布局文件。
item 中包含多种组件,例如 ImageView,Button 等,我们可以用 ArrayList 进行加载,将数据资源存储在一个类中,接着创建一个类继承 ArrayAdapter ,在 getView() 方法中将数据资源与 item 布局中的组件联系在一起即可,这在《第一行代码》有非常详细的讲解,在这篇博客中,我使用 SimpleAdapter 进行加载,会在下面进行讲解。
该 LIstView 只显示最简单的当行文本内容,因此选择一个android 自带的简单布局
android.R.layout.simple_list_item_1
d. 利用 adapter 将数据显示在 ListView 上。也就是实例化 ArrayAdapter 类,在 ListView 中添加 Adapter 两个步骤。
实例化 ArrayAdapter 类
ArrayAdapter 有很多的构造方法,例如
public ArrayAdapter(@NonNull Context context, @LayoutRes int resource) { this(context, resource, 0, new ArrayList<>()); } public ArrayAdapter(@NonNull Context context, @LayoutRes int resource, @IdRes int textViewResourceId) { this(context, resource, textViewResourceId, new ArrayList<>()); } public ArrayAdapter(@NonNull Context context, @LayoutRes int resource, @NonNull T[] objects) { this(context, resource, 0, Arrays.asList(objects)); } public ArrayAdapter(@NonNull Context context, @LayoutRes int resource, @IdRes int textViewResourceId, @NonNull T[] objects) { this(context, resource, textViewResourceId, Arrays.asList(objects)); } public ArrayAdapter(@NonNull Context context, @LayoutRes int resource, @NonNull List<T> objects) { this(context, resource, 0, objects); }
public ArrayAdapter(@NonNull Context context, @LayoutRes int resource, @IdRes int textViewResourceId, @NonNull List<T> objects) { this(context, resource, textViewResourceId, objects, false); }
我们可以发现,上面五个构造方法都是调用最后一个构造方法,而最后构造方法调用了另一个方法,这个方法是私有的。我们来详细看看这个构造方法
/** * Constructor * @param context 当前上下文。 * @param resource 子项布局id:布局文件的资源ID,其中包含在实例化视图时要使用的布局。 * @param textViewResourceId 布局资源中要填充的TextView的ID * @param objects 要在ListView中表示的对象 */ private ArrayAdapter(@NonNull Context context, @LayoutRes int resource, @IdRes int textViewResourceId, @NonNull List<T> objects, boolean objsFromResources) { mContext = context; mInflater = LayoutInflater.from(context); mResource = mDropDownResource = resource; mObjects = objects; mObjectsFromResources = objsFromResources; mFieldId = textViewResourceId; }
在上面的构造方法中,我们最经常使用的是第三个
/** * Constructor * @param context 当前上下文。 * @param resource 子项布局id:布局文件的资源ID,其中包含在实例化视图时要使用的布局。 * @param objects 要在ListView中表示的对象 */
public ArrayAdapter(@NonNull Context context, @LayoutRes int resource, @NonNull T[] objects) {
this(context, resource, 0, Arrays.asList(objects));
}
可以看到,在这个方法中,使用 泛型 来表示传进来的要在 ListView 中显示的对象,着说明你可以传入许多种数据类型,但是最终会会使用 Arrays.asList(Objects)) 进行类型转换,返回 MutableList
d. 添加监听——这里只添加 列表项的点击事件。使用
总的代码如下:
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main);
//获取 ListView 控件 ListView listView = (ListView)findViewById(R.id.list_view);
//获取数据 final String[] fruitArray = getResources().getStringArray(R.array.fruit_array);
// 为 ListView 添加控制器 ArrayAdapter ArrayAdapter adapter = new ArrayAdapter(this, android.R.layout.simple_list_item_1,fruitArray); listView.setAdapter(adapter);
//为 ListView 的列表项添加鼠标点击事件 listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { /** * @param adapterView 发生单击事件的列表项 ListView * @param view 被单击控件 view * @param i 在列表项中的位置 position * @param l 被单击列表项的行ID */ @Override public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) { String Tag = "onItemClick======"; Log.d(Tag,"position="+i); Log.d(Tag,"行 ID"+l); Toast.makeText(MainActivity.this,fruitArray[i],Toast.LENGTH_SHORT); } }); } }
3-2 基于 SimpleAdapter 使用 ListView
SimpleAdapter的扩展性最好,可以定义各种各样的布局出来,可以放上ImageView(图片),还可以放上Button(按钮),CheckBox(复选框)等等
a. 添加 ListView 组件(同上)
b. 设置列表项 item 的布局文件
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:id="@+id/image" android:layout_weight="4" android:layout_width="0dp" android:layout_height="150dp" android:layout_gravity="left" android:layout_marginLeft="1dp" android:padding="7dp"/> <LinearLayout android:layout_weight="6" android:layout_width="0dp" android:layout_height="wrap_content" android:orientation="vertical" android:layout_gravity="center" android:layout_marginLeft="5dp"> <TextView android:id="@+id/title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="left"/> <TextView android:id="@+id/info" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="left"/> <Button android:id="@+id/button" android:layout_width="30dp" android:layout_height="30dp" android:background="@drawable/btn" android:layout_gravity="right" android:layout_marginRight="30dp" android:padding="10dp" android:focusable="false"/> <!--android:focusable="false"--> </LinearLayout> </LinearLayout>
效果:
c. 存储数据。
同样,我们通过分析 SimplaeAdapter 构造方法来决定数据类型。SimpleAdapter 只有一个构造方法
/** * Constructor * * @param context 运行与此SimpleAdapter关联的View的上下文,即放置 ListView 的上下文环境 * @param data 数据为一个列表 list ,list 中的数据以 Map 类型存储列表中的每个条目对应于列表中的一行。 列表中的每个条目对应于列表中的一行。 * @param resource 列表项的布局文件 * @param from A list of column names that will be added to the Map associated with each item. * @param to The views that should display column in the "from" parameter. These should all be * TextViews. The first N views in this list are given the values of the first N columns * in the from parameter. */ public SimpleAdapter(Context context, List<? extends Map<String, ?>> data, @LayoutRes int resource, String[] from, @IdRes int[] to) { mData = data; mResource = mDropDownResource = resource; mFrom = from; mTo = to; mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); }
可以知道,数据存储在 List 中,List 中每一项数据对应 ListView 的一行,以 Map 的形式进行存储。 from 和 to 数组分别代表Map中 key 值和 Item 布局文件中组件的 ID 号,呈一一对应关系。
public List<HashMap<String,Object>> getData(){ List<HashMap<String,Object>> list = new ArrayList<>(); HashMap<String,Object> map; map= new HashMap<>(); map.put("image",R.drawable.i1); map.put("title","开心的哆啦A梦"); map.put("info","cute cute cute cute cute cute cute cute cute cute "); list.add(map); map = new HashMap<>(); map.put("image",R.drawable.i2); map.put("title","贪吃的哆啦A梦"); map.put("info","卡哇伊 卡哇伊 卡哇伊 卡哇伊 卡哇伊 卡哇伊 卡哇伊 "); list.add(map); map = new HashMap<>(); map.put("image",R.drawable.i3); map.put("title","大哭的哆啦A梦"); map.put("info","cute cute cute cute cute cute cute cute cute cute "); list.add(map); return list; }
d. 利用 adapter 加载数据/资源进行显示
//初始化列表数据 final List<HashMap<String,Object>> list = getData(); //为ListView 添加 Adapter String[] mapKeyArray = {"image","title","info"}; int[] layoutIdArray = {R.id.image,R.id.title,R.id.info}; SimpleAdapter adapter = new SimpleAdapter(MainActivity.this,list,R.layout.listview_item, mapKeyArray , layoutIdArray );
e. 添加监听
如果没有重写 Adapter 的 getView() 方法,给按钮单独添加监听是比较麻烦的,所以我们可以使用 BaseAdapter 再给按钮添加监听,这里给 Item 列表项添加监听。添加的方法和使用 ArrayAdapter 一样。所以这里就不展开了。
但是需要注意的是,由于 列表项中添加的 Button 按钮,按钮组件会获取焦点,所以需要设置 button 的 focusable 为 false。可以直接设置 button 的属性,也可以用java 方法进行设置。例如上面共参考的 item 布局文件就已经设置了。
3-3 基于 BaseAdapter 使用 ListView
添加 ListView 组件,存放数据,设置列表项的布局文件都和 SimpleAdapter 中的操作相同
d. 创建一个 Adapter 继承 BaseAdapter,并实现抽象方法。
BaseAdapter 有 4 个抽象方法
int getCount(); //返回的是数据源对象的个数,即列表项数 Object getItem(int var1); //返回指定位置position上的列表项 long getItemId(int var1); //返回指定位置处的行ID View getView(int var1, View var2, ViewGroup var3); //返回列表项对应的视图
继承 BaseAdapter 时需要去实现这 4 个抽象方法,这几个抽象方法都是 Adapter 接口中定义的方法。
以理解为adapter先由getCount确定数量,然后循环执行getView()方法将条目一个一个绘制出来。
必须重写的方法是getCount和getView方法。
前三个方法基本不需要过多修改,
public int getCount() { return mData.size(); } public Object getItem(int position) { return mData.get(position); } public long getItemId(int position) { return position; }
由于新建了一个 java 文件,所以建议把 上下文 context,和 存储数据的列表 list 传过来
public class BabyAdapter extends BaseAdapter { Context context; List<HashMap<String,Object>> list ; public BabyAdapter(Context context,List list){ super(); this.context = context; this.list = list; } @Override public int getCount() { return list.size(); } @Override public Object getItem(int i) { return list.get(i); } @Override public long getItemId(int i) { return i; } @Override public View getView(final int i, View convertView, ViewGroup viewGroup) { /** 在不重写getView 方法的情况下,每次调用 getView 时都要重新实例化子项 item 的布局文件,然后通过 findViewById 重新寻找 View 组件并绘画 着会导致两个问题,1. 重复加载布局文件;2. 重复多次寻找 View 组件 解决:使用 convertView 作为 View 缓存,形成 adapter 的itemView 重用机制,减少重绘 view 的次数 形参 view 中缓存了itemView 的布局文件
*/ View view; ViewHolder holder; if(convertView == null){
// LayoutInflater 用于加载布局的系统服务,实例化与Layout XML文件对应的View对象
// 不能直接使用, 需要通过getLayoutInflater( )方法或getSystemService( )方法来获得与当前Context绑定的 LayoutInflater实例。
LayoutInflater factory = LayoutInflater.from(context);
// reSource:View 的layout 的ID
// root: 生成的view 对象的父控件。若提供了 root(!null),则返回 root 作为根结点,否则,返回 view 对象的根布局作为根布局,
view = factory.inflate(R.layout.listview_item,null); // 2. 使用 ViewHolder 实现 View 组件的缓存 // 重用 View 时就不用通过 findViewById 重新寻找 view 组件,同时减少 view 组件重绘的次数 holder = new ViewHolder(); holder.image =(ImageView)view.findViewById(R.id.image); holder.title = (TextView)view.findViewById(R.id.title); holder.info = (TextView)view.findViewById(R.id.info); holder.button = (Button)view.findViewById(R.id.button); view.setTag(holder); }else{ view = convertView; holder = (ViewHolder)view.getTag(); } // 2. 使用 ViewHolder 实现 View 组件的缓存 // 重用 View 时就不用通过 findViewById 重新寻找 view 组件,同时减少 view 组件重绘的次数 HashMap<String,Object> map = list.get(i); holder.image.setImageResource((int)map.get("image")); holder.title.setText((String)map.get("title")); holder.info.setText((String)map.get("info")); // 给按钮添加监听 holder.button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { new AlertDialog.Builder(context) .setTitle(list.get(i).get("title").toString()) .setMessage(list.get(i).get("info").toString()) .setPositiveButton("确定", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { } }).show(); } }); return view; } class ViewHolder{ ImageView image; TextView title ; TextView info; Button button; } }
其中,converView 指 列表项视图,当该 item 从未再屏幕上出现过时,convertView 为空,一旦出现过,convertView 中就缓存了View 对象,下一次就可以直接取得 列表项中视图对象使用而不用再加载。
三、ListView 的子项 Item 缓存原理
1. 假设:屏幕只能显示5个Item,那么ListView只会创建(5+1)个Item的视图;当第1个Item完全离开屏幕后才会回收至缓存从而复用(用于显示第6个Item)
2. 假设:屏幕只能显示5个Item,那么ListView只会创建(5+1)个Item的视图;当第1个Item完全离开屏幕后才会回收至缓存从而复用(用于显示第7个Item)