Skip to content

Commit 9c6d6c7

Browse files
committed
Fixes for inline image preview size in landscape mode
1 parent 748f022 commit 9c6d6c7

9 files changed

Lines changed: 285 additions & 69 deletions

File tree

src/main/assets/changelog-alpha.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/Alpha 370 (2026-09-13)
2+
Fixes for inline image preview size in landscape mode
23
Improved memory usage for inline image previews
34

45
/Alpha 369 (2026-08-01)

src/main/assets/changelog.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
119/1.27
2+
Fixes for inline image preview size in landscape mode
23
Improved memory usage for inline image previews
34

45
118/1.26

src/main/java/org/quantumbadger/redreader/adapters/GroupedRecyclerViewAdapter.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,18 @@ public void setPreloadWindow(
214214
mPreloadWindow.addAll(newWindow);
215215
}
216216

217+
/**
218+
* Notifies every item in the preload window that it is in the window, despite it having
219+
* been there already. Used when the amount of space available to display each item has
220+
* changed, as an item may need to reload what it has preloaded at a different size.
221+
*/
222+
public void refreshPreloadWindow() {
223+
224+
for(final Item<?> item : mPreloadWindow) {
225+
item.onPreloadWindowChanged(true);
226+
}
227+
}
228+
217229
@NonNull
218230
@Override
219231
public RecyclerView.ViewHolder onCreateViewHolder(

src/main/java/org/quantumbadger/redreader/adapters/RedditListingManager.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,17 @@ public void updatePreloadWindow() {
154154
mLayoutManager.findLastVisibleItemPosition());
155155
}
156156

