您的位置:首页 > 其它

SwipeBackLayout源码解析

2017-12-19 09:47 344 查看
    SwipeBackLayout是一个开源的实现了Activity滑动返回的库,当Activity滑动返回时,另一个Activity界面逐渐显示。效果图如下:



SwipeBackLayout

    从上图中看出,滑动的是一个Activity上最外层的ViewGroup,使用帧布局比较合适,在SwipeBackLayout的源码中,他也是继承自帧布局。在帧布中定义了一些常量和一些成员变量,例如:滑动速率,屏幕边沿标志,滑动过程中的一些参数等等。在构造方法里面进行了一些自定义属性的初始化内容。在SwipeBackLayout控件中有一个ViewDragHelper,他是实现手势触摸识别的核心类,要研究SwipeBackLayout的源码,首先从ViewDragHelper进行着手。

ViewDragHelper

    在处理触摸事件中常常使用事件分发机制进行分析,对于复杂的事件处理使用这种机制分析起来很麻烦,正常情况下需要响应down事件,记录当时触摸的点,然后根据这个点,判断触摸到了哪个view,其次响应move事件,得到移动的x轴、y轴的偏移量,最后将偏移量作用于view。因此使用ViewDragHelper进行处理,他是官方v4包下提供的一个类,提供了一系列的方法和状态跟踪,能够简化事件处理流程,可以把以上事件处理全部交给ViewDragHelper进行处理。

    1、将事件拦截交给shouldInterceptTouchEvent(MotionEvent ev)方法

@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
if (!mEnable) {
return false;
}
try {
return mDragHelper.shouldInterceptTouchEvent(event);
} catch (ArrayIndexOutOfBoundsException e) {
return false;
}
}

    2、将事件触摸交给processTouchEvent(MotionEvent ev),并且返回true,响应此事件。

@Override
public boolean onTouchEvent(MotionEvent event) {
if (!mEnable) {
return false;
}
mDragHelper.processTouchEvent(event);
return true;
}

ViewDragCallback

    ViewDragCallback继承自ViewDragHelper.Callback拖拽效果实现类,核心方法如下:

tryCaptureView

    表示该view是否支持滑动,返回true表示支持滑动, false表示不支持滑动,包含两个参数:

    参数一:支持滑动的view

    参数二:多指触控

    实现步骤:

    1、是否支持边沿触控

    2、判断是那侧的边沿触控,设置触控回调

    3、判断是否为指定的滑动,防止乱划,斜着滑屏

    4、是边缘滑动并且满足可以滑动的时候,contentview才可以拖动  

@Override
public boolean tryCaptureView(View view, int i) {
//是否支持边沿触摸
boolean ret = mDragHelper.isEdgeTouched(mEdgeFlag, i);
if (ret) {
//判断三个边沿那侧支持触摸
if (mDragHelper.isEdgeTouched(EDGE_LEFT, i)) {
mTrackingEdge = EDGE_LEFT;
} else if (mDragHelper.isEdgeTouched(EDGE_RIGHT, i)) {
mTrackingEdge = EDGE_RIGHT;
} else if (mDragHelper.isEdgeTouched(EDGE_BOTTOM, i)) {
mTrackingEdge = EDGE_BOTTOM;
}
if (mListeners != null && !mListeners.isEmpty()) {
for (SwipeListener listener : mListeners) {
//给回调设置那侧支持触摸
listener.onEdgeTouch(mTrackingEdge);
}
}
mIsScrollOverValid = true;
}
//判断是否为指定的滑动,防止乱划,斜着滑屏
boolean directionCheck = false;
if (mEdgeFlag == EDGE_LEFT || mEdgeFlag == EDGE_RIGHT) {
//当左右滑动时候检查滑动方向是否是左右滑动,滑动过程中多次被调用
directionCheck = !mDragHelper.checkTouchSlop(ViewDragHelper.DIRECTION_VERTICAL, i);
} else if (mEdgeFlag == EDGE_BOTTOM) {
//当左右滑动时候检查滑动方向是否是上下滑动,滑动过程中多次被调用
directionCheck = !mDragHelper.checkTouchSlop(ViewDragHelper.DIRECTION_HORIZONTAL, i);
} else if (mEdgeFlag == EDGE_ALL) {
directionCheck = true;
}
//是边缘滑动并且满足可以滑动的时候,contentview才可以拖动
return ret & directionCheck;
}


