Source: my own question and aswer on StackOverflow
Question: How to show only half of a roatating wheel?
Answer:
- Simple answer for simple case we dont need to create custom imageview SOURCE.
First we would want tooverride the onDraw
method of our customized ImageView
to draw the image using an updated Matrix
on touch event. This guarantee performance as the image doesn't get redrawn everytime.
Next the important part is basing all of our calculation on the ImageView
origin, not the screen's origin, like in the following picture:
The code is as the following:
public class MyWheelView extends ImageView {
private static Bitmap imageOriginal, imageScaled;
private static Matrix matrix;
private static Paint paint;
private int mWheelHeight, mWheelWidth;
private static final String TAG = "TestWheelView3";
public MyWheelView(Context context, AttributeSet attrs){
super(context, attrs);
// load the image only once
if (imageOriginal == null) {
imageOriginal = BitmapFactory.decodeResource(getResources(), R.drawable.protractor_wheel);
}
// 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();
}
//initialize the paint only once
if(paint == null){
paint = new Paint();
}
setOnTouchListener(new MyOnTouchListener());
getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
// method called more than once, but the values only need to be initialized one time
if (mWheelHeight == 0 || mWheelWidth == 0) {
mWheelHeight = getHeight();
mWheelWidth = getWidth();
// resize
Matrix resize = new Matrix();
resize.postScale((float)Math.min(mWheelWidth, mWheelHeight) / (float)imageOriginal.getWidth(), (float)Math.min(mWheelWidth, mWheelHeight) / (float)imageOriginal.getHeight());
imageScaled = Bitmap.createBitmap(imageOriginal, 0, 0, imageOriginal.getWidth(), imageOriginal.getHeight(), resize, false);
setImageBitmap(imageScaled);
// center the image on x axis and move it upward on y axis
float translateX = mWheelWidth / 2 - imageScaled.getWidth() / 2;
float translateY = -imageScaled.getHeight()/2; //edit show how much of the image will get displayed
matrix.postTranslate(translateX, translateY);
setImageMatrix(matrix);
}
}
});
}
/**
* Simple implementation of an {@link View.OnTouchListener} for registering the mWheel's touch events.
*/
private class MyOnTouchListener implements View.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());
double delta = currentAngle - startAngle;
updateMatrix((float)delta);
invalidate();
Log.d(TAG, "Delta: " + delta);
startAngle = currentAngle;
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}
}
private void updateMatrix(float delta){
matrix.postRotate(delta, getWidth()/2, 0f);
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawBitmap(imageScaled, matrix, paint);
}
/**
* @return The angle of the unit circle with the image view's center
*/
private double getAngle(double xTouch, double yTouch) {
double x = xTouch - (imageScaled.getWidth() / 2d);
double y = imageScaled.getHeight() - yTouch - (imageScaled.getHeight() / 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;
}
}
}
Improvement:
My first solution work fine and all if you just want to display half of the rotating wheel in your layout. However, the downside is unused space on your ImageView
block other Views and if you has a complex layout, other View
below this ImageView
can't receive touch event. With this improved solution, you can limit the size of the ImageView
so it wont block other View as in the Picture below.
In your layout.xml
<.MyWheelView
android:id="@+id/some_id"
android:layout_width="match_parent"
android:layout_height="100dp" //change to suit your need
android:layout_centerInParent="true"
android:src="@drawable/protractor_wheel"
/>
In the onGlobalLayout()
method, modify as the following:
@Override
public void onGlobalLayout() {
// method called more than once, but the values only need to be initialized one time
if (mWheelHeight == 0 || mWheelWidth == 0) {
mWheelHeight = getHeight();
mWheelWidth = getWidth();
// resize
Matrix resize = new Matrix();
resize.postScale((float)Math.max(mWheelWidth, mWheelHeight) / (float)imageOriginal.getWidth(), (float)Math.max(mWheelWidth, mWheelHeight) / (float)imageOriginal.getHeight());
imageScaled = Bitmap.createBitmap(imageOriginal, 0, 0, imageOriginal.getWidth(), imageOriginal.getHeight(), resize, false);
setImageBitmap(imageScaled);
// center the image on x axis and move it upward on y axis
float translateX = mWheelWidth / 2 - imageScaled.getWidth() / 2;
float translateY = - 0.75f * imageScaled.getHeight(); //edit show how much of the image will get displayed (in my case I move 75% of my image upward)
matrix.postTranslate(translateX, translateY);
setImageMatrix(matrix);
//calculate pivotY only once
pivotY = 0.25f * imageScaled.getHeight() - (float)imageScaled.getHeight()/2;
}
}
});
And then in the updateMatrix()
we do this:
private void updateMatrix(float delta){
matrix.postRotate(delta, getWidth()/2, pivotY);
}
Improvement 2:
In other to position this rotating wheel below another element we use the setY(float y)
method. This will set the position of the wheel according to the parent container not the screen's origin.
First we center our rotating wheel and the other element in the container.
<FrameLayout
android:id="@+id/ucrop_frame"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.yalantis.ucrop.view.UCropView
android:id="@+id/ucrop"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:alpha="0"
android:layout_gravity="center"/>
<com.example.viet.myappdemo.ProtractorWheelView
android:id="@+id/protractor_wheel_view"
android:layout_width="match_parent"
android:layout_height="100dp"
android:src="@drawable/protractor_wheel"
android:layout_gravity="center"/>
</FrameLayout>
And then position our wheel:
mProtractorWheelView.setY((float)mUCropView.getHeight()/2 + mRectF.height()/2); //mRectF is the small cropping rectangle box
and we get this:
Improvement 3:
To reset the protractor to initial position, we save the matrix and recall it again.
private Matrix resetMatrix
@Override
public void onGlobalLayout() {
// method called more than once, but the values only need to be initialized one time
if (mWheelHeight == 0 || mWheelWidth == 0) {
...
//store this matrix
resetMatrix = new Matrix(matrix); //important deep copy the matrix
}
}
});
and create a public method to add the resetMatrix
back to the current matrix
:
public void resetMatrix() {
matrix = new Matrix(resetMatrix);
}
and the call it on the reset action:
private void resetRotation() {
//reset protractor
mMyWheelView.resetMatrix();
mMyWheelView.invalidate();
}