ListView
listView是用来解决大量的相似数据显示问题,同时大量的数据会导致不断在内存中创建对象,可能导致OOM。
listView属性
ListView的常用属性如下表所示:
名称 | 描述 |
---|---|
android:choiceMode | none,值为 0,表示无选择模式。 singleChoice,值为 1,表示最多可以有一项被选中。 multipleChoice,值为 2,表示可以多项被选中。 |
android:divider | 规定 List 项目之间用某个图形或颜色来分隔。 |
android:dividerHeight | 分隔符的高度。 |
android:entries | 引用一个将使用在此 ListView 里的数组。 |
android:footerDividersEnabled | 设成 flase 时,此 ListView 将不会在页脚视图前画分隔符。 此属性缺省值为 true。 属性值必须设置为 true 或false。 |
android:headerDividersEnabled | 设成 flase 时,此 ListView 将不会在页眉视图后画分隔符。此属性缺省值为 true。 |
该控件采用MVC的设计模式,而且还有大量适配器,一般情况是:BaseXXX、BasicXXX、SimpleXXX、DefaultXXX。
除此之外,来看看ListView的常用事件:
事件 | 描述 |
---|---|
setOnclickListener() | 列表点击事件 |
setOnItemLongClickListener() | 条目长按事件 |
setOnItemClickListener() | 条目点击事件 |
setOnScrollListener() | 列表滚动事件 |
setOnItemSelectedListener() | 条目选择事件 |
setOnTouchListener() | 列表触摸事件 |
简析Adapter
让我们来看看常用的Adapter:
名称 | 构造参数 | 功能 |
---|---|---|
ArrayAdapter | context:上下文,一般是this。 textViewResourceId:指定自定义的布局文件 objects:ListView视图中的类,类型根据泛型而变化 |
支持泛型操作,只能展示一行文字 |
SimpleAdapter | context:上下文,一般是this。 data:代表整个ListView的List集合 resource:自定义的布局文件或系统布局文件 from:对应的key的数组。 to:对应的value的数组。 |
同样具有良好扩展性的一个Adapter,可以自定义多种效果。 |
CursorAdapter | context:上下文,一般是this。 cursor:游标 flags:标志位 |
显示简单文本类型的listView,一般在数据库那里会用到。 |
BaseAdapter | 没有构造方法 | 抽象类,实际开发中我们会继承这个类并且重写相关方法,用得最多的一个Adapter。 |
ArrayAdapter
ArrayAdapter是BaseAdapter的子类,主要用于存放字符串。
public class MainActivity extends Activity {
private ListView mListView;
private static final String[] mDatas = {"功能1","功能2","功能3"};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mListView = (ListView) findViewById(R.id.listview);
// 设置适配器,第三个数组是根据泛型而变化的
mListView.setAdapter(new ArrayAdapter<String>(this, R.layout.list_item, mDatas));
}
}
需要注意的是,上面的list_item布局中,TextView必须作为根节点,否则报错:
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/tv_content"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
如果需要使用系统自定义的样式,由于ArrayAdapter是单行显示,所以只能用simple_list_item_1
mListView.setAdapter(new ArrayAdapter<String>(this,android.R.layout.simple_list_item_1,
new String[]{"功能1","功能2","功能3"}));
如果ArrayAdapter想要实现更多的效果则需要自定义ArrayAdapter,不过目前这种方式已经过时。
SimpleAdapter
SimpleAdapter也是BaseAdapter的子类,用来实现一些图片、文字并排的效果。
public class MainActivity extends Activity {
private ListView mListView;
private String[] mDatas = {"声音","显示","存储","电池","应用"};
private int[] resources = {R.drawable.akb,R.drawable.akc,R.drawable.akd,R.drawable.ake,R.drawable.akf};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mListView = (ListView) findViewById(R.id.listview);
// 构建数据模型
List<Map<String, Object>> lists = new ArrayList<Map<String,Object>>();
for (int i = 0; i < resources.length; i++) {
Map<String, Object> maps = new HashMap<String, Object>();
maps.put("name", mDatas[i]);
maps.put("drawable", resources[i]);
lists.add(maps);
}
mListView.setAdapter(new SimpleAdapter(this, lists, R.layout.item,
new String[]{"name","drawable"}, new int[]{R.id.tv_content,R.id.iv_img}));
}
}
如果需要用到系统的样式,有如下系统样式:
simple_list_item_1:单行文本组成
simple_list_item_2:两行文本组成
simple_list_item_checked:每项都是由一个已选中的列表框。
simple_list_item_single_choice:都带有一个单选按纽。
simple_list_item_multiple:全部带有一个复选框。
CursorAdapter
它同样是BaseAdapter的子类,它为Cursor和ListView提供连接的桥梁。
newView():并不是每次都被调用,它只在实例化和数据增加时调用,而修改条目的内容时不会调用。
bindView():在绘制item之前或重绘时一定会调用。
changeCursor():类似于notifyDataSetChange()方法。
从源码中可以看到:我们在写CursorAdapter时必须实现它的两个方法:
/**
* Makes a new view to hold the data pointed to by cursor.
* @param context Interface to application's global information
* @param cursor The cursor from which to get the data. The cursor is already
* moved to the correct position.
* @param parent The parent to which the new view is attached to
* @return the newly created view.
*/
public abstract View newView (Context context, Cursor cursor, ViewGroup parent);
/**
* Bind an existing view to the data pointed to by cursor
* @param view Existing view, returned earlier by newView
* @param context Interface to application's global information
* @param cursor The cursor from which to get the data. The cursor is already
* moved to the correct position.
*/
public abstract void bindView(View view, Context context, Cursor cursor);
简单的示例代码如下:
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
ViewHolder viewHolder= new ViewHolder();
LayoutInflater inflater=(LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE );
View view=inflater.inflate(R.layout.item_contacts ,parent,false);
viewHolder. tv_name=(TextView) view.findViewById(R.id.tv_showusername );
viewHolder. tv_phonenumber=(TextView) view.findViewById(R.id.tv_showusernumber );
view.setTag(viewHolder);
Log. i("cursor" ,"newView=" +view);
return view;
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
Log. i("cursor" ,"bindView=" +view);
ViewHolder viewHolder=(ViewHolder) view.getTag();
//从数据库中查询姓名字段
String name=cursor.getString(cursor.getColumnIndex(PersonInfo.NAME));
//从数据库中查询电话字段
String phoneNumber=cursor.getString(cursor.getColumnIndex(PersonInfo.PHONENUMBER));
viewHolder. tv_name.setText(name);
viewHolder. tv_phonenumber.setText(phoneNumber);
}
调用newView方法实例化条目,然后调用bindView绘制条目,当只绘制时不会调用newView方法。
btn_save.setOnClickListener( new OnClickListener() {
public void onClick(View v) {
userName=et_name.getText().toString();
userPhoneNumber=et_phonenumber .getText().toString();
ContentValues contentValues= new ContentValues();
contentValues.put(PersonInfo. NAME, userName);
contentValues.put(PersonInfo.PHONENUMBER ,userPhoneNumber );
//把EditText中的文本插入数据库
dataBase.insert(PersonInfo. PERSON_INFO_TABLE, null,contentValues);
//根据 _id 降序插叙数据库保证最后插入的在最上面
Cursor myCursor = dataBase.query(PersonInfo. PERSON_INFO_TABLE, null, null, null, null, null, orderBy);
//Cursor改变调用chanageCursor()方法
myCursorAdapter.changeCursor(myCursor);
}
});
ListView复用
ListView的复用机制可以参考下图:
BaseAdapter是经常用到的基础数据适配器,它的主要用途是将一组数据传到像ListView、Spinner、Gallery及GrideView等组件。
(1) getCount():是listView的长度。
(2) getView(): 根据这个长度逐一绘制它的每一行。
(3) getItem()和getItemId()则需要处理和取得Adapter中的数据时调用。
其中,getView()方法的写法如下:
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View view = View.inflate(MainActivity.this, R.layout.item, null);
TextView tvContent = (TextView) findViewById(R.id.tv_content);
tvContent.setText(mDatas.get(position));
return view;
}
复用对象
由于上方的代码每次需要一个View对象都会重新inflate一个view出来,没有实现对象的复用。
而系统给我们提供convertView,代表的是可复用的对象,当它为空则创建一个对象,否则直接复用。
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View view;
if(convertView == null){
view = View.inflate(MainActivity.this, R.layout.item, null);
}else{
view = convertView;
}
TextView tvContent = (TextView) view.findViewById(R.id.tv_content);
tvContent.setText(mDatas.get(position));
return view;
}
减少查找次数
当converView为空时,会重新inflate一个View对象,除此之外还会findViewById进行查找工作,我们可以通过一个ViewHolder类来存储对应的
成员变量,然后通过getTag和setTag来操作,这时,当convertView为空时,只需要取出ViewHolder中存储的成员变量进行复用即可。
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if(convertView == null){
convertView = View.inflate(MainActivity.this, R.layout.item, null);
holder = new ViewHolder();
holder.mTvContent = (TextView) convertView.findViewById(R.id.tv_content);
convertView.setTag(holder);
}else{
holder = (ViewHolder) convertView.getTag();
}
holder.mTvContent.setText(mDatas.get(position));
return convertView;
}
class ViewHolder{
TextView mTvContent;
}
ListView多样式
ListView已结内置实现多种样式的功能,使用步骤如下:
重写getViewTypeCount() -- 该方法返回多个不同的布局总数。
重写getItemViewType(int) -- 根据position返回响应的Item。
根据view item的类型,在getView中创建正确的convertView。
1、创建MyAdapter继承BaseAdapter,在适配的getItemViewType()中通过计算得出不同的状态,用常量进行标记,其中type必须从0开始,否则会报数组角标越界异常。
public static final int TYPE_1 = 0;
public static final int TYPE_2 = 1;
public static final int TYPE_3 = 2;
@Override
public int getItemViewType(int position) {
int p = position % 6;
if(p == 0){
return TYPE_1;
}else if (p < 3) {
return TYPE_2;
}else if (p < 6) {
return TYPE_3;
}else{
return TYPE_1;
}
}
2、在getViewTypeCount()中获取不同布局的种类数
@Override
public int getViewTypeCount() {
return 3;
}
3、此时我们需要给定义三个不同的布局,并创建三个不同的ViewHolder来针对不同的布局进行缓存复用
<TextView
android:id="@+id/textview1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/holo_green_dark"
android:gravity="center"
android:text="我是绿色的" />
其他三个布局都是如此,只是指定的背景颜色不一样,同时定义三个ViewHolder
class ViewHolder1{
TextView textView;
}
class ViewHolder2{
TextView textView;
}
class ViewHolder3{
TextView textView;
}
4、此时我们在getView中来判断常量,进行填充不同的布局以及设置资源等操作
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder1 holder1 = null;
ViewHolder2 holder2 = null;
ViewHolder3 holder3 = null;
int type = getItemViewType(position);
if(convertView == null){
// 按当前所需样式,确定new出的布局
switch (type) {
case TYPE_1:
convertView = View.inflate(MainActivity.this, R.layout.item1, null);
holder1 = new ViewHolder1();
holder1.textView = (TextView) convertView.findViewById(R.id.textview1);
convertView.setTag(holder1);
break;
case TYPE_2:
convertView = View.inflate(MainActivity.this, R.layout.item2, null);
holder2 = new ViewHolder2();
holder2.textView = (TextView) convertView.findViewById(R.id.textview2);
convertView.setTag(holder2);
break;
case TYPE_3:
convertView = View.inflate(MainActivity.this, R.layout.item3, null);
holder3 = new ViewHolder3();
holder3.textView = (TextView) convertView.findViewById(R.id.textview3);
convertView.setTag(holder3);
break;
}
}else{
switch (type) {
case TYPE_1:
holder1 = (ViewHolder1) convertView.getTag();
break;
case TYPE_2:
holder2 = (ViewHolder2) convertView.getTag();
break;
case TYPE_3:
holder3 = (ViewHolder3) convertView.getTag();
break;
}
}
// 根据不同样式设置资源
switch (type) {
case TYPE_1:
holder1.textView.setText("我是绿色"+mDatas.get(position));
break;
case TYPE_2:
holder2.textView.setText("我是蓝色"+mDatas.get(position));
break;
case TYPE_3:
holder3.textView.setText("我是红色"+mDatas.get(position));
break;
}
return convertView;
}
ListView的动画
1、在ListView布局使用layoutAnimation属性引入一个动画文件。
<ListView
android:id="@+id/listview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layoutAnimation="@anim/list_item_animation">
</ListView>
2、在anin文件下创建该布局动画文件list_item_animation
<?xml version="1.0" encoding="utf-8"?>
<layoutAnimation xmlns:android="http://schemas.android.com/apk/res/android"
android:delay="0.2"
android:animation="@anim/item_animation"
android:animationOrder="normal"/>
3、动画文件又引入item_animation文件,放在res/ani目录下,该文件描述动画效果。
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:fromXDelta="100%"
android:fromYDelta="0"
android:toXDelta="0"
android:toYDelta="0"
android:duration="1000"/>
<alpha
android:fromAlpha="0"
android:toAlpha="1"
android:duration="1000"/>
<rotate
android:fromDegrees="0"
android:toDegrees="360"
android:pivotX="50%"
android:pivotY="50%"
android:duration="1000"/>
</set>
4、我们也可以在代码中来设置Item的加载动画
Animation animation = AnimationUtils.loadAnimation(this, R.anim.item_animation);
LayoutAnimationController animationController = new LayoutAnimationController(animation);
animationController.setDelay(0.4f);// 设置间隔时间
animationController.setOrder(LayoutAnimationController.ORDER_NORMAL);// 设置列表显示顺序
mListView.setLayoutAnimation(animationController);
ListView的焦点
要想获取焦点,需要在Item布局的根节点添加上述属性,android:descendantFocusability="blocksDescendants" 即可,另外该属性有三个可供选择的值:
beforeDescendants:viewgroup会优先其子类控件而获取到焦点
afterDescendants:viewgroup只有当其子类控件不需要获取焦点时才获取焦点
blocksDescendants:viewgroup会覆盖子类控件而直接获得焦点
例如:ListView的Item条目中的Button事件冲突解决。
在ItemView配置的xml文件中的根节点添加属性android:descendantFocusability="blocksDescendants"
在要添加事件的控件上添加android:focusable="false"
示例代码如下所示:
<?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="wrap_content"
android:descendantFocusability="blocksDescendants"
android:orientation="vertical">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:focusable="false"
android:text="Button"/>
</LinearLayout>
复选框错位问题
1、首先是activity_maind的代码
<RelativeLayout 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"
tools:context="cn.legend.check.MainActivity" >
<ListView
android:id="@+id/listview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</RelativeLayout>
2、在MainActivity中实例化ListView,并模拟数据,设置适配器
public class MainActivity extends Activity {
private ListView mListView;
private MyAdapter mAdapter;
private ArrayList<String> mDatas;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mListView = (ListView) findViewById(R.id.listview);
mDatas = new ArrayList<String>();
initData();
mAdapter = new MyAdapter(mDatas, this);
mListView.setAdapter(mAdapter);
mListView.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
ViewHolder holder = (ViewHolder) view.getTag();
holder.cb.toggle();
// 获取到存储状态的数据集合,每次点击条目则让checkbox状态改变,并且将状态存储到状态集合中
MyAdapter.getSelectMaps().put(position, holder.cb.isChecked());
}
});
}
private void initData() {
for (int i = 0; i < 100; i++) {
mDatas.add("data" + " " + i);
}
}
}
3、接下来是适配器类的编写,具体思路是用HashMap将状态存储起来,然后防止Checkbox由于复用而错乱
public class MyAdapter extends BaseAdapter {
private ArrayList<String> mDatas;
private static HashMap<Integer, Boolean> selectMaps;
private Context context;
public MyAdapter(ArrayList<String> data, Context context) {
this.context = context;
this.mDatas = data;
selectMaps = new HashMap<Integer, Boolean>();
initData();
}
private void initData() {
for (int i = 0; i < mDatas.size(); i++) {
getSelectMaps().put(i, false);
}
}
@Override
public int getCount() {
return mDatas.size();
}
@Override
public Object getItem(int position) {
return mDatas.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder = null;
if (convertView == null) {
convertView = View.inflate(context, R.layout.item, null);
holder = new ViewHolder();
holder.tv = (TextView) convertView.findViewById(R.id.item_tv);
holder.cb = (CheckBox) convertView.findViewById(R.id.item_cb);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
holder.tv.setText(mDatas.get(position));
holder.cb.setChecked(getSelectMaps().get(position));
return convertView;
}
class ViewHolder {
TextView tv;
CheckBox cb;
}
public static HashMap<Integer, Boolean> getSelectMaps() {
return selectMaps;
}
}
4、其中使用到的item布局文件
<?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/item_tv"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
android:layout_marginTop="5dp"
android:layout_marginLeft="5dp"
android:layout_weight="1"
android:gravity="center_vertical" />
<CheckBox
android:id="@+id/item_cb"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
android:layout_marginTop="5dp"
android:layout_marginRight="5dp"
android:clickable="false"
android:focusable="false"
android:focusableInTouchMode="false"
android:gravity="center_vertical" />
</LinearLayout>