首先看一下效果图,有个直观认识
主要功能就是ListView的item可以侧滑,出来一个删除按钮,点击delete就删除该item。
这是一个相对比较综合的例子,来看看动手之前需要准备哪些知识。
1. 对自定义View要有一定的知识基础,参看View绘制流程
2. 事件的拦截以及反拦截的相关知识,以便很好的解决事件冲突问题,关于事件机制,可以参看android事件处理机制
3. 滑动器Scroller的使用,参看Scroller简单用法
4. 自定义View中的接口回调(View状态变化时执行回调)
下面我们一步一步来实现这个功能。
item的布局文件item_slide.xml,代码如下:
<com.chm.myapplication.view.SlideLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="60dp"
android:text="content"
android:textSize="25sp"
android:background="#d7d7d7"
android:gravity="center"/>
<TextView
android:id="@+id/menu"
android:layout_width="wrap_content"
android:layout_height="60dp"
android:text="delete"
android:textSize="25sp"
android:background="#ff0000"
android:gravity="center"
android:padding="8dp"/>
com.chm.myapplication.view.SlideLayout>
其中根元素是SlideLayout.java类,这是我们自定义的一个布局类,继承自FrameLayout,代码如下:
public class SlideLayout extends FrameLayout {
private View contentView;
private View menuView;
private int viewHeight; //高是相同的
private int contentWidth;
private int menuWidth;
//滑动器
private Scroller scroller;
public SlideLayout(Context context, AttributeSet attrs) {
super(context, attrs);
scroller = new Scroller(context);
}
/**
* 布局文件加载完成时被调用
*/
@Override
protected void onFinishInflate() {
super.onFinishInflate();
cOntentView= findViewById(R.id.content);
menuView = findViewById(R.id.menu);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
viewHeight = getMeasuredHeight();
cOntentWidth= contentView.getMeasuredWidth();
menuWidth = menuView.getMeasuredWidth();
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
menuView.layout(contentWidth, 0, contentWidth+menuWidth, viewHeight);
}
private float startX;
private float startY;
private float downX;
private float downY;
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
switch (event.getAction())
{
case MotionEvent.ACTION_DOWN:
startX = event.getX();
startY = event.getY();
break;
case MotionEvent.ACTION_MOVE:
float endX = event.getX();
float endY = event.getY();
//计算偏移量
float distanceX = endX - startX;
int toScrollX = (int) (getScrollX()-distanceX);
//屏蔽非法值
if (toScrollX <0 )
{
toScrollX = 0;
}
if (toScrollX > menuWidth)
{
toScrollX = menuWidth;
}
System.out.println("toScroll-->"+toScrollX+"-->"+getScrollX());
scrollTo(toScrollX,getScrollY());
startX = event.getX();
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}
}
完成上面的代码之后,我们可以将这个布局文件(item_slide.xml)放在Activity中显示出来,可以看到这时候我们左滑每个item后可以显示出删除按钮,这个按钮之所以会显示出是因为我们已经在onLayout方法中将这个删除按钮正好放在内容View的右侧了,所以不滑动时是看不到的,只有滑动时才显示出来。
现在滑动是可以了,但是我们希望滑动距离大于删除按钮宽度一半后,手抬起时可以直接显示删除按钮,当滑动距离小于删除按钮的一半时,直接回弹将删除按钮隐藏起来,所以我们修改onTouchEvent方法如下
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
switch (event.getAction())
{
case MotionEvent.ACTION_DOWN:
downX = startX = event.getX();
downY = startY = event.getY();
break;
case MotionEvent.ACTION_MOVE:
float endX = event.getX();
float endY = event.getY();
//计算偏移量
float distanceX = endX - startX;
int toScrollX = (int) (getScrollX()-distanceX);
//屏蔽非法值
if (toScrollX <0 )
{
toScrollX = 0;
}
if (toScrollX > menuWidth)
{
toScrollX = menuWidth;
}
System.out.println("toScroll-->"+toScrollX+"-->"+getScrollX());
scrollTo(toScrollX,getScrollY());
startX = event.getX();
break;
case MotionEvent.ACTION_UP:
if (getScrollX() > menuWidth/2)
{
//打开menu
openMenu();
}else {
closeMenu();
}
break;
}
return true;
}
/**
* 打开menu菜单
*/
public void openMenu() {
int dx = menuWidth-getScrollX();
scroller.startScroll(getScrollX(), getScrollY(),dx, getScrollY());
invalidate();
}
/**
* 关闭菜单
*/
public void closeMenu() {
//0表示menu移动到的目标距离,目标位置-起始位置
int dx = 0-getScrollX();
scroller.startScroll(getScrollX(), getScrollY(),dx, getScrollY());
invalidate();
}
@Override
public void computeScroll() {
super.computeScroll();
if (scroller.computeScrollOffset())
{
scrollTo(scroller.getCurrX(), scroller.getCurrY());
invalidate();
}
}
回弹使用了Scroller滑动器,invalidate()方法执行时会调用computeScroll()方法,computeScroll()方法每次执行都回调一小段距离,scroller.computeScrollOffset()判断是否需要继续回弹,这个判断里面的invalidate()执行会导致这个过程循环执行,直到回弹结束。
将item_slide.xml放在ListView中显示,这个就比较简单了,直接上代码。
public class SlideActivity extends Activity {
private ListView listView;
private ArrayList mDatas;
private MyAdapter myAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_slide);
listView = (ListView) findViewById(R.id.main_list);
mDatas = new ArrayList<>();
for (int i = 0; i <50; i++) {
mDatas.add(new MyContent("content"+i));
}
myAdapter = new MyAdapter(this, mDatas);
listView.setAdapter(myAdapter);
}
class MyAdapter extends BaseAdapter
{
private Context content;
private ArrayList datas;
private MyAdapter(Context context, ArrayList datas)
{
this.cOntent= context;
this.datas = datas;
}
@Override
public int getCount() {
return datas.size();
}
@Override
public Object getItem(int position) {
return datas.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder=null;
if (cOnvertView== null)
{
cOnvertView= LayoutInflater.from(content).inflate(R.layout.item_slide, null);
viewHolder = new ViewHolder();
viewHolder.cOntentView= (TextView) convertView.findViewById(R.id.content);
viewHolder.menuView = (TextView) convertView.findViewById(R.id.menu);
convertView.setTag(viewHolder);
}else {
viewHolder = (ViewHolder) convertView.getTag();
}
viewHolder.contentView.setText(datas.get(position).getContent());
return convertView;
}
}
static class ViewHolder
{
public TextView contentView;
public TextView menuView;
}
}
到这里,我们将SlideLayout放入ListView中显示出来,当我们滑动item时,在同时上下滑动时发现item是不会回弹的,这是什么原因?
这就需要你对事件机制了解清楚了,你再上下滑动的时候,滑动事件已经被ListView消耗了,SlideLayout中的onTouchEvent就得不到执行了,这是需要判断(也有重新ListView的),如果在左右滑动时,SlideLayout就需要向父级ListView请求不要拦截事件,如果是上下滑动,就不需要理会了,按默认的来。
所以我们修改SlideLayout中的onTouchEvent方法如下,Move事件处代码:
case MotionEvent.ACTION_MOVE:
float endX = event.getX();
float endY = event.getY();
//计算偏移量
float distanceX = endX - startX;
int toScrollX = (int) (getScrollX()-distanceX);
//屏蔽非法值
if (toScrollX <0 )
{
toScrollX = 0;
}
if (toScrollX > menuWidth)
{
toScrollX = menuWidth;
}
System.out.println("toScroll-->"+toScrollX+"-->"+getScrollX());
scrollTo(toScrollX,getScrollY());
startX = event.getX();
float dx = Math.abs(event.getX()-downX);
float dy = Math.abs(event.getY()-downY);
if (dx > dy && dx > 6)
{
//事件反拦截,使父ListView的事件传递到自身SlideLayout
getParent().requestDisallowInterceptTouchEvent(true);
}
break;
这样我们在左右滑动item时同时上下滑动是不起作用的。
这是,我们给item添加点击事件后,发现item又不能进行滑动了,原因同样是事件被别人消耗了,这次是被SlideLayout中的TextView消耗了,这时我们同样需要判断,如果是滑动就拦截事件,如果是点击就放行。
重新onInterceptTouchEvent方法
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
switch (event.getAction())
{
case MotionEvent.ACTION_DOWN:
downX = startX = event.getX();
downY = startY = event.getY();
break;
case MotionEvent.ACTION_MOVE:
float dx = Math.abs(event.getX()-downX);
float dy = Math.abs(event.getY()-downY);
if (dx > dy && dx > 6)
{
//拦截事件
return true;
}
break;
case MotionEvent.ACTION_UP:
break;
}
return super.onInterceptTouchEvent(event);
}
通过暴露接口的方式,在Activity中设置监听器,当SlideLayout滑动时,调用相关状态的方法,来控制item删除按钮的显示和隐藏。
public interface OnStateChangeListener
{
void onOpen(SlideLayout slideLayout);
void onMove(SlideLayout slideLayout);
void onClose(SlideLayout slideLayout);
}
public OnStateChangeListener onStateChangeListener;
public void setOnStateChangeListener(OnStateChangeListener onStateChangeListener) {
this
Activity中的全部代码:
public class SlideActivity extends Activity {
private ListView listView;
private ArrayList mDatas;
private MyAdapter myAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_slide);
listView = (ListView) findViewById(R.id.main_list);
mDatas = new ArrayList<>();
for (int i = 0; i <50; i++) {
mDatas.add(new MyContent("content"+i));
}
myAdapter = new MyAdapter(this, mDatas);
listView.setAdapter(myAdapter);
}
class MyAdapter extends BaseAdapter
{
private Context content;
private ArrayList datas;
private MyAdapter(Context context, ArrayList datas)
{
this.cOntent= context;
this.datas = datas;
}
@Override
public int getCount() {
return datas.size();
}
@Override
public Object getItem(int position) {
return datas.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder=null;
if (cOnvertView== null)
{
cOnvertView= LayoutInflater.from(content).inflate(R.layout.item_slide, null);
viewHolder = new ViewHolder();
viewHolder.cOntentView= (TextView) convertView.findViewById(R.id.content);
viewHolder.menuView = (TextView) convertView.findViewById(R.id.menu);
convertView.setTag(viewHolder);
}else {
viewHolder = (ViewHolder) convertView.getTag();
}
viewHolder.contentView.setText(datas.get(position).getContent());
viewHolder.contentView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(content, "click "+((TextView)v).getText(), Toast.LENGTH_SHORT).show();
}
});
final MyContent myCOntent= datas.get(position);
viewHolder.menuView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
datas.remove(myContent);
notifyDataSetChanged();
}
});
SlideLayout slideLayout = (SlideLayout) convertView;
slideLayout.setOnStateChangeListener(new MyOnStateChangeListener());
return convertView;
}
public SlideLayout slideLayout = null;
class MyOnStateChangeListener implements SlideLayout.OnStateChangeListener
{
@Override
public void onOpen(SlideLayout layout) {
slideLayout = layout;
}
@Override
public void onMove(SlideLayout layout) {
if (slideLayout != null && slideLayout !=layout)
{
slideLayout.closeMenu();
}
}
@Override
public void onClose(SlideLayout layout) {
if (slideLayout == layout)
{
slideLayout = null;
}
}
}
}
static class ViewHolder
{
public TextView contentView;
public TextView menuView;
}
}
MyContent.java
public class MyContent {
private String content;
public MyContent(String content) {
this.cOntent= content;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.cOntent= content;
}
}
SlideLayout全部代码:
public class SlideLayout extends FrameLayout {
private View contentView;
private View menuView;
private int viewHeight; //高是相同的
private int contentWidth;
private int menuWidth;
//滑动器
private Scroller scroller;
public SlideLayout(Context context, AttributeSet attrs) {
super(context, attrs);
scroller = new Scroller(context);
}
/**
* 布局文件加载完成时被调用
*/
@Override
protected void onFinishInflate() {
super.onFinishInflate();
cOntentView= findViewById(R.id.content);
menuView = findViewById(R.id.menu);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
viewHeight = getMeasuredHeight();
cOntentWidth= contentView.getMeasuredWidth();
menuWidth = menuView.getMeasuredWidth();
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
menuView.layout(contentWidth, 0, contentWidth+menuWidth, viewHeight);
}
private float startX;
private float startY;
private float downX;
private float downY;
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
switch (event.getAction())
{
case MotionEvent.ACTION_DOWN:
downX = startX = event.getX();
downY = startY = event.getY();
break;
case MotionEvent.ACTION_MOVE:
float endX = event.getX();
float endY = event.getY();
//计算偏移量
float distanceX = endX - startX;
int toScrollX = (int) (getScrollX()-distanceX);
//屏蔽非法值
if (toScrollX <0 )
{
toScrollX = 0;
}
if (toScrollX > menuWidth)
{
toScrollX = menuWidth;
}
System.out.println("toScroll-->"+toScrollX+"-->"+getScrollX());
scrollTo(toScrollX,getScrollY());
startX = event.getX();
float dx = Math.abs(event.getX()-downX);
float dy = Math.abs(event.getY()-downY);
if (dx > dy && dx > 6)
{
//事件反拦截,使父ListView的事件传递到自身SlideLayout
getParent().requestDisallowInterceptTouchEvent(true);
}
break;
case MotionEvent.ACTION_UP:
if (getScrollX() > menuWidth/2)
{
//打开menu
openMenu();
}else {
closeMenu();
}
break;
}
return true;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
switch (event.getAction())
{
case MotionEvent.ACTION_DOWN:
downX = startX = event.getX();
downY = startY = event.getY();
if (onStateChangeListener != null)
{
onStateChangeListener.onMove(this);
}
break;
case MotionEvent.ACTION_MOVE:
float dx = Math.abs(event.getX()-downX);
float dy = Math.abs(event.getY()-downY);
if (dx > dy && dx > 6)
{
//拦截事件
return true;
}
break;
case MotionEvent.ACTION_UP:
break;
}
return super.onInterceptTouchEvent(event);
}
/**
* 打开menu菜单
*/
public void openMenu() {
int dx = menuWidth-getScrollX();
scroller.startScroll(getScrollX(), getScrollY(),dx, getScrollY());
invalidate();
if (onStateChangeListener != null)
{
onStateChangeListener.onOpen(this);
}
}
/**
* 关闭菜单
*/
public void closeMenu() {
//0表示menu移动到的目标距离
int dx = 0-getScrollX();
scroller.startScroll(getScrollX(), getScrollY(),dx, getScrollY());
invalidate();
if (onStateChangeListener != null)
{
onStateChangeListener.onClose(this);
}
}
@Override
public void computeScroll() {
super.computeScroll();
if (scroller.computeScrollOffset())
{
scrollTo(scroller.getCurrX(), scroller.getCurrY());
invalidate();
}
}
public interface OnStateChangeListener
{
void onOpen(SlideLayout slideLayout);
void onMove(SlideLayout slideLayout);
void onClose(SlideLayout slideLayout);
}
public OnStateChangeListener onStateChangeListener;
public void setOnStateChangeListener(OnStateChangeListener onStateChangeListener) {
this.OnStateChangeListener= onStateChangeListener;
}
}
欢迎关注公众号。