1. 前言
写这篇文章也是一个机缘巧合。以前在学习自定义View的时候有了解过PorterDuffXfermode,但是也就只知道个大概,并没有在实战中运用到。
直到前段时间有个需求是做个指引流程的显示,我才想到PorterDuffXfermode可以实现这个功能,结果抱着侥幸的心理试了一下,真的就能实现。
所以基础知识还是比较重要的,虽然平时用不上,但是当你了解它是做什么的,一到可能用得上的场景时候,你就能联想出使用这项技术来实现。
2. 实现效果
先来看看实现效果,我要做一个指引流程的效果,要除了指引处是高亮,其他地方都置灰,以此来突出重点地方。效果大概是这样
平时这种效果还有一些图片和文字注释还有箭头,我这里为了方便演示,写个Demo就不带这些东西了。图片文字箭头这些都很容易去弄,主要是很多人可能不清楚这个高亮的效果怎么实现。
3. 实现思路
这种高亮的效果也可以有很多种思路,有很多种方案去实现。比如说可以先加一个蒙层,然后再添加一个大小和位置一模一样的view到蒙层上面,也能实现这样的效果。
但是当我看到这个需求的时候,我第一个想法是怎么让蒙层漏一个洞。这时候我就想到了PorterDuffXfermode,对它最根本的了解就是,能实现两个绘制区域各种叠加的效果。
可以看看官网对PorterDuff.Mode的介绍 developer.android.com/reference/a…
而我这里要实现的效果是两个区域叠加,然后移除两块绘制区域的重叠区域,正好有个XOR是这样的效果。
4. 实现
大致了解了使用的技术和原理,就来试试看具体是怎么去实现。
首先我把整个指引流程效果的绘制当成一个View,要自定义一个view,大小是全屏,绘制蒙层,并通过PorterDuffXfermode实现高亮区域(也就是让蒙层漏一个洞)
其实PorterDuffXfermode这个技术,可以把两个绘制的区域分为原图和目标图
原图的绘制直接这样就行,简单方便。
canvas?.drawColor(Color.parseColor("#80000000"))
目标图我们需要对画笔进行一下初始化
init {
// 让vg实现onDraw
setWillNotDraw(false)
// 目标区域
targetPaint = Paint()
targetPaint?.color = Color.parseColor("#ffffff")
targetPaint?.isAntiAlias = true
// 设置Mode为XOR
targetPaint?.xfermode = PorterDuffXfermode(PorterDuff.Mode.XOR)
// 关闭硬件加速
setLayerType(View.LAYER_TYPE_SOFTWARE, null)
}
注意这里需要setLayerType(View.LAYER_TYPE_SOFTWARE, null)关闭硬件加速,因为PorterDuffXfermode要关闭硬件加速才有效果。这个也是使用PorterDuffXfermode时的一个比较经典的问题。
然后我们需要绘制目标图,但是这个目标图的尺寸我们需要传进来。
var rectf: RectF? = null
fun setContentLocation(x: Int, y: Int, w: Int, h: Int) {
// 区域就是高亮的目标控件的区域加4dp
rectf = RectF(
x.toFloat() - dip2px(4f),
y.toFloat() - dip2px(4f),
(x + w).toFloat() + dip2px(4f),
(y + h).toFloat() + dip2px(4f)
)
}
然后绘制
targetPaint?.let {
// 加个弧度,纯长方形不好看
canvas?.drawRoundRect(
rectf!!,
dip2px(18f).toFloat(),
dip2px(18f).toFloat(),
it
)
}
具体的代码
class GuideView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
private var targetPaint: Paint? = null
init {
// 让vg实现onDraw
setWillNotDraw(false)
// 目标区域
targetPaint = Paint()
targetPaint?.color = Color.parseColor("#ffffff")
targetPaint?.isAntiAlias = true
// 设置Mode为XOR
targetPaint?.xfermode = PorterDuffXfermode(PorterDuff.Mode.XOR)
// 关闭硬件加速
setLayerType(View.LAYER_TYPE_SOFTWARE, null)
}
var rectf: RectF? = null
fun setContentLocation(x: Int, y: Int, w: Int, h: Int) {
rectf = RectF(
x.toFloat() - dip2px(4f),
y.toFloat() - dip2px(4f),
(x + w).toFloat() + dip2px(4f),
(y + h).toFloat() + dip2px(4f)
)
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
canvas?.drawColor(Color.parseColor("#80000000"))
targetPaint?.let {
canvas?.drawRoundRect(
rectf!!,
dip2px(18f).toFloat(),
dip2px(18f).toFloat(),
it
)
}
}
fun dip2px(dpValue: Float): Int {
val scale = Resources.getSystem().displayMetrics.density
return (dpValue * scale + 0.5f).toInt()
}
}
然后Demo里面有一个目标控件,我这里就用TextView,让高亮区域作用在这个控件上
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fl"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="22sp"
android:textColor="#000000"
android:text="引导此处"
android:textStyle="bold"
android:layout_marginTop="60dp"
android:layout_marginStart="60dp"
/>
</FrameLayout>
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// val wlp = window.attributes
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
// wlp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
// }
// window.attributes = wlp
// window.decorView.systemUiVisibility =
// View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
setContentView(R.layout.main)
val tv: TextView = findViewById(R.id.tv)
val fl: FrameLayout = findViewById(R.id.fl)
tv.post {
// 获取view位置
val location = IntArray(2)
tv.getLocationInWindow(location)
val x = location[0]
val y = location[1]
// 创建引导view
val guideView = GuideView(this)
guideView.layoutParams = FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
guideView.setContentLocation(x, y, tv.width, tv.height)
fl.addView(guideView)
}
}
}
看得出很简单,只需要创建指引控件,然后把需要指向的目标控件的位置传给它就行。可以看看最终的效果。
有人说,嗯?不对啊,你这个高亮区域都没对齐,都歪了。这是我故意的,认真想想歪的尺寸是多少?认真想想我注释的代码,如果实在不知道为什么,没关系,可以看看我这篇文章【狗头】 juejin.cn/post/720133…
5. 总结
写这篇文章,对外而言,可以介绍一下指引流程空间的实现方式和PorterDuffXfermode的基础用法。对我而言因为是一次比较有意思的体验,所以做个记录。
拿到这个需求的时候我并没有去查怎么实现,而是第一反应想到应该可以通过PorterDuffXfermode给蒙层挖个洞的方式。果然基础很重要,哪怕现在成熟的框架很多,但他们的底层实现也是基于某些技术的合理运用。