- 手指从屏幕侧边一定范围内滑出,贴边出现黑底白箭头View。不贴边无响应。
- 位移时View跟随触点变化。其X轴影响形状(凸起幅度/透明度/箭头形状),Y轴影响位置。当滑动到某阈值位置后View不再跟随X轴变形。
- View不再跟随X轴变形时(即滑动距离大于某阈值),松手会触发返回事件。
以上拆解,确定要监听OnTouchListener
,1、2、3情景分别对应ACTION_DOWN
、ACTION_MOVE
、ACTION_UP
。
ACTION_DOWN
- 判断按下点的X轴位置downX
是否靠近边缘,即event.getRawX()
是否小于某值。ACTION_MOVE
- 黑底白箭头View肯定要自定义实现。View跟随位移距离moveXLength
(即event.getRawX() - downX
,同View当前宽度*某系数)变化,同时设置View的topMargin为event.getRawY() - viewHeight/2
,以达到定位效果。当moveXLength
大于某阈值时,不再跟随其变化。ACTION_UP
- 手指抬起时,moveXLength
大于某阈值(即View最大宽度*某系数)则触发事件,事件需要定义一个接口回调给外部。
我们要监听谁的OnTouchListener
?
要在整个Activity响应,且与具体布局ContentView
无关。随后就想到了decorView
。
根据上述分析,我们需要自定义一个黑底白箭头View - SlideBackIconView。将黑底白箭头View添加到decorView
中,在OnTouchListener
事件中做上文分析的操作。
创建SlideBackIconView直接继承View,绘制黑底时需要贝塞尔曲线(可用css贝塞尔曲线可视化测试参数),绘制白箭头直接画个折线即可。
需要注意的点:
- 黑底是整个背景,所以画笔需要设置填充,
Paint.Style.FILL_AND_STROKE
或Paint.Style.FILL
。 - 白箭头是折线,画笔描边
Paint.Style.STROKE
,需设置结合处为圆弧,Paint.setStrokeJoin(Paint.Join.ROUND)
(如果不设置则拐角处棱角戳出来很难看)。 onDraw
时需要先Path.reset()
重置路径对象,因为onDraw会多次调用。- 注意位移过程中的效果,View随位移距离变化时,白箭头是透明度渐变,同时直线由短边长再折弯成角。由此可知,需要用View最大宽度与当前宽度的比例来控制透明度、线段长度与角度。
省略其他set方法,核心代码如下:
/**
* 因为过程中会多次绘制,所以要先重置路径再绘制。
* 贝塞尔曲线没什么好说的,相关文章有很多。此曲线经我测试比较类似“即刻App”。
* <p>
* 方便阅读再写一遍,此段代码中的变量定义:
* 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
}
在一个单独的管理器里,传入Activity对象,用以获取该Activity的decorView
对象,将黑底白箭头View添加进去并设置触摸监听OnTouchListener
- SlideBackManager。
监听事件的逻辑上面已经分析过,核心代码如下:
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;
}
});
侧滑管理器中两个主要方法:
- 注册 - 传入Activity对象,获取对应的
decorView
,将上述自定义的View添加进布局container.addView(slideBackIconView)
,并setOnTouchListener
。 - 解绑 - 将管理器中的变量置空垃圾回收。此操作不做也可以,无引用时会自动回收。
方便统一管理,我们使用WeakHashMap<Activity, SlideBackManager>
去管理每个Activity对应的侧滑管理器。注册时加入map,解绑时移除。同时因为WeakHashMap
内部使用弱引用,也避免了误操作导致的内存泄漏。
其他不详述了,具体参见SlideBack。
身为一个程序猿,抽象思维很重要。脱离现象去看本质,把现象拆解成好几步,只要你拆解得够细致,每一步都有可以实现的代码。串起来就是一个实现思路,有了思路再实践去验证。做了就明白了。
另外,多学多看。Github那么多优质开源项目可学习,没事多看看,从中学习别人思路与逻辑,理解转化成自己的东西。长此以往好处不言而喻。
PS:看完还有不懂的话,请阅读源码。还有问题可以issues提问。