Canvas->极坐标系计算图片旋转某个角度之后的坐标

极坐标系

假设我们有一个点 P ( x , y ) P(x,y) P(x,y)在二维平面上,绕原点 ( 0 , 0 ) (0,0) (0,0)旋转一个角度 θ θ θ后得到一个新的点 P ′​ ( x ​′ , y ​′ ) P′​(x​′,y​′) P′​(x​′,y​′)

   y
   |
   |
   |          P(x, y)
   |         / 
   |        /
   |       /
   |      /
   |     /
   |    /
   |   /
   |  / θ
   | /  
   |/________________ x
  O

旋转变换公式的推导

极坐标表示

开始点 P ( x , y ) P(x,y) P(x,y)的极坐标表示为 ( r , ϕ ) (r,ϕ) (r,ϕ) r r r是到原点的距离, ϕ ϕ ϕ是这个点与x轴正方向的夹角,转换为极坐标的公式为:
r = x 2 + y 2 φ = arctan ⁡ ( y x ) \begin{align*} r &= \sqrt{x^2 + y^2} \\ \varphi &= \arctan\left(\frac{y}{x}\right) \end{align*} rφ=x2+y2 =arctan(xy)
极坐标系转换到直角坐标系的公式为:
tan ⁡ ( φ ) = y x    ⟹    sin ⁡ ( φ ) cos ⁡ ( φ ) = y x x = r cos ⁡ ( φ ) , y = r sin ⁡ ( φ ) \tan(\varphi) = \frac{y}{x} \implies \frac{\sin(\varphi)}{\cos(\varphi)} = \frac{y}{x} \\ x = r \cos(\varphi), y = r \sin(\varphi) tan(φ)=xycos(φ)sin(φ)=xyx=rcos(φ),y=rsin(φ)

旋转后的极坐标

旋转后,结束点 P ′ P′ P的极坐标系为 ( r , ϕ + θ ) (r,ϕ+θ) (r,ϕ+θ),其中 r r r保持不变,角度变为 ( ϕ + θ ) (ϕ+θ) (ϕ+θ)

极坐标系转换为直角坐标系

使用旋转后的极坐标 ( r , ϕ + θ ) (r,ϕ+θ) (r,ϕ+θ) 转换回直角坐标
x ′ = r cos ⁡ ( φ + θ ) y ′ = r sin ⁡ ( φ + θ ) \begin{align*} x' &= r\cos(\varphi + \theta) \\ y' &= r\sin(\varphi + \theta) \end{align*} xy=rcos(φ+θ)=rsin(φ+θ)

三角函数的和差公式

cos ⁡ ( φ + θ ) = cos ⁡ ( φ ) cos ⁡ ( θ ) − sin ⁡ ( φ ) sin ⁡ ( θ ) sin ⁡ ( φ + θ ) = sin ⁡ ( φ ) cos ⁡ ( θ ) + cos ⁡ ( φ ) sin ⁡ ( θ ) \begin{align*} \cos(\varphi + \theta) &= \cos(\varphi) \cos(\theta) - \sin(\varphi) \sin(\theta) \\ \sin(\varphi + \theta) &= \sin(\varphi) \cos(\theta) + \cos(\varphi) \sin(\theta) \end{align*} cos(φ+θ)sin(φ+θ)=cos(φ)cos(θ)sin(φ)sin(θ)=sin(φ)cos(θ)+cos(φ)sin(θ)

代入 φ \varphi φ θ \theta θ

x ′ = r cos ⁡ ( φ + θ ) = r ( cos ⁡ ( φ ) cos ⁡ ( θ ) − sin ⁡ ( φ ) sin ⁡ ( θ ) ) = r cos ⁡ ( φ ) cos ⁡ ( θ ) − r sin ⁡ ( φ ) sin ⁡ ( θ ) = x cos ⁡ ( θ ) − y sin ⁡ ( θ ) \begin{align*} x' &= r \cos(\varphi + \theta) \\ &= r (\cos(\varphi) \cos(\theta) - \sin(\varphi) \sin(\theta)) \\ &= r \cos(\varphi) \cos(\theta) - r \sin(\varphi) \sin(\theta) \\ &= x \cos(\theta) - y \sin(\theta) \end{align*} x=rcos(φ+θ)=r(cos(φ)cos(θ)sin(φ)sin(θ))=rcos(φ)cos(θ)rsin(φ)sin(θ)=xcos(θ)ysin(θ)
y ′ = r sin ⁡ ( φ + θ ) = r ( sin ⁡ ( φ ) cos ⁡ ( θ ) + cos ⁡ ( φ ) sin ⁡ ( θ ) ) = r sin ⁡ ( φ ) cos ⁡ ( θ ) + r cos ⁡ ( φ ) sin ⁡ ( θ ) = x sin ⁡ ( θ ) + y cos ⁡ ( θ ) \begin{align*} y' &= r \sin(\varphi + \theta) \\ &= r (\sin(\varphi) \cos(\theta) + \cos(\varphi) \sin(\theta)) \\ &= r \sin(\varphi) \cos(\theta) + r \cos(\varphi) \sin(\theta) \\ &= x \sin(\theta) + y \cos(\theta) \end{align*} y=rsin(φ+θ)=r(sin(φ)cos(θ)+cos(φ)sin(θ))=rsin(φ)cos(θ)+rcos(φ)sin(θ)=xsin(θ)+ycos(θ)
x ′ = x cos ⁡ ( θ ) − y sin ⁡ ( θ ) y ′ = x sin ⁡ ( θ ) + y cos ⁡ ( θ ) \begin{align*} x' &= x \cos(\theta) - y \sin(\theta) \\ y' &= x \sin(\theta) + y \cos(\theta) \end{align*} xy=xcos(θ)ysin(θ)=xsin(θ)+ycos(θ)

