6

I am using https://github.com/chrisbanes/PhotoView and trying to animate its height.

I use ValueAnimator and update the layout height, so that triggers the internal PhotoViewAttacher and onGlobalLayoutwhich transforms the matrix.

Is there any workaround to prevent scale and y position to be unchanged, like could I somehow update the matrix myself to keep the image Y position and scaleX/scaleY unchanged? Now those are reset to scale 1.0 and y position center of image.

Animation code:

ValueAnimator animator = ValueAnimator.ofInt(start, end).setDuration(300);

animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            mImageView.getLayoutParams().height = (int) animation.getAnimatedValue();
            mImageView.requestLayout();
        }
    });

animator.start();
Niko
  • 8,093
  • 5
  • 49
  • 85

2 Answers2

3

I went through the source code, but haven't tested the following code. From what I can tell, you want to block the call to onGlobalLayout() for the duration of the animation. The following should achieve that:

onAnimationStart():

mPhotoView.getViewTreeObserver()
    .removeOnGlobalLayoutListener((PhotoViewAttacher)mPhotoView.getIPhotoViewImplementation());

onAnimationEnd():

mPhotoView.getViewTreeObserver()
    .addOnGlobalLayoutListener((PhotoViewAttacher)mPhotoView.getIPhotoViewImplementation());

Note thatremoveOnGlobalLayoutListener(OnGlobalLayoutListener) is available for API versions >=16. Before this, you'll use removeGlobalOnLayoutListener(OnGlobalLayoutListener).

onAnimationStart() and onAnimationEnd() callbacks are available by adding a ValueAnimator.AnimatorUpdateListener to your ValueAnimator.

Again, I don't know if this will work - looks like it should.

Edit:

Following is independent of the code above.

Instead of animating the height of PhotoView, you could animate its top & bottom properties. In my tests, animating these properties did not reset the y position, or change the scaleX/scaleY values:

int mOrigImageViewTop, mOrigImageViewBottom;

void crunchImageView() {
    // Hold on to original values
    if (mOrigImageViewTop == 0) {
        mOrigImageViewTop = mImageView.getTop();
        mOrigImageViewBottom = mImageView.getBottom();
    }

    // Top
    ObjectAnimator objectAnimatorTop = ObjectAnimator.ofInt(mImageView, 
            "top", mOrigImageViewTop, 
            mOrigImageViewTop + 200 /*should be calculated dynamically*/);

    // Bottom
    ObjectAnimator objectAnimatorBottom = ObjectAnimator.ofInt(mImageView, 
            "bottom", mOrigImageViewBottom, 
            mOrigImageViewBottom - 200 /*should be calculated dynamically*/);

    AnimatorSet animatorSet = new AnimatorSet();
    animatorSet.playTogether(objectAnimatorTop, objectAnimatorBottom);
    animatorSet.setDuration(5000L);
    animatorSet.start();
}

If you are animating the height to make room for other views above or below the PhotoView, animating top/bottom will not help. In this case, using a FrameLayout to host the PhotoView & other Views, and controlling their visibility may be an option.

Vikram
  • 51,313
  • 11
  • 93
  • 122
  • This approach has multiple problems, first the scale ratio and y position gets resetted when animation ends. Also if you scroll the image view bottom, then animate its height, the display matrix gets properly updated only after the animation. – Niko Dec 01 '15 at 06:44
  • @Niko From your question, I gathered that you don't want the `PhotoView` to update (adjust to the change in `height`) during the animation. Since my assumption was/is wrong, can you explain what the expected behavior is. – Vikram Dec 03 '15 at 12:13
  • Yes I want the height to be adjusted, but I also want the scale and y position to be unchanged. Maybe the explanation was slightly confusing – Niko Dec 03 '15 at 19:25
  • @Niko I have made an edit to my answer above. Will wait for your comment(s) on this. – Vikram Dec 05 '15 at 07:43
  • I am trying, but having problems. My init height is 200dp and then I want to make it 400dp. Animating bottom property doesn't seem to have any impact. – Niko Dec 05 '15 at 11:09
  • If the PhotoView height is set to wrap_content and the bottom set to 200p it almost works, but after animation the image cannot be scrolled anymore because the PhotoView thinks it is already full screen. So I believe the 100% working solution would be playing with the image Matrix. – Niko Dec 05 '15 at 16:30
  • Can you tell me what height and scale type you set in your testing and did the scaling/scrolling work after animating bottom property? – Niko Dec 07 '15 at 06:29
  • @Niko I tried a few different approaches (and have some ideas that I want to try). One thing that is clear to me is: as soon as we touch the `height`, for example: even changing the height by 1 pixel - the image is reset - scaleX/scaleY are not preserved, neither is the y position. So, the issue lies beyond the scope of this library – Vikram Dec 07 '15 at 12:05
  • Yeah, I have worked with this a day now. I have a workaround fix but I have to access private fields and methods for that. – Niko Dec 07 '15 at 12:09
  • @Niko Next, I tried retrieving the display matrix (`getDisplayMatrix()`) before changing the height, and resetting it after changing the `LayoutParams` (`setDisplayMatrix()`). This sort of works - the image is drawn _without_ scaleX, scaleY, yPosition. But, as soon as it is touched/dragged, the image is drawn _with_ scaleX, scaleY and yPosition. This tells me that the library does not account for changes in `height`. – Vikram Dec 07 '15 at 12:10
  • @Niko Animating `bottom` works because it does not _change_ the height. It only changes the view's drawing Rect bounds. Say your original view bounds are `100, 200, 500, 600` corresponding to `left, top, right, bottom`. When animating `bottom`, you are playing with the `600` value. You can set it any value between `[200, 600]` without a problem from the library - the scaleX/scaleY /yPosition are all honored. What you cannot do (and it was sort of confirmed by you) is go below `200` or over `600`. – Vikram Dec 07 '15 at 12:18
  • @Niko Your answer looks good. But, if you can work with the top/bottom approach, it would be the least invasive. – Vikram Dec 07 '15 at 12:23
  • 1
    Proper approach would be to fork the library and add the code there, but this workaround is enough for me. Thanks for your help :) – Niko Dec 07 '15 at 12:26
