Skip to content

Commit d9a92d8

Browse files
committed
refactor: decouple gradient scrim and optimize layout in FullBleedImageLayout
- Decouple the linear gradient background scrim from the text column into flat independent sibling layers inside GalleryItemCard. - Establish a reliable size-class dependent height (100.dp / 140.dp) for the bottom scrim drawable. - Promote LongTextLayoutDimensions to WidgetTextDimensions to allow shared metrics. - Add sample_scrim_gradient drawable. - Add truncateTextToFit helper in FontUtils.
1 parent 5e7b9ec commit d9a92d8

4 files changed

Lines changed: 126 additions & 66 deletions

File tree

‎samples/user-interface/appwidgets/src/main/java/com/example/platform/ui/appwidgets/glance/layout/text/layout/FullBleedImageLayout.kt‎

Lines changed: 38 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import androidx.annotation.RequiresApi
2121
import androidx.compose.runtime.Composable
2222
import androidx.compose.ui.graphics.Color
2323
import androidx.compose.ui.unit.dp
24-
import androidx.compose.ui.unit.sp
2524
import androidx.glance.unit.ColorProvider
2625
import androidx.glance.GlanceModifier
2726
import androidx.glance.Image
@@ -42,14 +41,14 @@ import androidx.glance.layout.fillMaxWidth
4241
import androidx.glance.layout.height
4342
import androidx.glance.layout.padding
4443
import androidx.glance.layout.width
44+
import androidx.glance.layout.wrapContentHeight
4545
import androidx.glance.text.FontWeight
4646
import androidx.glance.text.Text
4747
import androidx.glance.text.TextStyle
4848
import com.example.platform.ui.appwidgets.R
4949
import com.example.platform.ui.appwidgets.glance.layout.collections.layout.ImageGridItemData
5050
import com.example.platform.ui.appwidgets.glance.layout.collections.layout.NoDataContent
5151
import com.example.platform.ui.appwidgets.glance.layout.utils.ActionUtils.actionStartDemoActivity
52-
import com.example.platform.ui.appwidgets.glance.layout.utils.FontUtils
5352
import com.example.platform.ui.appwidgets.glance.layout.utils.MediumWidgetPreview
5453
import com.example.platform.ui.appwidgets.glance.layout.utils.SmallWidgetPreview
5554

