Skip to content

Commit b329401

Browse files
committed
Influence layout using intrinsics in GlideNode
1 parent d10c5ff commit b329401

File tree

2 files changed

+97
-48
lines changed

2 files changed

+97
-48
lines changed

integration/compose/src/main/java/com/bumptech/glide/integration/compose/GlideImage.kt

+14-15
Original file line numberDiff line numberDiff line change
@@ -34,29 +34,29 @@ public typealias RequestBuilderTransform<T> = (RequestBuilder<T>) -> RequestBuil
3434
* [alignment], [contentScale], [alpha], [colorFilter] and [contentDescription] have the same
3535
* defaults (if any) and function identically to the parameters in [Image].
3636
*
37-
* If you want to restrict the size of this [Composable], use the given [modifier]. If you'd like to
38-
* force the size of the pixels you load to be different than the display area, use
39-
* [RequestBuilder.override]. Often you can get better performance by setting an explicit size so
40-
* that we do not have to wait for layout to fetch the image. If the size set via the [modifier] is
37+
* Set the size this [Composable] using the given [modifier]. Use fixed sizes when you can for
38+
* better performance and to avoid layout jank when images are loaded. If you cannot use a fixed
39+
* size, try to at least set a bounded size. If the size set via the [modifier] is
4140
* dependent on the content, Glide will probably end up loading the image using
4241
* [com.bumptech.glide.request.target.Target.SIZE_ORIGINAL]. Avoid `SIZE_ORIGINAL`, implicitly or
4342
* explicitly if you can. You may end up loading a substantially larger image than you need, which
4443
* will increase memory usage and may also increase latency.
4544
*
46-
* This method will inspect [contentScale] and apply a matching transformation if one exists. Any
47-
* automatically applied transformation can be overridden using [requestBuilderTransform]. Either
48-
* apply a specific transformation instead, or use [RequestBuilder.dontTransform]]
45+
* You can force the size of the image you load to be different than the display area using
46+
* [RequestBuilder.override].
4947
*
50-
* Transitions set via [RequestBuilder.transition] are currently ignored.
51-
*
52-
* Note - this method is likely to change while we work on improving the API. Transitions are one
53-
* significant unexplored area. It's also possible we'll try and remove the [RequestBuilder] from
54-
* the direct API and instead allow all options to be set directly in the method.
48+
* [contentScale] will apply to both [loading] and [error] placeholders, as well as the the primary
49+
* request. If you'd like different scaling behavior for placeholders vs the primary request, use
50+
* [contentScale] to scale the placeholders and [requestBuilderTransform] to set a different
51+
* `Transformation` for the image load. [contentScale] will also be inspected to apply a matching
52+
* default `Transformation` if one exists. Any automatically applied `Transformation` based on
53+
* [contentScale] can be overridden using [requestBuilderTransform] via [RequestBuilder.transform]
54+
* or [RequestBuilder.dontTransform].
5555
*
5656
* [requestBuilderTransform] is overridden by any overlapping parameter defined in this method if
5757
* that parameter is non-null. For example, [loading] and [failure], if non-null will be used in
5858
* place of any placeholder set by [requestBuilderTransform] using [RequestBuilder.placeholder] or
59-
* [RequestBuilder.error].
59+
* [RequestBuilder.error]. Transitions set via [RequestBuilder.transition] are ignored.
6060
*
6161
* @param loading A [Placeholder] that will be displayed while the request is loading. Specifically
6262
* it's used if the request is cleared ([com.bumptech.glide.request.target.Target.onLoadCleared]) or
@@ -80,7 +80,6 @@ public typealias RequestBuilderTransform<T> = (RequestBuilder<T>) -> RequestBuil
8080
*/
8181
// TODO(judds): the API here is not particularly composeesque, we should consider alternatives
8282
// to RequestBuilder (though thumbnail() may make that a challenge).
83-
// TODO(judds): Consider how to deal with transitions.
8483
@ExperimentalGlideComposeApi
8584
@Composable
8685
public fun GlideImage(
@@ -131,7 +130,7 @@ public fun GlideImage(
131130
alignment,
132131
contentScale,
133132
alpha,
134-
colorFilter
133+
colorFilter,
135134
)
136135
}
137136
}