clampViewPositionHorizontal

    该类是获取child需要被移动到的x轴位置距离,接收三个参数:

    参数一:拖动的view

    参数二:x轴坐标位置

    参数三:x轴移动位置

    实现步骤:

    1、如果为左边界滑动,边界限制为(最小值为0,最大值为contentview的宽度)

    2、如果为右边界滑动,边界限制为(最小值为-contentView的宽度,最大值为0)

@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
//如果为左边界滑动,边界限制为(最小值为0,最大值为contentview的宽度)
//如果为右边界滑动,边界限制为(最小值为-contentView的宽度,最大值为0)
int ret = 0;
if ((mTrackingEdge & EDGE_LEFT) != 0) {
ret = Math.min(child.getWidth(), Math.max(left, 0));
} else if ((mTrackingEdge & EDGE_RIGHT) != 0) {
ret = Math.min(0, Math.max(left, -child.getWidth()));
}
return ret;
}


clampViewPositionVertical

    该类是获取child需要被移动到的y轴位置距离,接收三个参数:

    参数一:拖动的view

    参数二:y轴坐标位置

    参数三:y轴移动位置

    实现步骤:

    如果为底部滑动,边界限制为(最小值为-contentView的高度,最大值为0);

@Override
public int clampViewPositionVertical(View child, int top, int dy) {
//如果为底部滑动,边界限制为(最小值为-contentView的高度,最大值为0)
int ret = 0;
if ((mTrackingEdge & EDGE_BOTTOM) != 0) {
ret = Math.min(0, Math.max(top, -child.getHeight()));
}
return ret;
}

getViewHorizontalDragRange

    该类表示是否支持水平滑动,返回结果>0,支持水平拖动;<0,不支持,接收一个参数:

    参数一:拖动的view

    实现步骤:

    如果指定的滑动为左右滑动,则支持

@Override
public int getViewHorizontalDragRange(View child) {
//如果指定的滑动是做边沿或者有边沿则支持左右滑动
return mEdgeFlag & (EDGE_LEFT | EDGE_RIGHT);
}

getViewVerticalDragRange

    该类表示是否支持竖直滑动,返回结果>0,支持竖直拖动;<0,不支持,接收一个参数:

    参数一:拖动的view

    实现步骤:

    滑动边沿为底部时候支持竖直滑动

@Override
public int getViewVerticalDragRange(View child) {
//滑动边沿为底部时候支持竖直滑动
return mEdgeFlag & EDGE_BOTTOM;
}


onViewPositionChanged

    当位置发生改变时候回调,参数含义,接收三个参数:

    参数一:拖动的view

    参数二三:left,top变化时新的x左/y顶坐标

    参数四五:从旧到新位置的偏移量

    实现步骤:

    1、获取一个滑过位置占屏幕总宽或者高的百分比

    2、获取拖动过后的left跟top,然后调用invalidate会重新绘制ViewGroup,在onLayout方法中重新摆放子控件位置

    3、滑动超过设置自动滑动距离的临界值时回调

    4、当滑动完毕时候,关闭Activity

@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=拖动过后的left/(mContentView的宽度+左边阴影的宽度)
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()));
}
//获取拖动过后的left跟top,然后调用invalidate会重新绘制ViewGroup,在onLayout方法中重新摆放子控件位置
mContentLeft = left;
mContentTop = top;
invalidate();
if (mScrollPercent < mScrollThreshold && !mIsScrollOverValid) {
mIsScrollOverValid = true;
}
//滑动超过设置距离的临界值时回调
if (mListeners != null && !mListeners.isEmpty()
&& mDragHelper.getViewDragState() == STATE_DRAGGING
&& mScrollPercent >= mScrollThreshold && mIsScrollOverValid) {
mIsScrollOverValid = false;
for (SwipeListener listener : mListeners) {
listener.onScrollOverThreshold();
}
}
//当activity滑动的完全滑动完毕时候,关闭activity
if (mScrollPercent >= 1) {
if (!mActivity.isFinishing()) {
mActivity.finish();
mActivity.overridePendingTransition(0, 0);
}
}
}

