Redraw multiple Paths at same positions from previ

2019-03-27 16:58发布

问题:

Based on my previous question of "How to create a BottomBar as StickyBottomCaptureLayout in camera2 Android api?", I created a layout with a StickyBar (SB) which is always locked above/near the system bar. I set the default positions and coordinates of the SB and the other layout in onLayout() (exactly as my answer).

The upper layout is a simple custom DrawView which has an ArrayList of Paths drew by the user. When the device rotates, it recalls onDraw() and calls several times canvas.drawPath(). However, the Paths are redrew with the same coordinates as before but on a different position and layout size. These screenshots demonstrate the actual behavior:

left: portrait - right: landscape

But I want to keep the same coordinates and positions when the orientation changed, like this:

left: same portrait as above - right: landscape with "portrait" coordinates

Locking my activity with android:orientation="portrait" is not the expected solution. I use android:configChanges="orientation" and an OrientationListener to detect the rotation and prevent the total recreation of the Activity.

  • I tried to set other different positions in onLayout() but obviously, this is not the right way.
  • I previously tried to transform the multiple Paths like this:

    for (Path path : mPathList) {
        Matrix matrix = new Matrix();
        RectF bounds = new RectF();
        path.computeBounds(bounds, true);
    
        // center points to rotate
        final float px = bounds.centerX();
        final float py = bounds.centerY();
        // distance points to move 
        final float dx; // ?
        final float dy; // ?
        /** I tried many calculations without success, it's 
            not worth to paste these dumb calculations here... **/
    
        matrix.postRotate(rotation, px, py); // rotation is 90°, -90° or 0
        matrix.postTranslate(dx, dy); // ?
        path.transform(matrix);
    }
    
  • I also tried to rotate the canvas as follows:

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.save();
        canvas.rotate(rotation); // rotation is 90°, -90° or 0
    
        canvas.drawColor(mDrawHelper.getBackgroundViewColor());
        for (int i=0; i < mPathList.size(); i++) {
           canvas.drawPath(mPathList.get(i), mPaintList.get(i));
        }
        if (mPath != null && mPaint != null)
           canvas.drawPath(mPath, mPaint);
    
        canvas.restore();
    }  
    

Anyway, I tried many manipulations but nothing seems to work in this specific case. Does someone have a bright and fabulous idea to share which can lead me in the right direction?
Thanks in advance for the help.

回答1:

Update: Methodology has been simplified and made easier to follow. The sample app has been updated.

I think I understand what you are trying to do. You want the graphic to maintain its relationship with the StickyCaptureLayout that you have defined. I like the approach using Path and Matrix transformations.

After determining the rotation that the device has undergone, create a Matrix to do the appropriate rotation and rotate about the center of the graphic.

mMatrix.postRotate(rotationDegrees, oldBounds.centerX(), oldBounds.centerY());

Here oldBounds is the bounds of the graphic before location. We will use this to determine margins on the rotated graphic. Go ahead and do the rotation

mPath.transform(mMatrix)

The graphic has been rotated but its position is not correct. It is in the old position but rotated. Create a translation Matrix to move the Path to the appropriate location. The actual computation is dependent upon the rotation. For a 90 degree rotation the computation is

transY = -newBounds.bottom; // move bottom of graphic to top of View
transY += getHeight(); // move to bottom of rotated view
transY -= (getHeight() - oldBounds.right); // finally move up by old right margin
transX = -newBounds.left; // Pull graphic to left of container
transX += getWidth() - oldBounds.bottom; // and pull right for margin

where transY is the Y-translation and transX is the X-translation. oldBounds is the pre-rotation bounds and newBounds is the post-rotation bounds. Important to note here is that getWidth() will give you the "old" View height and getHeight() will give you the old View width.

Here is a sample program that accomplishes what I have described above. A couple of graphics follow showing a 90 degree rotation using this sample app.

Demo app

package com.example.rotatetranslatedemo;

import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.os.Bundle;
import android.view.Display;
import android.view.Surface;
import android.view.View;
import android.view.WindowManager;

public class MainActivity extends Activity {

