0

I'm trying to make a simple image editor. At the beginning I've thought that it'll be a good idea to simply save view state as Bitmap but, as it turned out, there is a wide range of screen resolutions and that leads to huge quality (and memory usage) fluctuations.

Now I'm trying to make a module that renders views state translated to desired resolution.

In the code below I'm trying to recreate current state of the views in canvas:

    Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.id.test_1_1);
    bitmap = Bitmap.createScaledBitmap(bitmap, parentView.getMeasuredWidth(), parentView.getMeasuredHeight(), true);

    Canvas canvas = new Canvas(bitmap);
    Paint paint = new Paint();
    for (View rootView : addedViews) {
        ImageView imageView = rootView.findViewById(R.id.sticker);

        float[] viewPosition = new float[2];
        transformToAncestor(viewPosition, parentView, imageView);

        Bitmap originalBitmap = ((BitmapDrawable) imageView.getDrawable()).getBitmap();
        Matrix adjustMatrix = new Matrix();
        adjustMatrix.postTranslate(viewPosition[0], viewPosition[1]);
        adjustMatrix.postScale(
                rootView.getScaleX(),
                rootView.getScaleY(),
                rootView.getWidth() / 2,
                rootView.getHeight() / 2);
        adjustMatrix.postRotate(rootView.getRotation(),
                rootView.getWidth() / 2,
                rootView.getHeight() / 2);

        canvas.drawBitmap(originalBitmap, adjustMatrix, paint);
    }

transformToAncestor function is from here.

public static void transformToAncestor(float[] point, final View ancestor, final View descendant) {
    final float scrollX = descendant.getScrollX();
    final float scrollY = descendant.getScrollY();
    final float left = descendant.getLeft();
    final float top = descendant.getTop();
    final float px = descendant.getPivotX();
    final float py = descendant.getPivotY();
    final float tx = descendant.getTranslationX();
    final float ty = descendant.getTranslationY();
    final float sx = descendant.getScaleX();
    final float sy = descendant.getScaleY();

    point[0] = left + px + (point[0] - px) * sx + tx - scrollX;
    point[1] = top + py + (point[1] - py) * sy + ty - scrollY;

    ViewParent parent = descendant.getParent();
    if (descendant != ancestor && parent != ancestor && parent instanceof View) {
        transformToAncestor(point, ancestor, (View) parent);
    }
}

(author wrote a note that his function does not support rotation, but there's not much rotation in my example so I don't think that important for now).

My problem is:

Image generated via drawing views state Image generated via views to image function

First image is generated via saving the parent view state. Second one is generated by translating views position, rotation and scale onto canvas. As you can see, on the canvas, not scaled stickers are positioned properly, but scaled are incorrectly positioned.

How to position those scaled views properly?

Marcin Wróblewski
  • 811
  • 1
  • 10
  • 25
  • what do you need `transformToAncestor` method at all? is your goal to create some kind of layers (in your case drawn with white stars) - each layer scaled, translated and possibly rotated? – pskink Jul 17 '18 at 09:53
  • I'm using this method to translate each star's position and then I'm scaling and rotating it. Is there a better way to do this? My goal is to render image based on views position, layers are not neccesary. – Marcin Wróblewski Jul 17 '18 at 09:58
  • i dont get it: you are using both `ImageView`s and `Canvas#drawBitmap`, what for? just use a custom view and `Canvas#drawBitmap` and you will control everything with almost no extra logic – pskink Jul 17 '18 at 09:59
  • like this https://stackoverflow.com/a/21657145/2252830 for example – pskink Jul 17 '18 at 10:02
  • User is editing image via manipulating ImageViews on the screen. After user is done, I want to recreate effect on Canvas to get a possibility to control output image resolution. In the code I provided I'm looping through views added to editor (stars) and I'm trying to place actual Bitmaps on canvas. – Marcin Wróblewski Jul 17 '18 at 10:08
  • how is he manipulating `ImageView`s? why not to manipulate `Bitmap`s itself? did you see the link i posted in my previous comment? – pskink Jul 17 '18 at 10:17
  • Yes, I saw the link. My editor is based on [this library](https://github.com/burhanrashid52/PhotoEditor). It's default way to save an edited image is to save state of the parent. I want to make the render function that only recreates views state. That will allow me to fully control output image resolution. – Marcin Wróblewski Jul 17 '18 at 10:24
  • 1
    personally i would fork that library and remove any `ImageView`s that it uses and replace with primitive, low level `Bitmap`s - if you want to use them both you will never get pixel perfect solution – pskink Jul 17 '18 at 10:39

1 Answers1

0

I've managed to fix the issue myself. It turned out my solution was nearly OK but I did not took into consideration that my manipulation of a matrix does change the arrangement of the original points, so my

rootView.getWidth() / 2,
rootView.getHeight() / 2

is no longer applicable as a center of the view after calling Matrix.postScale or Matrix.postRotation.

I wanted to:

  • apply scale with pivot on top left corner,
  • apply rotation with pivot on the center of the view.

Given the assumptions, here's the working code:

    // setup variables for sizing and transformation
    float position[] = new float[2];
    transformToAncestor(position, rootView, imageView);
    float desiredRotation = imageView.getRotation();
    float sizeDeltaX = imageView.getMeasuredWidth() / (float) imageBitmap.getWidth();
    float sizeDeltaY = imageView.getMeasuredHeight() / (float) imageBitmap.getHeight();
    float desiredScaleX = imageView.getScaleX() * sizeDeltaX * scaleX;
    float desiredScaleY = imageView.getScaleY() * sizeDeltaY * scaleY;
    float imageViewWidth = imageView.getMeasuredWidth() * imageView.getScaleX();
    float imageViewHeight = imageView.getMeasuredHeight() * imageView.getScaleY();

    float rootViewWidth = rootView.getMeasuredWidth();
    float rootViewHeight = rootView.getMeasuredHeight();
    float percentXPos = position[0] / rootViewWidth;
    float percentYPos = position[1] / rootViewHeight;
    float percentXCenterPos = (position[0] + imageViewWidth/2)
            / rootViewWidth;
    float percentYCenterPos = (position[1] + imageViewHeight/2)
            / rootViewHeight;

    float desiredPositionX = background.getWidth() * percentXPos;
    float desiredPositionY = background.getHeight() * percentYPos;
    float desiredCenterX = background.getWidth() * percentXCenterPos;
    float desiredCenterY = background.getHeight() * percentYCenterPos;

    // apply above variables to matrix
    Matrix matrix = new Matrix();
    float[] points = new float[2];
    matrix.postTranslate(
            desiredPositionX,
            desiredPositionY);

    matrix.mapPoints(points);
    matrix.postScale(
            desiredScaleX,
            desiredScaleY,
            points[0],
            points[1]);

    matrix.postRotate(
            desiredRotation,
            desiredCenterX,
            desiredCenterY);

    // apply matrix to bitmap, then draw it on canvas
    canvas.drawBitmap(imageBitmap, matrix, paint);

As you can see, the mapPoints method was the answer for my question - it simply returns points after tranformation.

Marcin Wróblewski
  • 811
  • 1
  • 10
  • 25