subsampling-scale-image-view
subsampling-scale-image-view copied to clipboard
Incorrect view (re)measurement when source image is set and MeasureSpec != EXACTLY
First of all, thanks for this great library and your efforts in maintaining this! We've been using it for a while now to provide high-res newspaper-like content to users.
Recently, we hit a snag, where the SSI did not resize as expected, which seems to be related to the way onMeasure is implemented. Specifically the part where it uses sWidth and sHeight to calculate the view size if the MeasureSpec mode is not EXACTLY. I feel like this is not taking into account all constraints, but I may very well be overlooking certain use cases I'm not familiar with.
Expected behaviour
The view will respect the incoming size constraints imposed by its parent. That is, if the parent gives the view a maximum size (MeasureSpec.AT_MOST), the view will not exceed the given width and height constraints.

Blue: view bounds
Red: visible part of image (w/ SCALE_TYPE_CENTER_INSIDE)
Actual behaviour
The view does not respect MeasureSpec.AT_MOST constraints. Example: let's say we have a portrait image in a landscape orientation. If heightSpecMode == MeasureSpec.AT_MOST and sWidth > 0 && sHeight > 0, the view will initially set itself to its parent size, but then update its measurements based on the size and aspect ratio of the source image. If that source image is large (which it generally is in the context of this library ;) ), the updated (height) measurement will exceed that of the parent, even though the parent told the view to be AT_MOST a certain size.

Blue: view bounds (no bottom bound, because it's waaay off screen)
Red: visible part of image (w/ SCALE_TYPE_CENTER_INSIDE)
Steps to reproduce
I have yet to create a sample project with the bare minimum to reproduce this.
Interesting to note though: the observed behaviour also depends on which ViewGroup the view is a child of. In other words, different ViewGroups seem to respond differently to child views sizing themselves beyond its parent's bounds.
FrameLayout: does not care. If the view sizes itself beyond its parent's bounds, it will be added to the view tree with that size. Any content beyond the parent's bounds is simply clipped. The visual effect can be seen under "Actual Behaviour".ConstraintLayout: enforces a second measurement, withMeasureSpec.EXACTLY. With the current implementation, this will cause the view to size itself to its parent's size. The visual effect can be seen under "Expected Behaviour".
Affected devices
This should not be device and/or OS version specific. I tested on Android 10, with a Nokia 7.1 and Pixel 2XL.
Affected images
Pick any image with dimensions larger than the view size.
Code
If I replace lines 647-648 with naive logic to prevent the view from sizing itself bigger than its parent, I also get the expected behaviour with a FrameLayout:
width = Math.min(width, parentWidth);
height = Math.min(height, parentHeight);
This is just for illustration purposes of course, because I suspect a proper solution should explicitly handle MeasureSpec.AT_MOST.
This is the intended behaviour. I think the container you've placed the view in is either a ScrollView or is not constrained to below the header and above the footer. The view itself probably may also have height = wrap_content.
When either height or width is not constrained, the view will match its aspect ratio to that of the image. It's fairly rare to use the view with unbounded height or width, and it's one of those things where the default won't suit everyone, but you can override onMeasure to get the behaviour you need.
Thanks for the quick reply!
I think the container you've placed the view in is either a ScrollView or is not constrained to below the header and above the footer.
I double checked this to be sure, but that's not the case. There's no ScrollView in the hierarchy and the SubsamplingScaleImageView and all its ancestors are all set to match_parent height.
When either height or width is not constrained, the view will match its aspect ratio to that of the image.
That totally makes sense, but specMode != MeasureSpec.EXACTLY is not the same thing as 'unconstrained', right? I mean, MeasureSpec.AT_MOST is still a constraint: it imposes a maximum size. The "actual behaviour" I'm seeing is caused by that maximum size being completely ignored if the view already has an image source set.
Looking at the current implementation:
MeasureSpec.EXACTLYwill cause the view to match its parent's size. That's perfect.- For
MeasureSpec.UNSPECIFIED(which is what you'd get in aScrollView), it allows the view to (re)size itself however it seems fit. If an image source is already set, it'll use that to derive a size. That's absolutely appropriate. - However, a
MeasureSpec.AT_MOSTconstraint is treated just likeMeasureSpec.UNSPECIFIED. Although the view should be allowed to resize itself in this case, it should not size itself larger than its parent.
It feels to me like the latter is what's currently missing? Unless I'm missing/not understanding something of course. In that case it boils down to the question why MeasureSpec.AT_MOST and MeasureSpec.UNSPECIFIED are treated as if they're the same thing?
@davemorrissey Just wondering if you had any new thoughts on this issue?
I believe others could also potentially benefit from a correct implementation for MeasureSpec.AT_MOST, but perhaps I'm missing or not understanding the use case(s) for treating it the same as MeasureSpec.UNSPECIFIED?