package com.dl7.mvp.widget;
import android.app.Activity;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.support.annotation.IntDef;
import android.support.v4.content.ContextCompat;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.ViewDragHelper;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import com.dl7.mvp.R;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
/**
* Created by long on 2017/1/19.
* 开源项目:https://github.com/ikew0ng/SwipeBackLayout
*/
public class SwipeBackLayout extends FrameLayout {
// 最小快速滑动速度
private static final int MIN_FLING_VELOCITY = 400; // dips per second
// 默认遮罩颜色
private static final int DEFAULT_SCRIM_COLOR = 0x99000000;
// 透明度基数
private static final int FULL_ALPHA = 255;
// 默认滚动退出临界值,超过这个值就滑动退出
private static final float DEFAULT_SCROLL_THRESHOLD = 0.3f;
// 超出滚动距离,就是在 activity 从界面滑出它的宽或高的基础上在多滑动 OVER_SCROLL_DISTANCE,这个应该是让滑出效果体验更好
private static final int OVER_SCROLL_DISTANCE = 10;
/**
* 边缘拖拽使能标志位,支持左右和下边缘
* 暂时不支持 EDGE_ALL,滑动会有问题,详见 @see {@link ViewDragCallback#clampViewPositionHorizontal}
*/
public static final int EDGE_INVALID = -1;
public static final int EDGE_LEFT = ViewDragHelper.EDGE_LEFT;
public static final int EDGE_RIGHT = ViewDragHelper.EDGE_RIGHT;
public static final int EDGE_BOTTOM = ViewDragHelper.EDGE_BOTTOM;
// @see {@link #writeFile(String, InputStream, boolean)}
public static final int EDGE_ALL = EDGE_LEFT | EDGE_RIGHT | EDGE_BOTTOM;
// 拖拽标志
private int mEdgeFlag;
// 滚动临界值
private float mScrollThreshold = DEFAULT_SCROLL_THRESHOLD;
// 绑定的Activity
private Activity mAttachActivity;
// 使能
private boolean mEnableScroll = true;
// Activity的ContentView
private View mContentView;
// 拖拽帮助类
private ViewDragHelper mDragHelper;
// 滚动百分比{0~1}
private float mScrollPercent;
// ContentView的左偏移
private int mContentLeft;
// ContentView的上偏移
private int mContentTop;
// 左边缘阴影
private Drawable mShadowLeft;
// 右边缘阴影
private Drawable mShadowRight;
// 下边缘阴影
private Drawable mShadowBottom;
// 遮罩不透明度百分比
private float mScrimOpacity;
// 遮罩颜色
private int mScrimColor = DEFAULT_SCRIM_COLOR;
// 是否正在处理onLayout()
private boolean mInLayout;
//
private Rect mContentRect = new Rect();
// 拖拽边缘
private int mTrackingEdge = EDGE_INVALID;
public SwipeBackLayout(Context context) {
this(context, null);
}
public SwipeBackLayout(Context context, AttributeSet attrs) {
this(context, attrs, -1);
}
public SwipeBackLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
_init();
}
private void _init() {
mDragHelper = ViewDragHelper.create(this, 1.0f, new ViewDragCallback());
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
mInLayout = true;
if (mContentView != null) {
mContentView.layout(mContentLeft, mContentTop,
mContentLeft + mContentView.getMeasuredWidth(),
mContentTop + mContentView.getMeasuredHeight());
}
mInLayout = false;
}
@Override
public void requestLayout() {
if (!mInLayout) {
super.requestLayout();
}
}
@Override
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
final boolean drawContent = child == mContentView;
// 绘制子控件
boolean ret = super.drawChild(canvas, child, drawingTime);
if (mScrimOpacity > 0 && drawContent
&& mDragHelper.getViewDragState() != ViewDragHelper.STATE_IDLE) {
// 根据拖拽状态来绘制阴影和遮罩
_drawShadow(canvas, child);
_drawScrim(canvas, child);
}
return ret;
}
/**
* 绘制遮罩
*
* @param canvas
* @param child
*/
private void _drawScrim(Canvas canvas, View child) {
// 获取遮罩颜色的基础透明度,>>>为无符号右移,因为透明度保存在最高位字节里0xff000000,所以不能用 >>
final int baseAlpha = (mScrimColor & 0xff000000) >>> 24;
// 在基础透明度的基础上根据 mScrimOpacity 来改变透明度
final int alpha = (int) (baseAlpha * mScrimOpacity);
// 遮罩的最终显示颜色
final int color = alpha << 24 | (mScrimColor & 0xffffff);
// 绘制遮罩,clipRect裁剪出遮罩大小
if ((mTrackingEdge & EDGE_LEFT) != 0) {
canvas.clipRect(0, 0, child.getLeft(), getHeight());
} else if ((mTrackingEdge & EDGE_RIGHT) != 0) {
canvas.clipRect(child.getRight(), 0, getRight(), getHeight());
} else if ((mTrackingEdge & EDGE_BOTTOM) != 0) {
canvas.clipRect(child.getLeft(), child.getBottom(), getRight(), getHeight());
}
canvas.drawColor(color);
}
/**
* 绘制阴影
*
* @param canvas
* @param child
*/
private void _drawShadow(Canvas canvas, View child) {
child.getHitRect(mContentRect);
if ((mEdgeFlag & EDGE_LEFT) != 0) {
mShadowLeft.setBounds(mContentRect.left - mShadowLeft.getIntrinsicWidth(), mContentRect.top,
mContentRect.left, mContentRect.bottom);
mShadowLeft.setAlpha((int) (mScrimOpacity * FULL_ALPHA));
mShadowLeft.draw(canvas);
}
if ((mEdgeFlag & EDGE_RIGHT) != 0) {
mShadowRight.setBounds(mContentRect.right, mContentRect.top,
mContentRect.right + mShadowRight.getIntrinsicWidth(), mContentRect.bottom);
mShadowRight.setAlpha((int) (mScrimOpacity * FULL_ALPHA));
mShadowRight.draw(canvas);
}
if ((mEdgeFlag & EDGE_BOTTOM) != 0) {
mShadowBottom.setBounds(mContentRect.left, mContentRect.bottom, mContentRect.right,
mContentRect.bottom + mShadowBottom.getIntrinsicHeight());
mShadowBottom.setAlpha((int) (mScrimOpacity * FULL_ALPHA));
mShadowBottom.draw(canvas);
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
if (!mEnableScroll) {
return false;
}
try {
return mDragHelper.shouldInterceptTouchEvent(event);
} catch (ArrayIndexOutOfBoundsException e) {
return false;
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!mEnableScroll) {
return false;
}
mDragHelper.processTouchEvent(event);
return true;
}
@Override
public void computeScroll() {
mScrimOpacity = 1 - mScrollPercent;
if (mDragHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
private class ViewDragCallback extends ViewDragHelper.Callback {
@Override
public boolean tryCaptureView(View child, int pointerId) {
boolean edgeTouched = mDragHelper.isEdgeTouched(mEdgeFlag, pointerId);
if (edgeTouched) {
if (mDragHelper.isEdgeTouched(EDGE_LEFT, pointerId)) {
mTrackingEdge = EDGE_LEFT;
} else if (mDragHelper.isEdgeTouched(EDGE_RIGHT, pointerId)) {
mTrackingEdge = EDGE_RIGHT;
} else if (mDragHelper.isEdgeTouched(EDGE_BOTTOM, pointerId)) {
mTrackingEdge = EDGE_BOTTOM;
}
}
boolean directionCheck = false;
if (mEdgeFlag == EDGE_LEFT || mEdgeFlag == EDGE_RIGHT) {
// 左右边缘则检测竖直方向的滑动
directionCheck = !mDragHelper.checkTouchSlop(ViewDragHelper.DIRECTION_VERTICAL, pointerId);
} else if (mEdgeFlag == EDGE_BOTTOM) {
// 下边缘则检测水平方向的滑动
directionCheck = !mDragHelper.checkTouchSlop(ViewDragHelper.DIRECTION_HORIZONTAL, pointerId);
} else if (mEdgeFlag == EDGE_ALL) {
directionCheck = true;
}
return edgeTouched && directionCheck;
}
@Override
public int getViewHorizontalDragRange(View child) {
// 返回0则不能水平滑动
return mEdgeFlag & (EDGE_LEFT | EDGE_RIGHT);
}
@Override
public int getViewVerticalDragRange(View child) {
// 返回0则不能垂直滑动
return mEdgeFlag & EDGE_BOTTOM;
}
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
int ret = 0;
/**
* 注意:原开源项目中的 ViewDragHelper 是自己写的,感兴趣的可以对比两者中的 shouldInterceptTouchEvent() 方法的区别;
* 这里和原开源项目处理不同,用 mEdgeFlag 判断而不是 mTrackingEdge,因为系统的 clampViewPositionHorizontal()
* 会先于 tryCaptureView() 调用,且如果水平方向上这里没发生移动就不会调用 tryCaptureView(),mTrackingEdge 是在
* tryCaptureView() 赋值的,所以这边要做些判断
*/
// 这里控制 ContentView 的水平滑动范围
int edgeFlag = mTrackingEdge == EDGE_INVALID ? mEdgeFlag : mTrackingEdge;
if ((edgeFlag & EDGE_LEFT) != 0) {
ret = Math.min(child.getWidth(), Math.max(left, 0));
} else if ((edgeFlag & EDGE_RIGHT) != 0) {
ret = Math.min(0, Math.max(left, -child.getWidth()));
}
return ret;
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
int ret = 0;
// 这里控制 ContentView 的垂直滑动范围
int edgeFlag = mTrackingEdge == EDGE_INVALID ? mEdgeFlag : mTrackingEdge;
if ((edgeFlag & EDGE_BOTTOM) != 0) {
ret = Math.min(0, Math.max(top, -child.getHeight()));
}
return ret;
}
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
super.onViewPositionChanged(changedView, left, top, dx, dy);
if ((mTrackingEdge & EDGE_LEFT) != 0) {
mScrollPercent = Math.abs((float) left / (mContentView.getWidth() + mShadowLeft.getIntrinsicWidth()));
} else if ((mTrackingEdge & EDGE_RIGHT) != 0) {
mScrollPercent = Math.abs((float) left / (mContentView.getWidth() + mShadowRight.getIntrinsicWidth()));
} else if ((mTrackingEdge & EDGE_BOTTOM) != 0) {
mScrollPercent = Math.abs((float) top / (mContentView.getHeight() + mShadowBottom.getIntrinsicHeight()));
}
mContentLeft = left;
mContentTop = top;
// 调用这个来绘制背景遮罩和阴影
invalidate();
if (mScrollPercent >= 1) {
// 滑动超过1则销毁 Activity
if (!mAttachActivity.isFinishing()) {
mAttachActivity.finish();
// 不显示 Activity 切换动画
mAttachActivity.overridePendingTransition(0, 0);
}
}
}
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
final int childWidth = releasedChild.getWidth();
final int childHeight = releasedChild.getHeight();
// 计算 ContentView 最终滑动目标 left 和 top
// 这里说明下 xvel 和 yvel,他俩的取值范围为系统判断触摸滑动fling事件的最大和最小滑动速率,小于 mMinVelocity 统统返回0
int left = 0, top = 0;
if ((mTrackingEdge & EDGE_LEFT) != 0) {
left = xvel > 0 || xvel == 0 && mScrollPercent > mScrollThreshold ? childWidth
+ mShadowLeft.getIntrinsicWidth() + OVER_SCROLL_DISTANCE : 0;
} else if ((mTrackingEdge & EDGE_RIGHT) != 0) {
left = xvel < 0 || xvel == 0 && mScrollPercent > mScrollThreshold ? -(childWidth
+ mShadowLeft.getIntrinsicWidth() + OVER_SCROLL_DISTANCE) : 0;
} else if ((mTrackingEdge & EDGE_BOTTOM) != 0) {
top = yvel < 0 || yvel == 0 && mScrollPercent > mScrollThreshold ? -(childHeight
+ mShadowBottom.getIntrinsicHeight() + OVER_SCROLL_DISTANCE) : 0;
}
// 让拖拽视图滑动到指定位置
mDragHelper.settleCapturedViewAt(left, top);
invalidate();
}
@Override
public void onViewDragStateChanged(int state) {
super.onViewDragStateChanged(state);
if (state == ViewDragHelper.STATE_IDLE) {
mTrackingEdge = EDGE_INVALID;
}
}
}
/**
================================== 外部调用 ==================================
*/
/**
* 设置控制滑动的 ContentView,也就是关联 Activity 的界面视图
*
* @param view
*/
private void _setContentView(View view) {
mContentView = view;
}
/**
* 设置关联的 Activity
* 重要!必须调用
*
* @param activity
*/
public void attachToActivity(Activity activity, @EdgeFlag int edgeFlag) {
mAttachActivity = activity;
TypedArray a = activity.getTheme().obtainStyledAttributes(new int[]{
android.R.attr.windowBackground
});
int background = a.getResourceId(0, 0);
a.recycle();
ViewGroup decor = (ViewGroup) activity.getWindow().getDecorView();
ViewGroup decorChild = (ViewGroup) decor.getChildAt(0);
decorChild.setBackgroundResource(background);
decor.removeView(decorChild);
addView(decorChild);
_setContentView(decorChild);
decor.addView(this);
setEdgeFlag(edgeFlag);
}
public int getEdgeFlag() {
return mEdgeFlag;
}
public void setEdgeFlag(@EdgeFlag int edgeFlag) {
mEdgeFlag = edgeFlag;
mDragHelper.setEdgeTrackingEnabled(edgeFlag);
if ((mEdgeFlag & EDGE_LEFT) != 0 && mShadowLeft == null) {
mShadowLeft = ContextCompat.getDrawable(mAttachActivity, R.drawable.ic_shadow_left);
}
if ((mEdgeFlag & EDGE_RIGHT) != 0 && mShadowRight == null) {
mShadowRight = ContextCompat.getDrawable(mAttachActivity, R.drawable.ic_shadow_right);
}
if ((mEdgeFlag & EDGE_BOTTOM) != 0 && mShadowBottom == null) {
mShadowBottom = ContextCompat.getDrawable(mAttachActivity, R.drawable.ic_shadow_bottom);
}
invalidate();
}
public float getScrollThreshold() {
return mScrollThreshold;
}
public void setScrollThreshold(float scrollThreshold) {
mScrollThreshold = scrollThreshold;
}
public boolean isEnableScroll() {
return mEnableScroll;
}
public void setEnableScroll(boolean enableScroll) {
mEnableScroll = enableScroll;
}
public void setShadowLeft(Drawable shadowLeft) {
mShadowLeft = shadowLeft;
}
public void setShadowRight(Drawable shadowRight) {
mShadowRight = shadowRight;
}
public void setShadowBottom(Drawable shadowBottom) {
mShadowBottom = shadowBottom;
}
public int getScrimColor() {
return mScrimColor;
}
public void setScrimColor(int scrimColor) {
mScrimColor = scrimColor;
}
/**
* 设置可拖拽的边缘大小
*
* @param edgeSize
*/
public void setEdgeSize(int edgeSize) {
try {
Field edgeSizeField = mDragHelper.getClass().getDeclaredField("mEdgeSize");
edgeSizeField.setAccessible(true);
edgeSizeField.setInt(mDragHelper, edgeSize);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
/**
* ================================== ==================================
*/
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.PARAMETER)
@IntDef({EDGE_LEFT, EDGE_RIGHT, EDGE_BOTTOM, EDGE_ALL})
public @interface EdgeFlag {
}
}