    private DrawingView dv;
    private Paint mPaint;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        dv = new DrawingView(this);
        setContentView(dv);
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setDither(true);
        mPaint.setColor(Color.GREEN);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeJoin(Paint.Join.ROUND);
        mPaint.setStrokeCap(Paint.Cap.ROUND);
        mPaint.setStrokeWidth(12);
    }

    public class DrawingView extends View {

        private Bitmap mBitmap;
        private Path mPath;
        private Paint mBitmapPaint;
        Context context;
        private Paint paint;
        Matrix mMatrix = new Matrix();
        RectF oldBounds = new RectF();
        RectF newBounds = new RectF();

        public DrawingView(Context c) {
            super(c);
            context = c;
            mBitmapPaint = new Paint(Paint.DITHER_FLAG);
            paint = new Paint();
            paint.setAntiAlias(true);
            paint.setColor(Color.BLUE);
            paint.setStyle(Paint.Style.STROKE);
            paint.setStrokeJoin(Paint.Join.MITER);
            paint.setStrokeWidth(4f);
        }

        @Override
        protected void onSizeChanged(int w, int h, int oldw, int oldh) {
            super.onSizeChanged(w, h, oldw, oldh);

            mBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
        }

        @Override
        protected void onDraw(Canvas canvas) {
            Display display = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE))
                    .getDefaultDisplay();
            int rotationDegrees = 0;
            float transX = 0;
            float transY = 0;

            super.onDraw(canvas);

            canvas.drawBitmap(mBitmap, 0, 0, mBitmapPaint);

            // Determine the rotation of the screen.
            switch (display.getRotation()) {
                case Surface.ROTATION_0:
                    break;

                case Surface.ROTATION_90:
                    rotationDegrees = 270;
                    break;

                case Surface.ROTATION_180:
                    rotationDegrees = 180;
                    break;

                case Surface.ROTATION_270:
                    rotationDegrees = 90;
                    break;

                default:
                    rotationDegrees = 0;
                    break;
            }

            if (mPath == null) { // Just define what we are drawing/moving
                mPath = setupGraphic();
            }

            // Reposition the graphic taking into account the current rotation.
            if (rotationDegrees != 0) {
                mMatrix.reset();
                // Rotate the graphic by its center and in place.
                mPath.computeBounds(oldBounds, true);
                mMatrix.postRotate(rotationDegrees, oldBounds.centerX(), oldBounds.centerY());
                mPath.transform(mMatrix);
                // Get the bounds of the rotated graphic
                mPath.computeBounds(newBounds, true);
                mMatrix.reset();
                if (rotationDegrees == 90) {
                    transY = -newBounds.bottom; // move bottom of graphic to top of View
                    transY += getHeight(); // move to bottom of rotated view
                    transY -= (getHeight() - oldBounds.right); // finally move up by old right margin
                    transX = -newBounds.left; // Pull graphic to left of container
                    transX += getWidth() - oldBounds.bottom; // and pull right for margin
                } else if (rotationDegrees == 270) {
                    transY = -newBounds.top; // pull top of graphic to the top of View
                    transY += getHeight() - oldBounds.right; // move down for old right margin
                    transX = getWidth() - newBounds.right; // Pull to right side of View
                    transX -= getHeight() - oldBounds.right; // Reestablish right margin
                }
                mMatrix.postTranslate(transX, transY);
                mPath.transform(mMatrix);
            }
            canvas.drawPath(mPath, mPaint);
        }

        // Define the graphix that we will draw and move.
        private Path setupGraphic() {
            int startX;
            int startY;
            final int border = 20;
            Path path;

            if (getHeight() > getWidth()) {
                startX = getWidth() - border - 1;
                startY = getHeight() - border - 1;
            } else {
                startX = getHeight() - border - 1;
                startY = getWidth() - border - 1;
            }
            startX = startX - 200;

            Pt[] myLines = {
                    new Pt(startX, startY),
                    new Pt(startX, startY - 500),

                    new Pt(startX, startY),
                    new Pt(startX - 100, startY),

                    new Pt(startX, startY - 500),
                    new Pt(startX - 50, startY - 400),

                    new Pt(startX, startY - 500),
                    new Pt(startX + 50, startY - 400),

                    new Pt(startX + 200, startY),
                    new Pt(startX + 200, startY - 500)
            };

            // Create the final Path
            path = new Path();
            for (int i = 0; i < myLines.length; i = i + 2) {
                path.moveTo(myLines[i].x, myLines[i].y);
                path.lineTo(myLines[i + 1].x, myLines[i + 1].y);
            }

            return path;
        }

        private static final String TAG = "DrawingView";

    }

    // Class to hold ordered pair
    private class Pt {
        float x, y;

        Pt(float _x, float _y) {
            x = _x;
            y = _y;
        }
    }
}

