最近对View的可定制化十分感兴趣,发现网络上的资源很少能够有实现QQ最新左滑策略的demo,自己想着做一个仿QQ左滑控件温习一下View的onTouchEvent事件处理机制。
目的:在ListView中添加删除、置顶和已未读按钮,功能比较简单,希望有需要的小伙伴直接可用,扩展功能自己去添加,有些需要注意的地方我会标注出来,节省大家一些宝贵的时间。
实现步骤:
1.实现一个简单的ListView控件
2.ListView的Item中根据用户类型来添加不同的按钮,并且将按钮的点击事件回调到Activity界面中
实现效果如下视频:
左滑删除ListView录屏
实现代码:
首先是自定义ListView的代码:
package com.example.qqlistviewdemo;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.animation.LinearInterpolator;
import android.widget.Button;
import android.widget.ListView;
import android.widget.Scroller;
import java.util.List;
/**
* 自定义ListView Item中可以左滑删除
*/
public class CustomListView extends ListView {
private static final String TAG = "CustomListView";
private Context mContext;
/**
* 设置的最小滑动距离
*/
private int touchSlop;
/**
* 记录是否点击事件的标识
*/
private boolean isPerformClick;
/**
* 上次手指按下点的x坐标
*/
private int lastX;
/**
* 上次手指按下点的y坐标
*/
private int lastY;
/**
* 当前手指按下的item的view
*/
private View mCurrentView;
/**
* 当前手指按压的item的position
*/
private int currentPosition;
/**
* 记录item是否已经开始滑动
*/
private boolean isStartScroll;
private Scroller mScroller;
/**
* 当前item是否正在滑动打开删除和指定按钮
*/
private boolean isCurrentItemMoving;
private boolean isDragging;
/**
* 指定按钮和删除按钮的宽度
*/
private int mMaxLength;
/**
* 删除和指定menu的状态:0 关闭;1 将要关闭;2 打开;3 将要打开
*/
private int menuStatus = 0;
private static final int MENU_CLOSE = 0;
private static final int MENU_WILL_CLOSE = 1;
private static final int MENU_OPEN = 2;
private static final int MENU_WILL_OPEN = 3;
private OnItemActionListener mListener;
/**
* 显示按钮的个数 0 一个;1 两个; 2 三个。对应用户类型
*/
private static final int ONE_BUTTON = 0;
private static final int TWO_BUTTONS = 1;
private static final int THREE_BUTTONS = 2;
private List<QQBean> mDatas;
public CustomListView(Context context) {
super(context);
mContext = context;
initView();
}
public CustomListView(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
initView();
}
public CustomListView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mContext = context;
initView();
}
public void initView() {
mScroller = new Scroller(mContext, new LinearInterpolator());
touchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop();
}
@Override
public void computeScroll() {
super.computeScroll();
//判断scroller是否完成滑动
if (mScroller.computeScrollOffset()) {
mCurrentView.scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
//这个很重要
invalidate();
//如果已经完成就改变状态
} else if (isStartScroll) {
isStartScroll = false;
if (menuStatus == MENU_WILL_CLOSE) {
menuStatus = MENU_CLOSE;
}
if (menuStatus == MENU_WILL_OPEN) {
menuStatus = MENU_OPEN;
}
}
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
int action = ev.getAction();
int x = (int) ev.getX();//触摸点到屏幕左边缘的距离 越往右值越大
int y = (int) ev.getY();//触摸点到view控件上边界的距离 越往下值越大
switch (action) {
case MotionEvent.ACTION_DOWN:
if (menuStatus == MENU_CLOSE) {//如果删除按钮是关闭状态 正常逻辑 也就是点击事件
//根据坐标点所在获取当前item的布局
//获取当前按下的点所在item的位置 也就是list中的position
currentPosition = pointToPosition(x, y);
mCurrentView = getChildAt(currentPosition - getFirstVisiblePosition());//使用当前按下点所在的position-最上面显示的item的position
//初始化需要显示的内容
Button btnDelete = mCurrentView.findViewById(R.id.btn_delete);
Button btnTop = mCurrentView.findViewById(R.id.btn_top);
Button btnRead = mCurrentView.findViewById(R.id.btn_read);
switch (mDatas.get(currentPosition).getUserType()) {
case ONE_BUTTON:
mMaxLength = btnDelete.getWidth();//删除按钮的宽度
btnTop.setVisibility(GONE);
btnRead.setVisibility(GONE);
break;
case TWO_BUTTONS:
mMaxLength = btnDelete.getWidth() + btnTop.getWidth();//删除按钮和置顶按钮的宽度
btnTop.setVisibility(VISIBLE);
btnRead.setVisibility(GONE);
break;
case THREE_BUTTONS:
mMaxLength = btnDelete.getWidth() + btnTop.getWidth() + btnRead.getWidth();//删除按钮、置顶和标记已读未读按钮的宽度
btnTop.setVisibility(VISIBLE);
btnRead.setVisibility(VISIBLE);
break;
}
//在不显示删除和置顶按钮的时候设置 删除和置顶按钮的点击事件
btnDelete.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mCurrentView.scrollTo(0, 0);
menuStatus = MENU_CLOSE;
if (mListener != null) {
mListener.OnItemDelete(currentPosition);
}
}
});
btnTop.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mCurrentView.scrollTo(0, 0);
menuStatus = MENU_CLOSE;
if (mListener != null) {
mListener.OnItemTop(currentPosition);
}
}
});
btnRead.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mCurrentView.scrollTo(0, 0);
menuStatus = MENU_CLOSE;
if (mListener != null) {
mListener.OnItemRead(currentPosition);
}
}
});
} else if (menuStatus == MENU_OPEN) {//如果删除按钮是打开状态 通过动画的方式滑动至删除按钮消失,并且return false
//原点(0,0)x轴坐标减去移动后的View视图左上角x轴坐标的值
mScroller.startScroll(mCurrentView.getScrollX(), 0, -mMaxLength, 0, 200);
//mCurrentView.scrollTo(0, 0);//如果用scrollTo方法那么没有一个动画效果,在用户看来会不友好
isStartScroll = true;
menuStatus = MENU_WILL_CLOSE;
invalidate();
return false;
} else {
return false;
}
break;
case MotionEvent.ACTION_MOVE:
int dx = lastX - x;
int dy = lastY - y;
//如果是上下滚动,就直接忽略这次手势
if (isDragging) {
lastX = x;
lastY = y;
return super.onTouchEvent(ev);
}
int scrollX = mCurrentView.getScrollX();//往左滑该值为正,往右滑该值为负值
if (Math.abs(dx) > Math.abs(dy)) {//表明是正在横向滑动
isCurrentItemMoving = true;
if (scrollX + dx <= 0) {//item的布局原始位置,也就是item的布局不能再向右滑动了
mCurrentView.scrollTo(0, 0);
return false;
}
if (scrollX + dx >= mMaxLength) {//item的布局已经完全显示出来删除和置顶按钮了,没必要再向左边滑动了
mCurrentView.scrollTo(mMaxLength, 0);
return false;
}
mCurrentView.scrollBy(dx, 0);
} else {
isDragging = true;
}
if (isCurrentItemMoving) {
lastX = x;
lastY = y;
return true;
}
break;
case MotionEvent.ACTION_UP:
if (!isCurrentItemMoving && !isDragging && mListener != null && Math.abs(lastX - x) < touchSlop) {//没有在移动中
//满足item的点击事件
mListener.OnItemClick(currentPosition);
}
int upScrollX = mCurrentView.getScrollX();
int deltaX = 0;
//对于滑动过程中的情况需要自动滑动到初始位置或者删除、置顶按钮显示的状态
if (upScrollX < mMaxLength / 2) {
//mCurrentView.scrollTo(0, 0);//如果用scrollTo方法那么没有一个动画效果,在用户看来会不友好
deltaX = -upScrollX;
menuStatus = MENU_WILL_CLOSE;
} else {
//mCurrentView.scrollTo(mMaxLength, 0);
deltaX = mMaxLength - upScrollX;
menuStatus = MENU_WILL_OPEN;
}
mScroller.startScroll(upScrollX, 0, deltaX, 0, 200);
isDragging = false;
isCurrentItemMoving = false;
isStartScroll = true;
//刷新界面
invalidate();
break;
}
lastX = x;
lastY = y;
return super.onTouchEvent(ev);
}
public void setActionListener(OnItemActionListener listener) {
this.mListener = listener;
}
/**
* 给左滑出来的布局设置需要显示的内容
*/
public void setData(List<QQBean> data) {
mDatas = data;
}
}
之后是MainActivity中的代码实现:
package com.example.qqlistviewdemo;
import android.os.Bundle;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private CustomListView mCustomListView;
private List<String> mNameDatas;
private List<QQBean> qqDataBeans;
private CustomListViewAdapter adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mCustomListView = findViewById(R.id.listView);
mNameDatas = new ArrayList<>(Arrays.asList("zhangsan", "lisi", "wangwu", "zhaoliu", "tianqi", "heiba", "qinjiu", "hushi", "ashiyi", "bshier", "cshisan", "dshisi", "eshiwu", "fshiliu", "gshiqi", "hshiba", "ishijiu", "jershi"));
adapter = new CustomListViewAdapter(this);
mCustomListView.setActionListener(new OnItemActionListener() {
@Override
public void OnItemClick(int position) {
Toast.makeText(MainActivity.this, "点击了第" + position + "条item", Toast.LENGTH_SHORT).show();
}
@Override
public void OnItemTop(int position) {
QQBean qqBean = qqDataBeans.get(position);
if (qqBean.isTop()) {
Collections.swap(qqDataBeans, position, qqBean.getOldPosition());
} else {
Collections.swap(qqDataBeans, position, 0);
}
qqBean.setTop(!qqBean.isTop());
adapter.setData(qqDataBeans);
}
@Override
public void OnItemRead(int position) {
QQBean qqBean = qqDataBeans.get(position);
if (qqBean.isRead()) {
//TODO 取消显示消息条数的图标,此处不做赘述
Toast.makeText(MainActivity.this, "标为未读", Toast.LENGTH_SHORT).show();
} else {
//TODO 显示消息条数的图标,此处不做赘述
Toast.makeText(MainActivity.this, "标为已读", Toast.LENGTH_SHORT).show();
}
qqBean.setRead(!qqBean.isRead());
adapter.setData(qqDataBeans);
}
@Override
public void OnItemDelete(int position) {
qqDataBeans.remove(position);
//更改oldPosition的值
for (int i = 0; i < qqDataBeans.size(); i++) {
qqDataBeans.get(i).setOldPosition(i);
}
adapter.setData(qqDataBeans);
}
});
mCustomListView.setAdapter(adapter);
//造一些假数据
qqDataBeans = new ArrayList<>();
QQBean bean = null;
for (int i = 0; i < mNameDatas.size(); i++) {
bean = new QQBean();
bean.setName(mNameDatas.get(i));
bean.setContent("消息内容" + i);
if (i == 0) {
bean.setDate("2020-03-31" + (i < 10 && i > 0 ? "0" + i : ""));
} else {
bean.setDate("2020-04-" + (i < 10 ? "0" + i : i));
}
if (i % 3 == 0) {
bean.setUserType(CustomListViewAdapter.NORMAL_USER);
}
if (i % 3 == 1) {
bean.setUserType(CustomListViewAdapter.GROUP_USER);
}
if (i % 3 == 2) {
bean.setUserType(CustomListViewAdapter.COMPANY_USER);
}
bean.setOldPosition(i);
bean.setTop(false);
bean.setDelete(false);
bean.setRead(false);
qqDataBeans.add(bean);
}
mCustomListView.setData(qqDataBeans);
adapter.setData(qqDataBeans);
}
}
删除按钮等点击事件的回调接口OnItemActionListener代码:
package com.example.qqlistviewdemo;
/**
* 点击事件的回调接口
*/
public interface OnItemActionListener {
void OnItemClick(int position);
void OnItemTop(int position);
void OnItemRead(int position);
void OnItemDelete(int position);
}
activity_main.xml中的代码:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
tools:context=".MainActivity">
<com.example.qqlistviewdemo.CustomListView
android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
ListView的adapter代码CustomListViewAdapter:
package com.example.qqlistviewdemo;
import android.content.Context;
import android.graphics.Color;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;
/**
* 自定义ListView的 仿QQ左滑删除列表的adapter
*/
public class CustomListViewAdapter extends BaseAdapter {
public static final int NORMAL_USER = 0;//普通用户
public static final int GROUP_USER = 1;//QQ群
public static final int COMPANY_USER = 2;//企业号
private Context mContext;
private List<QQBean> mDatas;
public CustomListViewAdapter(Context context) {
mContext = context;
mDatas = new ArrayList<>();
}
@Override
public int getCount() {
return mDatas.size();
}
@Override
public Object getItem(int position) {
return mDatas.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder = null;
if (convertView == null) {
holder = new ViewHolder();
convertView = LayoutInflater.from(mContext).inflate(R.layout.item_custom_listview, null);
holder.btnDelete = convertView.findViewById(R.id.btn_delete);
holder.btnTop = convertView.findViewById(R.id.btn_top);
holder.btnRead = convertView.findViewById(R.id.btn_read);
holder.tvName = convertView.findViewById(R.id.tv_name);
holder.tvContent = convertView.findViewById(R.id.tv_content);
holder.tvDate = convertView.findViewById(R.id.tv_date);
holder.ivIcon = convertView.findViewById(R.id.iv_icon);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
QQBean bean = mDatas.get(position);
//如果是获取的在线数据,最好判空
holder.tvName.setText(bean.getName());
holder.tvContent.setText(bean.getContent());
holder.tvDate.setText(bean.getDate());
holder.btnDelete.setText("删除");
if (bean.isTop()) {
convertView.setBackgroundColor(Color.GRAY);
holder.btnTop.setText("取消置顶");
} else {
convertView.setBackgroundColor(Color.WHITE);
holder.btnTop.setText("置顶聊天");
}
if (bean.isRead()) {
holder.btnRead.setText("标为未读");
} else {
holder.btnRead.setText("标为已读");
}
holder.ivIcon.setImageResource(R.mipmap.ic_launcher_round);
return convertView;
}
public void setData(List<QQBean> data) {
mDatas.clear();
mDatas.addAll(data);
notifyDataSetChanged();
}
static class ViewHolder {
TextView tvName;
TextView tvContent;
TextView tvDate;
ImageView ivIcon;
Button btnDelete;
Button btnTop;
Button btnRead;
}
}
adapter的item布局item_custom_listview.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/linear_show"
android:layout_width="match_parent"
android:layout_height="50dp"
android:orientation="horizontal"
android:paddingStart="5dp">
<ImageView
android:id="@+id/iv_icon"
android:layout_width="50dp"
android:layout_height="50dp"
android:contentDescription="@null"
android:scaleType="centerInside" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:orientation="vertical"
android:paddingStart="5dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="25dp"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_name"
android:layout_width="0dp"
android:layout_height="25dp"
android:layout_weight="3"
android:gravity="center_vertical"
android:text="name"
android:textColor="#000"
android:textSize="20sp" />
<TextView
android:id="@+id/tv_date"
android:layout_width="0dp"
android:layout_height="25dp"
android:layout_weight="1"
android:gravity="center"
android:text="2020-04-17"
android:textSize="14sp" />
</LinearLayout>
<TextView
android:id="@+id/tv_content"
android:layout_width="match_parent"
android:layout_height="25dp"
android:gravity="center_vertical"
android:text="content"
android:textSize="14sp" />
</LinearLayout>
</LinearLayout>
<Button
android:id="@+id/btn_top"
android:layout_width="80dp"
android:layout_height="50dp"
android:background="#999"
android:text="置顶聊天"
android:textColor="#fff" />
<Button
android:id="@+id/btn_read"
android:layout_width="80dp"
android:layout_height="50dp"
android:background="#f80"
android:text="标为已读"
android:textColor="#fff" />
<Button
android:id="@+id/btn_delete"
android:layout_width="80dp"
android:layout_height="50dp"
android:background="#f00"
android:text="删除"
android:textColor="#fff" />
</LinearLayout>
bean类代码QQBean:
package com.example.qqlistviewdemo;
public class QQBean {
private boolean isTop;//是否置顶
private boolean isDelete;//是否删除
private boolean isRead;//是否置顶
private int oldPosition;//记录置顶前item的position
private int messageCount;//记录消息的条数
private int userType;//用户类型 普通用户 0 显示删除、置顶和已读按钮;QQ群 1 显示删除、置顶按钮;企业号 2 显示删除按钮
private String name;//内容
private String imageUrl;//图片
private String content;//显示的内容
private String date;//日期
public boolean isTop() {
return isTop;
}
public void setTop(boolean top) {
isTop = top;
}
public boolean isDelete() {
return isDelete;
}
public void setDelete(boolean delete) {
isDelete = delete;
}
public int getOldPosition() {
return oldPosition;
}
public void setOldPosition(int oldPosition) {
this.oldPosition = oldPosition;
}
public void setRead(boolean read) {
isRead = read;
}
public int getMessageCount() {
return messageCount;
}
public void setMessageCount(int messageCount) {
this.messageCount = messageCount;
}
public int getUserType() {
return userType;
}
public void setUserType(int userType) {
this.userType = userType;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public boolean isRead() {
return isRead;
}
public String getImageUrl() {
return imageUrl;
}
public void setImageUrl(String imageUrl) {
this.imageUrl = imageUrl;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getDate() {
return date;
}
public void setDate(String date) {
this.date = date;
}
}
到此仿QQ左滑删除ListView功能基本实现,主要通过本篇文章复习一下自定义View的实现方式。
参考文章:https://www.jb51.net/article/100149.htm
github代码所在地址:https://github.com/cainiaobukeyi/QQListViewDemo
如有疑问可以微信:cai-niao-bu-ke-yi; QQ:1125325256 留言 个人网站:www.sevenyoung.net(不日即将开启,欢迎大家访问)