最近无聊,看了很多关于QQ列表侧滑删除的文章和技术文档,结合许多整理一下内容。
侧滑结构如图:
所需填充的数据由“文本”和“删除”2部分组成。代码如下
content.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="60dp"
android:background="#97d8da"
android:gravity="center"
android:orientation="vertical">
<TextView
android:id="@+id/Content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Text"
android:gravity="center"
android:textSize="24dp"
/>
</LinearLayout>
delete.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/Delete"
android:layout_width="130dp"
android:layout_height="60dp"
android:background="#ff0000"
android:gravity="center"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="删除"
android:textSize="24dp"
android:textColor="#ffffff"
/>
</LinearLayout>
ok,既然这2个做好了,那么就到最关键的一步了。就是当我们滑动“文本”和“删除”这一个整体的时候,我们所需要注意的需要删除控件的长度,直接贴代码:
DeleteListView:
import android.content.Context;
import android.support.v4.widget.ViewDragHelper;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
public class DeleteListView extends ViewGroup{
// 内容部分
private View mContent;
// 删除部分
private View mDelete;
private ViewDragHelper viewDragHelper;
private int mContentWidth;
private int mContentHeight;
private int mDeleteWidth;
private int mDeleteHeight;
public SlideDelete(Context context) {
super(context);
}
public SlideDelete(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SlideDelete(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
private OnSlideDeleteListener onSlideDeleteListener;
public void setOnSlideDeleteListener(OnSlideDeleteListener onSlideDeleteListener){
this.onSlideDeleteListener = onSlideDeleteListener;
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mContent = getChildAt(0);
mDelete = getChildAt(1);
viewDragHelper = ViewDragHelper.create(this,new MyDrawHelper());
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 这跟mContent的父亲的大小有关,父亲是宽填充父窗体,高度是和孩子一样是60dp
// 测量内容部分的大小
mContent.measure(widthMeasureSpec,heightMeasureSpec);
LayoutParams layoutParams = mDelete.getLayoutParams();
int deleteWidth = MeasureSpec.makeMeasureSpec(
layoutParams.width, MeasureSpec.EXACTLY);
int deleteHeight = MeasureSpec.makeMeasureSpec(
layoutParams.height, MeasureSpec.EXACTLY);
// 这个参数就需要指定为精确大小
// 测量删除部分的大小
mDelete.measure(deleteWidth, deleteHeight);
setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
mContentWidth = mContent.getMeasuredWidth();
mContentHeight = mContent.getMeasuredHeight();
// 摆放内容部分的位置
mContent.layout(0, 0, mContentWidth, mContentHeight);
mDeleteWidth = mDelete.getMeasuredWidth();
mDeleteHeight = mDelete.getMeasuredHeight();
// 摆放删除部分的位置
mDelete.layout(mContentWidth, 0,mContentWidth +
mDeleteWidth, mContentHeight);
}
class MyDrawHelper extends ViewDragHelper.Callback {
/**
* Touch的down事件会回调这个方法 tryCaptureView
* @Child:指定要动的孩子 (哪个孩子需要动起来)
* @pointerId: 点的标记
* @return : ViewDragHelper是否继续分析处理 child的相关touch事件
*/
@Override
public boolean tryCaptureView(View child, int pointerId) {
System.out.println("调用tryCaptureView");
System.out.println("contentView : " + (mContent == child));
return mContent == child || mDelete == child;
}
/**
* 捕获了水平方向移动的位移数据
* @param child 移动的孩子View
* @param left 父容器的左上角到孩子View的距离
* @param dx 增量值,其实就是移动的孩子View的左上角距离控件(父亲)的距离,包含正负
*/
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
if(child == mContent){
// 解决内容部分左右拖动的越界问题
if(left>0){
return 0;
}else if(-left>mDeleteWidth){
return -mDeleteWidth;
}
}
if(child == mDelete){
// 解决删除部分左右拖动的越界问题
if(left<mContentWidth - mDeleteWidth){
return mContentWidth - mDeleteWidth;
}else if(left > mContentWidth){
return mContentWidth;
}
}
return left;
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
return super.clampViewPositionVertical(child, top, dy);
}
/**
* 当View的位置改变时的回调 这个方法的价值是结合clampViewPositionHorizontal
* 或者clampViewPositionVertical
* @param changedView 哪个View的位置改变了
* @param left changedView的left
* @param top changedView的top
* @param dx x方向的上的增量值
* @param dy y方向上的增量值
*/
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
invalidate();
if(changedView == mContent){
// 如果移动的是mContent
//我们移动mContent的实惠要相应的联动改变mDelete的位置
// 怎么改变mDelete的位置,当然是mDelete的layput方法啦
int tempDeleteLeft = mContentWidth+left;
int tempDeleteRight = mContentWidth+left + mDeleteWidth;
mDelete.layout(tempDeleteLeft,0,tempDeleteRight,mDeleteHeight);
}else{ // touch的是mDelete
int tempContentLeft = left - mContentWidth;
int tempContentRight = left;
mContent.layout(tempContentLeft,0,tempContentRight,mContentHeight);
}
}
/**
* 相当于Touch的up的事件会回调onViewReleased这个方法
* @param releasedChild
* @param xvel x方向的速率
* @param yvel y方向的速率
*/
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
// 方法的参数里面没有left,那么我们就采用 getLeft()这个方法
int mConLeft = mContent.getLeft();
// 这里没必要分来两个孩子判断
if(-mConLeft>mDeleteWidth/2){
// mDelete展示起来
isShowDelete(true);
if(onSlideDeleteListener != null){
// 调用接口打开的方法
onSlideDeleteListener.onOpen(SlideDelete.this);
}
}else{
// mDetele隐藏起来
isShowDelete(false);
if(onSlideDeleteListener != null){
// 调用接口的关闭的方法
onSlideDeleteListener.onClose(SlideDelete.this);
}
}
super.onViewReleased(releasedChild, xvel, yvel);
}
}
/**
* 是否展示delete部分
* @param isShowDelete
*/
public void isShowDelete(boolean isShowDelete){
if(isShowDelete){
//采用ViewDragHelper的 smoothSlideViewTo 方法让移动变得顺滑自然,不会太生硬
//smoothSlideViewTo只是模拟了数据,但是不会真正的动起来,动起来需要调用 invalidate
// 而 invalidate 通过调用draw()等方法之后最后还是还是会调用 computeScroll 这个方法
// 所以,使用 smoothSlideViewTo 做过渡动画需要结合 invalidate方法 和 computeScroll方法
// smoothSlideViewTo的动画执行时间没有暴露的参数可以设置,但是这个时间是google给我们经过大量计算给出合理时间
viewDragHelper.smoothSlideViewTo(mContent,-mDeleteWidth,0);
viewDragHelper.smoothSlideViewTo(mDelete,mContentWidth-mDeleteWidth,0);
}else{
//mContent.layout(0,0,mContentWidth,mContentHeight);
//mDelete.layout(mContentWidth, 0, mContentWidth + mDeleteWidth, mDeleteHeight);
viewDragHelper.smoothSlideViewTo(mContent, 0, 0);
viewDragHelper.smoothSlideViewTo(mDelete, mContentWidth, 0);
}
invalidate();
}
@Override
public void computeScroll() {
//super.computeScroll();
// 把捕获的View适当的时间移动,其实也可以理解为 smoothSlideViewTo 的模拟过程还没完成
if(viewDragHelper.continueSettling(true)){
invalidate();
}
// 其实这个动画过渡的过程大概在怎么走呢?
// 1、smoothSlideViewTo方法进行模拟数据,模拟后就就调用invalidate();
// 2、invalidate()最终调用computeScroll,computeScroll做一次细微动画,
// computeScroll判断模拟数据是否彻底完成,还没完成会再次调用invalidate
// 3、递归调用,知道数据noni完成。
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// 使用ViewDragHelper必须复写onTouchEvent并调用这个方法
viewDragHelper.processTouchEvent(event);
//消费这个touch
return true;
}
// SlideDlete的接口
public interface OnSlideDeleteListener {
void onOpen(SlideDelete slideDelete);
void onClose(SlideDelete slideDelete);
}
}
最主要的我们既然做完了,那么就可以开始剩下的工作的:实现我们需要的功能!
将之前我们写的2个layou文件include到一个item.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="match_parent"
android:orientation="vertical">
<com.example.listview_delete.DeleteListView
android:id="@+id/mSlideDelete"
android:layout_width="match_parent"
android:layout_height="60dp">
<!--文本部分-->
<include layout="@layout/slide_content"/>
<!--删除部分-->
<include layout="@layout/slide_delete"/>
</com.example.listview_delete.DeleteListView>
</LinearLayout>
然后我们在activity_main.xml中添加代码:
<RelativeLayout 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"
tools:context="com.example.listview_delete.MainActivity" >
<ListView
android:id="@+id/mLv"
android:layout_width="match_parent"
android:layout_height="match_parent"></ListView>
</RelativeLayout>
MainActivity.java:
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.BaseAdapter;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.TextView;
public class MainActivity extends Activity {
private ListView mLv;
private ArrayList<String> mData;
// 继续有多少个条目的delete被展示出来的集合
private List<DeleteListView> slideDeleteArrayList = new ArrayList<DeleteListView>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mLv = (ListView) findViewById(R.id.mLv);
mData=new ArrayList<String>();
for(int i=0;i<20;i++){
mData.add("Text"+i);
}
mLv.setAdapter(new MyAdapter());
mLv.setOnScrollListener(new AbsListView.OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
if(scrollState == SCROLL_STATE_FLING || scrollState == SCROLL_STATE_TOUCH_SCROLL){
closeOtherItem();
}
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
}
});
}
class MyAdapter extends BaseAdapter{
@Override
public int getCount() {
if(mData!=null){
return mData.size();
}
return 0;
}
@Override
public Object getItem(int position) {
if(mData!=null){
return mData.get(position);
}
return null;
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder;
if(convertView == null){
viewHolder = new ViewHolder();
convertView = View.inflate(MainActivity.this,R.layout.item,null);
viewHolder.mSlideDelete = (DeleteListView) convertView.findViewById(R.id.mSlideDelete);
viewHolder.mTvContent = (TextView) convertView.findViewById(R.id.Content);
viewHolder.mLlDelete = (LinearLayout) convertView.findViewById(R.id.Delete);
convertView.setTag(viewHolder);
}else{
viewHolder = (ViewHolder) convertView.getTag();
}
viewHolder.mTvContent.setText(mData.get(position));
viewHolder.mSlideDelete.setOnSlideDeleteListener(new DeleteListView.OnSlideDeleteListener() {
@Override
public void onOpen(DeleteListView slideDelete) {
closeOtherItem();
slideDeleteArrayList.add(slideDelete);
Log.d("Slide", "slideDeleteArrayList当前数量:" + slideDeleteArrayList.size());
}
@Override
public void onClose(DeleteListView slideDelete) {
slideDeleteArrayList.remove(slideDelete);
Log.d("Slide", "slideDeleteArrayList当前数量:" + slideDeleteArrayList.size());
}
});
viewHolder.mLlDelete.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mData.remove(position);
notifyDataSetChanged();
}
});
return convertView;
}
}
class ViewHolder{
DeleteListView mSlideDelete;
TextView mTvContent;
LinearLayout mLlDelete;
}
private void closeOtherItem(){
// 采用Iterator的原因是for是线程不安全的,迭代器是线程安全的
ListIterator<DeleteListView> slideDeleteListIterator = slideDeleteArrayList.listIterator();
while(slideDeleteListIterator.hasNext()){
DeleteListView slideDelete = slideDeleteListIterator.next();
slideDelete.isShowDelete(false);
}
slideDeleteArrayList.clear();
}
}
大致代码就像上面的一样了,此外发现一些bug,不能快速左划删除,另外,当我们左划到一定程度,可以看见删除的时候,这时你点击删除,会发现一个问题:就是你选中的被删除了,但是其下面一条则是被选择像又滑动的状态。
能力有限,只能到这里,希望大家多多指教。
由于刚刚上传源码,所以链接的话还要等,需要的可以私聊我(一般都有空。。。)
来源:CSDN
作者:程序猿-Wood
链接:https://blog.csdn.net/qq_26888925/article/details/52324649