View->裁剪框View的绘制,手势处理

XML文件

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:background="@color/black">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">
        <com.yang.app.MyRootView
            android:id="@+id/my_root"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1"
            android:orientation="vertical"
            android:layout_marginLeft="60dp"
            android:layout_marginTop="60dp"
            android:layout_marginRight="60dp"
            android:layout_marginBottom="60dp">
        </com.yang.app.MyRootView>
    </LinearLayout>
    <com.yang.app.MyCropView
        android:id="@+id/my_crop"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
</RelativeLayout>

Activity代码

kotlin 复制代码
const val TAG = "Yang"
class MainActivity : AppCompatActivity() {
    var tempBitmap: Bitmap? = null
    var mRootView: MyRootView? = null
    var mCropView: MyCropView? = null
    @SuppressLint("MissingInflatedId")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val tempRect = RectF(0f, 0f, resources.displayMetrics.widthPixels.toFloat(), resources.displayMetrics.heightPixels.toFloat())
        mCropView = findViewById(R.id.my_crop) as? MyCropView
        mRootView = findViewById<MyRootView?>(R.id.my_root).apply {
            mCropView?.let {
                setRectChangeListener(it)
            }
        }

        CoroutineScope(Dispatchers.IO).launch {
            tempBitmap = getBitmap(resources, tempRect, R.drawable.real)
            withContext(Dispatchers.Main) {
                tempBitmap?.let {
                    // 设置裁剪框的初始位置
                    mCropView?.setOriginBitmapRect(RectF(0f, 0f, it.width.toFloat(), it.height.toFloat()))
                    mRootView?.setOriginBitmap(it)
                }
            }
        }
    }
}

fun getBitmap(resources : Resources, destRect : RectF, imageId: Int): Bitmap? {
    var imageWidth = -1
    var imageHeight = -1
    val preOption = BitmapFactory.Options().apply {
        // 只获取图片的宽高
        inJustDecodeBounds = true
        BitmapFactory.decodeResource(resources, imageId, this)
    }
    imageWidth = preOption.outWidth
    imageHeight = preOption.outHeight
    // 计算缩放比例
    val scaleMatrix = Matrix()
    // 确定未缩放Bitmap的RectF
    var srcRect = RectF(0f, 0f, imageWidth.toFloat(), imageHeight.toFloat())
    // 通过目标RectF, 确定缩放数值,存储在scaleMatrix中
    scaleMatrix.setRectToRect(srcRect, destRect, Matrix.ScaleToFit.CENTER)
    // 缩放数值再映射到原始Bitmap上,得到缩放后的RectF
    scaleMatrix.mapRect(srcRect)

    val finalOption = BitmapFactory.Options().apply {
        if (imageHeight > 0 && imageWidth > 0) {
            inPreferredConfig = Bitmap.Config.RGB_565
            inSampleSize = calculateInSampleSize(
                imageWidth,
                imageHeight,
                srcRect.width().toInt(),
                srcRect.height().toInt()
            )
        }
    }
    return BitmapFactory.decodeResource(resources, imageId, finalOption)
}

fun calculateInSampleSize(fromWidth: Int, fromHeight: Int, toWidth: Int, toHeight: Int): Int {
    var bitmapWidth = fromWidth
    var bitmapHeight = fromHeight
    if (fromWidth > toWidth|| fromHeight > toHeight) {
        var inSampleSize = 2
        // 计算最大的inSampleSize值,该值是2的幂,并保持原始宽高大于目标宽高
        while (bitmapWidth >= toWidth && bitmapHeight >= toHeight) {
            bitmapWidth /= 2
            bitmapHeight /= 2
            inSampleSize *= 2
        }
        return inSampleSize
    }
    return 1
}

fun setRectChangeListener(listener: RectChangedListener) {
	mRectChangeListener = listener
}

fun dpToPx(context: Context, dp: Float): Float {
    val metrics = context.resources.displayMetrics
    return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, metrics)
}

