Skip to content

Commit 97dc2ad

Browse files
nbradburyclaude
andauthored
Reader: Fit portrait featured images instead of cropping (#22749)
* Start reader improvements feature branch Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Remove content-scanning fallbacks for Reader featured image Only use editorial.image, featured_image, or featured_media.uri as featured image sources instead of scanning post content for suitable images or videos. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Move featured image above title and excerpt in Reader post cards Reorder ConstraintLayout constraints so the visual hierarchy is: blog header → featured image → title → excerpt → interactions → footer Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix grammar in code comments Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fit portrait featured images in Reader instead of cropping When a Reader post has a portrait featured image, the image is now scaled to fit the container height and centered horizontally with a gray background fill, instead of being center-cropped which often cuts off heads and other important content. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix import ordering in ReaderPostNewViewHolder Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix import ordering and remove unnecessary @jvmoverloads in ImageManager Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2945305 commit 97dc2ad

5 files changed

Lines changed: 122 additions & 4 deletions

File tree

WordPress/src/main/java/org/wordpress/android/ui/reader/discover/viewholders/ReaderPostNewViewHolder.kt

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import android.view.Gravity
44
import android.view.View
55
import android.view.ViewGroup
66
import androidx.appcompat.widget.ListPopupWindow
7+
import androidx.core.content.ContextCompat
78
import androidx.core.view.isVisible
89
import org.wordpress.android.R
910
import org.wordpress.android.WordPress
@@ -154,11 +155,18 @@ class ReaderPostNewViewHolder(
154155
if (state.featuredImageUrl == null) {
155156
imageManager.cancelRequestAndClearImageView(imageFeatured)
156157
} else {
157-
imageManager.loadImageWithCorners(
158+
imageManager.loadImageWithCornersPortraitAware(
158159
imageFeatured,
159160
PHOTO_ROUNDED_CORNERS,
160161
state.featuredImageUrl,
161-
uiHelpers.getPxOfUiDimen(WordPress.getContext(), state.featuredImageCornerRadius)
162+
uiHelpers.getPxOfUiDimen(
163+
WordPress.getContext(),
164+
state.featuredImageCornerRadius
165+
),
166+
ContextCompat.getColor(
167+
imageFeatured.context,
168+
R.color.reader_featured_image_background
169+
)
162170
)
163171
}
164172
}
@@ -183,11 +191,18 @@ class ReaderPostNewViewHolder(
183191
state.fullVideoUrl?.let { videoUrl ->
184192
ReaderVideoUtils.retrieveVideoThumbnailUrl(videoUrl, object : VideoThumbnailUrlListener {
185193
override fun showThumbnail(thumbnailUrl: String) {
186-
imageManager.loadImageWithCorners(
194+
imageManager.loadImageWithCornersPortraitAware(
187195
imageFeatured,
188196
PHOTO_ROUNDED_CORNERS,
189197
thumbnailUrl,
190-
uiHelpers.getPxOfUiDimen(WordPress.getContext(), state.featuredImageCornerRadius)
198+
uiHelpers.getPxOfUiDimen(
199+
WordPress.getContext(),
200+
state.featuredImageCornerRadius
201+
),
202+
ContextCompat.getColor(
203+
imageFeatured.context,
204+
R.color.reader_featured_image_background
205+
)
191206
)
192207
}
193208

WordPress/src/main/java/org/wordpress/android/util/image/ImageManager.kt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import android.widget.ImageView.ScaleType.FIT_START
2020
import android.widget.ImageView.ScaleType.FIT_XY
2121
import android.widget.ImageView.ScaleType.MATRIX
2222
import android.widget.TextView
23+
import androidx.annotation.ColorInt
2324
import androidx.annotation.DrawableRes
2425
import androidx.core.content.ContextCompat
2526
import androidx.core.graphics.drawable.toDrawable
@@ -274,6 +275,36 @@ class ImageManager @Inject constructor(
274275
.clearOnDetach()
275276
}
276277

278+
/**
279+
* Loads an image from the "imgUrl" into the ImageView with a corner radius.
280+
* For portrait images (height > width), fits the full image height and centers
281+
* horizontally with a background color fill. For landscape/square images,
282+
* applies standard CenterCrop.
283+
*/
284+
fun loadImageWithCornersPortraitAware(
285+
imageView: ImageView,
286+
imageType: ImageType,
287+
imgUrl: String,
288+
cornerRadius: Int,
289+
@ColorInt backgroundColor: Int,
290+
requestListener: RequestListener<Drawable>? = null
291+
) {
292+
val context = imageView.context
293+
if (!context.isAvailable()) return
294+
295+
Glide.with(context)
296+
.load(imgUrl)
297+
.transform(
298+
PortraitAwareCropTransformation(backgroundColor),
299+
RoundedCorners(cornerRadius)
300+
)
301+
.addFallback(imageType)
302+
.addPlaceholder(imageType)
303+
.attachRequestListener(requestListener)
304+
.into(imageView)
305+
.clearOnDetach()
306+
}
307+
277308
/**
278309
* Loads an image from the "imgUrl" into the ImageView. Adds a placeholder and an error placeholder depending
279310
* on the ImageType. Attaches the ResultListener so the client can manually show/hide progress and error
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package org.wordpress.android.util.image
2+
3+
import android.graphics.Bitmap
4+
import android.graphics.Canvas
5+
import android.graphics.Paint
6+
import android.graphics.RectF
7+
import androidx.annotation.ColorInt
8+
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool
9+
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
10+
import com.bumptech.glide.load.resource.bitmap.TransformationUtils
11+
import java.security.MessageDigest
12+
13+
/**
14+
* A Glide BitmapTransformation that handles portrait images differently
15+
* from landscape/square images:
16+
*
17+
* - Landscape/square: standard CenterCrop (existing behavior)
18+
* - Portrait (height > width): scales to fit the target height, centers
19+
* horizontally, and fills the remaining space with [backgroundColor]
20+
*/
21+
class PortraitAwareCropTransformation(
22+
@ColorInt private val backgroundColor: Int
23+
) : BitmapTransformation() {
24+
override fun transform(
25+
pool: BitmapPool,
26+
toTransform: Bitmap,
27+
outWidth: Int,
28+
outHeight: Int
29+
): Bitmap {
30+
if (toTransform.height <= toTransform.width) {
31+
return TransformationUtils.centerCrop(
32+
pool, toTransform, outWidth, outHeight
33+
)
34+
}
35+
36+
// Portrait: fit to height, center horizontally
37+
val result = pool.get(
38+
outWidth, outHeight, toTransform.config ?: Bitmap.Config.ARGB_8888
39+
)
40+
41+
val canvas = Canvas(result)
42+
canvas.drawColor(backgroundColor)
43+
44+
val scale = outHeight.toFloat() / toTransform.height.toFloat()
45+
val scaledWidth = toTransform.width * scale
46+
val left = (outWidth - scaledWidth) / 2f
47+
val destRect = RectF(left, 0f, left + scaledWidth, outHeight.toFloat())
48+
val paint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.FILTER_BITMAP_FLAG)
49+
canvas.drawBitmap(toTransform, null, destRect, paint)
50+
51+
return result
52+
}
53+
54+
override fun equals(other: Any?): Boolean {
55+
return other is PortraitAwareCropTransformation &&
56+
backgroundColor == other.backgroundColor
57+
}
58+
59+
override fun hashCode(): Int = ID.hashCode() + backgroundColor
60+
61+
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
62+
messageDigest.update((ID + backgroundColor).toByteArray(CHARSET))
63+
}
64+
65+
companion object {
66+
private const val ID =
67+
"org.wordpress.android.util.image" +
68+
".PortraitAwareCropTransformation"
69+
}
70+
}

WordPress/src/main/res/values-night/colors.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
<!-- Reader Post Card -->
2525
<color name="reader_post_list_background">@color/background_dark</color>
26+
<color name="reader_featured_image_background">@color/gray_80</color>
2627

2728
<!-- Prepublishing Nudges -->
2829
<color name="prepublishing_toolbar_icon_color">@color/gray_10</color>

WordPress/src/main/res/values/colors.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737

3838
<!--Reader Post Card -->
3939
<color name="reader_post_list_background">@color/white</color>
40+
<color name="reader_featured_image_background">@color/neutral_5</color>
4041

4142
<!-- Modal Layout Picker -->
4243
<color name="mlp_categories_button_background">#ffededed</color>

0 commit comments

Comments
 (0)