XML文件

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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">
    <com.yang.app.MyView
        android:id="@+id/rotate_view"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:gravity="center"
        android:textColor="@color/black"
        android:textSize="30sp"
        android:background="@android:color/holo_green_light"/>
    <com.yang.app.MyDegreeSeekBar
        android:id="@+id/degree_bar"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:requiresFadingEdge="horizontal"
        android:fadingEdgeLength="22dp"
        android:paddingLeft="20dp"
        android:paddingRight="20dp"
        android:paddingBottom="100dp"
        android:layout_marginLeft="4dp"
        android:layout_marginRight="4dp"/>
</LinearLayout>

自定义View代码

  • 旋转进度条View
class MyDegreeSeekBar(context: Context, attrs: AttributeSet) : View(context, attrs) {

    var mMinDegree = -45
    var mMaxDegree = 45
    val mIntegerCount = (mMaxDegree - mMinDegree) / 10
    val mNonIntegerCount = 9

    private var mIntegerLineAlpha = (0.7f * 255).toInt()
    private var mNonIntegerLineAlpha = (0.3f * 255).toInt()
    private val mIntegerLineHeight = dpToPx(context,10f)
    private val mNonIntegerLineHeight = dpToPx(context,8f)

    private val mCenterLineHeight = dpToPx(context, 15f)
    private val mCenterLineWidth = dpToPx(context, 2f)
    private val mOffsetWidth = dpToPx(context,3f) / 2
    private var mDegreeDistance = dpToPx(context, 6f)

    private var downX = 0f
    private var downY = 0f
    private var downScrollX = 0

    private var mStartFling = false