@@ -105,7 +104,7 @@ private fun GalleryItemCard(item: ImageGridItemData) {
105104
.height(size.height),
106105
contentAlignment = Alignment.BottomStart
107106
) {
108-
// Full bleed background image
107+
// Layer 1: Full bleed background photo
109108
val imageProvider = if (item.image != null) {
110109
ImageProvider(item.image)
111110
} else {
@@ -119,73 +118,52 @@ private fun GalleryItemCard(item: ImageGridItemData) {
119118
modifier = GlanceModifier.fillMaxSize()
120119
)
121120

122-
// Available space for texts within the card boundaries (with 16.dp padding top/bottom and start/end)
123-
val availableWidth = size.width - 32.dp
124-
val availableHeight = size.height - 32.dp
125-
126-
// Distribute available height proportionally (65% for Title, 35% for Caption)
127-
val titleAvailableHeight = availableHeight * 0.65f
128-
val captionAvailableHeight = availableHeight * 0.35f
129-
130-
// Calculate Caption size dynamically using FontUtils
131-
val captionText = item.supportingText ?: ""
132-
val (captionFontSize, captionMaxLines) = if (captionText.isNotEmpty()) {
133-
FontUtils.calculateFontSizeAndMaxLines(
134-
context = LocalContext.current,
135-
text = captionText,
136-
availableWidth = availableWidth,
137-
availableHeight = captionAvailableHeight,
138-
minFontSize = 10.sp,
139-
maxFontSize = 16.sp // Best available representing 36.sp caption
140-
)
141-
} else {
142-
12.sp to 1
143-
}
121+
val appIconSize = 24.dp
122+
val spacerHeight = 4.dp
144123

145-
// Calculate Title size dynamically using FontUtils
146-
val (titleFontSize, titleMaxLines) = if (itemTitle.isNotEmpty()) {
147-
FontUtils.calculateFontSizeAndMaxLines(
148-
context = LocalContext.current,
149-
text = itemTitle,
150-
availableWidth = availableWidth,
151-
availableHeight = titleAvailableHeight,
152-
minFontSize = 10.sp,
153-
maxFontSize = 28.sp // Best available representing 45.sp title
154-
)
124+
// Use discrete font sizing based on the same size-class threshold as LongTextLayout (height <= 180.dp) to avoid continuous scaling on resize.
125+
val isSmall = size.height <= 180.dp
126+
val titleFontSize = if (isSmall) {
127+
WidgetTextDimensions.primaryTextFontSizeAndMaxLines(itemTitle).first
155128
} else {
156-
16.sp to 1
129+
WidgetTextDimensions.maxPrimaryTextFontSize
157130
}
158131

159-
// Overlay layout containing Title and Caption, without gradient scrim.
160-
// We add generous padding so text doesn't touch the widget edges.
132+
// Determine a safe, explicit height for the gradient scrim area based on the size class
133+
val scrimHeight = if (isSmall) 100.dp else 140.dp
134+
135+
// Layer 2: Independent background gradient scrim positioned at the bottom of the card.
136+
// Giving this a hardcoded height guarantees it cannot stretch to full-widget height.
137+
Image(
138+
provider = ImageProvider(R.drawable.sample_scrim_gradient),
139+
contentDescription = null,
140+
contentScale = ContentScale.FillBounds,
141+
modifier = GlanceModifier
142+
.fillMaxWidth()
143+
.height(scrimHeight)
144+
)
145+
146+
// Layer 3: Independent content Column with no background, positioned at the bottom of the card.
147+
// Because it has no background and wraps content, padding is never squished.
161148
Column(
162149
modifier = GlanceModifier
163150
.fillMaxWidth()
164-
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
165-
verticalAlignment = Alignment.Bottom
151+
.wrapContentHeight()
152+
.padding(WidgetTextDimensions.widgetPadding),
153+
verticalAlignment = Alignment.Bottom,
166154
) {
167-
// Caption styled in explicit white color
168-
if (captionText.isNotEmpty()) {
169-
Text(
170-
text = captionText,
171-
maxLines = captionMaxLines,
172-
style = TextStyle(
173-
color = ColorProvider(Color.White),
174-
fontWeight = FontWeight.Normal,
175-
fontSize = captionFontSize
176-
),
177-
modifier = GlanceModifier.fillMaxWidth()
178-
)
179-
if (itemTitle.isNotEmpty()) {
180-
Spacer(modifier = GlanceModifier.height(4.dp))
181-
}
182-
}
155+
// App Logo Icon styled as a fixed size monochrome asset above the Title
156+
Image(
157+
provider = ImageProvider(R.drawable.sample_app_logo),
158+
contentDescription = null,
159+
contentScale = ContentScale.Fit,
160+
modifier = GlanceModifier.width(appIconSize).height(appIconSize)
161+
)
183162

184-
// Dynamic title sizing using FontUtils auto-scaling
185163
if (itemTitle.isNotEmpty()) {
164+
Spacer(modifier = GlanceModifier.height(spacerHeight))
186165
Text(
187166
text = itemTitle,
188-
maxLines = titleMaxLines,
189167
style = TextStyle(
190168
color = ColorProvider(Color.White),
191169
fontWeight = FontWeight.Bold,
@@ -198,6 +176,8 @@ private fun GalleryItemCard(item: ImageGridItemData) {
198176
}
199177
}
200178

179+
180+
201181
@RequiresApi(Build.VERSION_CODES_FULL.BAKLAVA_1)
202182
@SmallWidgetPreview
203183
@MediumWidgetPreview

‎samples/user-interface/appwidgets/src/main/java/com/example/platform/ui/appwidgets/glance/layout/text/layout/LongTextLayout.kt‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,9 @@ import androidx.glance.layout.padding
4040
import androidx.glance.text.Text
4141
import androidx.glance.text.TextStyle
4242
import com.example.platform.ui.appwidgets.R
43-
import com.example.platform.ui.appwidgets.glance.layout.text.layout.LongTextLayoutDimensions.captionFontSizeAndMaxLines
44-
import com.example.platform.ui.appwidgets.glance.layout.text.layout.LongTextLayoutDimensions.widgetPadding
45-
import com.example.platform.ui.appwidgets.glance.layout.text.layout.LongTextLayoutDimensions.primaryTextFontSizeAndMaxLines
43+
import com.example.platform.ui.appwidgets.glance.layout.text.layout.WidgetTextDimensions.captionFontSizeAndMaxLines
44+
import com.example.platform.ui.appwidgets.glance.layout.text.layout.WidgetTextDimensions.widgetPadding
45+
import com.example.platform.ui.appwidgets.glance.layout.text.layout.WidgetTextDimensions.primaryTextFontSizeAndMaxLines
4646
import com.example.platform.ui.appwidgets.glance.layout.utils.ActionUtils.actionStartDemoActivity
4747
import com.example.platform.ui.appwidgets.glance.layout.utils.FontUtils.calculateFontSizeAndMaxLines
4848
import com.example.platform.ui.appwidgets.glance.layout.utils.MediumWidgetPreview
@@ -221,7 +221,7 @@ data class LongTextLayoutData(
221221
val caption: String,
222222
)
223223

224-
private object LongTextLayoutDimensions {
224+
internal object WidgetTextDimensions {
225225
val widgetPadding = 16.dp
226226
private val titleBarHeight: Dp
227227
@Composable get() = if (LongTextLayoutSize.fromLocalSize() == LongTextLayoutSize.XSmall) {
@@ -242,14 +242,14 @@ private object LongTextLayoutDimensions {
242242
}
243243

244244
// Upper and lower bounds for the caption.
245-
private val minCaptionFontSize = 12.sp // low - GM3 Label Medium
246-
private val maxCaptionFontSize = 14.sp // high - GM3 Label Large
245+
internal val minCaptionFontSize = 12.sp // low - GM3 Label Medium
246+
internal val maxCaptionFontSize = 14.sp // high - GM3 Label Large
247247

248248
// Upper bound for primary text.
249-
private val maxPrimaryTextFontSize = 28.sp // GM3 Headline Medium
249+
internal val maxPrimaryTextFontSize = 28.sp // GM3 Headline Medium
250250

251251
// For a font size 16 of primary text, we want caption to be of size 14.
252-
private const val captionToPrimaryTextRatio = 0.875f
252+
internal const val captionToPrimaryTextRatio = 0.875f
253253

254254
@Composable
255255
fun primaryTextFontSizeAndMaxLines(text: String): Pair<TextUnit, Int> {

‎samples/user-interface/appwidgets/src/main/java/com/example/platform/ui/appwidgets/glance/layout/utils/FontUtils.kt‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,4 +83,43 @@ internal object FontUtils {
8383
val maxLines = (layoutHeight / textView.textSize).toInt().coerceAtLeast(1)
8484
return size.sp to maxLines
8585
}
86+
87+
/**
88+
* Returns a truncated version of the text with "..." if the text does not fit
89+
* within the available width at the given font size.
90+
*/
91+
fun truncateTextToFit(
92+
context: Context,
93+
text: String,
94+
fontSize: TextUnit,
95+
availableWidth: Dp,
96+
): String {
97+
fun dpToPx(dp: Dp): Int {
98+
return TypedValue.applyDimension(
99+
TypedValue.COMPLEX_UNIT_DIP,
100+
dp.value,
101+
context.resources.displayMetrics
102+
).toInt()
103+
}
104+
105+
val paint = android.text.TextPaint().apply {
106+
this.textSize = fontSize.value * context.resources.displayMetrics.scaledDensity
107+
}
108+
109+
val maxWidthPx = dpToPx(availableWidth)
110+
val textWidth = paint.measureText(text)
111+
112+
if (textWidth <= maxWidthPx) {
113+
return text
114+
}
115+
116+
val ellipsis = "..."
117+
val ellipsisWidth = paint.measureText(ellipsis)
118+
if (ellipsisWidth >= maxWidthPx) {
119+
return ellipsis
120+
}
121+
122+
val truncated = TextUtils.ellipsize(text, paint, maxWidthPx.toFloat(), TextUtils.TruncateAt.END)
123+
return truncated.toString()
124+
}
86125
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!--
3+
Copyright 2026 The Android Open Source Project
4+
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
-->
17+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
18+
xmlns:aapt="http://schemas.android.com/aapt"
19+
android:width="100dp"
20+
android:height="100dp"
21+
android:viewportWidth="100"
22+
android:viewportHeight="100">
23+
<path
24+
android:pathData="M0,0 h100 v100 h-100 z">
25+
<aapt:attr name="android:fillColor">
26+
<gradient
27+
android:type="linear"
28+
android:startX="50"
29+
android:startY="100"
30+
android:endX="50"
31+
android:endY="0">
32+
<item
33+
android:color="#8A000000"
34+
android:offset="0.0" />
35+
<item
36+
android:color="#00000000"
37+
android:offset="1.0" />
38+
</gradient>
39+
</aapt:attr>
40+
</path>
41+
</vector>

0 commit comments

Comments
 (0)