#### SlideBack库:https://github.com/ParfoisMeng/SlideBack --- ### 先看效果,做分析 从即刻效果图(下图)分析一下: ![即刻效果图](https://github.com/ParfoisMeng/SlideBack/raw/master/screenshot/jike.gif) ##### 拆解情景: 1. 手指从屏幕侧边一定范围内滑出,贴边出现黑底白箭头View。不贴边无响应。 2. 位移时View跟随触点变化。其X轴影响形状(凸起幅度/透明度/箭头形状),Y轴影响位置。当滑动到某阈值位置后View不再跟随X轴变形。 3. View不再跟随X轴变形时(即滑动距离大于某阈值),松手会触发返回事件。 ##### 初步思考: 以上拆解,确定要监听`OnTouchListener`,1、2、3情景分别对应`ACTION_DOWN`、`ACTION_MOVE`、`ACTION_UP`。 1. `ACTION_DOWN` - 判断按下点的X轴位置`downX`是否靠近边缘,即`event.getRawX()`是否小于某值。 2. `ACTION_MOVE` - 黑底白箭头View肯定要自定义实现。View跟随位移距离`moveXLength`(即`event.getRawX() - downX`,同View当前宽度*某系数)变化,同时设置View的topMargin为`event.getRawY() - viewHeight/2`,以达到定位效果。当`moveXLength`大于某阈值时,不再跟随其变化。 3. `ACTION_UP` - 手指抬起时,`moveXLength`大于某阈值(即View最大宽度*某系数)则触发事件,事件需要定义一个接口回调给外部。 我们要监听谁的`OnTouchListener`? 要在整个Activity响应,且与具体布局`ContentView`无关。随后就想到了`decorView`。 --- ### 分析完毕,开搞 根据上述分析,我们需要自定义一个*黑底白箭头View* - [SlideBackIconView](https://github.com/ParfoisMeng/SlideBack/blob/master/slidebacklib/src/main/java/com/parfoismeng/slidebacklib/widget/SlideBackIconView.java)。将*黑底白箭头View*添加到`decorView`中,在`OnTouchListener`事件中做上文分析的操作。 ##### 黑底白箭头的自定义View 创建SlideBackIconView直接继承View,绘制黑底时需要贝塞尔曲线(可用[css贝塞尔曲线可视化](http://cubic-bezier.com/)测试参数),绘制白箭头直接画个折线即可。 需要注意的点: 1. 黑底是整个背景,所以画笔需要设置填充,`Paint.Style.FILL_AND_STROKE`或`Paint.Style.FILL`。 2. 白箭头是折线,画笔描边`Paint.Style.STROKE`,需设置结合处为圆弧,`Paint.setStrokeJoin(Paint.Join.ROUND)`(如果不设置则拐角处棱角戳出来很难看)。 3. `onDraw`时需要先`Path.reset()`重置路径对象,因为onDraw会多次调用。 4. 注意位移过程中的效果,View随位移距离变化时,白箭头是透明度渐变,同时直线由短边长再折弯成角。由此可知,需要用View最大宽度与当前宽度的比例来控制透明度、线段长度与角度。 省略其他set方法,核心代码如下: ```java /** * 因为过程中会多次绘制,所以要先重置路径再绘制。 * 贝塞尔曲线没什么好说的,相关文章有很多。此曲线经我测试比较类似“即刻App”。 *
* 方便阅读再写一遍,此段代码中的变量定义:
* backViewHeight 控件高度
* slideLength 当前拉动距离
* maxSlideLength 最大拉动距离
* arrowSize 箭头图标大小
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 背景
bgPath.reset(); // 会多次绘制,所以先重置
bgPath.moveTo(0, 0);
bgPath.cubicTo(0, backViewHeight * 2 / 9, slideLength, backViewHeight / 3, slideLength, backViewHeight / 2);
bgPath.cubicTo(slideLength, backViewHeight * 2 / 3, 0, backViewHeight * 7 / 9, 0, backViewHeight);
canvas.drawPath(bgPath, bgPaint); // 根据设置的贝塞尔曲线路径用画笔绘制
// 箭头是先直线由短变长再折弯变成箭头状的
// 依据当前拉动距离和最大拉动距离计算箭头大小值
// 大小到一定值后开始折弯,计算箭头角度值
float arrowZoom = slideLength / maxSlideLength; // 箭头大小变化率
float arrowAngle = arrowZoom < 0.75f ? 0 : (arrowZoom - 0.75f) * 2; // 箭头角度变化率
// 箭头
arrowPath.reset(); // 先重置
// 结合箭头大小值与箭头角度值设置折线路径
arrowPath.moveTo(slideLength / 2 + (arrowSize * arrowAngle), backViewHeight / 2 - (arrowZoom * arrowSize));
arrowPath.lineTo(slideLength / 2 - (arrowSize * arrowAngle), backViewHeight / 2);
arrowPath.lineTo(slideLength / 2 + (arrowSize * arrowAngle), backViewHeight / 2 + (arrowZoom * arrowSize));
canvas.drawPath(arrowPath, arrowPaint);
setAlpha(slideLength / maxSlideLength - 0.2f); // 最多0.8透明度
}
/**
* 更新当前拉动距离并重绘
*
* @param slideLength 当前拉动距离
*/
public void updateSlideLength(float slideLength) {
this.slideLength = slideLength;
invalidate(); // 会再次调用onDraw
}
```
##### `decorView`的`OnTouchListener`事件
在一个单独的管理器里,传入Activity对象,用以获取该Activity的`decorView`对象,将*黑底白箭头View*添加进去并设置触摸监听`OnTouchListener` - [SlideBackManager](https://github.com/ParfoisMeng/SlideBack/blob/master/slidebacklib/src/main/java/com/parfoismeng/slidebacklib/SlideBackManager.java)。
监听事件的逻辑上面已经分析过,核心代码如下:
```
FrameLayout container = (FrameLayout) activity.getWindow().getDecorView();
container.addView(slideBackIconView);
container.setOnTouchListener(new View.OnTouchListener() {
private boolean isSideSlide = false; // 是否从边缘开始滑动
private float downX = 0; // 按下的X轴坐标
private float moveXLength = 0; // 位移的X轴距离
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: // 按下
downX = event.getRawX(); // 更新按下点的X轴坐标
if (downX <= sideSlideLength) { // 检验是否从边缘开始滑动
isSideSlide = true;
}
break;
case MotionEvent.ACTION_MOVE: // 移动
if (isSideSlide) { // 是从边缘开始滑动
moveXLength = Math.abs(event.getRawX() - downX); // 获取X轴位移距离
if (moveXLength / dragRate <= maxSlideLength) {
// 如果位移距离在可拉动距离内,更新SlideBackIconView的当前拉动距离并重绘
slideBackIconView.updateSlideLength(moveXLength / dragRate);
}
// 根据Y轴位置给SlideBackIconView定位
setSlideBackPosition(slideBackIconView, (int) (event.getRawY()));
}
break;
case MotionEvent.ACTION_UP: // 抬起
// 是从边缘开始滑动 且 抬起点的X轴坐标大于某值(默认3倍最大滑动长度)
if (isSideSlide && moveXLength / dragRate >= maxSlideLength) {
if (null != SlideBackManager.this.callBack) {
// 不为空则响应回调事件
SlideBackManager.this.callBack.onSlideBack();
}
}
isSideSlide = false; // 从边缘开始滑动结束
slideBackIconView.updateSlideLength(0); // 恢复0
break;
}
return isSideSlide;
}
});
```
##### 其他
侧滑管理器中两个主要方法:
1. 注册 - 传入Activity对象,获取对应的`decorView`,将上述自定义的View添加进布局`container.addView(slideBackIconView)`,并`setOnTouchListener`。
2. 解绑 - 将管理器中的变量置空垃圾回收。此操作不做也可以,无引用时会自动回收。
方便统一管理,我们使用`WeakHashMap