蒙层引导在我们项目中一直的做法都是让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;}
获取到高亮位置后,下一步就是如何高亮?通常的做法就是将高亮位置从整个屏幕当中挖出来,改变屏幕其它位置的背景。
如下图所示:
![](https://img.haomeiwen.com/i15405328/09724065a0e10f5d.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 这个参数。它的作用就是在裁剪下多个区域时,当这些区域有重叠的时候,决定重叠部分该如何处理,多次裁剪之后究竟获得了哪个区域。
![](https://img.haomeiwen.com/i15405328/0dae69a90b3060fb.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来确定。
具体规则如下:
![](https://img.haomeiwen.com/i15405328/412245e14744a4c6.png)
![](https://img.haomeiwen.com/i15405328/9f72534dea09cab6.png)
![](https://img.haomeiwen.com/i15405328/0e119f00915aded8.png)
到此,一个引导库的核心原理已经全部分析完毕。
其它功能分析
1. 多个引导分步骤如何实现?
可以先定义一个Java Bean对象,用来封装需要的一些参数,如需要引导的视图,提示视图等。然后用集合来保存,每次从集合中取第一个元素中的提示视图,添加到GuideView中,每次添加时,GuideView都要移除所有子视图,保证每次只显示一个引导。同时,添加完毕后,从集合中移除这个元素。这里应该能够想到集合应该用个队列来实现,Java的集合体系中,支持队列的有Deque接口,所有这里可以使用ArrayDeque或者LinkedList。
2. 如何控制引导的显示次数?
这个很容易想到用SP来控制。每次往SP中存取次数,默认只允许一次。当然还可以设定次数上限,或者永久显示用于调试。
【附】相关架构及资料
![](https://img.haomeiwen.com/i15233854/b85267d025040403.png)
资料领取
点赞+加群免费获取 Android IOC架构设计
加群领取获取往期Android高级架构资料、源码、笔记、视频。高级UI、性能优化、架构师课程、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter全方面的Android进阶实践技术,群内还有技术大牛一起讨论交流解决问题。
网友评论