Region.Op - canvas.clipxxx(xxx, Region.Op.XXX)
注: **canvas.clipxxx()** 对于圆角部分锯齿有些明显, Android9.0之后仅支持 **Region.Op.DIFFERENCE/Region.Op.INTERSECT** Region.Op 主要用于 canvas 的裁剪, 与其类似的还有 Path.Op 作用于 Path 上, 具体效果请参考下文的 Path.Op
@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);canvas.save();canvas.clipPath(mBgPath);// canvas.clipPath(mBgPath, Region.Op.DIFFERENCE)canvas.drawPath(mBgPath, mPaintBg);canvas.drawRect(mRectF, mPaint);canvas.restore();}
PorterDuff.Mode - paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.XXX)
注: PorterDuff 拥有抗锯齿属性
SRC: 设置了 xfermode 的所有图形(目标图形, 设置顺序会有)
DST: 没有设置 xfermode 的所有图形
PorterDuff.Mode 的合成模式主要依靠透明和重叠, 如果非重叠部分不透明, 非重叠部分也有可能会显示出来, 结果可能会跟预想的不同, 解决方法就是在非重叠部分添加透明层(注意: 透明层的添加顺序, 透明层是否设置 Xfermode)
// PorterDuff 示例 Viewclass PorterDuffView constructor(context: Context, attrs: AttributeSet? = null) : View(context, attrs) {private var type = 0private val xfermode: Xfermode by lazy {val mode = when (type) {0 -> {PorterDuff.Mode.CLEAR}1 -> {PorterDuff.Mode.SRC}2 -> {PorterDuff.Mode.DST}3 -> {PorterDuff.Mode.SRC_OVER}4 -> {PorterDuff.Mode.DST_OVER}5 -> {PorterDuff.Mode.SRC_IN}6 -> {PorterDuff.Mode.DST_IN}7 -> {PorterDuff.Mode.SRC_OUT}8 -> {PorterDuff.Mode.DST_OUT}9 -> {PorterDuff.Mode.SRC_ATOP}10 -> {PorterDuff.Mode.DST_ATOP}11 -> {PorterDuff.Mode.XOR}16 -> {PorterDuff.Mode.DARKEN}17 -> {PorterDuff.Mode.LIGHTEN}13 -> {PorterDuff.Mode.MULTIPLY}14 -> {PorterDuff.Mode.SCREEN}12 -> {PorterDuff.Mode.ADD}else -> {PorterDuff.Mode.OVERLAY}}PorterDuffXfermode(mode)}private val srcColor = Color.parseColor("#FF2C98F0")private val dstColor = Color.parseColor("#FFff2565")private val transparentColor = 0private val area = dp2px(100f)private val offset = dp2px(10f)private val radius = dp2px(30f)private val size = dp2px(50f)private val paint = Paint().apply {isAntiAlias = truestyle = Paint.Style.FILL_AND_STROKEstrokeWidth = 1f}private val path = Path()private val rectF = RectF(offset, area - offset - size, offset + size, area - offset)private val rectFBg = RectF(0f, 0f, area, area)init {attrs?.let {val types = context.obtainStyledAttributes(it, R.styleable.PorterDuffView)type = types.getInt(R.styleable.PorterDuffView_porter_type, type)types.recycle()}}override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {setMeasuredDimension(area.toInt(), area.toInt())}override fun onDraw(canvas: Canvas) {val count = canvas.saveLayer(null, null, Canvas.ALL_SAVE_FLAG)path.reset()path.addCircle(area - offset - radius, radius + offset, radius, Path.Direction.CCW)paint.color = dstColorcanvas.drawPath(path, paint)path.reset()path.addRect(rectF, Path.Direction.CCW)paint.color = srcColorpaint.xfermode = xfermodecanvas.drawPath(path, paint)paint.xfermode = nullpath.reset()path.addRect(rectFBg, Path.Direction.CCW)path.addRect(rectF, Path.Direction.CW)paint.color = transparentColorpaint.xfermode = xfermodecanvas.drawPath(path, paint)paint.xfermode = nullcanvas.restoreToCount(count)}private fun dp2px(dpValue: Float): Float {val scale = Resources.getSystem().displayMetrics.densityreturn dpValue * scale + 0.5f}}
<?xml version="1.0" encoding="utf-8"?><resources><declare-styleable name="PorterDuffView"><attr name="porter_type" format="enum" ><enum name="clear" value="0" /><enum name="src" value="1" /><enum name="dst" value="2" /><enum name="src_over" value="3" /><enum name="dst_over" value="4" /><enum name="src_in" value="5" /><enum name="dst_in" value="6" /><enum name="src_out" value="7" /><enum name="dst_out" value="8" /><enum name="src_atop" value="9" /><enum name="dst_atop" value="10" /><enum name="xor" value="11" /><enum name="darken" value="16" /><enum name="lighten" value="17" /><enum name="multiply" value="13" /><enum name="screen" value="14" /><enum name="add" value="12" /><enum name="overlay" value="15" /></attr></declare-styleable></resources>
<?xml version="1.0" encoding="utf-8"?><androidx.core.widget.NestedScrollView 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:background="@color/white"android:orientation="vertical"><com.ypw.code.view.FlowLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"><com.ypw.code.view.PorterDuffViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_margin="@dimen/sw_2"android:background="#f0eeeeee"app:porter_type="clear" /><com.ypw.code.view.PorterDuffViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_margin="@dimen/sw_2"android:background="#f0eeeeee"app:porter_type="src" /><com.ypw.code.view.PorterDuffViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_margin="@dimen/sw_2"android:background="#f0eeeeee"app:porter_type="dst" /><com.ypw.code.view.PorterDuffViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_margin="@dimen/sw_2"android:background="#f0eeeeee"app:porter_type="src_over" /><com.ypw.code.view.PorterDuffViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_margin="@dimen/sw_2"android:background="#f0eeeeee"app:porter_type="dst_over" /><com.ypw.code.view.PorterDuffViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_margin="@dimen/sw_2"android:background="#f0eeeeee"app:porter_type="src_in" /><com.ypw.code.view.PorterDuffViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_margin="@dimen/sw_2"android:background="#f0eeeeee"app:porter_type="dst_in" /><com.ypw.code.view.PorterDuffViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_margin="@dimen/sw_2"android:background="#f0eeeeee"app:porter_type="src_out" /><com.ypw.code.view.PorterDuffViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_margin="@dimen/sw_2"android:background="#f0eeeeee"app:porter_type="dst_out" /><com.ypw.code.view.PorterDuffViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_margin="@dimen/sw_2"android:background="#f0eeeeee"app:porter_type="src_atop" /><com.ypw.code.view.PorterDuffViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_margin="@dimen/sw_2"android:background="#f0eeeeee"app:porter_type="dst_atop" /><com.ypw.code.view.PorterDuffViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_margin="@dimen/sw_2"android:background="#f0eeeeee"app:porter_type="xor" /><com.ypw.code.view.PorterDuffViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_margin="@dimen/sw_2"android:background="#f0eeeeee"app:porter_type="darken" /><com.ypw.code.view.PorterDuffViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_margin="@dimen/sw_2"android:background="#f0eeeeee"app:porter_type="lighten" /><com.ypw.code.view.PorterDuffViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_margin="@dimen/sw_2"android:background="#f0eeeeee"app:porter_type="multiply" /><com.ypw.code.view.PorterDuffViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_margin="@dimen/sw_2"android:background="#f0eeeeee"app:porter_type="screen" /><com.ypw.code.view.PorterDuffViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_margin="@dimen/sw_2"android:background="#f0eeeeee"app:porter_type="overlay" /><com.ypw.code.view.PorterDuffViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_margin="@dimen/sw_2"android:background="#f0eeeeee"app:porter_type="add" /></com.ypw.code.view.FlowLayout></androidx.core.widget.NestedScrollView>
Path.fillType + Path.Direction.CW/CCW
path.fillType 路径的填充方式:
- WINDING: 非零环绕, 保留两种图形的全部区域
- INVERSEWINDING: 反非零环绕, **保留非两种图形的区域**_
- EVENODD: 奇偶, **去除第二种图形和第一种图形的重叠部分**_
- INVERSEEVEN_ODD: 反奇偶, **填充非图形区域并去除两种图形的非重叠部分_**
path.addxxx(xxx, Path.Direction.CW/CCW) 路径的绘制方向
- Path.Direction.CW: 顺时针方向
- Path.Direction.CCW: 逆时针方向
path.fillType 可单独使用, 也可以和 path.addxxx(xxx, Path.Direction.CW/CCW) 一起使用
第一种情况: 两种图形里有一个带圆角, 且是同向绘制
第二种情况: 两种图形里有一个带圆角, 且是异向绘制
第三种情况: 两种图形里不包含圆角, 且是同向绘制
第四种情况: 两种图形里不包含圆角, 且是异向绘制

