Skip to content

Commit

Permalink
Merge pull request #164 from skydoves/animation/crossfade
Browse files Browse the repository at this point in the history
Introduce CrossfadePlugin
  • Loading branch information
skydoves authored Sep 10, 2022
2 parents 1e44874 + 3c1464f commit 793a084
Show file tree
Hide file tree
Showing 13 changed files with 395 additions and 50 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ import com.github.skydoves.landscapistdemo.theme.background800
import com.github.skydoves.landscapistdemo.theme.shimmerHighLight
import com.skydoves.landscapist.ImageOptions
import com.skydoves.landscapist.animation.circular.CircularRevealPlugin
import com.skydoves.landscapist.animation.crossfade.CrossfadePlugin
import com.skydoves.landscapist.coil.CoilImage
import com.skydoves.landscapist.components.rememberImageComponent
import com.skydoves.landscapist.fresco.FrescoImage
Expand Down Expand Up @@ -122,6 +123,9 @@ private fun PosterItem(
.size(50.dp)
.clickable { vm.poster.value = poster },
imageOptions = ImageOptions(contentScale = ContentScale.Crop),
component = rememberImageComponent {
+CrossfadePlugin()
},
previewPlaceholder = R.drawable.poster
)
}
Expand Down
7 changes: 7 additions & 0 deletions landscapist-animation/api/landscapist-animation.api
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,10 @@ public final class com/skydoves/landscapist/animation/circular/CircularRevealPlu
public fun toString ()Ljava/lang/String;
}

