转载请注明出处:https://www.jianshu.com/p/c954e2aea2f3
本文出自 容华谢后的博客
0.写在前面
今天写一篇关于自定义控件的文章,基于ImageView控件,给它加上移动、多点缩放、两指旋转的功能,先看下效果:
效果图布局中可以添加多个MatrixImage,位置可以自由移动,涉及到一些简单的三角函数知识,说下实现的思路:
-
基于ImageView,因为要实现缩放、移动、旋转功能,将ImageView的scaleType设置为MATRIX模式
-
获取图片的显示区域,得到上、下、左、右位置信息
-
根据图片的显示区域,绘制四个边框,边框随着图片的区域变化而变化
-
绘制每个角的控制点,根据控制点的位置,实现缩放功能
-
重写onTouchEvent方法,实现图片的移动和旋转功能
一起来看下实现的代码逻辑,代码比较多,完整的项目代码在文末贴上。
1.准备
先初始化一些参数:
class MatrixImageView : AppCompatImageView {
// 控件宽度
private var mWidth = 0
// 控件高度
private var mHeight = 0
// 第一次绘制
private var mFirstDraw = true
// 是否显示控制框
private var mShowFrame = false
// 当前Image矩阵
private var mImgMatrix = Matrix()
// 画笔
private lateinit var mPaint: Paint
// 触摸模式
private var touchMode: MatrixImageUtils.TouchMode? = null
// 第二根手指是否按下
private var mIsPointerDown = false
// 按下点x坐标
private var mDownX = 0f
// 按下点y坐标
private var mDownY = 0f
// 上一次的触摸点x坐标
private var mLastX = 0f
// 上一次的触摸点y坐标
private var mLastY = 0f
// 旋转角度
private var mDegree: Float = 0.0f
// 旋转图标
private lateinit var mRotateIcon: Bitmap
// 图片控制框颜色
private var mFrameColor = Color.parseColor("#1677FF")
// 连接线宽度
private var mLineWidth = dp2px(context, 2f)
// 缩放控制点半径
var mScaleDotRadius = dp2px(context, 5f)
// 旋转控制点半径
var mRotateDotRadius = dp2px(context, 12f)
// 按下监听
private var mDownClickListener: ((view: View, pointF: PointF) -> Unit)? = null
// 长按监听
private var mLongClickListener: ((view: View, pointF: PointF) -> Unit)? = null
// 移动监听
private var mMoveListener: ((view: View, pointF: PointF) -> Unit)? = null
// 长按监听计时任务
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()
}
...
}
增加一些属性设置,可以在布局文件中对控件进行调整,初始化画笔和旋转控制点图标,控件的宽高在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 -> { // 旋转控制点半径
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
// 旋转图标
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.绘制
先获取图片的坐标信息,默认显示在控件中心,然后绘制边框和控制点:
override fun draw(canvas: Canvas?) {
super.draw(canvas)
if (canvas == null || drawable == null) {
return
}
val imgRect = getImageRectF(this)
// 左上角x坐标
val left = imgRect.left
// 左上角y坐标
val top = imgRect.top
// 右下角x坐标
val right = imgRect.right
// 右下角y坐标
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)
// 上中间控制点,旋转
val rotateLine = mRotateDotRadius / 3
canvas.drawLine(middleX, top - rotateLine, middleX, top, mPaint)
canvas.drawCircle(middleX, top - rotateLine - mRotateDotRadius, mRotateDotRadius, mPaint)
// 上中间控制点,旋转图标
canvas.drawBitmap(
mRotateIcon,
middleX - mRotateIcon.width / 2,
top - rotateLine - mRotateDotRadius - mRotateIcon.width / 2,
mPaint
)
}
绘制完成是这样的效果:
边框和控制点3.Touch事件处理
要处理移动、缩放、单个旋转控制点旋转,两指旋转这四种Touch事件,因为重写了onTouchEvent方法,还要再加上点击事件和长按事件的处理。
其中ACTION_POINTER_DOWN接收的是两指旋转中,第二根手指的坐标信息,单个旋转控制点旋转和两指旋转逻辑差不多,是以图片中心为第一根手指的位置,旋转控制点
为第二根手指的位置,关于旋转角度的计算,一起往下看。
override fun onTouchEvent(event: MotionEvent?): Boolean {
if (event == null || drawable == null) {
return super.onTouchEvent(event)
}
// x坐标
val x = event.x
// y坐标
val y = event.y
// 图片显示区域
val imageRect = getImageRectF(this)
// 图片中心点x坐标
val centerX = (imageRect.right - imageRect.left) / 2 + imageRect.left
// 图片中心点y坐标
val centerY = (imageRect.bottom - imageRect.top) / 2 + imageRect.top
when (event.action.and(ACTION_MASK)) {
ACTION_DOWN -> {
// 按下监听
mDownClickListener?.invoke(this, PointF(x, y))
// 判断是否在图片实际显示区域内
touchMode = getTouchMode(this, x, y)
if (touchMode == TOUCH_OUTSIDE) {
mShowFrame = false
invalidate()
return super.onTouchEvent(event)
}
mDownX = x
mDownY = y
mLastX = x
mLastY = y
// 旋转控制点,点击后以图片中心为基准,计算当前旋转角度
if (touchMode == TOUCH_ROTATE) {
// 旋转角度
mDegree = callRotation(centerX, centerY, x, y)
}
mShowFrame = true
invalidate()
// 长按监听计时
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 -> {
// 旋转事件
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方法主要处理图片的移动、旋转、缩放功能,在上述onTouchEvent方法中的ACTION_MOVE中被触发:
/**
* 手指移动
*
* @param x x坐标
* @param y y坐标
* @param imageRect 图片显示区域
*/
private fun touchMove(x: Float, y: Float, imageRect: RectF) {
// 左上角x坐标
val left = imageRect.left
// 左上角y坐标
val top = imageRect.top
// 右下角x坐标
val right = imageRect.right
// 右下角y坐标
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)
// 当前缩放距离
val scaleTrans = getDistanceOf2Points(mLastX, mLastY, x, y)
// 缩放系数,x轴方向
val scaleFactorX: Float
// 缩放系数,y轴方向
val scaleFactorY: Float
// 缩放基准点x坐标
val scaleBaseX: Float
// 缩放基准点y坐标
val scaleBaseY: Float
when (touchMode) {
TOUCH_IMAGE -> {
mImgMatrix.postTranslate(x - mLastX, y - mLastY)
imageMatrix = mImgMatrix
return
}
TOUCH_ROTATE -> {
// 图片中心点x坐标
val centerX = (imageRect.right - imageRect.left) / 2 + imageRect.left
// 图片中心点y坐标
val centerY = (imageRect.bottom - imageRect.top) / 2 + imageRect.top
// 旋转角度
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 计算旋转的角度
deltaX是图片中心点和旋转点的水平方向距离,deltaY是垂直方向距离,atan2是反正切,计算的是旋转控制点与中心点的连接线,与X轴的夹角弧度,然后通过toDegrees方法转换为夹角角度。
向右顺时针旋转,角度越来越大,角度递增图片向右旋转,向左则相反。
/**
* 计算旋转的角度
*
* @param baseX 基准点x坐标
* @param baseY 基准点y坐标
* @param rotateX 旋转点x坐标
* @param rotateY 旋转点y坐标
* @return 旋转的角度
*/
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()
}
看图说话:
计算旋转的角度了解下弧度与角度的计算公式:
-
完整圆的弧度为2π,角度为360度,所以180度等于π弧度
-
弧度 = 角度 / 180 * π
-
角度 = 弧度 / π * 180
4.3 计算两点之间的距离
这个比较简单了,三角形已知两条直角边的值求斜边,勾股定理:a² + b² = c²
/**
* 获取两个点之间的距离
*
* @param x1 第一个点x坐标
* @param y1 第一个点y坐标
* @param x2 第二个点x坐标
* @param y2 第二个点y坐标
* @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,谢谢!
网友评论