在Android系统中,针对大量数据的展示,可以使用ListView以列表的形式的呈现。虽然现在ListView在很多地方都被RecyclerView取代了,但是在一些合适的场景中依旧有用武之地。本文将详细讲解ListView的使用和常用技巧。
ListView的使用还是很简单的,重点在于数据由Adapter(适配器)提供的,ListView并不直接访问数据源。因此,可以将ListView的使用分为3步:
针对一些简单的场景(如只需要展示字符串),使用系统提供的ArrayAdapter即可。ArrayAdapter使用数组或List作为数据源,常用的两个构造方法如下:
//resource:列表项的布局文件
//objects:数据源
public ArrayAdapter(Context context,@LayoutRes int resource,T[] objects);
public ArrayAdapter(Context context,@LayoutRes int resource,List objects)
使用ListView的示例代码如下:
//初始化普通布局的ListView
String[] dataArray=new String[]{"coding","ending","CodingEnding","Github","coder","Android"};//1.建立数据源
ArrayAdapter<String> normalAdapter=new ArrayAdapter<String>(this,
android.R.layout.simple_list_item_1,dataArray);//2.建立适配器
normalListView.setAdapter(normalAdapter);//3.设置适配器
ArrayAdapter中使用的android.R.layout.simple_list_item_1
是系统提供的布局文件,其实就是一个TextView。
效果截图:
//监听单击事件
normalListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView> parent, View view, int position, long id) {
Log.i(TAG,"当前位置:"+position);
String msg= (String) parent.getAdapter().getItem(position);//获取选中对象
Toast.makeText(ListViewActivity.this,msg,Toast.LENGTH_SHORT).show();
}
});
//监听长按事件
normalListView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
@Override
public boolean onItemLongClick(AdapterView> parent, View view, int position, long id) {
Toast.makeText(ListViewActivity.this,"发生长按事件",Toast.LENGTH_SHORT).show();
return true;
}
});
可以看到,在监听器中通过parent.getAdapter().getItem(position)
获取选中对象。注意,这个方法的返回值是Object对象,因此需要进行强制转换。
android:divider:设置ListView各项之间的分割线 [color或drawable资源]
android:dividerHeight:分割线的高度
android:headerDividersEnabled:是否绘制每个HeaderView后的分割线 [默认为true]
android:footerDividersEnabled:是否绘制每个FooterView前的分割线 [默认为true]
android:listSelector:设置列表项被选中时的效果 [color或drawable资源]
android:fastScrollEnabled:是否在快速滑动的是否显示右侧的滑动块
android:scrollbars:设置滑动条的展示方式 [horizontal|vertical|none]
android:stackFromBottom:是否在初始状态时显示ListView的最底部。 [默认为false]
stackFromBottom这个属性需要简单解释一下:如果设置为true,那么打开ListView首先看到的就是最底部的内容,看起来就像是ListView已经滚动到了最后一行;如果设置为false,就和默认状态一样,首先看到第一行的内容。
如果需要展示的内容比较复杂(比如图片加文字),我们就应该自定义适配器,使用自己的布局去展示列表项。
首先,建立一个实体类Book:
public class Book {
private String name;
private int imageRes;//图片资源
public Book(String name, int imageRes) {
this.name = name;
this.imageRes = imageRes;
}
@Override
public String toString() {
return name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getImageRes() {
return imageRes;
}
public void setImageRes(int imageRes) {
this.imageRes = imageRes;
}
}
接着,自定义一个布局文件(左侧图片,右侧文字),本例中命名为listview_custom_item.xml
,代码如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
android:gravity="center_vertical">
<ImageView
android:id="@+id/book_image"
android:layout_width="45dp"
android:layout_height="45dp"
android:layout_marginLeft="8dp" />
<TextView
android:id="@+id/book_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:textAllCaps="false"
android:textSize="16sp" />
LinearLayout>
然后,通过继承BaseAdapter
实现我们自己的适配器,本例中命名为StyleListViewAdapter
:
public class StyleListViewAdapter extends BaseAdapter{
private Context context;
private List dataList;
public StyleListViewAdapter(Context context, List dataList) {
this.context = context;
this.dataList = dataList;
}
@Override
public int getCount() {
return dataList.size();
}
@Override
public Object getItem(int position) {
return dataList.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
Book book=dataList.get(position);
View view= LayoutInflater.from(context).inflate(R.layout.listview_custom_item,parent,false);
ImageView bookImageView=view.findViewById(R.id.book_image);
TextView bookNameView=view.findViewById(R.id.book_name);
bookImageView.setImageResource(book.getImageRes());
bookNameView.setText(book.getName());
return view;
}
}
可以看到,需要重写getCount、getItem、getItemId、getView
这四个方法。此外,还要提供一个构造方法用于外界传入Context和数据源(本例中为List
)。
在实际使用中,通常会使用ViewHolder
提升ListView的运行效率,这一方式将充分利用ListView中View的复用机制。
首先,在Adapter中建立一个静态内部类ViewHolder:
static class ViewHolder{
ImageView bookImageView;
TextView bookNameView;
}
然后,修改Adapter中的getView
方法,复用已有的View:
@Override
public View getView(int position, View convertView, ViewGroup parent) {
Book book=dataList.get(position);
ViewHolder viewHolder;
if(convertView==null){
convertView= LayoutInflater.from(context).inflate(
R.layout.listview_custom_item,parent,false);
viewHolder=new ViewHolder();
viewHolder.bookImageView=convertView.findViewById(R.id.book_image);
viewHolder.bookNameView=convertView.findViewById(R.id.book_name);
convertView.setTag(viewHolder);//存储ViewHolder
}else{//复用已有的View
viewHolder= (ViewHolder) convertView.getTag();
}
viewHolder.bookImageView.setImageResource(book.getImageRes());
viewHolder.bookNameView.setText(book.getName());
return convertView;
}
最后,在代码中为ListView设置自定义的适配器即可,代码如下:
//初始化自定义布局的ListView
List dataList=new ArrayList<>();
dataList.add(new Book("《小王子》",R.mipmap.ic_launcher));
dataList.add(new Book("《资本论》",R.mipmap.ic_launcher));
dataList.add(new Book("《三体》",R.mipmap.ic_launcher));
StyleListViewAdapter styleAdapter=new StyleListViewAdapter(this,dataList);
customListView.setAdapter(styleAdapter);
效果截图:
在实际使用中,列表项可能不止一种布局形式,典型的如通讯录列表就有联系人、标题(如A、B、C等)这两种形式的列表项。通过对Adapter的修改,可以通过ListView实现多布局列表。在这里,将介绍如何实现一个简单的多布局列表,最终的效果如下:
在本例中,主要有两种列表项,即标题项和内容项。因此,准备两个对应的布局文件,分别命名为listview_multi_title.xml
和listview_multi_item.xml
,代码如下:
listview_multi_title.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/item_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="6dp" />
LinearLayout>
listview_multi_item.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
android:gravity="center_vertical">
<ImageView
android:id="@+id/item_image"
android:layout_width="45dp"
android:layout_height="45dp"
android:layout_marginLeft="8dp" />
<TextView
android:id="@+id/item_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:textAllCaps="false"
android:textSize="16sp"
android:textColor="#000000"/>
LinearLayout>
对于不同的布局而言,应该使用不同的实体类。在本例中,有两种列表项,因此需要两个实体类。首先可以建立一个基类,本例中命名为BaseMultiBean
,代码如下:
public abstract class BaseMultiBean {
public static final int TYPE_TITLE=0;//标题项
public static final int TYPE_ITEM=1;//内容项
protected int type;//类型
public int getType() {
return type;
}
public void setType(int type) {
this.type = type;
}
}
可以看到,基类中主要是封装了实体的类型属性,这一属性将用于确定要使用的列表项布局。然后,再建立两个继承自基类的实体类,分别对应标题项和内容项,本例中命名为TitleBean
和ItemBean
,代码如下:
TitleBean
public class TitleBean extends BaseMultiBean{
private String title;
public TitleBean(String title) {
this.title = title;
this.type=TYPE_TITLE;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
}
ItemBean
public class ItemBean extends BaseMultiBean{
private int imageRes;//图片资源
private String content;//内容
public ItemBean(int imageRes, String content) {
this.imageRes = imageRes;
this.content = content;
this.type=TYPE_ITEM;
}
public int getImageRes() {
return imageRes;
}
public void setImageRes(int imageRes) {
this.imageRes = imageRes;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
有了布局和实体类,就可以开始着手创建适配器了,本例中命名为MultiListViewAdapter
。和前面提到的适配器相比,还需要实现getViewTypeCount
和getItemViewType
这两个方法。此外,getView
也需要修改,以及还要提供两种不同的ViewHolder分别对应两种列表项。示例代码如下:
public class MultiListViewAdapter extends BaseAdapter{
......
@Override
public int getViewTypeCount() {//返回类型种类数
return 2;
}
@Override
public int getItemViewType(int position) {//返回当前项的类型
BaseMultiBean bean=dataList.get(position);
return bean.getType();
}
static class TitleViewHolder{//针对标题项的复用
TextView titleView;
}
static class ItemViewHolder{//针对内容项的复用
ImageView itemImageView;
TextView itemContentView;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
TitleViewHolder titleViewHolder;
ItemViewHolder itemViewHolder;
switch(getItemViewType(position)){//根据Item的类型不同,执行相应的操作
case BaseMultiBean.TYPE_TITLE:
TitleBean titleBean= (TitleBean) dataList.get(position);
if(convertView==null){
convertView=inflater.inflate(R.layout.listview_multi_title,parent,false);
titleViewHolder=new TitleViewHolder();
titleViewHolder.titleView=convertView.findViewById(R.id.item_title);
convertView.setTag(titleViewHolder);
}else{
titleViewHolder= (TitleViewHolder) convertView.getTag();
}
titleViewHolder.titleView.setText(titleBean.getTitle());
break;
case BaseMultiBean.TYPE_ITEM:
ItemBean itemBean= (ItemBean) dataList.get(position);
if(convertView==null){
convertView=inflater.inflate(R.layout.listview_multi_item,parent,false);
itemViewHolder=new ItemViewHolder();
itemViewHolder.itemImageView=convertView.findViewById(R.id.item_image);
itemViewHolder.itemContentView=convertView.findViewById(R.id.item_content);
convertView.setTag(itemBean);
}else{
itemViewHolder= (ItemViewHolder) convertView.getTag();
}
itemViewHolder.itemImageView.setImageResource(itemBean.getImageRes());
itemViewHolder.itemContentView.setText(itemBean.getContent());
break;
default:break;
}
return convertView;
}
}
有了前面三步的准备工作,现在就可以着手为ListView设置适配器了,示例代码如下:
//初始化多状态布局的ListView(未设置点击监听)
List multiDataList=new ArrayList<>();
multiDataList.add(new TitleBean("第一个区域"));
multiDataList.add(new ItemBean(R.mipmap.ic_launcher,"《小王子》"));
multiDataList.add(new ItemBean(R.mipmap.ic_launcher,"《狮子王》"));
multiDataList.add(new TitleBean("第二个区域"));
multiDataList.add(new ItemBean(R.mipmap.ic_launcher,"《资本论》"));
multiDataList.add(new ItemBean(R.mipmap.ic_launcher,"《三体》"));
multiDataList.add(new ItemBean(R.mipmap.ic_launcher,"《孤独的进化者》"));
MultiListViewAdapter multiAdapter=new MultiListViewAdapter(this,multiDataList);
multiListView.setAdapter(multiAdapter);
public void setEmptyView(View emptyView);//在ListView的数据为空时显示emptyView
首先,在ListView所在的XML文件中定义一个EmptyView的布局,示例代码如下:
android:id="@+id/empty_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
"wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textSize="20sp"
android:text="暂无数据"/>
提示: EmptyView的width
和height
属性可以和ListView保持一致,这样在空数据时刚好可以让EmptyView占据ListView的空间。
然后,在代码中为ListView设置EmptyView,示例代码如下:
View view=findViewById(R.id.empty_view);
normalListView.setEmptyView(view);
效果截图:
只需将android:scrollbars
属性设置为none就可以隐藏ListView的滚动条。
只需将android:listSelector
属性设置为#00000000
就可以去掉默认的选中颜色(其实是设置为透明色)。
android:layoutAnimation:为ListView设置布局动画。 [使用layoutAnimation资源]
为ListView设置layoutAnimation属性后,ListView的所有可见项都会执行指定的动画,有多少可见项就会执行多少次动画。主要的使用步骤如下:
首先,在res文件夹下的anim
文件夹中建立一个set
动画资源,本例中命名为listview_anim.xml
,示例代码如下:
<set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha android:fromAlpha="0" android:toAlpha="1" android:duration="1000"/>
<translate android:fromXDelta="1000" android:toXDelta="0" android:duration="1000"/>
set>
这个动画的作用是让View从右侧飞入,且有一个由浅入深的渐变效果,每个动画持续1000ms。
然后,在anim文件夹下建立一个layoutAnimation
资源,本例中命名为listview_layout_animation.xml
,示例代码如下:
<layoutAnimation xmlns:android="http://schemas.android.com/apk/res/android"
android:delay="0.3"
android:animationOrder="random"
android:animation="@anim/listview_anim">
layoutAnimation>
delay
指定下一次动画相对上一次动画的延迟倍数,可以是0到1之间的值;animation
指定需要使用的动画资源。animationOrder
指定子View的动画执行顺序,可选值与含义如下:
最后,为ListView指定对应的layoutAnimation资源即可:
<ListView
android:id="@+id/list_view_normal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layoutAnimation="@anim/listview_layout_animation"/>
提示:如果需要手动执行布局动画,可以调用ListView的startLayoutAnimation
方法。
效果截图:
相关方法:
//data:为HeaderView绑定的数据(可通过ListAdapter#getItem方法获得)
//isSelectable:指定HeaderView是否可选中(是否触发OnItemClickListener和OnItemLongClickListener)
public void addHeaderView(View v, Object data, boolean isSelectable);//添加指定列表头(可调用多次,从上往下逐次添加)
public void addHeaderView(View v);//添加指定列表头(可调用多次,从上往下逐次添加)
public int getHeaderViewsCount();//获得列表头的个数
public boolean removeHeaderView(View v);//移除列表头
//data:为FooterView绑定的数据(可通过ListAdapter#getItem方法获得)
//isSelectable:指定FooterView是否可选中((是否触发OnItemClickListener和OnItemLongClickListener))
public void addFooterView(View v, Object data, boolean isSelectable);//添加指定列表头(可调用多次,从上往下逐次添加)
public void addFooterView(View v);//添加指定列表头(可调用多次,从上往下逐次添加)
public int getFooterViewsCount();//获得列表尾的个数
public boolean removeFooterView(View v);//移除列表尾
说明:方法中的addHeaderView(View v)
其实是通过调用addHeaderView(view, null, true)
的方式实现的。addFooterView(View v)
方法与之同理。
示例代码:
//为ListView添加列表头/尾
String[] dataArray=new String[]{"coding","ending","CodingEnding","Github","coder","Android"};
ArrayAdapter<String> headerFooterAdapter=new ArrayAdapter<String>(this,
android.R.layout.simple_list_item_1,dataArray);
LayoutInflater inflater=LayoutInflater.from(this);
View headerView=inflater.inflate(R.layout.listview_header,headerFooterListView,false);//实例化头布局
View footerView=inflater.inflate(R.layout.listview_footer,headerFooterListView,false);//实例化尾布局
headerFooterListView.addHeaderView(headerView,"HeaderView",true);//设置列表头可选中
headerFooterListView.addFooterView(footerView,"FooterView",false);//设置列表尾不可选中
headerFooterListView.setAdapter(headerFooterAdapter);
效果截图:
提示: addFooterView和addHeaderView应该在ListView使用setAdapter
设置适配器前调用,否则可能出现异常。
注意:如果为ListView设置了HeaderView或者FooterView,在OnItemClickListener
的onItemClick方法中,position可能不是我们希望取得的值(因为算上了HeaderView和FooterView的个数)。此时,如果想要获得选中项,应该通过AdapterView#getAdapter
获取,示例代码如下:
customListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView> parent, View view, int position, long id) {
Book book= (Book) parent.getAdapter().getItem(position);
}
});
parent.getAdapter()
获取的是ListView适配器的包装类,它的getItem
方法会排除HeaderView和FooterView的影响,返回正确的对象。
先向数据源(如ArrayList)中添加数据,然后调用ListAdapter#notifyDataSetChanged
方法通知列表刷新。示例代码如下:
List<Book> dataList dataList=new ArrayList<>();
.....
dataList.add(new Book("《新的书籍》",R.mipmap.ic_launcher));//为数据源新增数据
styleAdapter.notifyDataSetChanged();//通知ListView数据已更新
具体代码请参考demo。
android:transcriptMode:设置列表的滚动模式
ListView的滚动模式由transcriptMode
属性决定,它有三种可选值,含义如下:
在开发中根据实际需求选择相应的滚动模式即可。
//跳转到指定位置(让这个Item成为列表当前的第一个可见项)
public void setSelection(int position);
//让HeaderView成为列表当前的第一个可见项(如果HeaderView不存在则显示position为0的项)
public void setSelectionAfterHeaderView();
小技巧:如果这两个方法在调用时无效,可以先调用ListView的clearFocus
方法。
//平滑滚动到指定位置
public void smoothScrollToPosition(int position);
//平滑滚动n个列表项的距离
//offset:需要滚动的列表项个数(offset为正数时ListView向上滚动,为负数时向下滚动)
public void smoothScrollByOffset(int offset);
注意: smoothScrollToPosition并不保证将指定位置的列表项显示为列表当前的第一个可见项,只保证这个列表项在可视范围内。
效果截图:
只需要为ListView设置OnScrollListener
即可,示例代码如下:
//监听ListView的滑动状态
normalListView.setOnScrollListener(new AbsListView.OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
//滑动状态发生变化时触发
//scrollState的可能值:[SCROLL_STATE_IDLE|SCROLL_STATE_TOUCH_SCROLL|SCROLL_STATE_FLING]
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
//滑动完成时触发
//firstVisibleItem:第一个可见项的索引值
//visibleItemCount:可见项的个数
//totalItemCount:列表项的总数
}
});
onScrollStateChanged中的scrollState
可能有三种取值,含义如下:
小技巧:可在OnScrollListener的onScroll
方法中判断ListView是否已经滑动到末尾,示例代码如下:
@Override
public void onScroll(AbsListView view,int firstVisibleItem,int visibleItemCount,int totalItemCount) {
if(totalItemCount>0&&firstVisibleItem+visibleItemCount==totalItemCount){
//已经滚动到末尾
}
}
for(int i=0;i<normalListView.getChildCount();i++){
View view=normalListView.getChildAt(i);//可以强制转换为具体的View
}
问题描述:在自定义ListView列表项布局的时候,如果列表项中包含Button、CheckBox等需要焦点的控件,就可能导致点击列表项不起作用。
解决方案:
android:focusable
设置为false。android:descendantFocusability
属性设置为blocksDescendants。descendantFocusability
属性的可选值和效果说明如下:
上面两种解决方案任选一种即可。
问题描述:如果列表项中的图片需要异步加载,由于ListView的View复用机制,在图片下载完毕时原来的ImageView可能已被复用,就可能导致图片显示错位。
解决方案:首先调用setTag
方法为列表项中的ImageView设置标签。在异步加载完毕后,通过ListView的findViewWithTag
方法查找ImageView。如果ImageView已经被复用了,这个方法的返回值就是null。通过判断这个方法的返回值是否为null,决定是否为ImageView设置图片资源,就可以解决图片显示错位的问题。示例代码如下:
@Override
public View getView(int position, View convertView, ViewGroup parent) {
.....
imageView.setTag(imageUrl);//将图片地址作为ImageView的Tag
.....
}
ImageView imageView=listView.findViewWithTag(imageUrl);
if(imageView!=null){
imageView.setImageDrawable(drawable);
}
《 Android UI GridView讲解》:详细讲解GridView的使用方法和常用技巧。
《 Android UI 常用控件讲解》:包括CheckBox、RadioButton、ToggleButton、Switch、ProgressBar、SeekBar、RatingBar、Spinner、ImageButton。
https://github.com/CodingEnding/UISystemDemo [ 持续更新中 ]
http://gundumw100.iteye.com/blog/1169065
http://blog.csdn.net/guolin_blog/article/details/45586553
http://blog.csdn.net/yangshangwei/article/details/50322919
http://blog.csdn.net/csdn_aiyang/article/details/70739945
http://blog.csdn.net/zhuwentao2150/article/details/52425334
http://blog.csdn.net/quwei3930921/article/details/51013012