Android自定义View—直尺的旋转,缩放,平移以及拉伸

一、前提

上一篇文章中,我们实现了直尺的绘制Android自定义View---直尺 - 掘金 (juejin.cn)

这篇文章我们要去做直尺的变换,包括旋转,缩放,平移以及拉伸(刻度的伸长和缩短)

做的过程中踩过的坑挺多的......

这里我们采用的是自定义一个TransfromLayout布局,在这布局里面去包含RulerView

为什么这里不直接去改变RulerView呢,还要去嵌套一层layout呢?因为变换的时候,除了RulerView要变换外,旋转按钮以及拉伸按钮的位置也要跟着变换,用一个layout包裹着他们,layout变换的时候它的子控件也会一起跟着变换

重点:

视图平移的时候我们可能会想到设置ViewGroup的LayoutParams的leftMargin和topMargin,然后更新视图的LayoutParams,或者ViewGroup的layout()方法

视图的缩放可以采用改变LayoutParams的width和height

Layout也是View的一种,视图的旋转采用view.setRotation()

这时候问题就来了,这么多种方式,我该采用哪一种呢,混着来么,会有什么后果吗?

答案是有的,并且会为后续的开发留下坑,就比如我一开始平移使用的是改变LayoutParams的leftMargin和topMargin,旋转使用的是view.setRotation(),很好,功能也完成了,可是后面出了个新需求,要求落点在直尺上边的50px范围内,手指滑动绘制的时候是一条直线,如下图

此时,如果直尺会经过一系列的变换,例如平移,旋转,缩放,落点跟直尺的距离判断将变得非常的麻烦。

所以我后面对这些变换统一采用的是view的属性动画

  • 平移 view.setTranslationX和view.setTranslationY
  • 缩放 view.setScaleX和view.setScaleY
  • 旋转 view.setRotation

采用属性动画后,我们可以打开开发者选项的显示边界布局

移动下直尺 可以看到,图像的布局的边界位置还是在原来的地方,可是图像的渲染的位置改变了

我们点进去view.setTranslationX的源码看一下

可以看到,这里并没有对布局的layoutParams做处理,只是对图形的渲染的节点(mRenderNode)做了处理,所以图形的实际位置没有改变,只不过是渲染的位置做了变化,里面有个matrix记录着图形的各种变化,当我们手指按下的时候,我们可以通过视图matrix的逆矩阵,去映射落点,然后判断落点去直尺的距离,这样就会简单很多

并且官方也是这么操作的(手指落下,判断点是否在图像内部) 好了,废话讲了这么多,该进入正题了

二、平移

平移这里我采用的是GestureDetector手势