1

This was more tricky than first thought. The library doesn't seem to have public API's to get this to work, so here is a workaround using private methods and fields. Following solution seems to work with my cases where I can change the actual height of PhotoView and scale + y position keeps unchanged during the animations.

First extend PhotoView and write following code:

// Private fields and methods
private Matrix mBaseMatrix;
private Field mAttacher;
private Method mGetDrawMatrix;
private Method mSetImageViewMatrix;
private Method mCheckMatrixBounds;

...

@Override
protected void init() {
    super.init();

    getViewTreeObserver().removeOnGlobalLayoutListener(
            (ViewTreeObserver.OnGlobalLayoutListener) getIPhotoViewImplementation());

    getViewTreeObserver().addOnGlobalLayoutListener(
            new ViewTreeObserver.OnGlobalLayoutListener() {
                @Override
                public void onGlobalLayout() {
                    updateBaseMatrix();
                }
            });
}

private void updateBaseMatrix() {
    try {
        if (mBaseMatrix == null) {
            mBaseMatrix = getMatrix("mBaseMatrix");
        }

        if (mBaseMatrix != null) {
            mBaseMatrix.setValues(new float[]{
                    1.0f, 0.0f, 0.0f,
                    0.0f, 1.0f, 0.0f,
                    0.0f, 0.0f, 1.0f
            });
        }

        if (mAttacher == null) {
            mAttacher = PhotoView.class.getDeclaredField("mAttacher");
            mAttacher.setAccessible(true);
        }

        if (mGetDrawMatrix == null) {
            mGetDrawMatrix = PhotoViewAttacher.class.getDeclaredMethod("getDrawMatrix");
            mGetDrawMatrix.setAccessible(true);
        }

        Matrix drawMatrix = (Matrix) mGetDrawMatrix.invoke(mAttacher.get(this));

        if (mSetImageViewMatrix == null) {
            mSetImageViewMatrix = PhotoViewAttacher.class
                    .getDeclaredMethod("setImageViewMatrix", Matrix.class);
            mSetImageViewMatrix.setAccessible(true);
        }

        mSetImageViewMatrix.invoke(mAttacher.get(this), drawMatrix);

        if (mCheckMatrixBounds == null) {
            mCheckMatrixBounds = PhotoViewAttacher.class.getDeclaredMethod("checkMatrixBounds");
            mCheckMatrixBounds.setAccessible(true);
        }

        mCheckMatrixBounds.invoke(mAttacher.get(this));
    } catch (Exception e) {
        e.printStackTrace();
    }
}

private Matrix getMatrix(String fieldName) {
    try {
        Field f = PhotoView.class.getDeclaredField("mAttacher");
        f.setAccessible(true);

        PhotoViewAttacher a = (PhotoViewAttacher) f.get(this);

        f = PhotoViewAttacher.class.getDeclaredField(fieldName);
        f.setAccessible(true);

        return (Matrix) f.get(a);
    } catch (Exception e) {
        e.printStackTrace();
    }

    return null;
}
Niko
  • 8,093
  • 5
  • 49
  • 85