ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

深入源码:android_maskable_layout 如何用dispatchDraw与Bitmap实现布局遮罩?

2026/8/16 15:20:03 拓冰建站 浏览量
深入源码:android_maskable_layout 如何用dispatchDraw与Bitmap实现布局遮罩?

深入源码:android_maskable_layout 如何用dispatchDraw与Bitmap实现布局遮罩?

【免费下载链接】android_maskable_layoutA library that easily allows you to mask layouts/viewgroups项目地址: https://gitcode.com/gh_mirrors/an/android_maskable_layout

想把 Android 布局裁剪成圆形头像、星形徽章、气泡形状……android_maskable_layout就是为此而生的轻量级开源库。它提供的MaskableFrameLayout容器,只需几行 XML 配置就能给任意子 View 加上遮罩效果,还支持逐帧动画遮罩和多达 18 种 PorterDuff 混合模式。本文将从零拆解它用 dispatchDraw 与 Bitmap 实现布局遮罩的完整原理,并给出可直接照抄的接入步骤,全程尽量少贴代码,让你像读故事一样看懂源码。

遮罩原理总览:一次绘制里的"二次加工"

先建立一个直觉:在 Android 中"给布局加遮罩",本质上就是先正常画出子 View,再在它上面叠加一张"镂空"的遮罩图MaskableFrameLayout的聪明之处在于:它没有去拦截每个子 View 的绘制,而是把遮罩 Drawable 预先渲染成一张和布局等大的 Bitmap,然后在dispatchDraw这个"画完子 View"的钩子里,用PorterDuffXfermode混合模式把这张 Bitmap 合成上去。整个过程只有两样东西:一张 Bitmap、一个绘制回调。

构造方法里的三处关键伏笔

MaskableFrameLayout的构造方法construct()中埋下了三个决定成败的设置:

  • setLayerType(LAYER_TYPE_SOFTWARE, null):强制使用软件绘制层。因为 PorterDuff 混合模式在硬件加速层上很多不生效,这是源码注释里明确写的"Only works for software layers"。
  • 创建 Paint:为后续绘制遮罩准备画笔,并支持可选的反锯齿开关anti_aliasing
  • 读取自定义属性:从MaskableLayout样式声明中读取mask(遮罩资源)、porterduffxfermode(混合模式)、anti_aliasing三个属性。属性定义位于library/src/main/res/values/attrs_maskable_framelayout.xml

核心步骤一:MaskableFrameLayout 如何把 Drawable 转成 Bitmap

遮罩必须是一张"和布局等宽高"的位图。这一步由makeBitmapMask()完成:先Bitmap.createBitmap(width, height, ARGB_8888)创建透明位图,再把 Drawable 的边界设为整个布局区域,最后用 Canvas 把 Drawable 画进这张 Bitmap。核心代码只有四行:

Bitmap mask = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(mask); drawable.setBounds(0, 0, w, h); drawable.draw(canvas);

注意:如果布局还没有测量完成(宽或高为 0),这里会直接返回 null 并打印日志,所以生成 Bitmap 的时机非常讲究(见下文的"何时重建遮罩")。

核心步骤二:dispatchDraw 里的"临门一脚"

这是整个库的灵魂。dispatchDraw是 ViewGroup 分发绘制子 View 的回调,源码在library/src/main/java/com/christophesmet/android/views/maskableframelayout/MaskableFrameLayout.java中,完整逻辑只有短短几行:

@Override protected void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); // 1. 正常绘制所有子 View if (mFinalMask != null && mPaint != null) { mPaint.setXfermode(mPorterDuffXferMode); // 2. 挂上混合模式 canvas.drawBitmap(mFinalMask, 0f, 0f, mPaint); // 3. 绘制遮罩 Bitmap mPaint.setXfermode(null); // 4. 用完即卸,防止污染后续绘制 } }

