目录
概述
前几周app改版,在修改老代码的过程中发现了一个指引,让我想起很久以前项目里指引实现是在布局文件中添加布局,并在代码中插入很多非业务的代码,这样写感觉不好。指引本只是一个不太重要,可能经常变动的功能,说不定下个版本又改了,当它和正常业务耦合在一起以后,就显得代码有点混乱了。有没有一种方法,可以无缝嵌入,将指引和正常业务彻底解耦?前几天早晨,几个公众号都发了同样一篇博客来抠个图吧~——更优雅的Android UI界面控件高亮的实现,看到这篇博客的时候有种醍醐灌顶的感觉,这不正是我想要的指引吗?看完博客中的实现原理后,决定动手重复造一个轮子,本文简单分析一下这个轮子是如何实现的,并分析了一下优缺点。下面先看个效果:
指引需求分析
不谈技术实现,首先分析一下指引这个需求本身。
入门级指引
入门级的指引,就是最简单的指引,在app安装新版本或者覆盖安装新版本后第一时间弹出来几张图片。为了突出某些功能,会高亮显示一些内容,同时还有一些指示性的箭头,或者在高亮旁边有文本描述。
升级版指引
升级版指引,在用户第一次进入到个页面的时候,告诉用户哪几个按钮有是做什么的。指引内容和入门级的差不多,高亮显示View,箭头、文本描述,点击高亮View后跑到下一步指引直到指引结束。
指引需求的抽象
简单指引一般由UI切图就好,这里以app内部的指引需求分析指引,如图(图片是随便找的),指引一般包括以下内容:
- 满屏幕的半透明遮罩层;
- 高亮显示突出显示底层app页面的某个或者某些View;
- 在高亮显示的View旁边可能有一些带文本的图片,或者指示方向的图片+文本;
- 高亮部分可以响应点击事件,并且很有可能点击后继续显示下一步指引,直到显示完。
指引的技术实现
分析了指引的需求后,得到指引的基本元素,决定用自定义View实现,命名这个自定义View为GuideView。实现整个指引流程如下:
1.定义指引绘制要素Shape,及其派生类:Rectangle、Oval、BitmapDecoration、TextDecoration;
2.定义每一步指引的信息GuideInfo,并获取高亮区域的坐标矩形;
3.定义GuideView继承View,绘制指引要素:Shape;
4.定义GuideManager,管理多步骤指引;
5.定义GuideDialog承载GuideView,覆盖在页面上,和GuideManager
指引的要素:Shape
指引的基本要素包括:高亮显示的View区域,图片,文本等饰品。定义个接口,命名为Shape,那么Shape有子类:高亮的矩形(Rectangle),高亮的圆形(Oval),图片(BitmapDecoration),文本(TextDecoration)。指引要素实现代码如下:
interface Shape {
fun draw(canvas: Canvas, paint: Paint)
}
class Oval(private val rect: RectF) : Shape {
override fun draw(canvas: Canvas, paint: Paint) {
canvas.drawOval(rect, paint)
}
}
class Rectangle(
private val rect: RectF,
private val xRadius: Float = 0F,
private val yRadius: Float = 0F
) : Shape {
override fun draw(canvas: Canvas, paint: Paint) {
canvas.drawRoundRect(rect, xRadius, yRadius, paint)
}
}
class BitmapDecoration(
private val bitmap: Bitmap,
private val left: Float,
private val top: Float
) : Shape {
override fun draw(canvas: Canvas, paint: Paint) {
canvas.drawBitmap(bitmap, left, top, paint)
}
}
class TextDecoration(
protected val text: String, // 要绘制的文本
protected val textSize: Float, // 字体大小
protected val textColor: Int, // 字体颜色
protected val startX: Float, // x轴起点(left)
protected val startY: Float, // y轴七点(top)
protected val bold: Boolean = false // 粗体
) : Shape {
override fun draw(canvas: Canvas, paint: Paint) {
paint.color = textColor
paint.textSize = textSize
paint.typeface = if (bold) {
Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD)
} else {
Typeface.DEFAULT_BOLD
}
canvas.drawText(text, startX, startY, paint)
}
}
封装指引步骤:GuideInfo
上面介绍了指引要素Shape,接下来需要继续完成指引要素的封装,命名为GuideInfo。GuideInfo封装了一个指引页面(或者说一帧)包含的所有显示要素,也就是多个Shape,包括:高亮的View区域,图片,文本等。GuideView显示指引,也就是把一个GuideInfo对象的Shape绘制出来。
class GuideInfo(
private val targetView: View, // 高亮显示的,要指引的View
val padding: Int = 0, // 高亮区域的padding(如果要显示大一些时可设置padding)
val isOval: Boolean = false, // 高亮区域是否时圆形
val radius: Float = 0F, // 如果时矩形,那么可以设置圆角
private val paddingLeft: Int = 0, // 四个方向的padding
private val paddingTop: Int = 0,
private val paddingRight: Int = 0,
private val paddingBottom: Int = 0,
autoShape: Boolean = false // 是否使用自定义的高亮区域,true: 自动根据View的Background获取Shape
) {
val mShapes = mutableListOf<Shape>()
var mTargetHighlightShape: Shape? = null // 这就是高亮显示的地方
val targetBound: RectF // 高亮View的矩形区域,可根据这个矩形设置其它Shape的位置
}
一个GuideInfo对象代表一个指引步骤(一帧),一个完整的指引,可能包含多个步指引
绘制指引要素:GuideView
绘制指引的大概流程如下:
- 绘制一个半透明遮罩层;
- 绘制高亮显示的View区域;
- 绘制其它装饰,如图片,文本等。
代码如下:
class GuideView : View, View.OnTouchListener, GestureDetector.