自定义View代码

  • 显示图片的View
kotlin 复制代码
class MyRootView constructor(context: Context, attrs: AttributeSet? ) : View(context, attrs) {
    private var lastX = 0f
    private var lastY = 0f
    private val scroller = OverScroller(context)
    private var tracker: VelocityTracker? = null
    private var initialLeft = 0
    private var initialTop = 0
    private var mDestRect: RectF? = null
    private val mScaleMatrix = Matrix()
    private var mRectChangeListener: RectChangedListener? = null
    private var mPaint = Paint().apply {
        isAntiAlias = true
        isFilterBitmap = true
    }

    private var mOriginBitmap: Bitmap? = null

    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
        super.onLayout(changed, left, top, right, bottom)
        if (initialLeft == 0) initialLeft = left
        if (initialTop == 0) initialTop = top
        mDestRect = RectF(0f, 0f, measuredWidth.toFloat(), measuredHeight.toFloat())
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                tracker = VelocityTracker.obtain().apply {
                    addMovement(event)
                }
                lastX = event.rawX
                lastY = event.rawY
            }

            MotionEvent.ACTION_MOVE -> {
                if (tracker == null) {
                    tracker = VelocityTracker.obtain()
                    tracker?.addMovement(event)
                }
                val dx = event.rawX - lastX
                val dy = event.rawY - lastY
                val left = left + dx.toInt()
                val top = top + dy.toInt()
                val right = right + dx.toInt()
                val bottom = bottom + dy.toInt()
                layout(left, top, right, bottom)
                lastX = event.rawX
                lastY = event.rawY
            }

            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
                // 手指抬起时,根据速度进行惯性滑动
                // (int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY)
                // startX, startY:开始滑动的位置
                // velocityX, velocityY:滑动的速度
                // minX, maxX, minY, maxY:滑动的范围
                val parentView = (parent as? View)
                tracker?.computeCurrentVelocity(1000)
                scroller.fling(
                    initialLeft, initialTop,
                    -tracker?.xVelocity?.toInt()!!, -tracker?.yVelocity?.toInt()!!,
                    0, parentView?.width!! - width,
                    0, parentView?.height!! - height,
                    width, height
                )
                tracker?.recycle()
                tracker = null
                invalidate() // 请求重绘View,这会导致computeScroll()被调用
            }
        }
        return true
    }

    override fun computeScroll() {
        if (scroller.computeScrollOffset()) {
            // 更新View的位置
            val left = scroller.currX
            val top = scroller.currY
            val right = left + width
            val bottom = top + height
            layout(left, top, right, bottom)
            if (!scroller.isFinished) {
                invalidate()  // 继续请求重绘View,直到滑动结束
            }
        }
    }

    fun setOriginBitmap(bitmap: Bitmap) {
        mOriginBitmap = bitmap
        invalidate()
    }

    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        mOriginBitmap?.let {
            setScaleMatrix(it)
            canvas?.drawBitmap(it, mScaleMatrix, mPaint)
            mScaleMatrix.postTranslate(left.toFloat(), top.toFloat())
            mRectChangeListener?.onRectChanged(mScaleMatrix)
        }
    }

    fun setScaleMatrix(bitmap: Bitmap) {
        val scaleX = mDestRect?.width()!! / bitmap.width
        val scaleY = mDestRect?.height()!! / bitmap.height
        val scale = Math.min(scaleX, scaleY)
        val dx = (mDestRect?.width()!! - bitmap.width!! * scale) / 2
        val dy = (mDestRect?.height()!! - bitmap.height!! * scale) / 2
        mScaleMatrix.reset()
        mScaleMatrix.postScale(scale, scale)
        mScaleMatrix.postTranslate(dx, dy)
    }

    fun setRectChangeListener(listener: RectChangedListener) {
        mRectChangeListener = listener
    }
}
  • 裁剪框View
