在我们的项目中,并不是所有列表都是简单的使用。类似于新闻列表、QQ聊天列表等,具有图文并排的列表,每个item都有它不同的布局类型,都有其不同的实现方式。这类复杂的列表表现形式,在各类知名应用当中,不可或缺的存在着。今天我们就来看看,使用ListView能不能实现此类复杂布局。
今天的实例为众所周知的QQ聊天界面:
一、纵观全局
1、一个ListView。
2、圆形头像和消息框。
3、时间分隔行。
二、技术选型
1、列表当然选择ListView,我们今天使用的就是这个。
2、圆形头像,目前三方开源的比较多,有CircleImageView、AvatarImageView等可以选择,今天我就选择AvatarImageView。为什么要选择这个,它是我常用的控件之一,只是最近使用它的频率比较高,就选择它了。后续我会有专门贴子说明圆形头像控件的选择。
3、文本框+layout组合成消息框。文本框显示消息内容,layout显示消息背景泡泡。
4、item的实现有几种方法:
1)写一个布局xml文件。里面include三个xml布局,一是自己发送消息,消息显示在右边的布局。二是对方发消息,消息显示在界面左边的布局。三是时间布局,显示在中间,并起到分隔作用。然后根据不同的消息类型,控制布局的显示与隐藏。
2)根据数据时间变化,以及不同消息的类型。直接构造不同的布局文件。这样就比较轻量级。不会在同一个布局当中,存在许多无用的布局。
3)在此我选择第一种方式,因为ListView有所限制,使用第二种方式不是特别方便。第二种方式我会使用后续的RecyclerView来讲解。
三、码代码
1、在上一篇listview简单用法当中,我们使用的viewholder,是集成在adapter当中进行优化的。另外,在这里我引入另外一个解耦合比较高的ViewHolder的写法。
另外新建一个专门的ViewHolder类,在里面实现静态的getViewHolder方法,比较简单,只是将内容移了个地方而已,直接看代码:
package oliver.zhantao.oliverproject.listviewhigh;
import android.view.View;
import android.view.ViewStub;
import android.widget.TextView;
import cn.carbs.android.avatarimageview.library.AvatarImageView;
import oliver.zhantao.oliverproject.R;
/**
* ViewHolder的抽离,单独形成一个类,可以让代码更加明了
* 另外,也可以使adapter更加轻量级,降低耦合性。
* <p>
* 将ViewHolder的实现方法完全封装在其本身的类中,功能更强大
* 其方法与Adapter的getView()方法隔离,条理更加清晰,降低耦合性
* 当getView()中需要实现多种样式时,不需要写重复代码
* <p>
* Created by ZhanTao on 2017/4/19.
*/
public class HighViewHolder {
public View mineView;
public View otherView;
public View timeView;
public AvatarImageView mineView_head;
public AvatarImageView otherView_head;
public TextView mineView_message;
public TextView otherView_message;
public TextView timeView_time;
// 构造函数中就初始化View
public HighViewHolder(View convertView) {
mineView = convertView.findViewById(R.id.view_mine);
otherView = convertView.findViewById(R.id.view_other);
timeView = convertView.findViewById(R.id.view_time);
mineView_head = (AvatarImageView) mineView.findViewById(R.id.aiv_head);
otherView_head = (AvatarImageView) otherView.findViewById(R.id.aiv_head);
mineView_message = (TextView) mineView.findViewById(R.id.tv_message);
otherView_message = (TextView) otherView.findViewById(R.id.tv_message);
timeView_time = (TextView) timeView.findViewById(R.id.tv_time);
}
// 得到一个ViewHolder
public static HighViewHolder getViewHolder(View convertView) {
HighViewHolder viewHolder = (HighViewHolder) convertView.getTag();
if (viewHolder == null) {
viewHolder = new HighViewHolder(convertView);
convertView.setTag(viewHolder);
}
return viewHolder;
}
}
2、再来看我们的adapter,在这个adapter当中,直接通过静态方法就可以获取ViewHolder实例,是不是简单明了多了。注意看getView方法。
package oliver.zhantao.oliverproject.listviewhigh;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import java.util.List;
import oliver.zhantao.oliverproject.R;
/**
* 聊天列表适配器,在这里将数据与控件进行绑定
* <p>
* 项目中都是自定义适配器,很少用到系统提供的简易适配器。 所以果断抛弃系统的简易适配器,自定义想咋样就咋样,一个字,爽^_^
* <p>
* Created by ZhanTao on 2017/4/17.
*/
public class ListViewHighAdapter extends BaseAdapter {
private Context mContext;
// 餐厅列表
private List<MessageBean> messageList;
private LayoutInflater mInflater;
public ListViewHighAdapter(Context context, List<MessageBean> messageList) {
this.mContext = context;
this.messageList = messageList;
mInflater = LayoutInflater.from(context);
}
@Override
public int getCount() {
return messageList.size();
}
@Override
public Object getItem(int position) {
return position;
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public int getViewTypeCount() {
// 已经知道有三种布局了,直接写死。当然也可以做成一个可扩展的,项目中灵活性更强。
return 3;
}
@Override
public int getItemViewType(int position) {
return messageList.get(position).getLayoutType();
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
HighViewHolder holder;
// 1、优化框架搭起
if (convertView == null) {
convertView = mInflater.inflate(R.layout.activity_listview_high_item, null);
}
//这样使用Holder,简单明了,还不占用adapter
holder = HighViewHolder.getViewHolder(convertView);
// 2、绑定数据到控件
bindViewData(position, convertView, holder);
// 3、控制VIEW的显示与隐藏
controlViewVisible(position, holder);
// 4、绑定监听事件
bindViewClickListener(position, convertView, holder);
return convertView;
}
/**
* 绑定数据到控件,在这里做统一处理,方便数据抽离。
*
* @param position 列表索引
* @param convertView 每个position对应的itemView
* @param holder holder对象
*/
private void bindViewData(int position, View convertView, HighViewHolder holder) {
switch (getItemViewType(position)) {
case MessageBean.LAYOUT_TYPE_MINE:
holder.mineView_head.setImageResource(R.drawable.qq_head_mine);
holder.mineView_message.setText(messageList.get(position).getMsg());
break;
case MessageBean.LAYOUT_TYPE_OTHER:
holder.otherView_head.setImageResource(R.drawable.qq_head_other);
holder.otherView_message.setText(messageList.get(position).getMsg());
break;
case MessageBean.LAYOUT_TYPE_TIME:
holder.timeView_time.setText(messageList.get(position).getMsg());
break;
}
}
/**
* 控制每个页面的显示与隐藏,根据item类型显示需要的布局。
*
* @param position 列表索引
* @param holder holder对象
*/
private void controlViewVisible(int position, HighViewHolder holder) {
switch (getItemViewType(position)) {
case MessageBean.LAYOUT_TYPE_MINE:
holder.mineView.setVisibility(View.VISIBLE);
holder.otherView.setVisibility(View.GONE);
holder.timeView.setVisibility(View.GONE);
break;
case MessageBean.LAYOUT_TYPE_OTHER:
holder.mineView.setVisibility(View.GONE);
holder.otherView.setVisibility(View.VISIBLE);
holder.timeView.setVisibility(View.GONE);
break;
case MessageBean.LAYOUT_TYPE_TIME:
holder.mineView.setVisibility(View.GONE);
holder.otherView.setVisibility(View.GONE);
holder.timeView.setVisibility(View.VISIBLE);
break;
}
}
/**
* 绑定控件的各种View的事件,在这里做统一处理,比如点击事件,长安事件。
*
* @param position 列表索引
* @param convertView 每个position对应的itemView
* @param holder holder对象
*/
private void bindViewClickListener(int position, View convertView, HighViewHolder holder) {
}
}
3、上面只是为了展示另外一种ViewHolder的写法和用法,当然也贴也许多本例当中复杂列表的实现代码,这也是为了简单。以下我们再来分析一下这种复杂布局的实现。
首先,我们要在Adapter当中重写如下两个方法,这两个方法用于标识我们的列表当中,有几种不同的布局。在这里我们有三种布局。
其次,这个类型,我们需要在消息当中存储起来,当我们在getView当中,绘制布局的时候,就需要明确的知道,我们使用的是哪种布局,那么就需要通过数据当中的layoutType来判断。
@Override
public int getViewTypeCount() {
// 已经知道有三种布局了,直接写死。当然也可以做成一个可扩展的,项目中灵活性更强。
return 3;
}
@Override
public int getItemViewType(int position) {
return messageList.get(position).getLayoutType();
}
4、数据实体类
package oliver.zhantao.oliverproject.listviewhigh;
import java.io.Serializable;
/**
* 消息数据对象
*
* Created by ZhanTao on 2017/4/28.
*/
public class MessageBean implements Serializable {
//常量,用于标识布局。
public static final int LAYOUT_TYPE_MINE = 0;
public static final int LAYOUT_TYPE_OTHER = 1;
public static final int LAYOUT_TYPE_TIME = 2;
//消息内容
private String msg;
//头像URL
private String headUrl;
//消息发送时间
private long time;
//标识布局类型
private int layoutType;
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public String getHeadUrl() {
return headUrl;
}
public void setHeadUrl(String headUrl) {
this.headUrl = headUrl;
}
public long getTime() {
return time;
}
public void setTime(long time) {
this.time = time;
}
public int getLayoutType() {
return layoutType;
}
public void setLayoutType(int layoutType) {
this.layoutType = layoutType;
}
}
5、下面我们来查看布局activity_listview_high_item.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/dp_10"
android:paddingTop="@dimen/dp_10">
<include
android:id="@+id/view_mine"
android:layout_width="match_parent"
android:layout_height="wrap_content"
layout="@layout/activity_listview_high_item_mine" />
<include
android:id="@+id/view_other"
android:layout_width="match_parent"
android:layout_height="wrap_content"
layout="@layout/activity_listview_high_item_other" />
<include
android:id="@+id/view_time"
android:layout_width="match_parent"
android:layout_height="wrap_content"
layout="@layout/activity_listview_high_item_time" />
</RelativeLayout>
6、自己发送消息的布局。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<cn.carbs.android.avatarimageview.library.AvatarImageView
android:id="@+id/aiv_head"
android:layout_width="@dimen/dp_40"
android:layout_height="@dimen/dp_40"
android:layout_marginRight="@dimen/dp_16"
android:layout_alignParentRight="true" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/dp_80"
android:layout_toLeftOf="@id/aiv_head"
android:background="@drawable/bg_mine"
android:paddingTop="@dimen/dp_13"
android:paddingBottom="@dimen/dp_13"
android:paddingLeft="@dimen/dp_13"
android:paddingRight="@dimen/dp_13"
android:orientation="vertical">
<TextView
android:id="@+id/tv_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/color_000000"
android:gravity="right"
android:text=""
android:textSize="@dimen/dp_14" />
</LinearLayout>
</RelativeLayout>
7、对方发送消息的布局。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<cn.carbs.android.avatarimageview.library.AvatarImageView
android:id="@+id/aiv_head"
android:layout_width="@dimen/dp_40"
android:layout_height="@dimen/dp_40"
android:layout_marginLeft="@dimen/dp_16" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginRight="@dimen/dp_80"
android:layout_toRightOf="@id/aiv_head"
android:background="@drawable/bg_other"
android:paddingTop="@dimen/dp_13"
android:paddingBottom="@dimen/dp_13"
android:paddingLeft="@dimen/dp_13"
android:paddingRight="@dimen/dp_13"
android:orientation="vertical">
<TextView
android:id="@+id/tv_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/color_000000"
android:text=""
android:textSize="@dimen/dp_14" />
</LinearLayout>
</RelativeLayout>
8、时间分隔布局。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/dp_10"
android:paddingTop="@dimen/dp_10">
<TextView
android:id="@+id/tv_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text=""
android:textColor="@color/transparent_45"
android:textSize="@dimen/dp_12" />
</RelativeLayout>
9、以上布局,具体的实现方式,直接看代码, 我就不赘述了,下面贴Activity的代码。
package oliver.zhantao.oliverproject.listviewhigh;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.widget.ListView;
import java.util.ArrayList;
import java.util.List;
import oliver.zhantao.oliverproject.R;
import oliver.zhantao.oliverproject.constants.Constants;
import oliver.zhantao.oliverproject.listview.RestaurantBean;
/**
* 实战项目中简单的列表实现,自定义适配器。
* 仿写百度地图当中,收藏夹里面,收藏点的列表实现。
* 一切脱离实战的实例,都是耍流氓。
* <p>
* Created by ZhanTao on 2017/4/14.
*/
public class ListViewHighActivity extends AppCompatActivity {
private ListView listViewHigh;
private ListViewHighAdapter listViewHighAdapter;
private List<MessageBean> datas = null;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_listview_high);
datas = new ArrayList<>();
//1、找到布局当中的列表控件
listViewHigh = (ListView) findViewById(R.id.lv_resturant);
//2、创建适配器对象, 上下文必须传(非常有用)、数据必须传
listViewHighAdapter = new ListViewHighAdapter(ListViewHighActivity.this, datas);
//3、将适配器与listview进行绑定
listViewHigh.setAdapter(listViewHighAdapter);
//4、构造数据、刷新列表
getDatasFromNetwork();
}
/**
* 数据封装,模拟从网络上获取到的数据
*
* @return 返回数据列表
*/
private List<MessageBean> getDatasFromNetwork() {
MessageBean messageBean = new MessageBean();
messageBean.setMsg("哪个喊她穿高根鞋去追公交啊");
//这里可以放网络图片,在此我就不放了,我直接放了两张头像图片在工程当中
//adapter里我就直接使用了。
messageBean.setHeadUrl("");
messageBean.setTime(System.currentTimeMillis());
messageBean.setLayoutType(MessageBean.LAYOUT_TYPE_MINE);
datas.add(messageBean);
messageBean = new MessageBean();
messageBean.setMsg("死神到了,逃不脱");
messageBean.setTime(System.currentTimeMillis());
messageBean.setLayoutType(MessageBean.LAYOUT_TYPE_MINE);
datas.add(messageBean);
messageBean = new MessageBean();
messageBean.setMsg("看来你是不看新闻的");
messageBean.setTime(System.currentTimeMillis());
messageBean.setLayoutType(MessageBean.LAYOUT_TYPE_OTHER);
datas.add(messageBean);
messageBean = new MessageBean();
//这里为什么要加360000,怎么来的?6分钟*60秒*1000毫秒=360000
//我们在这里模拟超过5分钟的消息,要显示一个时间。要注意有一个布局是时间。
messageBean.setTime(System.currentTimeMillis() + 360000);
messageBean.setLayoutType(MessageBean.LAYOUT_TYPE_TIME);
//在这里处理时间,如果这条消息时间跟上一条时间相隔5分钟以上,就显示一个时间View。
//我这里是做得比较简单,原版时间算法,显示规则更加复杂些。
if(messageBean.getTime() - datas.get(datas.size() -1 ).getTime() > 300000){
//处理时间
messageBean.setMsg("下午2:41");
}
datas.add(messageBean);
messageBean = new MessageBean();
messageBean.setMsg("博越油耗相当高啊。12L");
messageBean.setTime(System.currentTimeMillis());
messageBean.setLayoutType(MessageBean.LAYOUT_TYPE_MINE);
datas.add(messageBean);
messageBean = new MessageBean();
messageBean.setMsg("百公里");
messageBean.setTime(System.currentTimeMillis());
messageBean.setLayoutType(MessageBean.LAYOUT_TYPE_MINE);
datas.add(messageBean);
messageBean = new MessageBean();
messageBean.setMsg("你才知道啊");
messageBean.setTime(System.currentTimeMillis());
messageBean.setLayoutType(MessageBean.LAYOUT_TYPE_OTHER);
datas.add(messageBean);
messageBean = new MessageBean();
messageBean.setMsg("手动的大概9L");
messageBean.setTime(System.currentTimeMillis());
messageBean.setLayoutType(MessageBean.LAYOUT_TYPE_OTHER);
datas.add(messageBean);
messageBean = new MessageBean();
messageBean.setMsg("自动的最小10L");
messageBean.setTime(System.currentTimeMillis());
messageBean.setLayoutType(MessageBean.LAYOUT_TYPE_OTHER);
datas.add(messageBean);
//刷新列表,如果是真实的网络数据,则放到请求回调函数当中使用。
//注意如果是在异步线程中,应该怎么使用?它必须在主线程中执行!!
listViewHighAdapter.notifyDataSetChanged();
return datas;
}
}
10、activity_listview_high.xml
<?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="oliver.zhantao.oliverproject.listview.ListViewSimpleActivity">
<ListView
android:id="@+id/lv_resturant"
android:layout_width="0dp"
android:layout_height="0dp"
android:listSelector="@drawable/listviewsimple_bg_selector"
android:divider="@null"
android:background="@color/color_ECEDF1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
下面我们来看看实际的运行效果,背景泡泡是我从网上扒的,太丑了。在项目中,有UI专门制作的。
以上,完了。代码贴完就完了。如有问题或好的建议希望大家留言,感激。
作者:Amir
博客:http://blog.youkuaiyun.com/amir_zt/
以上原创,转载请注明出处,谢谢。
http://blog.youkuaiyun.com/amir_zt/article/details/72547935