12.侧滑删除,slideView例子
分析
最近在群里有同学问我怎么写侧滑删除的控件,其实这个东西,我在公司里给做其他开发的同学培训过,现场写代码,所以现在写这篇文章应该也是可以直接就写代码的了!
我们要做一个侧滑删除的控件,第一步,我们就是要确定这个控件是View还是ViewGroup.有些同学可能不知道什么是View,什么是ViewGroup。
View:android里的控件都是View,包括ViewGroup,而ViewGroup则是继承自View,Group就是组的意思,所以ViewGroup其实是容器View,用于放置View的,比如说LinearLayout,RelativeLayout,这些Layout里面可以放置其他的ViewGroup和View吧,举个例子:你LinearLayout里可以继续放置LinearLayout吧,也可以放置Button,textView之类的。而View,里面不能再放View了,比如说,button里就不能再放东西了吧,TextView也不能做为一个容器里头再放其他的控件了吧!
关于命名,View的命名方式一般是:XxxView,Button除外,呵呵!比如说,TextView,ImageView。而ViewGroup的命名则是XxxLayout,比如说:LinearLayout,RelativeLayout
OK,到这里理解了View跟ViewGroup了吗?
我们继续分析我们的这个滑动删除控件,其实你把它看成View也行,看成ViewGroup也行。如果是看成View,那功能比较单一,也就是个滑动删除了。如果是个ViewGroup,那里面的东西由你自己放,那天你放图片进去也是可以滑动的嘛!
接下来的例子,我们将以ViewGroup的形式来实现!
实现过程
前面我们说到了,我们把它看成一个ViewGroup,第一步,我们就是要写一个类,继承自ViewGroup:
实现三个构造函数,覆写onLayout方法,这个是必须的。同学们有没有发现,我的构造方法都调用了this,其实就是不不管使用的地方是通过哪个构造方法调用进来的,都走到第三个构造方法,这样子我们可以统一地去处理一些逻辑代码,否则要独立出来,在三个方法里面单独调用。
为什么要强制实现onLayout呢?因为你要摆放孩子呀,你是一个ViewGroup呀,对不对!
这一步完成了,我们接下来干嘛呢!
大家要知道,写自定义控件的套路是固定的,我们这里实现的是ViewGrop,我们不需要把孩子画出来,这是由孩子自己完成的。ViewGroup的本质是什么呢?ViewGroup的本质是管理孩子。
所以接下来,我们是不是要摆放孩子呢,但是怎么摆放,我们是不是要先知道孩子的大小,并且摆放到什么位置上。
因 此,我们要获取到孩子的大小!
@Override
protected void onFinishInflate() {
super.onFinishInflate();
int childCount = getChildCount();
Log.d(TAG, "childCount -- > " + childCount);
//我们限定孩子的个数为2个,多了不行,少了不行。
if (childCount != 2) {
throw new IllegalArgumentException("It must be two child in this layout.");
}
//孩子判断OK,
//那么就拿到孩子的大小
View contentView = getChildAt(0);
View deleteView = getChildAt(1);
LayoutParams contentLayoutParams = contentView.getLayoutParams();
int contentHeight = contentLayoutParams.height;
Log.d(TAG, "contentHeight -- > " + contentHeight);
//内容的宽度不需要,我们直接让它跟老爸一样宽就可以了
//删除内容的高度和内容的高度一样,所以我们可以不拿了
//但是删除的宽度我们需要吧
LayoutParams deleteViewLayoutParams = deleteView.getLayoutParams();
int deleteWidth = deleteViewLayoutParams.width;
Log.d(TAG, "deleteWidth -- > " + deleteWidth);
}
于是我们引用一下,并且写上两个孩子在这个控件的里头:
然后跑起来:
可以看到打出来结果,正如我们所需要!
接下来,我们覆写测量方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//测量第一个孩子
//宽度直接跟老爸一样大,所以直接使用老爸的宽度即可
//高度使用我们取到的值,但需要构造一下
int contentHeightMeasureSpec = MeasureSpec.makeMeasureSpec(mContentHeight, MeasureSpec.EXACTLY);
mContentView.measure(widthMeasureSpec, contentHeightMeasureSpec);
//测量第二个孩子
//高度跟第一个孩子的高度一样高,但是宽度则是第二个孩子的宽度,所以
int deleteWidthMeasureSpec = MeasureSpec.makeMeasureSpec(mDeleteWidth, MeasureSpec.EXACTLY);
mDeleteView.measure(deleteWidthMeasureSpec,contentHeightMeasureSpec);
}
关于测量的方法,可以去翻看以前的博客了,呜呜,我这里就不去找了!
测量好了,当然就是摆放孩子的位置啦!
到onLayout方法了!
@Override
protected void onLayout(boolean b, int i, int i1, int i2, int i3) {
//这四个参数的意思是
//第一个,布局是否有改变
//第二个,左边,
//第三个,上边,
//第四个,右边,
//第五个,下边
//摆放第一个孩子:
int contentLeft = 0;
int contentTop = 0;
int contentRight = contentLeft + mContentView.getMeasuredWidth();
int contentBottom = contentTop + mContentView.getMeasuredHeight();
mContentView.layout(contentLeft, contentTop, contentRight, contentBottom);
//摆放第二个孩子:
int delLeft = contentRight;
int delTop = 0;
int delRight = delLeft + mDeleteView.getMeasuredWidth();
int delBottom = mDeleteView.getMeasuredHeight() + delTop;
mDeleteView.layout(delLeft, delTop, delRight, delBottom);
}
写好以后,我们可以预览一下: 可以看到,两个孩子都已经摆放好了!到这里, 同学们是不是已经知道了LinearLayout是怎么实现了呢?
孩子摆放好了,接下来要进行事件处理吧:比如说怎么样拖出来,拖出来的范围处理!
所以我们要覆写onTouch方法了吧!
处理触摸事件!
先完成移动的功能,边界和释放的位置我们稍后处理,运行结果如下图:
接下来,要处理越界问题了吧:
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
//触摸到屏幕
//拿到触摸下来的点,这里我们暂时不管角度的问题,只管x方向的移动,其实我们在实际开发的时候,还要处理角度问题的。
mDownX = event.getX();
break;
case MotionEvent.ACTION_MOVE:
//手指滑动
float moveX = event.getX();
//计算出dx
int dx = (int) (moveX - mDownX);
// Log.d(TAG, "dx -- > " + dx);
//进行移动,要考虑到方向问题,这里我们移动的是屏幕,所以方向相反,加个负号。
int scrollX = getScrollX();
// Log.d(TAG, "scrollX -- > " + scrollX);
//边界处理,方式不唯一,可以有很多种方法处理呢。
if (-dx + scrollX < 0) {
scrollTo(0, 0);
} else if (-dx + scrollX > mDeleteView.getMeasuredWidth()) {
scrollTo(mDeleteView.getMeasuredWidth(), 0);
} else {
scrollBy(-dx, 0);
}
mDownX = moveX;
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
//手指离开屏幕
//决定是打开,还是还原到原来的位置
//判断是打开,还是关闭
int scrolledX = getScrollX();
if (scrolledX > mDeleteView.getMeasuredWidth() / 2) {
//显示出来了超过了一半的位置,所以显示
scrollTo(mDeleteView.getMeasuredWidth(), 0);
} else {
scrollTo(0, 0);
}
break;
}
return true;
}
这么处理以后呢,就变成以下的结果了:
但是,回滚动回去的时候,看起来很生硬,所以我们要做一个阻尼的效果,手释放的时候,慢慢地回去
package com.sunofbeaches.slideviewdemo;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Scroller;
/**
* Created by TrillGates on 18/8/5.
* God bless my code!
*/
public class SlideLayout extends ViewGroup {
private static final String TAG = "SlideLayout";
private int mContentHeight;
private int mDeleteWidth;
private View mContentView;
private View mDeleteView;
private float mDownX;
private Scroller mScroller;
public SlideLayout(Context context) {
this(context, null);
}
public SlideLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SlideLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mScroller = new Scroller(context);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
int childCount = getChildCount();
Log.d(TAG, "childCount -- > " + childCount);
//我们限定孩子的个数为2个,多了不行,少了不行。
if (childCount != 2) {
throw new IllegalArgumentException("It must be two child in this layout.");
}
//孩子判断OK,
//那么就拿到孩子的大小
mContentView = getChildAt(0);
mDeleteView = getChildAt(1);
LayoutParams contentLayoutParams = mContentView.getLayoutParams();
mContentHeight = contentLayoutParams.height;
Log.d(TAG, "contentHeight -- > " + mContentHeight);
//内容的宽度不需要,我们直接让它跟老爸一样宽就可以了
//删除内容的高度和内容的高度一样,所以我们可以不拿了
//但是删除的宽度我们需要吧
LayoutParams deleteViewLayoutParams = mDeleteView.getLayoutParams();
mDeleteWidth = deleteViewLayoutParams.width;
Log.d(TAG, "deleteWidth -- > " + mDeleteWidth);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//测量第一个孩子
//宽度直接跟老爸一样大,所以直接使用老爸的宽度即可
//高度使用我们取到的值,但需要构造一下
int contentHeightMeasureSpec = MeasureSpec.makeMeasureSpec(mContentHeight, MeasureSpec.EXACTLY);
mContentView.measure(widthMeasureSpec, contentHeightMeasureSpec);
//测量第二个孩子
//高度跟第一个孩子的高度一样高,但是宽度则是第二个孩子的宽度,所以
int deleteWidthMeasureSpec = MeasureSpec.makeMeasureSpec(mDeleteWidth, MeasureSpec.EXACTLY);
mDeleteView.measure(deleteWidthMeasureSpec, contentHeightMeasureSpec);
}
@Override
protected void onLayout(boolean b, int i, int i1, int i2, int i3) {
//这四个参数的意思是
//第一个,布局是否有改变
//第二个,左边,
//第三个,上边,
//第四个,右边,
//第五个,下边
//摆放第一个孩子:
int contentLeft = 0;
int contentTop = 0;
int contentRight = contentLeft + mContentView.getMeasuredWidth();
int contentBottom = contentTop + mContentView.getMeasuredHeight();
mContentView.layout(contentLeft, contentTop, contentRight, contentBottom);
//摆放第二个孩子:
int delLeft = contentRight;
int delTop = 0;
int delRight = delLeft + mDeleteView.getMeasuredWidth();
int delBottom = mDeleteView.getMeasuredHeight() + delTop;
mDeleteView.layout(delLeft, delTop, delRight, delBottom);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
//触摸到屏幕
//拿到触摸下来的点,这里我们暂时不管角度的问题,只管x方向的移动,其实我们在实际开发的时候,还要处理角度问题的。
mDownX = event.getX();
break;
case MotionEvent.ACTION_MOVE:
//手指滑动
float moveX = event.getX();
//计算出dx
int dx = (int) (moveX - mDownX);
// Log.d(TAG, "dx -- > " + dx);
//进行移动,要考虑到方向问题,这里我们移动的是屏幕,所以方向相反,加个负号。
int scrollX = getScrollX();
// Log.d(TAG, "scrollX -- > " + scrollX);
//边界处理,方式不唯一,可以有很多种方法处理呢。
if (-dx + scrollX < 0) {
scrollTo(0, 0);
} else if (-dx + scrollX > mDeleteView.getMeasuredWidth()) {
scrollTo(mDeleteView.getMeasuredWidth(), 0);
} else {
scrollBy(-dx, 0);
}
mDownX = moveX;
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
//手指离开屏幕
//决定是打开,还是还原到原来的位置
//判断是打开,还是关闭
//一般来说,这个时间是动态计算的,这里就交给同学们去做啦,如果距离长一点,那么就时间 长一点,距离段,时间 可以短一点。
if (getScrollX() > mDeleteView.getMeasuredWidth() / 2) {
// 全部显示菜单部分
mScroller.startScroll(getScrollX(), 0, mDeleteView.getMeasuredWidth() - getScrollX(), 0, 500);
} else {
// 显示 内容部分
mScroller.startScroll(getScrollX(), 0, -getScrollX(), 0, 500);
}
invalidate();
break;
}
return true;
}
/**
* 这个方法用于生成剃度数据,就是让我们连续scroll到一个位置
* 看起来是流畅的滑动,达到一个阻尼的效果。
*/
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), 0);
invalidate();
}
}
}
效果是这样子的:
好啦,到这里的话,控件的效果就实现了,接下来,我们应该是暴露接口和相关的方法了吧!
封装控件
你这个控件是给别人用的嘛,所以你是不是要暴露出一些接口和判断的方法出来,比如说,判断当前的状态,是打开,还是关闭。还有就是设置通知接口,对吧!
所以我们定义以下接口:
public boolean isOpen() {
return mIsOpen;
}
private OnSlideStateListener mOnSlideStateListener = null;
/**
* 暴露给外面设置
*
* @param listener
*/
public void setOnSlideStateListener(OnSlideStateListener listener) {
this.mOnSlideStateListener = listener;
}
/**
* 状态接口,如果外面使用的人需要监听的话,那么就设置一个实现进来即可。
*/
public interface OnSlideStateListener {
void open();
void close();
}
在什么地方改变呢?当然是当我们的手离开的时候啦:
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
//手指离开屏幕
//决定是打开,还是还原到原来的位置
//判断是打开,还是关闭
//一般来说,这个时间是动态计算的,这里就交给同学们去做啦,如果距离长一点,那么就时间 长一点,距离段,时间 可以短一点。
if (getScrollX() > mDeleteView.getMeasuredWidth() / 2) {
// 全部显示菜单部分
mScroller.startScroll(getScrollX(), 0, mDeleteView.getMeasuredWidth() - getScrollX(), 0, 500);
mOnSlideStateListener.open();
mIsOpen = true;
} else {
// 显示 内容部分
mOnSlideStateListener.close();
mScroller.startScroll(getScrollX(), 0, -getScrollX(), 0, 500);
mIsOpen = false;
}
invalidate();
break;
好啦,到这里我们这控件基本写完了,后面还要添加删除的点击事件,外面实现,这样子就可以监听删除被点击了。
总结
自定义控件的几个步骤,今天这篇文章是ViewGroup的定制,本质就是对子控件进行管理。
也就是进行摆放,要摆放我们先要进行测量,要测量我们先要获得各个控件的大小。
一步一步逆推回去,如果 孩子摆放好了, 接下来就是处理动作事件了,什么情况下,控件的摆放怎么变化,实现的方式有很多种,思想才是重要的。
除了这种实现方式以外,也可以动态地改变布局位置,也可以挪动控件的位置。
好啦,祝大家学习快乐!有问题到群里或者网站上提问吧!