效果演示
高亮遮罩
/**
* @Description
* @Author elden
* @Date 2023/10/10 15:43
*/
class GuideFrame(context: Activity, private val guideView: BaseGuideView) : FrameLayout(context) {
companion object {
private val TAG = GuideFrame::class.java.simpleName
private var guideViewType: String = "" //当前传入的类型
private var nowShowType: String = "" //当前显示的提示
}
private var animator: ObjectAnimator? = null
private var targetPaint: Paint? = null
private var targetRectF: RectF? = null
private var allRect = Rect()
private val guideViewRect = Rect()
var isShowing = false
var callback: Callback? = null
private var targetViewReference: WeakReference<View>? = null
private var targetViewScrollTraceNum = 0 //targetView滚动次数记录
private val mInnerCallback = object : Callback() {
override fun onShow() {
super.onShow()
isShowing = true
}
override fun onDismiss() {
super.onDismiss()
isShowing = false
removeScrollListener()
}
}
open class Callback {
open fun onShow() {
}
open fun onDismiss() {
}
}
init {
// 让vg实现onDraw
setWillNotDraw(false)
// 目标区域
targetPaint = Paint()
targetPaint?.color = Color.parseColor("#ffffff")
targetPaint?.isAntiAlias = true
// 设置Mode为CLEAR
targetPaint?.xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
// 关闭硬件加速
setLayerType(View.LAYER_TYPE_SOFTWARE, null)
setBackgroundColor(Color.parseColor("#B3000000"))
setOnClickListener {
if (guideView.mOutSideCancelable) {
dismiss()
} else {
//防止提示不在屏幕上无法关闭
guideView.getGlobalVisibleRect(guideViewRect)
getGlobalVisibleRect(allRect)
if (!allRect.intersect(guideViewRect) || guideViewRect.right - guideViewRect.left == 0) {
LogUtil.e(TAG, "无法点击提示关闭,强制关闭 guideViewRect = ${guideViewRect},allRect = ${allRect} ")
dismiss()
}
}
}
}
fun show(targetView: View) {
guideViewType = guideView::class.java.simpleName
LogUtil.i(TAG, "show() guideViewType = $guideViewType")
if (PrefUtil.getBool(guideViewType, false)) {
Log.i(TAG, "${guideViewType}已显示过,无需显示提示")
doOnDismiss()
return
}
if (nowShowType == guideViewType) {
Log.e(TAG, "${guideViewType}正在显示,无需重新显示提示")
doOnDismiss()
return
}
if (targetView.visibility == View.GONE || targetView.visibility == View.INVISIBLE) {
Log.e(TAG, "该view未显示,无法提示")
doOnDismiss()
return
}
targetViewReference = WeakReference(targetView)
Log.e(TAG, "宽: ${targetView.width} 高: ${targetView.height}")
if (targetView.width <= 0 && targetView.height <= 0) {
targetView.post {
addMask(targetView)
}
} else {
addMask(targetView) //已知宽高就不用post了,连续显示会闪屏
}
}
private fun addMask(targetView: View) {
targetRectF = getTargetRect(targetView)
Log.i(TAG, "高亮位置 ${targetRectF!!.left}, ${targetRectF!!.top}, ${targetRectF!!.right}, ${targetRectF!!.bottom}")
val activity = (context as? Activity) ?: return
val params = WindowManager.LayoutParams()
params.format = PixelFormat.RGBA_8888
params.width = LayoutParams.MATCH_PARENT
params.height = LayoutParams.MATCH_PARENT
(activity.window.decorView as ViewGroup).addView(this, params) //添加遮罩
val rect =
Rect(targetRectF!!.left.toInt(), targetRectF!!.top.toInt(), targetRectF!!.right.toInt(), targetRectF!!.bottom.toInt())
val screenArr = QMUIDisplayHelper.getRealScreenSize(context)
allRect = Rect(0, 0, screenArr[0], screenArr[1])
Log.i(TAG, "allRect = $allRect")
if (allRect.intersect(rect)) {
addGuideView()
} else { //防止未获取到位置
Log.e(TAG, "未获取到位置,重新获取")
targetView.post {
targetRectF = getTargetRect(targetView)
val rect1 = Rect(
targetRectF!!.left.toInt(),
targetRectF!!.top.toInt(),
targetRectF!!.right.toInt(),
targetRectF!!.bottom.toInt()
)
Log.i(
TAG,
"高亮位置 ${targetRectF!!.left}, ${targetRectF!!.top}, ${targetRectF!!.right}, ${targetRectF!!.bottom}"
)
if (!allRect.intersect(rect1)) {
Log.e(TAG, "targetRectF区域无法获取,无法提示")
dismiss()
return@post
} else {
addGuideView()
invalidate()
}
}
}
addScrollListener()
}
private fun getTargetRect(targetView: View): RectF {
// 获取view位置
// val location = IntArray(2)
// targetView.getLocationInWindow(location)
// val targetX = location[0].toFloat()
// val targetY = location[1].toFloat()
val rect = Rect()
targetView.getGlobalVisibleRect(rect)
val targetX = rect.left.toFloat()
val targetY = rect.top.toFloat()
val extraBorderWidth = dip2px(guideView.mExtraBorderWidthDp)
when (guideView.mShape) {
BaseGuideView.SHAPE_CIRCLE -> {
val widthHeightDiffer = (targetView.measuredWidth - targetView.height) / 2
return RectF(
targetX - extraBorderWidth - dip2px(guideView.mExtraLeftDp),
targetY - extraBorderWidth - dip2px(guideView.mExtraLeftDp) - widthHeightDiffer,
targetX + targetView.measuredWidth + extraBorderWidth + dip2px(guideView.mExtraRightDp),
targetY + targetView.measuredWidth + extraBorderWidth + dip2px(guideView.mExtraRightDp) - widthHeightDiffer,
)
}
else -> {
return RectF(
targetX - extraBorderWidth - dip2px(guideView.mExtraLeftDp),
targetY - extraBorderWidth - dip2px(guideView.mExtraTopDp),
targetX + targetView.measuredWidth + extraBorderWidth + dip2px(guideView.mExtraRightDp),
targetY + targetView.measuredHeight + extraBorderWidth + dip2px(guideView.mExtraBottomDp),
)
}
}
}
private fun setGuideViewPosition() {
val left = (targetRectF?.left?.toInt() ?: 0)
val top = (targetRectF?.top?.toInt() ?: 0)
val screenArr = QMUIDisplayHelper.getRealScreenSize(context)
val right = (targetRectF?.right?.toInt() ?: screenArr[0])
val bottom = (targetRectF?.bottom?.toInt() ?: screenArr[1])
val xOffset = dip2px(guideView.mXOffset)
val yOffset = dip2px(guideView.mYOffset)
val width = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
val height = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
guideView.measure(width, height)
val params = FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
params.gravity = Gravity.TOP or Gravity.LEFT
when (guideView.mTipOrientation) {
BaseGuideView.ORIENTATION_LEFT -> {
params.leftMargin = left - guideView.measuredWidth + xOffset
params.topMargin = (top + bottom - guideView.measuredHeight) / 2 + yOffset
}
BaseGuideView.ORIENTATION_TOP -> {
params.leftMargin = (left + right - guideView.measuredWidth) / 2 + xOffset
params.topMargin = top - guideView.measuredHeight + yOffset
Log.d(
TAG,
"guideView.measuredWidth = ${guideView.measuredWidth}, xOffset = $xOffset, guideView.mXOffset = ${guideView.mXOffset}"
)
}
BaseGuideView.ORIENTATION_RIGHT -> {
params.leftMargin = right + guideView.measuredWidth + xOffset
params.topMargin = (top + bottom - guideView.measuredHeight) / 2 + yOffset
}
else -> {
params.leftMargin = (left + right - guideView.measuredWidth) / 2 + xOffset
params.topMargin = bottom + guideView.measuredHeight + yOffset
}
}
if (params.leftMargin < 0) {
params.leftMargin = 0
}
if (params.topMargin < 0) {
params.topMargin = 0
}
guideView.layoutParams = params
}
private fun addGuideView() {
setGuideViewPosition()
addView(guideView)
PrefUtil.setBool(guideViewType, true)
}
fun dismiss() {
val activity = (context as? Activity) ?: return
(activity.window.decorView as ViewGroup).removeView(this)
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
if (targetRectF == null) return
// canvas?.drawColor(Color.parseColor("#80000000"))
targetPaint?.let {
canvas?.drawRoundRect(
targetRectF!!,
dip2px(guideView.mRadiusDp).toFloat(),
dip2px(guideView.mRadiusDp).toFloat(),
it
)
}
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
doOnShow()
nowShowType = guideViewType
// animator?.cancel()
// animator = ObjectAnimator.ofFloat(this, "alpha", 0f, 1f)
// animator?.duration = 200
// animator?.start()
}
override fun onDetachedFromWindow() {
doOnDismiss()
nowShowType = ""
super.onDetachedFromWindow()
// animator?.cancel()
// animator = ObjectAnimator.ofFloat(this, "alpha", 1f, 0f)
// animator?.duration = 200
// animator?.start()
}
fun dip2px(dpValue: Float): Int {
//不要用Resources.getSystem().displayMetrics有机型适配问题
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpValue, context.resources.displayMetrics).toInt()
}
private fun doOnShow() {
mInnerCallback.onShow()
callback?.onShow()
}
private fun doOnDismiss() {
mInnerCallback.onDismiss()
callback?.onDismiss()
}
private val scrollChangeListener = ViewTreeObserver.OnScrollChangedListener {
if (targetViewScrollTraceNum > 50) {
removeScrollListener()
}
if (targetRectF != null && isShowing) {
targetViewReference?.get()?.let {
val tempRect = getTargetRect(it)
val xOffset = abs(tempRect.left - targetRectF!!.left)
val yOffset = abs(tempRect.top - targetRectF!!.top)
if ((xOffset < 1 && yOffset < 1) || xOffset > 500 || yOffset > 500) return@let
targetRectF = tempRect
Log.i(TAG, "位置改变 targetRectF = $targetRectF")
setGuideViewPosition()
invalidate()
targetViewScrollTraceNum ++
}
}
}
private fun addScrollListener() {
targetViewScrollTraceNum = 0
targetViewReference?.get()?.viewTreeObserver?.addOnScrollChangedListener(scrollChangeListener)
}
private fun removeScrollListener() {
targetViewScrollTraceNum = 0
targetViewReference?.get()?.viewTreeObserver?.removeOnScrollChangedListener(scrollChangeListener)
}
}
内部指引的View
/**
* @Description 基础指引布局,用于GuideFrame
* @Author elden
* @Date 2023/10/10 15:43
*/
open class BaseGuideView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
companion object {
const val ORIENTATION_LEFT = 0
const val ORIENTATION_TOP = 1
const val ORIENTATION_RIGHT = 2
const val ORIENTATION_BOTTOM = 3
const val SHAPE_RECTANGLE = 0
const val SHAPE_CIRCLE = 1
}
//提示view部分参数
var mTipOrientation = ORIENTATION_TOP //提示相对于框框所在方向
var mXOffset = 0f //x偏移量
var mYOffset = 0f //y偏移量
//高亮部分参数
var mShape = SHAPE_RECTANGLE //形状
var mRadiusDp = 0f //圆角大小
var mExtraLeftDp = 0f //额外左宽度
var mExtraTopDp = 0f //额外上宽度
var mExtraRightDp = 0f //额外右宽度
var mExtraBottomDp = 0f //额外下宽度
var mExtraBorderWidthDp = 0f //额外宽度
var mOutSideCancelable: Boolean = true //是否点击可取消,不可取消的要在内部view添加取消
fun dismiss() {
if (parent is GuideFrame) {
(parent as? GuideFrame)?.dismiss()
}
}
}
使用
继承BaseGuideView自定义引导的样式
/**
* @Description
* @Author elden
* @Date 2023/10/11 9:46
*/
class GuideViewFocus(context: Context) : BaseGuideView(context) {
init {
mOutSideCancelable = false
mYOffset = 6f
mXOffset = -71f
mExtraBorderWidthDp = 10f
mRadiusDp = 34f
LayoutInflater.from(context).inflate(R.layout.guide_focus, this, true)
val btn = findViewById<TextView>(R.id.btnIKnow)
btn.setOnClickListener {
dismiss()
}
}
}
val guideFrame = GuideFrame(this, GuideViewFocus(this))
guideFrame.show(mBinding.imgBgFocus)