Fix zoom reset on image load: replace Glide decoder, transfer zoom state with EXIF coordinate correction - #1117
Closed
wrzlpete wants to merge 1 commit into
Closed
Conversation
…bsamplingScaleImageView to fix zoom reset after initial load - Remove Glide dependency from MyGlideImageDecoder, use BitmapFactory.decodeStream() instead - Remove rotation and signature parameters from MyGlideImageDecoder constructor - Delete RotateTransformation class (no longer needed) - Fix URI encoding for special characters (%, #) in decode() - Add explicit ARGB_8888 config and error handling for null bitmaps - Update detekt baseline to remove RotateTransformation entry
Contributor
|
Fossify accepts code contributions only for open issues labeled |
7 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
When opening an image and immediately zooming in (e.g. double-tap or pinch), the zoom resets to the fit-to-screen state after the SubsamplingScaleImageView finishes loading the full-resolution bitmap. This has three root causes:
Glide-based decoder async mismatch:
MyGlideImageDecoderuses Glide'ssubmit().get()to decode the image, which goes through Glide's full request pipeline (cache lookup, transformation, target). The asynchronous nature of Glide's pipeline means the decoder returns a bitmap that may differ from what the view initially displayed, causing the view to reset its zoom state when the full-resolution tile is ready.Zoom state not transferred: Even with a synchronous decoder, when
SubsamplingScaleImageViewbecomes ready (onReady callback), it resets to the default fit-to-screen state, losing any zoom and pan the user had applied viaGestureImageViewduring the loading period.Coordinate and scale mismatch in transfer:
GestureImageViewoperates in rotated drawable space (Glide applies EXIF rotation to the bitmap pixels), whileSubsamplingScaleImageViewexpects unrotated source coordinates. Additionally,GestureImageView'sstate.zoomis an absolute scale (drawable→view), not a relative multiplier, and is relative to a potentially subsampled bitmap rather than the full-resolution source.Additionally, the Glide-based decoder has two secondary issues:
uri.toString().substringAfter("file://")does not handle special characters (%,#) in file paths, which can cause load failures for files with these characters in their names.RotateTransformationis applied during decode, butSubsamplingScaleImageViewalready handles EXIF rotation via itsorientationsetting. The double rotation can cause incorrect orientation in some cases.Fix
Commit 1: Replace Glide-based decoder with BitmapFactory
Replace the Glide-based decoder with a direct
BitmapFactory.decodeStream()call:MyGlideImageDecoder: removed Glide dependency, rotation, and signature parameters. Now usesBitmapFactory.decodeStream()withARGB_8888config directly fromContentResolver.openInputStream(). Added proper URI encoding for%and#characters. Added explicit error handling for null bitmaps.RotateTransformation: deleted (no longer needed — rotation is handled bySubsamplingScaleImageView's ownorientationproperty).MyGlideImageDecoderconstructor call (no longer takes rotation and signature parameters).detekt-baseline.xml: removed staleRotateTransformationentry.Commit 2: Transfer zoom state from GestureImageView to SubsamplingScaleImageView
Track
GestureImageView'sx,y, andzoomstate continuously via the existingOnStateChangeListener. WhenSubsamplingScaleImageViewreports ready (onReady callback), if the user had zoomed in beyond the initial fit zoom, the zoom level and pan position are transferred atomically usingAnimationBuilderwith a 10ms (near-instant) duration.AnimationBuilderis the public API that internally handles:[fullScale, maxScale]vialimitedScale()limitedSCenter()vTranslatefromsCenterfor the current rotationinvalidate()Commit 3: Correct coordinate and scale transformation
The initial transfer (commit 2) had two bugs that caused the zoom to still reset on images with EXIF rotation:
Scale mismatch:
state.zoomfromGestureImageViewis an absolute scale (drawable pixels → view pixels), which includes the initial fit-to-screen scaling and is relative to a potentially subsampled bitmap.SubsamplingScaleImageView's scale is also absolute but relative to the full-resolution source image. Fix: computezoomRatio = mCurrentGestureViewZoom / mInitialZoom, thentargetScale = scale * zoomRatio(wherescaleisSubsamplingScaleImageView's current full scale at onReady time).Position mismatch:
GestureImageView's coordinates are in rotated drawable space (Glide applies EXIF rotation to the bitmap pixels).SubsamplingScaleImageViewexpects unrotated source coordinates. Fix: added rotatedToSourceCoord() pure helper function that unrotates coordinates based on EXIF rotation degrees (0/90/180/270), mapping from rotated drawable space back to unrotated source space.What changed
app/src/main/kotlin/org/fossify/gallery/helpers/MyGlideImageDecoder.kt: replaced Glide decode pipeline withBitmapFactory.decodeStream(). Net -18 lines (simpler code).app/src/main/kotlin/org/fossify/gallery/helpers/RotateTransformation.kt: deleted (17 lines removed).OnStateChangeListener,AnimationBuildertransfer in onReady). Commit 3: +15 lines (rotatedToSourceCoord helper, scale ratio conversion, coordinate unrotation in onReady).app/detekt-baseline.xml: 1 line removed.No new dependencies.
BitmapFactoryis part of the Android SDK. TheSubsamplingScaleImageViewlibrary'sImageDecoderinterface already expects a synchronousdecode()call —BitmapFactory.decodeStream()is synchronous, whileGlide.submit().get()forces an async pipeline into synchronous mode, which is the root cause of the zoom reset.Testing
SubsamplingScaleImageViewbecomes ready. Before: reset to fit-to-screen even with the BitmapFactory decoder.%or#in its filename → loads correctly. Before: load failure.ARGB_8888.mCurrentGestureViewZoom > mInitialZoom + MAX_ZOOM_EQUALITY_TOLERANCE.Why this is safe
SubsamplingScaleImageView'sImageDecoderinterface is designed for synchronous decoding. The library's own defaultImageDecoderimplementation usesBitmapFactorydirectly. This change alignsMyGlideImageDecoderwith the library's intended usage pattern.RotateTransformationwas only used byMyGlideImageDecoder. Its deletion has no other callers.ARGB_8888config matches the previousDecodeFormat.PREFER_ARGB_8888setting — no image quality change.%→%25,#→%23) follows standard URI encoding rules and only affects paths that were previously broken.AnimationBuilder, which is the library's public API for programmatic zoom changes. It handles all clamping and bounds checking internally. The 10ms duration makes it visually instant.MAX_ZOOM_EQUALITY_TOLERANCE), so non-zoomed viewing is unaffected.