极坐标系
假设我们有一个点 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(φ)=xy⟹cos(φ)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*}
x′y′=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*}
x′y′=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
}
}
}
})
}
}