kotlin 复制代码
class MyCropView(context: Context, attrs: AttributeSet) : View(context, attrs), RectChangedListener {
    private val mRectLinePaint = Paint().apply {
        isAntiAlias = true
        color = Color.WHITE
        strokeWidth = dpToPx(context, 1.5f)
        style = Paint.Style.STROKE
    }
    private val mCornerAndCenterLinePaint = Paint().apply {
        isAntiAlias = true
        color = Color.RED
        strokeWidth = dpToPx(context, 3f)
        style = Paint.Style.STROKE
    }
    private val mDividerLinePaint = Paint().apply {
        isAntiAlias = true
        color = Color.WHITE
        strokeWidth = dpToPx(context, 3f) / 2f
        alpha = (0.5 * 255).toInt()
        style = Paint.Style.STROKE
    }

    private val mLineOffset = dpToPx(context, 3f) / 2f
    private val mLineWidth = dpToPx(context, 15f)
    private val mCenterLineWidth = dpToPx(context, 18f)
    private val mCoverColor = context.getColor(com.tran.edit.R.color.crop_cover_color)

    // 处理手势
    private var downX = 0f
    private var downY = 0f

    enum class MoveType {
        LEFT_TOP, RIGHT_TOP, LEFT_BOTTOM, RIGHT_BOTTOM, LEFT, TOP, RIGHT, BOTTOM
    }
    
    private var mMoveType : MoveType?= null
    private var mOriginBitmapRect = RectF()
    private var mOriginViewRect = RectF()
    private var mInitCropMatrix = Matrix()
    private var mCropMatrix = Matrix()
    private var mMinCropRect = RectF(0f, 0f, 200f , 200f)
    private var mActivePointerId = -1
    
