Android 指引流程使用PorterDuffXfermode实现

2,085 阅读4分钟

1. 前言

写这篇文章也是一个机缘巧合。以前在学习自定义View的时候有了解过PorterDuffXfermode,但是也就只知道个大概,并没有在实战中运用到。 直到前段时间有个需求是做个指引流程的显示,我才想到PorterDuffXfermode可以实现这个功能,结果抱着侥幸的心理试了一下,真的就能实现。
所以基础知识还是比较重要的,虽然平时用不上,但是当你了解它是做什么的,一到可能用得上的场景时候,你就能联想出使用这项技术来实现。

2. 实现效果

先来看看实现效果,我要做一个指引流程的效果,要除了指引处是高亮,其他地方都置灰,以此来突出重点地方。效果大概是这样

image.png

平时这种效果还有一些图片和文字注释还有箭头,我这里为了方便演示,写个Demo就不带这些东西了。图片文字箭头这些都很容易去弄,主要是很多人可能不清楚这个高亮的效果怎么实现。

3. 实现思路

这种高亮的效果也可以有很多种思路,有很多种方案去实现。比如说可以先加一个蒙层,然后再添加一个大小和位置一模一样的view到蒙层上面,也能实现这样的效果。

但是当我看到这个需求的时候,我第一个想法是怎么让蒙层漏一个洞。这时候我就想到了PorterDuffXfermode,对它最根本的了解就是,能实现两个绘制区域各种叠加的效果。

可以看看官网对PorterDuff.Mode的介绍 developer.android.com/reference/a…

image.png

image.png

而我这里要实现的效果是两个区域叠加,然后移除两块绘制区域的重叠区域,正好有个XOR是这样的效果。

image.png

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)
        }
    }

}

看得出很简单,只需要创建指引控件,然后把需要指向的目标控件的位置传给它就行。可以看看最终的效果。

image.png

有人说,嗯?不对啊,你这个高亮区域都没对齐,都歪了。这是我故意的,认真想想歪的尺寸是多少?认真想想我注释的代码,如果实在不知道为什么,没关系,可以看看我这篇文章【狗头】 juejin.cn/post/720133…

5. 总结

写这篇文章,对外而言,可以介绍一下指引流程空间的实现方式和PorterDuffXfermode的基础用法。对我而言因为是一次比较有意思的体验,所以做个记录。

拿到这个需求的时候我并没有去查怎么实现,而是第一反应想到应该可以通过PorterDuffXfermode给蒙层挖个洞的方式。果然基础很重要,哪怕现在成熟的框架很多,但他们的底层实现也是基于某些技术的合理运用。