Эллиптический просмотр в Android

Мне нужно создать ряд концентрических эллипсов (колец), и нужно поместить пользовательские значки на окружность этих эллипсов. См. Изображение ниже.

Rings View

До сих пор я рисовал 3 эллиптических концентрических круга на холсте и размещал пользовательские значки. Мне нужны пользовательские значки, которые можно перетаскивать по кольцам.

Просьба предложить способы реализации этого.

Ответ 1

Так как вы видите, что вы уже размещаете значки на окружности колец, я предполагаю, что вы знаете, как сделать математику [но см. править], чтобы определить точки вдоль окружности и спрашивать о перетаскивании.

Вероятно, вы захотите реализовать движение значков с помощью подхода перетаскивания. Предполагая, что вы держите кольца как одно изображение, у вас будет только один пункт назначения. Затем вам нужно будет математически проанализировать точку падения [см. Править] (путем определения цвета его пикселя), чтобы определить, в какое кольцо попал значок. Если вы создаете отдельные виды для колец, каждая из них может быть собственной точкой падения. (Вероятно, вам, вероятно, придется в конечном итоге решить, как перераспределять значки в каждом кольце, но это другой вопрос.)

Вот некоторый код, который показывает минимальный способ обработки перетаскивания с помощью значка одного изображения в одной группе представлений (где вы будете отображать изображение ваших колец).

MainActivity.java

package com.example.dragexample;

import android.app.Activity;
import android.content.ClipData;
import android.os.Bundle;
import android.util.Log;
import android.view.DragEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.DragShadowBuilder;
import android.view.View.OnDragListener;
import android.view.View.OnTouchListener;
import android.widget.ImageView;

public class MainActivity extends Activity {

static final String TAG = "DragActivity";

ImageView icon = null;


@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    findViewById(R.id.rings).setOnDragListener(new OnDragListener() {
        @Override
        public boolean onDrag(View vw, DragEvent event) {
            if (event.getAction() == DragEvent.ACTION_DROP) {
                // Drop the icon and redisplay it:
                icon.setX(event.getX());
                icon.setY(event.getY());
                icon.setVisibility(View.VISIBLE);

                // Analyze the drop point mathematically (or perhaps get its pixel color)
                //  to determine which ring the icon has been dragged into and then take
                //  appropriate action.
                int destRing = determineDestinationRing(event.getX(), event.getY());
            }

            return true;
        }
    });

    icon = (ImageView) findViewById(R.id.icon);
    icon.setOnTouchListener(new OnTouchListener() {
        public boolean onTouch(View vw, MotionEvent event) {
            Log.v(TAG, "Touch event " + event.getAction());
            if (event.getActionMasked() == MotionEvent.ACTION_MOVE) {
                Log.v(TAG, "Starting drag");

               // Set up clip data (empty) and drag shadow objects and start dragging:
                ClipData cd = ClipData.newPlainText("", "");
                DragShadowBuilder shadowBuilder = new View.DragShadowBuilder(vw);
                vw.startDrag(cd, shadowBuilder, vw, 0);
                vw.setVisibility(View.INVISIBLE);
            }

            return true;
        }
    });
}

public void resetImage(View vw) {
    Log.v(TAG, "Resetting image position");

    icon.setX(0f);
    icon.setY(0f);
    icon.setVisibility(View.VISIBLE);
}

}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"

    android:id="@+id/rings"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:onClick="resetImage" >

    <ImageView
        android:id="@+id/icon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/ic_launcher" />

</FrameLayout>

Изменить 1:

Чтобы математически определить, в какое кольцо помещается значок, вы можете использовать что-то вроде следующего, которое проходит через реализацию стандартного уравнения для эллипса с использованием жестко заданных размеров для осей. Обратите внимание, что нуль будет возвращен, если значок будет сброшен внутри самого внутреннего кольца. Кроме того, этот подход будет немного более сложным на практике из-за того, что размеры экрана на экране, скорее всего, будут скорректированы при визуализации макета. В этом случае конечные размеры осей необходимо будет определять во время выполнения.

// Axis values must be ascending order; ring 0 is 'me';
float a[] = {50, 100, 150, 200};
float b[] = {100, 200, 300, 400};

