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