效果图
布局效果图
LeftSlideDeleteListView代码,只有一个文件,使用和普通ListView没有区别
package com.example.slidedelete;
import android.animation.ObjectAnimator;
import android.annotation.SuppressLint;
import android.content.Context;
import android.database.DataSetObserver;
import android.graphics.Color;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.TextView;
/**
*
* @author young
*</br>
*左划删除ListView,调用setOnListViewItemDeleteClikListener为LeftSlideDeleteListView对象添加删除监听
*</br></br>
*基本思路:</br>
* 重写setAdapter方法,拦截用户的adapter,为该adapter设置代理adapter,用于拦截getView方法,拦截后调用用户adapter的getView方法得到用户的View,为该
*View包裹支持左划的SlideContainer后返回该SlideContainer。adapter中其他方法不拦截,直接返回用户adapter中对应方法的值
*</br>参考:http://blog.csdn.net/qingchunweiliang/article/details/46969895中第一种方法</br>
*http://blog.csdn.net/qingchunweiliang/article/details/47284789
*/
public class LeftSlideDeleteListView extends ListView {
/**
* 用户使用setAdapter传入的adapter
*/
private ListAdapter targetAdapter;
private OnListViewItemDeleteClikListener deleteListener;
/**
* 动画持续时间(毫秒)
*/
private final int SLIDE_ANIM_TIME=300;
private OnItemClickListener itemClickListener;
private OnScrollListener scrollListener;
public interface OnListViewItemDeleteClikListener{
public void onListViewItemDeleteClick(int position) ;
}
/**
* 重写该方法,拦截itemclick事件处理对象,点击SlideContainer后触发itemClick事件
*/
@Override
public void setOnItemClickListener(android.widget.AdapterView.OnItemClickListener listener) {
this.itemClickListener=listener;
}
public void setOnListViewItemDeleteClikListener(OnListViewItemDeleteClikListener listener) {
deleteListener=listener;
}
/**
* 点击删除按钮后交给该函数处理
* @param v 删除按钮
*/
private void delectClick(View v) {
int first=getFirstVisiblePosition();
int last=getLastVisiblePosition();
/*
* 查找点击的删除按钮所在的position。
* 每次滚动时会把getView中传入的position设为SlideContainer中删除按钮的tag
*/
for (int i = 0; i <= last-first; i++) {
/*
* SlideContainer的结构详见图片。
* 根据删除按钮的tag查找删除按钮所在的SlideContainer,找到后让其子View复位
*/
SlideContainer sc=(SlideContainer) getChildAt(i);
ViewGroup deleteContainer=(ViewGroup) sc.getChildAt(0);
Integer positionTag=(Integer)deleteContainer.getChildAt(0) .getTag();
if (positionTag==v.getTag()) {//v就是触发点击事件的按钮
//找到删除按钮所在的SlideContainer,让其下标为1的子View(用户定义的View)复位到初始位置(0位置)
sc.getChildAt(1).setTranslationX(0);
break;
}
}
//调用该方法通知用户要删除的位置
deleteListener.onListViewItemDeleteClick((Integer) v.getTag());
}
/**
* listView中直接操作的其实是ProxyAdapter。
* 拦截用户的adapter,为其adapter设置代理,拦截getView方法。
*
*/
@Override
public void setAdapter(ListAdapter adapter) {
targetAdapter=adapter;//记录用户的adapter
super.setAdapter(new ProxyAdapter());//listView中直接操作的其实是ProxyAdapter,
}
public LeftSlideDeleteListView(Context context) {
super(context );
init();
}
public LeftSlideDeleteListView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public LeftSlideDeleteListView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
@SuppressLint("NewApi")
public LeftSlideDeleteListView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
/**
* 重写该方法,拦截滚动事件,需要在onScrollStateChanged把控件复位
*/
@Override
public void setOnScrollListener(OnScrollListener l) {
scrollListener=l;
}
private void init() {
//监听滚动开始事件,开始滚动时把所有控件复位,由于重写过setOnScrollListener,所以要调用super.setOnScrollListener添加滚动监听
super.setOnScrollListener(new OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
// TODO Auto-generated method stub
if (scrollState==1) {
int first =getFirstVisiblePosition();
int last=getLastVisiblePosition();
for (int i = 0; i <= last-first; i++) {
SlideContainer sc=(SlideContainer) getChildAt(i);
sc.getChildAt(1).setTranslationX(0);
}
}
if (scrollListener!=null) {
/*
* 若用户调用了setOnScrollListener(LeftSlideDeleteListView已经重写过setOnScrollListener方法)
* 为ListView添加滚动监听,则需要在这调用用户的onScrollStateChanged方法把事件交给用户去处理
*/
scrollListener.onScrollStateChanged(view, scrollState);
}
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
if (scrollListener!=null) {
//同样也要交给用户去处理
scrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount);
}
}
});
}
/**
*
* @author young
*</br>
*SlideContainer继承自FrameLayout,用于包裹用户的Listview中的每一个条目View,每一个条目View对应一个SlideContainer。
*SlideContainer具有左滑移动的功能。SlideContainer中下面是一个LinearLayout,用于包裹删除按钮,SlideContainer中上面就是用户的View
*
*该SlideContainer不需要用户调用,当用户设置adapter后会自动为用户View外面包裹SlideContainer
*/
private class SlideContainer extends FrameLayout {
private int dis;
private TextView delete;//删除按钮
public SlideContainer(Context context ,View topChild) {
super(context);
//获取肉眼可见的最小移动距离
dis=ViewConfiguration.get(getContext()).getScaledTouchSlop();
//用于包裹删除按钮
LinearLayout bottomContainer=new LinearLayout(context);
//居右,竖直居中
bottomContainer.setGravity(Gravity.RIGHT|Gravity.CENTER_VERTICAL);
delete=new TextView(context);
delete.setText("删除");
delete.setBackgroundColor(Color.RED);
delete.setGravity(Gravity.CENTER);
int padding=dip2px(getContext(), 30);
delete.setPadding(padding, 0, padding, 0);
delete.setTextColor(Color.WHITE);
LinearLayout.LayoutParams params=new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.MATCH_PARENT);
delete.setLayoutParams(params);
bottomContainer.addView(delete);
//为删除按钮添加点击事件
delete.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
delectClick(v);
}
});
/*
* 为每个条目添加点击事件。
* 若不处理会导致用户设置的setOnItemClickListener无效
*/
setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
int first=getFirstVisiblePosition();
int last=getLastVisiblePosition();
//同样是根据tag查找位置
for (int i = 0; i <= last-first; i++) {
SlideContainer sc=(SlideContainer) LeftSlideDeleteListView.this.getChildAt(i);
ViewGroup deleteContainer=(ViewGroup) sc.getChildAt(0);//获取到包裹删除按钮的LinearLayout
Integer positionTag=(Integer)deleteContainer.getChildAt(0) .getTag();//获取删除按钮的tag
if (positionTag==v.getTag()) {//如果点击的按钮v的tag和positionTag则找到了位置
if (itemClickListener!=null) {
itemClickListener.onItemClick(null, null, i+first, 0);
}
return;
}
}
}
});
//先添加删除按钮所在的布局,让其位于底部
addView(bottomContainer);
//再添加用户的控件,覆盖在删除按钮上部
addView(topChild);
}
private int dip2px(Context context, float dpValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
public void setPositionTag(int position) {
delete.setTag(position);
}
private float downX;
private float downY;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
downX=ev.getX();
downY=ev.getY();
break;
case MotionEvent.ACTION_MOVE:
float moveX=ev.getX();
float moveY=ev.getY();
float dx= Math.abs(moveX-downX);
float dy=Math.abs(moveY-downY);
if (dx>dis&&dx>dy) {
return true;//拦截到事件,交给自己的onTouchEvent处理,同时不再向下传递给子控件
}
}
return super.onInterceptTouchEvent(ev);//对事件放行,交给子控件处理
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
float moveX=ev.getX();
float moveY=ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_MOVE:
float dx= Math.abs(moveX-downX);
float dy=Math.abs(moveY-downY);
if (dx>dis&&dx>dy) {
//不让ListView响应事件,否则会出现左上或左下划时Listview跟着上下移动
getParent().requestDisallowInterceptTouchEvent(true);
if (moveX<downX) {
getChildAt(1).setTranslationX(moveX-downX);
}else
getChildAt(1).setTranslationX(0);
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
//若控件未移动并且手指移动距离小于dis,则视为是itemClick事件,调用performClick后回触发该控件的点击事件,在点击事件内部触发itemClick事件
if (Math.abs(moveX-downX)<dis&& Math.abs(moveY-downY)<dis&&
Math.abs(getChildAt(1).getTranslationX())<dis) {
performClick();
}
//如果超过了删除按钮宽度,抬起手指时就把用户View向左移动删除按钮宽度的距离
if (moveX<downX&& downX-moveX>delete.getWidth() ) {
translationTopView(getChildAt(1), getChildAt(1).getTranslationX(), -delete.getWidth());
}else{
translationTopView(getChildAt(1), getChildAt(1).getTranslationX(), 0);
}
break;
}
return true;
}
/**
* 把控件v以属性动画形式从start移动到end。 属性动画中不管怎么移动,只要调用setTranslationX(0),控件就会回到原来位置。
* 属性动画没有累加效果,即:调用setTranslationX(x1) 后再调用setTranslationX(x2)结果和setTranslationX(x1+x2)不一样
* @param v
* @param start
* @param end
*/
private void translationTopView(View v,float start,float end){
ObjectAnimator animator=ObjectAnimator.ofFloat(v, "translationX", start,end);
animator.setDuration(SLIDE_ANIM_TIME);
animator.start();
}
}
///
/**
*
* @author young
*</br></br>
*代理adapter,拦截getView方法,为用户View包裹SlideContainer父布局。
*ListView直接操作的是该ProxyAdapter,再由ProxyAdapter转发到对应方法处理
*/
private class ProxyAdapter implements ListAdapter{
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView==null) {//null说明之前没缓存过(第一屏显示的View都是调用该方法得到的)
View v=targetAdapter.getView(position, convertView, parent);//调用用户adapter的getView方法得到用户布局
SlideContainer sc= new SlideContainer(getContext(), v);//为用户布局包裹支持左划的控件
sc.setPositionTag(position);//为删除按钮设置tag。由于listview中控件是复用的,所以每次getView时都需要修改tag值,通过tag值可以找到该删除按钮对应的位置
sc.setTag(position);//同样设置tag,用于把找到SlideContainer所在位置,转发给onItemClick方法处理item点击事件
return sc;
}else{
ViewGroup vg=(ViewGroup) convertView;
// vg.getChildAt(1)获取的是用户定义的View,所以调用用户adapter的getView时要把用户View传进去
View v=targetAdapter.getView(position, vg.getChildAt(1), parent);
SlideContainer sc;
if (v!=vg.getChildAt(1)) {//判断用户的getView是否使用了缓存的view(部分用户可能每次getView中都创建新的控件)
sc= new SlideContainer(getContext(), v);//用户并没有使用缓存控件而是新创建的控件,所以要包裹SlideContainer
}else
sc=(SlideContainer) convertView;
sc.setPositionTag(position);//设置tag
sc.setTag(position);
return sc;
}
}
/*下面的方法不需要拦截,直接返回用户adapter对应方法的值或调用用户adapter对应方法即可
*/
@Override
public void registerDataSetObserver(DataSetObserver observer) {
targetAdapter.registerDataSetObserver(observer);
}
@Override
public void unregisterDataSetObserver(DataSetObserver observer) {
targetAdapter.unregisterDataSetObserver(observer);
}
@Override
public int getCount() {
return targetAdapter.getCount();
}
@Override
public Object getItem(int position) {
return targetAdapter.getItem(position);
}
@Override
public long getItemId(int position) {
return targetAdapter.getItemId(position);
}
@Override
public boolean hasStableIds() {
return targetAdapter.hasStableIds();
}
@Override
public int getItemViewType(int position) {
return targetAdapter.getItemViewType(position);
}
@Override
public int getViewTypeCount() {
return targetAdapter.getViewTypeCount();
}
@Override
public boolean isEmpty() {
return targetAdapter.isEmpty();
}
@Override
public boolean areAllItemsEnabled() {
return targetAdapter.areAllItemsEnabled();
}
@Override
public boolean isEnabled(int position) {
return targetAdapter.isEnabled(position);
}
}
}
调用方式
package com.example.slidedelete;
import android.support.v7.app.ActionBarActivity;
import java.util.ArrayList;
import java.util.List;
import com.example.slidedelete.LeftSlideDeleteListView.OnListViewItemDeleteClikListener;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.AbsListView.OnScrollListener;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.BaseAdapter;
import android.widget.TextView;
import android.widget.Toast;
public class MainActivity extends ActionBarActivity {
List<String>data;
LeftSlideDeleteListView listView;
Adapter adapter;
Toast toast;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
toast=Toast.makeText(getApplicationContext(), "", 0);
data=new ArrayList<String>();
for (int i = 0; i < 50; i++) {
data.add(""+i);
}
listView=new LeftSlideDeleteListView(getApplicationContext());
setContentView(listView);
adapter=new Adapter();
listView.setAdapter(adapter);
listView.setOnListViewItemDeleteClikListener(new OnListViewItemDeleteClikListener() {
@Override
public void onListViewItemDeleteClick(int position) {
// TODO Auto-generated method stub
System.out.println(position);
toast.setText("删除位置:"+ position+"");
toast.show();
data.remove(position);
adapter.notifyDataSetChanged();
}
});
listView.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
// TODO Auto-generated method stub
toast.setText("item click 位置:"+position);
toast.show();
}
});
// listView.setOnScrollListener(new OnScrollListener() {
//
// @Override
// public void onScrollStateChanged(AbsListView view, int scrollState) {
// // TODO Auto-generated method stub
// System.out.println("onScrollStateChanged...");
// }
//
// @Override
// public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
// // TODO Auto-generated method stub
// System.out.println("onScroll...");
// }
// });
}
class Adapter extends BaseAdapter {
class ViewHolder{
TextView index;
TextView data;
TextView click;
}
ViewHolder holder;
//不管是否使用缓存布局都支持左划删除
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// TODO Auto-generated method stub
if (convertView==null) {
holder=new ViewHolder();
/*
* LayoutInflater中第二个参数是布局的父控件,若为null则布局的padding属性不起作用,
* 第三个参数若是true,则该方法内部会调用父控件的addView方法把布局加入到其中,并返回父布局。由于ListView不允许使用addView添加控件,所以会抛异常。
* 若是false则不调用addView
*/
convertView=LayoutInflater.from(getApplicationContext()).inflate(R.layout.item, listView, false);
holder.index=(TextView) convertView.findViewById(R.id.index);
holder.data=(TextView) convertView.findViewById(R.id.data);
holder.click=(TextView) convertView.findViewById(R.id.click);
holder.click.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
toast.setText("item button click。。。");
toast.show();
}
});
convertView.setTag(holder);
}
holder=(ViewHolder) convertView.getTag();
holder.index.setText(""+position);
holder.data.setText(data.get(position)+"");
return convertView;
// View v=LayoutInflater.from(getApplicationContext()).inflate(R.layout.item, listView, false);
// ((TextView)v.findViewById(R.id.index)).setText(""+position);
// ((TextView)v.findViewById(R.id.data)).setText(""+data.get(position));
//
//
// ((TextView)v.findViewById(R.id.click)).setOnClickListener(new OnClickListener() {
//
// @Override
// public void onClick(View v) {
// // TODO Auto-generated method stub
// Toast.makeText(getApplicationContext(), "click...", 0).show();
// }
// });
//
// return v;
}
@Override
public long getItemId(int position) {
// TODO Auto-generated method stub
return 0;
}
@Override
public Object getItem(int position) {
// TODO Auto-generated method stub
return null;
}
@Override
public int getCount() {
// TODO Auto-generated method stub
return data.size();
}
}
}
用户Item布局
<?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:background="#eee"
android:padding="20dp"
android:orientation="horizontal" >
<TextView
android:id="@+id/index"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.0"
android:padding="15dp"
android:text="x"
android:textColor="#000" />
<TextView
android:id="@+id/data"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.0"
android:padding="15dp"
android:text="x"
android:gravity="center"
android:textColor="#000" />
<TextView
android:id="@+id/click"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.0"
android:background="#888"
android:padding="15dp"
android:text="Click"
android:textColor="#000" />
</LinearLayout>