Android仿QQ侧滑删除实现

效果图如下



首先可以分析下,整行继承自线性布局,分为内容区域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();
    }
}

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值