说起ListView,就不得不说iOS的UITableView,毫不夸张的说的,放在3年前,如果你去面试的时候,你说你会用UITableView,知道UITableView的代理方法,不用说了,你可以直接来上班了。

ListView在Android中开发的重要性不言而喻,学好ListView,我想在以后的列表开发中就不用发愁了。什么是列表开发?这么说吧,在你使用的APP中,80%的都会用到列表开发,比如微信的聊天页,QQ的个人空间页。为何要用列表开发,列表开发的优越在哪里?我今天来一探究竟。

ListView初体验

我们先来写个小例子,根据这个小例子我们再做进一步的介绍。我们先在xml中创建一个ListView,这里我直接使用ConstraintLayout约束布局。

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.guiyongdong.listviewdemo2.MainActivity">
<ListView
android:layout_width="368dp"
android:layout_height="495dp"
app:layout_constraintTop_toTopOf="parent"
android:layout_marginTop="0dp"
android:layout_marginLeft="0dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginBottom="0dp"
android:layout_marginRight="0dp"
app:layout_constraintRight_toRightOf="parent"
android:id="@+id/listView"/>
</android.support.constraint.ConstraintLayout>

MainActivity的代码:

public class MainActivity extends AppCompatActivity {
private ListView mListView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mListView = (ListView) findViewById(R.id.listView);
String[] data = {"A","b","c","d","A","b","c","d","A","b","c","d","A","b","c","d","A","b","c","d","A","b","c","d"};
ArrayAdapter<String> adapter = new ArrayAdapter<String>(MainActivity.this,android.R.layout.simple_list_item_1,data);
mListView.setAdapter(adapter);
}
}

运行如下:



ListView

在Android所有常用的原生控件当中,用法最复杂的应该就是ListView了,它专门用于处理那种内容元素很多,手机屏幕无法展示出所有内容的情况。ListView可以使用列表的形式来展示内容,超出屏幕部分的内容只需要通过手指滑动就可以移动到屏幕内了。

我们先看看ListView的继承体系:



属性
  • android:divider 在列表条目之间显示的图片或者颜色(drawable或color)
  • android:dividerHeight 用来指定divider的高度
  • android:scrollbars 设置滚动条状态,不需要滚动条时,设置为none
  • android:listSelector 设置条目选中后的颜色,可设置为#00000000或者@android:color/transparent 取消选中色
  • android:footerDividersEnabled 当设置为false时,ListView将不会在各个footer之间绘制divider,默认为true
  • android:headerDividersEnabled 当设为false时,ListView将不会在各个header之间绘制divider,默认为true

其他继承父类的属性就不说了。

方法
  • void addFooterView(View v) 添加一个固定在列表底部的View
  • boolean removeFooterView(View v) 删除一个之前添加的FooterView,参数为欲删除的视图,返回是否删除成功
  • void addHeaderView(View v) 添加一个固定在列表顶部的View
  • boolean removeHeaderView(View v) 删除一个之前添加的HeaderView,参数为欲删除的视图,返回是否删除成功
  • void setAdapter(ListAdapter adapter) 为ListView绑定Adapter
  • ListAdapter getAdapter() 返回ListView正在使用的Adapter
  • void setEmptyView(View emptyView) 当数据的个数为0的时候显示一个提示视图

通过上面一个简单的例子我们可以看出,ListView如果想要显示数据,必须需要一个Adapter来适配。Android为什么这么设计呢?如果学习过iOS的同学都知道,我们在使用UITableView的时候,一定会实现它的数据源代理方法,在代理方法中我们会返回条目数和条目View。这种设计方法有效的分离了UITableView和数据源的直接打交道,让数据源的显示交于用户来选择。Android也是如此。

ListView只承担交互和展示工作,至于这些数据来源于哪里,ListView并不关心。于是就有了Adapter这样一个机制的出现。Adapter是适配器的意思,它在ListView和数据源之间起到了一个桥梁的作用,ListView会借助Adapter这个桥梁去访问真正的数据源,因为Adapter的接口都是统一的,因此我们可以通过实现接口来定制各种类型的Adapter。另外系统也为我们实现了一些常用的Adapter,比如我们上面用到的ArrayAdapter等。

