From 4ea5c40cc065d9d26593d3031cda016bcbdf0c0d Mon Sep 17 00:00:00 2001 From: andretortolano Date: Wed, 26 Jun 2024 13:34:53 -0400 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20EffectsTrait=20+=20Confetti?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- appcues/build.gradle | 2 + .../data/mapper/styling/StyleColorMapper.kt | 4 ++ .../data/mapper/styling/StyleMapper.kt | 18 ++++--- .../appcues/data/model/AppcuesConfigMapExt.kt | 13 +++++ .../data/model/styling/ComponentStyle.kt | 1 + .../appcues/response/styling/StyleResponse.kt | 1 + .../java/com/appcues/trait/TraitRegistry.kt | 2 + .../com/appcues/trait/appcues/EffectsTrait.kt | 54 +++++++++++++++++++ .../trait/appcues/effects/ConfettiEffects.kt | 47 ++++++++++++++++ 9 files changed, 134 insertions(+), 8 deletions(-) create mode 100644 appcues/src/main/java/com/appcues/trait/appcues/EffectsTrait.kt create mode 100644 appcues/src/main/java/com/appcues/trait/appcues/effects/ConfettiEffects.kt diff --git a/appcues/build.gradle b/appcues/build.gradle index 9c0c35715..df3cb37d0 100644 --- a/appcues/build.gradle +++ b/appcues/build.gradle @@ -110,6 +110,8 @@ dependencies { // Play In-App Review implementation 'com.google.android.play:review:2.0.1' implementation 'com.google.android.play:review-ktx:2.0.1' + // Animation + implementation 'nl.dionsegijn:konfetti-compose:2.0.4' testImplementation "junit:junit:4.13.2" testImplementation "io.mockk:mockk:1.13.5" diff --git a/appcues/src/main/java/com/appcues/data/mapper/styling/StyleColorMapper.kt b/appcues/src/main/java/com/appcues/data/mapper/styling/StyleColorMapper.kt index 99058bc7b..acbf8029c 100644 --- a/appcues/src/main/java/com/appcues/data/mapper/styling/StyleColorMapper.kt +++ b/appcues/src/main/java/com/appcues/data/mapper/styling/StyleColorMapper.kt @@ -12,3 +12,7 @@ internal fun StyleColorResponse.mapComponentColor(): ComponentColor { dark = dark, ) } + +internal fun List?.mapToColors(): List { + return this?.map { normalizeToArgbLong(it) } ?: listOf() +} diff --git a/appcues/src/main/java/com/appcues/data/mapper/styling/StyleMapper.kt b/appcues/src/main/java/com/appcues/data/mapper/styling/StyleMapper.kt index 70aed7555..bc5d74000 100644 --- a/appcues/src/main/java/com/appcues/data/mapper/styling/StyleMapper.kt +++ b/appcues/src/main/java/com/appcues/data/mapper/styling/StyleMapper.kt @@ -8,14 +8,6 @@ import com.appcues.data.remote.appcues.response.styling.StyleResponse internal fun StyleResponse?.mapComponentStyle(): ComponentStyle { if (this == null) return ComponentStyle() - fun StyleGradientColorResponse?.toComponentColorList(): List? { - if (this == null) return null - return arrayListOf().apply { - colors.forEach { fromColor -> - add(fromColor.mapComponentColor()) - } - } - } return ComponentStyle( width = width, height = height, @@ -32,6 +24,7 @@ internal fun StyleResponse?.mapComponentStyle(): ComponentStyle { backgroundColor = backgroundColor?.mapComponentColor(), backgroundImage = backgroundImage?.mapComponentBackgroundImage(), shadow = shadow?.mapComponentShadow(), + colors = colors.mapToColors(), // Not dealing with direction, every gradient is horizontal from start to end backgroundGradient = backgroundGradient.toComponentColorList(), borderColor = borderColor?.mapComponentColor(), @@ -45,3 +38,12 @@ internal fun StyleResponse?.mapComponentStyle(): ComponentStyle { horizontalAlignment = mapComponentHorizontalAlignment(horizontalAlignment), ) } + +private fun StyleGradientColorResponse?.toComponentColorList(): List? { + if (this == null) return null + return arrayListOf().apply { + colors.forEach { fromColor -> + add(fromColor.mapComponentColor()) + } + } +} diff --git a/appcues/src/main/java/com/appcues/data/model/AppcuesConfigMapExt.kt b/appcues/src/main/java/com/appcues/data/model/AppcuesConfigMapExt.kt index 0bd11d2a6..aa5af464c 100644 --- a/appcues/src/main/java/com/appcues/data/model/AppcuesConfigMapExt.kt +++ b/appcues/src/main/java/com/appcues/data/model/AppcuesConfigMapExt.kt @@ -42,6 +42,19 @@ internal fun AppcuesConfigMap.getConfigInt(key: String): Int? { } } +internal fun AppcuesConfigMap.getConfigDouble(key: String): Double? { + // if hash config is null, return default + if (this == null) return null + // get value by key as Double? + return get(key)?.let { + when (it) { + is Double -> it + is Int -> it.toDouble() + else -> null + } + } +} + internal fun AppcuesConfigMap.getConfigStyle(key: String): ComponentStyle? { return getConfig(key)?.let { MoshiConfiguration.fromAny(it).mapComponentStyle() diff --git a/appcues/src/main/java/com/appcues/data/model/styling/ComponentStyle.kt b/appcues/src/main/java/com/appcues/data/model/styling/ComponentStyle.kt index 0ef2d07d0..c3248f00e 100644 --- a/appcues/src/main/java/com/appcues/data/model/styling/ComponentStyle.kt +++ b/appcues/src/main/java/com/appcues/data/model/styling/ComponentStyle.kt @@ -20,6 +20,7 @@ internal data class ComponentStyle( val borderColor: ComponentColor? = null, val borderWidth: Double? = null, val shadow: ComponentShadow? = null, + val colors: List? = null, // Text related properties val fontName: String? = null, diff --git a/appcues/src/main/java/com/appcues/data/remote/appcues/response/styling/StyleResponse.kt b/appcues/src/main/java/com/appcues/data/remote/appcues/response/styling/StyleResponse.kt index af4f2bc31..44bbb4271 100644 --- a/appcues/src/main/java/com/appcues/data/remote/appcues/response/styling/StyleResponse.kt +++ b/appcues/src/main/java/com/appcues/data/remote/appcues/response/styling/StyleResponse.kt @@ -16,6 +16,7 @@ internal data class StyleResponse( val paddingTrailing: Double? = null, val cornerRadius: Double? = null, val shadow: StyleShadowResponse? = null, + val colors: List? = null, val foregroundColor: StyleColorResponse? = null, val backgroundColor: StyleColorResponse? = null, val backgroundGradient: StyleGradientColorResponse? = null, diff --git a/appcues/src/main/java/com/appcues/trait/TraitRegistry.kt b/appcues/src/main/java/com/appcues/trait/TraitRegistry.kt index 9f0d261c0..efc94e400 100644 --- a/appcues/src/main/java/com/appcues/trait/TraitRegistry.kt +++ b/appcues/src/main/java/com/appcues/trait/TraitRegistry.kt @@ -10,6 +10,7 @@ import com.appcues.trait.appcues.BackdropKeyholeTrait import com.appcues.trait.appcues.BackdropTrait import com.appcues.trait.appcues.BackgroundContentTrait import com.appcues.trait.appcues.CarouselTrait +import com.appcues.trait.appcues.EffectsTrait import com.appcues.trait.appcues.EmbedTrait import com.appcues.trait.appcues.ModalTrait import com.appcues.trait.appcues.PagingDotsTrait @@ -47,6 +48,7 @@ internal class TraitRegistry( register(TargetElementTrait.TYPE) { config, _ -> TargetElementTrait(config) } register(TargetRectangleTrait.TYPE) { config, _ -> TargetRectangleTrait(config) } register(BackdropTrait.TYPE) { config, _ -> BackdropTrait(config) } + register(EffectsTrait.TYPE) { config, _ -> EffectsTrait(config) } register(BackdropKeyholeTrait.TYPE) { config, _ -> BackdropKeyholeTrait(config) } register(CarouselTrait.TYPE) { config, _ -> CarouselTrait(config) } register(PagingDotsTrait.TYPE) { config, _ -> PagingDotsTrait(config) } diff --git a/appcues/src/main/java/com/appcues/trait/appcues/EffectsTrait.kt b/appcues/src/main/java/com/appcues/trait/appcues/EffectsTrait.kt new file mode 100644 index 000000000..c881275f5 --- /dev/null +++ b/appcues/src/main/java/com/appcues/trait/appcues/EffectsTrait.kt @@ -0,0 +1,54 @@ +package com.appcues.trait.appcues + +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.runtime.Composable +import com.appcues.data.model.AppcuesConfigMap +import com.appcues.data.model.getConfig +import com.appcues.data.model.getConfigDouble +import com.appcues.data.model.getConfigInt +import com.appcues.data.model.getConfigStyle +import com.appcues.trait.AppcuesTraitException +import com.appcues.trait.BackdropDecoratingTrait +import com.appcues.trait.appcues.EffectsTrait.PresentationStyle.CONFETTI +import com.appcues.trait.appcues.effects.ConfettiEffect + +internal class EffectsTrait( + override val config: AppcuesConfigMap, +) : BackdropDecoratingTrait { + + companion object { + + const val TYPE = "@appcues/effects" + const val DEFAULT_DURATION = 2000 + const val DEFAULT_INTENSITY = 1.0 + } + + private enum class PresentationStyle { + CONFETTI + } + + private val presentationStyle = config.getConfig("presentationStyle").toPresentationStyle() + + private val duration = config.getConfigInt("duration") ?: DEFAULT_DURATION + + private val intensity = config.getConfigDouble("intensity") ?: DEFAULT_INTENSITY + + private val style = config.getConfigStyle("style") + + @Composable + override fun BoxScope.BackdropDecorate(content: @Composable BoxScope.() -> Unit) { + // other backdrop decorate traits renders first (putting this one on top) + content() + + when (presentationStyle) { + CONFETTI -> ConfettiEffect(style, duration, intensity) + } + } + + private fun String?.toPresentationStyle(): PresentationStyle { + return when (this) { + "confetti" -> CONFETTI + else -> throw AppcuesTraitException("invalid effects presentation style: $this") + } + } +} diff --git a/appcues/src/main/java/com/appcues/trait/appcues/effects/ConfettiEffects.kt b/appcues/src/main/java/com/appcues/trait/appcues/effects/ConfettiEffects.kt new file mode 100644 index 000000000..a16da3c6f --- /dev/null +++ b/appcues/src/main/java/com/appcues/trait/appcues/effects/ConfettiEffects.kt @@ -0,0 +1,47 @@ +package com.appcues.trait.appcues.effects + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import com.appcues.data.mapper.styling.mapToColors +import com.appcues.data.model.styling.ComponentStyle +import nl.dionsegijn.konfetti.compose.KonfettiView +import nl.dionsegijn.konfetti.core.Party +import nl.dionsegijn.konfetti.core.Position.Relative +import nl.dionsegijn.konfetti.core.emitter.Emitter +import nl.dionsegijn.konfetti.core.models.Size +import java.util.concurrent.TimeUnit.MILLISECONDS + +private const val EMITTER_INTENSITY_RAW = 200 + +@Composable +internal fun ConfettiEffect(style: ComponentStyle?, duration: Int, intensity: Double) { + // default colors in case colors style property is empty + val colors = remember { + val styleColors = style?.colors ?: listOf() + + styleColors.ifEmpty { listOf("#5C5CFF", "#20E0D6", "#FF5290").mapToColors() }.map { it.toInt() } + } + + // this is a center point on the top of the screen, that will disperse particles + // in a 180 angle spread before it start falling straight down + val party = Party( + speed = 0f, + maxSpeed = 60f, + damping = 0.9f, + spread = 180, + size = listOf(Size(sizeInDp = 10), Size(sizeInDp = 12), Size(sizeInDp = 16)), + angle = 270, + timeToLive = 5000, + fadeOutEnabled = true, + colors = colors, + position = Relative(x = 0.5, y = -0.05), + emitter = Emitter(duration = duration.toLong(), MILLISECONDS).perSecond((EMITTER_INTENSITY_RAW * intensity).toInt()) + ) + + KonfettiView( + modifier = Modifier.fillMaxSize(), + parties = listOf(party), + ) +}