在canvas中调用drawPath方法涂抹区域的时候 两条path如果是半透明的颜色 颜色值会叠加
设置Paint的Xfermode根本没用 最直接的办法就是使用path.addPath方法将所有path合并为一个path
再绘制
// 创建一条合并的path
Path().apply { mPathList.forEach { addPath(it) } }
// 绘制
canvas.drawPath(it, mPaint)
就可以使两条path或者多个path的半透明颜色不叠加
分享一个放大缩小涂抹图片的自定义View
/**
* 放大缩小涂鸦
*/
class ApplyView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private var mCurrentStatus = 0
// 缩放控制器
private var mScaleDetector: ScaleGestureDetector? = null
// 图片被拖动的X距离
private var mFocusX = 0f
// 图片被拖动的Y距离
private var mFocusY = 0f
// 初始化缩放比例
private var mInitScale = 0f
// 总缩放比例
private var mTotalScale = 0f
// 每次缩放的倍数
private var mScaledRatio = 0f
// 控件的宽度
private var mWidth = 0
// 控件的高度
private var mHeight = 0
// 源图像
private var mSourceBitmap: Bitmap? = null
// 是否初始化绘制完成
private var isInitDrawFinish = false
// 涂抹显示的画笔
private var mPaint = Paint().apply {
// 消除锯齿
isAntiAlias = true
// 画笔样式:线
style = Paint.Style.STROKE
// 设置笔刷的样式:圆
strokeJoin = Paint.Join.ROUND
strokeCap = Paint.Cap.ROUND
// 画笔宽度:50
strokeWidth = 50f
// 画笔颜色:红色
color = Color.parseColor("#CDFC46")
alpha = 178
}
// 每一笔手写路径
private var mApplyPath: Path? = null
// 合并所有path之后再绘制 这就是path交叉没有重叠颜色的关键 其他设置都没有用!!!
private var mPath: Path? = null
// 保存涂鸦操作,撤销使用
private val mPathList = CopyOnWriteArrayList<Path>()
/*用于对图片进行移动和缩放变换的矩阵*/
private var mMatrix = Matrix()
// 记录图片横向偏移值
private var mTotalTranslateX = 0f
// 记录图片纵向偏移值
private var mTotalTranslateY = 0f
// 当前图片的宽
private var mCurrentBitmapWidth = 0f
// 预览图片的高
private var mCurrentBitmapHeight = 0f
init {
mCurrentStatus = STATUS_INIT
setLayerType(LAYER_TYPE_SOFTWARE, null)
mScaleDetector = ScaleGestureDetector(context, ScaleListener())
}
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
if (changed) {
mWidth = width
mHeight = height
}
}
fun setBitmap(bitmap: Bitmap?) {
if (bitmap == null) {
return
}
mSourceBitmap = bitmap
invalidate()
}
override fun onDraw(canvas: Canvas) {
canvas.save()
when (mCurrentStatus) {
STATUS_INIT -> initBitmap(canvas)
STATUS_CHANGE -> {
change(canvas)
handWriting(canvas, STATUS_HANDWRITING)
}
STATUS_HANDWRITING -> handWriting(canvas, STATUS_HANDWRITING)
STATUS_CLEAR -> handWriting(canvas, STATUS_CLEAR)
STATUS_UNDO -> handWriting(canvas, STATUS_UNDO)
}
canvas.restore()
}
/**
* 初始化预览图,居中显示
*/
private fun initBitmap(canvas: Canvas) {
if (mSourceBitmap == null) {
return
}
// 重置当前Matrix(将当前Matrix重置为单位矩阵)
mMatrix.reset()
// 获取图片实际宽高
val bitmapWidth = mSourceBitmap!!.width
val bitmapHeight = mSourceBitmap!!.height
// mWidth为控件宽,产品要求:将图片宽度充满,高度等比缩放
val ratio = mWidth / (bitmapWidth * 1.0f)
mMatrix.postScale(ratio, ratio)
// mHeight为控件高,在纵坐标方向上进行偏移,以保证图片居中显示
val translateY = (mHeight - bitmapHeight * ratio) / 2f
mMatrix.postTranslate(0f, translateY)
// 记录图片在矩阵上的纵向偏移值
mTotalTranslateY = translateY
// 记录图片在矩阵上的总缩放比例
mTotalScale = ratio.also { mInitScale = it }
// 当前图片的宽
mCurrentBitmapWidth = bitmapWidth * mInitScale
// 当前图片的高
mCurrentBitmapHeight = bitmapHeight * mInitScale
// 绘制图片
canvas.drawBitmap(mSourceBitmap!!, mMatrix, null)
//计算图片的四个顶点坐标,以便涂鸦时候的边界判断.
computeBoundary(mTotalScale, mTotalTranslateX, mTotalTranslateY)
isInitDrawFinish = true
}
// 避免float精度损失引起误差
private var totalScale = .0f
/**
* 对图片进行缩放和移动
*/
private fun change(canvas: Canvas) {
mMatrix.reset()
// 将图片按总缩放比例进行缩放
mMatrix.postScale(mTotalScale, mTotalScale)
// 图片变化后的宽度
val scaledWidth = mSourceBitmap!!.width * mTotalScale
// 图片变化后的高度
val scaledHeight = mSourceBitmap!!.height * mTotalScale
// 当前图片的宽度
mCurrentBitmapWidth = scaledWidth
// 当前图片的高度
mCurrentBitmapHeight = scaledHeight
// 缩放后对图片进行偏移,以保证缩放后中心点位置不变
// 缩放后对图片进行偏移,以保证缩放后中心点位置不变
var translateX: Float
var translateY: Float
// 缩放后的图片宽度小于的控件的宽度时,x基于控件中心缩放
if (scaledWidth < mWidth) {
translateX = (mWidth - scaledWidth) / 2f
} else {
// 推到过程:假设被放大的图片是一个矩形,左上角坐标为x0,y0,基点为x1,y1,图形被放大的倍数为q,求放大后的左上角坐标为x2,y2,现在我们要求这个x2,y2。根据图形可以得出公式:
// [(x0 - x2) + (x1 - x0)] / (x1 -x0) = q,然后就可以求出坐标x2的值,同理可以求出y2。x2和y2即图片需要移动的距离。
translateX =
mTotalTranslateX * mScaledRatio + mCenterPointX * (1 - mScaledRatio) + mFocusX
// 避免float的精度损失引起误差
if (totalScale == mTotalScale) {
translateX = mTotalTranslateX + mFocusX
}
// 进行边界检查,不允许将图片拖出边界
if (translateX > 0) {
// x方向上,左边界检查
translateX = 0f
} else if (scaledWidth - mWidth < abs(translateX)) {
// x方向上,右边界检查
translateX = mWidth - scaledWidth
}
}
// 缩放后的图片高度小于控件的高度时,y基于控件中心缩放
if (scaledHeight < mHeight) {
translateY = (mHeight - scaledHeight) / 2f
} else {
translateY =
mTotalTranslateY * mScaledRatio + mCenterPointY * (1 - mScaledRatio) + mFocusY
// 避免float的精度损失引起误差
if (totalScale == mTotalScale) {
translateY = mTotalTranslateY + mFocusY
}
// 进行边界检查,不允许将图片拖出边界
if (translateY > 0) {
// y方向上,上边界检查
translateY = 0f
} else if (scaledHeight - mHeight < abs(translateY)) {
// y方向上,下边界检查
translateY = mHeight - scaledHeight
}
}
mMatrix.postTranslate(translateX, translateY)
mTotalTranslateX = translateX
mTotalTranslateY = translateY
// 避免float精度损失引起误差
totalScale = mTotalScale
// 绘制
canvas.drawBitmap(mSourceBitmap!!, mMatrix, null)
// 截取
canvas.clipRect(
mTotalTranslateX, mTotalTranslateY,
mTotalTranslateX + mCurrentBitmapWidth,
mTotalTranslateY + mCurrentBitmapHeight
)
computeBoundary(mTotalScale, mTotalTranslateX, mTotalTranslateY)
}
/**
* 绘制
*/
private fun handWriting(canvas: Canvas, type: Int) {
mMatrix.reset()
// 将图片按总缩放比例进行缩放
mMatrix.postScale(mTotalScale, mTotalScale)
// 缩放后对图片进行偏移,以保证缩放后中心点位置不变
mMatrix.postTranslate(mTotalTranslateX, mTotalTranslateY)
canvas.drawBitmap(mSourceBitmap!!, mMatrix, null)
canvas.clipRect(
mTotalTranslateX, mTotalTranslateY,
mTotalTranslateX + mCurrentBitmapWidth,
mTotalTranslateY + mCurrentBitmapHeight
)
// 给canvas必须设置matrix,不然canvas会按初始化的方式去绘制
canvas.setMatrix(mMatrix)
// 线保留之前的画笔路径
when (type) {
STATUS_HANDWRITING -> {
mPath = combinePath()
mApplyPath?.let { mPath?.addPath(it) }
mPath?.let { canvas.drawPath(it, mPaint) }
}
STATUS_UNDO -> {
mPath = combinePath()
mPath?.let { canvas.drawPath(it, mPaint) }
}
// 清空的时候就不再绘制了
else -> {}
}
}
private inner class ScaleListener : SimpleOnScaleGestureListener() {
override fun onScale(detector: ScaleGestureDetector): Boolean {
if (mCurrentStatus == STATUS_CHANGE) {
// 每次缩放倍数
mScaledRatio = detector.scaleFactor
mTotalScale *= mScaledRatio
// 控制图片的缩放范围
mTotalScale = max(mInitScale, min(mTotalScale, mInitScale * 4))
//mScaledRatio是用来在图片缩放时,计算位移的,如果图片没有缩放,mScaledRatio始终为1,避免错误计算。
if (mTotalScale == mInitScale * 4) {
mScaledRatio = 1f
}
}
return true
}
}
// 用于贝塞尔曲线绘制
private var mDrawLastX = 0f
private var mDrawLastY = 0f
private var mTouchX = 0f
private var mTouchY = 0f
// 用于图片移动
private var mLastPointerCount = 0
private var mMoveLastX = 0f
private var mMoveLastY = 0f
// 图片左上角X坐标
private var mBitmapLeftTopX = 0f
// 图片左上角Y坐标
private var mBitmapLeftTopY = 0f
// 图片右上角X坐标
private var mBitmapRightTopX = 0f
// 图片左下角Y坐标
private var mBitmapLeftBottomY = 0f
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
if (!isInitDrawFinish) {
return true
}
mScaleDetector?.onTouchEvent(event)
var xTranslate = 0f
var yTranslate = 0f
// 拿到触摸点的个数
val pointerCount = event.pointerCount
// 得到多个触摸点的x与y均值
for (i in 0 until pointerCount) {
xTranslate += event.getX(i)
yTranslate += event.getY(i)
}
xTranslate /= pointerCount
yTranslate /= pointerCount
// 每当触摸点发生变化时,重置mLasX , mMoveLastY
if (pointerCount != mLastPointerCount) {
mMoveLastX = xTranslate
mMoveLastY = yTranslate
}
mLastPointerCount = pointerCount
val x = event.x
val y = event.y
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
if (x in mBitmapLeftTopX..mBitmapRightTopX && y >= mBitmapLeftTopY && y < mBitmapLeftBottomY) {
mDrawLastX = x
mTouchX = mDrawLastX
mDrawLastY = y
mTouchY = mDrawLastY
mApplyPath = Path()
mApplyPath?.moveTo(transformX(event.x), transformY(event.y))
mTouchBlock?.invoke()
}
}
MotionEvent.ACTION_MOVE -> {
if (event.pointerCount == 1) {
mCurrentStatus = STATUS_HANDWRITING
if (x in mBitmapLeftTopX..mBitmapRightTopX && y >= mBitmapLeftTopY && y < mBitmapLeftBottomY) {
mApplyPath?.let {
mDrawLastX = mTouchX
mDrawLastY = mTouchY
mTouchX = event.x
mTouchY = event.y
// 贝塞尔曲线绘制:控制点为上一次touch位置,结束点为移动距离的一半。
it.quadTo(
transformX(mDrawLastX), transformY(mDrawLastY),
(transformX(mDrawLastX) + transformX(mTouchX)) / 2,
(transformY(mDrawLastY) + transformY(mTouchY)) / 2
)
}
}
}
// 双指为移动
if (event.pointerCount == 2) {
centerPointBetweenFingers(event)
mCurrentStatus = STATUS_CHANGE
mApplyPath = null
}
if (mCurrentStatus == STATUS_CHANGE) {
val dX = xTranslate - mMoveLastX
val dY = yTranslate - mMoveLastY
// 缩放后的图片宽度大于控件宽度时
if (mCurrentBitmapWidth > mWidth) {
// 只有在图片可左右移动时,增加x
if (dX > 0 && mBitmapLeftTopX < 0 || dX < 0 && mBitmapRightTopX > mWidth) {
mFocusX = dX
}
}
// 缩放后的图片高度大于控件宽度时
if (mCurrentBitmapHeight > mHeight) {
// 只有在图片可上下移动时,增加y
if (dY > 0 && mBitmapLeftTopY < 0 || dY < 0 && mBitmapLeftBottomY > mHeight) {
mFocusY = dY
}
}
mMoveLastX = xTranslate
mMoveLastY = yTranslate
}
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
mLastPointerCount = 0
if (mCurrentStatus == STATUS_HANDWRITING) {
// 保存路径到集合中,撤销使用
mApplyPath?.let { mPathList.add(it) }
}
}
else -> {}
}
invalidate()
return true
}
/**
* 将屏幕触摸坐标x转换成在图片中的坐标
*/
private fun transformX(mTouchX: Float) = (mTouchX - mTotalTranslateX) / mTotalScale
/**
* 将屏幕触摸坐标y转换成在图片中的坐标
*/
private fun transformY(mTouchY: Float) = (mTouchY - mTotalTranslateY) / mTotalScale
/**
* 记录两指同时放在屏幕上时,中心点的横坐标值
*/
private var mCenterPointX = 0f
/**
* 记录两指同时放在屏幕上时,中心点的纵坐标值
*/
private var mCenterPointY = 0f
/**
* 计算两个手指之间中心点的坐标。
*
* @param event
*/
private fun centerPointBetweenFingers(event: MotionEvent) {
val xPoint0 = event.getX(0)
val yPoint0 = event.getY(0)
val xPoint1 = event.getX(1)
val yPoint1 = event.getY(1)
mCenterPointX = (xPoint0 + xPoint1) / 2
mCenterPointY = (yPoint0 + yPoint1) / 2
}
/**
* @param totalScale 图片缩放倍数
* @param totalTranslateX 图片在矩阵上的横向偏移值
* @param totalTranslateY 图片在矩阵上的纵向偏移值
* @Description:计算顶点坐标
*/
private fun computeBoundary(totalScale: Float, totalTranslateX: Float, totalTranslateY: Float) {
mBitmapLeftTopX = 0f * totalScale
mBitmapLeftTopY = 0f * totalScale
mBitmapLeftTopX += totalTranslateX
mBitmapLeftTopY += totalTranslateY
mBitmapRightTopX = mBitmapLeftTopX + mCurrentBitmapWidth
mBitmapLeftBottomY = mBitmapLeftTopY + mCurrentBitmapHeight
}
/**
* 清空涂鸦
*/
fun clearPath() {
mCurrentStatus = STATUS_CLEAR
mApplyPath = null
mPathList.clear()
invalidate()
}
/**
* 撤销涂鸦
*/
fun undoPath() {
if (mPathList.isEmpty()) {
return
}
mCurrentStatus = STATUS_UNDO
mApplyPath = null
mPathList.remove(mPathList.last())
invalidate()
}
// 合并Path
private fun combinePath() = Path().apply { mPathList.forEach { addPath(it) } }
/**
* 获取当前绘制完成的bitmap
*/
fun getApplyBitmap(): Bitmap? = mSourceBitmap?.run {
val bitmap = copy(Bitmap.Config.ARGB_8888, true)
val canvas = Canvas(bitmap)
canvas.drawPath(combinePath(), mPaint)
return bitmap
}
private var mTouchBlock: (() -> Unit)? = null
fun setTouchListener(block: (() -> Unit)?) {
mTouchBlock = block
}
companion object {
// 初始化
private const val STATUS_INIT = 1
// 图片变化
private const val STATUS_CHANGE = 2
// 涂鸦状态
private const val STATUS_HANDWRITING = 3
// 清除涂鸦
private const val STATUS_CLEAR = 4
// 撤销涂鸦
private const val STATUS_UNDO = 5
}
}