Android中仿QQ左滑刪除

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章