onViewReleased

    前被捕获的View释放之后回调,即手指抬起的回调,接收三个参数:

    参数一:拖动的view

    参数二:x轴方向瞬时速度

    参数三:y轴方向瞬时速度

    实现步骤:

    1、计算自动滚动到指定的left跟top位置

    2、设置自动滚动到指定的left跟top位置

    3、重绘

@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
final int childWidth = releasedChild.getWidth();
final int childHeight = releasedChild.getHeight();

//满足条件时候是设置left,top的值,条件内容:如果释放位置的滑动距离大于设定距离自动滑动关闭,
//否则自动滑动到开始位置
int left = 0, top = 0;
if ((mTrackingEdge & EDGE_LEFT) != 0) {
//当为左边滑动的时候
//x轴上滑动的速度>=0并且滑动的距离到达总距离自定的0.3f的时候,
//left即为(content的宽度+左阴影的宽度+超出滑动距离的offset)
left = xvel > 0 || xvel == 0 && mScrollPercent > mScrollThreshold ? childWidth
+ mShadowLeft.getIntrinsicWidth() + OVERSCROLL_DISTANCE : 0;
} else if ((mTrackingEdge & EDGE_RIGHT) != 0) {
left = xvel < 0 || xvel == 0 && mScrollPercent > mScrollThreshold ? -(childWidth
+ mShadowLeft.getIntrinsicWidth() + OVERSCROLL_DISTANCE) : 0;
} else if ((mTrackingEdge & EDGE_BOTTOM) != 0) {
top = yvel < 0 || yvel == 0 && mScrollPercent > mScrollThreshold ? -(childHeight
+ mShadowBottom.getIntrinsicHeight() + OVERSCROLL_DISTANCE) : 0;
}
//自动滚动到指定的left跟top位置
mDragHelper.settleCapturedViewAt(left, top);
invalidate();
}

onViewDragStateChanged

    当ViewDragHelper状态发生变化时回调,接收一个参数:

    参数一:滑动状态表示值

    实现步骤:设置回调通知

@Override
public void onViewDragStateChanged(int state) {
super.onViewDragStateChanged(state);
if (mListeners != null && !mListeners.isEmpty()) {
for (SwipeListener listener : mListeners) {
listener.onScrollStateChange(state, mScrollPercent);
}
}
}

    以上就是SwipeBackLayout核心类ViewDragHelper中所处理的内容,每个方法都有总结,并且源码中都有详细的备注。
    在使用ViewDragHelper.Callback处理完毕滑动事件之后就需要对View进行重新位置的摆放和重绘了。需要重写onLayout,drawChild两个方法,做出相应处理。

onLayout

    onLayout是指在前面Callback的onViewPositionChanged方法中已经获得滑动后View的left、top、mScrollPercent,在调用invalidate时候会调用onLayout方法,重新摆放控件位置。

    实现内容:

    mContentView调用layout方法,进行设置。

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
mInLayout = true;
if (mContentView != null)
//对控件进行重新定位调用onLayout()这个方法mContentLeft,mContentTop为拖动后的宽高
mContentView.layout(mContentLeft, mContentTop,
mContentLeft + mContentView.getMeasuredWidth(),
mContentTop + mContentView.getMeasuredHeight());
mInLayout = false;
}

drawChild

    drawChild是指当View滑动过后,滑过空间部分半透明阴影部分的绘制。

实现内容:
1、获取child的坐标范围,绘制阴影部分,动态设置透明度
2、阴影部分裁剪,防止其余部分被显示

/**
* 遍历了所有子View,每个子View都调用了drawChild这个方法
*/
@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;
}