    override fun onTouchEvent(event: MotionEvent?): Boolean {
        when (event?.actionMasked) {
            MotionEvent.ACTION_DOWN -> {
                // 记录第一根手指的位置和id
                val pointerIndex = event.actionIndex
                mActivePointerId = event.getPointerId(pointerIndex)
                downX = event.getX(pointerIndex)
                downY = event.getY(pointerIndex)

                val cropRect = getCropRect()
                // 计算初始拖动裁剪框的大致方向
                val leftTopRect = getStartCropCornerRect(cropRect.left, cropRect.top)
                if (leftTopRect.contains(event.x , event.y)) {
                    mMoveType = MoveType.LEFT_TOP
                    return true
                }

                val leftBottomRect = getStartCropCornerRect(cropRect.left, cropRect.bottom)
                if (leftBottomRect.contains(event.x , event.y)) {
                    mMoveType = MoveType.LEFT_BOTTOM
                    return true
                }

                val rightTopRect = getStartCropCornerRect(cropRect.right, cropRect.top)
                if (rightTopRect.contains(event.x , event.y)) {
                    mMoveType = MoveType.RIGHT_TOP
                    return true
                }

                val rightBottomRect = getStartCropCornerRect(cropRect.right, cropRect.bottom)
                if (rightBottomRect.contains(event.x , event.y)) {
                    mMoveType = MoveType.RIGHT_BOTTOM
                    return true
                }

                val leftCenterRect = getStartCropCenterRect(cropRect.left, cropRect.left, cropRect.top, cropRect.bottom)
                if (leftCenterRect.contains(event.x , event.y)) {
                    mMoveType = MoveType.LEFT
                    return true
                }

                val rightCenterRect = getStartCropCenterRect(cropRect.right, cropRect.right, cropRect.top, cropRect.bottom)
                if (rightCenterRect.contains(event.x , event.y)) {
                    mMoveType = MoveType.RIGHT
                    return true
                }

                val topCenterRect = getStartCropCenterRect(cropRect.left, cropRect.right, cropRect.top, cropRect.top)
                if (topCenterRect.contains(event.x , event.y)) {
                    mMoveType = MoveType.TOP
                    return true
                }

                val bottomCenterRect = getStartCropCenterRect(cropRect.left, cropRect.right, cropRect.bottom, cropRect.bottom)
                if (bottomCenterRect.contains(event.x , event.y)) {
                    mMoveType = MoveType.BOTTOM
                    return true
                }
                return true
            }
            
            MotionEvent.ACTION_POINTER_DOWN->{
                // 记录第二根手指的位置和id
                val pointerIndex = event.actionIndex
                mActivePointerId = event.getPointerId(pointerIndex)
                downX = event.getX(pointerIndex)
                downY = event.getY(pointerIndex)
            }
            
            MotionEvent.ACTION_MOVE -> {
                mMoveType ?: return false

                // 如果此时屏幕上有两根手指,这个时候mActivePointerId就是第二根手指的id,不支持多指更新位置
                val pointerIndex = event.findPointerIndex(mActivePointerId)
                if (pointerIndex < 0 || pointerIndex != 0) {
                    return false
                }

                var deltaX = event.getX(pointerIndex) - downX
                var deltaY = event.getY(pointerIndex) - downY

                downX = event.getX(pointerIndex)
                downY = event.getY(pointerIndex)
                val originalRect = getInitCropRect()
                val startCropRect = getCropRect()
                val endCropRect = RectF(startCropRect)

                when (mMoveType) {
                    MoveType.LEFT_TOP -> {
                        endCropRect.left += deltaX
                        endCropRect.top += deltaY
                    }

                    MoveType.LEFT_BOTTOM -> {
                        endCropRect.left += deltaX
                        endCropRect.bottom += deltaY
                    }

                    MoveType.RIGHT_TOP -> {
                        endCropRect.right += deltaX
                        endCropRect.top += deltaY
                    }

                    MoveType.RIGHT_BOTTOM -> {
                        endCropRect.right += deltaX
                        endCropRect.bottom += deltaY
                    }

                    MoveType.LEFT -> {
                        endCropRect.left += deltaX
                    }

                    MoveType.RIGHT -> {
                        endCropRect.right += deltaX
                    }

                    MoveType.TOP -> {
                        endCropRect.top += deltaY
                    }

                    MoveType.BOTTOM -> {
                        endCropRect.bottom += deltaY
                    }

                    else -> {
                        //
                    }

                }
                // 限制不超过初始裁剪框的大小
                endCropRect.left = max(endCropRect.left, originalRect.left)
                endCropRect.top = max(endCropRect.top, originalRect.top)
                endCropRect.right = min(endCropRect.right, originalRect.right)
                endCropRect.bottom = min(endCropRect.bottom, originalRect.bottom)
                if (endCropRect.width() < mMinCropRect.width() || endCropRect.height() < mMinCropRect.height()) {
                    // 将裁剪框的大小调整到最小范围
                    adjustCropRect(endCropRect, mMinCropRect, originalRect)
                    return true
                }
                mCropMatrix.setRectToRect(startCropRect, endCropRect, Matrix.ScaleToFit.FILL)
                invalidate()
                mOriginViewRect.set(getCropRect())
            }
            
            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
                // 所有手指抬起,重置状态
                downX = -1f
                downY = -1f
                mMoveType = null
                mActivePointerId = -1
            }

            MotionEvent.ACTION_POINTER_UP -> {
                // 假如屏幕上有两根手指
                // 按下第二根,抬起第二根,mActivePointerId == pointerId, 活动手指更新为第一根
                // 按下第二根,抬起第一根,mActivePointerId != pointerId, 活动手指为第二根,不变
                val pointerIndex = event.actionIndex
                val pointerId = event.getPointerId(pointerIndex)
                if (mActivePointerId == pointerId) {
                    // 选择一个新的活动手指
                    val newPointerIndex = if (pointerIndex == 0) 1 else 0
                    mActivePointerId = event.getPointerId(newPointerIndex)
                    downX = event.getX(newPointerIndex)
                    downY = event.getY(newPointerIndex)
                }
            }

        }
        return true
    }

    fun adjustCropRect(rect: RectF, minRect: RectF, maxRect: RectF) {
        if (rect.width() <= minRect.width()) {
            // 当前裁剪框的左右边界加上距离最小裁剪框的距离
            val xOffset = (minRect.width() - rect.width()) / 2
            rect.left -= xOffset
            rect.right += xOffset
            // 如果左边界小于最小裁剪框的左边界,那么左边界就等于最小裁剪框的左边界
            if (rect.left < maxRect.left) {
                rect.offset(maxRect.left - rect.left, 0f)
            }
            if (rect.right > maxRect.right) {
                rect.offset(maxRect.right - rect.right, 0f)
            }
        }
        if (rect.height() <= minRect.height()) {
            // 当前裁剪框的上下边界加上距离最小裁剪框的距离
            val yOffset = (minRect.height() - rect.height()) / 2
            rect.top -= yOffset
            rect.bottom += yOffset

            // 如果上边界小于最小裁剪框的上边界,那么上边界就等于最小裁剪框的上边界
            if (rect.top < maxRect.top) {
                rect.offset(0f, maxRect.top - rect.top)
            }
            if (rect.bottom > maxRect.bottom) {
                rect.offset(0f, maxRect.bottom - rect.bottom)
            }
        }
    }

    fun getStartCropCornerRect(startX : Float, startY : Float): RectF {
        return RectF(startX - mLineWidth, startY - mLineWidth, startX + mLineWidth, startY + mLineWidth)
    }

    fun getStartCropCenterRect(startX : Float, endX : Float, startY : Float, endY : Float): RectF {
        if (startX == endX){
            return RectF(startX - mLineWidth, startY, startX + mLineWidth, endY)
        }else{
            return RectF(startX, startY - mLineWidth, endX, startY + mLineWidth)
        }
    }

    override fun onRectChanged(changedMatrix: Matrix) {
        mInitCropMatrix.set(changedMatrix)
        val initCropRect = RectF(mOriginBitmapRect)
        mInitCropMatrix.mapRect(initCropRect)
        mOriginViewRect.set(initCropRect)
        invalidate()
    }

    fun getOriginViewRect(): RectF {
        return RectF(mOriginViewRect)
    }

    fun getInitCropRect(): RectF {
        val initCropRect = RectF(mOriginBitmapRect)
        mInitCropMatrix.mapRect(initCropRect)
        return initCropRect
    }

    fun getCropRect(): RectF {
        val cropRect = getOriginViewRect()
        mCropMatrix.mapRect(cropRect)
        mCropMatrix.reset()
        return cropRect
    }

    fun setOriginBitmapRect(rectF: RectF){
        mOriginBitmapRect = rectF
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        val drawRect = getCropRect()

        drawRect?.let { rect->
            // 1. 绘制遮罩
            canvas.save()
            canvas.clipOutRect(rect)
            canvas.drawColor(Color.argb(mCoverColor.alpha, mCoverColor.red, mCoverColor.green, mCoverColor.blue))
            canvas.restore()

            // 2. 绘制边框
            canvas?.drawRect(rect, mRectLinePaint)

            // 3. 绘制分割线
            val x1 = rect.left + rect.width() / 3
            val x2 = rect.left + rect.width() * 2 / 3
            val y1 = rect.top + rect.height() / 3
            val y2 = rect.top + rect.height() * 2 / 3
            canvas.drawLine(x1, rect.top, x1, rect.bottom, mDividerLinePaint)
            canvas.drawLine(x2, rect.top, x2, rect.bottom, mDividerLinePaint)
            canvas.drawLine(rect.left, y1, rect.right, y1, mDividerLinePaint)
            canvas.drawLine(rect.left, y2, rect.right, y2, mDividerLinePaint)

            // 4. 绘制四个角的折线
            canvas.drawLine(
                rect.left - mLineOffset, rect.top - mLineOffset * 2, rect.left - mLineOffset, rect.top + mLineWidth, mCornerAndCenterLinePaint
            )
            canvas.drawLine(
                rect.left - mLineOffset * 2, rect.top - mLineOffset, rect.left + mLineWidth, rect.top - mLineOffset, mCornerAndCenterLinePaint
            )
            canvas.drawLine(
                rect.right + mLineOffset, rect.top - mLineOffset * 2, rect.right + mLineOffset, rect.top + mLineWidth, mCornerAndCenterLinePaint
            )
            canvas.drawLine(
                rect.right + mLineOffset * 2, rect.top - mLineOffset, rect.right - mLineWidth, rect.top - mLineOffset, mCornerAndCenterLinePaint
            )
            canvas.drawLine(
                rect.right + mLineOffset, rect.bottom + mLineOffset * 2, rect.right + mLineOffset, rect.bottom - mLineWidth, mCornerAndCenterLinePaint
            )
            canvas.drawLine(
                rect.right + mLineOffset * 2, rect.bottom + mLineOffset, rect.right - mLineWidth, rect.bottom + mLineOffset, mCornerAndCenterLinePaint
            )
            canvas.drawLine(
                rect.left - mLineOffset, rect.bottom + mLineOffset * 2, rect.left - mLineOffset, rect.bottom - mLineWidth, mCornerAndCenterLinePaint
            )
            canvas.drawLine(
                rect.left - mLineOffset * 2, rect.bottom + mLineOffset, rect.left + mLineWidth, rect.bottom + mLineOffset, mCornerAndCenterLinePaint
            )

            // 5. 绘制四条边的中间线
            canvas.drawLine(
                rect.left - mLineOffset, rect.centerY() - mCenterLineWidth / 2, rect.left - mLineOffset, rect.centerY() + mCenterLineWidth / 2, mCornerAndCenterLinePaint
            )
            canvas.drawLine(
                rect.right + mLineOffset, rect.centerY() - mCenterLineWidth / 2, rect.right + mLineOffset, rect.centerY() + mCenterLineWidth / 2, mCornerAndCenterLinePaint
            )
            canvas.drawLine(
                rect.centerX() - mCenterLineWidth / 2, rect.top - mLineOffset, rect.centerX() + mCenterLineWidth / 2, rect.top - mLineOffset, mCornerAndCenterLinePaint
            )
            canvas.drawLine(
                rect.centerX() - mCenterLineWidth / 2, rect.bottom + mLineOffset, rect.centerX() + mCenterLineWidth / 2, rect.bottom + mLineOffset, mCornerAndCenterLinePaint
            )
        }
    }
}

效果图

相关推荐
2401_857439692 小时前
SSM 架构下 Vue 电脑测评系统:为电脑性能评估赋能
开发语言·php
SoraLuna2 小时前
「Mac畅玩鸿蒙与硬件47」UI互动应用篇24 - 虚拟音乐控制台
开发语言·macos·ui·华为·harmonyos
xlsw_2 小时前
java全栈day20--Web后端实战(Mybatis基础2)
java·开发语言·mybatis
Dream_Snowar3 小时前
速通Python 第三节
开发语言·python
拭心4 小时前
Google 提供的 Android 端上大模型组件:MediaPipe LLM 介绍
android
高山我梦口香糖4 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
信号处理学渣5 小时前
matlab画图,选择性显示legend标签
开发语言·matlab
红龙创客5 小时前
某狐畅游24校招-C++开发岗笔试(单选题)
开发语言·c++
jasmine s5 小时前
Pandas
开发语言·python
biomooc5 小时前
R 语言 | 绘图的文字格式(绘制上标、下标、斜体、文字标注等)
开发语言·r语言