Skip to content

Commit 1cbe0f3

Browse files
authored
Merge pull request #754 from skydoves/feature/crossfade
Reimplement crossfade for applying between state changes
2 parents 4f4e0e7 + c503e73 commit 1cbe0f3

File tree

21 files changed

+742
-528
lines changed

21 files changed

+742
-528
lines changed

app/src/main/kotlin/com/github/skydoves/landscapistdemo/ui/MainPosters.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,9 @@ import com.github.skydoves.landscapistdemo.theme.background
5959
import com.kmpalette.palette.graphics.Palette
6060
import com.skydoves.landscapist.ImageOptions
6161
import com.skydoves.landscapist.animation.circular.CircularRevealPlugin
62-
import com.skydoves.landscapist.animation.crossfade.CrossfadePlugin
6362
import com.skydoves.landscapist.coil3.CoilImage
6463
import com.skydoves.landscapist.components.rememberImageComponent
64+
import com.skydoves.landscapist.crossfade.CrossfadePlugin
6565
import com.skydoves.landscapist.fresco.FrescoImage
6666
import com.skydoves.landscapist.glide.GlideImage
6767
import com.skydoves.landscapist.palette.PalettePlugin

benchmark-landscapist-app/src/main/kotlin/com/skydoves/benchmark/landscapist/app/MainActivity.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ import androidx.compose.ui.res.painterResource
2828
import androidx.compose.ui.semantics.semantics
2929
import androidx.compose.ui.semantics.testTagsAsResourceId
3030
import com.skydoves.landscapist.animation.circular.CircularRevealPlugin
31-
import com.skydoves.landscapist.animation.crossfade.CrossfadePlugin
3231
import com.skydoves.landscapist.components.LocalImageComponent
3332
import com.skydoves.landscapist.components.imageComponent
33+
import com.skydoves.landscapist.crossfade.CrossfadePlugin
3434
import com.skydoves.landscapist.palette.PalettePlugin
3535
import com.skydoves.landscapist.placeholder.placeholder.PlaceholderPlugin
3636
import com.skydoves.landscapist.placeholder.shimmer.ShimmerPlugin

coil/src/main/kotlin/com/skydoves/landscapist/coil/CoilImage.kt

Lines changed: 57 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ import com.skydoves.landscapist.components.imagePlugins
5656
import com.skydoves.landscapist.components.rememberImageComponent
5757
import com.skydoves.landscapist.constraints.Constrainable
5858
import com.skydoves.landscapist.constraints.constraint
59+
import com.skydoves.landscapist.crossfade.CrossfadePlugin
60+
import com.skydoves.landscapist.crossfade.CrossfadeWithEffect
5961
import com.skydoves.landscapist.plugins.ImagePlugin
6062
import com.skydoves.landscapist.rememberDrawablePainter
6163
import kotlinx.coroutines.channels.trySendBlocking
@@ -221,58 +223,67 @@ public fun CoilImage(
221223
}
222224
}
223225