    private val mScroller = Scroller(context)
    private var mTracker: VelocityTracker? = null
    private val mLineDrawable = ResourcesCompat.getDrawable(context.resources, com.tran.edit.R.drawable.shape_degree_line, null)
    private var mDegreeListener: OnDegreeListener? = null

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        when (event?.action) {
            MotionEvent.ACTION_DOWN -> {
                // 按下之后, 停止正常滑动和惯性滑动
                mScroller.forceFinished(true)
                mStartFling = false

                mTracker = VelocityTracker.obtain().apply {
                    addMovement(event)
                }
                downX = event.x
                downY = event.y

                downScrollX = scrollX
            }
            MotionEvent.ACTION_MOVE -> {
                if (mTracker == null) {
                    mTracker = VelocityTracker.obtain()
                }
                mTracker?.addMovement(event)
                scrollX = (downScrollX + downX - event.x).toInt()
                // 限制滑动距离最小距离和最大距离之间
                val maxScrollDistance = degreeToScrollDistance(mMaxDegree)
                val minScrollDistance = degreeToScrollDistance(mMinDegree)
                if (scrollX < Math.min(maxScrollDistance, minScrollDistance) || scrollX > Math.max(maxScrollDistance, minScrollDistance)) {
                    setScrollThreshold()
                    mScroller.forceFinished(true)
                }
                mDegreeListener?.notifyDegreeChanged(distanceToDegree())
                invalidate()
            }
            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL-> {
                mTracker?.addMovement(event)
                mTracker?.computeCurrentVelocity(1000)
                // 开始惯性滑动
                mStartFling = true
                mScroller.fling(
                    scrollX, scrollY,
                    -mTracker!!.xVelocity.toInt(), 0,
                    Int.MIN_VALUE, Int.MAX_VALUE,
                    Int.MIN_VALUE, Int.MAX_VALUE
                )
                mTracker?.recycle()
                mTracker = null
                invalidate()
            }
        }
        return true
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)

        canvas.save()
        canvas.clipRect(
            (scrollX + paddingLeft).toFloat(),
            0f,
            (scrollX + measuredWidth - paddingRight).toFloat(),
            (measuredHeight - paddingBottom).toFloat())

        val rectBottom = measuredHeight - paddingBottom
        var offsetX = getDrawOffsetX()

        // 绘制最左边的非10倍数刻度
        for (count in 0 until (mNonIntegerCount + 1) / 2){
            drawNonIntegerLine(canvas, offsetX, rectBottom)
            // 增加一个刻度的距离
            offsetX += getLineDistance()
        }

        // 绘制中间的10倍数刻度和非10倍数刻度
        for (count1 in 0 until mIntegerCount){
            drawIntegerLine(canvas, offsetX, rectBottom)
            // 增加一个刻度的距离
            offsetX += getLineDistance()
            // 最后一个正整数刻度之后不再绘制
            if (count1 == mIntegerCount - 1) break

            for (count2 in 0 until mNonIntegerCount){
                drawNonIntegerLine(canvas, offsetX, rectBottom)
                // 增加一个刻度的距离
                offsetX += getLineDistance()
            }
        }

        // 绘制最右边的非10倍数刻度
        for (count in 0 until (mNonIntegerCount + 1) / 2){
            drawNonIntegerLine(canvas, offsetX, rectBottom)
            // 增加一个刻度的距离
            offsetX += getLineDistance()
        }

        drawCenterLine(canvas, rectBottom)

        canvas.restore()
    }

    override fun computeScroll() {
        super.computeScroll()
        if (mScroller.isFinished && mStartFling) {
            mStartFling = false
            return
        }
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.currX, mScroller.currY)
            val maxScrollDistance = degreeToScrollDistance(mMaxDegree)
            val minScrollDistance = degreeToScrollDistance(mMinDegree)
            if (scrollX < Math.min(maxScrollDistance, minScrollDistance) || scrollX > Math.max(maxScrollDistance, minScrollDistance)) {
                setScrollThreshold()
                mScroller.forceFinished(true)
            }
            mDegreeListener?.notifyDegreeChanged(distanceToDegree())
        }
        invalidate()
    }

    fun setDegreeListener(listener: OnDegreeListener) {
        mDegreeListener = listener
    }
    fun drawCenterLine(canvas: Canvas, rectBottom: Int) {
        mLineDrawable?.setBounds(
            measuredWidth / 2 - mCenterLineWidth / 2 + scrollX,
            rectBottom - mCenterLineHeight,
            measuredWidth / 2 + mCenterLineWidth  / 2 + scrollX,
            rectBottom
        )
        mLineDrawable?.alpha = 255
        mLineDrawable?.draw(canvas)
    }

    fun setScrollThreshold(){
        scrollX = Math.min(degreeToScrollDistance(mMaxDegree), Math.max(scrollX, degreeToScrollDistance(mMinDegree)))
    }

    fun degreeToScrollDistance(degree : Int) : Int {
        return Math.round(degree * getLineDistance())
    }

    fun distanceToDegree() : Int {
        val degree = (Math.abs(scrollX) / getLineDistance() + 0.5f).toInt()
        var sign = Math.signum(scrollX.toFloat())
        if (scrollX == 0) sign = 0f
        return (degree * sign).toInt()
    }

    fun drawIntegerLine(canvas: Canvas, offsetX: Float, rectBottom: Int){
        mLineDrawable?.setBounds(
            (offsetX - mOffsetWidth / 2).toInt(), rectBottom - mIntegerLineHeight,
            (offsetX + mOffsetWidth / 2).toInt(), rectBottom)
        mLineDrawable?.alpha = mIntegerLineAlpha
        mLineDrawable?.draw(canvas)
    }

    fun drawNonIntegerLine(canvas: Canvas, offsetX: Float, rectBottom: Int) {
        mLineDrawable?.setBounds(
            (offsetX - mOffsetWidth / 2).toInt(), rectBottom - mNonIntegerLineHeight,
            (offsetX + mOffsetWidth / 2).toInt(), rectBottom)
        mLineDrawable?.alpha = mNonIntegerLineAlpha
        mLineDrawable?.draw(canvas)
    }

    fun getTotalDegreeCount(): Int {
        return mMaxDegree - mMinDegree
    }

    fun getDrawOffsetX(): Float {
        return measuredWidth / 2 - (getTotalDegreeCount() / 2 * getLineDistance())
    }

    fun getLineDistance(): Float {
        return mDegreeDistance.toFloat()
    }

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

    interface OnDegreeListener {
        fun notifyDegreeChanged(degree: Int)
    }
}
  • 预览显示图片的View
class MyView(context: Context, attrs: AttributeSet) : View(context, attrs) {
    private val mPaint = Paint().apply {
        isAntiAlias = true
        isFilterBitmap = true
    }
    private val mRectPaint = Paint().apply {
        isAntiAlias = true
        strokeWidth = 20f
        style = Paint.Style.STROKE
        color = Color.RED
    }
    private var mBitmap : Bitmap ?= null
    private var mDrawBitmapRect = RectF()
    private var mTransformerMatrix = Matrix()
    private var mTempMatrix = Matrix()
    private var mPreviewRect = RectF()
    private var mBitmapRect = RectF()

