效果图如下
首先可以分析下,整行继承自线性布局,分为内容区域ContentRect 和 操作区域(即删除,置顶的操作)。
则整个线性布局下有两个child:一个内容View,一个可操作view,可以简单的理解为根据用户的手势来向左,向右滑动子元素,每次都requestLayout 产生的位移来重新布局子元素的位置,ok原理就是这样,无非就处理内容区域和操作区域的临界点,可以看到,当打开侧滑菜单即向左滑动时内容区域的左边Left范围是从0到负的操作区域这个范围
也即leftX = [0,-optionViewWidth];optionView的Left则需要加上内容的宽度,因为他永远在内容区域的右边即[contentViewwidth + leftX];同理,当向右滑动,即关闭这个侧滑菜单时,内容区域的leftX = -optionViewWidth + leftX,因为不能够大于0,他的范围是从-optionViewWidth 处 位移到0的过程,操作区域的的与之类似
1 最主要的方法就在onLayout了,如下所示 leftX 是我们定义的一个手指触摸滑动的变量,contentViewWidth 和 optionViewWidth 是我们可以获取到的内容区域和操作区域
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); contentView.layout(leftX, 0, contentViewWidth + leftX, mHeight); deleteView.layout(contentViewWidth + leftX, 0, contentViewWidth + optionViewWidth + leftX, mHeight); Log.e("onLayout", "onLayout" + String.valueOf(contentView.getLeft()) + "==" + String.valueOf(deleteView.getLeft())); if(mCurrentState == SlideState.OPEN){ //打开状态获取此时内容和删除区域的rectF,用户再次单击时获取是否在内容区域内,如果在,则执行关闭动画,反之,则是删除区域的操作 contentRectF.top = contentView.getTop(); contentRectF.left = contentView.getLeft(); contentRectF.right = contentView.getRight(); contentRectF.bottom = contentView.getBottom(); deleteRectF.left = deleteView.getLeft(); deleteRectF.right = deleteView.getRight(); deleteRectF.bottom = deleteView.getBottom(); deleteRectF.top = deleteView.getTop(); } }
2.在onSizechanged方法里初始化数据,根据扣扣的截图(3倍图),删除和置顶的宽高大约为100px,80px,则大约对应于2倍图的54dp,63dp,因此算法如下:
@Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); this.mHeight = h; this.mWidth = w; contentRectF = new RectF(); deleteRectF = new RectF(); deleteView = LayoutInflater.from(getContext()).inflate(R.layout.item_delete_options, null); contentView = LayoutInflater.from(getContext()).inflate(R.layout.item_content, null); LinearLayout.LayoutParams contentLayoutParams = new LayoutParams(mWidth, (int) getResources().getDimension(R.dimen.dimens_54_dp)); LinearLayout.LayoutParams deleteLayoutParams = new LayoutParams((int) getResources().getDimension(R.dimen.dimens_63_dp) * 2, (int) getResources().getDimension(R.dimen.dimens_54_dp)); contentView.setLayoutParams(contentLayoutParams); deleteView.setLayoutParams(deleteLayoutParams); optionViewWidth = (int) getResources().getDimension(R.dimen.dimens_63_dp) * 2; contentViewWidth = mWidth; this.removeAllViews(); this.setGravity(Gravity.CENTER_VERTICAL); this.addView(contentView); this.addView(deleteView); Log.e("onSizeChanged", "onSizeChanged"); }3 我们事先定义两个变量 存储滑动状态
public static class SlideState { public static final int OPEN = 0; public static final int CLOSE = 1; }4.onTouchEvent 则主要是滑动及临界值的处理,leftX 主要变量,每次都会requestLayout 通知更新子元素的位置
case MotionEvent.ACTION_DOWN: downX = (int) event.getX(); break; case MotionEvent.ACTION_MOVE: int moveX = (int) event.getX(); if (downX - moveX > 10 && mCurrentState == SlideState.CLOSE) {//打开 if (downX - moveX >= optionViewWidth) { leftX = -optionViewWidth; mCurrentState = SlideState.OPEN; requestLayout(); } else { leftX = moveX - downX; requestLayout(); } } else if (moveX - downX > 10 && mCurrentState == SlideState.OPEN) {//关闭 if (moveX - downX >= optionViewWidth) { leftX = 0; mCurrentState = SlideState.CLOSE; requestLayout(); } else { leftX = -optionViewWidth + (moveX - downX); requestLayout(); } }5.响应单击事件并动画关闭,这里主要处理好动画的移动范围,很明显,在打开状态时,用户感觉是从左到右的动画,运动范围[-optionViewWidth,0]则动画如下
public void closeAnimation() { ValueAnimator valueAnimator = ValueAnimator.ofFloat(1, 0); valueAnimator.setRepeatCount(0); valueAnimator.setDuration(200); valueAnimator.setInterpolator(new LinearInterpolator()); valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { Float atFloat = (Float) valueAnimator.getAnimatedValue(); leftX = (int) (-optionViewWidth * atFloat); requestLayout(); } }); valueAnimator.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animator) { } @Override public void onAnimationEnd(Animator animator) { mCurrentState = SlideState.CLOSE; } @Override public void onAnimationCancel(Animator animator) { } @Override public void onAnimationRepeat(Animator animator) { } }); valueAnimator.start(); }6 单击的事件则判断手指离开后的X坐标与按下的坐标直接的距离,我设置的是10个像素,如果大于10,则视为移动onMove,反之,响应单击事件,代码如下所示
case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: int upX = (int) event.getX(); if (mCurrentState == SlideState.OPEN && Math.abs(downX - upX) <= 10 && contentRectF.contains(upX,event.getY())) { closeAnimation(); } if (mCurrentState == SlideState.OPEN && Math.abs(downX - upX) <= 10 && deleteRectF.contains(upX,event.getY())) { if(upX > optionViewWidth / 2 + (mWidth - optionViewWidth)){//执行删除操作 if(null != mOnSlideOpenOrCloseListener){ mOnSlideOpenOrCloseListener.option(1); } Toast.makeText(getContext(),"删除",Toast.LENGTH_SHORT).show(); }else {//置顶等其他操作 if(null != mOnSlideOpenOrCloseListener){ mOnSlideOpenOrCloseListener.option(0); } Toast.makeText(getContext(),"置顶",Toast.LENGTH_SHORT).show(); } closeAnimation(); } if (leftX <= -optionViewWidth / 2) { leftX = -optionViewWidth; mCurrentState = SlideState.OPEN; if(null != mOnSlideCompletionListener){ mOnSlideCompletionListener.swiping(); } requestLayout(); } else { leftX = 0; mCurrentState = SlideState.CLOSE; requestLayout(); } break;7 上面至于你点击是哪个区域,我用rectF来记录两个子元素的运动轨迹,如果手指在此范围则可执行相应的操作
if (mCurrentState == SlideState.OPEN && Math.abs(downX - upX) <= 10 && deleteRectF.contains(upX,event.getY())) { if(upX > optionViewWidth / 2 + (mWidth - optionViewWidth)){//执行删除操作 if(null != mOnSlideOpenOrCloseListener){ mOnSlideOpenOrCloseListener.option(1); } Toast.makeText(getContext(),"删除",Toast.LENGTH_SHORT).show(); }else {//置顶等其他操作 if(null != mOnSlideOpenOrCloseListener){ mOnSlideOpenOrCloseListener.option(0); } Toast.makeText(getContext(),"置顶",Toast.LENGTH_SHORT).show(); } closeAnimation(); }8,在onAttachedToWindow,加载到窗口的时候注意延迟发送requestLayout,让其初次布局并执行onLayout方法。
@Override protected void onAttachedToWindow() { super.onAttachedToWindow(); Log.e("onAttachedToWindow", "onAttachedToWindow"); postDelayed(new Runnable() { @Override public void run() { requestLayout(); } }, 100); }
以上就完成了侧滑的功能,当然也有许多的可扩展性,以后可代码扩展添加自己的contentView 和 optionView,上述的属性集大家可以自己加,自己动起手来,好了,自定义这块东西是挺多,但是我觉得还要更全面,往NDK,React Native,H5方面多看看,毕竟目前来说,技术层出不穷,要收拾好心情,继续的去不断学习,与君共勉吧!
代码片如下:
package com.example.mrboudar.playboy.widgets;
import android.animation.Animator;
import android.animation.ValueAnimator;
import android.app.IntentService;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.GradientDrawable;
import android.support.annotation.Dimension;
import android.support.annotation.Px;
import android.support.v7.widget.LinearLayoutCompat;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.LinearInterpolator;
import android.widget.LinearLayout;
import android.widget.Toast;
import com.example.mrboudar.playboy.L;
import com.example.mrboudar.playboy.R;
import com.example.mrboudar.playboy.model.GameCard;
/**
* Created by MrBoudar on 16/9/11.
* use in xml
* use in code
*/
public class SlideDeleteView extends LinearLayout {
private int mWidth;
private int mHeight;
private View contentView;
private View deleteView;
//首次触摸
private int downX;
//位移变量
private int leftX;
//侧滑打开状态
private int mCurrentState = SlideState.CLOSE;
//qq截图 3倍图的大小
private static int defaultOptionsWidth = 100 / 3;
private static int defaultOptionsHeight = 80 / 3;
//内容的宽度
private int optionViewWidth;
//option选项的宽度
private int contentViewWidth;
//用来处理单击事件是否在内容区域内
private RectF contentRectF;
private RectF deleteRectF;
private IntentService intentService;
public SlideDeleteView(Context context) {
this(context, null);
}
public SlideDeleteView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SlideDeleteView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//init type arrays
setOrientation(LinearLayout.HORIZONTAL);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
this.mHeight = h;
this.mWidth = w;
contentRectF = new RectF();
deleteRectF = new RectF();
deleteView = LayoutInflater.from(getContext()).inflate(R.layout.item_delete_options, null);
contentView = LayoutInflater.from(getContext()).inflate(R.layout.item_content, null);
LinearLayout.LayoutParams contentLayoutParams = new LayoutParams(mWidth, (int) getResources().getDimension(R.dimen.dimens_54_dp));
LinearLayout.LayoutParams deleteLayoutParams = new LayoutParams((int) getResources().getDimension(R.dimen.dimens_63_dp) * 2, (int) getResources().getDimension(R.dimen.dimens_54_dp));
contentView.setLayoutParams(contentLayoutParams);
deleteView.setLayoutParams(deleteLayoutParams);
optionViewWidth = (int) getResources().getDimension(R.dimen.dimens_63_dp) * 2;
contentViewWidth = mWidth;
this.removeAllViews();
this.setGravity(Gravity.CENTER_VERTICAL);
this.addView(contentView);
this.addView(deleteView);
Log.e("onSizeChanged", "onSizeChanged");
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
Log.e("onAttachedToWindow", "onAttachedToWindow");
postDelayed(new Runnable() {
@Override
public void run() {
requestLayout();
}
}, 100);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
Log.e("onDetachedFromWindow", "onDetachedFromWindow");
}
@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
super.onWindowFocusChanged(hasWindowFocus);
Log.e("onWindowFocusChanged", "onWindowFocusChanged");
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Log.e("onMeasure", "onMeasure");
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
contentView.layout(leftX, 0, contentViewWidth + leftX, mHeight);
deleteView.layout(contentViewWidth + leftX, 0, contentViewWidth + optionViewWidth + leftX, mHeight);
Log.e("onLayout", "onLayout" + String.valueOf(contentView.getLeft()) + "==" + String.valueOf(deleteView.getLeft()));
contentRectF.top = contentView.getTop();
contentRectF.left = contentView.getLeft();
contentRectF.right = contentView.getRight();
contentRectF.bottom = contentView.getBottom();
if (mCurrentState == SlideState.OPEN) {
//打开状态获取此时内容和删除区域的rectF,用户再次单击时获取是否在内容区域内,如果在,则执行关闭动画,反之,则是删除区域的操作
deleteRectF.left = deleteView.getLeft();
deleteRectF.right = deleteView.getRight();
deleteRectF.bottom = deleteView.getBottom();
deleteRectF.top = deleteView.getTop();
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getActionMasked();
switch (action) {
case MotionEvent.ACTION_DOWN:
downX = (int) event.getX();
break;
case MotionEvent.ACTION_MOVE:
int moveX = (int) event.getX();
if (downX - moveX > 10 && mCurrentState == SlideState.CLOSE) {//打开
if (downX - moveX >= optionViewWidth) {
leftX = -optionViewWidth;
callbackOpen();
} else {
leftX = moveX - downX;
requestLayout();
}
} else if (moveX - downX > 10 && mCurrentState == SlideState.OPEN) {//关闭
if (moveX - downX >= optionViewWidth) {
leftX = 0;
callbackClose();
} else {
leftX = -optionViewWidth + (moveX - downX);
requestLayout();
}
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
int upX = (int) event.getX();
if (mCurrentState == SlideState.OPEN && Math.abs(downX - upX) <= 10 && contentRectF.contains(upX, event.getY())) {
closeAnimation();
}
if(mCurrentState == SlideState.CLOSE && Math.abs(downX - upX) <= 10 && contentRectF.contains(upX, event.getY())){
//响应单击事件
if (null != mOnSlideOpenOrCloseListener) {
mOnSlideOpenOrCloseListener.option(2);
}
}
if (mCurrentState == SlideState.OPEN && Math.abs(downX - upX) <= 10 && deleteRectF.contains(upX, event.getY())) {
if (upX > optionViewWidth / 2 + (mWidth - optionViewWidth)) {//执行删除操作
if (null != mOnSlideOpenOrCloseListener) {
mOnSlideOpenOrCloseListener.option(1);
}
Toast.makeText(getContext(), "删除", Toast.LENGTH_SHORT).show();
} else {//置顶等其他操作
if (null != mOnSlideOpenOrCloseListener) {
mOnSlideOpenOrCloseListener.option(0);
}
Toast.makeText(getContext(), "置顶", Toast.LENGTH_SHORT).show();
}
closeAnimation();
}
if (leftX <= -optionViewWidth / 2) {
leftX = -optionViewWidth;
callbackOpen();
} else {
leftX = 0;
callbackClose();
}
break;
}
return true;
}
public void callbackOpen() {
mCurrentState = SlideState.OPEN;
if (null != mOnSlideCompletionListener) {
mOnSlideCompletionListener.open();
}
requestLayout();
}
public void callbackClose() {
mCurrentState = SlideState.CLOSE;
if (null != mOnSlideCompletionListener) {
mOnSlideCompletionListener.close();
}
requestLayout();
}
public static class SlideState {
public static final int OPEN = 0;
public static final int CLOSE = 1;
}
private onSlideOpenOrCloseListener mOnSlideOpenOrCloseListener;
public interface onSlideOpenOrCloseListener {
//0 置顶 1删除 2 跳转
public void option(int state);
}
public void setOnSlideOpenOrCloseListener(onSlideOpenOrCloseListener onSlideOpenOrCloseListener) {
this.mOnSlideOpenOrCloseListener = onSlideOpenOrCloseListener;
}
public int getState() {
return mCurrentState;
}
private onSlideCompletionListener mOnSlideCompletionListener;
public interface onSlideCompletionListener {
public void open();
public void close();
}
public void setOnSlideCompltetionListener(onSlideCompletionListener onSlideCompltetionListener) {
this.mOnSlideCompletionListener = onSlideCompltetionListener;
}
public void closeAnimation() {
ValueAnimator valueAnimator = ValueAnimator.ofFloat(1, 0);
valueAnimator.setRepeatCount(0);
valueAnimator.setDuration(200);
valueAnimator.setInterpolator(new LinearInterpolator());
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
Float atFloat = (Float) valueAnimator.getAnimatedValue();
leftX = (int) (-optionViewWidth * atFloat);
requestLayout();
}
});
valueAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
}
@Override
public void onAnimationEnd(Animator animator) {
mCurrentState = SlideState.CLOSE;
}
@Override
public void onAnimationCancel(Animator animator) {
}
@Override
public void onAnimationRepeat(Animator animator) {
}
});
valueAnimator.start();
}
}