Photoview: Zoom get reset when layout change

Created on 2 Apr 2014  路  22Comments  路  Source: Baseflow/PhotoView

Hi,

I've used PhotoView with an activity full screen. I hide/show the actionbar on user click.

When that happens any zoom get reset to the default, same goes for when I rotate the device.

Is this expected behavior?
I've scrambled the code in PhotoViewAttacher and saw the onGlobalLayout() method which apparently is there to recompute the zoom.

Unfortunately my experience with custom view is sparse and I couldn't really get the grip of what's going on in there at a quick reading.

I'm using Android 4.4 Kitkat and library version 1.2.2 version

To intercept the touch events I created a custom ViewGroup extending the FrameLayout and pass the MotionEvent(s) to my a gesture detector using onInterceptEvent (I always return false, do not intercept) and when I identify a tap I toggle the actionbar show state.

Of course this also happen when you rotate the device (orientation change).

bug

All 22 comments

Yes, and that's your responsibility. When you rotate the device, Android recreates all your views from scratch. You're reponsible to store any and all state information you have and restore your views as you want them on rotation. In this particular case, use your activity's onSaveInstanceState() to store all your PhoneView zoom settings and restore them when the Activity is recreated. Because this is a constant grief for beginner Android developers, googling for onSaveInstanceState() will bring up a large amount of info.

Note that some people would recommend disabling rotation behavior via the application manifest. This is plainly wrong; you have to solve it the proper way. Also note that while rotation is the most obvious one to trigger it, the same situation can arise without rotation (this is why disabling is worse than a hack, it solves nothing), for instance, when the user gets a phone call or switches temporarily to another application. Or whenever the system gets low on resources and Android decides to kill your app without thinking too much about is.

@deakjahn views are supposed to store they're state.

I'm not a beginner and that doesn't help either cause I handled the orientation change manually so the onSaveInstanceState is not even called when I rotate the device.

This is about layout change.
This is something the library has to do.
There is already code in the library for that very reason but it apparently doesn't work.

Well, then, add to it. ;-))

In my own work, I never expected it to retain zoom level because upon rotation, the actual size of the view changes, so I need to recalculate everything, anyway. Setting back to the numerically same zoom level might not be the display actually expected. But after all, let the dev reply whether this is working as intended or indeed a bug, I don't know.

@deakjahn that's good news.

From what you say you have the code to save the zoom state, recompute with the new layout and restore it, can you share it? Maybe a public gits linked here.

If it works I can apply it in my onConfigurationChange and handle onLayout configuration in my activity.

Unfortunately, I don't think my code would help you, and for two reasons. First, I don't save anything related to zoom at all, I just let onCreate do the same initial calculations again when called to do so, with the currently applicable metrics. Second, my initial calculations are rather different from what you would expect in a similar case. The reason is that my app does way more than mere picture display. It is part of a real life printing application where pictures have tangible dimensions in millimeters and I use PhotoView to allow the users to manipulate these values. Hence, I modify the size of the view itself to reproduce the real life aspect ratio and use heavily modified calculated display scales to map the real picture to the actual view size. All based on many external values provided to the displaying activity (actually, a DialogFragment) from outside.

I never ever needed to handle configuration changes myself, I always use the "standard" way. So, in your case, I guess I'd think of keeping track of zoom values by implementing onMatrixChanged() and use that when you do the onLayout(). But this is hardly a new idea to you, so there might be some drawbacks or problems, hence your issue report in the first place...

But still, it was interesting and challenging. :-) I tried this rapidly now and at first sight, it seems to work all right:

private float imageScale;

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
  final View layout = inflater.inflate(R.layout.edit_image, container, false);

  if (savedInstanceState == null)
    imageScale = getArguments().getFloat("scale");
  else
    imageScale = savedInstanceState.getFloat("scale");
...
  final PhotoView image = (PhotoView) layout.findViewById(R.id.image);
  image.setOnMatrixChangeListener(new PhotoViewAttacher.OnMatrixChangedListener() {
    @Override
    public void onMatrixChanged(RectF rect) {
      imageScale = image.getScale();
    }
  });
  image.setScale(imageScale);
...
}

@Override
public void onSaveInstanceState(Bundle outState) {
  outState.putFloat("scale", imageScale);
  super.onSaveInstanceState(outState);
}

But, as I said, this was only a quick check, so there might be some issues and ramifications...

@deakjahn I investigated a little after your effort.

I also need the image to keep the focal point.

In my tests playing with the image scale didn't gave me any result.
I had to play directly with the Attacher.

My Image is coming from a remote source and is loaded asynchronously so I have to set those values only when the image is set.
I have a cache but it still goes in background in some occasion.

To do so I extended the PhotoView and created a listener to know when the image is being set to the PhotoView.

I've tried to compute the focal point but I probably am doing some math error because rotating the keep the correct scale but do not position it correctly (I expect the previous center of the image to be in the center again)

