④ ListView控件的设计遵循MVC设计模式:
ListAdapter
类把数据项与View关联起来。
ListView
控件的步骤
① 在布局中添加ListView
标签,如下:
<ListView android:id="@+id/lv" android:layout_width="match_parent" android:layout_height="match_parent" />
② 在代码中找到控件,并为其设置实现ListAdapter
接口的适配器,如下:
ListView lv = (ListView) findViewById(R.id.lv);
ListAdapter adapter = new MyAdapter();
lv.setAdapter(adapter);
ListAdapter
接口。这里有些需要注意的需要说明。
ListAdapter
接口中,有大量需要实现的方法,显而易见,在实际工作中每次都实现一遍,需要花费巨大的工作量。这一点,Google工程师,也帮我们考虑到了,为我们提供了几个默认的实现类,类图如下:
这里我们使用的就是BaseAdapter
,创建一个内部类,继承它,代码如下,当然这些只是简单的实现,后续还会继续深入。
class MyAdapter extends BaseAdapter {
// 适配器中有多少个数据项需要被显示
@Override
public int getCount() {
return 100;
}
// 获取指定位置上的数据项,默认不实现;使用的很少
@Override
public Object getItem(int position) {
return position;
}
// 获取指定位置数据项的ID,默认不实现;使用的很少
@Override
public long getItemId(int position) {
return position;
}
// 获取一个视图展示数据到指定位置(position)
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// 这里只给一个简单的实现
TextView tv = new TextView(getApplicationContext());
tv.setText("喜欢学习Android,第(" + position + ")天");
tv.setTextColor(Color.RED);
tv.setTextSize(23);
return tv;
}
}
值得说明的:手机屏幕的高度有限制,而在getCount()
返回的是100,需要向下不断的滑动,才能全部加载显示初始来。而一个屏幕能够显示多少个条目与以下几点有关系:
1.. 屏幕的高度
2.. 数据项(item)的高度
② 上个案例中在getView()
方法中,每次都是创建新的TextView
对象,代码如下:
// 获取一个视图展示数据到指定位置(position)
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// 这里只给一个简单的实现
TextView tv = new TextView(getApplicationContext());
tv.setText("喜欢学习Android,第(" + position + ")天");
tv.setTextColor(Color.RED);
tv.setTextSize(23);
return tv;
}
getCount()
返回的是100,也就是说,如果要全部显示完,那么需要创建100个对象。如果们让
getCount()
设置为
Integer.MAX_VALUE
,要全部显示完 ,需要创建
个对象,这显然是非常恐怖的。
那么该如何优化?
通过使用getView
参数中的View convertView
复用历史对象!
代码如下:
public View getView(int position, View convertView, ViewGroup parent) {
TextView tv = null;
// 判断历史缓存对象为空则代表还没有创建对象
if (convertView == null) {
tv = new TextView(getApplicationContext());
Log.i(TAG, "新创建了TextView对象,当前位置(" + position + ")");
} else {
// 复用
tv = (TextView) convertView;
Log.i(TAG, "利用converView复用的历史对象,当前位置(" + position + ")");
}
tv.setText("喜欢学习Android,第(" + position + ")天");
tv.setTextColor(Color.RED);
tv.setTextSize(23);
return tv;
}
当我们运行程序,稍微向下滑动一点点时,历史对象就已经开始复用了。
而当我们快速向下滑动时,依旧使用的历史对象,也并未新建对象。
另外,关于ListView的优化。一般来说还会在getView方法中使用ViewHolder方式再对控件的寻找做进一步的优化,这里就不涉及了。
除此之外在ListView
还有隐藏起来的一个小问题
在之前的布局文件中,ListView
标签的android:layout_height="match_parent"
属性值都是match_parent
,如果我们改成包裹内容wrap_content
,并把代码中的getCount()
返回值改成3,再运行程序会发生什么?
<ListView android:id="@+id/lv" android:layout_width="match_parent" android:layout_height="wrap_content" />
public int getCount() {
return 3;
}
但是我们看看,LogCat中发生了什么?
竟然调用了很多次创建和多次复用!这是由于ListView
的布局文件中高度使用的是warp_content
,在加载布局时,会不断的测定ListView
的高度,并不断加载所造成的。这样也会造成性能的损耗,而避免的方法也很简单,用到ListView
标签的地方,高度使用match_parent
就可以了。
最后,ListView中可以优化的地方还有很多,这些就等在以后再叙述了。
在之上的例子中,getView()
方法中获取的View
都是我们手动创建的对象。而查看API可以看到这句话:You can either create a View manually or inflate it from an XML layout file。
它简单的意思是,即可以手工创建一个View
,也可以使用inflate
把一个XML布局文件加载成为View
对象。
那么下面我们就这样做,在getView()
方法中,加载一个复杂一些的布局。
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// 这里就暂时不复用历史对象了
// 解释一下inflate三个参数的含义
// 参数1:使用上下文环境
// 参数2:被加载的XML布局文件ID
// 参数3:传入null时,返回整个XML布局对象(ViewGorup),包括ViewGroup的所有子布局;不为null,只返回XML布局对象的父元素;
View view = View.inflate(getApplicationContext(), R.layout.item,
null);
TextView tvTitle = (TextView) view.findViewById(R.id.tv_title);
TextView tvContent = (TextView) view.findViewById(R.id.tv_content);
tvTitle.setText("标题(" + position + ")");
tvContent.setText("当前位置:" + position);
return view;
}
这里,最重要的就是使用View.inflate()
方法来加载XML布局文件,具体参数的含义也已经在代码中解释了。
测试结果:
方式一
View view = View.inflate(getApplicationContext(), R.layout.item,
null);
方式二
View view1 = LayoutInflater.from(getApplicationContext()).inflate(
R.layout.item, null);
方式三
View view2 = ((LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE))
.inflate(R.layout.item, null);
关于LayoutInflater的更多详细信息,可以参看我的另一个文章: Android LayoutInflater 详解
在所有的适配器中,最常用的莫过于BaseAdapter
了,但是除了它,Android还提供了一些其他易用的适配器。
① 写一个用于显示内容的布局文件(items.xml),如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" >
<TextView android:id="@+id/tv" android:textSize="25sp" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="TextView" />
</LinearLayout>
② 为ListView
设置ArrayAdapter
适配器,代码如下:
ListView lv = (ListView) findViewById(R.id.lv);
// 上下文对象
Context context = getApplicationContext();
// XML布局文件ID
int resource = R.layout.item;
// 被显示的TextView控件的ID
int textViewResourceId = R.id.tv;
// 被显示的数据
String[] objects = {"别志华","胡玉琼"};
ListAdapter adapter = new ArrayAdapter<String>(context, resource, textViewResourceId, objects);
lv.setAdapter(adapter );
这个例子,需要使用到Android下数据库的第二种增删改查方式中的InfoDao和MyOpenHelper两个类
① 在布局中增加<ListView>
标签
<ListView android:id="@+id/lv" android:layout_width="match_parent" android:layout_height="match_parent" >
</ListView>
② 为适配器准备一个item子布局,内容很简单,里面有两个<TextView>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal" >
<TextView android:id="@+id/tv_name" android:layout_width="0dip" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="name" android:textSize="18sp" />
<TextView android:id="@+id/tv_phone" android:layout_width="0dip" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="phone" android:textSize="18sp" />
</LinearLayout>
③ 把数据库中获取到的数据封装成List<Bean>
集合,需要准备bean
和Dao
public class InfoBean {
private String name;
private String phone;
}
操作也比较简单,拿到游标循环给Bean
设置属性,并添加到List
集合中。
// 查询
public List<InfoBean> find() {
Cursor cursor = db.query("info", new String[] { "name", "phone" },
null, null, null, null, null, null);
List<InfoBean> infoList = null;
if (cursor != null) {
infoList = new ArrayList<InfoBean>();
InfoBean info = null;
while (cursor.moveToNext()) {
int columnIndex = 0;
columnIndex = cursor.getColumnIndex("name");
String name = cursor.getString(columnIndex);
columnIndex = cursor.getColumnIndex("phone");
String phone = cursor.getString(columnIndex);
// 设置info属性
info = new InfoBean();
info.setName(name);
info.setPhone(phone);
// 存入集合中
infoList.add(info);
}
} // end if
return infoList;
}
④ 找到布局文件中ListView
控件,做成成员变量,为其设置适配器。获取到数据集合,也做成成员变量,这样才可以在内部类中MyAdapter
中访问。
lv = (ListView) findViewById(R.id.lv);
// 查询
public void find(View v) {
infoList = dao.find();
if (infoList == null) {
throw new RuntimeException();
}
lv.setAdapter(new MyAdapter());
}
一个比较完整的适配器如下,需要注意的两点:1. 在getCount()
中使用了return infoList.size();
返回,即动态又实效。2. 在getView()
中使用InfoBean info = infoList.get(position);
当前条目的position
来获取真实的数据,用以展示。
class MyAdapter extends BaseAdapter {
@Override
public int getCount() {return infoList.size(); }
@Override
public Object getItem(int position) {return position;}
@Override
public long getItemId(int position) {return position;}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// 复用对象
View view = null;
if (convertView == null) {
view = ((LayoutInflater)getSystemService(LAYOUT_INFLATER_SERVICE)).inflate(R.layout.item, null);
} else {
view = convertView;
}
// 找到item中的控件,并设置值
InfoBean info = infoList.get(position);
TextView tvName = (TextView) view.findViewById(R.id.tv_name);
TextView tvPhone = (TextView) view.findViewById(R.id.tv_phone);
tvName.setText(info.getName());
tvPhone.setText(info.getPhone());
return view;
}
}