先上效果图:

代码实现:
kotlin
class EraseView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private val mPaint = Paint(Paint.ANTI_ALIAS_FLAG) // 防锯齿画笔
private val mTextPaint = TextPaint(Paint.ANTI_ALIAS_FLAG) // 防锯齿画笔
private var mPath = Path() // 擦除路径
// 源图像
private lateinit var srcBitmap: Bitmap
// 目标图层
private lateinit var dstBitmap: Bitmap
private var showText = "" //文案
private var isShowResultDirect = false //是否直接展示结果
init {
setLayerType(LAYER_TYPE_SOFTWARE, null) //关闭硬件加速器
mTextPaint.apply {
color = Color.RED // 设置擦除画笔为红色
style = Paint.Style.FILL // 设置画笔样式为描边
strokeWidth = 50f // 设置画笔粗细
strokeCap = Paint.Cap.ROUND // 设置画笔端点为圆形
strokeJoin = Paint.Join.ROUND // 设置拐角为圆形
textSize = 24.sp2px().toFloat()
}
mPaint.apply {
color = Color.RED // 设置擦除画笔为红色
style = Paint.Style.STROKE // 设置画笔样式为描边
strokeWidth = 20.dp2px().toFloat() // 设置画笔粗细
strokeCap = Paint.Cap.ROUND // 设置画笔端点为圆形
strokeJoin = Paint.Join.ROUND // 设置拐角为圆形
}
showText = getRandomStr()
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
// 初始化图片资源
val originBitmap = BitmapFactory.decodeResource(resources, R.drawable.icon_guaguaka)
srcBitmap = Bitmap.createScaledBitmap(originBitmap, w, h, false)
dstBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
}
@SuppressLint("DrawAllocation")
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (!this::srcBitmap.isInitialized || !this::dstBitmap.isInitialized) return
kotlin.runCatching {
canvas.drawColor(Color.parseColor("#BBBBBB"))
}
val textWidth = mTextPaint.measureText(showText) //文本宽度
val fontMetrics = mTextPaint.fontMetrics
val baselineOffset = (fontMetrics.bottom + fontMetrics.top) / 2
//显示的文案居中绘制
canvas.drawText(showText, (width - textWidth) / 2f, height / 2f - baselineOffset, mTextPaint)
val layerId = canvas.saveLayer(0f, 0f, width.toFloat(), height.toFloat(), null)
// 1. 在画布上绘制擦除路径
canvas.drawPath(mPath, mPaint)
// 2. 绘制背景图像(目标图像)
canvas.drawBitmap(dstBitmap, 0f, 0f, mPaint)
// 3. 手动擦除,设置混合模式为 SRC_OUT,实现擦除效果;直接展示结果,用DST_OUT
mPaint.xfermode = PorterDuffXfermode(if (isShowResultDirect) PorterDuff.Mode.DST_OUT else PorterDuff.Mode.SRC_OUT)
canvas.drawBitmap(srcBitmap, 0f, 0f, mPaint)
mPaint.xfermode = null
canvas.restoreToCount(layerId)
}
/**
* 重置状态,再刮一次
*/
fun resetState() {
mPath.reset()
isShowResultDirect = false
showText = getRandomStr()
invalidate()
}
/**
* 直接展示结果
*/
fun showResultDirect() {
isShowResultDirect = true
invalidate()
}
/**
* 重置文案
*/
private fun getRandomStr(): String {
return listOf("谢谢惠顾", "特等奖", "一等奖", "二等奖", "三等奖").random()
}
// 处理触摸事件,动态更新擦除路径
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
val x = event.x
val y = event.y
when (event.action) {
MotionEvent.ACTION_DOWN -> {
// 请求父视图不要拦截事件
parent.requestDisallowInterceptTouchEvent(true)
mPath.moveTo(x, y) // 路径起点
}
MotionEvent.ACTION_MOVE -> {
mPath.lineTo(x, y) // 路径延续
invalidate() // 重新绘制
}
MotionEvent.ACTION_UP -> {
parent.requestDisallowInterceptTouchEvent(false)
}
}
return true
}
}
上述代码通过 PorterDuffXfermode 实现了刮刮卡的效果。刮刮卡的效果是通过在 View 上绘制一层覆盖图像(类似遮罩层),然后通过手势擦除这层图像以显示底下的内容。这里使用了 PorterDuffXfermode 来实现擦除效果。
PorterDuffXfermode 是 Android 提供的图像混合模式,它定义了两张图像(源图像 src 和目标图像 dst)在绘制时如何混合。通过不同的模式,可以实现各种图像合成的效果。关于PorterDuffXfermode的使用,可以参见之前的文章:Android 图像合成:玩转 PorterDuff.Mode 的 18 种混合模式
saveLayer 创建了一个独立的画布图层,用于处理复杂的混合模式绘制。混合模式会影响图层内的绘制结果,而不会影响其他部分的画布。
XML文件:
kotlin
//fragment_ggk.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:gravity="center_horizontal"
android:orientation="vertical">
<org.ninetripods.mq.study.widget.xfermode.EraseView
android:id="@+id/erase_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_gravity="center"
app:layout_constraintBottom_toTopOf="@+id/ll_btn"
app:layout_constraintDimensionRatio="1000:527"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
<LinearLayout
android:id="@+id/ll_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:gravity="center"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@+id/erase_view">
<Button
android:id="@+id/btn_reset"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="再刮一次" />
<Button
android:id="@+id/btn_show_result"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:gravity="center"
android:text="直接显示结果" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
Fragment中:
kotlin
/**
* 刮刮卡效果
*/
class XFerModeGGKFragment : Fragment() {
private val mEraseView: EraseView by id(R.id.erase_view)
private val mBtnRest: Button by id(R.id.btn_reset)
private val mBtnShow: Button by id(R.id.btn_show_result)
override fun getLayoutId(): Int {
return R.layout.fragment_ggk
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
mBtnRest.setOnClickListener { mEraseView.resetState() }
mBtnShow.setOnClickListener { mEraseView.showResultDirect() }
}
}