public final class com/skydoves/landscapist/animation/crossfade/CrossfadePlugin : com/skydoves/landscapist/plugins/ImagePlugin$PainterPlugin {
public fun <init> ()V
public fun <init> (I)V
public synthetic fun <init> (IILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun compose (Landroidx/compose/ui/graphics/ImageBitmap;Landroidx/compose/ui/graphics/painter/Painter;Landroidx/compose/runtime/Composer;I)Landroidx/compose/ui/graphics/painter/Painter;
}

Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.painter.Painter

/**
* circularReveal is an extension of the [Painter] for animating a clipping circle to reveal an image.
* This is an extension of the [Painter] for animating a clipping circle to reveal an image.
* The animation has two states [CircularRevealState.None], [CircularRevealState.Finished].
*
* @param imageBitmap an image bitmap for loading the content.
Expand All @@ -39,8 +39,11 @@ internal fun Painter.rememberCircularRevealPainter(
onFinishListener: CircularRevealFinishListener? = null
): Painter {
// Defines a transition of `CircularRevealState`, and updates the transition when the provided state changes.
val transitionState = remember { MutableTransitionState(CircularRevealState.None) }
transitionState.targetState = CircularRevealState.Finished
val transitionState = remember {
MutableTransitionState(CircularRevealState.None).apply {
targetState = CircularRevealState.Finished
}
}

// Our actual transition, which reads our transitionState
val transition = updateTransition(transitionState, label = null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,61 +50,57 @@ internal class CircularRevealPainter(
var radius by mutableStateOf(0f, policy = neverEqualPolicy())

override fun DrawScope.onDraw() {
val paint = paintPool.acquire() ?: Paint()
val shaderMatrix = Matrix()
var scale: Float
var dx = 0f
var dy = 0f
var scale: Float
val shaderMatrix = Matrix()
val shader = ImageShader(imageBitmap, TileMode.Clamp)
val brush = ShaderBrush(shader)
val paint = paintPool.acquire() ?: Paint()
paint.asFrameworkPaint().apply {
isAntiAlias = true
isDither = true
isFilterBitmap = true
}

try {
val shader = ImageShader(imageBitmap, TileMode.Clamp)
val brush = ShaderBrush(shader)

paint.asFrameworkPaint().apply {
isAntiAlias = true
isDither = true
isFilterBitmap = true
}

drawIntoCanvas { canvas ->
canvas.saveLayer(size.toRect(), paint)

val mDrawableRect = RectF(0f, 0f, size.width, size.height)
val bitmapWidth: Int = imageBitmap.asAndroidBitmap().width
val bitmapHeight: Int = imageBitmap.asAndroidBitmap().height

if (bitmapWidth * mDrawableRect.height() > mDrawableRect.width() * bitmapHeight) {
scale = mDrawableRect.height() / bitmapHeight.toFloat()
dx = (mDrawableRect.width() - bitmapWidth * scale) * 0.5f
} else {
scale = mDrawableRect.width() / bitmapWidth.toFloat()
dy = (mDrawableRect.height() - bitmapHeight * scale) * 0.5f
}

// resize the matrix to scale by sx and sy.
shaderMatrix.setScale(scale, scale)

// post translate the matrix with the specified translation.
shaderMatrix.postTranslate(
(dx + 0.5f) + mDrawableRect.left,
(dy + 0.5f) + mDrawableRect.top
)

shader.setLocalMatrix(shaderMatrix)
drawIntoCanvas { canvas ->
// cache the paint in the internal stack.
canvas.saveLayer(size.toRect(), paint)

val calculatedRadius = size.width.coerceAtLeast(size.height) * radius
drawCircle(brush, calculatedRadius, Offset(size.width / 2, size.height / 2))
val mDrawableRect = RectF(0f, 0f, size.width, size.height)
val bitmapWidth: Int = imageBitmap.asAndroidBitmap().width
val bitmapHeight: Int = imageBitmap.asAndroidBitmap().height

canvas.restore()
if (bitmapWidth * mDrawableRect.height() > mDrawableRect.width() * bitmapHeight) {
scale = mDrawableRect.height() / bitmapHeight.toFloat()
dx = (mDrawableRect.width() - bitmapWidth * scale) * 0.5f
} else {
scale = mDrawableRect.width() / bitmapWidth.toFloat()
dy = (mDrawableRect.height() - bitmapHeight * scale) * 0.5f
}
} finally {

// resize the matrix to scale by sx and sy.
shaderMatrix.setScale(scale, scale)

// post translate the matrix with the specified translation.
shaderMatrix.postTranslate(
(dx + 0.5f) + mDrawableRect.left,
(dy + 0.5f) + mDrawableRect.top
)
// apply the scaled matrix to the shader.
shader.setLocalMatrix(shaderMatrix)
// calculate radius and draw an image bitmap as a circle.
val calculatedRadius = size.width.coerceAtLeast(size.height) * radius
drawCircle(brush, calculatedRadius, Offset(size.width / 2, size.height / 2))
// restore canvas.
canvas.restore()
// resets the paint and release to the pool.
paint.asFrameworkPaint().reset()
paintPool.release(paint)
}
}

/** return the dimension size of the [ImageBitmap]'s intrinsic width and height. */
/** return the dimension size of the [painter]'s intrinsic width and height. */
override val intrinsicSize: Size get() = painter.intrinsicSize
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import androidx.compose.ui.graphics.painter.Painter
import com.skydoves.landscapist.plugins.ImagePlugin

/**
* An image plugin that extends [ImagePlugin.PainterPlugin] to be executed over the given painter.ø
* An image plugin that extends [ImagePlugin.PainterPlugin] to be executed while rendering painters.
*
* @property duration milli-second times from start to finish animation.
* @property onFinishListener A finish listener of the circular reveal animation.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Designed and developed by 2020-2022 skydoves (Jaewoong Eum)
*
* 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
*
* http://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.
*/
package com.skydoves.landscapist.animation.crossfade

import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ColorMatrix
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.painter.Painter

/**
* This is an extension of the [Painter] for giving crossfade animation to the given [imageBitmap].
*
* @param imageBitmap an image bitmap for loading the content.
* @param durationMs milli-second times from start to finish animation.
*/
@Composable
internal fun Painter.rememberCrossfadePainter(
imageBitmap: ImageBitmap,
durationMs: Int
): Painter {
val size = Size(imageBitmap.width.toFloat(), imageBitmap.height.toFloat())
val colorMatrix = remember { ColorMatrix() }
val fadeInTransition = updateFadeInTransition(key = size, durationMs = durationMs)
val transitionColorFilter = if (!fadeInTransition.isFinished) {
colorMatrix.apply {
updateAlpha(fadeInTransition.alpha)
updateBrightness(fadeInTransition.brightness)
updateSaturation(fadeInTransition.saturation)
}.let { ColorFilter.colorMatrix(it) }
} else {
// If the fade-in isn't running, reset the color matrix
null
}

return remember(
key1 = fadeInTransition.alpha,
key2 = fadeInTransition.brightness,
key3 = fadeInTransition.saturation
) {
CrossfadePainter(
imageBitmap = imageBitmap,
painter = this
).also {
it.transitionColorFilter = transitionColorFilter
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* Designed and developed by 2020-2022 skydoves (Jaewoong Eum)
*
* 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
*
* http://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.
*/
package com.skydoves.landscapist.animation.crossfade

import android.graphics.Matrix
import android.graphics.RectF
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.geometry.toRect
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.ImageShader
import androidx.compose.ui.graphics.Paint
import androidx.compose.ui.graphics.ShaderBrush
import androidx.compose.ui.graphics.TileMode
import androidx.compose.ui.graphics.asAndroidBitmap
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.painter.Painter
import androidx.core.util.Pools

/**
* CrossfadePainter is a [Painter] that applies crossfade filter effect on the given [imageBitmap].
*
* @param imageBitmap an image bitmap for loading for the content.
* @param painter an image painter to draw an [ImageBitmap] into the provided canvas.
*/
internal class CrossfadePainter(
private val imageBitmap: ImageBitmap,
private val painter: Painter
) : Painter() {

/** return the dimension size of the [painter]'s intrinsic width and height. */
override val intrinsicSize: Size get() = painter.intrinsicSize

/** color filter that will be applied to draw the [imageBitmap]. */
var transitionColorFilter by mutableStateOf<ColorFilter?>(null)

override fun DrawScope.onDraw() {
drawIntoCanvas { canvas ->
var dx = 0f
var dy = 0f
val scale: Float
val shaderMatrix = Matrix()
val shader = ImageShader(imageBitmap, TileMode.Clamp)
val brush = ShaderBrush(shader)
val paint = paintPool.acquire() ?: Paint()
paint.asFrameworkPaint().apply {
isAntiAlias = true
isDither = true
isFilterBitmap = true
}

// cache the paint in the internal stack.
canvas.saveLayer(size.toRect(), paint)

val mDrawableRect = RectF(0f, 0f, size.width, size.height)
val bitmapWidth: Int = imageBitmap.asAndroidBitmap().width
val bitmapHeight: Int = imageBitmap.asAndroidBitmap().height

if (bitmapWidth * mDrawableRect.height() > mDrawableRect.width() * bitmapHeight) {
scale = mDrawableRect.height() / bitmapHeight.toFloat()
dx = (mDrawableRect.width() - bitmapWidth * scale) * 0.5f
} else {
scale = mDrawableRect.width() / bitmapWidth.toFloat()
dy = (mDrawableRect.height() - bitmapHeight * scale) * 0.5f
}

// resize the matrix to scale by sx and sy.
shaderMatrix.setScale(scale, scale)

// post translate the matrix with the specified translation.
shaderMatrix.postTranslate(
(dx + 0.5f) + mDrawableRect.left,
(dy + 0.5f) + mDrawableRect.top
)
// apply the scaled matrix to the shader.
shader.setLocalMatrix(shaderMatrix)
// draw an image bitmap as a rect.
drawRect(brush = brush, colorFilter = transitionColorFilter)
// restore canvas.
canvas.restore()
// resets the paint and release to the pool.
paint.asFrameworkPaint().reset()
paintPool.release(paint)
}
}
}

/** paint pool which caching and reusing [Paint] instances. */
private val paintPool = Pools.SimplePool<Paint>(2)
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Designed and developed by 2020-2022 skydoves (Jaewoong Eum)
*
* 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
*
* http://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.
*/
package com.skydoves.landscapist.animation.crossfade

import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.painter.Painter
import com.skydoves.landscapist.plugins.ImagePlugin

/**
* An image plugin that extends [ImagePlugin.PainterPlugin] to be executed while rendering painters.
*
* @property duration milli-second times from start to finish animation.
*/
@Immutable
public class CrossfadePlugin(
private val duration: Int = 700
) : ImagePlugin.PainterPlugin {

@Composable
override fun compose(imageBitmap: ImageBitmap, painter: Painter): Painter {
return painter.rememberCrossfadePainter(
imageBitmap = imageBitmap,
durationMs = duration
)
}
}
Loading

0 comments on commit 793a084

Please sign in to comment.