前几周app改版,在修改老代码的过程中发现了一个指引,让我想起很久以前项目里指引实现是在布局文件中添加布局,并在代码中插入很多非业务的代码,这样写感觉不好。指引本只是一个不太重要,可能经常变动的功能,说不定下个版本又改了,当它和正常业务耦合在一起以后,就显得代码有点混乱了。有没有一种方法,可以无缝嵌入,将指引和正常业务彻底解耦?前几天早晨,几个公众号都发了同样一篇博客来抠个图吧~——更优雅的Android UI界面控件高亮的实现,看到这篇博客的时候有种醍醐灌顶的感觉,这不正是我想要的指引吗?看完博客中的实现原理后,决定动手重复造一个轮子,本文简单分析一下这个轮子是如何实现的,并分析了一下优缺点。下面先看个效果: 不谈技术实现,首先分析一下指引这个需求本身。 入门级的指引,就是最简单的指引,在app安装新版本或者覆盖安装新版本后第一时间弹出来几张图片。为了突出某些功能,会高亮显示一些内容,同时还有一些指示性的箭头,或者在高亮旁边有文本描述。 升级版指引,在用户第一次进入到个页面的时候,告诉用户哪几个按钮有是做什么的。指引内容和入门级的差不多,高亮显示View,箭头、文本描述,点击高亮View后跑到下一步指引直到指引结束。 简单指引一般由UI切图就好,这里以app内部的指引需求分析指引,如图(图片是随便找的),指引一般包括以下内容: 分析了指引的需求后,得到指引的基本元素,决定用自定义View实现,命名这个自定义View为GuideView。实现整个指引流程如下: 指引的基本要素包括:高亮显示的View区域,图片,文本等饰品。定义个接口,命名为Shape,那么Shape有子类:高亮的矩形(Rectangle),高亮的圆形(Oval),图片(BitmapDecoration),文本(TextDecoration)。指引要素实现代码如下: 上面介绍了指引要素Shape,接下来需要继续完成指引要素的封装,命名为GuideInfo。GuideInfo封装了一个指引页面(或者说一帧)包含的所有显示要素,也就是多个Shape,包括:高亮的View区域,图片,文本等。GuideView显示指引,也就是把一个GuideInfo对象的Shape绘制出来。 一个GuideInfo对象代表一个指引步骤(一帧),一个完整的指引,可能包含多个步指引 绘制指引的大概流程如下: 代码如下: 一个GuideInfo代表一帧指引,一个完整的指引可能会包含多帧,所以,指引流程需要定义一个管理类GuideManager,作为中间层,上对接用户,下对接GuideDialog,比较简单,就是定义了一个GuideInfo数组和一个指向当前步骤的指针,还定义了一些回调,直接贴代码吧: 这个就更简单了,直接看代码: 这里举个栗子,自己再封装一下,就算是一行代码接入,代码中的imgLogo,imgLogo2,tvName分别是页面上的ImageView和TextView: 通过View#getLocationOnScreen()方法可以获得目标View左上角顶点的屏幕坐标,已知目标View的宽高,然后可以得到高亮View的矩形(划重点:是屏幕坐标,后面绘制需要根据GuideView左上角坐标做一次变换,这样绘制出来刚好和目标View重叠): 如代码所示,绘制高亮View实际是从半透明背景中把目标View的区域抠出来,这样底层View就能正常显示,对比半透明层就是高亮效果了。关键点一,绘制完半透明背景后,需要转换一次坐标,xy就是GuideView左上角顶点坐标;关键点二,mPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT),有兴趣可以深入学习一下,这里不解析(自己不懂,就不要忽悠别人)。 上面已经得到了高亮View的矩形区域RectF,只要监听onTouch,判断事件是否落在这个矩形内即可。通过GestureDetector接管onTouch事件,可以轻松实现高亮View区域的点击事件。 优点: 缺点: 代码不多,三五百行,核心的就那么几十行,下面附上项目githug地址: 最后特别感谢来抠个图吧~——更优雅的Android UI界面控件高亮的实现的作者,如果没有他的思路我也难以实现这个指引小项目,也就没有本文。目录
概述
指引需求分析
入门级指引
升级版指引
指引需求的抽象
指引的技术实现
1.定义指引绘制要素Shape,及其派生类:Rectangle、Oval、BitmapDecoration、TextDecoration;
2.定义每一步指引的信息GuideInfo,并获取高亮区域的坐标矩形;
3.定义GuideView继承View,绘制指引要素:Shape;
4.定义GuideManager,管理多步骤指引;
5.定义GuideDialog承载GuideView,覆盖在页面上,和GuideManager指引的要素:Shape
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
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的位置 }
绘制指引要素:GuideView
class GuideView : View, View.OnTouchListener, GestureDetector.OnGestureListener { /** * 当点击了高亮区域时响应 */ interface OnClickListener { fun onClick() } private val location = IntArray(2) private var initLocation = false private val mPaint = Paint() private var mGuideInfo: GuideInfo? = null private var background: Int = 0 private lateinit var mGestureDetector: GestureDetector var mOnClickListener: OnClickListener? = null private fun initView(attrs: AttributeSet) { background = getColor(context, R.color.translucent) val typedArray: TypedArray = context.obtainStyledAttributes(attrs, R.styleable.GuideView) background = typedArray.getColor(R.styleable.GuideView_background_translucent, background) typedArray.recycle() mPaint.isAntiAlias = true setOnTouchListener(this) mGestureDetector = GestureDetector(context, this) } constructor(context: Context) : super(context) constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initView(attrs) } constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initView(attrs) } override fun onDraw(canvas: Canvas) { super.onDraw(canvas) if(!initLocation) { getLocationOnScreen(location) initLocation = true } drawBackGround(canvas) drawShapes(canvas) } override fun onTouch(v: View?, event: MotionEvent?): Boolean { if (v != this || event == null || mGuideInfo == null) { return false } return mGestureDetector.onTouchEvent(event) } override fun onShowPress(e: MotionEvent?) { } override fun onSingleTapUp(event: MotionEvent?): Boolean { if (event == null || mGuideInfo == null) { return false } if (mGuideInfo!!.targetBound.contains(location[0] + event.x, location[1] + event.y)) { mOnClickListener?.onClick() return true } return false } override fun onDown(e: MotionEvent?): Boolean { return true } override fun onFling( e1: MotionEvent?, e2: MotionEvent?, velocityX: Float, velocityY: Float ): Boolean { return false } override fun onScroll( e1: MotionEvent?, e2: MotionEvent?, distanceX: Float, distanceY: Float ): Boolean { return false } override fun onLongPress(e: MotionEvent?) { } fun showGuide(guideStep: GuideInfo) { this.mGuideInfo = guideStep postInvalidate() } private fun drawBackGround(canvas: Canvas) { mPaint.xfermode = null mPaint.color = background canvas.drawRect(0F, 0F, width.toFloat(), height.toFloat(), mPaint) } private fun drawShapes(canvas: Canvas) { if (mGuideInfo == null) { return } // 先转换一下坐标,这样绘制得到的和底层目标View区域重叠 canvas.translate(-location[0].toFloat(), -location[1].toFloat()) // 1.先绘制要抠图的部分,也就是高亮的区域 mPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT) mGuideInfo?.mTargetHighlightShape?.draw(canvas, mPaint) // 2.再绘制其它的箭头,文本等指示性的Shape mPaint.xfermode = null mGuideInfo?.mShapes?.forEach { it.draw(canvas, mPaint) } } }
管理指引:GuideManager
interface GuideListener { fun onNextStep(step: Int) fun onCompleted() } class GuideManager(activity: Activity) : GuideDialog.OnNextStepListener { private val mGuideSteps = mutableListOf<GuideInfo>() private var currentStep = -1 val mGuideDialog: GuideDialog = GuideDialog(activity) var mGuideListener: GuideListener? = null var showGuideButton = false init { mGuideDialog.mOnNextStepListener = this if (!showGuideButton) { mGuideDialog.btnPreStep.visibility = View.GONE mGuideDialog.btnNextStep.visibility = View.GONE } } fun addGuideStep(guideInfo: GuideInfo) { mGuideSteps.add(guideInfo) } fun guideStepCount(): Int { return mGuideSteps.size } fun show() { mGuideDialog.show() onNextStep() } override fun onNextStep() { if (currentStep >= mGuideSteps.size - 1) { mGuideDialog.dismiss() mGuideListener?.onCompleted() return } currentStep++ val guideStep: GuideInfo = mGuideSteps[currentStep] mGuideDialog.guideView.showGuide(guideStep) if (currentStep > 0 && showGuideButton) { mGuideDialog.btnPreStep.visibility = View.VISIBLE } updateNextText() } override fun onPreStep() { if (currentStep == 0) { return } currentStep -= 1 val guideStep: GuideInfo = mGuideSteps[currentStep] mGuideDialog.guideView.showGuide(guideStep) if (currentStep == 0 && showGuideButton) { mGuideDialog.btnPreStep.visibility = View.GONE } updateNextText() } private fun updateNextText() { if (currentStep == mGuideSteps.size - 1) { mGuideDialog.btnNextStep.setText(R.string.end) } else { mGuideDialog.btnNextStep.setText(R.string.next_step) } mGuideListener?.onNextStep(currentStep) } }
承载GuideView的载体:GuideDialog
class GuideDialog : BaseDialog, View.OnClickListener, GuideView.OnClickListener { var mOnNextStepListener: OnNextStepListener? = null constructor(context: Context): super(context, R.style.DialogFullScreenTranslucent){ setContentView(R.layout.dialog_guide) btnNextStep.setOnClickListener(this) btnPreStep.setOnClickListener(this) guideView.mOnClickListener = this } override fun onClick(view: View) { if (view.id == R.id.btnNextStep) { mOnNextStepListener?.onNextStep() } else if (view.id == R.id.btnPreStep) { mOnNextStepListener?.onPreStep() } } interface OnNextStepListener { fun onNextStep() fun onPreStep() } override fun onClick() { mOnNextStepListener?.onNextStep() } }
接入项目
fun showGuide(context: Activity) { GuideManager(context).apply { addGuideStep(GuideInfo(imgLogo, isOval = true).apply { val textShape = TextDecoration( "这是圆形高亮区域,点击高亮进入下一步", sp2px(context, 12F).toFloat(), ContextCompat.getColor(context, R.color.white), targetBound.right + dip2px(context, 8F), targetBound.centerY() ) addShape(textShape) }) addGuideStep(GuideInfo(imgLogo2, radius = 16F).apply { val textShape = TextDecoration( "这是圆角矩形高亮区域,点击高亮继续进入下一步", sp2px(context, 12F).toFloat(), ContextCompat.getColor(context, R.color.white), targetBound.left, targetBound.bottom + dip2px(context, 24F) ) addShape(textShape) }) addGuideStep(GuideInfo(tvName, padding = 20).apply { val bitmap = BitmapFactory.decodeResource(resources, R.mipmap.ic_add_location_white_48dp) val bitmapShape = BitmapDecoration( bitmap, targetBound.left, targetBound.top - dip2px(context, 45F) ) addShape(bitmapShape) val textShape = TextDecoration( "点击高亮结束指引", sp2px(context, 14F).toFloat(), ContextCompat.getColor(context, R.color.white), targetBound.centerX(), targetBound.bottom + dip2px(context, 32F) ) addShape(textShape) }) mGuideListener = object: GuideListener { override fun onNextStep(step: Int) { Toast.makeText(context, "当前步骤:${step + 1}", Toast.LENGTH_SHORT).show() } override fun onCompleted() { tvShowGuide.visibility = View.VISIBLE } } // 如果要显示“上一步”,“下一步”,可以设置GuideManager中的mGuideDialog, }.show() }
关键技术点
定位高亮区域
fun targetViewRectF(): RectF { val location = IntArray(2) targetView.getLocationOnScreen(location) val rectF = RectF( location[0].toFloat(), location[1].toFloat(), location[0].toFloat() + targetView.width, location[1].toFloat() + targetView.height ) return rectF }
绘制高亮的View区域
private fun drawShapes(canvas: Canvas) { if (mGuideInfo == null) { return } // 转换一下坐标,这样绘制得到的和底层目标View区域重叠 canvas.translate(-location[0].toFloat(), -location[1].toFloat()) // 1.先绘制要抠图的部分,也就是高亮的区域 mPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT) mGuideInfo?.mTargetHighlightShape?.draw(canvas, mPaint) // 2.再绘制其它的箭头,文本等指示性的Shape mPaint.xfermode = null mGuideInfo?.mShapes?.forEach { it.draw(canvas, mPaint) } }
高亮区域点击事件
至此,指引绘制部分,GuideView基本就这么点代码,就OK了。 override fun onTouch(v: View?, event: MotionEvent?): Boolean { if (v != this || event == null || mGuideInfo == null) { return false } return mGestureDetector.onTouchEvent(event) } override fun onSingleTapUp(event: MotionEvent?): Boolean { if (event == null || mGuideInfo == null) { return false } if (mGuideInfo!!.targetBound.contains(event.rawX, event.rawY)) { mOnClickListener?.onClick() return true } return false }
优缺点
项目地址
GuideView总结
本网页所有视频内容由 imoviebox边看边下-网页视频下载, iurlBox网页地址收藏管理器 下载并得到。
ImovieBox网页视频下载器 下载地址: ImovieBox网页视频下载器-最新版本下载
本文章由: imapbox邮箱云存储,邮箱网盘,ImageBox 图片批量下载器,网页图片批量下载专家,网页图片批量下载器,获取到文章图片,imoviebox网页视频批量下载器,下载视频内容,为您提供.
阅读和此文章类似的: 全球云计算