1818import androidx .recyclerview .widget .RecyclerView ;
1919
2020import com .bumptech .glide .Glide ;
21+ import com .bumptech .glide .load .engine .DiskCacheStrategy ;
2122import com .bumptech .glide .load .resource .drawable .DrawableTransitionOptions ;
2223import com .fastcomments .model .FeedPost ;
2324import com .fastcomments .model .FeedPostLink ;
@@ -62,6 +63,22 @@ public FeedPostsAdapter(Context context, List<FeedPost> feedPosts, FastCommentsF
6263 // Set date format based on SDK configuration
6364 this .useAbsoluteDates = Boolean .TRUE .equals (sdk .getConfig ().absoluteDates );
6465 }
66+
67+ /**
68+ * Get the standard image height from resources
69+ * @return Standard image height in pixels
70+ */
71+ private int getDefaultImageHeight () {
72+ return context .getResources ().getDimensionPixelSize (R .dimen .feed_image_height );
73+ }
74+
75+ /**
76+ * Get the half-size image height from resources
77+ * @return Half image height in pixels
78+ */
79+ private int getHalfImageHeight () {
80+ return context .getResources ().getDimensionPixelSize (R .dimen .feed_image_half_height );
81+ }
6582
6683 @ Override
6784 public int getItemViewType (int position ) {
@@ -146,12 +163,49 @@ public void updatePosts(List<FeedPost> newPosts) {
146163 this .feedPosts .clear ();
147164 this .feedPosts .addAll (newPosts );
148165 notifyDataSetChanged ();
166+
167+ // Preload the first few images to reduce pop-in effect
168+ preloadImages (0 , Math .min (5 , newPosts .size ()));
169+ }
170+
171+ /**
172+ * Preload images for a range of posts to prevent pop-in during scrolling
173+ *
174+ * @param startPosition Starting position to preload
175+ * @param count Number of posts to preload
176+ */
177+ public void preloadImages (int startPosition , int count ) {
178+ if (startPosition >= feedPosts .size () || count <= 0 ) {
179+ return ;
180+ }
181+
182+ int endPosition = Math .min (startPosition + count , feedPosts .size ());
183+
184+ for (int i = startPosition ; i < endPosition ; i ++) {
185+ FeedPost post = feedPosts .get (i );
186+ if (post .getMedia () != null && !post .getMedia ().isEmpty ()) {
187+ FeedPostMediaItem mediaItem = post .getMedia ().get (0 );
188+ if (mediaItem .getSizes () != null && !mediaItem .getSizes ().isEmpty ()) {
189+ FeedPostMediaItemAsset bestAsset = selectBestImageSize (mediaItem .getSizes ());
190+ if (bestAsset != null && bestAsset .getSrc () != null ) {
191+ // Preload image into memory cache
192+ Glide .with (context )
193+ .load (bestAsset .getSrc ())
194+ .diskCacheStrategy (DiskCacheStrategy .ALL )
195+ .preload ();
196+ }
197+ }
198+ }
199+ }
149200 }
150201
151202 public void addPosts (List <FeedPost > morePosts ) {
152203 int startPosition = this .feedPosts .size ();
153204 this .feedPosts .addAll (morePosts );
154205 notifyItemRangeInserted (startPosition , morePosts .size ());
206+
207+ // Preload the newly added images
208+ preloadImages (startPosition , Math .min (5 , morePosts .size ()));
155209 }
156210
157211 public void updatePost (int position , FeedPost updatedPost ) {
@@ -160,6 +214,82 @@ public void updatePost(int position, FeedPost updatedPost) {
160214 notifyItemChanged (position );
161215 }
162216 }
217+
218+ /**
219+ * Select the best image size based on device display metrics
220+ * Prioritizes images that fit well on the screen while maintaining quality
221+ *
222+ * @param sizes List of available image sizes
223+ * @return The most appropriate FeedPostMediaItemAsset or the first one if no optimal size is found
224+ */
225+ private FeedPostMediaItemAsset selectBestImageSize (List <FeedPostMediaItemAsset > sizes ) {
226+ if (sizes == null || sizes .isEmpty ()) {
227+ return null ;
228+ }
229+
230+ // If there's only one size, use it
231+ if (sizes .size () == 1 ) {
232+ return sizes .get (0 );
233+ }
234+
235+ // Get screen width for comparison
236+ int screenWidth = context .getResources ().getDisplayMetrics ().widthPixels ;
237+
238+ // Target a size that's close to the screen width for optimal display
239+ // We'll tolerate images up to 1.5x screen width to maintain quality
240+ double optimalWidth = screenWidth ;
241+ double maxAcceptableWidth = screenWidth * 1.5 ;
242+
243+ FeedPostMediaItemAsset bestMatch = null ;
244+ double smallestDiff = Double .MAX_VALUE ;
245+
246+ // First pass: find close matches to optimal width
247+ for (FeedPostMediaItemAsset asset : sizes ) {
248+ if (asset == null || asset .getW () == null || asset .getSrc () == null ) {
249+ continue ;
250+ }
251+
252+ double width = asset .getW ();
253+ double diff = Math .abs (width - optimalWidth );
254+
255+ // If width is within acceptable range and has smaller difference than current best match
256+ if (width <= maxAcceptableWidth && diff < smallestDiff ) {
257+ bestMatch = asset ;
258+ smallestDiff = diff ;
259+ }
260+ }
261+
262+ // If no match found in optimal range, just use the largest that's not excessively large
263+ if (bestMatch == null ) {
264+ double largestAcceptableWidth = 0 ;
265+
266+ for (FeedPostMediaItemAsset asset : sizes ) {
267+ if (asset == null || asset .getW () == null || asset .getSrc () == null ) {
268+ continue ;
269+ }
270+
271+ double width = asset .getW ();
272+
273+ // Find largest image that's not too oversized
274+ if (width > largestAcceptableWidth && width <= maxAcceptableWidth * 2 ) {
275+ bestMatch = asset ;
276+ largestAcceptableWidth = width ;
277+ }
278+ }
279+
280+ // If still no match, use the first valid asset
281+ if (bestMatch == null ) {
282+ for (FeedPostMediaItemAsset asset : sizes ) {
283+ if (asset != null && asset .getSrc () != null ) {
284+ return asset ;
285+ }
286+ }
287+ }
288+ }
289+
290+ // Return best match or first asset if no match found
291+ return bestMatch != null ? bestMatch : sizes .get (0 );
292+ }
163293
164294 enum FeedPostType {
165295 TEXT_ONLY ,
@@ -358,9 +488,15 @@ private void bindSingleImagePost(FeedPost post) {
358488 if (bestSizeAsset != null && bestSizeAsset .getSrc () != null ) {
359489 mediaContainer .setVisibility (View .VISIBLE );
360490
491+ // Pre-set a placeholder with a fixed height before loading the image
492+ // This prevents the layout from jumping when the image loads
493+ mediaImageView .setMinimumHeight (getDefaultImageHeight ()); // Match the 300dp in layout
494+
495+ // Use Glide's preload to cache the image before displaying it
361496 Glide .with (context )
362497 .load (bestSizeAsset .getSrc ())
363- .transition (DrawableTransitionOptions .withCrossFade ())
498+ .centerCrop ()
499+ .transition (DrawableTransitionOptions .withCrossFade (300 ))
364500 .error (R .drawable .image_placeholder )
365501 .into (mediaImageView );
366502
@@ -593,9 +729,13 @@ private void loadImageIntoView(FeedPostMediaItem mediaItem, ImageView imageView)
593729 if (mediaItem .getSizes () != null && !mediaItem .getSizes ().isEmpty ()) {
594730 FeedPostMediaItemAsset bestAsset = selectBestImageSize (mediaItem .getSizes ());
595731 if (bestAsset != null && bestAsset .getSrc () != null ) {
732+ // Set a minimum height to prevent layout jumps
733+ imageView .setMinimumHeight (getHalfImageHeight ()); // Half height for smaller images
734+
596735 Glide .with (context )
597736 .load (bestAsset .getSrc ())
598- .transition (DrawableTransitionOptions .withCrossFade ())
737+ .centerCrop ()
738+ .transition (DrawableTransitionOptions .withCrossFade (300 ))
599739 .error (R .drawable .image_placeholder )
600740 .into (imageView );
601741 } else {
@@ -616,6 +756,7 @@ private ImageView createImageView(FeedPostMediaItem mediaItem) {
616756 ImageView imageView = new ImageView (context );
617757 imageView .setScaleType (ImageView .ScaleType .CENTER_CROP );
618758 imageView .setBackgroundColor (context .getResources ().getColor (android .R .color .darker_gray , null ));
759+ imageView .setMinimumHeight (getDefaultImageHeight ()); // Set a minimum height to prevent layout jumps
619760
620761 // Load image using Glide if media item has sizes
621762 if (!mediaItem .getSizes ().isEmpty ()) {
@@ -624,6 +765,7 @@ private ImageView createImageView(FeedPostMediaItem mediaItem) {
624765 if (bestSizeAsset != null ) {
625766 Glide .with (context )
626767 .load (bestSizeAsset .getSrc ())
768+ .centerCrop ()
627769 .transition (DrawableTransitionOptions .withCrossFade ())
628770 .error (R .drawable .image_placeholder )
629771 .into (imageView );
@@ -678,9 +820,15 @@ private void bindTaskPost(FeedPost post) {
678820
679821 if (bestSizeAsset != null && bestSizeAsset .getSrc () != null ) {
680822 mediaContainer .setVisibility (View .VISIBLE );
823+ // Pre-set a placeholder with a fixed height before loading the image
824+ // This prevents the layout from jumping when the image loads
825+ mediaImageView .setMinimumHeight (getDefaultImageHeight ()); // Match the 300dp in layout
826+
827+ // Use Glide's preload to cache the image before displaying it
681828 Glide .with (context )
682829 .load (bestSizeAsset .getSrc ())
683- .transition (DrawableTransitionOptions .withCrossFade ())
830+ .centerCrop ()
831+ .transition (DrawableTransitionOptions .withCrossFade (300 ))
684832 .error (R .drawable .image_placeholder )
685833 .into (mediaImageView );
686834
@@ -1013,81 +1161,5 @@ private String formatTimestamp(OffsetDateTime date) {
10131161 return relativeTime .toString ();
10141162 }
10151163 }
1016-
1017- /**
1018- * Select the best image size based on device display metrics
1019- * Prioritizes images that fit well on the screen while maintaining quality
1020- *
1021- * @param sizes List of available image sizes
1022- * @return The most appropriate FeedPostMediaItemAsset or the first one if no optimal size is found
1023- */
1024- private FeedPostMediaItemAsset selectBestImageSize (List <FeedPostMediaItemAsset > sizes ) {
1025- if (sizes == null || sizes .isEmpty ()) {
1026- return null ;
1027- }
1028-
1029- // If there's only one size, use it
1030- if (sizes .size () == 1 ) {
1031- return sizes .get (0 );
1032- }
1033-
1034- // Get screen width for comparison
1035- int screenWidth = context .getResources ().getDisplayMetrics ().widthPixels ;
1036-
1037- // Target a size that's close to the screen width for optimal display
1038- // We'll tolerate images up to 1.5x screen width to maintain quality
1039- double optimalWidth = screenWidth ;
1040- double maxAcceptableWidth = screenWidth * 1.5 ;
1041-
1042- FeedPostMediaItemAsset bestMatch = null ;
1043- double smallestDiff = Double .MAX_VALUE ;
1044-
1045- // First pass: find close matches to optimal width
1046- for (FeedPostMediaItemAsset asset : sizes ) {
1047- if (asset == null || asset .getW () == null || asset .getSrc () == null ) {
1048- continue ;
1049- }
1050-
1051- double width = asset .getW ();
1052- double diff = Math .abs (width - optimalWidth );
1053-
1054- // If width is within acceptable range and has smaller difference than current best match
1055- if (width <= maxAcceptableWidth && diff < smallestDiff ) {
1056- bestMatch = asset ;
1057- smallestDiff = diff ;
1058- }
1059- }
1060-
1061- // If no match found in optimal range, just use the largest that's not excessively large
1062- if (bestMatch == null ) {
1063- double largestAcceptableWidth = 0 ;
1064-
1065- for (FeedPostMediaItemAsset asset : sizes ) {
1066- if (asset == null || asset .getW () == null || asset .getSrc () == null ) {
1067- continue ;
1068- }
1069-
1070- double width = asset .getW ();
1071-
1072- // Find largest image that's not too oversized
1073- if (width > largestAcceptableWidth && width <= maxAcceptableWidth * 2 ) {
1074- bestMatch = asset ;
1075- largestAcceptableWidth = width ;
1076- }
1077- }
1078-
1079- // If still no match, use the first valid asset
1080- if (bestMatch == null ) {
1081- for (FeedPostMediaItemAsset asset : sizes ) {
1082- if (asset != null && asset .getSrc () != null ) {
1083- return asset ;
1084- }
1085- }
1086- }
1087- }
1088-
1089- // Return best match or first asset if no match found
1090- return bestMatch != null ? bestMatch : sizes .get (0 );
1091- }
10921164 }
10931165}
0 commit comments