Skip to content

Instantly share code, notes, and snippets.

@darvld
Created October 3, 2021 23:03
Show Gist options
  • Save darvld/eb3844474baf2f3fc6d3ab44a4b4b5f8 to your computer and use it in GitHub Desktop.
Save darvld/eb3844474baf2f3fc6d3ab44a4b4b5f8 to your computer and use it in GitHub Desktop.
A circular reveal effect modifier for Jetpack Compose.
package cu.spin.catalog.ui.components
import android.annotation.SuppressLint
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.updateTransition
import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.drawscope.clipPath
import androidx.compose.ui.platform.debugInspectorInfo
import kotlin.math.sqrt
/**A modifier that clips the composable content using an animated circle. The circle will
* expand/shrink with an animation whenever [visible] changes.
*
* For more fine-grained control over the transition, see this method's overload, which allows passing
* a [State] object to control the progress of the reveal animation.
*
* By default, the circle is centered in the content, but custom positions may be specified using
* [revealFrom]. Specified offsets should be between 0 (left/top) and 1 (right/bottom).*/
@SuppressLint("UnnecessaryComposedModifier")
fun Modifier.circularReveal(
visible: Boolean,
revealFrom: Offset = Offset(0.5f, 0.5f),
): Modifier = composed(
factory = {
val factor = updateTransition(visible, label = "Visibility")
.animateFloat(label = "revealFactor") { if (it) 1f else 0f }
circularReveal(factor, revealFrom)
},
inspectorInfo = debugInspectorInfo {
name = "circularReveal"
properties["visible"] = visible
properties["revealFrom"] = revealFrom
}
)
/**A modifier that clips the composable content using a circular shape. The radius of the circle
* will be determined by the [transitionProgress].
*
* The values of the progress should be between 0 and 1.
*
* By default, the circle is centered in the content, but custom positions may be specified using
* [revealFrom]. Specified offsets should be between 0 (left/top) and 1 (right/bottom).
* */
fun Modifier.circularReveal(
transitionProgress: State<Float>,
revealFrom: Offset = Offset(0.5f, 0.5f)
): Modifier {
return drawWithCache {
val path = Path()
val center = revealFrom.mapTo(size)
val radius = calculateRadius(revealFrom, size)
path.addOval(Rect(center, radius * transitionProgress.value))
onDrawWithContent {
clipPath(path) { this@onDrawWithContent.drawContent() }
}
}
}
private fun Offset.mapTo(size: Size): Offset {
return Offset(x * size.width, y * size.height)
}
private fun calculateRadius(normalizedOrigin: Offset, size: Size) = with(normalizedOrigin) {
val x = (if (x > 0.5f) x else 1 - x) * size.width
val y = (if (y > 0.5f) y else 1 - y) * size.height
sqrt(x * x + y * y)
}
@andraantariksa
Copy link

Thanks!

@Skaldebane
Copy link

Super useful, thanks!

@vrajendraBhavsar
Copy link

Thanks, It's working like a charm!

@vikas-kmr1
Copy link

@vrajendraBhavsar @Skaldebane @andraantariksa @darvld
Can any help me with, how to use it for a composable?

@yasincidem
Copy link

yasincidem commented Nov 8, 2023

Here is a modification in case someone has to make the composable invisible when transitionProgress value equals to zero

fun Modifier.circularReveal(
    transitionProgress: State<Float>,
    revealFrom: Offset = Offset(0.5f, 0.5f)
): Modifier {
    return if (transitionProgress.value == 0f) {
        Modifier.layout { measurable, constraints ->
            val placeable = measurable.measure(constraints)
            layout(placeable.width, placeable.height) {}
        }
    } else {
        drawWithCache {
            val path = Path()

            val center = revealFrom.mapTo(size)
            val radius = calculateRadius(revealFrom, size)

            path.addOval(Rect(center, radius * transitionProgress.value))

            onDrawWithContent {
                clipPath(path) { this@onDrawWithContent.drawContent() }
            }
        }
    }
}
Modifier.layout { measurable, constraints ->
            val placeable = measurable.measure(constraints)
            layout(placeable.width, placeable.height) {}
        }

@kibotu
Copy link

kibotu commented Dec 22, 2023

thanks for sharing, I've also added a few modifications in case you want to reveal only a box instead of the entire thing.

here is also a preview example

https://gist.github.com/kibotu/996eab1b82237a94b2faedc5a90746e7

(check out the RevealTest)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment