-
Notifications
You must be signed in to change notification settings - Fork 197
/
EasyGuideLayer.kt
427 lines (377 loc) · 17.3 KB
/
EasyGuideLayer.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
@file:Suppress("unused","RtlHardcoded")
package com.haoge.easyandroid.easy
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.graphics.*
import android.graphics.drawable.Drawable
import android.support.annotation.DrawableRes
import android.support.annotation.IntDef
import android.support.annotation.LayoutRes
import android.view.*
import android.widget.FrameLayout
import android.widget.ImageView
import com.haoge.easyandroid.EasyAndroid
private typealias OnGuideShowListener = (Boolean) -> Unit
private typealias OnGuideViewAttachedListener = (View, EasyGuideLayer.Controller) -> Unit
private typealias OnDrawHighLightCallback = (canvas:Canvas, rect:RectF, paint:Paint) -> Unit
private typealias OnHighLightClickListener = (EasyGuideLayer.Controller) -> Unit
private typealias OnGuideViewOffsetProvider = (Point, RectF, View) -> Unit
class EasyGuideLayer private constructor(private val anchor: View){
private val items = mutableListOf<GuideItem>()
private var onGuideShowListener:OnGuideShowListener? = null
private var backgroundColor:Int = 0x33000000
private var dismissOnClickOutside = false
private var dismissIfNoItems = false
/** 设置蒙层的背景色[color]*/
fun setBackgroundColor(color:Int): EasyGuideLayer {
this.backgroundColor = color
return this
}
/** 设置蒙层的展示/消失事件监听器。
*
* 回调参数:Boolean, True表示蒙层为展示状态
*/
fun setOnGuideShownListener(listener: OnGuideShowListener?): EasyGuideLayer {
this.onGuideShowListener = listener
return this
}
/** 设置当点击到非预期区域(即非内部view点击内部消费事件)的时候,是否自动关闭蒙层, True表示自动关闭*/
fun setDismissOnClickOutside(dismiss: Boolean): EasyGuideLayer {
this.dismissOnClickOutside = dismiss
return this
}
/** 设置是否当此layer不存在绑定的GuideItem时,自动关闭蒙层。 True表示自动关闭*/
fun setDismissIfNoItems(dismiss: Boolean): EasyGuideLayer {
this.dismissIfNoItems = dismiss
return this
}
/** 添加引导层item*/
fun addItem(item:GuideItem): EasyGuideLayer {
if (items.contains(item).not()) items.add(item)
return this
}
/** 移除制定的引导层item*/
fun removeItem(item: GuideItem): EasyGuideLayer {
if (items.contains(item)) items.remove(item)
return this
}
/** 清理所有的已添加的引导层*/
fun clearItems(): EasyGuideLayer {
items.clear()
return this
}
fun show() {
val activity = getActivity(anchor)?:throw RuntimeException("")
val parent = (anchor.parent as ViewGroup?)?:throw RuntimeException("")
// 从指定布局节点处寻找或创建guideLayout容器。进行绑定展示
val guideLayout = if (anchor is FrameLayout && getLastView(anchor) is GuideLayout) {
getLastView(anchor) as GuideLayout
} else if (parent is FrameLayout && getLastView(parent) is GuideLayout) {
getLastView(parent) as GuideLayout
} else if (anchor is FrameLayout) {
val guide = GuideLayout(activity)
anchor.addView(guide, FrameLayout.LayoutParams(-1, -1))
guide
} else {
val guide = GuideLayout(activity)
val frame = FrameLayout(activity)
val params = anchor.layoutParams
val index = parent.indexOfChild(anchor)
parent.removeView(anchor)
parent.addView(frame, index, params)
frame.addView(anchor, FrameLayout.LayoutParams(-1, -1))
frame.addView(guide, FrameLayout.LayoutParams(-1, -1))
guide
}
if (items.isEmpty() && dismissIfNoItems) {
guideLayout.dismiss()
} else {
guideLayout.bindLayer(this)
}
}
// 获取View容器中的顶层View。
private fun getLastView(view:ViewGroup):View? =
if (view.childCount > 0) view.getChildAt(view.childCount - 1) else null
private fun getActivity(view:View):Activity? {
var bindAct: Activity? = null
var context = view.context
do {
if (context is Activity) {
bindAct = context
break
} else if (context is ContextWrapper) {
context = context.baseContext
} else {
break
}
} while (true)
return bindAct
}
companion object {
fun with(anchor:View) = EasyGuideLayer(anchor)
fun with(activity: Activity) = EasyGuideLayer.with(activity.findViewById<View>(android.R.id.content))
}
class GuideLayout(context: Context?) : FrameLayout(context), Controller, View.OnClickListener {
private var layer:EasyGuideLayer? = null
private var container = mutableMapOf<RectF, GuideItem>()
private val paint = Paint()
private var copyPaint: Paint
init {
paint.isAntiAlias = true
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
paint.maskFilter = BlurMaskFilter(10f, BlurMaskFilter.Blur.INNER)
copyPaint = Paint(paint)
setLayerType(View.LAYER_TYPE_SOFTWARE, null)
setWillNotDraw(true)
setOnClickListener(this)
}
override fun getGuideLayer(): EasyGuideLayer = layer?:throw RuntimeException("")
override fun dismiss() {
container.clear()
val parent = parent as ViewGroup?
parent?.removeView(this)
layer = null
}
fun bindLayer(layer:EasyGuideLayer) {
removeAllViews()
this.layer = layer
layer.onGuideShowListener?.invoke(true)
setBackgroundColor(layer.backgroundColor)
layer.items.forEach { item ->
val child = createItemView(item)
child.setTag(GuideItem.TAG_ID, item)
addView(child)
item.getOnViewAttachedListener()?.invoke(child, this)
}
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
layer?.onGuideShowListener?.invoke(false)
}
override fun onClick(v: View?) {
if (layer?.dismissOnClickOutside == true) {
dismiss()
}
}
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
container.clear()
for (index in 0 until childCount) {
val child = getChildAt(index)
val item = child.getTag(GuideItem.TAG_ID) as? GuideItem ?: continue
val rect = getRectFromItem(item)
container[rect] = item
resetChildLayoutParams(item, rect, child)
}
}
// 计算出蒙层View的具体位置。 item: 蒙层实例。 rect: 高亮区域, child: 蒙层View
private fun resetChildLayoutParams(item: GuideItem, rect:RectF, child:View) {
// 根据不同的gravity获取指定的child顶点(X * Y)
val point:Point = when(item.getGravity()) {
Gravity.LEFT or Gravity.TOP -> {Point(rect.left.toInt() - child.width, rect.top.toInt() - child.height)}
Gravity.RIGHT or Gravity.TOP -> {Point(rect.right.toInt(), rect.top.toInt() - child.height)}
Gravity.LEFT or Gravity.BOTTOM -> {Point(rect.left.toInt() - child.width, rect.bottom.toInt())}
Gravity.RIGHT or Gravity.BOTTOM -> {Point(rect.right.toInt(), rect.bottom.toInt())}
Gravity.TOP -> {Point(rect.left.toInt(), (rect.top - child.height).toInt())}
Gravity.BOTTOM -> {Point(rect.left.toInt(), rect.bottom.toInt())}
Gravity.LEFT -> {Point((rect.left - child.width).toInt(), rect.top.toInt())}
Gravity.RIGHT -> {Point(rect.right.toInt(), rect.top.toInt())}
else -> {Point(rect.left.toInt(), rect.top.toInt())}
}
item.getOffsetProvider()?.invoke(point, rect, child)
val params = child.layoutParams as LayoutParams
if (params.leftMargin != point.x || params.topMargin != point.y) {
params.leftMargin = point.x
params.topMargin = point.y
child.layoutParams = params
}
}
private fun getRectFromItem(item: GuideItem): RectF {
if (item.rect != null) return item.rect
if (item.view == null) return RectF(0f, 0f, 0f, 0f)
val viewRect = Rect()
val guideRect = Rect()
item.view.getGlobalVisibleRect(viewRect)
getGlobalVisibleRect(guideRect)
if (guideRect.contains(viewRect).not()) return RectF(0f, 0f, 0f, 0f)
return RectF((viewRect.left - guideRect.left - item.padding).toFloat(),
(viewRect.top - guideRect.top - item.padding).toFloat(),
(viewRect.left - guideRect.left + viewRect.width() + item.padding).toFloat(),
(viewRect.top - guideRect.top + viewRect.height() + item.padding).toFloat())
}
private fun createItemView(item:GuideItem):View {
if (item.getLayout() != 0) {
val inflater = LayoutInflater.from(context)
return inflater.inflate(item.getLayout(), this, false)
} else if (item.getDrawable() != null) {
val image = ImageView(context)
image.layoutParams = FrameLayout.LayoutParams(-2, -2)
image.adjustViewBounds = true
image.setImageDrawable(item.getDrawable())
return image
}
return EmptyItem(context)
}
override fun onDraw(canvas: Canvas) {
container.forEach { entry ->
entry.value.getOnDrawHighLightListener()?.let {
it.invoke(canvas, entry.key, copyPaint)
return@forEach
}
when(entry.value.getShapeType()) {
GuideItem.SHAPE_RECT -> canvas.drawRect(entry.key, paint)
GuideItem.SHAPE_OVAL -> canvas.drawOval(entry.key, paint)
}
}
}
private var downPoint:PointF? = null
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
val touchSlop = ViewConfiguration.get(context).scaledTouchSlop
when(event.action) {
MotionEvent.ACTION_DOWN -> { downPoint = PointF(event.x, event.y) }
MotionEvent.ACTION_MOVE -> {
val down = downPoint?:return super.onTouchEvent(event)
if (event.x - down.x > touchSlop || event.y - down.y > touchSlop) downPoint = null
}
MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> {
val down = downPoint?:return super.onTouchEvent(event)
container.forEach {
if (it.key.contains(down.x, down.y) &&
it.value.getShapeType() != GuideItem.SHAPE_NONE &&
it.value.getOnHighLightClickListenter() != null) {
it.value.getOnHighLightClickListenter()?.invoke(this)
return true
}
}
}
}
return super.onTouchEvent(event)
}
}
private class EmptyItem(context: Context?) : View(context) {
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
setMeasuredDimension(0, 0)
}
}
interface Controller {
fun getGuideLayer():EasyGuideLayer
fun dismiss()
}
}
/** 一个GuideItem实例表示蒙层上的一块引导层。一个引导层包括有:
*
* 1. 一个指定的区域用于进行View位置定位以及高亮效果展示:由[rect], [view], [padding]提供定位区域, [shapeType]提供高亮展示效果
* 2. 引导View,在蒙层上展示给用户查看的一些额外提示性View。由[setLayout]或者[setDrawable]进行提供。
* */
class GuideItem private constructor(val rect: RectF? = null, val view: View? = null, val padding: Int = 0){
private var shapeType = SHAPE_NONE
private var onViewAttached:OnGuideViewAttachedListener? = null
private var onDrawHighLight:OnDrawHighLightCallback? = null
private var onHighLightClickListener:OnHighLightClickListener? = null
private var offsetProvider:OnGuideViewOffsetProvider? = null
private var drawable:Drawable? = null
private var layout:Int = 0
private var gravity = Gravity.NO_GRAVITY
fun getShapeType() = shapeType
fun getOnViewAttachedListener() = onViewAttached
fun getOnDrawHighLightListener() = onDrawHighLight
fun getOnHighLightClickListenter() = onHighLightClickListener
fun getDrawable() = drawable
fun getLayout() = layout
fun getGravity() = gravity
fun getOffsetProvider() = offsetProvider
/** 设置此接口用于对引导层的位置进行微调。回调参数分别为:
* 1. Point: 根据[setGravity]配置计算出的引导层顶点坐标,
* 2. RectF: 引导层的定位区域
* 3. View: 顶层的引导View。可直接在回调用获取到View的Width以及Height。便于动态计算进行位置展示
*/
fun setOffsetProvider(offsetProvider:OnGuideViewOffsetProvider?): GuideItem {
this.offsetProvider = offsetProvider
return this
}
/** 设置高亮区域的显示效果。默认为[GuideItem.SHAPE_NONE]: 不展示高亮效果。*/
fun setHighLightShape(@ShapeType shapeType:Int): GuideItem {
this.shapeType = shapeType
return this
}
/**
* 设置引导View被创建时的回调监听器。可实现此监听器对引导View进行操作。比如添加点击监听等,回调参数分别为:
* 1. View:被创建的引导View。
* 2. Controller: 提供给上层使用的控制器。可用于进行蒙层关闭或者蒙层引导更新等操作。
*/
fun setOnViewAttachedListener(onViewAttachedListener: OnGuideViewAttachedListener?): GuideItem {
this.onViewAttached = onViewAttachedListener
return this
}
/**
* 设置自定义高亮绘制回调。用于进行高亮区域的自定义绘制逻辑(因为默认值提供了Rect与Oval模式绘制).回调参数分别为:
* 1. Canvas: 用于进行绘制的画布
* 2. RectF: 需要进行高亮绘制的位置区域。
* 3. Paint: 提供的画笔。默认Xfermode为PorterDuff.Mode.CLEAR
*/
fun setOnDrawHighLightCallback(callback: OnDrawHighLightCallback?): GuideItem {
this.onDrawHighLight = callback
return this
}
/**
* 设置高亮点击监听器,当点击到对应高亮区域(高亮模式必须为非SHAPE_NONE模式)时触发。回调参数为:
* 1. Controller: 提供给上层使用的控制器。可用于进行蒙层关闭或者蒙层引导更新等操作。
*/
fun setOnHighLightClickListener(onHighLightClickListener: OnHighLightClickListener?): GuideItem {
this.onHighLightClickListener = onHighLightClickListener
return this
}
/**
* 设置引导层View的布局id。与[setDrawable]互斥。
*/
fun setLayout(@LayoutRes layout:Int): GuideItem {
this.layout = layout
this.drawable = null
return this
}
/**
* 根据提供的drawable创建一个模式为adjustViewBounds的ImageView。作为引导层View使用。与[setLayout]互斥
*/
fun setDrawable(drawable: Drawable): GuideItem {
this.drawable = drawable
this.layout = 0
return this
}
/**
* 根据提供的drawable创建一个模式为adjustViewBounds的ImageView。作为引导层View使用。与[setLayout]互斥
*/
fun setDrawable(@DrawableRes drawableRes: Int): GuideItem {
@Suppress("DEPRECATION")
this.drawable = EasyAndroid.getApplicationContext().resources.getDrawable(drawableRes)
this.layout = 0
return this
}
/**
* 设置引导层相对于高亮区域的相对位置,目前仅支持[Gravity.LEFT], [Gravity.TOP], [Gravity.BOTTOM], [Gravity.RIGHT], [Gravity.NO_GRAVITY]
*/
fun setGravity(gravity: Int): GuideItem {
this.gravity = gravity
return this
}
companion object {
/** 不进行高亮绘制*/
const val SHAPE_NONE = -1
/** 绘制成矩形的高亮区域*/
const val SHAPE_RECT = 0
/** 绘制成椭圆的高亮区域*/
const val SHAPE_OVAL = 1
internal const val TAG_ID = 0x0F000000
/** 不指定具体的高亮绘制区域,使用默认区域Rect(0,0,0,0)*/
fun newInstance() = GuideItem()
/** 通过指定view与padding创建具体的高亮绘制区域*/
fun newInstance(view: View, padding: Int = 0) = GuideItem(view = view, padding = padding)
/** 直接指定具体的高亮绘制区域*/
fun newInstance(rect: RectF) = GuideItem(rect = rect)
}
}
@Retention(AnnotationRetention.SOURCE)
@IntDef(GuideItem.SHAPE_NONE, GuideItem.SHAPE_RECT, GuideItem.SHAPE_OVAL)
annotation class ShapeType