157+
/**
158+
* As {@link #updatePreloadWindow()}, except that every item in the window is notified,
159+
* rather than only those which have entered or left it. Should be called when the size
160+
* of the list changes, for example due to the screen being rotated.
161+
*/
162+
public void refreshPreloadWindow() {
163+
General.checkThisIsUIThread();
164+
updatePreloadWindow();
165+
mAdapter.refreshPreloadWindow();
166+
}
167+
157168
public void clearPreloadWindow() {
158169
General.checkThisIsUIThread();
159170
mAdapter.setPreloadWindow(-1, -1);

src/main/java/org/quantumbadger/redreader/fragments/PostListingFragment.java

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,10 @@ public class PostListingFragment extends RRFragment
123123
private final PostListingManager mPostListingManager;
124124
private final RecyclerView mRecyclerView;
125125

126+
// The size the list was at when the preload window was last recalculated
127+
private int mLastListWidth = -1;
128+
private int mLastListHeight = -1;
129+
126130
private final View mOuter;
127131

128132
private RedditIdAndType mAfter = null;
@@ -240,7 +244,7 @@ public void onScrolled(
240244
// Scrolling isn't the only thing which changes what's on screen -- posts being
241245
// added, posts being hidden, and the screen being rotated all do too
242246
mRecyclerView.getViewTreeObserver().addOnGlobalLayoutListener(
243-
mPostListingManager::updatePreloadWindow);
247+
this::onListLayout);
244248

245249
General.setLayoutMatchParent(mRecyclerView);
246250

@@ -411,6 +415,25 @@ public Bundle onSaveInstanceState() {
411415
return bundle;
412416
}
413417

418+
private void onListLayout() {
419+
420+
final int width = mRecyclerView.getWidth();
421+
final int height = mRecyclerView.getHeight();
422+
423+
if(width == mLastListWidth && height == mLastListHeight) {
424+
mPostListingManager.updatePreloadWindow();
425+
return;
426+
}
427+
428+
mLastListWidth = width;
429+
mLastListHeight = height;
430+
431+
// Each post now has a different amount of space to display its image preview in, so
432+
// the previews which are already loaded may be at the wrong resolution. This happens
433+
// when the screen is rotated, which doesn't rebind any of the posts on it.
434+
mPostListingManager.refreshPreloadWindow();
435+
}
436+
414437
public void cancel() {
415438

416439
if(mRequest != null) {

src/main/java/org/quantumbadger/redreader/reddit/prepared/InlinePreviewLoader.java

Lines changed: 64 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -79,26 +79,48 @@ public interface Listener {
7979
}
8080

8181
/**
82-
* The preview image to show for a post, together with the size of the box it will be
83-
* displayed in.
82+
* The preview image to show for a post, together with the size of the image itself and
83+
* the size of the box to decode it into.
8484
*/
8585
public static final class PreviewDetails {
8686

8787
@NonNull public final UriString url;
88+
89+
// The size of the image itself, which determines the shape of the area it is
90+
// displayed in
91+
public final int imageWidthPx;
92+
public final int imageHeightPx;
93+
94+
// The bounds the image is decoded within, which limit the memory it uses. These
95+
// are not the size it is displayed at, which is only known once the view
96+
// displaying it is measured.
8897
public final int boxWidthPx;
8998
public final int boxHeightPx;
9099

91100
private PreviewDetails(
92101
@NonNull final UriString url,
102+
final int imageWidthPx,
103+
final int imageHeightPx,
93104
final int boxWidthPx,
94105
final int boxHeightPx) {
95106

96107
this.url = url;
108+
this.imageWidthPx = imageWidthPx;
109+
this.imageHeightPx = imageHeightPx;
97110
this.boxWidthPx = boxWidthPx;
98111
this.boxHeightPx = boxHeightPx;
99112
}
100113
}
101114

115+
/**
116+
* The maximum height, in pixels, which an inline preview may take up, given the height
117+
* of the area it is displayed in. Taller images are letterboxed into this height, so
118+
* that a post never takes up so much of the list that it is hard to scroll past.
119+
*/
120+
public static int getMaxPreviewHeightPx(final int displayAreaHeightPx) {
121+
return Math.max(1, (Math.max(400, displayAreaHeightPx) * 7) / 8);
122+
}
123+
102124
/**
103125
* Returns null if there is no inline preview to show for this post.
104126
*/
@@ -115,23 +137,38 @@ public static PreviewDetails calculatePreviewDetails(
115137
final Rect windowVisibleDisplayFrame
116138
= DisplayUtils.getWindowVisibleDisplayFrame(activity);
117139

118-
final int screenWidth
119-
= Math.min(1080, Math.max(720, windowVisibleDisplayFrame.width()));
120-
final int screenHeight
121-
= Math.min(2000, Math.max(400, windowVisibleDisplayFrame.height()));
140+
final int windowWidth = Math.max(1, windowVisibleDisplayFrame.width());
141+
142+
// Bounded to keep the memory used by each preview reasonable
143+
final int boxWidth = Math.min(1080, Math.max(720, windowWidth));
122144

123145
final RedditParsedPost.ImagePreviewDetails preview
124-
= post.src.getPreview(screenWidth, 0);
146+
= post.src.getPreview(boxWidth, 0);
125147

126148
if(preview == null || preview.width < 10 || preview.height < 10) {
127149
return null;
128150
}
129151

130-
final int boundedImageHeight = Math.max(1, Math.min(
131-
(screenHeight * 2) / 3,
132-
(int)(((long)preview.height * screenWidth) / preview.width)));
133-
134-
return new PreviewDetails(preview.url, screenWidth, boundedImageHeight);
152+
// A preview is displayed at the width of the post, which is normally the width of
153+
// the window, so scaling the height limit by the same factor as the width gives the
154+
// height to decode within. This is only an estimate of the size the image will be
155+
// displayed at, as the list is not necessarily as large as the window: the post is
156+
// narrower in the two pane tablet layout, and shorter wherever there is a toolbar.
157+
// Both make this an overestimate, which costs a little memory but never quality.
158+
final int maxBoxHeight = Math.max(1, (int)(
159+
((long)getMaxPreviewHeightPx(windowVisibleDisplayFrame.height()) * boxWidth)
160+
/ windowWidth));
161+
162+
final int boxHeight = Math.max(1, Math.min(
163+
maxBoxHeight,
164+
(int)(((long)preview.height * boxWidth) / preview.width)));
165+
166+
return new PreviewDetails(
167+
preview.url,
168+
preview.width,
169+
preview.height,
170+
boxWidth,
171+
boxHeight);
135172
}
136173

137174
@NonNull private final BaseActivity mActivity;
@@ -259,13 +296,17 @@ private void startLoad(@NonNull final PreviewDetails details) {
259296

260297
final int generation = ++mGeneration;
261298

262-
mBitmap = null;
263-
mError = null;
264-
mState = State.LOADING;
265299
mLoadedForWidthPx = details.boxWidthPx;
266300
mLoadedForHeightPx = details.boxHeightPx;
267301

268-
notifyListener();
302+
// When reloading at a higher resolution, keep showing the image we already have
303+
// until the new one is ready, rather than flashing up a loading spinner
304+
if(mState != State.LOADED || mBitmap == null) {
305+
mBitmap = null;
306+
mError = null;
307+
mState = State.LOADING;
308+
notifyListener();
309+
}
269310

270311
mRequest = new CacheRequest(
271312
details.url,
@@ -305,6 +346,13 @@ private void onLoadFailed(final int generation, @NonNull final RRError error) {
305346
}
306347

307348
mRequest = null;
349+
350+
if(mState == State.LOADED && mBitmap != null) {
351+
// A reload at a higher resolution failed -- keep showing the lower resolution
352+
// image, which is much better than showing an error in its place
353+
return;
354+
}
355+
308356
mBitmap = null;
309357
mError = error;
310358
mState = State.FAILED;
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/*******************************************************************************
2+
* This file is part of RedReader.
3+
*
4+
* RedReader is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation, either version 3 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* RedReader is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with RedReader. If not, see <http://www.gnu.org/licenses/>.
16+
******************************************************************************/
17+
18+
package org.quantumbadger.redreader.views;
19+
20+
import android.content.Context;
21+
import android.graphics.Rect;
22+
import android.util.AttributeSet;
23+
import android.view.ViewParent;
24+
import android.widget.FrameLayout;
25+
26+
import androidx.annotation.NonNull;
27+
import androidx.annotation.Nullable;
28+
import androidx.recyclerview.widget.RecyclerView;
29+
30+
import org.quantumbadger.redreader.reddit.prepared.InlinePreviewLoader;
31+
32+
/**
33+
* Displays an inline image preview at the aspect ratio of the image itself, except where
34+
* that would make it taller than {@link InlinePreviewLoader#getMaxPreviewHeightPx(int)},
35+
* in which case it is limited to that height and the image is letterboxed within it.
36+
*
37+
* <p>The height is calculated during measurement, rather than when the preview is attached,
38+
* so that it is always based on the space this view has actually been given -- both the
39+
* width it is being measured against, and the height of the list it is in. Neither can be
40+
* known in advance: the list may take up only part of the screen, as it does in the two
41+
* pane tablet layout, and the screen can be rotated without the posts on it being rebound.
42+
*/
43+
public final class InlinePreviewHolderView extends FrameLayout {
44+
45+
private final Rect mWindowVisibleDisplayFrame = new Rect();
46+
47+
private int mImageWidthPx;
48+
private int mImageHeightPx;
49+
50+
public InlinePreviewHolderView(@NonNull final Context context) {
51+
super(context);
52+
}
53+
54+
public InlinePreviewHolderView(
55+
@NonNull final Context context,
56+
@Nullable final AttributeSet attrs) {
57+
super(context, attrs);
58+
}
59+
60+
public InlinePreviewHolderView(
61+
@NonNull final Context context,
62+
@Nullable final AttributeSet attrs,
63+
final int defStyleAttr) {
64+
super(context, attrs, defStyleAttr);
65+
}
66+
67+
/**
68+
* Sets the size of the image to be displayed. Only the ratio between the two values is
69+
* used -- the image is displayed at the width of this view, whatever that turns out to
70+
* be. Pass zero for both when there is no image to display.
71+
*/
72+
public void setImageSize(final int widthPx, final int heightPx) {
73+
74+
if(widthPx == mImageWidthPx && heightPx == mImageHeightPx) {
75+
return;
76+
}
77+
78+
mImageWidthPx = widthPx;
79+
mImageHeightPx = heightPx;
80+
81+
requestLayout();
82+
}
83+
84+
/**
85+
* The height of the list this view is in, which is the space actually available to
86+
* display a post. Falls back to the height of the window if the list hasn't been laid
87+
* out yet, or if this view isn't in one.
88+
*/
89+
private int getDisplayAreaHeightPx() {
90+
91+
ViewParent parent = getParent();
92+
93+
while(parent != null) {
94+
95+
if(parent instanceof RecyclerView) {
96+
97+
final int height = ((RecyclerView)parent).getHeight();
98+
99+
if(height > 0) {
100+
return height;
101+
}
102+
103+
break;
104+
}
105+
106+
parent = parent.getParent();
107+
}
108+
109+
getWindowVisibleDisplayFrame(mWindowVisibleDisplayFrame);
110+
111+
return mWindowVisibleDisplayFrame.height();
112+
}
113+
114+
@Override
115+
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
116+
117+
if(mImageWidthPx < 1 || mImageHeightPx < 1) {
118+
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
119+
return;
120+
}
121+
122+
final int width = MeasureSpec.getSize(widthMeasureSpec);
123+
124+
final int height = Math.max(1, Math.min(
125+
InlinePreviewLoader.getMaxPreviewHeightPx(getDisplayAreaHeightPx()),
126+
(int)(((long)width * mImageHeightPx) / mImageWidthPx)));
127+
128+
super.onMeasure(
129+
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
130+
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
131+
}
132+
}

src/main/java/org/quantumbadger/redreader/views/RedditPostView.java

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ public final class RedditPostView extends FlingableItemView
7777
@NonNull private final LinearLayout mCommentsButton;
7878
@NonNull private final TextView mCommentsText;
7979
@NonNull private final LinearLayout mPostErrors;
80-
@NonNull private final FrameLayout mImagePreviewHolder;
80+
@NonNull private final InlinePreviewHolderView mImagePreviewHolder;
8181
@NonNull private final ImageView mImagePreviewImageView;
8282
@NonNull private final ConstraintLayout mImagePreviewPlayOverlay;
8383
@NonNull private final LinearLayout mImagePreviewOuter;
@@ -514,18 +514,14 @@ private void attachInlinePreview(@NonNull final RedditPreparedPost post) {
514514
final InlinePreviewLoader loader = post.getInlinePreviewLoader(mActivity);
515515

516516
if(details == null || loader == null) {
517+
mImagePreviewHolder.setImageSize(0, 0);
517518
mImagePreviewOuter.setVisibility(GONE);
518519
mImagePreviewLoadingSpinner.setVisibility(GONE);
519520
setBottomMargin(false);
520521
return;
521522
}
522523

523-
final ConstraintLayout.LayoutParams imagePreviewLayoutParams
524-
= (ConstraintLayout.LayoutParams)mImagePreviewHolder.getLayoutParams();
525-
526-
imagePreviewLayoutParams.dimensionRatio
527-
= details.boxWidthPx + ":" + details.boxHeightPx;
528-
mImagePreviewHolder.setLayoutParams(imagePreviewLayoutParams);
524+
mImagePreviewHolder.setImageSize(details.imageWidthPx, details.imageHeightPx);
529525

530526
mImagePreviewOuter.setVisibility(VISIBLE);
531527
setBottomMargin(true);

0 commit comments

Comments
 (0)