224-
when (coilImageState) {
225-
is CoilImageState.None -> Unit
226+
val crossfadePlugin = component.imagePlugins.filterIsInstance<CrossfadePlugin>().firstOrNull()
226227

227-
is CoilImageState.Loading -> {
228-
component.ComposeLoadingStatePlugins(
229-
modifier = Modifier.constraint(this),
230-
imageOptions = imageOptions,
231-
executor = { size ->
232-
CoilThumbnail(
233-
requestSize = size,
234-
recomposeKey = StableHolder(imageRequest.invoke()),
235-
imageLoader = StableHolder(imageLoader.invoke()),
236-
imageOptions = imageOptions,
237-
)
238-
},
239-
)
240-
loading?.invoke(this, coilImageState)
241-
}
228+
CrossfadeWithEffect(
229+
targetState = coilImageState,
230+
durationMs = crossfadePlugin?.duration ?: 0,
231+
contentKey = { coilImageState },
232+
enabled = crossfadePlugin != null,
233+
) {
234+
when (coilImageState) {
235+
is CoilImageState.None -> Unit
242236

243-
is CoilImageState.Failure -> {
244-
component.ComposeFailureStatePlugins(
245-
modifier = Modifier.constraint(this),
246-
imageOptions = imageOptions,
247-
reason = coilImageState.reason,
248-
)
249-
failure?.invoke(this, coilImageState)
250-
}
237+
is CoilImageState.Loading -> {
238+
component.ComposeLoadingStatePlugins(
239+
modifier = Modifier.constraint(this),
240+
imageOptions = imageOptions,
241+
executor = { size ->
242+
CoilThumbnail(
243+
requestSize = size,
244+
recomposeKey = StableHolder(imageRequest.invoke()),
245+
imageLoader = StableHolder(imageLoader.invoke()),
246+
imageOptions = imageOptions,
247+
)
248+
},
249+
)
250+
loading?.invoke(this, coilImageState)
251+
}
251252

252-
is CoilImageState.Success -> {
253-
component.ComposeSuccessStatePlugins(
254-
modifier = Modifier.constraint(this),
255-
imageModel = imageRequest.invoke().data,
256-
imageOptions = imageOptions,
257-
imageBitmap = coilImageState.drawable?.toBitmap()
258-
?.copy(Bitmap.Config.ARGB_8888, true)?.asImageBitmap(),
259-
)
253+
is CoilImageState.Failure -> {
254+
component.ComposeFailureStatePlugins(
255+
modifier = Modifier.constraint(this),
256+
imageOptions = imageOptions,
257+
reason = coilImageState.reason,
258+
)
259+
failure?.invoke(this, coilImageState)
260+
}
260261

261-
val drawable = coilImageState.drawable ?: return@ImageRequest
262-
val painter = rememberDrawablePainter(
263-
drawable = drawable,
264-
imagePlugins = component.imagePlugins,
265-
)
262+
is CoilImageState.Success -> {
263+
component.ComposeSuccessStatePlugins(
264+
modifier = Modifier.constraint(this),
265+
imageModel = imageRequest.invoke().data,
266+
imageOptions = imageOptions,
267+
imageBitmap = coilImageState.drawable?.toBitmap()
268+
?.copy(Bitmap.Config.ARGB_8888, true)?.asImageBitmap(),
269+
)
266270

267-
if (success != null) {
268-
success.invoke(this, coilImageState, painter)
269-
} else {
270-
imageOptions.LandscapistImage(
271-
modifier = Modifier
272-
.constraint(this)
273-
.testTag(imageOptions.tag),
274-
painter = painter,
271+
val drawable = coilImageState.drawable ?: return@CrossfadeWithEffect
272+
val painter = rememberDrawablePainter(
273+
drawable = drawable,
274+
imagePlugins = component.imagePlugins,
275275
)
276+
277+
if (success != null) {
278+
success.invoke(this, coilImageState, painter)
279+
} else {
280+
imageOptions.LandscapistImage(
281+
modifier = Modifier
282+
.constraint(this)
283+
.testTag(imageOptions.tag),
284+
painter = painter,
285+
)
286+
}
276287
}
277288
}
278289
}

coil3/src/commonMain/kotlin/com/skydoves/landscapist/coil3/CoilImage.kt

Lines changed: 54 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ import com.skydoves.landscapist.components.imagePlugins
4646
import com.skydoves.landscapist.components.rememberImageComponent
4747
import com.skydoves.landscapist.constraints.Constrainable
4848
import com.skydoves.landscapist.constraints.constraint
49+
import com.skydoves.landscapist.crossfade.CrossfadePlugin
50+
import com.skydoves.landscapist.crossfade.CrossfadeWithEffect
4951
import com.skydoves.landscapist.plugins.ImagePlugin
5052
import kotlinx.coroutines.flow.channelFlow
5153

@@ -199,54 +201,63 @@ public fun CoilImage(
199201
}
200202
}
201203

202-
when (coilImageState) {
203-
is CoilImageState.None -> Unit
204+
val crossfadePlugin = component.imagePlugins.filterIsInstance<CrossfadePlugin>().firstOrNull()
204205

205-
is CoilImageState.Loading -> {
206-
component.ComposeLoadingStatePlugins(
207-
modifier = Modifier.constraint(this),
208-
imageOptions = imageOptions,
209-
executor = { size ->
210-
CoilThumbnail(
211-
requestSize = size,
212-
recomposeKey = StableHolder(imageRequest.invoke()),
213-
imageLoader = StableHolder(imageLoader.invoke()),
214-
imageOptions = imageOptions,
215-
)
216-
},
217-
)
218-
loading?.invoke(this, coilImageState)
219-
}
206+
CrossfadeWithEffect(
207+
targetState = coilImageState,
208+
durationMs = crossfadePlugin?.duration ?: 0,
209+
contentKey = { coilImageState },
210+
enabled = crossfadePlugin != null,
211+
) {
212+
when (coilImageState) {
213+
is CoilImageState.None -> Unit
220214

221-
is CoilImageState.Failure -> {
222-
component.ComposeFailureStatePlugins(
223-
modifier = Modifier.constraint(this),
224-
imageOptions = imageOptions,
225-
reason = coilImageState.reason,
226-
)
227-
failure?.invoke(this, coilImageState)
228-
}
229-
230-
is CoilImageState.Success -> {
231-
component.ComposeSuccessStatePlugins(
232-
modifier = Modifier.constraint(this),
233-
imageModel = imageRequest.invoke().data,
234-
imageOptions = imageOptions,
235-
imageBitmap = coilImageState.imageBitmap,
236-
)
215+
is CoilImageState.Loading -> {
216+
component.ComposeLoadingStatePlugins(
217+
modifier = Modifier.constraint(this),
218+
imageOptions = imageOptions,
219+
executor = { size ->
220+
CoilThumbnail(
221+
requestSize = size,
222+
recomposeKey = StableHolder(imageRequest.invoke()),
223+
imageLoader = StableHolder(imageLoader.invoke()),
224+
imageOptions = imageOptions,
225+
)
226+
},
227+
)
228+
loading?.invoke(this, coilImageState)
229+
}
237230

238-
val image = coilImageState.image ?: return@ImageRequest
239-
val painter = rememberImagePainter(image = image, imagePlugins = component.imagePlugins)
231+
is CoilImageState.Failure -> {
232+
component.ComposeFailureStatePlugins(
233+
modifier = Modifier.constraint(this),
234+
imageOptions = imageOptions,
235+
reason = coilImageState.reason,
236+
)
237+
failure?.invoke(this, coilImageState)
238+
}
240239

241-
if (success != null) {
242-
success.invoke(this, coilImageState, painter)
243-
} else {
244-
imageOptions.LandscapistImage(
245-
modifier = Modifier
246-
.constraint(this)
247-
.testTag(imageOptions.tag),
248-
painter = painter,
240+
is CoilImageState.Success -> {
241+
component.ComposeSuccessStatePlugins(
242+
modifier = Modifier.constraint(this),
243+
imageModel = imageRequest.invoke().data,
244+
imageOptions = imageOptions,
245+
imageBitmap = coilImageState.imageBitmap,
249246
)
247+
248+
val image = coilImageState.image ?: return@CrossfadeWithEffect
249+
val painter = rememberImagePainter(image = image, imagePlugins = component.imagePlugins)
250+
251+
if (success != null) {
252+
success.invoke(this, coilImageState, painter)
253+
} else {
254+
imageOptions.LandscapistImage(
255+
modifier = Modifier
256+
.constraint(this)
257+
.testTag(imageOptions.tag),
258+
painter = painter,
259+
)
260+
}
250261
}
251262
}
252263
}

fresco/src/main/kotlin/com/skydoves/landscapist/fresco/FrescoImage.kt

Lines changed: 62 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ import com.skydoves.landscapist.components.ImageComponent
4646
import com.skydoves.landscapist.components.imagePlugins
4747
import com.skydoves.landscapist.components.rememberImageComponent
4848
import com.skydoves.landscapist.constraints.constraint
49+
import com.skydoves.landscapist.crossfade.CrossfadePlugin
50+
import com.skydoves.landscapist.crossfade.CrossfadeWithEffect
4951
import com.skydoves.landscapist.plugins.ImagePlugin
5052
import com.skydoves.landscapist.rememberBitmapPainter
5153
import kotlinx.coroutines.suspendCancellableCoroutine
@@ -122,64 +124,73 @@ public fun FrescoImage(
122124
modifier = modifier,
123125
) ImageRequest@{ imageState ->
124126

125-
val state: FrescoImageState = imageState.toFrescoImageState()
126-
val frescoImageState = remember(state) {
127-
state.apply {
128-
onImageStateChanged.invoke(this)
129-
}
130-
}
127+
val crossfadePlugin = component.imagePlugins.filterIsInstance<CrossfadePlugin>().firstOrNull()
131128

132-
when (frescoImageState) {
133-
is FrescoImageState.None -> Unit
134-
135-
is FrescoImageState.Loading -> {
136-
component.ComposeLoadingStatePlugins(
137-
modifier = Modifier.constraint(this),
138-
imageOptions = imageOptions,
139-
executor = { size ->
140-
FrescoImageThumbnail(
141-
requestSize = size,
142-
recomposeKey = imageUrl,
143-
imageOptions = imageOptions,
144-
imageRequest = StableHolder(imageRequest.invoke()),
145-
)
146-
},
147-
)
148-
loading?.invoke(this, frescoImageState)
129+
CrossfadeWithEffect(
130+
targetState = imageState,
131+
durationMs = crossfadePlugin?.duration ?: 0,
132+
contentKey = { imageState },
133+
enabled = crossfadePlugin != null,
134+
) {
135+
val state: FrescoImageState = imageState.toFrescoImageState()
136+
val frescoImageState = remember(state) {
137+
state.apply {
138+
onImageStateChanged.invoke(this)
139+
}
149140
}
150141

151-
is FrescoImageState.Failure -> {
152-
component.ComposeFailureStatePlugins(
153-
modifier = Modifier.constraint(this),
154-
imageOptions = imageOptions,
155-
reason = frescoImageState.reason,
156-
)
157-
failure?.invoke(this, frescoImageState)
158-
}
142+
when (frescoImageState) {
143+
is FrescoImageState.None -> Unit
159144

160-
is FrescoImageState.Success -> {
161-
component.ComposeSuccessStatePlugins(
162-
modifier = Modifier.constraint(this),
163-
imageModel = imageUrl,
164-
imageOptions = imageOptions,
165-
imageBitmap = frescoImageState.imageBitmap,
166-
)
145+
is FrescoImageState.Loading -> {
146+
component.ComposeLoadingStatePlugins(
147+
modifier = Modifier.constraint(this),
148+
imageOptions = imageOptions,
149+
executor = { size ->
150+
FrescoImageThumbnail(
151+
requestSize = size,
152+
recomposeKey = imageUrl,
153+
imageOptions = imageOptions,
154+
imageRequest = StableHolder(imageRequest.invoke()),
155+
)
156+
},
157+
)
158+
loading?.invoke(this, frescoImageState)
159+
}
167160

168-
val imageBitmap = frescoImageState.imageBitmap ?: return@ImageRequest
169-
val painter = rememberBitmapPainter(
170-
imagePlugins = component.imagePlugins,
171-
imageBitmap = imageBitmap,
172-
)
161+
is FrescoImageState.Failure -> {
162+
component.ComposeFailureStatePlugins(
163+
modifier = Modifier.constraint(this),
164+
imageOptions = imageOptions,
165+
reason = frescoImageState.reason,
166+
)
167+
failure?.invoke(this, frescoImageState)
168+
}
173169

174-
if (success != null) {
175-
success.invoke(this, frescoImageState, painter)
176-
} else {
177-
imageOptions.LandscapistImage(
178-
modifier = Modifier
179-
.constraint(this)
180-
.testTag(imageOptions.tag),
181-
painter = painter,
170+
is FrescoImageState.Success -> {
171+
component.ComposeSuccessStatePlugins(
172+
modifier = Modifier.constraint(this),
173+
imageModel = imageUrl,
174+
imageOptions = imageOptions,
175+
imageBitmap = frescoImageState.imageBitmap,
182176
)
177+
178+
val imageBitmap = frescoImageState.imageBitmap ?: return@CrossfadeWithEffect
179+
val painter = rememberBitmapPainter(
180+
imagePlugins = component.imagePlugins,
181+
imageBitmap = imageBitmap,
182+
)
183+
184+
if (success != null) {
185+
success.invoke(this, frescoImageState, painter)
186+
} else {
187+
imageOptions.LandscapistImage(
188+
modifier = Modifier
189+
.constraint(this)
190+
.testTag(imageOptions.tag),
191+
painter = painter,
192+
)
193+
}
183194
}
184195
}
185196
}

0 commit comments

Comments
 (0)