    fun setBitmap(bitmap: Bitmap){
        mBitmap = bitmap
        mBitmapRect = RectF(0f, 0f, bitmap.width.toFloat(),bitmap.height.toFloat())
        invalidate()
    }

    fun getTransformerMatrix() : Matrix {
        return mTransformerMatrix
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        mPreviewRect.set(0f, 0f, measuredWidth.toFloat(),measuredHeight.toFloat())
    }
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        drawBackground(canvas)
        mBitmap?.let {
            canvas.concat(mTransformerMatrix)
            canvas.drawBitmap(it, null, mDrawBitmapRect, mPaint)
        }
    }
    fun drawBackground(canvas : Canvas){
        val matrix = Matrix()
        matrix.setRectToRect(mBitmapRect, mPreviewRect, Matrix.ScaleToFit.CENTER)
        matrix.mapRect(mBitmapRect)
        mDrawBitmapRect.set(mBitmapRect)
        canvas.drawRect(mBitmapRect, mRectPaint)
    }
    fun getDrawBitmapRect() : RectF {
        return mDrawBitmapRect
    }
}

Activity代码

class MainActivity : AppCompatActivity() {
    private var mDegreeSeekBar: MyDegreeSeekBar? = null
    private var mRotationView : MyView?= null
    private var mDegree : Int = 0
    var tempBitmap: Bitmap? = null
    var screenWidth = 0
    var screenHeight = 0
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        mDegreeSeekBar = findViewById(R.id.degree_bar)
        mRotationView = findViewById(R.id.rotate_view)

        // 屏幕宽高的一半作为临时RectF, 用于压缩Bitmap
        screenWidth = resources.displayMetrics.widthPixels
        screenHeight = resources.displayMetrics.heightPixels
        val tempRect = RectF(0f, 0f, screenWidth.toFloat() / 2, screenHeight.toFloat() / 2)

        CoroutineScope(Dispatchers.IO).launch {
            tempBitmap = getBitmap(resources, tempRect, R.drawable.real)
            withContext(Dispatchers.Main) {
                mRotationView?.post {
                    tempBitmap?.let {
                        mRotationView?.setBitmap(it)
                    }

                }

            }
        }

        mDegreeSeekBar?.setDegreeListener(object : MyDegreeSeekBar.OnDegreeListener {
            override fun notifyDegreeChanged(degree: Int) {
                if (mDegree == degree) return
                mRotationView?.let {view->
                    degree?.toFloat()?.let {
                        val radians = Math.toRadians(it.toDouble())

                        val cosTheta = Math.abs(Math.cos(radians))
                        val sinTheta = Math.abs(Math.sin(radians))

                        // 原始矩形的顶点坐标
                        val halfWidth = view.getDrawBitmapRect().width() / 2
                        val halfHeight = view.getDrawBitmapRect().height() / 2
                        var centerX = view.getDrawBitmapRect().centerX()
                        var centerY = view.getDrawBitmapRect().centerY()
                        // 原始顶点坐标相对于矩形的中心点
                        val originVectors = arrayOf(
                            floatArrayOf(-halfWidth, -halfHeight),
                            floatArrayOf(halfWidth, -halfHeight),
                            floatArrayOf(halfWidth, halfHeight),
                            floatArrayOf(-halfWidth, halfHeight)
                        )

                        // 旋转后的顶点坐标相对于矩形的中心点
                        val rotatedVectors = originVectors.map { originVector ->
                            floatArrayOf(
                                (originVector[0] * cosTheta - originVector[1] * sinTheta).toFloat(),
                                (originVector[0] * sinTheta + originVector[1] * cosTheta).toFloat()
                            )
                        }
                        // 计算旋转后的边界
                        val minX = rotatedVectors.minOf { it[0] }
                        val maxX = rotatedVectors.maxOf { it[0] }
                        val minY = rotatedVectors.minOf { it[1] }
                        val maxY = rotatedVectors.maxOf { it[1] }

                        // 生成targetRect
                        val targetRect = RectF(minX, minY, maxX, maxY)

                        // 计算缩放值,确保旋转后的矩形的四个顶点贴住原始RectF

                        val scale = min(view.getDrawBitmapRect().width() /  targetRect.width(), view.getDrawBitmapRect().height() / targetRect.height())

                        view.getTransformerMatrix().setScale(scale, scale, centerX, centerY)
                        view.getTransformerMatrix().postRotate(it, centerX, centerY)
                        view.invalidate()
                        mDegree = degree
                    }
                }
            }
        })
    }
}

效果图

在这里插入图片描述

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值