Adapter

我们先来看看继承体系:



Adapter定义的抽象函数主要包括:

  • void registerDataSetObserver(DataSetObserver observer) 添加数据源变化的observer,如增加、删除等将会执行
  • void unregisterDataSetObserver(DataSetObserver observer) 取消注册的observer
  • int getCount() 显示有多少个数据项 即adapter有多少个条目
  • Object getItem(int position) 返回数据集中position位置所对应的数据项
  • long getItemId(int position) 返回position位置所对应的ID号,通常即为position
  • View getView(int position, View convertView, ViewGroup parent) 核心函数,返回position数据项对应的条目View

上个示例我们使用的ArrayAdapter,他只能用来显示TextView,如果我们想显示更多的不同种类的条目,我们需要继承BaseAdapter,并重写相关方法,我们现在来看看如何重写。

先上示例图:



我们新建一个Dog类,有nameimageId两个成员变量,分别表示狗的名字和图片资源(这里使用本地图片):

public class Dog extends Object {
private String name;
private int imageId;
public Dog(String name, int imageId) {
this.name = name;
this.imageId = imageId;
}
public String getName() {
return name;
}
public int getImageId() {
return imageId;
}
}

我们在新建一个DogAdapter继承于ArrayAdapter,因为我们这里的数据不复杂,直接继承ArrayAdapter,如果我们的数据是一个更复杂的嵌套很深的模型,我们可以直接继承BaseAdapter。

public class DogAdapter extends ArrayAdapter<Dog> {
private int resourceId;
public DogAdapter(Context context, int resourceId, List<Dog> dogList) {
super(context, resourceId, dogList);
// 记录当前布局资源
this.resourceId = resourceId;
}
@NonNull
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
Dog dog = getItem(position);//获取当前Dog实例
View view;
ViewHolder viewHolder;//tag缓存
if (convertView == null){
view = LayoutInflater.from(getContext()).inflate(resourceId,parent,false);
viewHolder = new ViewHolder();
viewHolder.mImageView = (ImageView) view.findViewById(R.id.dogImage);
viewHolder.mTextView = (TextView) view.findViewById(R.id.dogName);
view.setTag(viewHolder);//设置tag绑定
}else {
view = convertView;
viewHolder = (ViewHolder) view.getTag();
}
ImageView imageView = viewHolder.mImageView;
TextView textView = viewHolder.mTextView;
imageView.setImageResource(dog.getImageId());
textView.setText(dog.getName());
return view;
}
class ViewHolder {
ImageView mImageView;
TextView mTextView;
}
}

注意:我们都知道ListView的强大,它强大就强大在无论我们设置多少条数据源,ListView都不会完全的把这些条目都创建,而是通过复用已经消失在屏幕的条目来展示新的条目。
getView函数中,有个convertView参数,如果它不为空,就表示ListView的缓存池中有可复用的条目,我们直接取来用就行。而且我们还创建了一个内部类ViewHolder,声明了两个属性mImageViewmTextView,我们可以给View设置tag,方便下次给View赋值的时候,不需要再次调用findViewById方法来重新查找属性。

再来看我们如何使用DogAdapter:

public class MainActivity extends AppCompatActivity {
private ListView mListView;
private ArrayList<Dog> mDogArrayList;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//初始化数据源
initDogArrayList();
mListView = (ListView) findViewById(R.id.listView);
DogAdapter dogAdapter = new DogAdapter(this,R.layout.dog_item,mDogArrayList);
mListView.setAdapter(dogAdapter);
}
//模拟数据
private void initDogArrayList() {
mDogArrayList = new ArrayList<Dog>();
for (int i=0; i<30; i++){
Dog dog = new Dog("小狗"+i,R.drawable.dog);
mDogArrayList.add(dog);
}
}
}

