Android自定義控件 支持移動、縮放、旋轉(zhuǎn)功能的ImageView

封面

轉(zhuǎn)載請注明出處:http://www.itdecent.cn/p/c954e2aea2f3

本文出自 容華謝后的博客

0.寫在前面

今天寫一篇關(guān)于自定義控件的文章,基于ImageView控件,給它加上移動、多點縮放、兩指旋轉(zhuǎn)的功能,先看下效果:

效果圖

布局中可以添加多個MatrixImage,位置可以自由移動,涉及到一些簡單的三角函數(shù)知識,說下實現(xiàn)的思路:

  • 基于ImageView,因為要實現(xiàn)縮放、移動、旋轉(zhuǎn)功能,將ImageView的scaleType設(shè)置為MATRIX模式

  • 獲取圖片的顯示區(qū)域,得到上、下、左、右位置信息

  • 根據(jù)圖片的顯示區(qū)域,繪制四個邊框,邊框隨著圖片的區(qū)域變化而變化

  • 繪制每個角的控制點,根據(jù)控制點的位置,實現(xiàn)縮放功能

  • 重寫onTouchEvent方法,實現(xiàn)圖片的移動和旋轉(zhuǎn)功能

一起來看下實現(xiàn)的代碼邏輯,代碼比較多,完整的項目代碼在文末貼上。

1.準(zhǔn)備

先初始化一些參數(shù):

class MatrixImageView : AppCompatImageView {

    // 控件寬度
    private var mWidth = 0

    // 控件高度
    private var mHeight = 0

    // 第一次繪制
    private var mFirstDraw = true

    // 是否顯示控制框
    private var mShowFrame = false

    // 當(dāng)前Image矩陣
    private var mImgMatrix = Matrix()

    // 畫筆
    private lateinit var mPaint: Paint

    // 觸摸模式
    private var touchMode: MatrixImageUtils.TouchMode? = null

    // 第二根手指是否按下
    private var mIsPointerDown = false

    // 按下點x坐標(biāo)
    private var mDownX = 0f

    // 按下點y坐標(biāo)
    private var mDownY = 0f

    // 上一次的觸摸點x坐標(biāo)
    private var mLastX = 0f

    // 上一次的觸摸點y坐標(biāo)
    private var mLastY = 0f

    // 旋轉(zhuǎn)角度
    private var mDegree: Float = 0.0f

    // 旋轉(zhuǎn)圖標(biāo)
    private lateinit var mRotateIcon: Bitmap

    // 圖片控制框顏色
    private var mFrameColor = Color.parseColor("#1677FF")

    // 連接線寬度
    private var mLineWidth = dp2px(context, 2f)

    // 縮放控制點半徑
    var mScaleDotRadius = dp2px(context, 5f)

    // 旋轉(zhuǎn)控制點半徑
    var mRotateDotRadius = dp2px(context, 12f)

    // 按下監(jiān)聽
    private var mDownClickListener: ((view: View, pointF: PointF) -> Unit)? = null

    // 長按監(jiān)聽
    private var mLongClickListener: ((view: View, pointF: PointF) -> Unit)? = null

    // 移動監(jiān)聽
    private var mMoveListener: ((view: View, pointF: PointF) -> Unit)? = null

    // 長按監(jiān)聽計時任務(wù)
    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()
    }

    ...
}

增加一些屬性設(shè)置,可以在布局文件中對控件進行調(diào)整,初始化畫筆和旋轉(zhuǎn)控制點圖標(biāo),控件的寬高在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 -> { // 旋轉(zhuǎn)控制點半徑
                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

    // 旋轉(zhuǎn)圖標(biāo)
    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.繪制

先獲取圖片的坐標(biāo)信息,默認顯示在控件中心,然后繪制邊框和控制點:

override fun draw(canvas: Canvas?) {
    super.draw(canvas)
    if (canvas == null || drawable == null) {
        return
    }

    val imgRect = getImageRectF(this)
    // 左上角x坐標(biāo)
    val left = imgRect.left
    // 左上角y坐標(biāo)
    val top = imgRect.top
    // 右下角x坐標(biāo)
    val right = imgRect.right
    // 右下角y坐標(biāo)
    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)
    // 上中間控制點,旋轉(zhuǎn)
    val rotateLine = mRotateDotRadius / 3
    canvas.drawLine(middleX, top - rotateLine, middleX, top, mPaint)
    canvas.drawCircle(middleX, top - rotateLine - mRotateDotRadius, mRotateDotRadius, mPaint)
    // 上中間控制點,旋轉(zhuǎn)圖標(biāo)
    canvas.drawBitmap(
        mRotateIcon,
        middleX - mRotateIcon.width / 2,
        top - rotateLine - mRotateDotRadius - mRotateIcon.width / 2,
        mPaint
    )
}

繪制完成是這樣的效果:

邊框和控制點

3.Touch事件處理

要處理移動、縮放、單個旋轉(zhuǎn)控制點旋轉(zhuǎn),兩指旋轉(zhuǎn)這四種Touch事件,因為重寫了onTouchEvent方法,還要再加上點擊事件和長按事件的處理。