拆开看就四步:先super.dispatchDraw(canvas)让子 View 正常上屏;然后给画笔设置PorterDuffXfermode(默认DST_IN,即"目标像素 = 源遮罩与目标内容相交的部分",从而把内容"抠"成遮罩的形状);接着把预先做好的遮罩 Bitmap 画上去;最后立刻把 Xfermode 置空,避免影响之后的其他绘制。这样一个"先画内容、再画遮罩"的顺序,就完成了布局遮罩效果。

何时重建遮罩?onSizeChanged 与布局监听

遮罩 Bitmap 依赖布局尺寸,所以尺寸一变就得重做。源码里通过两处兜底:

  • onSizeChanged():当布局宽高改变时,自动调用setSize()重建遮罩 Bitmap。
  • registerMeasure():布局刚 inflate 出来时宽高还是 0,所以用ViewTreeObserver.addOnGlobalLayoutListener()等第一次布局完成后再生成遮罩,并在回调里及时移除监听,避免重复执行。

这两个机制保证了"遮罩永远和布局一样大、一样新"。

进阶玩法:AnimationDrawable 逐帧动画遮罩

这个库最酷的特性是支持动画遮罩。它在initMask()里检测到遮罩是AnimationDrawable时,会把自己注册为 Drawable 的 Callback,并重写了invalidateDrawable()scheduleDrawable()unscheduleDrawable()三个回调——每当动画帧切换时,就用新的一帧重建 Bitmap 并invalidate()触发重绘。Sample 工程里的animation_mask.xml就是一个包含 60 帧的动画列表,配合MainActivity里的"press me"按钮即可播放出遮罩形状连续变化的炫酷效果。

18 种 PorterDuff 混合模式怎么选?

源码里用getModeFromInteger()porterduffxfermode属性值映射到 Android 原生PorterDuff.Mode,共支持 18 种:ADDCLEARDARKENDSTDST_ATOPDST_INDST_OUTDST_OVERLIGHTENMULTIPLYOVERLAYSCREENSRCSRC_ATOPSRC_INSRC_OUTSRC_OVERXOR。日常遮罩需求选DST_IN就够了;想做颜色叠加、变亮变暗等滤镜效果,可以尝试MULTIPLYSCREENDARKEN等,玩法很多。

快速上手:XML 配置三步走

第一步,在布局里用MaskableFrameLayout包住目标 View,并指定遮罩资源和混合模式:

<com.christophesmet.android.views.maskableframelayout.MaskableFrameLayout android:layout_width="100dp" android:layout_height="100dp" app:mask="@drawable/masking_masking_01" app:porterduffxfermode="DST_IN"> <ImageView android:layout_width="match_parent" android:layout_height="match_parent" android:scaleType="centerCrop" android:src="@drawable/unicorn"/> </com.christophesmet.android.views.maskableframelayout.MaskableFrameLayout>

第二步,在代码中也可以动态更换遮罩:调用setMask(drawableRes)setMask(drawable),配合setPorterDuffXferMode()随时切换混合模式。第三步,如果遮罩是AnimationDrawable,像MainActivity那样start()即可播放动画。完整示例布局见Sample/src/main/res/layout/activity_main.xml

源码文件导航

  • 核心实现:MaskableFrameLayout.java
  • 自定义属性:library/src/main/res/values/attrs_maskable_framelayout.xml
  • 示例页面:Sample/src/main/java/com/christophesmet/android/views/MainActivity.java
  • 动画遮罩资源:Sample/src/main/res/drawable/animation_mask.xml

想亲手运行 Sample 工程看效果,可以 clone 仓库:https://gitcode.com/gh_mirrors/an/android_maskable_layout

总结

android_maskable_layout用不到 400 行代码,就把"布局遮罩"这件看似复杂的事讲得清清楚楚:先把遮罩 Drawable 渲染成 Bitmap,再在 dispatchDraw 里用 PorterDuffXfermode 完成合成。理解了这个套路,你甚至可以自己动手写一个圆角布局、气泡布局或者异形控件。如果你也想让 Android 布局拥有"任意形状",不妨现在就试试它吧!🎨

【免费下载链接】android_maskable_layoutA library that easily allows you to mask layouts/viewgroups项目地址: https://gitcode.com/gh_mirrors/an/android_maskable_layout

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考