其他方法

触摸监听 OnTouchListener
mListView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
//触摸时操作
break;
case MotionEvent.ACTION_MOVE:
//移动是操作
break;
case MotionEvent.ACTION_UP:
//手指离开时操作
break;
}
return false;
}
});
滑动监听 OnScrollListener
//滑动监听
mListView.setOnScrollListener(new AbsListView.OnScrollListener() {
//滚动状态发生改变
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
switch (scrollState){
case SCROLL_STATE_IDLE:
//停止滑动
Log.d("gg","停止滑动了");
break;
case SCROLL_STATE_TOUCH_SCROLL:
//正在滚动
Log.d("gg","正在滑动了");
break;
case SCROLL_STATE_FLING:
//手指快速滑动,手指离开屏幕后由于惯性继续滑动
Log.d("gg","惯性滑动了");
break;
default:
break;
}
}
//滚动时一直调用
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
Log.d("gg","混动了");
}
});
条目点击 OnItemClickListener
mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Log.d("gg","点击了第"+position+"条目");
}
});

仿微信聊天界面

说了这么多,再来做一个例子,仿一下微信的聊天界面。

先看布局,这里依旧使用约束布局:

ListView布局
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.guiyongdong.listviewdemo2.WXChatActivity">
<ListView
android:id="@+id/wx_listView"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_marginBottom="0dp"
app:layout_constraintBottom_toTopOf="@+id/wx_send"/>
<Button
android:id="@+id/wx_send"
android:layout_width="wrap_content"
android:layout_height="44dp"
android:layout_marginBottom="0dp"
android:layout_marginRight="0dp"
android:text="发送"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent"/>
<EditText
android:id="@+id/wx_editText"
android:layout_width="0dp"
android:layout_height="44dp"
android:layout_marginBottom="0dp"
android:layout_marginLeft="0dp"
android:layout_marginRight="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="0.501"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@+id/wx_send"
android:hint="说些什么吧"
android:maxLines="1"/>
</android.support.constraint.ConstraintLayout>
item布局

这里我们把两种布局都定义在一个xml文件中,稍后会根据代码来决定隐藏哪种类型。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp">
<!--好友的信息-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/wx_item_left">
<ImageView
android:layout_width="60dp"
android:layout_height="60dp"
android:src="@drawable/left_item_image"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="你打我试试"
android:background="@drawable/message_left"
android:layout_marginLeft="5dp"
android:gravity="left|center"
android:id="@+id/wx_item_left_textView"/>
</LinearLayout>
<!--我的消息-->
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/wx_item_right"
android:orientation="horizontal">
<ImageView
android:layout_width="60dp"
android:layout_height="60dp"
android:src="@drawable/right_item_imgae"
android:layout_alignParentRight="true"
android:id="@+id/wx_item_right_image"/>
<TextView
android:id="@+id/wx_item_right_textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="打你就打你"
android:background="@drawable/message_right"
android:gravity="center|left"
android:paddingLeft="8dp"
android:paddingRight="25dp"
android:layout_toLeftOf="@+id/wx_item_right_image"
/>
</RelativeLayout>
</LinearLayout>
消息实体 MSG类

我们再创建一个消息实体,来存储消息信息。这里我们定义了两种消息类型,TYPE_RECEIVED表示接收的消息,TYPE_SENT表示发送的消息。

public class WXMsg extends Object {
public static final int TYPE_RECEIVED = 0; //接收类型
public static final int TYPE_SENT = 1; //发送类型
private String content;
private int type;
public WXMsg(String content, int type){
this.content = content;
this.type = type;
}
public String getContent() {
return content;
}
public int getType() {
return type;
}
}
消息适配器 WXMsgAdapter

重点来了,我们会根据消息的类型来决定显示哪种布局方式。

