最近對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(不日即將開啓,歡迎大家訪問)