我有一个名为
MyView
的自定义视图,它有两个锚点。我希望能够从它的锚点旋转这个视图,一个锚点作为旋转中心,而用户拖动另一个锚点。
为简单起见,我画了一条线,但实际上,我会画别的东西。以下是步骤:
setPivotX()
和setPivotY()
使另一个锚点成为旋转中心。这里是相关代码:
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
tapX = event.getX();
tapY = event.getY();
if (anchorRect1.contains(tapX, tapY)) {
viewState = ViewState.ANCHOR1;
setPivotX(anchorRect2.centerX());
setPivotY(anchorRect2.centerY());
} else if (anchorRect2.contains(tapX, tapY)) {
viewState = ViewState.ANCHOR2;
setPivotX(anchorRect1.centerX());
setPivotY(anchorRect1.centerY());
} else {
viewState = ViewState.VIEW;
}
break;
}
case MotionEvent.ACTION_MOVE: {
switch (viewState) {
//Anchor 1 clicked
case ANCHOR1: {
float angle = (float) Math.toDegrees(Math.atan2(event.getY() - anchorRect2.centerY(), getWidth()));
setRotation(getRotation() - angle);
break;
}
//Anchor 2 clicked
case ANCHOR2: {
float angle = (float) Math.toDegrees(Math.atan2(event.getY() - anchorRect1.centerY(), getWidth()));
setRotation(getRotation() + angle);
break;
}
//View Clicked
case VIEW: {
float deltaX = event.getX() - tapX;
float deltaY = event.getY() - tapY;
// Transform point
PointF transformedPoint = transformPoint(deltaX, deltaY, getRotation());
//Perform the rotation
setX(getX() + transformedPoint.x);
setY(getY() + transformedPoint.y);
break;
}
}
break;
}
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL: {
break;
}
}
return true;
}
这里是当前输出的截图:
问题是,当我第一次旋转视图时,它工作正常,但是当我从另一个锚点旋转它时,它跳到另一个位置。我相信这是因为旋转不会影响视图的位置。为了解决这个问题,我需要重新计算视图的新位置,以便它相应地移动,但我不确定该怎么做。
这就是我想要实现的目标(使用 GeoGebra 工具创建):
此外,我相信这个问题不仅限于Android平台,因为我在使用Qt框架实现this功能时遇到了类似的问题。
我已经投入了大量时间来解决这个问题,非常感谢您提供的任何帮助。
--------------------开始编辑------------------------ ----------
根据 here 的回答,当前实施的问题似乎是视图没有保存之前的转换。每次更新轴心点时,所有先前的转换都会重置,这会导致视图行为不正确。
为了解决这个问题,我们需要链接这些转换(一个接一个地应用)。在 Qt 中,这可以通过将转换保存在列表中来实现,如下代码所示:
QList<QGraphicsTransform*> trans;
auto angle = qRadiansToDegrees(qAtan2( _anchor2.y() -p.y(), width));
QGraphicsRotation* rot = new QGraphicsRotation;
rot->setOrigin(QVector3D(cp2.x(), cp2.y(), 0));
rot->setAxis(Qt::ZAxis);
rot->setAngle(angle);
trans.push_back(rot);
setTransformations(_trans);
实现此行为的另一种方法是直接使用
QTransform
类。在这种情况下,重要的是在 isRelative
中将 setTransform()
参数设置为 true,以便将转换链接在一起。
QTransform t;
auto angle = qRadiansToDegrees(qAtan2(_anchor2.y() -p.y(), width));
t.translate(cp2.x(), cp2.y());
t.rotate(angle);
t.translate(-cp2.x(), -cp2.y());
setTransform(t, true);
在Android中,
QTransform
相当于Matrix
类。但是,与QT不同的是,Android中的View
类没有setMatrix()
方法。这意味着在执行转换时,您不能简单地将当前矩阵应用到视图,这使得它更难处理。
--------------------编辑结束---------------------------- --------------
这里是重现问题的repo,下面是完整代码:
MyView.java
public class MyView extends View {
enum ViewState {
ANCHOR1, ANCHOR2, VIEW
}
public static final int RADIUS = 24; // Anchor radius
public static final int OFFSET = 1;
private final Paint paint;
public RectF anchorRect1;
public RectF anchorRect2;
private ViewState viewState;
private float tapX;
private float tapY;
public MyView(Context context) {
super(context);
paint = new Paint();
paint.setStyle(Paint.Style.STROKE);
anchorRect1 = new RectF();
anchorRect2 = new RectF();
}
@Override
protected void onDraw(Canvas canvas) {
var width = getWidth();
var height = getHeight();
//Draw line between anchor points
canvas.drawLine(
anchorRect1.centerX(), anchorRect1.centerY(),
anchorRect2.centerX(), anchorRect2.centerY(), paint);
//Draw anchor circle
canvas.drawCircle(anchorRect1.centerX(), anchorRect1.centerY(), RADIUS, paint);
canvas.drawCircle(anchorRect2.centerX(), anchorRect2.centerY(), RADIUS, paint);
//Draw bounding rect
canvas.drawRect(OFFSET, OFFSET, width - OFFSET, height - OFFSET, paint);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
//Recompute anchors rect
anchorRect1.set(
OFFSET, h /2f - RADIUS + OFFSET,
2*RADIUS, h /2f + RADIUS - OFFSET
);
anchorRect2.set(
w - 2*RADIUS, h /2f - RADIUS + OFFSET,
w - OFFSET, h /2f + RADIUS - OFFSET
);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// Measure exactly
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(width, height);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
tapX = event.getX();
tapY = event.getY();
// Anchor 1 clicked
if (anchorRect1.contains(tapX, tapY)) {
viewState = ViewState.ANCHOR1;
setPivotX(anchorRect2.centerX());
setPivotY(anchorRect2.centerY());
}
// Anchor 2 clicked
else if (anchorRect2.contains(tapX, tapY)) {
viewState = ViewState.ANCHOR2;
setPivotX(anchorRect1.centerX());
setPivotY(anchorRect1.centerY());
}
// View body clicked
else {
viewState = ViewState.VIEW;
}
break;
}
case MotionEvent.ACTION_MOVE: {
switch (viewState) {
//Anchor 1 clicked
case ANCHOR1: {
float angle = (float) Math.toDegrees(Math.atan2(event.getY() - anchorRect2.centerY(), getWidth()));
setRotation(getRotation() - angle);
break;
}
//Anchor 2 clicked
case ANCHOR2: {
float angle = (float) Math.toDegrees(Math.atan2(event.getY() - anchorRect1.centerY(), getWidth()));
setRotation(getRotation() + angle);
break;
}
//View Clicked
case VIEW: {
float deltaX = event.getX() - tapX;
float deltaY = event.getY() - tapY;
// Transform point
PointF transformedPoint = transformPoint(deltaX, deltaY, getRotation());
//Perform the rotation
setX(getX() + transformedPoint.x);
setY(getY() + transformedPoint.y);
break;
}
}
break;
}
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL: {
break;
}
}
return true;
}
private PointF transformPoint(float x, float y, float degree) {
//https://en.wikipedia.org/wiki/Rotation_matrix
double radian = Math.toRadians(degree);
float xp = (float) (x*Math.cos(radian) - y*Math.sin(radian));
float yp = (float) (x*Math.sin(radian) + y*Math.cos(radian));
return new PointF(xp, yp);
}
}
FixedPosLayout.java
public class FixedPosLayout extends ViewGroup {
public FixedPosLayout(Context context) {
super(context);
}
public FixedPosLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public FixedPosLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int count = getChildCount();
for (int i = 0; i<count; ++i) {
View child = getChildAt(i);
FixedPosLayout.LayoutParams params =
(FixedPosLayout.LayoutParams) child.getLayoutParams();
int left = params.x;
int top = params.y;
child.layout(left, top,
left + child.getMeasuredWidth(),
top + child.getMeasuredHeight());
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// Measure this view
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// Measure children
int count = getChildCount();
for (int i = 0; i<count; ++i) {
View child = getChildAt(i);
FixedPosLayout.LayoutParams lp =
(FixedPosLayout.LayoutParams) child.getLayoutParams();
int childWidthMeasureSpec =
MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY);
int childHeightMeasureSpec =
MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
public static class LayoutParams extends ViewGroup.LayoutParams {
public int x;
public int y;
public LayoutParams(int width, int height, int x, int y) {
super(width, height);
this.x = x;
this.y = y;
}
}
}
MainActivity.java
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
FixedPosLayout fixedPosLayout = findViewById(R.id.fixedPosLayout);
MyView myView = new MyView(this);
FixedPosLayout.LayoutParams params = new FixedPosLayout.LayoutParams(300, 80, 200, 400);
fixedPosLayout.addView(myView, params);
}
}
main_activity.xml
<com.abdo.rotateviewquestion.FixedPosLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/fixedPosLayout"
tools:context=".MainActivity"/>