public class WXAdapter extends ArrayAdapter {
private int resourceId;
public WXAdapter(Context context, int resourceId, List<WXMsg> msgArrayList) {
super(context, resourceId, msgArrayList);
this.resourceId = resourceId;
}
@NonNull
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
View view;
WXViewHolder wxViewHolder;
WXMsg msg = (WXMsg) getItem(position);
if (convertView == null){
view = LayoutInflater.from(getContext()).inflate(resourceId,null);
wxViewHolder = new WXViewHolder(view);
view.setTag(wxViewHolder);
}else {
view = convertView;
wxViewHolder = (WXViewHolder) view.getTag();
}
if (msg.getType() == WXMsg.TYPE_RECEIVED) {
//如果是收到的消息 显示左边的消息布局,隐藏右边的消息布局
wxViewHolder.rightLinearLayout.setVisibility(View.GONE);
wxViewHolder.leftLinearLayout.setVisibility(View.VISIBLE);
wxViewHolder.leftMsg.setText(msg.getContent());
}else if (msg.getType() == WXMsg.TYPE_SENT) {
//如果是发送的消息 显示右边的消息布局,隐藏左边的消息布局
wxViewHolder.rightLinearLayout.setVisibility(View.VISIBLE);
wxViewHolder.leftLinearLayout.setVisibility(View.GONE);
wxViewHolder.rightMsg.setText(msg.getContent());
}
return view;
}
class WXViewHolder {
LinearLayout leftLinearLayout;
RelativeLayout rightLinearLayout;
TextView leftMsg;
TextView rightMsg;
public WXViewHolder(View view){
leftLinearLayout = (LinearLayout) view.findViewById(R.id.wx_item_left);
rightLinearLayout = (RelativeLayout) view.findViewById(R.id.wx_item_right);
leftMsg = (TextView) view.findViewById(R.id.wx_item_left_textView);
rightMsg = (TextView) view.findViewById(R.id.wx_item_right_textView);
}
}
}

我们在WXChatActivity中这样用:

public class WXChatActivity extends AppCompatActivity {
private ListView mListView;
private Button mSendButton;
private EditText mEditText;
private ArrayList<WXMsg> mWXMsgArrayList;
private WXAdapter mWXAdapter;
//模拟一个回复消息池
private final String[] allmsgArray = {"你信不信我打你!","现在的年轻人一言不合就斗图~","看过一千多部岛国成人片,从人到动物,大妈到熟妇,御姐到萝莉,会一千多种姿势,上百种插法,告诉你,不要惹我,不然你怎么怀孕的都不知道"
,"你放学别走!","不服是不是 不服来打我啊!"};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_wxchat);
mListView = (ListView) findViewById(R.id.wx_listView);
mSendButton = (Button) findViewById(R.id.wx_send);
mEditText = (EditText) findViewById(R.id.wx_editText);
mWXMsgArrayList = new ArrayList<WXMsg>();
//先随机添加一条接收的消息
addReceivedMsg();
mWXAdapter = new WXAdapter(this,R.layout.wx_item,mWXMsgArrayList);
mListView.setAdapter(mWXAdapter);
//监听按钮点击
mSendButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String content = mEditText.getText().toString();
if (!"".equals(content)){
//添加发送信息
WXMsg msg = new WXMsg(content,WXMsg.TYPE_SENT);
mWXMsgArrayList.add(msg);
//添加接收信息
addReceivedMsg();
//有消息更新 刷新界面
mWXAdapter.notifyDataSetChanged();
//ListView滚动到最后一行
mListView.smoothScrollToPosition(mWXMsgArrayList.size()-1);
//清空输入框
mEditText.setText("");
}
}
});
}
//随机添加接收消息
public void addReceivedMsg(){
int index = new Random().nextInt(allmsgArray.length);
String content = allmsgArray[index];
WXMsg msg = new WXMsg(content,WXMsg.TYPE_RECEIVED);
mWXMsgArrayList.add(msg);
}
}

运行结果如下:



小结

写完微信的小例子,心里的成就感还是很强的,ListView的强大还远不止如此,因为我知道iOS的UITableView的重要性,相信在以后的开发中我会经常和ListView打交道的。