Android(安卓)UI ListView讲解
前言
在Android系统中,针对大量数据的展示,可以使用ListView以列表的形式的呈现。虽然现在ListView在很多地方都被RecyclerView取代了,但是在一些合适的场景中依旧有用武之地。本文将详细讲解ListView的使用和常用技巧。
基本使用
ListView的使用还是很简单的,重点在于数据由Adapter(适配器)提供的,ListView并不直接访问数据源。因此,可以将ListView的使用分为3步:
- 获得数据源(如数组,List等)
- 通过数据源建立适配器(如ArrayAdapter等)
- 为ListView设置适配器
使用系统提供的布局
针对一些简单的场景(如只需要展示字符串),使用系统提供的ArrayAdapter即可。ArrayAdapter使用数组或List作为数据源,常用的两个构造方法如下:
//resource:列表项的布局文件//objects:数据源public ArrayAdapter(Context context,@LayoutRes int resource,T[] objects);public ArrayAdapter(Context context,@LayoutRes int resource,List objects)
使用ListView的示例代码如下:
//初始化普通布局的ListViewString[] 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提升运行效率
在实际使用中,通常会使用ViewHolder
提升ListView的运行效率,这一方式将充分利用ListView中View的复用机制。
首先,在Adapter中建立一个静态内部类ViewHolder:
static class ViewHolder{ ImageView bookImageView; TextView bookNameView;}
然后,修改Adapter中的getView
方法,复用已有的View:
@Overridepublic 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设置自定义的适配器即可,代码如下:
//初始化自定义布局的ListViewList 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设置适配器了,示例代码如下:
//初始化多状态布局的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的动画执行顺序,可选值与含义如下:
- normal:ListView的列表项顺序执行动画(从第一个可见列表项开始执行到最后一个可见列表项)
- random:ListView的列表项随机执行动画
- reverse:ListView的列表项逆序执行动画(从最后一个可见列表项开始执行到第一个可见列表项)
最后,为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
属性决定,它有三种可选值,含义如下:
- disabled:列表有新的数据增加时,ListView并不发生滚动。[默认值]
- normal:如果最后一个列表项在可视范围内,当有新的数据增加时,列表会自动滚动到底部。
- alwaysScroll:只要有新的数据增加时,列表就会自动滚动到底部。
在开发中根据实际需求选择相应的滚动模式即可。
跳转到指定位置
//跳转到指定位置(让这个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
可能有三种取值,含义如下:
- SCROLL_STATE_IDLE:静止状态
- SCROLL_STATE_TOUCH_SCROLL:滑动状态(用户此时触碰着屏幕且在滑动)
- SCROLL_STATE_FLING:惯性滑动状态(用户此时未触碰屏幕,ListView借助上一次滑动的惯性滑动)
小技巧:可在OnScrollListener的onScroll
方法中判断ListView是否已经滑动到末尾,示例代码如下:
@Overridepublic 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等需要焦点的控件,就可能导致点击列表项不起作用。
解决方案:
- 将列表项中Button、CheckBox等控件的
android:focusable
设置为false。 - 将列表项根布局的
android:descendantFocusability
属性设置为blocksDescendants。
descendantFocusability
属性的可选值和效果说明如下:
- beforeDescendants:ViewGroup会在所有子View之前获得焦点
- afterDescendants:ViewGroup会在所有子View之后获得焦点
- blocksDescendants:ViewGroup会组织子View获得焦点
上面两种解决方案任选一种即可。
异步加载时图片显示错位
问题描述:如果列表项中的图片需要异步加载,由于ListView的View复用机制,在图片下载完毕时原来的ImageView可能已被复用,就可能导致图片显示错位。
解决方案:首先调用setTag
方法为列表项中的ImageView设置标签。在异步加载完毕后,通过ListView的findViewWithTag
方法查找ImageView。如果ImageView已经被复用了,这个方法的返回值就是null。通过判断这个方法的返回值是否为null,决定是否为ImageView设置图片资源,就可以解决图片显示错位的问题。示例代码如下:
@Overridepublic 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。
demo下载地址
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
更多相关文章
- CSDN精选Android开发博客
- Android学习笔记(16):绝对布局AbsoluteLayout、常用距离单位
- android简单demo学习系例之排版(TableLayout)[xml-based]
- Android布局的优化
- android ListView的使用方法
- Android(安卓)提高显示布局文件的性能[Lesson 2 - 使用include标
- android intent 及 intent action全面描述
- Android中fitsSystemWindows属性的用法总结
- Android(安卓)动态创建一个组件