kotlin 复制代码
class TransformLayout @JvmOverloads constructor(
    private val mContext: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : RelativeLayout(mContext, attrs, defStyleAttr) {

    private var lastX: Float = 0f
    private var lastY: Float = 0f
    private var dX: Float = 0f
    private var dY: Float = 0f


    //手势监听
    private val gestureDetector = GestureDetector(mContext, GestureListener())

    override fun onTouchEvent(event: MotionEvent): Boolean {
        return gestureDetector.onTouchEvent(event)
    }

    inner class GestureListener: GestureDetector.SimpleOnGestureListener() {
        override fun onDown(e: MotionEvent): Boolean {
            lastX = e.rawX
            lastY = e.rawY
            return true
        }
        override fun onScroll(
            e1: MotionEvent,
            e2: MotionEvent,
            distanceX: Float,
            distanceY: Float
        ): Boolean {
            //获取当前实时点信息
            val rawX = e2.rawX
            val rawY = e2.rawY

            //变化量
            dX = rawX - lastX
            dY = rawY - lastY


            this@TransformLayout.translationX +=dX
            this@TransformLayout.translationY +=dY


            //更新最后屏幕点信息
            lastX = rawX
            lastY = rawY

            return true
        }
    }
}

主要就是获得平移的变化量,然后设置给layout的translationX和translationY

三、缩放

缩放是两指捏合进行缩放,这里采用的是ScaleGestureDetector手势的SimpleOnScaleGestureListener()

kotlin 复制代码
class TransformLayout @JvmOverloads constructor(
    private val mContext: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : RelativeLayout(mContext, attrs, defStyleAttr) {

    ......
    
    private var scaleFactor = 1.0f // 初始缩放比例为 1.0


    //缩放监听
    private val scaleGestureDetector = ScaleGestureDetector(mContext, ScaleListener())

    //手势监听
    private val gestureDetector = GestureDetector(mContext, GestureListener())

    override fun onTouchEvent(event: MotionEvent): Boolean {
        return scaleGestureDetector.onTouchEvent(event) or gestureDetector.onTouchEvent(event)
    }

    inner class GestureListener: GestureDetector.SimpleOnGestureListener() {
        ......
    }

    inner class ScaleListener: ScaleGestureDetector.SimpleOnScaleGestureListener() {
        /**
         * 缩放进行中,返回值表示是否下次缩放需要重置,如果返回ture,那么detector就会重置缩放事件,如果返回false,detector会在之前的缩放上继续进行计算
         */
        override fun onScale(detector: ScaleGestureDetector): Boolean {
            scaleFactor *= detector.scaleFactor
            scaleFactor = java.lang.Float.max(0.5f, min(scaleFactor, 2f)) // 设置最大和最小缩放比例
            this@TransformLayout.scaleY = scaleFactor
            this@TransformLayout.scaleX = scaleFactor
            return false
        }

        /**
         * 缩放开始,返回值表示是否受理后续的缩放事件
         */
        override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
            return true
        }

        override fun onScaleEnd(detector: ScaleGestureDetector) {}

    } 
}

四、旋转

旋转我这里做的是单指按住旋转的按钮进行旋转操作

kotlin 复制代码
class TransformLayout @JvmOverloads constructor(
    private val mContext: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : RelativeLayout(mContext, attrs, defStyleAttr) {

   ......
    private var layoutDegree = 0f
    private var oriX = 0f
    private var oriY = 0f

    //缩放监听
    private val scaleGestureDetector = ScaleGestureDetector(mContext, ScaleListener())

    //手势监听
    private val gestureDetector = GestureDetector(mContext, GestureListener())

    override fun onTouchEvent(event: MotionEvent): Boolean {
        return scaleGestureDetector.onTouchEvent(event) or gestureDetector.onTouchEvent(event)
    }

    inner class GestureListener: GestureDetector.SimpleOnGestureListener() {
        ......
    }

    inner class ScaleListener: ScaleGestureDetector.SimpleOnScaleGestureListener() {
        ......

    }

    fun rotateLayout(event: MotionEvent) {
        when (event.actionMasked) {
            MotionEvent.ACTION_DOWN -> {
                oriX = event.x
                oriY = event.y
                layoutDegree = rotation
            }
            MotionEvent.ACTION_MOVE -> {
                val tempRawX = event.x
                val tempRawY = event.y
                val first = Point(oriX.toInt(), oriY.toInt())
                val second = Point(tempRawX.toInt(), tempRawY.toInt())
                val cen = Point(width / 2, height / 2)
                //旋转
                val angle = angle(cen, first, second)
                layoutDegree += angle
                rotation = layoutDegree
            }
            MotionEvent.ACTION_UP -> {}
            else -> {}
        }
    }

    private fun angle(cen: Point, first: Point, second: Point): Float {
        val dx1: Float = (first.x - cen.x).toFloat()
        val dy1: Float = (first.y - cen.y).toFloat()
        val dx2: Float = (second.x - cen.x).toFloat()
        val dy2: Float = (second.y - cen.y).toFloat()

        // 计算三边的平方
        val ab2 =
            ((second.x - first.x) * (second.x - first.x) + (second.y - first.y) * (second.y - first.y)).toFloat()
        val oa2 = dx1 * dx1 + dy1 * dy1
        val ob2 = dx2 * dx2 + dy2 * dy2

        // 根据两向量的叉乘来判断顺逆时针
        val isClockwise =
            (first.x - cen.x) * (second.y - cen.y) - (first.y - cen.y) * (second.x - cen.x) > 0
        // 根据余弦定理计算旋转角的余弦值
        var cosDegree =
            (oa2 + ob2 - ab2) / (2 * Math.sqrt(oa2.toDouble()) * Math.sqrt(ob2.toDouble()))

        // 异常处理,因为算出来会有误差绝对值可能会超过一,所以需要处理一下
        if (cosDegree > 1) {
            cosDegree = 1.0
        } else if (cosDegree < -1) {
            cosDegree = -1.0
        }

        // 计算弧度
        val radian = acos(cosDegree)

        // 计算旋转过的角度,顺时针为正,逆时针为负
        return (if (isClockwise) Math.toDegrees(radian) else -Math.toDegrees(radian)).toFloat()
    }

}
ini 复制代码
//这里的totateView指的是上面箭头所指的View
//这里需要将触摸位置进行偏移,因为是通过触摸rotateView去旋转transformLayout,并不是旋转rotateView自身
rotateView.setOnTouchListener { v, event ->
    val offsetX: Float = (rotateView.left-transformLayout.scrollX).toFloat()
    val offsetY: Float = (rotateView.top-transformLayout.scrollY).toFloat()
    event.offsetLocation(offsetX, offsetY)

    transformLayout.pivotX = transformLayout.left+transformLayout.width/2f
    transformLayout.pivotY = transformLayout.top+transformLayout.height/2f
    transformLayout.rotateLayout(event)
    true
}

五、拉伸

拉伸这里就没办法用属性动画了,因为我们确实要改变图形的宽度

csharp 复制代码
//addLengthBtn是上面箭头所指的+
addLengthBtn.setOnTouchListener { v, event ->
    when (event.actionMasked) {
        MotionEvent.ACTION_DOWN -> {
            lastX = event.rawX.toInt()
        }
        MotionEvent.ACTION_MOVE -> {
            //获取当前实时点信息
            val rawX = event.rawX.toInt()
            //变化量
            dX = rawX - lastX
            val params = transformLayout.layoutParams as LayoutParams
            //这里指定下宽高,否则如果布局文件指定了match_parent/wrap_content,会导致自动伸缩宽高
            params.width = transformLayout.measuredWidth + dX
            params.height = transformLayout.measuredHeight
            transformLayout.layoutParams = params
        }
        MotionEvent.ACTION_UP -> {}
        else -> {}
    }
    return@setOnTouchListener true
}

六、注意的点

视图的默认摆放位置不要在xml里面去指定,不然后面对于落点坐标的位置进行逆矩阵映射会有影响

之前就是在xml里面去默认让视图居中,结果后面判断落点位置的时候一直不准确,有谁知道为什么会这样的吗?排查了一天才排查出来,要将这个android:layout_centerInParent="true"去掉,在代码里面setTranslateX和setTranslateY去改变初始位置

相关推荐
断墨先生3 分钟前
uniapp—android原生插件开发(3Android真机调试)
android·uni-app
无极程序员2 小时前
PHP常量
android·ide·android studio
萌面小侠Plus3 小时前
Android笔记(三十三):封装设备性能级别判断工具——低端机还是高端机
android·性能优化·kotlin·工具类·低端机
慢慢成长的码农3 小时前
Android Profiler 内存分析
android
大风起兮云飞扬丶3 小时前
Android——多线程、线程通信、handler机制
android
L72563 小时前
Android的Handler
android
清风徐来辽3 小时前
Android HandlerThread 基础
android
HerayChen4 小时前
HbuildderX运行到手机或模拟器的Android App基座识别不到设备 mac
android·macos·智能手机
顾北川_野4 小时前
Android 手机设备的OEM-unlock解锁 和 adb push文件
android·java
hairenjing11234 小时前
在 Android 手机上从SD 卡恢复数据的 6 个有效应用程序
android·人工智能·windows·macos·智能手机