My code looks like this:

@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);
    mImageView = (ZoomablePhotoView) view.findViewById(R.id.photo);
    mImageView.setOnBitmapChangeListener(this);

    if (savedInstanceState == null) {
        mImageScale = 1.0f;
        mImageFocalX = 0f;
        mImageFocalY = 0f;
    } else {
        mImageScale = savedInstanceState.getFloat(STAT_IMAGE_SCALE);
        mImageFocalX = savedInstanceState.getFloat(STAT_IMAGE_FOCAL_X);
        mImageFocalY = savedInstanceState.getFloat(STAT_IMAGE_FOCAL_Y);
    }
    mAttacher = new PhotoViewAttacher(mImageView);
    mAttacher.setZoomable(true);
}

@Override
public void onDestroyView() {
    mImageView.setOnBitmapChangeListener(null);
    super.onDestroyView();
}

@Override
public void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    float scale = Math.max(mAttacher.getMinimumScale(), Math.min(mAttacher.getScale(), mAttacher.getMaximumScale()));
    RectF original = new RectF();
    original.top = mImageView.getTop();
    original.bottom = mImageView.getBottom();
    original.left = mImageView.getLeft();
    original.right = mImageView.getRight();
    RectF relative = mAttacher.getDisplayRect();

    float focalX = (0.5f * (original.left + original.right) - relative.left) / scale;
    float focalY = (0.5f * (original.top + original.bottom) - relative.top) / scale;
    outState.putFloat(STAT_IMAGE_SCALE, scale);
    outState.putFloat(STAT_IMAGE_FOCAL_X, focalX);
    outState.putFloat(STAT_IMAGE_FOCAL_Y, focalY);
}

@Override
public void onSetBitmap(ZoomablePhotoView view, Bitmap bitmap) {
    if (mAttacher != null) {
        mAttacher.setScale(mImageScale, mImageFocalX, mImageFocalY, view.getWindowToken() != null);
    }
}

I'm trying to understand what's wrong in my code... but I still think this is something that should be taken into account by the library itself :)

By the way the changelog say:

Support for restoring state (zoom/position) of underlying ImageView

so this should actually work out of the box :/

I've just created another issue a couple of hours ago, #171. Would that help, maybe? I had some seemingly similar issue with the focal point.

When I use ObjectAnimation and then PhotoView will reset, Because it use 'addOnGlobalLayoutListener'. So I invoke removeOnGlobalLayoutListener when I animation start.

@smarek I didn't even noticed this was assigned to me. This bug is so old that I don't even remember for which project I needed this or how I did in the end.
Still think this is a must-have feature for a photo view library.

@danielesegato could you give any help to solve this problem?

@pavei I'm sorry, so much time has passed that I don't even remember the project for which I used this. And I do not remember what I did at all. Can't help you

Is this still an issue that people would like resolved? If so, I could take a look at it, I've spent a lot of time playing with it and wouldn't mind perfecting it for everyone: http://stackoverflow.com/questions/31482007/android-photoview-keep-zoom-after-orientation-change

I'm not using this library anymore because of this issue.
So yes, I would see it solved.

This may work.

``` @Override
protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = super.setFrame(left, top, right, bottom);
if (changed && mAttacher!=null) {
mAttacher.update();
}
return changed;

 }```

any one found the solution.. i am still in the same boat! it happens with android 7.
Worked fine with android 6

Any updates on this? Thank you

In case someone want to replace the image and preserve the zoom (position and scale), you could change the update() method in the PhotoViewAttacher like this:

public void update() {
        if (updateEnabled) { // a boolean that you can set to false to preserve the zoom (position and scale)
            if (mZoomEnabled) {
                // Update the base matrix using the current drawable
                updateBaseMatrix(mImageView.getDrawable());
            } else {
                // Reset the Matrix...
                resetMatrix();
            }
        }
    }

@maxcorrads , can you show full sample, how to save position and scale, when you replace image?

@GrishinSergey to preserve the zoom (position and scale) just replace the update() method in the PhotoViewAttacher like this https://github.com/chrisbanes/PhotoView/issues/168#issuecomment-431873864

updateEnabled is just a boolean that you have to set to false once you have set the image:

private CustomPhotoView image; //CustomPhotoView is just a PhotoViewAttacher with the update method replaced with the one above and the boolean "updateEnabled"

image.setImageBitmap(aBitmap);
image.getAttacher().setUpdateEnabled(false);

That was enough for my needs, there is no need to get position and scale.

@maxcorrads, I tried to set custom attacher -- this field without setter. Also I tried to extends from PhotoView and change init() method. This also impossible, because some methods, which used in it are private. Then I tried just left method update() empty (code in it will never executed cuz bool flag). But this variant also works not so properly.
Maybe, you mean to change it in sources of library and attach them?

Was this page helpful?
0 / 5 - 0 ratings