attachToActivity

    通过阴影部分的绘制,以及透明度的动态设置,就会出现当View滑动时,滑动过的部分渐渐变亮,只到完全显示。现在来看自定义的View如何设置到Activity上去,使Activity变成可拖动的。步骤如下:

    1、获取Activity顶级视图

    2、获得Activity界面所用的xml文件的根view

    3、给Activity的根View设置背景

    4、从Activity的顶级视图移除activity的xml的根view

    5、给SwipeBackLayout添加子view,作为SwipeBackLayout的第一个view

    6、把decorChild当成SwipeBackLayout的contentView进行设置

    7、把SwipeBackLayout作为Activity顶级视图的子View进行设置

public void attachToActivity(Activity activity) {
mActivity = activity;
// 返回一个与主题Theme定义的 attrs数组对应的typedArray类型数组
TypedArray a = activity.getTheme().obtainStyledAttributes(new int[]{
android.R.attr.windowBackground
});
// 获取typedArray数组中指定位置的资源id值
int background = a.getResourceId(0, 0);
a.recycle();

// 返回顶层窗口根视图
ViewGroup decor = (ViewGroup) activity.getWindow().getDecorView();
//得到我activity的xml的根view
ViewGroup decorChild = (ViewGroup) decor.getChildAt(0);
// 给顶层窗口根视图的根view设置背景资源
decorChild.setBackgroundResource(background);
// 移除activity的xml的根view
decor.removeView(decorChild);
//给SwipeBackLayout添加子view,作为SwipeBackLayout的第一个view
addView(decorChild);
//把decorChild当成SwipeBackLayout的contentView进行设置
setContentView(decorChild);
decor.addView(this);
}
    这样View就和Activiy建立起关联了,只需要把decorChild传递进去,SwipeBackLayout作为他的父View,结构如下:



版本兼容

    SwipeBackLayout绘在5.0下的版本出现透明度显示问题,为了兼容5.0以下的版本,在convertActivityToTranslucent做了一个版本兼容。在Utils类中做了转换:

public static void convertActivityToTranslucent(Activity activity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
convertActivityToTranslucentAfterL(activity);
} else {
convertActivityToTranslucentBeforeL(activity);
}
}
    主要内容如下:

    1、通过关键字段反射获得透明度设置的类名,方法名称,

    2、传递参数手动调用透明度设置的方法。

    3、另外在使用时候还需要在应用的主题下添加:

    <item name="android:windowIsTranslucent">true</item>

Window设置

    Activity的Window默认颜色是白色的,如果不进行设置就会遮住下面的Activity,所在加载布局之前需要设置当前窗口颜色。在SwipeBackActivityHelper类中有个onActivityCreate方法就做了如下处理:
1、设置当前的Window为透明色
2、去掉Activity根视图的背景颜色

public void onActivityCreate() {
//加载布局之前设置当前窗口颜色
mActivity.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
mActivity.getWindow().getDecorView().setBackgroundDrawable(null);
……………………………………………………………………………………省略部分代码…………………………………………………………………………………
}


    以上就是SwipeBackLayout的实现原理,接下来看看SwipeBackActivity。

SwipeBackActivity

    1、SwipeBackActivity 需要实现接口SwipeBackActivityBase,重写以下三个方法:

     (1) getSwipeBackLayout:获取SwipeBackLayout控件

     (2)setSwipeBackEnable:设置是否支持手势滑动

     (3)scrollToFinishActivity:滑动结束后关闭Activity

    2、onCreate

    这里重写onActivityCreate方法,调用SwipeBackActivityHelper中的onActivityCreate方法:

    (1)加载布局前设置window的颜色

    (2)加载布局前解决滑动时候5.0之前黑屏闪动的bug兼容

    3、onPostCreate

    这里调用attachToActivity方法,在onStart之后把当前的SwipeBackLayout设置为activity的顶层view。

    4、其他设置,例如:获取SwipeBackLayout布局,设置是否支持触摸等。

用法

    1、引入库文件或者添加依赖

        compile 'me.imid.swipebacklayout.lib:library:1.1.0'

    2、继承自SwipeBackActivity
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息