这次讲一下矩形的单边拉伸,即非对称拉伸。
对矩形旋转缩放平移这些不怎么了解的可以参考我的上一篇旋转控件(一):矩形的平移旋转缩放
可能有人会想矩形的单边拉伸还不简单?按照上一篇矩形控制位置和大小,matrix控制旋转的思路,矩形拉伸不是分分钟的事情,拉左边就改left,拉上边就改top,反正又不会影响旋转角度。
一、发现问题
emmm....最开始听到产品经理加这个需求,我也是这么想的
按照以前的思路,四边中心添加按钮,然后touch选中后更改矩形对应边,so easy,拿出电脑啪啪啪一顿按
private void onStretch(int mode, float dx, float dy) {
//测试左边拉伸
if (mode == HitModes.LEFT_STRETCH) {
mRect.left -= dx;
invalidateMatrix();
invalidate();
} else if (mode == HitModes.RIGHT_STRETCH) {
} else if (mode == HitModes.TOP_STRETCH) {
} else if (mode == HitModes.BOTTOM_STRETCH) {
}
}
测试一下拉伸左边,然后跑起来看看效果
嗯,没有旋转时拉伸是这么回事。旋转一点后怎么拉伸左边,其他三边也在变,导致整个图片像是在偏移。特别是转到接近90(270)度后,好像怎么拉也拉不动了。
what fuck!!!!
这就触及到我的知识盲区了啊,明明旋转角度没变的说....
二、分析问题
冷静分析,emmm...不旋转时看起来是没问题的,旋转后就出问题了。旋转角度是没变的,和缩放应该差不多的操作,也是绕中心旋转,而且旋转中心没变。
等等,单边拉伸的旋转中心好像是变了,不能再用原来的旋转矩阵了
private void invalidateMatrix() {
mRotateMatrix.reset();
mRotateMatrix.postTranslate(-mRect.centerX(), -mRect.centerY());
mRotateMatrix.postRotate(mRotation);
mRotateMatrix.postTranslate(mRect.centerX(), mRect.centerY());
}
上面代码里的centerX,centerY和上一次计算matrix的应该是不一样了,这时候的matrix应该是有问题的(用前朝的剑斩今朝的官 哈哈);
为了验证和旋转中心变化有关,做了一次测试。既然拉伸单边旋转中心有问题,那我拉伸对称边,这样旋转中心也是不变的,看看有没有问题。
代码改成这样:
private void onStretch(int mode, float dx, float dy) {
if (mode == HitModes.LEFT_STRETCH) {
mRect.inset(-dx, 0);
invalidateMatrix();
invalidate();
} else if (mode == HitModes.RIGHT_STRETCH) {
} else if (mode == HitModes.TOP_STRETCH) {
} else if (mode == HitModes.BOTTOM_STRETCH) {
}
}
效果如下:
可以看出对称拉伸后,由于旋转中心不变,旋转后拉伸是没问题的。但是90度左右这个还是有拉不动的情况,不过这个是另外一个问题。所以可以确定单边拉伸和旋转中心有关。
三、解决问题
现在有2个问题,最大的问题就是旋转中心改变后,导致拉伸漂移;另外一个问题其实有经验的能猜到,就是旋转后点对点之间的距离计算。
旋转中心方案一:尝试不更新matrix(失败)
其实从上面代码能看出来,每次拉伸后要重新计算旋转矩阵,然后让canvas去旋转,从而使得旋转角度应用上去。那么既然单边拉伸没有改变角度,那我们在这种情况下不让matrix改变是不是就行了?把重新计算martrix的注释掉
if (mode == HitModes.LEFT_STRETCH) {
mRect.left -= dx;
//invalidateMatrix();
invalidate();
}
效果:
看就起来好了?其实是一种错觉,而且很愚蠢的自欺欺人。类似view动画和属性动画,这里就像是只改变了视图效果,但是点击区域和视图已经对不上了,这时候再去点角标,基本上已经不响应了。因为类似马赛克效果需要c层来渲染这种,c层拿到的矩形位置和旋转角度,渲染出来后就发现和view位置完全对不上了。
现在想来就感觉这个做法好蠢啊,反正当事人就是非常后悔......
旋转中心方案二:借助逆矩阵(失败)
继续分析一会后,感觉似乎直接改rect的做法有问题?
视图上的rect应该是被旋转影响过的rect,也就是被matrix映射过的矩形。然后我们对rect的操作应该是基于这个矩形,再用matrix的逆矩阵再把映射过的矩形映射回去,负负它就得正了哇。
if (mode == HitModes.LEFT_STRETCH) {
//映射矩形
RectF rectF = new RectF(mRect);
mRotateMatrix.mapRect(rectF);
rectF.left -= dx;
//取逆矩阵
Matrix matrix = new Matrix();
mRotateMatrix.invert(matrix);
//再映射回原来的矩形(负负得正)
matrix.mapRect(rectF);
mRect.set(rectF);
invalidateMatrix();
invalidate();
}
效果:
emmm...虽然结果很炸裂,但这次就不后悔了。就咱这天马行空的想象力,我就问:还有谁!!!
旋转中心方案三:自己计算旋转中心(成功)
其实中间还试了matrix先绕左端缩放,再绕中心旋转后给canvas,这样也是可以实现这个拉伸效果,但是因为和要给c层一个矩形位置和旋转角度的场景不符,所以没有使用这个。
说到底,前面就是失败就是靠matrix计算不行,那么就自己算咯。
现在想来计算也不复杂。比如旋转后拉伸左边,旋转中心一定是在left和right中心线上变化,那条线和水平的夹角可以通过旋转角度得到。然后距离可以通过拉伸距离得到。
就构成了一个直角三角形,通过三角函数得到旋转中心的位移加上原旋转中心就是新的旋转中心。然后旋转不影响大小,拉伸后的宽高加上新的旋转中心就可以算出新的未旋转的矩形。
private void onStretch(int mode, float dx, float dy) {
RectF rectF = new RectF(mRect);
if (mode == HitModes.LEFT_STRETCH) {
//映射矩形
rectF.left -= dx;
} else if (mode == RIGHT_STRETCH) {
rectF.right += dx;
} else if (mode == HitModes.TOP_STRETCH) {
rectF.top -= dy;
} else if (mode == HitModes.BOTTOM_STRETCH) {
rectF.bottom += dy;
}
invalidateAfterStretch(mode, rectF);
invalidate();
}
private void invalidateAfterStretch(int mode, RectF newRect) {
//新的中心
float x, y;
//老的中心
float xOld = mRect.centerX();
float yOld = mRect.centerY();
//新的宽高
float width = newRect.width();
float height = newRect.height();
float length;
if (mode == HitModes.RIGHT_STRETCH) {
//以right实验 算出right的拉伸
length = (newRect.right - mRect.right) / 2;
x = (float) (xOld + length * Math.cos(Math.toRadians(mRotation)));
y = (float) (yOld + length * Math.sin(Math.toRadians(mRotation)));
} else if (mode == HitModes.LEFT_STRETCH) {
length = -(newRect.left - mRect.left) / 2;
x = (float) (xOld - length * Math.cos(Math.toRadians(mRotation)));
y = (float) (yOld - length * Math.sin(Math.toRadians(mRotation)));
} else if (mode == HitModes.TOP_STRETCH) {
length = -(newRect.top - mRect.top) / 2;
x = (float) (xOld + length * Math.sin(Math.toRadians(mRotation)));
y = (float) (yOld - length * Math.cos(Math.toRadians(mRotation)));
} else {
length = (newRect.bottom - mRect.bottom) / 2;
x = (float) (xOld - length * Math.sin(Math.toRadians(mRotation)));
y = (float) (yOld + length * Math.cos(Math.toRadians(mRotation)));
}
//新的矩形
float right = (2 * x + width) / 2;
float left = (2 * x - width) / 2;
float bottom = (2 * y + height) / 2;
float top = (2 * y - height) / 2;
mRect.set(left, top, right, bottom);
if (mRect.height() > 0) {
mRatio = mRect.width() / mRect.height();
}
mRotateMatrix.reset();
mRotateMatrix.postTranslate(-x, -y);
mRotateMatrix.postRotate(mRotation);
mRotateMatrix.postTranslate(x, y);
}
效果也解决了拉伸单边后旋转中心变化的问题:
旋转90度拉不动的问题
一个最极端的例子:旋转90度后,你手指往下拉,这时候因为水平没有动,所以dx为0;但是代码里作用加在left上的是dx,所以这时候出现完全拉不动的情况。
其实这个就是需要考虑旋转角度后点对点的计算滑动距离了。比如拉左边时,通过拉动前左边按钮和中心点的距离以及拉后新的左边按钮和中心算出来的距离(dx、dy通过matrix map后的值)的差值就可以当成手指滑动的距离。
/**
* 根据控件拉伸方向,算出实际distance(包含rotate的影响)
*
* @param dx event dx
* @param dy event dy
* @param mode 上下左右
*/
private float calculateStretchDistance(float dx, float dy, int mode) {
//中心点
float pt1[] = new float[] { mRect.centerX(), mRect.centerY() };
//源rect上 edge上的圆点
float pt2[];
if (mode == RIGHT_STRETCH) {
pt2 = new float[] { mRect.right, mRect.centerY() };
} else if (mode == HitModes.LEFT_STRETCH) {
pt2 = new float[] { mRect.left, mRect.centerY() };
} else if (mode == HitModes.TOP_STRETCH) {
pt2 = new float[] { mRect.centerX(), mRect.top };
} else {
pt2 = new float[] { mRect.centerX(), mRect.bottom };
}
float points[] = new float[] { dx, dy };
Matrix rotateMatrix = new Matrix();
rotateMatrix.postRotate(-mRotation);
rotateMatrix.mapPoints(points);
//映射上角度后 实际的dx,dy
dx = points[0];
dy = points[1];
//result rect上 edge上的圆点
float pt3[];
if (mode == RIGHT_STRETCH) {
pt3 = new float[] { mRect.right + dx, mRect.centerY() + dy };
} else if (mode == HitModes.LEFT_STRETCH) {
pt3 = new float[] { mRect.left + dx, mRect.centerY() + dy };
} else if (mode == HitModes.TOP_STRETCH) {
pt3 = new float[] { mRect.centerX() + dx, mRect.top + dy };
} else {
pt3 = new float[] { mRect.centerX() + dx, mRect.bottom + dy };
}
double distance1 = PointUtil.calculatePointDistance(pt1, pt2);
double distance2 = PointUtil.calculatePointDistance(pt1, pt3);
return (float) (distance2 - distance1);
}
最后上图 丝滑般的体验哈哈:
demo见https://github.com/dynamicBai/ScaleRotateView
都看到这里了,给个star鼓励下呗