蒙层引导在我们项目中一直的做法都是让 UI 直接切一整张静态图, 这样的做法虽然省事, 但带来的后果就是适配性太差, 还会出现引导图和下面真正的界面不符的情况, 让用户感到莫名其妙. 因此, 就有必要自定义一个蒙层引导视图来解决这个问题. 本篇文章主要是对核心原理实现的剖析.
核心原理分析
自定义引导视图 (GuideView) 其实最主要的是需要解决三个问题:
引导视图应该是按需加载, 在需要展示时浮在整个页面上, 展示完毕后从页面视图中移除
确定需要引导的视图位置, 并将之高亮
支持自定义提示视图, 并根据高亮视图的位置摆放
对于第一个问题, 很容易想到 GuideView 应该继承自帧布局, 这样可以保证浮在整个页面上. 按需加载的话, 可以在需要展示引导时, 添加到页面的根视图 (即 DecorView) 上, 展示完毕后再从根视图中移除即可解决. 并且为了让蒙层布满全屏, 应该在添加时, 指定宽高为 Decorview 的宽高.
对于第二个问题, 高亮视图的位置, 可以通过系统提供的获取屏幕位置的 API 来解决
public static RectF getRectOnScreen(View view) { if (view == null) { return new RectF(); } RectF result = new RectF(); int[] pos = new int[2]; view.getLocationOnScreen(pos); result.left = pos[0]; result.top = pos[1]; result.right = result.left + view.getMeasuredWidth(); result.bottom = result.top + view.getMeasuredHeight(); return result;}
获取到高亮位置后, 下一步就是如何高亮? 通常的做法就是将高亮位置从整个屏幕当中挖出来, 改变屏幕其它位置的背景.
如下图所示:
image.PNG
要实现这样的效果, 就需要知道画布裁剪的知识.
裁剪共分为: 裁剪路径, 裁剪矩形, 裁剪区域. 裁剪后, 只能编辑该区域, 其它的区域并没有消失!
这里, 我们可以选择裁剪路径. 因为一般的引导库为了能够更好地引导用户, 会在高亮区周围会绘制一些内容.
而路径 (path) 很好地封装了由直线和曲线构的几何图形, 如添加圆形, 矩形, 圆角矩形, 椭圆形甚至一些复杂图形. 因此在确定好高亮区位置后, 我们也可以根据位置信息通过 path 完成一些图片的绘制, 如圆形:
mPath.addCircle(mDrawRect.centerX(), mDrawRect.centerY(), radius, Path.Direction.CW);
mDrawRect 就是高亮区的位置. 注意: 这里只是在 path 中描述了图形的轮廓.
继续说裁剪路径. 裁剪路径的 API 有以下两个.
// 方法 1public boolean clipPath(@NonNull Path path)// 方法 2public boolean clipPath(@NonNull Path path, @NonNull Region.Op op)
方法 1 默认调用了方法 2, 只是第二个参数传的值是 Region.Op.INTERSECT
/** * Intersect the current clip with the specified path. * * @param path The path to intersect with the current clip * @return true if the resulting clip is non-empty */public boolean clipPath(@NonNull Path path) { return clipPath(path, Region.Op.INTERSECT);}
下面一张图解释下 Region.Op 这个参数. 它的作用就是在裁剪下多个区域时, 当这些区域有重叠的时候, 决定重叠部分该如何处理, 多次裁剪之后究竟获得了哪个区域.
image.PNG
这里我们需要的是先裁剪出高亮区之外的区域, 因为要绘制蒙层背景, 但不能绘制到高亮区. 所以选择的参数应该是 Region.Op.DIFFERENCE.
裁剪后, 绘制需要的背景.
绘制完背景后, 如果有需要, 还可通过画布可以将上面的 path 中描述的图形再绘制出来.
@Overrideprotected void onDraw(Canvas canvas) { super.onDraw(canvas); // 以下为伪代码 // 第一步: 裁剪非高亮区 canvas.clipPath(guideShape.getShapePath(), Region.Op.DIFFERENCE); // 第二步: 绘制蒙层背景 canvas.drawColor(mBgColor); // 第三步: 如果需要, 绘制高亮区内容 if (mEnableDrawShape) { guideShape.onDraw(canvas); }}
注意: ViewGroup 默认是不绘制的, 因此这里要绘制的话, 需要将开关打开.
// 需要重写 onDraw 设置为 falsesetWillNotDraw(false);
第三个问题, 外部可以传入布局 Id, 也可以直接传入 View, 然后将提示视图添加到自定义的 GuideView 中. 如果传入的是 ID, 我们内部可以使用布局解析器解析出来, 然后再添加.
添加之后, 如何摆放呢? 这里我们可以通过 Margin 和 Gravity 来确定.
具体规则如下:
image.PNG
image.PNG
image.PNG
到此, 一个引导库的核心原理已经全部分析完毕.
其它功能分析
1. 多个引导分步骤如何实现?
可以先定义一个 Java Bean 对象, 用来封装需要的一些参数, 如需要引导的视图, 提示视图等. 然后用集合来保存, 每次从集合中取第一个元素中的提示视图, 添加到 GuideView 中, 每次添加时, GuideView 都要移除所有子视图, 保证每次只显示一个引导. 同时, 添加完毕后, 从集合中移除这个元素. 这里应该能够想到集合应该用个队列来实现, Java 的集合体系中, 支持队列的有 Deque 接口, 所有这里可以使用 ArrayDeque 或者 LinkedList.
2. 如何控制引导的显示次数?
这个很容易想到用 SP 来控制. 每次往 SP 中存取次数, 默认只允许一次. 当然还可以设定次数上限, 或者永久显示用于调试.
[附] 相关架构及资料
image
资料领取
点赞 + 加群免费获取 Android IoC 架构设计
加群领取获取往期 Android 高级架构资料, 源码, 笔记, 视频. 高级 UI, 性能优化, 架构师课程, NDK, 混合式开发 (ReactNative+Weex) 微信小程序, Flutter 全方面的 Android 进阶实践技术, 群内还有技术大牛一起讨论交流解决问题.
来源: http://www.jianshu.com/p/ed53e3d39fff