ListView使用总结
虽然随着RecyclerView
的不断普及,相应的资源也越来越多,许多的项目都在使用RecyclerView
,但作为他的前辈ListView
,加深对ListView
的使用有助于我们更好的适应到RecyclerView
的使用中。
首先看一下我们实现的效果一些简单效果
这只是前面的一些简单效果,后面会有一些进阶的效果,希望能耐心的看下去。
ListView的优化
ListView
的优化主要包括两个方面,分别是对自身的优化以及其适配器(Adapter
)的优化。
ListView
自身的优化
主要包括一条。对于ListView
的layout_height
和layout_width
设置为match_parent
,如果设置为match_parent
,一般ListView
的宽高会测量三次以上。具体的源码没有深入研究。但为什么会要测量多次,如果对于自定义View
稍微有点基础的会知道,对于View
的测量大小有三个类型:
- UNSPECIFIED
:未指定的,父类不对子类施加任何限制。
- EXACTLY
:确定的,父类确定其子类控件的大小。
- AT_MOST
:最大值,需要子类去测量自身大小确定。
如果我们设置宽高为wrap_content
,即AT_MOST
,表示其宽高有控件本身去测量确定,而如果是match_parent
,则EXACTLY
,表示确定的大小。
Adapter
优化。
对于Adapter的优化,主要包括以下几个步骤:
复用
convertView
,减少子布局的生成。定义
ViewHolder
,减少findViewById()
的次数。
下面看一下代码
/*** 最基础的adapter* Created by Alex_MaHao on 2016/5/17.*/public class SimpleBaseAdapter extends BaseAdapter {private List<String> datas;public SimpleBaseAdapter(List<String> datas) {this.datas = datas;}@Overridepublic int getCount() {return datas==null?0:datas.size();}@Overridepublic Object getItem(int position) {return null;}@Overridepublic long getItemId(int position) {return 0;}@Overridepublic View getView(int position, View convertView, ViewGroup parent) {ViewHolder vHolder;if(convertView==null){//初始化item布局convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_listview_sample,parent,false);//创建记事本类vHolder = new ViewHolder();//查找控件,保存控件的引用vHolder.tv = ((TextView) convertView.findViewById(R.id.listview_sample_tv));//将当前viewHolder与converView绑定convertView.setTag(vHolder);}else{//如果不为空,获取vHolder = (ViewHolder) convertView.getTag();}vHolder.tv.setText(datas.get(position));return convertView;}/*** 笔记本类,保存对象的引用*/class ViewHolder{TextView tv;}
}
根据代码分析思路:
- ListView
中的item
滑出屏幕时,滑出的item
会在getView()
方法中返回,及converView
参数。所以在这里判断,是否为null,如果不为null,表示我们可以对该布局重新设置数据并返回到列表中显示。
- ViewHolder
,保存item
中子控件的引用。因为我们复用了converView
,那么对于同一个converView
布局,其子控件的引用应该是不变的。所以我们可以获取到其上的所有子控件的引用并通过ViewHolder
进行保存。
- setTag(),getTag()
:该方法实现了ViewHolder
和converView
的绑定,就类似于通过setTag()
方法,将ViewHolder
打包成一个包裹,放在了converView
上,再通过getTag()
方法,将这个包裹取出。
在很多优化中,会将
ViewHolder
定义为static
。但在这里我并没加上。因为经过测试,加或不加,ViewHolder
的创建次数不变。网上查了很多资料,也没有找到一个让我信服的理由。唯一有点理的就是基于java的特性。静态内部类的对象不依赖于其所在的外部类对象。
Adapter的封装–BaseAppAdapter
上一节说了Adapter
的优化,但如果我们每次写都要写这么多的优化代码,这不符合程序员懒惰的天性。那我们只能把他封装,提取出一个公共的基类,在基类中,我们把布局加载,优化等都默认实现,只需让子类构造布局文件,以及绑定数据。
那么,从我们上一节的代码看,有以下模块都可以提取为基类:
- 数据集合datas:对于数据集合,我们通常都是一个
List
集合,在这里定义泛型来表述其所包含的内容。 - 数据优化:布局的复用以及
ViewHolder
与converView
的绑定。 ViewHolder
:定义一个ViewHolder
,通过map
保存控件与id;
子类所需实现的:
- 数据的初始化
- 确定
item
的布局文件 - 将数据与视图绑定。
那么直接看一下我们继承好的代码:
public abstract class BaseAppAdapter<T> extends BaseAdapter {/*** 泛型,保存数据*/protected List<T> datas;/*** 构造方法,子类必须实现其构造方法,并初始化数据* @param datas*/public BaseAppAdapter(List<T> datas) {this.datas = datas;}@Overridepublic int getCount() {return datas==null?0:datas.size();}@Overridepublic Object getItem(int position) {return null;}@Overridepublic long getItemId(int position) {return 0;}@Overridepublic View getView(int position, View convertView, ViewGroup parent) {BaseViewHolder vHolder;if(convertView==null){convertView = LayoutInflater.from(parent.getContext()).inflate(getItemLayoutId(),parent,false);vHolder = new BaseViewHolder(convertView);convertView.setTag(vHolder);}else{vHolder = (BaseViewHolder) convertView.getTag();}/*** 数据绑定的回调*/bindData(vHolder,datas.get(position));return convertView;}/*** 子类实现,获取item布局文件的id* @return*/protected abstract int getItemLayoutId();/*** 子类实现,绑定数据* @param vHolder 对应position的ViewHolder* @param data 对应的数据绑定*/protected abstract void bindData(BaseViewHolder vHolder,T data);/*** ViewHolder类*/class BaseViewHolder{/*** 保存view,以id-view的形式*/Map<Integer,View> mapView;View rootView;public BaseViewHolder(View rootView){this.rootView = rootView;mapView = new HashMap<Integer,View>();}/*** 通过id查找控件* @param id* @return*/public View getView(Integer id){View view = mapView.get(id);if(view==null){view = rootView.findViewById(id);mapView.put(id,view);}return view;}}}
在以上代码中有几个关键点:
- 有参的构造方法:子类实现时,必须调用父类的构造方法,用以保存数据。
getItemLayoutId()
:item
的布局文件id。抽象方法,子类必须实现。对于每一个BaseAppAdapter
的子类,都需要通过该方法返回他们所特有的item
的id。bindData(BaseViewHolder vHolder,T data)
:数据绑定方法,子类必须实现。子类在此方法中将数据设置到试图上BaseViewHolder
中的getView(Integer id)
:该方法设计比较巧妙,因为,对于基类我们不知道有哪些id
需要查询,如果直接通过findViewById()
方法,则并没有减少查询次数。在这里,通过保存一个map
对象,通过id
先从map
中取,如果不存在,说明是第一次获取,则使用findViewById
查找控件,并将控件以键值对的形式保存到map
中,则下次查找,会从map
中取,减少了findViewById
的次数。可能有人会认为,添加了一个map
对象,内存占用不是增大了吗,但是,第一,我们map
中保存的都是引用。第二,findViewById()
是按照深度优先遍历查询的,如果不懂,遍历肯定能明白吧。
ListView 分割线的高度和颜色设置。
简单的两个属性
android:divider
:设置颜色,背景android:dividerHeight
:设置高度
如果设置无分割线,可以设置android:divider="@null"
。
该两个属性必须同时使用,如果只设置
divider
,则没有效果,同时默认的分割线也会消失。
当然,我们也可以在item
布局文件中添加分割线(在底部添加一个线),麻烦点而已。
隐藏ListView 的滚动条
android:scrollbars="none"
取消ListView的点击反馈效果
对于ListView
,在android5.0一下是一个变色,在android5.0以上是一个波纹动画。我们可以通过一些设置取消掉他的反馈效果。使用如下代码
android:listSelector="#0000"
:点击颜色设置为透明
设置空试图
ListView
作为列表显示的控件,那么肯定会存在数据为空的情况,如果我们就直接显示一面白无疑是不友好的,ListView
可以通过设置一个视图,当列表为null,显示该视图。
关键方法为setEmpty(View)
。具体使用方法如下:
xml文件中,在ListView
下设置一个图片,如下
<com.mahao.alex.systemwidgetdemo.listView.MyListView
android:id="@+id/listview_lv"android:divider="#f00"android:scrollbars="none"android:listSelector="#0000"android:dividerHeight="3dp"android:layout_width="match_parent"android:layout_height="match_parent"/><ImageView
android:id="@+id/listview_empty_view"android:layout_width="match_parent"android:src="@mipmap/ic_launcher"android:layout_height="match_parent" />
在java代码中设置如下
//设置空视图,调用此方法会默认隐藏Empty_viewmLv.setEmptyView(findViewById(R.id.listview_empty_view));
注意:我们无需设置
ImageView
的visible
属性,因为通过setEmpty()
方法,已将其显示与隐藏于ListView
所绑定,由ListView
来管理。
ListView
的滚动
对于显示数据,我们可能会有一些特殊的要求,比如初始显示到第几列,或者要调到第几条显示。对于此种要求,ListView
已经实现了相应的方法让我调用。
public void setSelection(int position)
:position
为需要显示在第一条的数据。该方法为瞬间滚动,无滑动效果,类似图片上瞬间滑动按钮实现的效果。public void smoothScrollToPosition(int position)
:与上面方法作用相同,不过其有过度效果,类似平滑滚动实现的效果。
ListView 下拉顶部空白回弹效果
在ListView
中,有如下方法,他控制了回弹设置的值:
protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent)
maxOverScrollY
参数便是设置我们可以下拉的空白区域的高度。具体实现如下
public class MyListView extends ListView {/*** 下拉回弹效果,下拉的最大距离*/private int mMaxOverDistance;public MyListView(Context context) {super(context);}public MyListView(Context context, AttributeSet attrs) {super(context, attrs);//初始化最大距离DisplayMetrics metrics = context.getResources().getDisplayMetrics();float density = metrics.density;mMaxOverDistance = (int) (100*density);}public MyListView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);}@Overrideprotected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) {//注意第九个参数,设置为了我们自定义的值return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY, maxOverScrollX, mMaxOverDistance, isTouchEvent);}
}
该方法是保护类型的,所以我们需要通过继承ListView
来修改此方法的实现。
在看到此方法时,第一反应是ScrollView
有没有该方法,查了以后,发现ScrollView
也有该方法。那么,同理,ScrollView
也能通过重写overScrollBy
实现下拉回弹的效果。
ListView 进阶–实现跟随滑动消失和显现的ToolBar
首先看一下效果图:
分析其逻辑:
- 当我们向上滑动列表时,
toolbar
慢慢消失到顶部。 - 当我们向下滑动列表时,
toolbar
慢慢的从顶部显现。
实现逻辑:
- setOnTouchListener
,设置触摸监听。
- 对当前手指移动事件进行判断。
- 如果是向下移动,且ToolBar
是隐藏的,则给toolbar
设置一个向下显现的动画。
- 如果是向上移动,且toolbar
是显现的,则给toolbar
设置一个向上隐藏的动画。
看一下实现代码:
/*** Created by Alex_MaHao on 2016/5/17.*/
public class ToorBarListViewActivity extends AppCompatActivity implements View.OnTouchListener {private Toolbar mToolBar;private ListView mLv;private SimpleBaseAdapter mAdapter;private List<String> datas = new ArrayList<>();//系统默认的滑动最小偏移量private int mTouchSlop;//手指初次触摸时的Y坐标private float mFirstY;//当前手指触摸的Y坐标private float mCurrentY;//手指移动的方向,0代表向下滑动,1代表向上滑动private int direction;//toobar的显示状态private boolean isShow = true;//toobar显示和隐藏的动画private Animator mAnimator;@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_listview_toobar);// 把toolbar替代ActionBarmToolBar = ((Toolbar) findViewById(R.id.toolbar));setSupportActionBar(mToolBar);mLv = ((ListView) findViewById(R.id.listview_toolbar_lv));initDatas();mAdapter = new SimpleBaseAdapter(datas);mLv.setAdapter(mAdapter);/*** 添加一个头部View,不然使用RelativeLayout,会被遮挡*/addHeadView();//最小移动距离,判断是否滑动mTouchSlop = ViewConfiguration.get(this).getScaledTouchSlop();//设置触摸监听mLv.setOnTouchListener(this);}/*** 添加头布局,避免被遮挡*/private void addHeadView() {//创建一个与ToolBar等高的空白view,添加到ListView的头布局中View head = new View(this);TypedArray actionbarSizeTypedArray = getApplicationContext().obtainStyledAttributes(new int[] { android.R.attr.actionBarSize });head.setLayoutParams(new AbsListView.LayoutParams(AbsListView.LayoutParams.MATCH_PARENT, (int) actionbarSizeTypedArray.getDimension(0, 0)));mLv.addHeaderView(head);}private void initDatas() {for(int i = 0;i<20;i++){datas.add("添加数据~~"+i);}}@Overridepublic boolean onTouch(View v, MotionEvent event) {switch (event.getAction()){case MotionEvent.ACTION_DOWN://记录初次触摸的Y坐标mFirstY = event.getY();break;case MotionEvent.ACTION_MOVE:mCurrentY = event.getY();if(mCurrentY-mFirstY>mTouchSlop){//向下滑动direction = 0;}else if(mFirstY-mCurrentY>mTouchSlop){//向上滑动direction = 1;}if(direction==1){//向上滑动,且toolbar在显示状态,则隐藏if(isShow){toolbarAnim(false);isShow = !isShow;}}else if(direction==0){//向下滑动,且toolbar在隐藏状态,则显示if(!isShow){toolbarAnim(true);isShow = !isShow;}}break;}return false;}public void toolbarAnim(boolean isShow){if(mAnimator!=null&&mAnimator.isRunning()){mAnimator.cancel();}if(isShow){//显现toobarmAnimator = ObjectAnimator.ofFloat(mToolBar,"translationY",mToolBar.getTranslationY(),0);}else{//隐藏toobarmAnimator = ObjectAnimator.ofFloat(mToolBar,"translationY",mToolBar.getTranslationY(),-mToolBar.getHeight());}//启动动画mAnimator.start();}
}
代码注释非常详细,需要注意的有以下三点:
因为我们的父布局是
RelativeLayout
,如果不给ListView
设置一个headView
,则在最顶部时,ListView
顶部数据会被隐藏。当然,可能会想到为什么不用垂直布局,把ListView
放在toolbar
的下面呢,这样会导致另一个问题,即当toobar
消失时,会导致顶部有一个空白效果。如下图所示:
监听两个条件,滑动方向和
toolbar
当前显示的状态,如果不监听当前toobar
状态,会导致动画重复调用。会存在一种情况,及
toolbar
处在某一个动画的时候,突然该表方向,我们需要先停止当前动画,并以当前偏移量上为基础,创建新的动画。这也是创建动画时使用mToolBar.getTranslationY()
作为参数的原因。
ListView 进阶–聊天列表的实现
在显示数据中,可能会有这种需求,显示的列表的布局不同,例如聊天界面,根据不同的聊天人,显示的位置不同,及加载不同的itemView
那么我们如何实现呢。先看一下实现效果:
请忽略他的丑陋,目的是为了实现效果,下面直接上源码,看一下实现过程,最后在总结实现的关键点:
聊天的实体类,在聊天实体类中,我们必须通过类型区分该聊天内容是自己还是朋友。
/*** 聊天的实体类* Created by Alex_MaHao on 2016/5/18.*/
public class ChatBean {/*** 聊天的两种类型,自己和朋友*/public static final int CHAT_MYSELF = 0;public static final int CHAT_FIRENDS = 1;/*** 聊天的头像*/private Drawable userIcon;/*** 发送的消息*/private String message;/*** 确定是否为当前聊天者*/private int type;public ChatBean() {}public ChatBean(String message, int type, Drawable userIcon) {this.message = message;this.type = type;this.userIcon = userIcon;}public String getMessage() {return message;}public void setMessage(String message) {this.message = message;}public int getType() {return type;}public void setType(int type) {this.type = type;}public Drawable getUserIcon() {return userIcon;}public void setUserIcon(Drawable userIcon) {this.userIcon = userIcon;}
}
ListView
所在的布局文件activity_listview_chat.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"android:orientation="vertical" android:layout_width="match_parent"android:layout_height="match_parent"><ListView
android:id="@+id/listview_chat_lv"android:divider="#0000"android:listSelector="#0000"android:dividerHeight="10dp"android:layout_width="match_parent"android:layout_height="match_parent"android:layout_marginBottom="20dp"/></RelativeLayout>
因为聊天条目没有点击反馈效果,所以通过android:listSelector
设置无反馈效果。使用android:divider="#0000"
和android:dividerHeight="10dp"
设置每一个聊天条目的间隔。
我们需要加载不同的布局,所以我们有两个item
布局文件,用来显示自己的消息和朋友的消息:
item_listview_chat_left
,显示在左边,朋友消息显示布局
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"android:orientation="vertical" android:layout_width="match_parent"android:layout_height="match_parent"><ImageView
android:layout_alignParentRight="true"android:layout_marginRight="20dp"android:id="@+id/chat_right_icon"android:src="@mipmap/qq"android:scaleType="fitXY"android:layout_width="40dp"android:layout_height="40dp" /><TextView
android:id="@+id/chat_right_msg"android:gravity="center"android:text="dsadsadsasdasdasd"android:textColor="#fff"android:layout_marginLeft="10dp"android:background="@drawable/listview_chat_right"android:layout_toLeftOf="@id/chat_right_icon"android:layout_width="wrap_content"android:layout_height="wrap_content" /></RelativeLayout>
item_listview_chat_right
,显示在右边,自己消息的显示布局
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"android:orientation="vertical" android:layout_width="match_parent"android:layout_height="match_parent"><ImageView
android:layout_alignParentRight="true"android:layout_marginRight="20dp"android:id="@+id/chat_right_icon"android:src="@mipmap/qq"android:scaleType="fitXY"android:layout_width="40dp"android:layout_height="40dp" /><TextView
android:id="@+id/chat_right_msg"android:gravity="center"android:text="dsadsadsasdasdasd"android:textColor="#fff"android:layout_marginLeft="10dp"android:background="@drawable/listview_chat_right"android:layout_toLeftOf="@id/chat_right_icon"android:layout_width="wrap_content"android:layout_height="wrap_content" /></RelativeLayout>
关键点来了,构造ChatAdapter
,用以显示数据。
/*** 聊天的Adapter,实现加载不同的布局* Created by Alex_MaHao on 2016/5/18.*/
public class ChatAdapter extends BaseAdapter {List<ChatBean> chatBeens ;public ChatAdapter(List<ChatBean> chatBeens) {this.chatBeens = chatBeens;}@Overridepublic int getCount() {return chatBeens.size();}@Overridepublic Object getItem(int position) {return null;}@Overridepublic long getItemId(int position) {return 0;}@Overridepublic int getItemViewType(int position) {//返回值需要从0开始,对应数据所要加载的对应布局标识return chatBeens.get(position).getType();}@Overridepublic int getViewTypeCount() {//不同布局的种类数量return 2;}@Overridepublic View getView(int position, View convertView, ViewGroup parent) {//获取当前数据的类型int type = getItemViewType(position);ViewHolder vHolder = null;if(convertView==null){switch (type){case ChatBean.CHAT_MYSELF://加载自己消息显示的布局convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_listview_chat_right,parent,false);vHolder = new ViewHolder();vHolder.icon = ((ImageView) convertView.findViewById(R.id.chat_right_icon));vHolder.msg = ((TextView) convertView.findViewById(R.id.chat_right_msg));break;case ChatBean.CHAT_FIRENDS://加载朋友消息显示的布局convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_listview_chat_left,parent,false);vHolder = new ViewHolder();vHolder.icon = ((ImageView) convertView.findViewById(R.id.chat_left_icon));vHolder.msg = ((TextView) convertView.findViewById(R.id.chat_left_msg));break;}//控件应用与控件绑定convertView.setTag(vHolder);}else{//获取控件引用vHolder = (ViewHolder) convertView.getTag();}//设置数值vHolder.icon.setImageDrawable(chatBeens.get(position).getUserIcon());vHolder.msg.setText(chatBeens.get(position).getMessage());return convertView;}class ViewHolder{ImageView icon;TextView msg;}
}
在adapter
中有两个关键的方法:
getViewTypeCount()
:返回布局的种类数,比如当前我们分为自己的消息和朋友的消息两个布局,那么就返回2.
-getItemViewType()
:返回对应条目下,item
布局的类型。ListView
通过该方法,实现在getView()
中返回相同类型的converView
复用。注意,该返回类型要从0开始,不然会数组越界。
因为在这里,比较巧合,我们的item
布局控件都一样,只不过显示不一样,所以只定义了一个ViewHolder
类。
关键的实现了,那么看一下activity
中的代码吧
public class ChatListViewActivity extends AppCompatActivity {private ListView mChatLv;private List<ChatBean> chatBeanList = new ArrayList<>();private ChatAdapter adapter;@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_listview_chat);mChatLv = ((ListView) findViewById(R.id.listview_chat_lv));initChatBeanList();adapter = new ChatAdapter(chatBeanList);mChatLv.setAdapter(adapter);mChatLv.postDelayed(new Runnable() {@Overridepublic void run() {//设置我们ListView显示在底部mChatLv.setSelection(adapter.getCount());}},200);}@Overrideprotected void onStart() {super.onStart();}/*** 初始化聊天内容*/private void initChatBeanList() {for (int i = 0;i<10;i++){ChatBean chat = new ChatBean();if(i%2==0){chat.setType(ChatBean.CHAT_MYSELF);chat.setUserIcon(getApplicationContext().getResources().getDrawable(R.mipmap.qq));chat.setMessage("微信,微信,我是QQ");}else{chat.setType(ChatBean.CHAT_FIRENDS);chat.setUserIcon(getApplicationContext().getResources().getDrawable(R.mipmap.weixin));chat.setMessage("QQ,QQ,我是微信");}chatBeanList.add(chat);}}
}
实现该布局的两个关键方法(adapter中的方法)
-getViewTypeCount()
:返回布局的种类数,比如当前我们分为自己的消息和朋友的消息两个布局,那么就返回2.
-getItemViewType()
:返回对应条目下,item
布局的类型。ListView
通过该方法,实现在getView()
中返回相同类型的converView
复用。注意,该返回类型要从0开始,不然会数组越界。
ListView 的上拉加载和下拉刷新
决定使用多种方式实现下拉刷新和上拉加载,会在下一篇博客中实现。绝对满满的干货。
ListView上拉加载和下拉刷新多种实现方式已经发表,欢迎观看,学习,并留下您宝贵的意见。
该项目源码已经共享到我的github,在模块
systemwidgetdemo
中。