Portrait

Landscape



回答2:

Your solution #2 is almost correct. All you need to do is translate your canvas appropriately.

Assuming that rotation is declared as int and may be only 90, -90 or 0, you need to replace this line:

canvas.rotate(rotation); // rotation is 90°, -90° or 0

by the following code:

if (rotation == 90) {
    canvas.translate(canvas.getWidth(), 0);
    canvas.rotate(90);
} else if (rotation == -90) {
    canvas.translate(0, canvas.getHeight());
    canvas.rotate(-90);
}

This will work. I can set up a demo project if needed.



回答3:

Instead of implementing a solution that is very specific to your problem, you can just implement a more generic one. For example a layout that will rotate everything what is inside, which in my opinion is much more elegant.

public class RotatedLayout extends FrameLayout {

    public RotatedLayout(Context context) {
        super(context);
    }

    ...

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int rotation = 0;
        boolean swapDimensions = false;
        int translationX = 0;
        int translationY = 0;

        final Display display = ((WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
        switch (display.getRotation()) {
            case Surface.ROTATION_0:
                rotation = 0;
                break;
            case Surface.ROTATION_90:
                rotation = -90;
                swapDimensions = true;
                break;
            case Surface.ROTATION_180:
                rotation = 180;
                break;
            case Surface.ROTATION_270:
                rotation = 90;
                swapDimensions = true;
                break;
        }

        if (swapDimensions) {
            final int width = MeasureSpec.getSize(widthMeasureSpec);
            final int height = MeasureSpec.getSize(heightMeasureSpec);
            translationX = (width - height) / 2;
            translationY = (height - width) / 2;
            final int tmpMeasureSpec = heightMeasureSpec;
            heightMeasureSpec = widthMeasureSpec;
            widthMeasureSpec = tmpMeasureSpec;
        }
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        setTranslationX(translationX);
        setTranslationY(translationY);
        setRotation(rotation);
    }
}

This layout is rather straightforward. It forces itself to be measured with swapped dimensions if displayed in landscape mode. It doesn't care what is inside, so you can put everything there, also a regular interface. After measuring itself (and children) with swapped MeasureSpecs it rotates itself and translates, using view properties, to fit the new position. As a bonus of using view properties - touch events works just fine and this button can be pressed as usual.

In portrait orientation:

Rotated to the left:

Problem with onConfigurationChanged

Although this layout will always draw itself in correct orientation, there must be some event that will cause it to be re-drawn. This may be a problem if you rely only on onConfigurationChanged. In your case Activity can react on changes from landscape to portrait and portrait to landscape. But there is no event sent when switching directly from:

  • portrait orientation to reverted portrait (if you have the reversed portrait enabled in your AndroidManifest) - marked in blue.
  • landscape orientation to reversed landscape - marked in red

Please keep in mind that such direct orientation swapping to the reversed orientation is a normal interaction on regular device, it is not something artificial that you can do on emulator only.

There is no standard event sent that will cause views to redraw themselves - no onConfigurationChanged, onMeasure, onLayout, onDraw etc. is invoked. System just rotates everything for you (without even redrawing it) and it will result in wrong rotation of the view RotatedLayout had no changes to correct it. So be aware that you have to handle this case.

You can see it here in an answered by Dianne Hackborn.

This is simply not a configuration change. There is no notification the platform provides for when it does this, because it is invisible to the environment the app is in.

To solve this problem you would have to use SensorManager and register OrientationEventListener to determine when to refresh your view instead of relying on onConfigurationChanged method.