public int determineDestinationRing(float x, float y) {

    // Check for inclusion within each ring:
    for (int i = 0; i < a.length; i++) {
        if (((x * x) / (a[i] * a[i]) + (y * y) / (b[i] * b[i])) <= 1)
            return i;
    }

    return -1;
}

Ответ 2

Чтобы решить проблему, вам нужно использовать уравнение эллипса:

(x/a) 2 + (y/a) 2= 1,

где:
x, y - координаты любой точки на окружности эллипса a, b - радиус на оси x и y соответственно.

Когда пользователь перетаскивает и опускает значок, если центральные координаты значка пересекаются с окружностью эллипса, то вышеприведенная формула должна быть хорошей. В этом случае вы можете поместить значок на эллипс. Поскольку у вас есть 3 эллипса, вам придется делать эту проверку пару раз, каждый раз для двух других эллипсов.

public class CustomView extends View {

        // Other methods 

        public boolean onTouchEvent(MotionEvent e) {
            int index = e.getActionIndex();
            float x = e.getX(index);
            float y = e.getY(index);

            int a = this.getWidth()/2;
            int b = this.getHeight()/2;

            // x-a instead of x and y-b instead of y, to get the offset from centre of ellipse.
            double result = Math.pow(((x-a)/a), 2) + Math.pow(((y-b)/b), 2);

            Log.v(TAG, "(" + (x-a) + "/" + a + ")2 + (" + (y-b) + "/" + b + ")2 = " + result);

            return true;
        }
}

Ответ 3

Я бы сделал это так. Прежде всего нам нужно определить, какой значок коснулся события TOUCH_DOWN. Это можно просто сделать, сравнив координаты точки касания и значков. Как только найден ближайший значок, мы также должны знать, к какому эллипсу принадлежит этот значок, то есть мы знаем как горизонтальный, так и вертикальный радиусы этого эллипса. Мы также знаем координаты центра эллипсов.

Тогда вот идея того, что мы будем делать. Мы вычислим угол между линией, проходящей через точку касания, и центром эллипсов и горизонтальной линией, проходящей через центр.

               o <- touch point
              /
             / \ <- angle
  center->  o-----  <- horizontal line

Теперь, зная угол, мы будем использовать уравнение круга, настроенное на эллипс, чтобы пересчитать новые координаты значков, чтобы привязать наш значок точно к этому эллипсу. Перейдем к алгоритму.

Чтобы определить угол касания, мы будем использовать функцию atan2.

double atan2Angle = Math.atan2(touchY - centerY, touchX - centerX);

Это даст нам следующие значения, зависящие от угла.

        -π
   -2*π      -0
         o          <- center 
    2*π       0
         π

Чтобы использовать этот угол в уравнении круга, нам нужно преобразовать его в более традиционное представление, подобное приведенному ниже.

        270°

   180°  o    0°/360°

         90°

Это можно сделать следующим образом.

    float angleFactor = (float) (atan2Angle / 2 * Math.PI);
    if (angleFactor < 0) {
        angleFactor += 1f;
    }

    float touchAngle = angleFactor * 360f;
    if (angle < 0) {
        angle += 360f;
    }

Теперь, после того, как мы получим наш угол касания в градусе, мы можем вычислить новые координаты для нашего значка с помощью уравнения эллипса.

double touchAngleRad = Math.toRadians(touchAngle);
float iconX = centerX + (float) (radiusX * Math.cos(touchAngleRad));
float iconY = centerY + (float) (radiusY * Math.sin(touchAngleRad));

// centerX, centerY - coordinates of the center of ellipses
// radiusX, radiusY - horizontal and vertical radiuses of the ellipse, to which 
//                    the touched icon belongs
// iconX, iconY     - new coordinates of the icon lying on that ellipse

Если вы пересчитаете положение значка в соответствии с этим алгоритмом на каждом TOUCH_MOVE и сделаете недействительным представление, тогда ваш значок будет перемещаться по его эллипсу.

Код может быть дополнительно оптимизирован с использованием радианов вместо градусов, но я думал, что градусы лучше подходят для объяснения. Надеюсь, эта помощь. Отправьте свой код, если у вас возникнут проблемы с реализацией.