Android中Paint调用drawPath两条线颜色叠加

在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
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,837评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,551评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,417评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,448评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,524评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,554评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,569评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,316评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,766评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,077评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,240评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,912评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,560评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,176评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,425评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,114评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,114评论 2 352

推荐阅读更多精彩内容