美文网首页Android进阶之路Android开发
Android自定义控件 支持移动、缩放、旋转功能的ImageV

Android自定义控件 支持移动、缩放、旋转功能的ImageV

作者: 容华谢后 | 来源:发表于2023-07-12 08:03 被阅读0次
    封面

    转载请注明出处:https://www.jianshu.com/p/c954e2aea2f3

    本文出自 容华谢后的博客

    0.写在前面

    今天写一篇关于自定义控件的文章,基于ImageView控件,给它加上移动、多点缩放、两指旋转的功能,先看下效果:

    效果图

    布局中可以添加多个MatrixImage,位置可以自由移动,涉及到一些简单的三角函数知识,说下实现的思路:

    • 基于ImageView,因为要实现缩放、移动、旋转功能,将ImageView的scaleType设置为MATRIX模式

    • 获取图片的显示区域,得到上、下、左、右位置信息

    • 根据图片的显示区域,绘制四个边框,边框随着图片的区域变化而变化

    • 绘制每个角的控制点,根据控制点的位置,实现缩放功能

    • 重写onTouchEvent方法,实现图片的移动和旋转功能

    一起来看下实现的代码逻辑,代码比较多,完整的项目代码在文末贴上。

    1.准备

    先初始化一些参数:

    class MatrixImageView : AppCompatImageView {
    
        // 控件宽度
        private var mWidth = 0
    
        // 控件高度
        private var mHeight = 0
    
        // 第一次绘制
        private var mFirstDraw = true
    
        // 是否显示控制框
        private var mShowFrame = false
    
        // 当前Image矩阵
        private var mImgMatrix = Matrix()
    
        // 画笔
        private lateinit var mPaint: Paint
    
        // 触摸模式
        private var touchMode: MatrixImageUtils.TouchMode? = null
    
        // 第二根手指是否按下
        private var mIsPointerDown = false
    
        // 按下点x坐标
        private var mDownX = 0f
    
        // 按下点y坐标
        private var mDownY = 0f
    
        // 上一次的触摸点x坐标
        private var mLastX = 0f
    
        // 上一次的触摸点y坐标
        private var mLastY = 0f
    
        // 旋转角度
        private var mDegree: Float = 0.0f
    
        // 旋转图标
        private lateinit var mRotateIcon: Bitmap
    
        // 图片控制框颜色
        private var mFrameColor = Color.parseColor("#1677FF")
    
        // 连接线宽度
        private var mLineWidth = dp2px(context, 2f)
    
        // 缩放控制点半径
        var mScaleDotRadius = dp2px(context, 5f)
    
        // 旋转控制点半径
        var mRotateDotRadius = dp2px(context, 12f)
    
        // 按下监听
        private var mDownClickListener: ((view: View, pointF: PointF) -> Unit)? = null
    
        // 长按监听
        private var mLongClickListener: ((view: View, pointF: PointF) -> Unit)? = null
    
        // 移动监听
        private var mMoveListener: ((view: View, pointF: PointF) -> Unit)? = null
    
        // 长按监听计时任务
        private var mLongClickJob: Job? = null
    
        constructor(context: Context) : this(context, null)
    
        constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
    
        constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
            context,
            attrs,
            defStyleAttr
        ) {
            setAttribute(attrs)
            init()
        }
    
        ...
    }
    

    增加一些属性设置,可以在布局文件中对控件进行调整,初始化画笔和旋转控制点图标,控件的宽高在onSizeChanged方法中确定:

    private fun setAttribute(attrs: AttributeSet?) {
        if (attrs == null) {
            return
        }
        val typedArray = context.obtainStyledAttributes(attrs, R.styleable.MatrixImageView)
        val indexCount = typedArray.indexCount
        for (i in 0 until indexCount) {
            when (val attr = typedArray.getIndex(i)) {
                R.styleable.MatrixImageView_fcLineWidth -> { // 连接线宽度
                    mLineWidth = typedArray.getDimension(attr, mLineWidth)
                }
                R.styleable.MatrixImageView_fcScaleDotRadius -> { // 缩放控制点半径
                    mScaleDotRadius = typedArray.getDimension(attr, mScaleDotRadius)
                }
                R.styleable.MatrixImageView_fcRotateDotRadius -> { // 旋转控制点半径
                    mRotateDotRadius = typedArray.getDimension(attr, mRotateDotRadius)
                }
                R.styleable.MatrixImageView_fcFrameColor -> { // 图片控制框颜色
                    mFrameColor = typedArray.getColor(attr, mFrameColor)
                }
            }
        }
        typedArray.recycle()
    }
    
    private fun init() {
        mPaint = Paint()
        mPaint.isAntiAlias = true
        mPaint.strokeWidth = mLineWidth
        mPaint.color = mFrameColor
        mPaint.style = Paint.Style.FILL
    
        // Matrix模式
        scaleType = ScaleType.MATRIX
    
        // 旋转图标
        val rotateIcon = decodeResource(resources, R.mipmap.ic_mi_rotate)
        val rotateIconWidth = (mRotateDotRadius * 1.6f).toInt()
        mRotateIcon = createScaledBitmap(rotateIcon, rotateIconWidth, rotateIconWidth, true)
    }
    
    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        this.mWidth = w
        this.mHeight = h
    }
    

    2.绘制

    先获取图片的坐标信息,默认显示在控件中心,然后绘制边框和控制点:

    override fun draw(canvas: Canvas?) {
        super.draw(canvas)
        if (canvas == null || drawable == null) {
            return
        }
    
        val imgRect = getImageRectF(this)
        // 左上角x坐标
        val left = imgRect.left
        // 左上角y坐标
        val top = imgRect.top
        // 右下角x坐标
        val right = imgRect.right
        // 右下角y坐标
        val bottom = imgRect.bottom
    
        // 图片移动到控件中心
        if (mFirstDraw) {
            mFirstDraw = false
            val centerX = (mWidth / 2).toFloat()
            val centerY = (mHeight / 2).toFloat()
            val imageWidth = right - left
            val imageHeight = bottom - top
            mImgMatrix.postTranslate(centerX - imageWidth / 2, centerY - imageHeight / 2)
            // 如果图片较大,缩放0.5倍
            if (imageWidth > width || imageHeight > height) {
                mImgMatrix.postScale(0.5f, 0.5f, centerX, centerY)
            }
            imageMatrix = mImgMatrix
        }
    
        // 不绘制控制框
        if (!mShowFrame) {
            return
        }
    
        // 上边框
        canvas.drawLine(left, top, right, top, mPaint)
        // 下边框
        canvas.drawLine(left, bottom, right, bottom, mPaint)
        // 左边框
        canvas.drawLine(left, top, left, bottom, mPaint)
        // 右边框
        canvas.drawLine(right, top, right, bottom, mPaint)
    
        // 左上角控制点,等比缩放
        canvas.drawCircle(left, top, mScaleDotRadius, mPaint)
        // 右上角控制点,等比缩放
        canvas.drawCircle(right, top, mScaleDotRadius, mPaint)
        // 左中间控制点,横向缩放
        canvas.drawCircle(left, top + (bottom - top) / 2, mScaleDotRadius, mPaint)
        // 右中间控制点,横向缩放
        canvas.drawCircle(right, top + (bottom - top) / 2, mScaleDotRadius, mPaint)
        // 左下角控制点,等比缩放
        canvas.drawCircle(left, bottom, mScaleDotRadius, mPaint)
        // 右下角控制点,等比缩放
        canvas.drawCircle(right, bottom, mScaleDotRadius, mPaint)
        // 下中间控制点,竖向缩放
        val middleX = (right - left) / 2 + left
        canvas.drawCircle(middleX, bottom, mScaleDotRadius, mPaint)
        // 上中间控制点,旋转
        val rotateLine = mRotateDotRadius / 3
        canvas.drawLine(middleX, top - rotateLine, middleX, top, mPaint)
        canvas.drawCircle(middleX, top - rotateLine - mRotateDotRadius, mRotateDotRadius, mPaint)
        // 上中间控制点,旋转图标
        canvas.drawBitmap(
            mRotateIcon,
            middleX - mRotateIcon.width / 2,
            top - rotateLine - mRotateDotRadius - mRotateIcon.width / 2,
            mPaint
        )
    }
    

    绘制完成是这样的效果:

    边框和控制点

    3.Touch事件处理

    要处理移动、缩放、单个旋转控制点旋转,两指旋转这四种Touch事件,因为重写了onTouchEvent方法,还要再加上点击事件和长按事件的处理。

    其中ACTION_POINTER_DOWN接收的是两指旋转中,第二根手指的坐标信息,单个旋转控制点旋转和两指旋转逻辑差不多,是以图片中心为第一根手指的位置,旋转控制点
    为第二根手指的位置,关于旋转角度的计算,一起往下看。

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        if (event == null || drawable == null) {
            return super.onTouchEvent(event)
        }
        // x坐标
        val x = event.x
        // y坐标
        val y = event.y
        // 图片显示区域
        val imageRect = getImageRectF(this)
        // 图片中心点x坐标
        val centerX = (imageRect.right - imageRect.left) / 2 + imageRect.left
        // 图片中心点y坐标
        val centerY = (imageRect.bottom - imageRect.top) / 2 + imageRect.top
    
        when (event.action.and(ACTION_MASK)) {
            ACTION_DOWN -> {
                // 按下监听
                mDownClickListener?.invoke(this, PointF(x, y))
                // 判断是否在图片实际显示区域内
                touchMode = getTouchMode(this, x, y)
                if (touchMode == TOUCH_OUTSIDE) {
                    mShowFrame = false
                    invalidate()
                    return super.onTouchEvent(event)
                }
                mDownX = x
                mDownY = y
                mLastX = x
                mLastY = y
                // 旋转控制点,点击后以图片中心为基准,计算当前旋转角度
                if (touchMode == TOUCH_ROTATE) {
                    // 旋转角度
                    mDegree = callRotation(centerX, centerY, x, y)
                }
                mShowFrame = true
                invalidate()
    
                // 长按监听计时
                mLongClickJob = coroutineDelay(Main, 500) {
                    val offsetX = abs(x - mLastX)
                    val offsetY = abs(y - mLastY)
                    val offset = dp2px(context, 10f)
                    if (offsetX <= offset && offsetY <= offset) {
                        mLongClickListener?.invoke(this, PointF(x, y))
                    }
                }
                return true
            }
            ACTION_CANCEL -> {
                mLongClickJob?.cancel()
            }
            ACTION_POINTER_DOWN -> {
                mLongClickJob?.cancel()
                mDegree = callRotation(event)
                mIsPointerDown = true
                return true
            }
            ACTION_MOVE -> {
                // 旋转事件
                if (event.pointerCount == 2) {
                    if (!mIsPointerDown) {
                        return true
                    }
                    val rotate = callRotation(event)
                    val rotateNow = rotate - mDegree
                    mDegree = rotate
                    mImgMatrix.postRotate(rotateNow, centerX, centerY)
                    imageMatrix = mImgMatrix
                    return true
                }
                if (mIsPointerDown) {
                    return true
                }
                // 移动、缩放事件
                touchMove(x, y, imageRect)
                mLastX = x
                mLastY = y
                invalidate()
                val offsetX = abs(x - mDownX)
                val offsetY = abs(y - mDownY)
                val offset = dp2px(context, 10f)
                if (offsetX > offset || offsetY > offset) {
                    mMoveListener?.invoke(this, PointF(x, y))
                }
                return true
            }
            ACTION_UP -> {
                mLongClickJob?.cancel()
                touchMode = null
                mIsPointerDown = false
                mDegree = 0f
            }
        }
        return super.onTouchEvent(event)
    }
    

    touchMove方法主要处理图片的移动、旋转、缩放功能,在上述onTouchEvent方法中的ACTION_MOVE中被触发:

    /**
     * 手指移动
     *
     * @param x         x坐标
     * @param y         y坐标
     * @param imageRect 图片显示区域
     */
    private fun touchMove(x: Float, y: Float, imageRect: RectF) {
        // 左上角x坐标
        val left = imageRect.left
        // 左上角y坐标
        val top = imageRect.top
        // 右下角x坐标
        val right = imageRect.right
        // 右下角y坐标
        val bottom = imageRect.bottom
        // 总的缩放距离,斜角
        val totalTransOblique = getDistanceOf2Points(left, top, right, bottom)
        // 总的缩放距离,水平
        val totalTransHorizontal = getDistanceOf2Points(left, top, right, top)
        // 总的缩放距离,垂直
        val totalTransVertical = getDistanceOf2Points(left, top, left, bottom)
        // 当前缩放距离
        val scaleTrans = getDistanceOf2Points(mLastX, mLastY, x, y)
        // 缩放系数,x轴方向
        val scaleFactorX: Float
        // 缩放系数,y轴方向
        val scaleFactorY: Float
        // 缩放基准点x坐标
        val scaleBaseX: Float
        // 缩放基准点y坐标
        val scaleBaseY: Float
    
        when (touchMode) {
            TOUCH_IMAGE -> {
                mImgMatrix.postTranslate(x - mLastX, y - mLastY)
                imageMatrix = mImgMatrix
                return
            }
            TOUCH_ROTATE -> {
                // 图片中心点x坐标
                val centerX = (imageRect.right - imageRect.left) / 2 + imageRect.left
                // 图片中心点y坐标
                val centerY = (imageRect.bottom - imageRect.top) / 2 + imageRect.top
                // 旋转角度
                val rotate = callRotation(centerX, centerY, x, y)
                val rotateNow = rotate - mDegree
                mDegree = rotate
                mImgMatrix.postRotate(rotateNow, centerX, centerY)
                imageMatrix = mImgMatrix
                return
            }
            TOUCH_CONTROL_1 -> {
                // 缩小
                scaleFactorX = if (x - mLastX > 0) {
                    (totalTransOblique - scaleTrans) / totalTransOblique
                } else {
                    (totalTransOblique + scaleTrans) / totalTransOblique
                }
                scaleFactorY = scaleFactorX
                // 右下角
                scaleBaseX = imageRect.right
                scaleBaseY = imageRect.bottom
            }
            TOUCH_CONTROL_2 -> {
                // 缩小
                scaleFactorX = if (x - mLastX < 0) {
                    (totalTransOblique - scaleTrans) / totalTransOblique
                } else {
                    (totalTransOblique + scaleTrans) / totalTransOblique
                }
                scaleFactorY = scaleFactorX
                // 左下角
                scaleBaseX = imageRect.left
                scaleBaseY = imageRect.bottom
            }
            TOUCH_CONTROL_3 -> {
                // 缩小
                scaleFactorX = if (x - mLastX > 0) {
                    (totalTransOblique - scaleTrans) / totalTransOblique
                } else {
                    (totalTransOblique + scaleTrans) / totalTransOblique
                }
                scaleFactorY = scaleFactorX
                // 右上角
                scaleBaseX = imageRect.right
                scaleBaseY = imageRect.top
            }
            TOUCH_CONTROL_4 -> {
                // 缩小
                scaleFactorX = if (x - mLastX < 0) {
                    (totalTransOblique - scaleTrans) / totalTransOblique
                } else {
                    (totalTransOblique + scaleTrans) / totalTransOblique
                }
                scaleFactorY = scaleFactorX
                // 左上角
                scaleBaseX = imageRect.left
                scaleBaseY = imageRect.top
            }
            TOUCH_CONTROL_5 -> {
                // 缩小
                scaleFactorX = if (x - mLastX > 0) {
                    (totalTransHorizontal - scaleTrans) / totalTransHorizontal
                } else {
                    (totalTransHorizontal + scaleTrans) / totalTransHorizontal
                }
                scaleFactorY = 1f
                // 右上角
                scaleBaseX = imageRect.right
                scaleBaseY = imageRect.top
            }
            TOUCH_CONTROL_6 -> {
                // 缩小
                scaleFactorX = if (x - mLastX < 0) {
                    (totalTransHorizontal - scaleTrans) / totalTransHorizontal
                } else {
                    (totalTransHorizontal + scaleTrans) / totalTransHorizontal
                }
                scaleFactorY = 1f
                // 左上角
                scaleBaseX = imageRect.left
                scaleBaseY = imageRect.top
            }
            TOUCH_CONTROL_7 -> {
                // 缩小
                scaleFactorX = 1f
                scaleFactorY = if (y - mLastY < 0) {
                    (totalTransVertical - scaleTrans) / totalTransVertical
                } else {
                    (totalTransVertical + scaleTrans) / totalTransVertical
                }
                // 左上角
                scaleBaseX = imageRect.left
                scaleBaseY = imageRect.top
            }
            else -> {
                return
            }
        }
    
        // 最小缩放值限制
        val scaleMatrix = Matrix(mImgMatrix)
        scaleMatrix.postScale(scaleFactorX, scaleFactorY, scaleBaseX, scaleBaseY)
        val scaleRectF = getImageRectF(this, scaleMatrix)
        if (scaleRectF.right - scaleRectF.left < mScaleDotRadius * 6
            || scaleRectF.bottom - scaleRectF.top < mScaleDotRadius * 6
        ) {
            return
        }
        // 缩放
        mImgMatrix.postScale(scaleFactorX, scaleFactorY, scaleBaseX, scaleBaseY)
        imageMatrix = mImgMatrix
    }
    

    4.一些计算

    4.1 获取图片在ImageView中的实际显示位置:

    /**
     * 获取图片在ImageView中的实际显示位置
     *
     * @param view ImageView
     * @return RectF
     */
    fun getImageRectF(view: ImageView): RectF {
        // 获得ImageView中Image的变换矩阵
        val matrix = view.imageMatrix
        return getImageRectF(view, matrix)
    }
    
    /**
     * 获取图片在ImageView中的实际显示位置
     *
     * @param view ImageView
     * @param matrix Matrix
     * @return RectF
     */
    fun getImageRectF(view: ImageView, matrix: Matrix): RectF {
        // 获得ImageView中Image的显示边界
        val bounds = view.drawable.bounds
        val rectF = RectF()
        matrix.mapRect(
            rectF,
            RectF(
                bounds.left.toFloat(),
                bounds.top.toFloat(),
                bounds.right.toFloat(),
                bounds.bottom.toFloat()
            )
        )
        return rectF
    }
    

    4.2 计算旋转的角度

    deltaX是图片中心点和旋转点的水平方向距离,deltaY是垂直方向距离,atan2是反正切,计算的是旋转控制点与中心点的连接线,与X轴的夹角弧度,然后通过toDegrees方法转换为夹角角度。

    向右顺时针旋转,角度越来越大,角度递增图片向右旋转,向左则相反。

    /**
     * 计算旋转的角度
     *
     * @param baseX 基准点x坐标
     * @param baseY 基准点y坐标
     * @param rotateX 旋转点x坐标
     * @param rotateY 旋转点y坐标
     * @return 旋转的角度
     */
    fun callRotation(baseX: Float, baseY: Float, rotateX: Float, rotateY: Float): Float {
        val deltaX = (baseX - rotateX).toDouble()
        val deltaY = (baseY - rotateY).toDouble()
        val radius = atan2(deltaY, deltaX)
        return Math.toDegrees(radius).toFloat()
    }
    

    看图说话:

    计算旋转的角度

    了解下弧度与角度的计算公式:

    • 完整圆的弧度为2π,角度为360度,所以180度等于π弧度

    • 弧度 = 角度 / 180 * π

    • 角度 = 弧度 / π * 180

    4.3 计算两点之间的距离

    这个比较简单了,三角形已知两条直角边的值求斜边,勾股定理:a² + b² = c²

    /**
     * 获取两个点之间的距离
     *
     * @param x1 第一个点x坐标
     * @param y1 第一个点y坐标
     * @param x2 第二个点x坐标
     * @param y2 第二个点y坐标
     * @return 两个点之间的距离
     */
    internal fun getDistanceOf2Points(x1: Float, y1: Float, x2: Float, y2: Float): Float {
        return sqrt((x1 - x2).pow(2) + (y1 - y2).pow(2))
    }
    

    5.写在最后

    最后附上多个控件的效果图:

    多个控件效果图

    GitHub地址:https://github.com/alidili/MatrixImage

    到这里,自定义控件MatrixImage的基本步骤就介绍完了,如有问题可以给我留言评论或者在GitHub中提交Issues,谢谢!

    相关文章

      网友评论

        本文标题:Android自定义控件 支持移动、缩放、旋转功能的ImageV

        本文链接:https://www.haomeiwen.com/subject/bkgyudtx.html