vxh.viet
4/13/2016 - 3:50 AM

How to show half of a rotating wheel Android? + Reset it to original position.

How to show half of a rotating wheel Android? + Reset it to original position.

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();
    }