// 正方形和圆角正方形override fun onDraw(canvas: Canvas) {val size = min(width, height) / 4fval path = Path()val rectF = RectF(size / 2, size / 2, width - size / 2, height - size / 2)path.fillType = dirpath.addRect(rectF, Path.Direction.CW)path.addRoundRect(rectF, size / 2, size / 2, Path.Direction.CW)canvas.drawPath(path, paint)}// 两个圆形override fun onDraw(canvas: Canvas) {val centerX = width / 2fval centerY = height / 2fval size = min(centerX, centerY) / 2fval path = Path()path.fillType = dirpath.addCircle(centerX - size / 2, centerY, size, Path.Direction.CW)path.addCircle(centerX + size / 2, centerY, size, Path.Direction.CCW)canvas.drawPath(path, paint)}// 正方形和五角星override fun onDraw(canvas: Canvas) {val pathData = context.getString(R.string.path_star)// 根据 矢量图形里的 PathData 创建 Pathval pathStart = PathUtils.createPathFromPathData(pathData)// 应用 Matrixif (pathStart != null) {// 计算缩放比例val scaleWidth: Float = width / 1024.0fval scaleHeight: Float = height / 1024.0f// 创建 Matrix 处理 path 缩放val matrix = Matrix()matrix.postScale(scaleWidth, scaleHeight)pathStart.transform(matrix)}val size = min(width, height) / 4fval path = Path()val rectF = RectF(size / 2, size / 2, width - size / 2, height - size / 2)val rectF1 = RectF(size, size, width - size, height - size)path.fillType = dirpath.addRect(rectF, Path.Direction.CCW)path.addPath(pathStart)canvas.drawPath(path, paint)}// 大小正方形override fun onDraw(canvas: Canvas) {val size = min(width, height) / 4fval path = Path()val rectF = RectF(size / 2, size / 2, width - size / 2, height - size / 2)val rectF1 = RectF(size, size, width - size, height - size)path.fillType = dirpath.addRect(rectF1, Path.Direction.CCW)path.addRect(rectF, Path.Direction.CW)canvas.drawPath(path, paint)}
Path.Op - path.op(xxx, Path.Op.XXX)
注: Path.Op 添加于 Android4.4 - API 19
与 Region.Op 类似, 只是少了一个 REPLACE, Region.Op
class TempTextView constructor(context: Context, attrs: AttributeSet? = null) : TextView(context, attrs) {val paint = Paint().apply {isAntiAlias = truecolor = Color.parseColor("#FF2C98F0")style = Paint.Style.FILL}val pathRect1: Path by lazy {Path().apply {addRect(0f, 0f, width * 0.6f, height * 0.6f, Path.Direction.CW)}}val pathRect2: Path by lazy {Path().apply {addRect(width * 0.4f, height * 0.4f, width.toFloat(), height.toFloat(), Path.Direction.CW)}}var type = 0private val op: Path.Op by lazy {val p = when (type) {0 -> {Path.Op.DIFFERENCE}1 -> {Path.Op.INTERSECT}2 -> {Path.Op.UNION}3 -> {Path.Op.XOR}else -> {Path.Op.REVERSE_DIFFERENCE}}text = p.namep}init {attrs?.let {val types = context.obtainStyledAttributes(it, R.styleable.TempTextView)type = types.getInt(R.styleable.TempTextView_op_type, type)types.recycle()}setLayerType(LAYER_TYPE_SOFTWARE, null)}override fun onDraw(canvas: Canvas) {val path = Path()path.op(pathRect1, pathRect2, op)canvas.drawPath(path, paint)super.onDraw(canvas)}}
<?xml version="1.0" encoding="utf-8"?><resources><declare-styleable name="TempTextView"><attr name="op_type" format="enum"><enum name="DIFFERENCE" value="0"/><enum name="INTERSECT" value="1"/><enum name="UNION" value="2"/><enum name="XOR" value="3"/><enum name="REVERSE_DIFFERENCE" value="4"/><enum name="REPLACE" value="5"/></attr></declare-styleable></resources>
<?xml version="1.0" encoding="utf-8"?><androidx.core.widget.NestedScrollView 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:background="@color/white"android:orientation="vertical"><com.ypw.code.view.FlowLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"><com.ypw.code.view.TempTextViewandroid:layout_width="@dimen/sw_160"android:layout_height="@dimen/sw_160"android:textSize="@dimen/sw_16"android:gravity="center"android:layout_margin="@dimen/sw_5"android:background="#80eeeeee"app:op_type="DIFFERENCE"/><com.ypw.code.view.TempTextViewandroid:layout_width="@dimen/sw_160"android:layout_height="@dimen/sw_160"android:textSize="@dimen/sw_16"android:gravity="center"android:layout_margin="@dimen/sw_5"android:background="#80eeeeee"app:op_type="INTERSECT"/><com.ypw.code.view.TempTextViewandroid:layout_width="@dimen/sw_160"android:layout_height="@dimen/sw_160"android:textSize="@dimen/sw_16"android:gravity="center"android:layout_margin="@dimen/sw_5"android:background="#80eeeeee"app:op_type="UNION"/><com.ypw.code.view.TempTextViewandroid:layout_width="@dimen/sw_160"android:layout_height="@dimen/sw_160"android:textSize="@dimen/sw_16"android:layout_margin="@dimen/sw_5"android:background="#80eeeeee"android:gravity="center"app:op_type="XOR"/><com.ypw.code.view.TempTextViewandroid:layout_width="@dimen/sw_160"android:layout_height="@dimen/sw_160"android:textSize="@dimen/sw_16"android:layout_margin="@dimen/sw_5"android:background="#80eeeeee"android:gravity="center"app:op_type="REVERSE_DIFFERENCE"/></com.ypw.code.view.FlowLayout></androidx.core.widget.NestedScrollView>
