如何在 Android 中从锚点旋转自定义视图时固定它的位置?

问题描述 投票:0回答:0

我有一个名为

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"/>
java android kotlin rotation android-custom-view
© www.soinside.com 2019 - 2024. All rights reserved.