进入全面屏时代,iPhone 的交互是业界标杆,所以Android阵营都向其靠拢.小米全面屏手势是我挺喜欢的交互.所以模仿下小米的返回操作.其返回逻辑远不如微信返回动画复杂,几乎没有机型适配问题的存在.
动画效果如图
看到动画效果我们需要达到的效果是在 Activity 的 DecorView 上绘制此 View,所以就像当于我们所有页面的的根布局需要添加此 View,如果每个页面都手动添加,则显得太过麻烦,显然在抽象的 BaseActivity 中 setContentView 方法之后调用activity.getWindow().getDecorView()
,然后获取 content 根布局FrameLayout content = viewGroup.findViewById(android.R.id.content); content.addView(slideBackView);
将我们带有返回 View 的布局添加到 FameLayout 的顶层,这样就达到的最小化的修改代码实现效果.
对于返回图形的绘制,主要运用了触摸事件、贝塞尔曲线等知识点
- 触摸事件,在 View 内重写 onTouchEvent 方法,判断左右滑动并且根据滑动距离绘制返回图形宽度
- 对于返回图形可以余弦公式,也使用贝塞尔曲线画成,但贝塞尔曲线看起来更加顺滑自然,但是 Android 原生最高支持三阶,对于我们所要绘制的图形三阶会不够自然,使用六阶会比较好,但是又不支持,所以只能由三个二阶组合成返回图形
View触摸事件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101
| private float thresholdLeft;
private float thresholdRight;
private float backMaxWidth;
private float backEdgeWidth;
private float bufferX;
@Override public boolean onTouchEvent(MotionEvent ev) { float currentX = ev.getX(); currentY = ev.getY(); switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: downX = ev.getX(); forwardX = downX; if (downX <= backEdgeWidth) { isEdge = true; left = true; right = false; } else if (downX >= getWidth() - backEdgeWidth) { isEdge = true; right = true; left = false; } break; case MotionEvent.ACTION_MOVE: deltaX = currentX - downX; float diff = forwardX - currentX; if (diff > 0) { if (currentX < thresholdLeft && left) { deltaX = backMaxWidth; deltaX -= (thresholdLeft - currentX) / 2; bufferX = deltaX; if (deltaX < 0) { deltaX = 0; bufferX = 0; } } else if ((currentX > thresholdRight) && right) { bufferX -= diff; deltaX = bufferX; } } else { if ((currentX < thresholdLeft) && left) { bufferX += Math.abs(diff); deltaX = bufferX; } else if (currentX > thresholdRight && right) { deltaX = -backMaxWidth; deltaX += (currentX - thresholdRight) / 2; bufferX = deltaX; if (deltaX < -backMaxWidth) { deltaX = -backMaxWidth; bufferX = -backMaxWidth; } } } forwardX = currentX; if (isEdge) { invalidate(); } break; case MotionEvent.ACTION_UP: if (isEdge) { if (deltaX >= backMaxWidth && left) { back(); } else if (Math.abs(deltaX) >= backMaxWidth && right) { if (!isOnlyLeftBack) { back(); } } deltaX = 0; invalidate(); } left = false; right = false; isEdge = false; bufferX = 0; break; default: break; } return isEdge; }
|
通过以上代码,分析了手势与绘制图形的关系,具体逻辑可以根据效果分析,视差部分的效果可以根据自身进行调整.
贝塞尔曲线绘制
使用贝塞尔曲线绘制返回图形和里面的返回图标.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
| private Paint backPaint; private Paint arrowPaint; private Path backPath; private Path arrowPath;
private void init() { backPath = new Path(); arrowPath = new Path();
backPaint = new Paint(Paint.ANTI_ALIAS_FLAG); backPaint.setColor(0xAA000000); backPaint.setStyle(Paint.Style.FILL_AND_STROKE);
arrowPaint = new Paint(Paint.ANTI_ALIAS_FLAG); arrowPaint.setColor(Color.WHITE); arrowPaint.setStyle(Paint.Style.STROKE); arrowPaint.setStrokeWidth(6); }
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (deltaX > backMaxWidth && left) { deltaX = backMaxWidth; } else if (deltaX < -backMaxWidth && right) { deltaX = -backMaxWidth; } float deltaY = currentY - backViewHeight / 2; backPath.reset(); arrowPath.reset();
if (deltaX > 0 && left) { backPath.moveTo(0, deltaY); backPath.quadTo(0, backViewHeight / 4 + deltaY, deltaX / 3, backViewHeight * 3 / 8 + deltaY); backPath.quadTo(deltaX * 5 / 8, backViewHeight / 2 + deltaY, deltaX / 3, backViewHeight * 5 / 8 + deltaY); backPath.quadTo(0, backViewHeight * 6 / 8 + deltaY, 0, backViewHeight + deltaY); canvas.drawPath(backPath, backPaint);
arrowPath.moveTo(deltaX / 6 + (15 * (deltaX / (width / 6))), backViewHeight * 15 / 32 + deltaY); arrowPath.lineTo(deltaX / 6, backViewHeight * 16.1f / 32 + deltaY); arrowPath.moveTo(deltaX / 6, backViewHeight * 15.9f / 32 + deltaY); arrowPath.lineTo(deltaX / 6 + (15 * (deltaX / (width / 6))), backViewHeight * 17 / 32 + deltaY); canvas.drawPath(arrowPath, arrowPaint); } else if (deltaX < 0 && right) { if (!isOnlyLeftBack) { deltaX = Math.abs(deltaX); backPath.moveTo(width, deltaY); backPath.quadTo(width, backViewHeight / 4 + deltaY, width - deltaX / 3, backViewHeight * 3 / 8 + deltaY); backPath.quadTo(width - deltaX * 5 / 8, backViewHeight / 2 + deltaY, width - deltaX / 3, backViewHeight * 5 / 8 + deltaY); backPath.quadTo(width, backViewHeight * 6 / 8 + deltaY, width, backViewHeight + deltaY); canvas.drawPath(backPath, backPaint);
arrowPath.moveTo(width - deltaX / 6 - (15 * (deltaX / (width / 6))), backViewHeight * 15 / 32 + deltaY); arrowPath.lineTo(width - deltaX / 6, backViewHeight * 16.1f / 32 + deltaY); arrowPath.moveTo(width - deltaX / 6, backViewHeight * 15.9f / 32 + deltaY); arrowPath.lineTo(width - deltaX / 6 - (15 * (deltaX / (width / 6))), backViewHeight * 17 / 32 + deltaY); canvas.drawPath(arrowPath, arrowPaint); } } setAlpha(deltaX / backMaxWidth); }
|
返回图形及返回坐标的形状都可以通过这是不同的结束点和控制点去调节,形成自己想要的效果,不过最好先通过绘图软件,分析好想要的图形及其绘制比例.
总结
SlideBack用法
1 2
| SlideBack.Builder().init(layoutResID).build(this)
|
使用自定义 View 可以作出各种设计师想实现的动画效果,一般使用 onTouchEvent、onMeasure、onLayout、onDraw 等方法,根据实际情况重写这些方法即可实现.通过实现这个效果可以更好的理解 Android 中各种设计,提升自己开发水平.
项目地址
GitHub项目地址 里面有一些 library 集合
cn.libery:slideback:latest