Skip to content

Instantly share code, notes, and snippets.

@darvld
Created October 3, 2021 23:03

Revisions

  1. darvld created this gist Oct 3, 2021.
    79 changes: 79 additions & 0 deletions CircularReveal.kt
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,79 @@
    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)
    }