-
Notifications
You must be signed in to change notification settings - Fork 585
/
Coil.kt
349 lines (323 loc) · 13.9 KB
/
Coil.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:JvmName("CoilImage")
@file:JvmMultifileClass
package dev.chrisbanes.accompanist.coil
import android.graphics.drawable.Drawable
import androidx.compose.foundation.Box
import androidx.compose.foundation.Image
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.launchInComposition
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.referentialEqualityPolicy
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.stateFor
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.WithConstraints
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ImageAsset
import androidx.compose.ui.graphics.asImageAsset
import androidx.compose.ui.graphics.painter.ImagePainter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.ContextAmbient
import androidx.compose.ui.unit.IntSize
import androidx.core.graphics.drawable.toBitmap
import coil.Coil
import coil.ImageLoader
import coil.decode.DataSource
import coil.request.ImageRequest
import coil.request.ImageResult
/**
* Creates a composable that will attempt to load the given [data] using [Coil], and then
* display the result in an [Image].
*
* @param data The data to load. See [ImageRequest.Builder.data] for the types allowed.
* @param modifier [Modifier] used to adjust the layout algorithm or draw decoration content.
* @param alignment Optional alignment parameter used to place the loaded [ImageAsset] in the
* given bounds defined by the width and height.
* @param contentScale Optional scale parameter used to determine the aspect ratio scaling to be
* used if the bounds are a different size from the intrinsic size of the loaded [ImageAsset].
* @param colorFilter Optional colorFilter to apply for the [Painter] when it is rendered onscreen.
* @param getSuccessPainter Optional builder for the [Painter] to be used to draw the successful
* loading result. Passing in `null` will result in falling back to the default [Painter].
* @param getFailurePainter Optional builder for the [Painter] to be used to draw the failure
* loading result. Passing in `null` will result in falling back to the default [Painter].
* @param loading Content to be displayed when the request is in progress.
* @param imageLoader The [ImageLoader] to use when requesting the image. Defaults to [Coil]'s
* default image loader.
* @param shouldRefetchOnSizeChange Lambda which will be invoked when the size changes, allowing
* optional re-fetching of the image. Return true to re-fetch the image.
* @param onRequestCompleted Listener which will be called when the loading request has finished.
*/
@Composable
fun CoilImage(
data: Any,
modifier: Modifier = Modifier,
alignment: Alignment = Alignment.Center,
contentScale: ContentScale = ContentScale.Fit,
colorFilter: ColorFilter? = null,
getSuccessPainter: @Composable ((SuccessResult) -> Painter)? = null,
getFailurePainter: @Composable ((ErrorResult) -> Painter?)? = null,
loading: @Composable (() -> Unit)? = null,
imageLoader: ImageLoader = Coil.imageLoader(ContextAmbient.current),
shouldRefetchOnSizeChange: (currentResult: RequestResult, size: IntSize) -> Boolean = defaultRefetchOnSizeChangeLambda,
onRequestCompleted: (RequestResult) -> Unit = emptySuccessLambda
) {
CoilImage(
request = when (data) {
// If the developer is accidentally using the wrong function (data vs request), just
// pass the request through
is ImageRequest -> data
// Otherwise we construct a GetRequest using the data parameter
else -> {
val context = ContextAmbient.current
remember(data) { ImageRequest.Builder(context).data(data).build() }
}
},
alignment = alignment,
contentScale = contentScale,
colorFilter = colorFilter,
onRequestCompleted = onRequestCompleted,
getSuccessPainter = getSuccessPainter,
getFailurePainter = getFailurePainter,
loading = loading,
imageLoader = imageLoader,
shouldRefetchOnSizeChange = shouldRefetchOnSizeChange,
modifier = modifier
)
}
/**
* Creates a composable that will attempt to load the given [request] using [Coil], and then
* display the result in an [Image].
*
* @param request The request to execute. If the request does not have a [ImageRequest.sizeResolver]
* set, one will be set on the request using the layout constraints.
* @param modifier [Modifier] used to adjust the layout algorithm or draw decoration content.
* @param alignment Optional alignment parameter used to place the loaded [ImageAsset] in the
* given bounds defined by the width and height.
* @param contentScale Optional scale parameter used to determine the aspect ratio scaling to be
* used if the bounds are a different size from the intrinsic size of the loaded [ImageAsset].
* @param colorFilter Optional colorFilter to apply for the [Painter] when it is rendered onscreen.
* @param getSuccessPainter Optional builder for the [Painter] to be used to draw the successful
* loading result. Passing in `null` will result in falling back to the default [Painter].
* @param getFailurePainter Optional builder for the [Painter] to be used to draw the failure
* loading result. Passing in `null` will result in falling back to the default [Painter].
* @param loading Content to be displayed when the request is in progress.
* @param imageLoader The [ImageLoader] to use when requesting the image. Defaults to [Coil]'s
* default image loader.
* @param shouldRefetchOnSizeChange Lambda which will be invoked when the size changes, allowing
* optional re-fetching of the image. Return true to re-fetch the image.
* @param onRequestCompleted Listener which will be called when the loading request has finished.
*/
@Composable
fun CoilImage(
request: ImageRequest,
modifier: Modifier = Modifier,
alignment: Alignment = Alignment.Center,
contentScale: ContentScale = ContentScale.Fit,
colorFilter: ColorFilter? = null,
getSuccessPainter: @Composable ((SuccessResult) -> Painter)? = null,
getFailurePainter: @Composable ((ErrorResult) -> Painter?)? = null,
loading: @Composable (() -> Unit)? = null,
imageLoader: ImageLoader = Coil.imageLoader(ContextAmbient.current),
shouldRefetchOnSizeChange: (currentResult: RequestResult, size: IntSize) -> Boolean = defaultRefetchOnSizeChangeLambda,
onRequestCompleted: (RequestResult) -> Unit = emptySuccessLambda
) {
var result by stateFor<RequestResult?>(request) { null }
// This may look a little weird, but allows the launchInComposition callback to always
// invoke the last provided [onRequestCompleted].
//
// If a composition happens *after* launchInComposition has launched, the given
// [onRequestCompleted] might have changed. If the actor lambda below directly referenced
// [onRequestCompleted] it would have captured access to the initial onRequestCompleted
// value, not the latest.
//
// This `callback` state enables the actor lambda to only capture the remembered state
// reference, which we can update on each composition.
val callback = remember { mutableStateOf(onRequestCompleted, referentialEqualityPolicy()) }
callback.value = onRequestCompleted
val requestActor = remember(imageLoader, request) {
CoilRequestActor(imageLoader, request)
}
launchInComposition(requestActor) {
// Launch the Actor
requestActor.run { _, actorResult ->
// Update the result state
result = actorResult
if (actorResult != null) {
// Execute the onRequestCompleted callback if we have a new result
callback.value(actorResult)
}
}
}
val painter = when (val r = result) {
is SuccessResult -> {
if (getSuccessPainter != null) {
getSuccessPainter(r)
} else {
defaultSuccessPainterGetter(r)
}
}
is ErrorResult -> {
if (getFailurePainter != null) {
getFailurePainter(r)
} else {
defaultFailurePainterGetter(r)
}
}
else -> null
}
WithConstraints(modifier) {
// We remember the last size in a MutableRef (below) rather than a MutableState.
// This is because we don't need value changes to trigger a re-composition, we are only
// using it to store the last value.
val lastRequestedSize = remember(requestActor) { MutableRef(IntSize.Zero) }
val requestSize = IntSize(
width = if (constraints.hasBoundedWidth) constraints.maxWidth else UNSPECIFIED,
height = if (constraints.hasBoundedHeight) constraints.maxHeight else UNSPECIFIED
)
val r = result
if (lastRequestedSize.value != requestSize &&
(r == null || shouldRefetchOnSizeChange(r, requestSize))
) {
requestActor.send(requestSize)
lastRequestedSize.value = requestSize
}
if (painter == null) {
// If we don't have a result painter, we add a Box...
Box {
// If we don't have a result yet, we can show the loading content
// (if not null)
if (result == null && loading != null) {
loading()
}
}
} else {
Image(
painter = painter,
contentScale = contentScale,
alignment = alignment,
colorFilter = colorFilter,
)
}
}
}
/**
* Value for a [IntSize] dimension, where the dimension is not specified or is unknown.
*/
private const val UNSPECIFIED = -1
@Stable
private data class MutableRef<T>(var value: T)
private fun CoilRequestActor(
imageLoader: ImageLoader,
request: ImageRequest
) = RequestActor<IntSize, RequestResult?> { size ->
when {
request.defined.sizeResolver != null -> {
// If the request has a size resolver set we just execute the request as-is
request
}
size.width == UNSPECIFIED || size.height == UNSPECIFIED -> {
// If the size contains an unspecified dimension, we don't specify a size
// in the Coil request
request
}
size != IntSize.Zero -> {
// If we have a non-zero size, we can modify the request to include the size
request.newBuilder().size(size.width, size.height).build()
}
else -> {
// Otherwise we have a zero size, so no point executing a request
null
}
}?.let { transformedRequest ->
// Now execute the request in Coil...
imageLoader
.execute(transformedRequest)
.toResult(size)
.also {
// Tell RenderThread to pre-upload this bitmap. Saves the GPU upload cost on the
// first draw. See https://github.com/square/picasso/issues/1620 for a explanation
// from @ChrisCraik
it.image?.prepareToDraw()
}
}
}
/**
* Represents the result of an image request.
*/
sealed class RequestResult {
abstract val image: ImageAsset?
}
/**
* Indicates that the request completed successfully.
*
* @param image The result image.
* @param source The data source that the image was loaded from.
*/
data class SuccessResult(
override val image: ImageAsset,
val source: DataSource
) : RequestResult() {
internal constructor(result: coil.request.SuccessResult, fallbackSize: IntSize) : this(
image = result.drawable.toImageAsset(fallbackSize),
source = result.metadata.dataSource
)
}
/**
* Indicates that an error occurred while executing the request.
*
* @param image The error image.
* @param throwable The error that failed the request.
*/
data class ErrorResult(
override val image: ImageAsset?,
val throwable: Throwable
) : RequestResult() {
internal constructor(result: coil.request.ErrorResult, fallbackSize: IntSize) : this(
image = result.drawable?.toImageAsset(fallbackSize),
throwable = result.throwable
)
}
private fun ImageResult.toResult(
fallbackSize: IntSize = IntSize.Zero
): RequestResult = when (this) {
is coil.request.SuccessResult -> SuccessResult(this, fallbackSize)
is coil.request.ErrorResult -> ErrorResult(this, fallbackSize)
}
@Composable
internal fun defaultFailurePainterGetter(error: ErrorResult): Painter? {
return error.image?.let { image ->
remember(image) { ImagePainter(image) }
}
}
@Composable
internal fun defaultSuccessPainterGetter(result: SuccessResult): Painter {
return remember(result.image) { ImagePainter(result.image) }
}
internal val emptySuccessLambda: (RequestResult) -> Unit = {}
internal val defaultRefetchOnSizeChangeLambda: (RequestResult, IntSize) -> Boolean = { _, _ -> false }
internal fun Drawable.toImageAsset(fallbackSize: IntSize): ImageAsset {
return toBitmap(
width = if (intrinsicWidth > 0) intrinsicWidth else fallbackSize.width,
height = if (intrinsicHeight > 0) intrinsicHeight else fallbackSize.height
).asImageAsset()
}