1. 程式人生 > >Android仿QQ側滑刪除實現

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();
    }
}