/* Моя кошка замечательно разбирается в программировании. Стоит мне объяснить проблему ей - и все становится ясно. */
John Robbins, Debugging Applications, Microsoft Press, 2000
Эта старая статья Android SDK: Creating a Rotating Dialer - Tuts+ Code Tutorial написана в декабре 2011 года. Оставил себе на память, так как понравился эффект. Я только поменял картинку. Но сам код нуждается в ревизии. Если вам интересно, то читайте статью по ссылке.
Реализуем вращение круга вокруг своего центра, напоминающее вращение дискового номеронаберателя в старых домашних телефонах.
Подготовим нужное изображение круга. Я подготовил два изображения. Одно послужило мне фоном - круг с цифрами, а второе изображение - полупрозрачный круг с дырочками, которое будет наложено поверх первого изображения.
Разметка. Я задал жестко размеры для ImageView, чтобы фон и накладываемое изображение были одинаковыми. Вы можете придумать свою реализацию для этой задачи.
<LinearLayout 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:orientation="vertical"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".MainActivity" >
<ImageView
android:id="@+id/imageView_ring"
android:layout_width="300dp"
android:layout_height="300dp"
android:background="@drawable/ring22"
android:scaleType="matrix"
android:src="@drawable/ring11" >
</ImageView>
</LinearLayout>
И сам код
package ru.alexanderklimov.test;
import ...
public class MainActivity 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;
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// load the image only once
if (imageOriginal == null) {
imageOriginal = BitmapFactory.decodeResource(getResources(),
R.drawable.ring11);
}
// 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.imageView_ring);
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);
}
}
});
}
/**
* 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:
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:
break;
}
return true;
}
}
/**
* @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:
return 180 - Math.asin(y / Math.hypot(x, y)) * 180 / Math.PI;
case 3:
return 180 + (-1 * 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:
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;
}
}
/**
* Rotate the dialer.
*
* @param degrees
* The degrees, the dialer should get rotated.
*/
private void rotateDialer(float degrees) {
matrix.postRotate(degrees, dialerWidth / 2, dialerHeight / 2);
dialer.setImageMatrix(matrix);
}
/**
* 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);
}
}
}
}
Результат
Learn to create a Rotary Dialer application for Android