Соответствующий повернутый объект числовым значениям
У меня есть кодовый замок, вращающийся в круге на 360 градусов.
Кодовый замок имеет числовые значения, они являются чисто графическими.
Мне нужен способ перевести поворот изображения в 0-99 значений на графике.
На этом первом рисунке значение должно быть в состоянии сказать мне "0"
http://i48.tinypic.com/27y67b7.png
На этом рисунке после того, как пользователь повернул изображение, значение должно быть в состоянии сказать мне "72"
http://i46.tinypic.com/2ueiogh.png
Вот код:
package co.sts.combinationlock;
import android.os.Bundle;
import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.util.Log;
import android.view.GestureDetector;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.GestureDetector.SimpleOnGestureListener;
import android.view.View.OnTouchListener;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.widget.ImageView;
import android.support.v4.app.NavUtils;
public class ComboLock extends Activity{
private static Bitmap imageOriginal, imageScaled;
private static Matrix matrix;
private ImageView dialer;
private int dialerHeight, dialerWidth;
private GestureDetector detector;
// needed for detecting the inversed rotations
private boolean[] quadrantTouched;
private boolean allowRotating;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_combo_lock);
// load the image only once
if (imageOriginal == null) {
imageOriginal = BitmapFactory.decodeResource(getResources(), R.drawable.numbers);
}
// initialize the matrix only once
if (matrix == null) {
matrix = new Matrix();
} else {
// not needed, you can also post the matrix immediately to restore the old state
matrix.reset();
}
detector = new GestureDetector(this, new MyGestureDetector());
// there is no 0th quadrant, to keep it simple the first value gets ignored
quadrantTouched = new boolean[] { false, false, false, false, false };
allowRotating = true;
dialer = (ImageView) findViewById(R.id.locknumbers);
dialer.setOnTouchListener(new MyOnTouchListener());
dialer.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
// method called more than once, but the values only need to be initialized one time
if (dialerHeight == 0 || dialerWidth == 0) {
dialerHeight = dialer.getHeight();
dialerWidth = dialer.getWidth();
// resize
Matrix resize = new Matrix();
//resize.postScale((float)Math.min(dialerWidth, dialerHeight) / (float)imageOriginal.getWidth(), (float)Math.min(dialerWidth, dialerHeight) / (float)imageOriginal.getHeight());
imageScaled = Bitmap.createBitmap(imageOriginal, 0, 0, imageOriginal.getWidth(), imageOriginal.getHeight(), resize, false);
// translate to the image view's center
float translateX = dialerWidth / 2 - imageScaled.getWidth() / 2;
float translateY = dialerHeight / 2 - imageScaled.getHeight() / 2;
matrix.postTranslate(translateX, translateY);
dialer.setImageBitmap(imageScaled);
dialer.setImageMatrix(matrix);
}
}
});
}
/**
* Rotate the dialer.
*
* @param degrees The degrees, the dialer should get rotated.
*/
private void rotateDialer(float degrees) {
matrix.postRotate(degrees, dialerWidth / 2, dialerHeight / 2);
//need to print degrees
dialer.setImageMatrix(matrix);
}
/**
* @return The angle of the unit circle with the image view's center
*/
private double getAngle(double xTouch, double yTouch) {
double x = xTouch - (dialerWidth / 2d);
double y = dialerHeight - yTouch - (dialerHeight / 2d);
switch (getQuadrant(x, y)) {
case 1:
return Math.asin(y / Math.hypot(x, y)) * 180 / Math.PI;
case 2:
case 3:
return 180 - (Math.asin(y / Math.hypot(x, y)) * 180 / Math.PI);
case 4:
return 360 + Math.asin(y / Math.hypot(x, y)) * 180 / Math.PI;
default:
// ignore, does not happen
return 0;
}
}
/**
* @return The selected quadrant.
*/
private static int getQuadrant(double x, double y) {
if (x >= 0) {
return y >= 0 ? 1 : 4;
} else {
return y >= 0 ? 2 : 3;
}
}
/**
* Simple implementation of an {@link OnTouchListener} for registering the dialer's touch events.
*/
private class MyOnTouchListener implements OnTouchListener {
private double startAngle;
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// reset the touched quadrants
for (int i = 0; i < quadrantTouched.length; i++) {
quadrantTouched[i] = false;
}
allowRotating = false;
startAngle = getAngle(event.getX(), event.getY());
break;
case MotionEvent.ACTION_MOVE:
double currentAngle = getAngle(event.getX(), event.getY());
rotateDialer((float) (startAngle - currentAngle));
startAngle = currentAngle;
break;
case MotionEvent.ACTION_UP:
allowRotating = true;
break;
}
// set the touched quadrant to true
quadrantTouched[getQuadrant(event.getX() - (dialerWidth / 2), dialerHeight - event.getY() - (dialerHeight / 2))] = true;
detector.onTouchEvent(event);
return true;
}
}
/**
* Simple implementation of a {@link SimpleOnGestureListener} for detecting a fling event.
*/
private class MyGestureDetector extends SimpleOnGestureListener {
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
// get the quadrant of the start and the end of the fling
int q1 = getQuadrant(e1.getX() - (dialerWidth / 2), dialerHeight - e1.getY() - (dialerHeight / 2));
int q2 = getQuadrant(e2.getX() - (dialerWidth / 2), dialerHeight - e2.getY() - (dialerHeight / 2));
// the inversed rotations
if ((q1 == 2 && q2 == 2 && Math.abs(velocityX) < Math.abs(velocityY))
|| (q1 == 3 && q2 == 3)
|| (q1 == 1 && q2 == 3)
|| (q1 == 4 && q2 == 4 && Math.abs(velocityX) > Math.abs(velocityY))
|| ((q1 == 2 && q2 == 3) || (q1 == 3 && q2 == 2))
|| ((q1 == 3 && q2 == 4) || (q1 == 4 && q2 == 3))
|| (q1 == 2 && q2 == 4 && quadrantTouched[3])
|| (q1 == 4 && q2 == 2 && quadrantTouched[3])) {
dialer.post(new FlingRunnable(-1 * (velocityX + velocityY)));
} else {
// the normal rotation
dialer.post(new FlingRunnable(velocityX + velocityY));
}
return true;
}
}
/**
* A {@link Runnable} for animating the the dialer's fling.
*/
private class FlingRunnable implements Runnable {
private float velocity;
public FlingRunnable(float velocity) {
this.velocity = velocity;
}
@Override
public void run() {
if (Math.abs(velocity) > 5 && allowRotating) {
//rotateDialer(velocity / 75);
//velocity /= 1.0666F;
// post this instance again
dialer.post(this);
}
}
}
}
Я думаю, что мне нужно перевести некоторую информацию из матрицы в значение 0-99.
4 ответа
Вы должны полностью реорганизовать свой код. Пост умножение новых вращений в матрицу снова и снова является численно нестабильным вычислением. В конце концов растровое изображение будет искажено. Попытка извлечь угол поворота из матрицы слишком сложна и не нужна.
Прежде всего обратите внимание, что это полезная предыдущая статья о рисовании растровых изображений с вращением вокруг выбранной точки.
Просто поддерживать один double dialAngle = 0
это текущий угол поворота шкалы.
Вы выполняете слишком много работы, чтобы восстановить угол касания. Позволять (x0,y0)
быть местом, где начинается касание. В это время,
// Record the angle at initial touch for use in dragging.
dialAngleAtTouch = dialAngle;
// Find angle from x-axis made by initial touch coordinate.
// y-coordinate might need to be negated due to y=0 -> screen top.
// This will be obvious during testing.
a0 = Math.atan2(y0 - yDialCenter, x0 - xDialCenter);
Это начальный угол. Когда касание тянется к (x,y)
, используйте эту координату, чтобы отрегулировать циферблат относительно начального касания. Затем обновите матрицу и перерисуйте:
// Find new angle to x-axis. Same comment as above on y coord.
a = Math.atan2(y - yDialCenter, x - xDialCenter);
// New dial angle is offset from the one at initial touch.
dialAngle = dialAngleAtTouch + (a - a0);
// normalize angles to the interval [0..2pi)
while (dialAngle < 0) dialAngle += 2 * Math.PI;
while (dialAngle >= 2 * Math.PI) dialAngle -= 2 * Math.PI;
// Set the matrix for every frame drawn. Matrix API has a call
// for rotation about a point. Use it!
matrix.setRotate((float)dialAngle * (180 / 3.1415926f), xDialCenter, yDialCenter);
// Invalidate the view now so it's redrawn in with the new matrix value.
Заметка Math.atan2(y, x)
делает все, что вы делаете с квадрантами и арксинусами.
Чтобы получить "отметку" текущего угла, вам нужно 2 пи радиана, чтобы соответствовать 100, так что все очень просто:
double fractionalTick = dialAngle / (2 * Math.Pi) * 100;
Чтобы найти фактический ближайший тик как целое число, округлите дробь и мод на 100. Обратите внимание, что вы можете игнорировать матрицу!
int tick = (int)(fractionalTick + 0.5) % 100;
Это всегда будет работать, потому что dialAngle
находится в [0..2pi). Мод необходим для отображения округленного значения 100 обратно в 0.
Чтобы лучше понять, что делает матрица, полезно разобраться с матрицами преобразования 2D-графики: http://en.wikipedia.org/wiki/Transformation_matrix. Если единственное, что вы делаете, это вращение (не скажем, преобразование или масштабирование), то относительно легко извлечь вращение. Но, на практике, вы можете изменить код поворота и сохранить переменную состояния
private float rotationDegrees = 0;
/**
* Rotate the dialer.
*
* @param degrees The degrees, the dialer should get rotated.
*/
private void rotateDialer(float degrees)
matrix.postRotate(degrees, dialerWidth / 2, dialerHeight / 2);
this.rotationDegrees += degrees;
// Make sure we don't go over 360
this.rotationDegrees = this.rotationDegrees % 360
dialer.setImageMatrix(matrix);
}
Сохраняйте переменную для хранения общего поворота в градусах, который вы увеличиваете в своей функции поворота. Теперь мы знаем, что 3,6 градуса - это отметка. Простая математическая доходность
tickNumber = (int)rotation*100/360
// It could be negative
if (tickNumber < 0)
tickNumber = 100 - tickNumber
И последнее, что вы должны проверить: если у вас поворот на 360 градусов или число тиков 100, вы должны рассматривать его как 0 (так как тика 100 нет)
Это должно быть простое умножение с "масштабным" коэффициентом, который уменьшает ваше значение степени (0-359) до вашей шкалы 0-99:
float factor = 99f / 359f;
float scaled = rotationDegree * factor;
РЕДАКТИРОВАТЬ: Исправление функции getAngle
Вместо getAngle вы можете использовать функцию atan2, которая преобразует декартовы координаты в угол.
Просто сохраните первую координату касания при касании, и при движении вы можете применить следующий расчет:
// PointF a = touch start point
// PointF b = current touch move point
// Translate to origin:
float x = b.x - a.x;
float y = b.y - a.y;
float radians = (float) ((Math.atan2(-y, x) + Math.PI + HALF_PI) % TWO_PI);
Радианы имеют диапазон двух пи. вычисления по модулю вращают его, так что значение 0 пунктов вверх. Направление вращения против часовой стрелки.
Поэтому вам нужно преобразовать это в градусы и изменить направление вращения, чтобы получить правильный угол.
Циферблат должен быть повернут ровно на 3,6 градуса, чтобы перейти от одной отметки к следующей или предыдущей. Каждый раз, когда касание пользователя поворачивается (вокруг центра) на 3,6 градуса, циферблат должен поворачиваться на 1 метку (3,6 градуса).
Фрагмент кода:
float touchAngle = getTouchAngle();
float mark = touchAngle / 3.6f;
if (mCurrentMark != mark) {
setDialMark(mark);
}
getTouchAngle()
вычисляет угол точки касания пользователя относительно центра набора номера, используяatan2
,setDialMark
вращает диск на количество измененных марок.
,
void setDialMark(int mark) {
rotateDialBy(mCurrentMark - mark);
mCurrentMark = mark;
}
void rotateDialBy(int rotateMarks) {
rotationAngle = rotateMarks * 3.6;
...
/* Rotate the dial by rotationAngle. */
}