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
@Override
protected 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 示例 View
class PorterDuffView constructor(context: Context, attrs: AttributeSet? = null) : View(context, attrs) {
private var type = 0
private 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 = 0
private 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 = true
style = Paint.Style.FILL_AND_STROKE
strokeWidth = 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 = dstColor
canvas.drawPath(path, paint)
path.reset()
path.addRect(rectF, Path.Direction.CCW)
paint.color = srcColor
paint.xfermode = xfermode
canvas.drawPath(path, paint)
paint.xfermode = null
path.reset()
path.addRect(rectFBg, Path.Direction.CCW)
path.addRect(rectF, Path.Direction.CW)
paint.color = transparentColor
paint.xfermode = xfermode
canvas.drawPath(path, paint)
paint.xfermode = null
canvas.restoreToCount(count)
}
private fun dp2px(dpValue: Float): Float {
val scale = Resources.getSystem().displayMetrics.density
return 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.FlowLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.ypw.code.view.PorterDuffView
android: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.PorterDuffView
android: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.PorterDuffView
android: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.PorterDuffView
android: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.PorterDuffView
android: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.PorterDuffView
android: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.PorterDuffView
android: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.PorterDuffView
android: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.PorterDuffView
android: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.PorterDuffView
android: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.PorterDuffView
android: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.PorterDuffView
android: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.PorterDuffView
android: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.PorterDuffView
android: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.PorterDuffView
android: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.PorterDuffView
android: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.PorterDuffView
android: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.PorterDuffView
android: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) / 4f
val path = Path()
val rectF = RectF(size / 2, size / 2, width - size / 2, height - size / 2)
path.fillType = dir
path.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 / 2f
val centerY = height / 2f
val size = min(centerX, centerY) / 2f
val path = Path()
path.fillType = dir
path.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 创建 Path
val pathStart = PathUtils.createPathFromPathData(pathData)
// 应用 Matrix
if (pathStart != null) {
// 计算缩放比例
val scaleWidth: Float = width / 1024.0f
val scaleHeight: Float = height / 1024.0f
// 创建 Matrix 处理 path 缩放
val matrix = Matrix()
matrix.postScale(scaleWidth, scaleHeight)
pathStart.transform(matrix)
}
val size = min(width, height) / 4f
val 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 = dir
path.addRect(rectF, Path.Direction.CCW)
path.addPath(pathStart)
canvas.drawPath(path, paint)
}
// 大小正方形
override fun onDraw(canvas: Canvas) {
val size = min(width, height) / 4f
val 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 = dir
path.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 = true
color = 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 = 0
private 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.name
p
}
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.FlowLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.ypw.code.view.TempTextView
android: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.TempTextView
android: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.TempTextView
android: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.TempTextView
android: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.TempTextView
android: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>