其中ACTION_POINTER_DOWN接收的是兩指旋轉(zhuǎn)中,第二根手指的坐標(biāo)信息,單個旋轉(zhuǎn)控制點旋轉(zhuǎn)和兩指旋轉(zhuǎn)邏輯差不多,是以圖片中心為第一根手指的位置,旋轉(zhuǎn)控制點
為第二根手指的位置,關(guān)于旋轉(zhuǎn)角度的計算,一起往下看。

override fun onTouchEvent(event: MotionEvent?): Boolean {
    if (event == null || drawable == null) {
        return super.onTouchEvent(event)
    }
    // x坐標(biāo)
    val x = event.x
    // y坐標(biāo)
    val y = event.y
    // 圖片顯示區(qū)域
    val imageRect = getImageRectF(this)
    // 圖片中心點x坐標(biāo)
    val centerX = (imageRect.right - imageRect.left) / 2 + imageRect.left
    // 圖片中心點y坐標(biāo)
    val centerY = (imageRect.bottom - imageRect.top) / 2 + imageRect.top

    when (event.action.and(ACTION_MASK)) {
        ACTION_DOWN -> {
            // 按下監(jiān)聽
            mDownClickListener?.invoke(this, PointF(x, y))
            // 判斷是否在圖片實際顯示區(qū)域內(nèi)
            touchMode = getTouchMode(this, x, y)
            if (touchMode == TOUCH_OUTSIDE) {
                mShowFrame = false
                invalidate()
                return super.onTouchEvent(event)
            }
            mDownX = x
            mDownY = y
            mLastX = x
            mLastY = y
            // 旋轉(zhuǎn)控制點,點擊后以圖片中心為基準(zhǔn),計算當(dāng)前旋轉(zhuǎn)角度
            if (touchMode == TOUCH_ROTATE) {
                // 旋轉(zhuǎn)角度
                mDegree = callRotation(centerX, centerY, x, y)
            }
            mShowFrame = true
            invalidate()

            // 長按監(jiān)聽計時
            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 -> {
            // 旋轉(zhuǎn)事件
            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方法主要處理圖片的移動、旋轉(zhuǎn)、縮放功能,在上述onTouchEvent方法中的ACTION_MOVE中被觸發(fā):

/**
 * 手指移動
 *
 * @param x         x坐標(biāo)
 * @param y         y坐標(biāo)
 * @param imageRect 圖片顯示區(qū)域
 */
private fun touchMove(x: Float, y: Float, imageRect: RectF) {
    // 左上角x坐標(biāo)
    val left = imageRect.left
    // 左上角y坐標(biāo)
    val top = imageRect.top
    // 右下角x坐標(biāo)
    val right = imageRect.right
    // 右下角y坐標(biāo)
    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)
    // 當(dāng)前縮放距離
    val scaleTrans = getDistanceOf2Points(mLastX, mLastY, x, y)
    // 縮放系數(shù),x軸方向
    val scaleFactorX: Float
    // 縮放系數(shù),y軸方向
    val scaleFactorY: Float
    // 縮放基準(zhǔn)點x坐標(biāo)
    val scaleBaseX: Float
    // 縮放基準(zhǔn)點y坐標(biāo)
    val scaleBaseY: Float

    when (touchMode) {
        TOUCH_IMAGE -> {
            mImgMatrix.postTranslate(x - mLastX, y - mLastY)
            imageMatrix = mImgMatrix
            return
        }
        TOUCH_ROTATE -> {
            // 圖片中心點x坐標(biāo)
            val centerX = (imageRect.right - imageRect.left) / 2 + imageRect.left
            // 圖片中心點y坐標(biāo)
            val centerY = (imageRect.bottom - imageRect.top) / 2 + imageRect.top
            // 旋轉(zhuǎn)角度
            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 計算旋轉(zhuǎn)的角度

deltaX是圖片中心點和旋轉(zhuǎn)點的水平方向距離,deltaY是垂直方向距離,atan2是反正切,計算的是旋轉(zhuǎn)控制點與中心點的連接線,與X軸的夾角弧度,然后通過toDegrees方法轉(zhuǎn)換為夾角角度。

向右順時針旋轉(zhuǎn),角度越來越大,角度遞增圖片向右旋轉(zhuǎn),向左則相反。

/**
 * 計算旋轉(zhuǎn)的角度
 *
 * @param baseX 基準(zhǔn)點x坐標(biāo)
 * @param baseY 基準(zhǔn)點y坐標(biāo)
 * @param rotateX 旋轉(zhuǎn)點x坐標(biāo)
 * @param rotateY 旋轉(zhuǎn)點y坐標(biāo)
 * @return 旋轉(zhuǎn)的角度
 */
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()
}

看圖說話:

計算旋轉(zhuǎn)的角度

了解下弧度與角度的計算公式:

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

  • 弧度 = 角度 / 180 * π

  • 角度 = 弧度 / π * 180

4.3 計算兩點之間的距離

這個比較簡單了,三角形已知兩條直角邊的值求斜邊,勾股定理:a2 + b2 = c2

/**
 * 獲取兩個點之間的距離
 *
 * @param x1 第一個點x坐標(biāo)
 * @param y1 第一個點y坐標(biāo)
 * @param x2 第二個點x坐標(biāo)
 * @param y2 第二個點y坐標(biāo)
 * @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,謝謝!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容