integration/compose/src/main/java/com/bumptech/glide/integration/compose/GlideModifier.kt

+83-33
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package com.bumptech.glide.integration.compose
22

33
import android.graphics.PointF
44
import android.graphics.drawable.Drawable
5-
import android.util.Log
65
import androidx.compose.ui.Alignment
76
import androidx.compose.ui.ExperimentalComposeUiApi
87
import androidx.compose.ui.Modifier
@@ -12,6 +11,7 @@ import androidx.compose.ui.graphics.ColorFilter
1211
import androidx.compose.ui.graphics.DefaultAlpha
1312
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
1413
import androidx.compose.ui.graphics.drawscope.DrawScope
14+
import androidx.compose.ui.graphics.drawscope.clipRect
1515
import androidx.compose.ui.graphics.drawscope.translate
1616
import androidx.compose.ui.graphics.painter.Painter
1717
import androidx.compose.ui.graphics.withSave
@@ -36,6 +36,8 @@ import androidx.compose.ui.semantics.semantics
3636
import androidx.compose.ui.unit.Constraints
3737
import androidx.compose.ui.unit.IntOffset
3838
import androidx.compose.ui.unit.IntSize
39+
import androidx.compose.ui.unit.constrainHeight
40+
import androidx.compose.ui.unit.constrainWidth
3941
import androidx.compose.ui.unit.toSize
4042
import com.bumptech.glide.RequestBuilder
4143
import com.bumptech.glide.integration.ktx.AsyncGlideSize
@@ -72,7 +74,7 @@ internal fun Modifier.glideNode(
7274
colorFilter: ColorFilter? = null,
7375
transitionFactory: Transition.Factory? = null,
7476
requestListener: RequestListener? = null,
75-
draw: Boolean? = true,
77+
draw: Boolean? = null,
7678
): Modifier {
7779
return this then GlideNodeElement(
7880
requestBuilder,
@@ -151,6 +153,7 @@ internal class GlideNode : DrawModifierNode, LayoutModifierNode, SemanticsModifi
151153
private lateinit var requestBuilder: RequestBuilder<Drawable>
152154
private lateinit var contentScale: ContentScale
153155
private lateinit var alignment: Alignment
156+
private lateinit var resolvableGlideSize: ResolvableGlideSize
154157
private var alpha: Float = DefaultAlpha
155158
private var colorFilter: ColorFilter? = null
156159
private var transitionFactory: Transition.Factory = DoNotTransition.Factory
@@ -163,14 +166,16 @@ internal class GlideNode : DrawModifierNode, LayoutModifierNode, SemanticsModifi
163166
// Only used for debugging
164167
private var drawable: Drawable? = null
165168
private var state: RequestState = RequestState.Loading
166-
private var resolvableGlideSize: ResolvableGlideSize? = null
167169
private var placeholder: Painter? = null
168170
private var isFirstResource = true
169171

170172
// Avoid allocating Point/PointFs during draw
171173
private var placeholderPositionAndSize: CachedPositionAndSize? = null
172174
private var drawablePositionAndSize: CachedPositionAndSize? = null
173175

176+
private var hasFixedSize: Boolean = false
177+
private var inferredGlideSize: com.bumptech.glide.integration.ktx.Size? = null
178+
174179
private var transition: Transition = DoNotTransition
175180

176181
private fun RequestBuilder<*>.maybeImmediateSize() =
@@ -199,37 +204,35 @@ internal class GlideNode : DrawModifierNode, LayoutModifierNode, SemanticsModifi
199204
this.requestListener = requestListener
200205
this.draw = draw ?: true
201206
this.transitionFactory = transitionFactory ?: DoNotTransition.Factory
207+
this.resolvableGlideSize =
208+
requestBuilder.maybeImmediateSize()
209+
?: inferredGlideSize?.let { ImmediateGlideSize(it) }
210+
?: AsyncGlideSize()
202211

203212
if (restartLoad) {
204213
clear()
205214
updateDrawable(null)
206215

207216
// If we're not attached, we'll be measured when we eventually are attached.
208217
if (isAttached) {
209-
// If we don't have a fixed size, we need a new layout pass to figure out how large the
210-
// image should be. Ideally we'd retain the old resolved glide size unless some other
211-
// modifier node had already requested measurement. Since we can't tell if measurement is
212-
// requested, we can either re-use the old resolvableGlideSize, which will be incorrect if
213-
// measurement was requested. Or we can invalidate resolvableGlideSize and ensure that it's
214-
// resolved by requesting measurement ourselves. Requesting is less efficient, but more
215-
// correct.
216-
// TODO(sam): See if we can find a reasonable way to remove this behavior, or be more
217-
// targeted.
218-
if (requestBuilder.overrideSize() == null) {
219-
invalidateMeasurement()
220-
}
221218
launchRequest(requestBuilder)
222219
}
223220
} else {
224221
invalidateDraw()
225222
}
226223
}
227224

225+
private val Size.isValidWidth
226+
get() = this != Size.Unspecified && this.width.isValidDimension
227+
228+
private val Size.isValidHeight
229+
get() = this != Size.Unspecified && this.height.isValidDimension
230+
228231
private val Float.isValidDimension
229-
get() = this > 0f
232+
get() = this > 0f && isFinite()
230233

231234
private val Size.isValid
232-
get() = width.isValidDimension && height.isValidDimension
235+
get() = isValidWidth && isValidHeight
233236

234237
private fun Size.roundToInt() = IntSize(width.roundToInt(), height.roundToInt())
235238

@@ -248,12 +251,12 @@ internal class GlideNode : DrawModifierNode, LayoutModifierNode, SemanticsModifi
248251
val currentPositionAndSize = if (cache != null) {
249252
cache
250253
} else {
251-
val srcWidth = if (painter.intrinsicSize.width.isValidDimension) {
254+
val srcWidth = if (painter.intrinsicSize.isValidWidth) {
252255
painter.intrinsicSize.width
253256
} else {
254257
size.width
255258
}
256-
val srcHeight = if (painter.intrinsicSize.height.isValidDimension) {
259+
val srcHeight = if (painter.intrinsicSize.isValidHeight) {
257260
painter.intrinsicSize.height
258261
} else {
259262
size.height
@@ -275,8 +278,10 @@ internal class GlideNode : DrawModifierNode, LayoutModifierNode, SemanticsModifi
275278
)
276279
}
277280

278-
translate(currentPositionAndSize.position.x, currentPositionAndSize.position.y) {
279-
drawOne.invoke(this, currentPositionAndSize.size)
281+
clipRect {
282+
translate(currentPositionAndSize.position.x, currentPositionAndSize.position.y) {
283+
drawOne.invoke(this, currentPositionAndSize.size)
284+
}
280285
}
281286
return currentPositionAndSize
282287
}
@@ -355,11 +360,10 @@ internal class GlideNode : DrawModifierNode, LayoutModifierNode, SemanticsModifi
355360
}
356361
Preconditions.checkArgument(currentJob == null)
357362
currentJob = (coroutineScope + Dispatchers.Main.immediate).launch {
358-
this@GlideNode.resolvableGlideSize = requestBuilder.maybeImmediateSize() ?: AsyncGlideSize()
359363
placeholder = null
360364
placeholderPositionAndSize = null
361365

362-
requestBuilder.flowResolvable(resolvableGlideSize!!).collect {
366+
requestBuilder.flowResolvable(resolvableGlideSize).collect {
363367
val (state, drawable) =
364368
when (it) {
365369
is Resource -> {
@@ -382,7 +386,11 @@ internal class GlideNode : DrawModifierNode, LayoutModifierNode, SemanticsModifi
382386
updateDrawable(drawable)
383387
requestListener?.onStateChanged(requestBuilder.internalModel, drawable, state)
384388
this@GlideNode.state = state
385-
invalidateDraw()
389+
if (hasFixedSize) {
390+
invalidateDraw()
391+
} else {
392+
invalidateMeasurement()
393+
}
386394
}
387395
}
388396
}
@@ -405,7 +413,6 @@ internal class GlideNode : DrawModifierNode, LayoutModifierNode, SemanticsModifi
405413

406414
private fun clear() {
407415
isFirstResource = true
408-
resolvableGlideSize = null
409416
currentJob?.cancel()
410417
currentJob = null
411418
state = RequestState.Loading
@@ -419,23 +426,66 @@ internal class GlideNode : DrawModifierNode, LayoutModifierNode, SemanticsModifi
419426
): MeasureResult {
420427
placeholderPositionAndSize = null
421428
drawablePositionAndSize = null
429+
hasFixedSize = constraints.hasFixedSize()
430+
inferredGlideSize = constraints.inferredGlideSize()
431+
422432
when (val currentSize = resolvableGlideSize) {
423433
is AsyncGlideSize -> {
424-
val inferredSize = constraints.inferredGlideSize()
425-
if (inferredSize != null) {
426-
currentSize.setSize(inferredSize)
427-
}
434+
inferredGlideSize?.also { currentSize.setSize(it) }
428435
}
429-
// Do nothing.
436+
430437
is ImmediateGlideSize -> {}
431-
null -> {}
432438
}
433-
val placeable = measurable.measure(constraints)
434-
return layout(constraints.maxWidth, constraints.maxHeight) {
439+
val placeable = measurable.measure(modifyConstraints(constraints))
440+
return layout(placeable.width, placeable.height) {
435441
placeable.placeRelative(0, 0)
436442
}
437443
}
438444

445+
private fun Constraints.hasFixedSize() = hasFixedWidth && hasFixedHeight
446+
447+
private fun modifyConstraints(constraints: Constraints): Constraints {
448+
if (constraints.hasFixedSize()) {
449+
return constraints.copy(
450+
minWidth = constraints.maxWidth,
451+
minHeight = constraints.maxHeight
452+
)
453+
}
454+
455+
val intrinsicSize = painter?.intrinsicSize ?: return constraints
456+
457+
val intrinsicWidth =
458+
if (constraints.hasFixedWidth) {
459+
constraints.maxWidth
460+
} else if (intrinsicSize.isValidWidth) {
461+
intrinsicSize.width.roundToInt()
462+
} else {
463+
constraints.minWidth
464+
}
465+
466+
val intrinsicHeight =
467+
if (constraints.hasFixedHeight) {
468+
constraints.maxHeight
469+
} else if (intrinsicSize.isValidHeight) {
470+
intrinsicSize.height.roundToInt()
471+
} else {
472+
constraints.minHeight
473+
}
474+
475+
val constrainedWidth = constraints.constrainWidth(intrinsicWidth)
476+
val constrainedHeight = constraints.constrainHeight(intrinsicHeight)
477+
478+
val srcSize = Size(intrinsicWidth.toFloat(), intrinsicHeight.toFloat())
479+
val scaledSize =
480+
srcSize * contentScale.computeScaleFactor(
481+
srcSize, Size(constrainedWidth.toFloat(), constrainedHeight.toFloat())
482+
)
483+
484+
val minWidth = constraints.constrainWidth(scaledSize.width.roundToInt())
485+
val minHeight = constraints.constrainHeight(scaledSize.height.roundToInt())
486+
return constraints.copy(minWidth = minWidth, minHeight = minHeight)
487+
}
488+
439489
override fun SemanticsPropertyReceiver.applySemantics() {
440490
displayedDrawable = { drawable }
441491
}

0 commit comments

Comments
 (0)