之前做了这个东西,一个内部素材加外操作边框,包含基本的移动、缩放、旋转,拉伸,快速定位,十字对齐等操作。常见使用场景如添加马赛克,添加画中画等。感觉比较有意思而且中间也遇到了一些问题就记录一下
先上图:
如图,这次就先讲一下平移、旋转、缩放
如果只是view做平移,有很多种实现方式比如通过layout、动画等。
基于我们的使用场景:知道一个图片的位置信息和旋转角度(中心旋转),就可以将它画出来。所以选择了维护一个rect以及绕中心旋转的角度rotation,这样一些第三方如需要c层操作的地方,直接把这些信息给c层也可以马上明确。
一、如何画
可以看出这个控件其实是一个图片+外面一个框,不过要考虑到旋转的情况,这里我们就借助matrix来做中心旋转。其实相对的就是先把canvas绕图片的中心做一次旋转
@Override protected void dispatchDraw(Canvas canvas) {
canvas.save();
canvas.concat(mRotateMatrix);
mainDrawable.setBounds((int) mRect.left, (int) mRect.top, (int) mRect.right,
(int) mRect.bottom);
mainDrawable.draw(canvas);
canvas.drawRect(mRect, mPaint);
drawAnchors(canvas, mRect);
canvas.restore();
super.dispatchDraw(canvas);
}
虚线就用paint设置setPathEffect就可以了,虚线框角上的几个角标也是借助rect的位置信息来画出来
比如画右上角的旋转按钮:
if (rotateDrawable != null) {
//右上角
rotateDrawable.setBounds(right - drawableWidth, top - drawableHeight, right + drawableWidth,
top + drawableHeight);
rotateDrawable.draw(canvas);
}
重点就是去计算上面代码的mRotateMatrix
private void invalidateMatrix() {
mRotateMatrix.reset();
mRotateMatrix.postTranslate(-mRect.centerX(), -mRect.centerY());
mRotateMatrix.postRotate(mRotation);
mRotateMatrix.postTranslate(mRect.centerX(), mRect.centerY());
}
这里简单提一下matrix。学过线性代数的都知道矩阵吧,matrix其实就是个3x3的矩阵,里面的元素控制着旋转、缩放、平移、错切。直接new出来的矩阵是一个单位矩阵,描述的就是原来的图形信息,没有做变换。
然后就是矩阵计算不满足交换律,换句话说矩阵的前乘和后乘结果不一样,反应在matrix里面就是pre和post接口效果不一样。
简单点理解就是pre是放在操作队列头,post放在队尾,还有个set会清空整个队列再把它放进去。如果觉得担心记混,推荐就用post接口,符合先进先出的原则,先post的先执行。
二、如何判断什么时候该缩放、旋转或者平移
这里肯定是事件处理相关的了。处于方便,我们在ontouchEvent的时候把操作委托给GestureDetector,在onDown回调时判断点到了哪里,比如旋转、缩放、平移。然后在onScroll回调的时候通过每次的增量dx,dy计算缩放的比例、旋转角度以及移动距离。
在手指头按下时,由于我们知道图片rec的位置信息和角标位置信息,所以通过x和y可以判断是否点到了角标。
但是有个问题是,画的矩形是在旋转过的画布上面,我们手指头的xy是屏幕上的位置,这里对应不起来,会出现旋转后就点不到角标了
为了解决这个问题,手指头按下的point需要利用matix做一次映射。在这里的场景其实就相当于把旋转的信息考虑进去,其实也完全可以自己用三角函数算,但是matrix已经提供这种接口了,而且是调用的c层计算,效率应该更高些。
final Matrix rotateMatrix = new Matrix();
//反向旋转回去 抵消canvas的旋转
rotateMatrix.postTranslate(-mRect.centerX(), -mRect.centerY());
rotateMatrix.postRotate(-mRotation);
rotateMatrix.postTranslate(mRect.centerX(), mRect.centerY());
rotateMatrix.mapPoints(point);
eventX = point[0];
eventY = point[1];
RectF rectF = mRect;
//hit rotate 右上
if (Math.abs(rectF.right - eventX) < drawableWidth * 2
&& Math.abs(rectF.top - eventY) < drawableHeight * 2) {
return HitModes.ROTATE;
}
需要注意的是这里matix的旋转角度和canvas的是相反的,其实就相当于转了n角度,然后又转回去n角度,相当于没有转。然后就可以按照没有旋转的情况判断有没有点到角标。
三、如何进行旋转、缩放、平移
第二步已经判断到用户想要进行什么操作了,接下来就是执行对应的操作了。
平移
先说简单的平移吧,上面说了,我们的场景是一个矩形位置信息+绕中心旋转角度。所以平移其实就是改矩形的位置罢了,直接调rec的offset,然后一定要记得在重新绘制前更新canvas的旋转矩阵
private void onMove(float dx, float dy) {
mRect.offset(-dx, -dy);
invalidateMatrix();
invalidate();
}
缩放
首先我们要明确一个东西,就是缩放时旋转中心一定是不变的。所以可以算出中心和右下角的距离以及scroll后的中心和右下角的距离算出两个距离的变化当成x的变化。我们这里是等比例缩放,根据比例算出y的变化。
这里做了一个缩放最小的限制,缩放到1.5个角度宽高后就不嫩再缩小了,这样可以保证角标不会挤在一起。
private void onScale(float dx, float dy) {
// TODO: 2019/4/8 这里的dx,dy计算需要改进
float[] pt1 = new float[] { mRect.centerX(), mRect.centerY() };
float[] pt2 = new float[] { mRect.right, mRect.bottom };
float[] pt3 = new float[] { mRect.right + dx, mRect.bottom + dy };
float distance1 = getPointDistance(pt1, pt2);
float distance2 = getPointDistance(pt1, pt3);
float distance = distance1 - distance2;
if (!checkCanScale(distance)) {
return;
}
mRect.inset(-distance, -distance / mRatio);
invalidateMatrix();
invalidate();
}
旋转
至于旋转其实也简单,因为旋转时中心也是不变的,类似缩放的操作。根据右上角的旋转角标和中心的角度,以及scroll后右上角和中心的角度,这两个的角度差就是旋转角度。
已知两个点的位置,通过math的atan2函数可以算出角度
private void onRotate(float triggerX, float triggerY) {
// TODO: 2019/4/8 这里的dx,dy计算需要改进
float[] pt1 = new float[] { mRect.centerX(), mRect.centerY() };
float[] pt2 = new float[] { mRect.right, mRect.top };
float[] pt3 = new float[] { triggerX, triggerY };
double angel1 = PointUtil.calculateAngleBetweenPoints(pt2, pt1);
double angel2 = PointUtil.calculateAngleBetweenPoints(pt3, pt1);
mRotation = (float) (angel1 - angel2);
invalidateMatrix();
invalidate();
}
到这里基本的平移旋转缩放都介绍完了,详细的一些位置计算可以参考demo
https://github.com/dynamicBai/ScaleRotateView 给个star鼓励下呗
后面会逐步介绍:拉伸四边的操作(旋转中心会变化)、图片在屏幕上的快速定位和微调、移动时十字辅助线对齐等效果。