From 930d1438cdb2d94bb4ff0caae5c5c6bcb92f70cc Mon Sep 17 00:00:00 2001 From: Nelson Glauber Date: Tue, 25 Jun 2024 11:02:56 -0300 Subject: [PATCH 1/4] Added finiteAnimationSpec common functions support --- .../liveview/data/constants/Attrs.kt | 6 +- .../data/constants/ComposableTypes.kt | 1 + .../liveview/data/constants/Values.kt | 6 + .../domain/extensions/ListExtensions.kt | 20 - .../liveview/ui/modifiers/ModifiersParser.kt | 5 +- .../liveview/ui/modifiers/Util.kt | 4 +- .../ui/registry/ComposableNodeFactory.kt | 2 + .../liveview/ui/view/CrossfadeView.kt | 89 +++++ .../liveview/ui/view/SharedAttributes.kt | 250 +++++++++++- .../liveview/ui/modifiers/ActionsTest.kt | 39 +- .../liveview/ui/view/SharedAttributesTest.kt | 355 ++++++++++++++++++ 11 files changed, 735 insertions(+), 42 deletions(-) delete mode 100644 liveview-android/src/main/java/org/phoenixframework/liveview/domain/extensions/ListExtensions.kt create mode 100644 liveview-android/src/main/java/org/phoenixframework/liveview/ui/view/CrossfadeView.kt diff --git a/liveview-android/src/main/java/org/phoenixframework/liveview/data/constants/Attrs.kt b/liveview-android/src/main/java/org/phoenixframework/liveview/data/constants/Attrs.kt index 8e509b3f..2acee7de 100644 --- a/liveview-android/src/main/java/org/phoenixframework/liveview/data/constants/Attrs.kt +++ b/liveview-android/src/main/java/org/phoenixframework/liveview/data/constants/Attrs.kt @@ -10,6 +10,7 @@ object Attrs { const val attrAlignment = "alignment" const val attrAlpha = "alpha" const val attrAlwaysShowLabel = "alwaysShowLabel" + const val attrAnimationSpec = "animationSpec" const val attrAutoCorrect = "autoCorrect" const val attrBeyondBoundsPageCount = "beyondBoundsPageCount" const val attrBorder = "border" @@ -100,8 +101,10 @@ object Attrs { const val attrMinSize = "minSize" const val attrMinValue = "minValue" const val attrNavigate = "navigate" - const val attrOverflow = "overflow" + const val attrOffsetMillis = "offsetMillis" + const val attrOffsetType = "offsetType" const val attrOnDismissRequest = "onDismissRequest" + const val attrOverflow = "overflow" const val attrPageCount = "pageCount" const val attrPageSize = "pageSize" const val attrPageSpacing = "pageSpacing" @@ -151,6 +154,7 @@ object Attrs { const val attrStrokeCap = "strokeCap" const val attrStrokeWidth = "strokeWidth" const val attrStyle = "style" + const val attrTargetState = "targetState" const val attrTemplate = "template" const val attrText = "text" const val attrTextAlign = "textAlign" diff --git a/liveview-android/src/main/java/org/phoenixframework/liveview/data/constants/ComposableTypes.kt b/liveview-android/src/main/java/org/phoenixframework/liveview/data/constants/ComposableTypes.kt index 72eaa3e4..e90fb678 100644 --- a/liveview-android/src/main/java/org/phoenixframework/liveview/data/constants/ComposableTypes.kt +++ b/liveview-android/src/main/java/org/phoenixframework/liveview/data/constants/ComposableTypes.kt @@ -17,6 +17,7 @@ object ComposableTypes { const val checkbox = "CheckBox" const val circularProgressIndicator = "CircularProgressIndicator" const val column = "Column" + const val crossfade = "Crossfade" const val datePicker = "DatePicker" const val datePickerDialog = "DatePickerDialog" const val dateRangePicker = "DateRangePicker" diff --git a/liveview-android/src/main/java/org/phoenixframework/liveview/data/constants/Values.kt b/liveview-android/src/main/java/org/phoenixframework/liveview/data/constants/Values.kt index 66d8a88d..fca3c518 100644 --- a/liveview-android/src/main/java/org/phoenixframework/liveview/data/constants/Values.kt +++ b/liveview-android/src/main/java/org/phoenixframework/liveview/data/constants/Values.kt @@ -59,6 +59,7 @@ object EnterExitTransitionFunctions { const val slideInHorizontally = "slideInHorizontally" // Argument names used for the functions above + const val argAnimationSpec = "animationSpec" const val argClip = "clip" const val argExpandFrom = "expandFrom" const val argHeight = "height" @@ -300,6 +301,11 @@ object SnackbarDurationValues { const val short = "short" } +object StartOffsetTypeValues { + const val delay = "Delay" + const val fastForward = "FastForward" +} + object StrokeCapValues { const val round = "round" const val square = "square" diff --git a/liveview-android/src/main/java/org/phoenixframework/liveview/domain/extensions/ListExtensions.kt b/liveview-android/src/main/java/org/phoenixframework/liveview/domain/extensions/ListExtensions.kt deleted file mode 100644 index 1c1dcb6b..00000000 --- a/liveview-android/src/main/java/org/phoenixframework/liveview/domain/extensions/ListExtensions.kt +++ /dev/null @@ -1,20 +0,0 @@ -package org.phoenixframework.liveview.domain.extensions - -fun List.orderedMix(other: List): List { - val count = count() - val otherCount = other.count() - val largerCount = maxOf(count, otherCount) - val listBuilder = ArrayList() - - for (i in 0..largerCount) { - if (i < count) { - listBuilder.add(this[i]) - } - - if (i < otherCount) { - listBuilder.add(other[i]) - } - } - - return listBuilder -} \ No newline at end of file diff --git a/liveview-android/src/main/java/org/phoenixframework/liveview/ui/modifiers/ModifiersParser.kt b/liveview-android/src/main/java/org/phoenixframework/liveview/ui/modifiers/ModifiersParser.kt index f5a130b3..01840a72 100644 --- a/liveview-android/src/main/java/org/phoenixframework/liveview/ui/modifiers/ModifiersParser.kt +++ b/liveview-android/src/main/java/org/phoenixframework/liveview/ui/modifiers/ModifiersParser.kt @@ -210,7 +210,10 @@ internal object ModifiersParser { val mapContext = mapExprContext.map() val mapEntryContext = mapContext?.map_entries()?.map_entry()?.map { mapEntryContext -> // The map key is the style name and the map value contain the list of modifiers - val styleName = mapEntryContext.expression(0).text.replace("\"", "") + val styleName = mapEntryContext.expression(0).text + .removePrefix("\"") + .removeSuffix("\"") + .replace("\\\"", "\"") // replacing backslash quotes by quotes val mapValueContext = mapEntryContext.expression(1) // Map value must be a list of tuples diff --git a/liveview-android/src/main/java/org/phoenixframework/liveview/ui/modifiers/Util.kt b/liveview-android/src/main/java/org/phoenixframework/liveview/ui/modifiers/Util.kt index 40c0cfc5..1305ac11 100644 --- a/liveview-android/src/main/java/org/phoenixframework/liveview/ui/modifiers/Util.kt +++ b/liveview-android/src/main/java/org/phoenixframework/liveview/ui/modifiers/Util.kt @@ -707,9 +707,9 @@ internal fun eventFromArgument(argument: ModifierDataAdapter.ArgumentData): Pair return if (argument.type == typeEvent) { val (event, args) = Pair( argument.listValue.getOrNull(0)?.stringValue, - argument.listValue.getOrNull(1)?.listValue?.map { it.value } + argument.listValue.getOrNull(1)?.listValue?.map { it.value } ?: emptyList() ) - if (event != null && args != null) { + if (event != null) { val pushArgs = if (args.isEmpty()) null else if (args.size == 1) args.first() else null event to pushArgs } else null diff --git a/liveview-android/src/main/java/org/phoenixframework/liveview/ui/registry/ComposableNodeFactory.kt b/liveview-android/src/main/java/org/phoenixframework/liveview/ui/registry/ComposableNodeFactory.kt index 2bfe6b0b..90a9acd7 100644 --- a/liveview-android/src/main/java/org/phoenixframework/liveview/ui/registry/ComposableNodeFactory.kt +++ b/liveview-android/src/main/java/org/phoenixframework/liveview/ui/registry/ComposableNodeFactory.kt @@ -20,6 +20,7 @@ import org.phoenixframework.liveview.ui.view.CardView import org.phoenixframework.liveview.ui.view.CheckBoxView import org.phoenixframework.liveview.ui.view.ChipView import org.phoenixframework.liveview.ui.view.ColumnView +import org.phoenixframework.liveview.ui.view.CrossfadeView import org.phoenixframework.liveview.ui.view.DatePickerDialogView import org.phoenixframework.liveview.ui.view.DatePickerView import org.phoenixframework.liveview.ui.view.DividerView @@ -93,6 +94,7 @@ object ComposableNodeFactory { ProgressIndicatorView.Factory ) registerComponent(ComposableTypes.column, ColumnView.Factory) + registerComponent(ComposableTypes.crossfade, CrossfadeView.Factory) registerComponent(ComposableTypes.datePicker, DatePickerView.Factory) registerComponent(ComposableTypes.datePickerDialog, DatePickerDialogView.Factory) registerComponent(ComposableTypes.dateRangePicker, DatePickerView.Factory) diff --git a/liveview-android/src/main/java/org/phoenixframework/liveview/ui/view/CrossfadeView.kt b/liveview-android/src/main/java/org/phoenixframework/liveview/ui/view/CrossfadeView.kt new file mode 100644 index 00000000..40b47a23 --- /dev/null +++ b/liveview-android/src/main/java/org/phoenixframework/liveview/ui/view/CrossfadeView.kt @@ -0,0 +1,89 @@ +package org.phoenixframework.liveview.ui.view + +import androidx.compose.animation.Crossfade +import androidx.compose.animation.core.FiniteAnimationSpec +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import kotlinx.collections.immutable.ImmutableList +import org.phoenixframework.liveview.data.constants.Attrs.attrAnimationSpec +import org.phoenixframework.liveview.data.constants.Attrs.attrLabel +import org.phoenixframework.liveview.data.constants.Attrs.attrTargetState +import org.phoenixframework.liveview.data.core.CoreAttribute +import org.phoenixframework.liveview.domain.data.ComposableTreeNode +import org.phoenixframework.liveview.ui.base.CommonComposableProperties +import org.phoenixframework.liveview.ui.base.ComposableProperties +import org.phoenixframework.liveview.ui.base.ComposableView +import org.phoenixframework.liveview.ui.base.ComposableViewFactory +import org.phoenixframework.liveview.ui.base.PushEvent +import org.phoenixframework.liveview.ui.phx_components.PhxLiveView + +internal class CrossfadeView private constructor(props: Properties) : + ComposableView(props) { + @Composable + override fun Compose( + composableNode: ComposableTreeNode?, + paddingValues: PaddingValues?, + pushEvent: PushEvent + ) { + val targetState = props.targetState + val animationSpec = props.animationSpec + val label = props.label + Crossfade( + targetState = targetState, + modifier = props.commonProps.modifier, + animationSpec = animationSpec ?: tween(), + label = label ?: "Crossfade", + ) { targetValue -> + // TODO Should we pass the target value to the server somehow? + targetValue + composableNode?.children?.forEach { + PhxLiveView(it, pushEvent, composableNode, null, this) + } + } + } + + @Stable + internal data class Properties( + val targetState: Any = Unit, + val animationSpec: FiniteAnimationSpec? = null, + val label: String? = null, + override val commonProps: CommonComposableProperties = CommonComposableProperties(), + ) : ComposableProperties + + internal object Factory : ComposableViewFactory() { + + override fun buildComposableView( + attributes: ImmutableList, + pushEvent: PushEvent?, + scope: Any?, + ): CrossfadeView = CrossfadeView(attributes.fold(Properties()) { props, attribute -> + when (attribute.name) { + attrAnimationSpec -> animationSpec(props, attribute.value) + attrLabel -> label(props, attribute.value) + attrTargetState -> targetState(props, attribute.value) + else -> props.copy( + commonProps = handleCommonAttributes( + props.commonProps, + attribute, + pushEvent, + scope + ) + ) + } + }) + + private fun animationSpec(props: Properties, value: String): Properties { + return props.copy(animationSpec = finiteAnimationSpecFromString(value)) + } + + private fun label(props: Properties, value: String): Properties { + return props.copy(label = value) + } + + private fun targetState(props: Properties, value: String): Properties { + return props.copy(targetState = value) + } + } +} diff --git a/liveview-android/src/main/java/org/phoenixframework/liveview/ui/view/SharedAttributes.kt b/liveview-android/src/main/java/org/phoenixframework/liveview/ui/view/SharedAttributes.kt index 708d85c4..d83788a6 100644 --- a/liveview-android/src/main/java/org/phoenixframework/liveview/ui/view/SharedAttributes.kt +++ b/liveview-android/src/main/java/org/phoenixframework/liveview/ui/view/SharedAttributes.kt @@ -2,6 +2,7 @@ package org.phoenixframework.liveview.ui.view import androidx.compose.animation.EnterTransition import androidx.compose.animation.ExitTransition +import androidx.compose.animation.core.DurationBasedAnimationSpec import androidx.compose.animation.core.Ease import androidx.compose.animation.core.EaseIn import androidx.compose.animation.core.EaseInBack @@ -37,6 +38,20 @@ import androidx.compose.animation.core.EaseOutQuart import androidx.compose.animation.core.EaseOutQuint import androidx.compose.animation.core.EaseOutSine import androidx.compose.animation.core.Easing +import androidx.compose.animation.core.ExperimentalAnimationSpecApi +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.FiniteAnimationSpec +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.StartOffset +import androidx.compose.animation.core.StartOffsetType +import androidx.compose.animation.core.VisibilityThreshold +import androidx.compose.animation.core.keyframes +import androidx.compose.animation.core.keyframesWithSpline +import androidx.compose.animation.core.repeatable +import androidx.compose.animation.core.snap +import androidx.compose.animation.core.spring +import androidx.compose.animation.core.tween import androidx.compose.animation.expandHorizontally import androidx.compose.animation.expandIn import androidx.compose.animation.expandVertically @@ -71,6 +86,8 @@ import org.phoenixframework.liveview.data.constants.AlignmentValues import org.phoenixframework.liveview.data.constants.Attrs.attrBottom import org.phoenixframework.liveview.data.constants.Attrs.attrColor import org.phoenixframework.liveview.data.constants.Attrs.attrLeft +import org.phoenixframework.liveview.data.constants.Attrs.attrOffsetMillis +import org.phoenixframework.liveview.data.constants.Attrs.attrOffsetType import org.phoenixframework.liveview.data.constants.Attrs.attrPivotFractionX import org.phoenixframework.liveview.data.constants.Attrs.attrPivotFractionY import org.phoenixframework.liveview.data.constants.Attrs.attrRight @@ -78,6 +95,7 @@ import org.phoenixframework.liveview.data.constants.Attrs.attrTop import org.phoenixframework.liveview.data.constants.Attrs.attrWidth import org.phoenixframework.liveview.data.constants.ContentScaleValues import org.phoenixframework.liveview.data.constants.EasingValues +import org.phoenixframework.liveview.data.constants.EnterExitTransitionFunctions.argAnimationSpec import org.phoenixframework.liveview.data.constants.EnterExitTransitionFunctions.argClip import org.phoenixframework.liveview.data.constants.EnterExitTransitionFunctions.argExpandFrom import org.phoenixframework.liveview.data.constants.EnterExitTransitionFunctions.argHeight @@ -118,9 +136,12 @@ import org.phoenixframework.liveview.data.constants.EnterExitTransitionFunctions import org.phoenixframework.liveview.data.constants.EnterExitTransitionFunctions.slideOut import org.phoenixframework.liveview.data.constants.EnterExitTransitionFunctions.slideOutHorizontally import org.phoenixframework.liveview.data.constants.EnterExitTransitionFunctions.slideOutVertically +import org.phoenixframework.liveview.data.constants.FiniteAnimationSpecFunctions import org.phoenixframework.liveview.data.constants.HorizontalAlignmentValues import org.phoenixframework.liveview.data.constants.HorizontalArrangementValues +import org.phoenixframework.liveview.data.constants.RepeatModeValues import org.phoenixframework.liveview.data.constants.SecureFlagPolicyValues +import org.phoenixframework.liveview.data.constants.StartOffsetTypeValues import org.phoenixframework.liveview.data.constants.TileModeValues import org.phoenixframework.liveview.data.constants.TransformOriginValues import org.phoenixframework.liveview.data.constants.VerticalAlignmentValues @@ -355,6 +376,7 @@ internal fun enterTransitionFromString(animationJson: String): EnterTransition? // e.g: {"expandHorizontally": {"expandFrom": "Center", "clip": true, "initialWidth": 100}} val animationType = currentJsonElement?.entrySet()?.firstOrNull()?.key val animationParams = currentJsonElement?.entrySet()?.firstOrNull()?.value?.asJsonObject + val animationSpec = animationParams?.get(argAnimationSpec)?.asJsonObject?.asString val transition = when (animationType) { expandHorizontally -> { val expandFrom = animationParams?.get(argExpandFrom)?.let { @@ -363,7 +385,12 @@ internal fun enterTransitionFromString(animationJson: String): EnterTransition? val clip = animationParams?.get(argClip)?.asBoolean ?: true val initialWidth = animationParams?.get(argInitialWidth)?.asInt ?: 0 expandHorizontally( - // TODO animationSpec: FiniteAnimationSpec + animationSpec = animationSpec?.let { + finiteAnimationSpecFromString(it) + } ?: spring( + stiffness = Spring.StiffnessMediumLow, + visibilityThreshold = IntSize.VisibilityThreshold + ), expandFrom = expandFrom, clip = clip, initialWidth = { initialWidth } @@ -381,7 +408,12 @@ internal fun enterTransitionFromString(animationJson: String): EnterTransition? IntSize(width, height) } ?: IntSize.Zero expandIn( - // TODO animationSpec: FiniteAnimationSpec + animationSpec = animationSpec?.let { + finiteAnimationSpecFromString(it) + } ?: spring( + stiffness = Spring.StiffnessMediumLow, + visibilityThreshold = IntSize.VisibilityThreshold + ), expandFrom = expandFrom, clip = clip, initialSize = { initialSize } @@ -395,7 +427,12 @@ internal fun enterTransitionFromString(animationJson: String): EnterTransition? val clip = animationParams?.get(argClip)?.asBoolean ?: true val initialHeight = animationParams?.get(argInitialHeight)?.asInt ?: 0 expandVertically( - // TODO animationSpec: FiniteAnimationSpec + animationSpec = animationSpec?.let { + finiteAnimationSpecFromString(it) + } ?: spring( + stiffness = Spring.StiffnessMediumLow, + visibilityThreshold = IntSize.VisibilityThreshold + ), expandFrom = expandFrom, clip = clip, initialHeight = { initialHeight } @@ -405,7 +442,9 @@ internal fun enterTransitionFromString(animationJson: String): EnterTransition? fadeIn -> { val initialAlpha = animationParams?.get(argInitialAlpha)?.asFloat ?: 0f fadeIn( - // TODO animationSpec: FiniteAnimationSpec, + animationSpec = animationSpec?.let { + finiteAnimationSpecFromString(it) + } ?: spring(stiffness = Spring.StiffnessMediumLow), initialAlpha = initialAlpha ) } @@ -416,7 +455,9 @@ internal fun enterTransitionFromString(animationJson: String): EnterTransition? transformOriginFromString(it.toString()) } ?: TransformOrigin.Center scaleIn( - // TODO animationSpec: FiniteAnimationSpec, + animationSpec = animationSpec?.let { + finiteAnimationSpecFromString(it) + } ?: spring(stiffness = Spring.StiffnessMediumLow), initialScale = initialScale, transformOrigin = transformOrigin ) @@ -429,7 +470,12 @@ internal fun enterTransitionFromString(animationJson: String): EnterTransition? IntOffset(x, y) } ?: IntOffset.Zero slideIn( - // TODO animationSpec: FiniteAnimationSpec, + animationSpec = animationSpec?.let { + finiteAnimationSpecFromString(it) + } ?: spring( + stiffness = Spring.StiffnessMediumLow, + visibilityThreshold = IntOffset.VisibilityThreshold + ), initialOffset = { initialOffset } ) } @@ -437,7 +483,12 @@ internal fun enterTransitionFromString(animationJson: String): EnterTransition? slideInHorizontally -> { val initialOffsetX = animationParams?.get(argInitialOffsetX)?.asInt slideInHorizontally( - // TODO animationSpec: FiniteAnimationSpec, + animationSpec = animationSpec?.let { + finiteAnimationSpecFromString(it) + } ?: spring( + stiffness = Spring.StiffnessMediumLow, + visibilityThreshold = IntOffset.VisibilityThreshold + ), initialOffsetX = { initialOffsetX ?: (-it / 2) } ) } @@ -445,7 +496,12 @@ internal fun enterTransitionFromString(animationJson: String): EnterTransition? slideInVertically -> { val initialOffsetY = animationParams?.get(argInitialOffsetY)?.asInt slideInVertically( - // TODO animationSpec: FiniteAnimationSpec, + animationSpec = animationSpec?.let { + finiteAnimationSpecFromString(it) + } ?: spring( + stiffness = Spring.StiffnessMediumLow, + visibilityThreshold = IntOffset.VisibilityThreshold + ), initialOffsetY = { initialOffsetY ?: (-it / 2) } ) } @@ -483,11 +539,15 @@ internal fun exitTransitionFromString(animationJson: String): ExitTransition? { // e.g: {"expandHorizontally": {"expandFrom": "Center", "clip": true, "initialWidth": 100}} val animationType = currentJsonElement?.entrySet()?.firstOrNull()?.key val animationParams = currentJsonElement?.entrySet()?.firstOrNull()?.value?.asJsonObject + val animationSpec = animationParams?.get(argAnimationSpec)?.asJsonObject?.asString + val transition = when (animationType) { fadeOut -> { val targetAlpha = animationParams?.get(argTargetAlpha)?.asFloat ?: 0f fadeOut( - // TODO animationSpec: FiniteAnimationSpec, + animationSpec = animationSpec?.let { + finiteAnimationSpecFromString(it) + } ?: spring(stiffness = Spring.StiffnessMediumLow), targetAlpha = targetAlpha ) } @@ -498,7 +558,9 @@ internal fun exitTransitionFromString(animationJson: String): ExitTransition? { transformOriginFromString(it.toString()) } ?: TransformOrigin.Center scaleOut( - // TODO animationSpec: FiniteAnimationSpec, + animationSpec = animationSpec?.let { + finiteAnimationSpecFromString(it) + } ?: spring(stiffness = Spring.StiffnessMediumLow), targetScale = targetScale, transformOrigin = transformOrigin ) @@ -511,7 +573,12 @@ internal fun exitTransitionFromString(animationJson: String): ExitTransition? { IntOffset(x, y) } ?: IntOffset.Zero slideOut( - // TODO animationSpec: FiniteAnimationSpec, + animationSpec = animationSpec?.let { + finiteAnimationSpecFromString(it) + } ?: spring( + stiffness = Spring.StiffnessMediumLow, + visibilityThreshold = IntOffset.VisibilityThreshold + ), targetOffset = { targetOffset } ) } @@ -519,7 +586,12 @@ internal fun exitTransitionFromString(animationJson: String): ExitTransition? { slideOutHorizontally -> { val initialOffsetX = animationParams?.get(argTargetOffsetX)?.asInt slideOutHorizontally( - // TODO animationSpec: FiniteAnimationSpec, + animationSpec = animationSpec?.let { + finiteAnimationSpecFromString(it) + } ?: spring( + stiffness = Spring.StiffnessMediumLow, + visibilityThreshold = IntOffset.VisibilityThreshold + ), targetOffsetX = { initialOffsetX ?: (-it / 2) } ) } @@ -527,7 +599,12 @@ internal fun exitTransitionFromString(animationJson: String): ExitTransition? { slideOutVertically -> { val targetOffsetY = animationParams?.get(argTargetOffsetY)?.asInt slideOutVertically( - // TODO animationSpec: FiniteAnimationSpec, + animationSpec = animationSpec?.let { + finiteAnimationSpecFromString(it) + } ?: spring( + stiffness = Spring.StiffnessMediumLow, + visibilityThreshold = IntOffset.VisibilityThreshold + ), targetOffsetY = { targetOffsetY ?: (-it / 2) } ) } @@ -539,7 +616,12 @@ internal fun exitTransitionFromString(animationJson: String): ExitTransition? { val clip = animationParams?.get(argClip)?.asBoolean ?: true val targetWidth = animationParams?.get(argTargetWidth)?.asInt ?: 0 shrinkHorizontally( - // TODO animationSpec: FiniteAnimationSpec + animationSpec = animationSpec?.let { + finiteAnimationSpecFromString(it) + } ?: spring( + stiffness = Spring.StiffnessMediumLow, + visibilityThreshold = IntSize.VisibilityThreshold + ), shrinkTowards = shrinkTowards, clip = clip, targetWidth = { targetWidth } @@ -558,7 +640,12 @@ internal fun exitTransitionFromString(animationJson: String): ExitTransition? { IntSize(width, height) } ?: IntSize.Zero shrinkOut( - // TODO animationSpec: FiniteAnimationSpec + animationSpec = animationSpec?.let { + finiteAnimationSpecFromString(it) + } ?: spring( + stiffness = Spring.StiffnessMediumLow, + visibilityThreshold = IntSize.VisibilityThreshold + ), shrinkTowards = shrinkTowards, clip = clip, targetSize = { targetSize } @@ -572,14 +659,18 @@ internal fun exitTransitionFromString(animationJson: String): ExitTransition? { val clip = animationParams?.get(argClip)?.asBoolean ?: true val targetHeight = animationParams?.get(argTargetHeight)?.asInt ?: 0 shrinkVertically( - // TODO animationSpec: FiniteAnimationSpec + animationSpec = animationSpec?.let { + finiteAnimationSpecFromString(it) + } ?: spring( + stiffness = Spring.StiffnessMediumLow, + visibilityThreshold = IntSize.VisibilityThreshold + ), shrinkTowards = expandFrom, clip = clip, targetHeight = { targetHeight } ) } - else -> null } if (transition != null) { @@ -644,4 +735,129 @@ internal fun easingFromString(string: String): Easing? { EasingValues.easeInOutBounce -> EaseInOutBounce else -> null } +} + +/* +FiniteAnimationSpec is the interface that all non-infinite AnimationSpecs implement, including: +TweenSpec, SpringSpec, KeyframesSpec, RepeatableSpec, SnapSpec, etc. + */ +internal fun finiteAnimationSpecFromString(string: String): FiniteAnimationSpec? { + return JsonParser.parse>(string)?.let { + finiteAnimationSpecFromMap(it) + } +} + +@OptIn(ExperimentalAnimationSpecApi::class) +private fun finiteAnimationSpecFromMap(jsonMap: Map): FiniteAnimationSpec? { + val function = jsonMap.keys.firstOrNull() ?: return null + val map = jsonMap[function] as? Map ?: return null + val defaultDurationMillis = 300 + return when (function) { + FiniteAnimationSpecFunctions.tween -> { + val duration = (map[FiniteAnimationSpecFunctions.argDurationMillis] as? Number) + ?: defaultDurationMillis + val delay = (map[FiniteAnimationSpecFunctions.argDelayMillis] as? Number) ?: 0 + val ease = + (map[FiniteAnimationSpecFunctions.argEasing] as? String)?.let { easingFromString(it) } + ?: FastOutSlowInEasing + tween( + durationMillis = duration.toInt(), delayMillis = delay.toInt(), easing = ease + ) + } + + FiniteAnimationSpecFunctions.spring -> { + val dampingRatio = (map[FiniteAnimationSpecFunctions.argDampingRatio] as? Number) + ?: Spring.DampingRatioNoBouncy + val stiffness = + (map[FiniteAnimationSpecFunctions.argStiffness] as? Number) + ?: Spring.StiffnessMedium + spring( + dampingRatio = dampingRatio.toFloat(), + stiffness = stiffness.toFloat(), + // TODO visibilityThreshold = + ) + } + + FiniteAnimationSpecFunctions.keyframes -> { + val duration = (map[FiniteAnimationSpecFunctions.argDurationMillis] as? Number) + ?: defaultDurationMillis + val delay = (map[FiniteAnimationSpecFunctions.argDelayMillis] as? Number) ?: 0 + keyframes { + // TODO KeyframesSpecConfig + this.durationMillis = duration.toInt() + this.delayMillis = delay.toInt() + } + } + + FiniteAnimationSpecFunctions.keyframesWithSpline -> { + val duration = (map[FiniteAnimationSpecFunctions.argDurationMillis] as? Number) + ?: defaultDurationMillis + val delay = (map[FiniteAnimationSpecFunctions.argDelayMillis] as? Number) ?: 0 + keyframesWithSpline { + // TODO KeyframesWithSplineSpecConfig + this.durationMillis = duration.toInt() + this.delayMillis = delay.toInt() + } + } + + FiniteAnimationSpecFunctions.repeatable -> { + val iterations = + (map[FiniteAnimationSpecFunctions.argIterations] as? Number) + ?: defaultDurationMillis + val animation = map[FiniteAnimationSpecFunctions.argAnimation]?.let { + it as? Map + }?.let { + finiteAnimationSpecFromMap(it) + } + val repeatMode = (map[FiniteAnimationSpecFunctions.argRepeatMode] as? String)?.let { + repeatModeFromString(it) + } ?: RepeatMode.Restart + val startOffset = + (map[FiniteAnimationSpecFunctions.argInitialStartOffset] as? Map)?.let { + startOffsetFromMap(it) + } ?: StartOffset(0) + if (animation is DurationBasedAnimationSpec) { + repeatable( + iterations = iterations.toInt(), + animation = animation, + repeatMode = repeatMode, + initialStartOffset = startOffset, + ) + } else null + } + + FiniteAnimationSpecFunctions.snap -> { + val delay = (map[FiniteAnimationSpecFunctions.argDelayMillis] as? Number) ?: 0 + snap(delayMillis = delay.toInt()) + } + + else -> null + } +} + +internal fun repeatModeFromString(string: String): RepeatMode? { + return when (string) { + RepeatModeValues.restart -> RepeatMode.Restart + RepeatModeValues.reverse -> RepeatMode.Reverse + else -> null + } +} + +internal fun startOffsetFromMap(map: Map): StartOffset? { + return map[attrOffsetMillis]?.let { + it as? Number + }?.let { value -> + val type = map[attrOffsetType]?.let { + startOffsetTypeFromString(it.toString()) + } ?: StartOffsetType.Delay + StartOffset(value.toInt(), type) + } +} + +internal fun startOffsetTypeFromString(string: String): StartOffsetType? { + return when (string) { + StartOffsetTypeValues.delay -> StartOffsetType.Delay + StartOffsetTypeValues.fastForward -> StartOffsetType.FastForward + else -> null + } } \ No newline at end of file diff --git a/liveview-android/src/test/java/com/dockyard/liveviewtest/liveview/ui/modifiers/ActionsTest.kt b/liveview-android/src/test/java/com/dockyard/liveviewtest/liveview/ui/modifiers/ActionsTest.kt index bd68f633..98ea911d 100644 --- a/liveview-android/src/test/java/com/dockyard/liveviewtest/liveview/ui/modifiers/ActionsTest.kt +++ b/liveview-android/src/test/java/com/dockyard/liveviewtest/liveview/ui/modifiers/ActionsTest.kt @@ -13,13 +13,14 @@ import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith import org.phoenixframework.liveview.data.constants.Attrs.attrClass +import org.phoenixframework.liveview.data.constants.Attrs.attrStyle import org.phoenixframework.liveview.data.constants.Attrs.attrText -import org.phoenixframework.liveview.ui.modifiers.ModifiersParser import org.phoenixframework.liveview.data.constants.ComposableTypes import org.phoenixframework.liveview.ui.base.ComposableView.Companion.EVENT_TYPE_CLICK import org.phoenixframework.liveview.ui.base.ComposableView.Companion.EVENT_TYPE_DOUBLE_CLICK import org.phoenixframework.liveview.ui.base.ComposableView.Companion.EVENT_TYPE_LONG_CLICK import org.phoenixframework.liveview.ui.base.PushEvent +import org.phoenixframework.liveview.ui.modifiers.ModifiersParser @RunWith(AndroidJUnit4::class) class ActionsTest : BaseComposableModifierTest() { @@ -60,6 +61,42 @@ class ActionsTest : BaseComposableModifierTest() { assertEquals(10, counter) } + @Test + fun clickableWithBackslashTest() { + var counter = 0 + val pushEvent: PushEvent = { _, _, _, _ -> + // Changing the counter value to check if the button is clicked + counter = 10 + } + ModifiersParser.fromStyleFile( + """ + %{"clickable(__event__(\"Test\"))" => [ + {:clickable, [], [ + {:__event__, [], ["Test", []]} + ]} + ]} + """, pushEvent + ) + + composeRule.run { + setContent { + ViewFromTemplate( + template = """ + <${ComposableTypes.box}> + <${ComposableTypes.text} + ${attrStyle}="clickable(__event__("Test"))" + ${attrText}="Clickable" /> + + """, + pushEvent = pushEvent + ) + } + + onNodeWithText("Clickable").performClick() + } + assertEquals(10, counter) + } + @Test fun clickableNamedTest() { var counter = 0 diff --git a/liveview-android/src/test/java/com/dockyard/liveviewtest/liveview/ui/view/SharedAttributesTest.kt b/liveview-android/src/test/java/com/dockyard/liveviewtest/liveview/ui/view/SharedAttributesTest.kt index 021478e5..b909fe5d 100644 --- a/liveview-android/src/test/java/com/dockyard/liveviewtest/liveview/ui/view/SharedAttributesTest.kt +++ b/liveview-android/src/test/java/com/dockyard/liveviewtest/liveview/ui/view/SharedAttributesTest.kt @@ -1,5 +1,18 @@ package com.dockyard.liveviewtest.liveview.ui.view +import androidx.compose.animation.core.EaseInBounce +import androidx.compose.animation.core.ExperimentalAnimationSpecApi +import androidx.compose.animation.core.KeyframesSpec +import androidx.compose.animation.core.KeyframesWithSplineSpec +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.StartOffset +import androidx.compose.animation.core.StartOffsetType +import androidx.compose.animation.core.keyframes +import androidx.compose.animation.core.keyframesWithSpline +import androidx.compose.animation.core.repeatable +import androidx.compose.animation.core.snap +import androidx.compose.animation.core.spring +import androidx.compose.animation.core.tween import androidx.compose.animation.expandHorizontally import androidx.compose.animation.expandIn import androidx.compose.animation.expandVertically @@ -36,12 +49,15 @@ import org.phoenixframework.liveview.data.constants.AlignmentValues import org.phoenixframework.liveview.data.constants.Attrs.attrBottom import org.phoenixframework.liveview.data.constants.Attrs.attrColor import org.phoenixframework.liveview.data.constants.Attrs.attrLeft +import org.phoenixframework.liveview.data.constants.Attrs.attrOffsetMillis +import org.phoenixframework.liveview.data.constants.Attrs.attrOffsetType import org.phoenixframework.liveview.data.constants.Attrs.attrPivotFractionX import org.phoenixframework.liveview.data.constants.Attrs.attrPivotFractionY import org.phoenixframework.liveview.data.constants.Attrs.attrRight import org.phoenixframework.liveview.data.constants.Attrs.attrTop import org.phoenixframework.liveview.data.constants.Attrs.attrWidth import org.phoenixframework.liveview.data.constants.ContentScaleValues +import org.phoenixframework.liveview.data.constants.EasingValues import org.phoenixframework.liveview.data.constants.EnterExitTransitionFunctions.argClip import org.phoenixframework.liveview.data.constants.EnterExitTransitionFunctions.argExpandFrom import org.phoenixframework.liveview.data.constants.EnterExitTransitionFunctions.argInitialAlpha @@ -74,11 +90,14 @@ import org.phoenixframework.liveview.data.constants.EnterExitTransitionFunctions import org.phoenixframework.liveview.data.constants.EnterExitTransitionFunctions.slideOut import org.phoenixframework.liveview.data.constants.EnterExitTransitionFunctions.slideOutHorizontally import org.phoenixframework.liveview.data.constants.EnterExitTransitionFunctions.slideOutVertically +import org.phoenixframework.liveview.data.constants.FiniteAnimationSpecFunctions import org.phoenixframework.liveview.data.constants.HorizontalAlignmentValues import org.phoenixframework.liveview.data.constants.HorizontalAlignmentValues.centerHorizontally import org.phoenixframework.liveview.data.constants.HorizontalAlignmentValues.start import org.phoenixframework.liveview.data.constants.HorizontalArrangementValues +import org.phoenixframework.liveview.data.constants.RepeatModeValues import org.phoenixframework.liveview.data.constants.SecureFlagPolicyValues +import org.phoenixframework.liveview.data.constants.StartOffsetTypeValues import org.phoenixframework.liveview.data.constants.SystemColorValues import org.phoenixframework.liveview.data.constants.TileModeValues import org.phoenixframework.liveview.data.constants.VerticalAlignmentValues @@ -90,9 +109,13 @@ import org.phoenixframework.liveview.ui.view.contentScaleFromString import org.phoenixframework.liveview.ui.view.elevationsFromString import org.phoenixframework.liveview.ui.view.enterTransitionFromString import org.phoenixframework.liveview.ui.view.exitTransitionFromString +import org.phoenixframework.liveview.ui.view.finiteAnimationSpecFromString import org.phoenixframework.liveview.ui.view.horizontalAlignmentFromString import org.phoenixframework.liveview.ui.view.horizontalArrangementFromString +import org.phoenixframework.liveview.ui.view.repeatModeFromString import org.phoenixframework.liveview.ui.view.secureFlagPolicyFromString +import org.phoenixframework.liveview.ui.view.startOffsetFromMap +import org.phoenixframework.liveview.ui.view.startOffsetTypeFromString import org.phoenixframework.liveview.ui.view.tileModeFromString import org.phoenixframework.liveview.ui.view.verticalAlignmentFromString import org.phoenixframework.liveview.ui.view.verticalArrangementFromString @@ -573,6 +596,288 @@ class SharedAttributesTest { assertEquals(expected.toString(), actual.toString()) } + @Test + fun finiteAnimationSpecFromStringTweenTest() { + assertEquals( + tween(), + finiteAnimationSpecFromString( + """ + {'${FiniteAnimationSpecFunctions.tween}': {}} + """.trimIndent() + ) + ) + assertEquals( + tween(1500), + finiteAnimationSpecFromString( + """ + {'${FiniteAnimationSpecFunctions.tween}': {'${FiniteAnimationSpecFunctions.argDurationMillis}': 1500 }} + """.trimIndent() + ) + ) + assertEquals( + tween(1500, 2000), + finiteAnimationSpecFromString( + """ + {'${FiniteAnimationSpecFunctions.tween}': + { + '${FiniteAnimationSpecFunctions.argDurationMillis}': 1500, + '${FiniteAnimationSpecFunctions.argDelayMillis}': 2000 + } + }""".trimIndent() + ) + ) + assertEquals( + tween(1500, 2000, EaseInBounce), + finiteAnimationSpecFromString( + """ + {'${FiniteAnimationSpecFunctions.tween}': + { + '${FiniteAnimationSpecFunctions.argDurationMillis}': 1500, + '${FiniteAnimationSpecFunctions.argDelayMillis}': 2000, + '${FiniteAnimationSpecFunctions.argEasing}': '${EasingValues.easeInBounce}' + } + } + """.trimIndent() + ) + ) + } + + @Test + fun finiteAnimationSpecFromStringSpringTest() { + assertEquals( + spring(), + finiteAnimationSpecFromString( + """ + {'${FiniteAnimationSpecFunctions.spring}': {} } + """.trimIndent() + ) + ) + assertEquals( + spring(200f), + finiteAnimationSpecFromString( + """ + {'${FiniteAnimationSpecFunctions.spring}': + { + '${FiniteAnimationSpecFunctions.argDampingRatio}': 200 + } + } + """.trimIndent() + ) + ) + assertEquals( + spring(200f, 100f), + finiteAnimationSpecFromString( + """ + {'${FiniteAnimationSpecFunctions.spring}': + { + '${FiniteAnimationSpecFunctions.argDampingRatio}': 200, + '${FiniteAnimationSpecFunctions.argStiffness}': 100 + } + } + """.trimIndent() + ) + ) + } + + @Test + fun finiteAnimationSpecFromStringKeyframesTest() { + var expect = keyframes { } + var actual = finiteAnimationSpecFromString( + """ + {'${FiniteAnimationSpecFunctions.keyframes}': {} } + """.trimIndent() + ) + assert(actual is KeyframesSpec) + assertEquals( + expect.config.durationMillis, + (actual as KeyframesSpec).config.durationMillis + ) + assertEquals( + expect.config.delayMillis, + actual.config.delayMillis + ) + + expect = keyframes { + this.durationMillis = 1500 + } + actual = finiteAnimationSpecFromString( + """ + {'${FiniteAnimationSpecFunctions.keyframes}': + { + '${FiniteAnimationSpecFunctions.argDurationMillis}': 1500 + } + } + """.trimIndent() + ) + assert(actual is KeyframesSpec) + assertEquals( + expect.config.durationMillis, + (actual as KeyframesSpec).config.durationMillis + ) + assertEquals( + expect.config.delayMillis, + actual.config.delayMillis + ) + + expect = keyframes { + this.durationMillis = 1500 + this.delayMillis = 2000 + } + actual = finiteAnimationSpecFromString( + """ + {'${FiniteAnimationSpecFunctions.keyframes}': + { + '${FiniteAnimationSpecFunctions.argDurationMillis}': 1500, + '${FiniteAnimationSpecFunctions.argDelayMillis}': 2000 + } + } + """.trimIndent() + ) + assert(actual is KeyframesSpec) + assertEquals( + expect.config.durationMillis, + (actual as KeyframesSpec).config.durationMillis + ) + assertEquals( + expect.config.delayMillis, + actual.config.delayMillis + ) + } + + @OptIn(ExperimentalAnimationSpecApi::class) + @Test + fun finiteAnimationSpecFromStringKeyframesWithSplineTest() { + var expect = keyframesWithSpline { } + var actual = finiteAnimationSpecFromString( + """ + {'${FiniteAnimationSpecFunctions.keyframesWithSpline}': {} } + """.trimIndent() + ) + assert(actual is KeyframesWithSplineSpec) + assertEquals( + expect.config.durationMillis, + (actual as KeyframesWithSplineSpec).config.durationMillis + ) + assertEquals( + expect.config.delayMillis, + actual.config.delayMillis + ) + + expect = keyframesWithSpline { + this.durationMillis = 1500 + } + actual = finiteAnimationSpecFromString( + """ + {'${FiniteAnimationSpecFunctions.keyframesWithSpline}': + { + '${FiniteAnimationSpecFunctions.argDurationMillis}': 1500 + } + } + """.trimIndent() + ) + assert(actual is KeyframesWithSplineSpec) + assertEquals( + expect.config.durationMillis, + (actual as KeyframesWithSplineSpec).config.durationMillis + ) + assertEquals( + expect.config.delayMillis, + actual.config.delayMillis + ) + + expect = keyframesWithSpline { + this.durationMillis = 1500 + this.delayMillis = 2000 + } + actual = finiteAnimationSpecFromString( + """ + {'${FiniteAnimationSpecFunctions.keyframesWithSpline}': + { + '${FiniteAnimationSpecFunctions.argDurationMillis}': 1500, + '${FiniteAnimationSpecFunctions.argDelayMillis}': 2000 + } + } + """.trimIndent() + ) + assert(actual is KeyframesWithSplineSpec) + assertEquals( + expect.config.durationMillis, + (actual as KeyframesWithSplineSpec).config.durationMillis + ) + assertEquals( + expect.config.delayMillis, + actual.config.delayMillis + ) + } + + @Test + fun finiteAnimationSpecFromStringRepeatableTest() { + var expect = repeatable(10, tween()) + var actual = finiteAnimationSpecFromString( + """ + {'${FiniteAnimationSpecFunctions.repeatable}': { + '${FiniteAnimationSpecFunctions.argIterations}': 10, + '${FiniteAnimationSpecFunctions.argAnimation}': { + '${FiniteAnimationSpecFunctions.tween}': {} + } + }} + """.trimIndent() + ) + assertEquals(expect, actual) + + expect = repeatable(10, tween(), RepeatMode.Reverse) + actual = finiteAnimationSpecFromString( + """ + {'${FiniteAnimationSpecFunctions.repeatable}': { + '${FiniteAnimationSpecFunctions.argIterations}': 10, + '${FiniteAnimationSpecFunctions.argAnimation}': { + '${FiniteAnimationSpecFunctions.tween}': {} + }, + '${FiniteAnimationSpecFunctions.argRepeatMode}': '${RepeatModeValues.reverse}' + }} + """.trimIndent() + ) + assertEquals(expect, actual) + + expect = repeatable(10, tween(), RepeatMode.Reverse, StartOffset(200)) + actual = finiteAnimationSpecFromString( + """ + {'${FiniteAnimationSpecFunctions.repeatable}': { + '${FiniteAnimationSpecFunctions.argIterations}': 10, + '${FiniteAnimationSpecFunctions.argAnimation}': { + '${FiniteAnimationSpecFunctions.tween}': {} + }, + '${FiniteAnimationSpecFunctions.argRepeatMode}': '${RepeatModeValues.reverse}', + '${FiniteAnimationSpecFunctions.argInitialStartOffset}': { + '${attrOffsetMillis}': 200 + } + }} + """.trimIndent() + ) + assertEquals(expect, actual) + } + + @Test + fun finiteAnimationSpecFromStringSnapTest() { + var expect = snap() + var actual = finiteAnimationSpecFromString( + """ + {'${FiniteAnimationSpecFunctions.snap}': {} } + """.trimIndent() + ) + assertEquals(expect, actual) + + expect = snap(250) + actual = finiteAnimationSpecFromString( + """ + {'${FiniteAnimationSpecFunctions.snap}': { + '${FiniteAnimationSpecFunctions.argDelayMillis}': 250 + } } + """.trimIndent() + ) + assertEquals(expect, actual) + } + @Test fun horizontalAlignmentFromStringTest() { assertEquals( @@ -623,6 +928,18 @@ class SharedAttributesTest { assertEquals(TileMode.Repeated, tileModeFromString(TileModeValues.repeated, TileMode.Clamp)) } + @Test + fun repeatModeFromStringTest() { + assertEquals( + RepeatMode.Reverse, + repeatModeFromString(RepeatModeValues.reverse) + ) + assertEquals( + RepeatMode.Restart, + repeatModeFromString(RepeatModeValues.restart) + ) + } + @Test fun secureFlagPolicyFromStringTest() { assertEquals( @@ -639,6 +956,44 @@ class SharedAttributesTest { ) } + @Test + fun startOffsetFromMapTest() { + assertEquals( + StartOffset(1), + startOffsetFromMap(mapOf(attrOffsetMillis to 1)) + ) + assertEquals( + StartOffset(2, StartOffsetType.FastForward), + startOffsetFromMap( + mapOf( + attrOffsetMillis to 2, + attrOffsetType to StartOffsetTypeValues.fastForward + ) + ) + ) + assertEquals( + StartOffset(3, StartOffsetType.Delay), + startOffsetFromMap( + mapOf( + attrOffsetMillis to 3, + attrOffsetType to StartOffsetTypeValues.delay + ) + ) + ) + } + + @Test + fun startOffsetTypeFromStringTest() { + assertEquals( + StartOffsetType.Delay, + startOffsetTypeFromString(StartOffsetTypeValues.delay) + ) + assertEquals( + StartOffsetType.FastForward, + startOffsetTypeFromString(StartOffsetTypeValues.fastForward) + ) + } + @Test fun verticalAlignmentFromStringTest() { assertEquals(Alignment.Top, verticalAlignmentFromString(VerticalAlignmentValues.top)) From 5bb62a200bdf19eea3f3121e602332f9e7b6fddf Mon Sep 17 00:00:00 2001 From: Nelson Glauber Date: Wed, 26 Jun 2024 15:59:32 -0300 Subject: [PATCH 2/4] Fix issue with border modifier --- .../liveview/ui/modifiers/Border.kt | 9 ++-- .../liveview/ui/phx_components/LiveView.kt | 14 +++-- .../liveview/ui/modifiers/BorderTest.kt | 54 ++++++++++--------- 3 files changed, 41 insertions(+), 36 deletions(-) diff --git a/liveview-android/src/main/java/org/phoenixframework/liveview/ui/modifiers/Border.kt b/liveview-android/src/main/java/org/phoenixframework/liveview/ui/modifiers/Border.kt index 23bc5b81..aefeb935 100644 --- a/liveview-android/src/main/java/org/phoenixframework/liveview/ui/modifiers/Border.kt +++ b/liveview-android/src/main/java/org/phoenixframework/liveview/ui/modifiers/Border.kt @@ -8,7 +8,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.Shape import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp import org.phoenixframework.liveview.data.constants.ModifierArgs.argBorder import org.phoenixframework.liveview.data.constants.ModifierArgs.argBrush import org.phoenixframework.liveview.data.constants.ModifierArgs.argColor @@ -32,7 +31,7 @@ fun Modifier.borderFromStyle(arguments: List): if (arg != null) borderColor = colorFromArgument(arg) arg = borderArguments.find { it.name == argWidth } - if (arg != null) borderWidth = (arg.intValue ?: 0).dp + if (arg != null) borderWidth = dpFromArgument(arg) arg = borderArguments.find { it.name == argBorder } if (arg != null) borderStroke = borderStrokeFromArgument(arg) @@ -56,10 +55,10 @@ fun Modifier.borderFromStyle(arguments: List): borderArgument.type == typeShape -> borderShape = shapeFromArgument(borderArgument) - borderArgument.isInt -> - borderWidth = (borderArgument.intValue ?: 0).dp + borderArgument.isDot || borderArgument.isAtom -> { + if (borderWidth == null) + borderWidth = dpFromArgument(borderArgument) - borderArgument.isDot -> { val b = brushFromArgument(borderArgument) if (b != null) borderBrush = b diff --git a/liveview-android/src/main/java/org/phoenixframework/liveview/ui/phx_components/LiveView.kt b/liveview-android/src/main/java/org/phoenixframework/liveview/ui/phx_components/LiveView.kt index e4ba9aa7..d6f7b407 100644 --- a/liveview-android/src/main/java/org/phoenixframework/liveview/ui/phx_components/LiveView.kt +++ b/liveview-android/src/main/java/org/phoenixframework/liveview/ui/phx_components/LiveView.kt @@ -71,10 +71,12 @@ private fun NavDestination( LocalHttpUrl provides liveViewCoordinator.httpBaseUrl, LocalNavController provides navController, ) { - PhxLiveView( - composableNode = state.composableTreeNode.children.first(), - pushEvent = liveViewCoordinator::pushEvent - ) + state.composableTreeNode.children.forEach { + PhxLiveView( + composableNode = it, + pushEvent = liveViewCoordinator::pushEvent + ) + } } } else { val error = state.throwable @@ -135,7 +137,9 @@ internal fun generateRelativePath(currentUrl: String, newUrl: String): String { // Add remaining parts of newUrl relativeParts.addAll(newParts) - return currentParts.joinToString("/", prefix = "/", postfix = "/") + relativeParts.joinToString("/") + return currentParts.joinToString("/", prefix = "/", postfix = "/") + relativeParts.joinToString( + "/" + ) } /** diff --git a/liveview-android/src/test/java/com/dockyard/liveviewtest/liveview/ui/modifiers/BorderTest.kt b/liveview-android/src/test/java/com/dockyard/liveviewtest/liveview/ui/modifiers/BorderTest.kt index 7bc9e397..0abc5fcc 100644 --- a/liveview-android/src/test/java/com/dockyard/liveviewtest/liveview/ui/modifiers/BorderTest.kt +++ b/liveview-android/src/test/java/com/dockyard/liveviewtest/liveview/ui/modifiers/BorderTest.kt @@ -3,11 +3,13 @@ package com.dockyard.liveviewtest.liveview.ui.modifiers import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.border import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.TileMode +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.test.ext.junit.runners.AndroidJUnit4 import com.dockyard.liveviewtest.liveview.test.util.ModifierBaseTest @@ -22,10 +24,10 @@ class BorderTest : ModifierBaseTest() { assertModifierFromStyle( """ %{"borderWithColorTest" => [ - {:border, [], [2, {:., [], [:Color, :Red]}]}, + {:border, [], [{:., [], [1.5, :dp]}, {:., [], [:Color, :Red]}]}, ]} """, - Modifier.border(2.dp, Color.Red) + Modifier.border(1.5.dp, Color.Red) ) } @@ -34,7 +36,7 @@ class BorderTest : ModifierBaseTest() { assertModifierFromStyle( """ %{"borderWithColorNamedParamsTest" => [ - {:border, [], [[width: 2, color: {:., [], [:Color, :Red]}]]}, + {:border, [], [[width: {:., [], [2, :dp]}, color: {:., [], [:Color, :Red]}]]}, ]} """, Modifier.border(2.dp, Color.Red) @@ -46,7 +48,7 @@ class BorderTest : ModifierBaseTest() { assertModifierFromStyle( """ %{"borderWithColorAndShapeTest" => [ - {:border, [], [1, {:., [], [:Color, :Green]}, {:., [], [:CircleShape]}]}, + {:border, [], [{:., [], [1, :dp]}, {:., [], [:Color, :Green]}, :CircleShape]}, ]} """, Modifier.border(1.dp, Color.Green, CircleShape) @@ -58,7 +60,7 @@ class BorderTest : ModifierBaseTest() { assertModifierFromStyle( """ %{"borderWithColorAndShapeNamedParamsTest" => [ - {:border, [], [[width: 1, color: {:., [], [:Color, :Green]}, shape: {:., [], [:CircleShape]}]]}, + {:border, [], [[width: {:., [], [1, :dp]}, color: {:., [], [:Color, :Green]}, shape: :CircleShape]]}, ]} """, Modifier.border(1.dp, Color.Green, CircleShape) @@ -134,7 +136,7 @@ class BorderTest : ModifierBaseTest() { """ %{"borderWithBrushHorizontalGradient" => [{ :border, [], [[ - width: 2, + width: {:., [], [2, :dp]}, brush: {:., [], [ :Brush, {:horizontalGradient, [], [ @@ -148,7 +150,7 @@ class BorderTest : ModifierBaseTest() { {:., [], [:TileMode, :Clamp]} ]} ]}, - shape: {:., [], [:CircleShape]} + shape: :CircleShape ]] }]} """.trimStyle(), @@ -171,7 +173,7 @@ class BorderTest : ModifierBaseTest() { """ %{"borderWithBrushHorizontalGradient" => [{ :border, [], [[ - width: 2, + width: {:., [], [2, :dp]}, brush: {:., [], [ :Brush, {:horizontalGradient, [], [[ @@ -185,7 +187,7 @@ class BorderTest : ModifierBaseTest() { endX: 500.0 ]]} ]}, - shape: {:., [], [:CircleShape]} + shape: :CircleShape ]] }]} """.trimStyle(), @@ -208,7 +210,7 @@ class BorderTest : ModifierBaseTest() { """ %{"borderWithBrushVerticalGradient" => [{ :border, [], [[ - width: 2, + width: {:., [], [2, :dp]}, brush: {:., [], [ :Brush, {:verticalGradient, [], [ @@ -222,7 +224,7 @@ class BorderTest : ModifierBaseTest() { {:., [], [:TileMode, :Decal]} ]} ]}, - shape: {:., [], [:CircleShape]} + shape: :CircleShape ]] }]} """.trimStyle(), @@ -246,7 +248,7 @@ class BorderTest : ModifierBaseTest() { """ %{"borderWithBrushVerticalGradient" => [{ :border, [], [[ - width: 2, + width: {:., [], [2, :dp]}, brush: {:., [], [ :Brush, {:verticalGradient, [], [[ @@ -260,7 +262,7 @@ class BorderTest : ModifierBaseTest() { ] ]]} ]}, - shape: {:., [], [:CircleShape]} + shape: :CircleShape ]] }]} """.trimStyle(), @@ -284,7 +286,7 @@ class BorderTest : ModifierBaseTest() { """ %{"borderWithBrushLinearGradient" => [{ :border, [], [[ - width: 2, + width: {:., [], [2, :dp]}, brush: {:., [], [ :Brush, {:linearGradient, [], [ @@ -298,7 +300,7 @@ class BorderTest : ModifierBaseTest() { {:., [], [:TileMode, :Mirror]} ]} ]}, - shape: {:., [], [:CircleShape]} + shape: :CircleShape ]] }]} """.trimStyle(), @@ -322,7 +324,7 @@ class BorderTest : ModifierBaseTest() { """ %{"borderWithBrushLinearGradient" => [{ :border, [], [[ - width: 2, + width: {:., [], [2, :dp]}, brush: {:., [], [ :Brush, {:linearGradient, [], [[ @@ -336,7 +338,7 @@ class BorderTest : ModifierBaseTest() { tileMode: {:., [], [:TileMode, :Mirror]} ]]} ]}, - shape: {:., [], [:CircleShape]} + shape: :CircleShape ]] }]} """.trimStyle(), @@ -360,7 +362,7 @@ class BorderTest : ModifierBaseTest() { """ %{"borderWithBrushLinearGradient" => [{ :border, [], [[ - width: 2, + width: {:., [], [2, :dp]}, brush: {:., [], [ :Brush, {:radialGradient, [], [ @@ -374,7 +376,7 @@ class BorderTest : ModifierBaseTest() { {:., [], [:TileMode, :Repeated]} ]} ]}, - shape: {:., [], [:CircleShape]} + shape: {:RoundedCornerShape, [], [{:Dp, [], [12]}]} ]] }]} """.trimStyle(), @@ -387,7 +389,7 @@ class BorderTest : ModifierBaseTest() { radius = 50f, tileMode = TileMode.Repeated, ), - CircleShape + RoundedCornerShape(Dp(12f)) ) ) } @@ -398,7 +400,7 @@ class BorderTest : ModifierBaseTest() { """ %{"borderWithBrushLinearGradient" => [{ :border, [], [[ - width: 2, + width: {:., [], [2, :dp]}, brush: {:., [], [ :Brush, {:radialGradient, [], [[ @@ -412,7 +414,7 @@ class BorderTest : ModifierBaseTest() { tileMode: {:., [], [:TileMode, :Repeated]} ]]} ]}, - shape: {:., [], [:CircleShape]} + shape: :CircleShape ]] }]} """.trimStyle(), @@ -436,7 +438,7 @@ class BorderTest : ModifierBaseTest() { """ %{"borderWithBrushSweepGradient" => [{ :border, [], [[ - width: 2, + width: {:., [], [2, :dp]}, brush: {:., [], [ :Brush, {:sweepGradient, [], [ @@ -448,7 +450,7 @@ class BorderTest : ModifierBaseTest() { {:Offset, [], [10, 20]} ]} ]}, - shape: {:., [], [:CircleShape]} + shape: :CircleShape ]] }]} """.trimStyle(), @@ -470,7 +472,7 @@ class BorderTest : ModifierBaseTest() { """ %{"borderWithBrushSweepGradient" => [{ :border, [], [[ - width: 2, + width: {:., [], [2, :dp]}, brush: {:., [], [ :Brush, {:sweepGradient, [], [[ @@ -482,7 +484,7 @@ class BorderTest : ModifierBaseTest() { center: {:Offset, [], [10, 20]} ]]} ]}, - shape: {:., [], [:CircleShape]} + shape: :CircleShape ]] }]} """.trimStyle(), From 1f2f042dd4c360b45233cd8d7d2d41a6ccfb2c39 Mon Sep 17 00:00:00 2001 From: Nelson Glauber Date: Sat, 6 Jul 2024 13:24:50 -0300 Subject: [PATCH 3/4] Add support to pass value to children --- .../liveview/data/constants/Attrs.kt | 1 + .../liveview/domain/LiveViewCoordinator.kt | 6 +- .../liveview/ui/base/ComposableView.kt | 14 + .../ui/modifiers/ModifierDataAdapter.kt | 19 +- .../liveview/ui/modifiers/ModifiersParser.kt | 273 ++++++++++++------ .../liveview/ui/modifiers/Util.kt | 7 +- .../ui/registry/ComposableNodeFactory.kt | 28 +- .../liveview/ui/view/AsyncImageView.kt | 11 +- .../liveview/ui/view/CrossfadeView.kt | 49 +++- .../liveview/test/util/ModifierBaseTest.kt | 2 +- 10 files changed, 302 insertions(+), 108 deletions(-) diff --git a/liveview-android/src/main/java/org/phoenixframework/liveview/data/constants/Attrs.kt b/liveview-android/src/main/java/org/phoenixframework/liveview/data/constants/Attrs.kt index 2acee7de..b1dd3844 100644 --- a/liveview-android/src/main/java/org/phoenixframework/liveview/data/constants/Attrs.kt +++ b/liveview-android/src/main/java/org/phoenixframework/liveview/data/constants/Attrs.kt @@ -173,6 +173,7 @@ object Attrs { const val attrUserScrollEnabled = "userScrollEnabled" const val attrVerticalAlignment = "verticalAlignment" const val attrVerticalArrangement = "verticalArrangement" + const val attrViewId = "viewId" const val attrVisible = "visible" const val attrVisualTransformation = "visualTransformation" const val attrUnselectedContentColor = "unselectedContentColor" diff --git a/liveview-android/src/main/java/org/phoenixframework/liveview/domain/LiveViewCoordinator.kt b/liveview-android/src/main/java/org/phoenixframework/liveview/domain/LiveViewCoordinator.kt index 0f06749a..1b194a44 100644 --- a/liveview-android/src/main/java/org/phoenixframework/liveview/domain/LiveViewCoordinator.kt +++ b/liveview-android/src/main/java/org/phoenixframework/liveview/domain/LiveViewCoordinator.kt @@ -15,15 +15,15 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.phoenixframework.Message import org.phoenixframework.liveview.data.core.CoreNodeElement -import org.phoenixframework.liveview.ui.modifiers.ModifiersParser import org.phoenixframework.liveview.data.repository.Repository import org.phoenixframework.liveview.data.service.ChannelService import org.phoenixframework.liveview.data.service.SocketService -import org.phoenixframework.liveview.ui.registry.ComposableNodeFactory import org.phoenixframework.liveview.domain.data.ComposableTreeNode import org.phoenixframework.liveview.lib.Document import org.phoenixframework.liveview.lib.Node import org.phoenixframework.liveview.lib.NodeRef +import org.phoenixframework.liveview.ui.modifiers.ModifiersParser +import org.phoenixframework.liveview.ui.registry.ComposableNodeFactory internal class LiveViewCoordinator( internal val httpBaseUrl: String, @@ -84,7 +84,7 @@ internal class LiveViewCoordinator( repository.loadStyleData()?.let { styleFileContentAsString -> ModifiersParser.fromStyleFile( styleFileContentAsString, - ::pushEvent + null, ) } } diff --git a/liveview-android/src/main/java/org/phoenixframework/liveview/ui/base/ComposableView.kt b/liveview-android/src/main/java/org/phoenixframework/liveview/ui/base/ComposableView.kt index 8963e461..15706ad2 100644 --- a/liveview-android/src/main/java/org/phoenixframework/liveview/ui/base/ComposableView.kt +++ b/liveview-android/src/main/java/org/phoenixframework/liveview/ui/base/ComposableView.kt @@ -75,6 +75,20 @@ abstract class ComposableView(protected open val prop internal const val EVENT_TYPE_SUBMIT = "submit" internal const val KEY_PHX_VALUE = "value" + + private val viewValues = mutableMapOf() + + fun saveViewValue(viewId: String, value: Any) { + viewValues[viewId] = value + } + + fun removeViewValue(viewId: String) { + viewValues.remove(viewId) + } + + fun getViewValue(viewId: String): Any? { + return viewValues[viewId] + } } } diff --git a/liveview-android/src/main/java/org/phoenixframework/liveview/ui/modifiers/ModifierDataAdapter.kt b/liveview-android/src/main/java/org/phoenixframework/liveview/ui/modifiers/ModifierDataAdapter.kt index 88716fb6..4e99a533 100644 --- a/liveview-android/src/main/java/org/phoenixframework/liveview/ui/modifiers/ModifierDataAdapter.kt +++ b/liveview-android/src/main/java/org/phoenixframework/liveview/ui/modifiers/ModifierDataAdapter.kt @@ -389,15 +389,16 @@ class ModifierDataAdapter(tupleExpression: ElixirParser.TupleExprContext) { ) companion object { - private const val TypeAtom = "Atom" - private const val TypeBoolean = "Boolean" - private const val TypeDot = "." - private const val TypeFloat = "Float" - private const val TypeInt = "Int" - private const val TypeList = "List" - private const val TypeString = "String" - private const val TypeUnary = "Unary" - private const val TypeUndefined = "" + internal const val TypeAtom = "Atom" + internal const val TypeBoolean = "Boolean" + internal const val TypeDot = "." + internal const val TypeFloat = "Float" + internal const val TypeInt = "Int" + internal const val TypeLambdaValue = "lambdaValue" + internal const val TypeList = "List" + internal const val TypeString = "String" + internal const val TypeUnary = "Unary" + internal const val TypeUndefined = "" } } diff --git a/liveview-android/src/main/java/org/phoenixframework/liveview/ui/modifiers/ModifiersParser.kt b/liveview-android/src/main/java/org/phoenixframework/liveview/ui/modifiers/ModifiersParser.kt index 01840a72..54730ebb 100644 --- a/liveview-android/src/main/java/org/phoenixframework/liveview/ui/modifiers/ModifiersParser.kt +++ b/liveview-android/src/main/java/org/phoenixframework/liveview/ui/modifiers/ModifiersParser.kt @@ -121,7 +121,12 @@ import org.phoenixframework.liveview.stylesheet.ElixirParser.ListContext import org.phoenixframework.liveview.stylesheet.ElixirParser.ListExprContext import org.phoenixframework.liveview.stylesheet.ElixirParser.MapExprContext import org.phoenixframework.liveview.stylesheet.ElixirParser.TupleExprContext +import org.phoenixframework.liveview.ui.base.ComposableView import org.phoenixframework.liveview.ui.base.PushEvent +import org.phoenixframework.liveview.ui.modifiers.ModifierDataAdapter.Companion.TypeAtom +import org.phoenixframework.liveview.ui.modifiers.ModifierDataAdapter.Companion.TypeDot +import org.phoenixframework.liveview.ui.modifiers.ModifierDataAdapter.Companion.TypeLambdaValue +import org.phoenixframework.liveview.ui.modifiers.ModifierDataAdapter.Companion.TypeString import org.phoenixframework.liveview.ui.view.ExposedDropdownMenuBoxScopeWrapper internal object ModifiersParser { @@ -134,14 +139,14 @@ internal object ModifiersParser { modifiersCacheTable.clear() } - fun fromStyleFile(fileContent: String, pushEvent: PushEvent? = null) { + fun fromStyleFile(fileContent: String, scope: Any? = null, pushEvent: PushEvent? = null) { clearCacheTable() parseStyleFileContent(fileContent)?.forEach { pair -> val (styleName, tupleExpressionList) = pair val modifiersList = mutableListOf() tupleExpressionList.forEach { tupleExpr -> try { - fromTupleExpression(tupleExpr, pushEvent)?.let { + fromTupleExpression(tupleExpr, scope, pushEvent)?.let { modifiersList.add(it) } } catch (e: Exception) { @@ -154,13 +159,17 @@ internal object ModifiersParser { } } - private fun fromTupleExpression(tupleExpr: TupleExprContext, pushEvent: PushEvent?): Modifier? { + private fun fromTupleExpression( + tupleExpr: TupleExprContext, + scope: Any?, + pushEvent: PushEvent? + ): Modifier? { // Each tuple has 3 expressions: // modifier name, meta data, and arguments to create the modifier return ModifierDataAdapter(tupleExpr).let { modifierDataAdapter -> modifierDataAdapter.modifierName?.let { modifierName -> try { - handleModifier(modifierName, modifierDataAdapter.arguments, null, pushEvent) + handleModifier(modifierName, modifierDataAdapter.arguments, scope, pushEvent) } catch (e: Exception) { Log.e( TAG, @@ -245,15 +254,152 @@ internal object ModifiersParser { } @SuppressLint("ModifierFactoryExtensionFunction") - @OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) private fun handleModifier( modifierId: String, - argListContext: List, + arguments: List, scope: Any?, pushEvent: PushEvent? + ): Modifier? { + if (arguments.any { it.type == TypeLambdaValue } && scope == null) + return Modifier.placeholderModifier(modifierId, arguments) + + val argListContext = parseArguments(arguments) + + parseScopedModifiers(modifierId, argListContext, scope)?.let { + return it + } + parseActionModifiers(modifierId, argListContext, pushEvent)?.let { + return it + } + parseNonParamsModifiers(modifierId)?.let { + return it + } + parseStaticModifiers(modifierId, argListContext)?.let { + return it + } + return null + } + + @SuppressLint("ModifierFactoryExtensionFunction") + private fun parseStaticModifiers( + modifierId: String, + argListContext: List, + ): Modifier? { + return when (modifierId) { + modifierAbsoluteOffset -> Modifier.absoluteOffsetFromStyle(argListContext) + modifierAbsolutePadding -> Modifier.absolutePaddingFromStyle(argListContext) + modifierAlpha -> Modifier.alphaFromStyle(argListContext) + modifierAspectRatio -> Modifier.aspectRatioFromStyle(argListContext) + modifierBackground -> Modifier.backgroundFromStyle(argListContext) + modifierBorder -> Modifier.borderFromStyle(argListContext) + modifierClip -> Modifier.clipFromStyle(argListContext) + modifierDefaultMinSize -> Modifier.defaultMinSizeFromStyle(argListContext) + modifierFillMaxHeight -> Modifier.fillMaxHeightFromStyle(argListContext) + modifierFillMaxSize -> Modifier.fillMaxSizeFromStyle(argListContext) + modifierFillMaxWidth -> Modifier.fillMaxWidthFromStyle(argListContext) + modifierFocusable -> Modifier.focusableFromStyle(argListContext) + modifierHeight -> Modifier.heightFromStyle(argListContext) + modifierHeightIn -> Modifier.heightInFromStyle(argListContext) + modifierHorizontalScroll -> Modifier.horizontalScrollFromStyle(argListContext) + modifierLayoutId -> Modifier.layoutIdFromStyle(argListContext) + modifierOffset -> Modifier.offsetFromStyle(argListContext) + modifierPadding -> Modifier.paddingFromStyle(argListContext) + modifierPaddingFrom -> Modifier.paddingFromFromStyle(argListContext) + modifierPaddingFromBaseline -> Modifier.paddingFromBaselineFromStyle(argListContext) + modifierProgressSemantics -> Modifier.progressSemanticsFromStyle(argListContext) + modifierRequiredHeight -> Modifier.requiredHeightFromStyle(argListContext) + modifierRequiredHeightIn -> Modifier.requiredHeightInFromStyle(argListContext) + modifierRequiredSize -> Modifier.requiredSizeFromStyle(argListContext) + modifierRequiredSizeIn -> Modifier.requiredSizeInFromStyle(argListContext) + modifierRequiredWidth -> Modifier.requiredWidthFromStyle(argListContext) + modifierRequiredWidthIn -> Modifier.requiredWidthInFromStyle(argListContext) + modifierRotate -> Modifier.rotateFromStyle(argListContext) + modifierScale -> Modifier.scaleFromStyle(argListContext) + modifierShadow -> Modifier.shadowFromStyle(argListContext) + modifierSize -> Modifier.sizeFromStyle(argListContext) + modifierSizeIn -> Modifier.sizeInFromStyle(argListContext) + modifierTestTag -> Modifier.testTagFromStyle(argListContext) + modifierVerticalScroll -> Modifier.verticalScrollFromStyle(argListContext) + modifierWindowInsetsBottomHeight -> Modifier.windowInsetsBottomHeightFromStyle( + argListContext + ) + + modifierWindowInsetsEndWidth -> Modifier.windowInsetsEndWidthFromStyle(argListContext) + modifierWindowInsetsStartWidth -> Modifier.windowInsetsStartWidthFromStyle( + argListContext + ) + + modifierWindowInsetsTopHeight -> Modifier.windowInsetsTopHeightFromStyle(argListContext) + modifierWidth -> Modifier.widthFromStyle(argListContext) + modifierWidthIn -> Modifier.widthInFromStyle(argListContext) + modifierWindowInsetsPadding -> Modifier.windowInsetsPaddingFromStyle(argListContext) + modifierWrapContentHeight -> Modifier.wrapContentHeightFromStyle(argListContext) + modifierWrapContentSize -> Modifier.wrapContentSizeFromStyle(argListContext) + modifierWrapContentWidth -> Modifier.wrapContentWidthFromStyle(argListContext) + modifierZIndex -> Modifier.zIndexFromStyle(argListContext) + else -> null + } + } + + @SuppressLint("ModifierFactoryExtensionFunction") + private fun parseActionModifiers( + modifierId: String, + argListContext: List, + pushEvent: PushEvent? + ): Modifier? { + fun modifierOrPlaceholder(modifier: Modifier) = if (pushEvent != null) + modifier + else + Modifier.placeholderModifier(modifierId, argListContext) + + return when (modifierId) { + modifierAnimateContentSize -> + Modifier.animateContentSizeFromStyle(argListContext, pushEvent) + + modifierClickable -> modifierOrPlaceholder( + Modifier.clickableFromStyle(argListContext, pushEvent) + ) + + modifierCombinedClickable -> modifierOrPlaceholder( + Modifier.combinedClickableFromStyle(argListContext, pushEvent) + ) + + modifierOnFocusedBoundsChanged -> modifierOrPlaceholder( + Modifier.onFocusedBoundsChangedFromStyle(argListContext, pushEvent) + ) + + modifierOnFocusChanged -> modifierOrPlaceholder( + Modifier.onFocusChangedFromStyle(argListContext, pushEvent) + ) + + modifierOnFocusEvent -> modifierOrPlaceholder( + Modifier.onFocusEventFromStyle(argListContext, pushEvent) + ) + + modifierSelectable -> modifierOrPlaceholder( + Modifier.selectableFromStyle(argListContext, pushEvent) + ) + + modifierToggleable -> modifierOrPlaceholder( + Modifier.toggleableFromStyle(argListContext, pushEvent) + ) + + modifierTriStateToggleable -> modifierOrPlaceholder( + Modifier.triStateToggleableFromStyle(argListContext, pushEvent) + ) + + else -> null + } + } + + @OptIn(ExperimentalMaterial3Api::class) + @SuppressLint("ModifierFactoryExtensionFunction") + private fun parseScopedModifiers( + modifierId: String, + argListContext: List, + scope: Any?, ): Modifier? { return when (modifierId) { - // Scoped Modifiers (will be handled at runtime) modifierMenuAnchor -> { if (scope is ExposedDropdownMenuBoxScopeWrapper) { scope.scope.run { @@ -322,7 +468,15 @@ internal object ModifiersParser { else Modifier.placeholderModifier(modifierId, argListContext) } - // No param modifiers + + else -> null + } + } + + @SuppressLint("ModifierFactoryExtensionFunction") + @OptIn(ExperimentalLayoutApi::class) + private fun parseNonParamsModifiers(modifierId: String): Modifier? { + return when (modifierId) { modifierCaptionBarPadding -> Modifier.captionBarPadding() modifierClipToBounds -> Modifier.clipToBounds() modifierDisplayCutoutPadding -> Modifier.displayCutoutPadding() @@ -344,87 +498,34 @@ internal object ModifiersParser { modifierSystemGestureExclusion -> Modifier.systemGestureExclusion() modifierToolingGraphicsLayer -> Modifier.toolingGraphicsLayer() modifierWaterfallPadding -> Modifier.waterfallPadding() - // Parameterized modifiers - modifierAbsoluteOffset -> Modifier.absoluteOffsetFromStyle(argListContext) - modifierAbsolutePadding -> Modifier.absolutePaddingFromStyle(argListContext) - modifierAlpha -> Modifier.alphaFromStyle(argListContext) - modifierAnimateContentSize -> Modifier.animateContentSizeFromStyle( - argListContext, - pushEvent - ) - - modifierAspectRatio -> Modifier.aspectRatioFromStyle(argListContext) - modifierBackground -> Modifier.backgroundFromStyle(argListContext) - modifierBorder -> Modifier.borderFromStyle(argListContext) - modifierClickable -> Modifier.clickableFromStyle(argListContext, pushEvent) - modifierClip -> Modifier.clipFromStyle(argListContext) - modifierCombinedClickable -> Modifier.combinedClickableFromStyle( - argListContext, - pushEvent - ) - - modifierDefaultMinSize -> Modifier.defaultMinSizeFromStyle(argListContext) - modifierFillMaxHeight -> Modifier.fillMaxHeightFromStyle(argListContext) - modifierFillMaxSize -> Modifier.fillMaxSizeFromStyle(argListContext) - modifierFillMaxWidth -> Modifier.fillMaxWidthFromStyle(argListContext) - modifierFocusable -> Modifier.focusableFromStyle(argListContext) - modifierHeight -> Modifier.heightFromStyle(argListContext) - modifierHeightIn -> Modifier.heightInFromStyle(argListContext) - modifierHorizontalScroll -> Modifier.horizontalScrollFromStyle(argListContext) - modifierLayoutId -> Modifier.layoutIdFromStyle(argListContext) - modifierOffset -> Modifier.offsetFromStyle(argListContext) - modifierOnFocusedBoundsChanged -> Modifier.onFocusedBoundsChangedFromStyle( - argListContext, - pushEvent - ) - - modifierOnFocusChanged -> Modifier.onFocusChangedFromStyle(argListContext, pushEvent) - modifierOnFocusEvent -> Modifier.onFocusEventFromStyle(argListContext, pushEvent) - modifierPadding -> Modifier.paddingFromStyle(argListContext) - modifierPaddingFrom -> Modifier.paddingFromFromStyle(argListContext) - modifierPaddingFromBaseline -> Modifier.paddingFromBaselineFromStyle(argListContext) - modifierProgressSemantics -> Modifier.progressSemanticsFromStyle(argListContext) - modifierRequiredHeight -> Modifier.requiredHeightFromStyle(argListContext) - modifierRequiredHeightIn -> Modifier.requiredHeightInFromStyle(argListContext) - modifierRequiredSize -> Modifier.requiredSizeFromStyle(argListContext) - modifierRequiredSizeIn -> Modifier.requiredSizeInFromStyle(argListContext) - modifierRequiredWidth -> Modifier.requiredWidthFromStyle(argListContext) - modifierRequiredWidthIn -> Modifier.requiredWidthInFromStyle(argListContext) - modifierRotate -> Modifier.rotateFromStyle(argListContext) - modifierScale -> Modifier.scaleFromStyle(argListContext) - modifierSelectable -> Modifier.selectableFromStyle(argListContext, pushEvent) - modifierShadow -> Modifier.shadowFromStyle(argListContext) - modifierSize -> Modifier.sizeFromStyle(argListContext) - modifierSizeIn -> Modifier.sizeInFromStyle(argListContext) - modifierTestTag -> Modifier.testTagFromStyle(argListContext) - modifierToggleable -> Modifier.toggleableFromStyle(argListContext, pushEvent) - modifierTriStateToggleable -> Modifier.triStateToggleableFromStyle( - argListContext, - pushEvent - ) - - modifierVerticalScroll -> Modifier.verticalScrollFromStyle(argListContext) - - modifierWindowInsetsBottomHeight -> Modifier.windowInsetsBottomHeightFromStyle( - argListContext - ) - - modifierWindowInsetsEndWidth -> Modifier.windowInsetsEndWidthFromStyle(argListContext) - modifierWindowInsetsStartWidth -> Modifier.windowInsetsStartWidthFromStyle( - argListContext - ) - - modifierWindowInsetsTopHeight -> Modifier.windowInsetsTopHeightFromStyle(argListContext) - modifierWidth -> Modifier.widthFromStyle(argListContext) - modifierWidthIn -> Modifier.widthInFromStyle(argListContext) - modifierWindowInsetsPadding -> Modifier.windowInsetsPaddingFromStyle(argListContext) - modifierWrapContentHeight -> Modifier.wrapContentHeightFromStyle(argListContext) - modifierWrapContentSize -> Modifier.wrapContentSizeFromStyle(argListContext) - modifierWrapContentWidth -> Modifier.wrapContentWidthFromStyle(argListContext) - modifierZIndex -> Modifier.zIndexFromStyle(argListContext) else -> null } } + private fun parseArguments( + arguments: List, + ): List { + return arguments.map { + if (it.type == TypeLambdaValue && it.listValue.size == 2) { + ModifierDataAdapter.ArgumentData( + it.name, + TypeDot, + listOf( + ModifierDataAdapter.ArgumentData( + null, + TypeAtom, + it.listValue[0].value.toString() + ), + ModifierDataAdapter.ArgumentData( + null, + TypeString, + ComposableView.getViewValue(it.listValue[1].value.toString()) + ), + ) + ) + } else it + } + } + private const val TAG = "ModifiersParser" } \ No newline at end of file diff --git a/liveview-android/src/main/java/org/phoenixframework/liveview/ui/modifiers/Util.kt b/liveview-android/src/main/java/org/phoenixframework/liveview/ui/modifiers/Util.kt index 1305ac11..b009f8d9 100644 --- a/liveview-android/src/main/java/org/phoenixframework/liveview/ui/modifiers/Util.kt +++ b/liveview-android/src/main/java/org/phoenixframework/liveview/ui/modifiers/Util.kt @@ -707,11 +707,12 @@ internal fun eventFromArgument(argument: ModifierDataAdapter.ArgumentData): Pair return if (argument.type == typeEvent) { val (event, args) = Pair( argument.listValue.getOrNull(0)?.stringValue, - argument.listValue.getOrNull(1)?.listValue?.map { it.value } ?: emptyList() + argument.listValue.getOrNull(1)?.listValue?.associate { + it.name to it.value.toString() + } ?: emptyMap() ) if (event != null) { - val pushArgs = if (args.isEmpty()) null else if (args.size == 1) args.first() else null - event to pushArgs + event to args } else null } else null } diff --git a/liveview-android/src/main/java/org/phoenixframework/liveview/ui/registry/ComposableNodeFactory.kt b/liveview-android/src/main/java/org/phoenixframework/liveview/ui/registry/ComposableNodeFactory.kt index 90a9acd7..307ce231 100644 --- a/liveview-android/src/main/java/org/phoenixframework/liveview/ui/registry/ComposableNodeFactory.kt +++ b/liveview-android/src/main/java/org/phoenixframework/liveview/ui/registry/ComposableNodeFactory.kt @@ -1,12 +1,17 @@ package org.phoenixframework.liveview.ui.registry +import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import org.phoenixframework.liveview.data.constants.ComposableTypes +import org.phoenixframework.liveview.data.core.CoreAttribute import org.phoenixframework.liveview.data.core.CoreNodeElement +import org.phoenixframework.liveview.data.mappers.JsonParser import org.phoenixframework.liveview.domain.data.ComposableTreeNode import org.phoenixframework.liveview.lib.NodeRef import org.phoenixframework.liveview.ui.base.ComposableView import org.phoenixframework.liveview.ui.base.PushEvent +import org.phoenixframework.liveview.ui.modifiers.ModifierDataAdapter.Companion.TypeLambdaValue import org.phoenixframework.liveview.ui.view.AlertDialogView import org.phoenixframework.liveview.ui.view.AnimatedVisibilityView import org.phoenixframework.liveview.ui.view.AsyncImageView @@ -250,7 +255,7 @@ object ComposableNodeFactory { ): ComposableView<*> { return if (element != null) { val tag = element.tag - val attrs = element.attributes + val attrs = parseAttributeList(element.attributes) ComposableRegistry.getComponentFactory(tag, parentTag)?.buildComposableView( attrs, pushEvent, scope ) ?: run { @@ -270,4 +275,25 @@ object ComposableNodeFactory { ) } } + + private fun parseAttributeList( + attributes: ImmutableList + ): ImmutableList { + val lambdaValuePrefix = "{\"${TypeLambdaValue}\":" + // Special case to parse the lambda value from parent composable view + return if (attributes.none { it.value.startsWith(lambdaValuePrefix) }) + attributes + else + attributes.map { + if (it.value.startsWith(lambdaValuePrefix)) { + val map = JsonParser.parse>(it.value) + CoreAttribute( + it.name, + it.namespace, + ComposableView.getViewValue(map?.get(TypeLambdaValue).toString()) + ?.toString() ?: it.value + ) + } else it + }.toImmutableList() + } } \ No newline at end of file diff --git a/liveview-android/src/main/java/org/phoenixframework/liveview/ui/view/AsyncImageView.kt b/liveview-android/src/main/java/org/phoenixframework/liveview/ui/view/AsyncImageView.kt index d30944dd..f622552b 100644 --- a/liveview-android/src/main/java/org/phoenixframework/liveview/ui/view/AsyncImageView.kt +++ b/liveview-android/src/main/java/org/phoenixframework/liveview/ui/view/AsyncImageView.kt @@ -58,9 +58,14 @@ internal class AsyncImageView private constructor(props: Properties) : val baseUrl = LocalHttpUrl.current val resolvedImageUrl = remember(imageUrl) { - val baseUri = URI.create(baseUrl) - val relativeUri = baseUri.resolve(imageUrl) - relativeUri.toString() + try { + val baseUri = URI.create(baseUrl) + val relativeUri = baseUri.resolve(imageUrl) + relativeUri.toString() + } catch (e: Exception) { + e.printStackTrace() + "" + } } val loadingComposable = remember(composableNode?.children) { composableNode?.children?.find { it.node?.template == Templates.templateLoading } diff --git a/liveview-android/src/main/java/org/phoenixframework/liveview/ui/view/CrossfadeView.kt b/liveview-android/src/main/java/org/phoenixframework/liveview/ui/view/CrossfadeView.kt index 40b47a23..0a54ec60 100644 --- a/liveview-android/src/main/java/org/phoenixframework/liveview/ui/view/CrossfadeView.kt +++ b/liveview-android/src/main/java/org/phoenixframework/liveview/ui/view/CrossfadeView.kt @@ -5,11 +5,13 @@ import androidx.compose.animation.core.FiniteAnimationSpec import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.PaddingValues import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.Stable import kotlinx.collections.immutable.ImmutableList import org.phoenixframework.liveview.data.constants.Attrs.attrAnimationSpec import org.phoenixframework.liveview.data.constants.Attrs.attrLabel import org.phoenixframework.liveview.data.constants.Attrs.attrTargetState +import org.phoenixframework.liveview.data.constants.Attrs.attrViewId import org.phoenixframework.liveview.data.core.CoreAttribute import org.phoenixframework.liveview.domain.data.ComposableTreeNode import org.phoenixframework.liveview.ui.base.CommonComposableProperties @@ -19,6 +21,37 @@ import org.phoenixframework.liveview.ui.base.ComposableViewFactory import org.phoenixframework.liveview.ui.base.PushEvent import org.phoenixframework.liveview.ui.phx_components.PhxLiveView +/** + * Crossfade allows to switch between two layouts with a crossfade animation. + * The current layout must be set in the `targetState` and the new layout value can be get in + * different ways depending of the usage. In the example below, the value is obtained using + * `labelValue(, )` where the `ViewId` must be same specified in the `viewId` + * attribute, and the `Type` must be: `String`, `Int`, `Float`, `Boolean`, or the class name of a + * class supported by modifiers or attributes. + * ``` + * + * + * + * ``` + * In case of the child of `CrossfadeView` needs the parent value in a property, you can do the + * following: + * ``` + * + * + * + * + * ``` + */ internal class CrossfadeView private constructor(props: Properties) : ComposableView(props) { @Composable @@ -36,8 +69,14 @@ internal class CrossfadeView private constructor(props: Properties) : animationSpec = animationSpec ?: tween(), label = label ?: "Crossfade", ) { targetValue -> - // TODO Should we pass the target value to the server somehow? - targetValue + props.viewId?.let { + saveViewValue(it, targetValue) + DisposableEffect(it) { + onDispose { + removeViewValue(it) + } + } + } composableNode?.children?.forEach { PhxLiveView(it, pushEvent, composableNode, null, this) } @@ -49,6 +88,7 @@ internal class CrossfadeView private constructor(props: Properties) : val targetState: Any = Unit, val animationSpec: FiniteAnimationSpec? = null, val label: String? = null, + val viewId: String? = null, override val commonProps: CommonComposableProperties = CommonComposableProperties(), ) : ComposableProperties @@ -63,6 +103,7 @@ internal class CrossfadeView private constructor(props: Properties) : attrAnimationSpec -> animationSpec(props, attribute.value) attrLabel -> label(props, attribute.value) attrTargetState -> targetState(props, attribute.value) + attrViewId -> viewId(props, attribute.value) else -> props.copy( commonProps = handleCommonAttributes( props.commonProps, @@ -85,5 +126,9 @@ internal class CrossfadeView private constructor(props: Properties) : private fun targetState(props: Properties, value: String): Properties { return props.copy(targetState = value) } + + private fun viewId(props: Properties, value: String): Properties { + return props.copy(viewId = value) + } } } diff --git a/liveview-android/src/test/java/com/dockyard/liveviewtest/liveview/test/util/ModifierBaseTest.kt b/liveview-android/src/test/java/com/dockyard/liveviewtest/liveview/test/util/ModifierBaseTest.kt index bf9479df..fae085aa 100644 --- a/liveview-android/src/test/java/com/dockyard/liveviewtest/liveview/test/util/ModifierBaseTest.kt +++ b/liveview-android/src/test/java/com/dockyard/liveviewtest/liveview/test/util/ModifierBaseTest.kt @@ -23,7 +23,7 @@ abstract class ModifierBaseTest: BaseTest() { scope: Any? = null, pushEvent: PushEvent? = null ) { - ModifiersParser.fromStyleFile(styleContentString, pushEvent) + ModifiersParser.fromStyleFile(styleContentString, scope, pushEvent) val styleName = getStyleName(styleContentString) val result = Modifier.fromStyleName(styleName, scope, pushEvent) assertEquals(result, targetModifier) From 7f3fcfc72268c53b1cc043b30eed66ea8ca927aa Mon Sep 17 00:00:00 2001 From: Nelson Glauber Date: Tue, 9 Jul 2024 11:00:51 -0300 Subject: [PATCH 4/4] Added BoxWithConstraints and fix some issues --- .../data/constants/ComposableTypes.kt | 1 + .../ui/modifiers/ModifierDataAdapter.kt | 11 ++ .../liveview/ui/modifiers/ModifiersParser.kt | 40 ++--- .../liveview/ui/modifiers/Util.kt | 7 +- .../ui/registry/ComposableNodeFactory.kt | 2 + .../ui/view/BoxWithConstraintsView.kt | 153 ++++++++++++++++++ .../liveview/ui/view/CrossfadeView.kt | 47 ++++++ 7 files changed, 241 insertions(+), 20 deletions(-) create mode 100644 liveview-android/src/main/java/org/phoenixframework/liveview/ui/view/BoxWithConstraintsView.kt diff --git a/liveview-android/src/main/java/org/phoenixframework/liveview/data/constants/ComposableTypes.kt b/liveview-android/src/main/java/org/phoenixframework/liveview/data/constants/ComposableTypes.kt index e90fb678..6ef53c37 100644 --- a/liveview-android/src/main/java/org/phoenixframework/liveview/data/constants/ComposableTypes.kt +++ b/liveview-android/src/main/java/org/phoenixframework/liveview/data/constants/ComposableTypes.kt @@ -9,6 +9,7 @@ object ComposableTypes { const val badgedBox = "BadgedBox" const val basicAlertDialog = "BasicAlertDialog" const val box = "Box" + const val boxWithConstraints = "BoxWithConstraints" const val bottomAppBar = "BottomAppBar" const val bottomSheetScaffold = "BottomSheetScaffold" const val button = "Button" diff --git a/liveview-android/src/main/java/org/phoenixframework/liveview/ui/modifiers/ModifierDataAdapter.kt b/liveview-android/src/main/java/org/phoenixframework/liveview/ui/modifiers/ModifierDataAdapter.kt index 4e99a533..1bb8ced8 100644 --- a/liveview-android/src/main/java/org/phoenixframework/liveview/ui/modifiers/ModifierDataAdapter.kt +++ b/liveview-android/src/main/java/org/phoenixframework/liveview/ui/modifiers/ModifierDataAdapter.kt @@ -399,6 +399,17 @@ class ModifierDataAdapter(tupleExpression: ElixirParser.TupleExprContext) { internal const val TypeString = "String" internal const val TypeUnary = "Unary" internal const val TypeUndefined = "" + + // TODO We're just supporting primitive types from now + fun typeFromClass(value: Any?): String { + return when (value) { + is Boolean -> TypeBoolean + is Float, is Double -> TypeFloat + is Int -> TypeInt + is String -> TypeString + else -> TypeString + } + } } } diff --git a/liveview-android/src/main/java/org/phoenixframework/liveview/ui/modifiers/ModifiersParser.kt b/liveview-android/src/main/java/org/phoenixframework/liveview/ui/modifiers/ModifiersParser.kt index 54730ebb..ff01e30b 100644 --- a/liveview-android/src/main/java/org/phoenixframework/liveview/ui/modifiers/ModifiersParser.kt +++ b/liveview-android/src/main/java/org/phoenixframework/liveview/ui/modifiers/ModifiersParser.kt @@ -123,10 +123,7 @@ import org.phoenixframework.liveview.stylesheet.ElixirParser.MapExprContext import org.phoenixframework.liveview.stylesheet.ElixirParser.TupleExprContext import org.phoenixframework.liveview.ui.base.ComposableView import org.phoenixframework.liveview.ui.base.PushEvent -import org.phoenixframework.liveview.ui.modifiers.ModifierDataAdapter.Companion.TypeAtom -import org.phoenixframework.liveview.ui.modifiers.ModifierDataAdapter.Companion.TypeDot import org.phoenixframework.liveview.ui.modifiers.ModifierDataAdapter.Companion.TypeLambdaValue -import org.phoenixframework.liveview.ui.modifiers.ModifierDataAdapter.Companion.TypeString import org.phoenixframework.liveview.ui.view.ExposedDropdownMenuBoxScopeWrapper internal object ModifiersParser { @@ -506,26 +503,31 @@ internal object ModifiersParser { arguments: List, ): List { return arguments.map { + // Lambda value must have two arguments: Data type and ViewId if (it.type == TypeLambdaValue && it.listValue.size == 2) { - ModifierDataAdapter.ArgumentData( - it.name, - TypeDot, - listOf( - ModifierDataAdapter.ArgumentData( - null, - TypeAtom, - it.listValue[0].value.toString() - ), - ModifierDataAdapter.ArgumentData( - null, - TypeString, - ComposableView.getViewValue(it.listValue[1].value.toString()) - ), - ) - ) + parseLambdaValueArgument(it) } else it } } + private fun parseLambdaValueArgument( + argument: ModifierDataAdapter.ArgumentData, + ): ModifierDataAdapter.ArgumentData { + val viewValue = ComposableView.getViewValue(argument.listValue[1].value.toString()) + val viewValueType = ModifierDataAdapter.typeFromClass(viewValue) + + return ModifierDataAdapter.ArgumentData( + argument.name, + argument.listValue[0].stringValueWithoutColon.toString(), + listOf( + ModifierDataAdapter.ArgumentData( + null, + viewValueType, + viewValue + ), + ) + ) + } + private const val TAG = "ModifiersParser" } \ No newline at end of file diff --git a/liveview-android/src/main/java/org/phoenixframework/liveview/ui/modifiers/Util.kt b/liveview-android/src/main/java/org/phoenixframework/liveview/ui/modifiers/Util.kt index b009f8d9..f572cdcb 100644 --- a/liveview-android/src/main/java/org/phoenixframework/liveview/ui/modifiers/Util.kt +++ b/liveview-android/src/main/java/org/phoenixframework/liveview/ui/modifiers/Util.kt @@ -335,9 +335,14 @@ fun colorFromArgument(argument: ModifierDataAdapter.ArgumentData): Color? { } else if (argument.type == typeColor) { val colorArgbParts = if (argsToCreateArg.first().isList) argsToCreateArg.first().listValue else argsToCreateArg - return if (colorArgbParts.size == 1) { + return if (colorArgbParts.size == 1 && colorArgbParts.first().isInt) { + // Creating color from a single Int Color(colorArgbParts.first().intValue ?: 0) + } else if (colorArgbParts.size == 1 && colorArgbParts.first().isString) { + // Creating color from a String as hex color + colorArgbParts.first().stringValue?.toColor() } else { + // Creating color passing RGBA parts Color( argOrNamedArg(colorArgbParts, argRed, 0)?.intValue ?: 0, argOrNamedArg(colorArgbParts, argGreen, 1)?.intValue ?: 0, diff --git a/liveview-android/src/main/java/org/phoenixframework/liveview/ui/registry/ComposableNodeFactory.kt b/liveview-android/src/main/java/org/phoenixframework/liveview/ui/registry/ComposableNodeFactory.kt index 307ce231..1ed2713c 100644 --- a/liveview-android/src/main/java/org/phoenixframework/liveview/ui/registry/ComposableNodeFactory.kt +++ b/liveview-android/src/main/java/org/phoenixframework/liveview/ui/registry/ComposableNodeFactory.kt @@ -20,6 +20,7 @@ import org.phoenixframework.liveview.ui.view.BadgedBoxView import org.phoenixframework.liveview.ui.view.BottomAppBarView import org.phoenixframework.liveview.ui.view.BottomSheetScaffoldView import org.phoenixframework.liveview.ui.view.BoxView +import org.phoenixframework.liveview.ui.view.BoxWithConstraintsView import org.phoenixframework.liveview.ui.view.ButtonView import org.phoenixframework.liveview.ui.view.CardView import org.phoenixframework.liveview.ui.view.CheckBoxView @@ -88,6 +89,7 @@ object ComposableNodeFactory { registerComponent(ComposableTypes.badgedBox, BadgedBoxView.Factory) registerComponent(ComposableTypes.basicAlertDialog, AlertDialogView.Factory) registerComponent(ComposableTypes.box, BoxView.Factory) + registerComponent(ComposableTypes.boxWithConstraints, BoxWithConstraintsView.Factory) registerComponent(ComposableTypes.bottomAppBar, BottomAppBarView.Factory) registerComponent(ComposableTypes.bottomSheetScaffold, BottomSheetScaffoldView.Factory) registerComponent(ComposableTypes.button, ButtonView.Factory) diff --git a/liveview-android/src/main/java/org/phoenixframework/liveview/ui/view/BoxWithConstraintsView.kt b/liveview-android/src/main/java/org/phoenixframework/liveview/ui/view/BoxWithConstraintsView.kt new file mode 100644 index 00000000..36df9e15 --- /dev/null +++ b/liveview-android/src/main/java/org/phoenixframework/liveview/ui/view/BoxWithConstraintsView.kt @@ -0,0 +1,153 @@ +package org.phoenixframework.liveview.ui.view + +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.Stable +import androidx.compose.ui.Alignment +import kotlinx.collections.immutable.ImmutableList +import org.phoenixframework.liveview.data.constants.Attrs.attrContentAlignment +import org.phoenixframework.liveview.data.constants.Attrs.attrPropagateMinConstraints +import org.phoenixframework.liveview.data.constants.Attrs.attrViewId +import org.phoenixframework.liveview.data.core.CoreAttribute +import org.phoenixframework.liveview.domain.data.ComposableTreeNode +import org.phoenixframework.liveview.domain.extensions.paddingIfNotNull +import org.phoenixframework.liveview.ui.base.CommonComposableProperties +import org.phoenixframework.liveview.ui.base.ComposableProperties +import org.phoenixframework.liveview.ui.base.ComposableView +import org.phoenixframework.liveview.ui.base.ComposableViewFactory +import org.phoenixframework.liveview.ui.base.PushEvent +import org.phoenixframework.liveview.ui.phx_components.PhxLiveView + +/** + * `BoxWithConstraints` defines its own content according to the available space, based on the + * incoming constraints or the current LayoutDirection. + * ``` + * + * + * Text + * + * + * ``` + */ +internal class BoxWithConstraintsView private constructor(props: Properties) : + ComposableView(props) { + + @Composable + override fun Compose( + composableNode: ComposableTreeNode?, + paddingValues: PaddingValues?, + pushEvent: PushEvent, + ) { + val contentAlignment: Alignment = props.contentAlignment + val propagateMinConstraints = props.propagateMinConstraints + + BoxWithConstraints( + contentAlignment = contentAlignment, + propagateMinConstraints = propagateMinConstraints, + modifier = props.commonProps.modifier + .paddingIfNotNull(paddingValues), + ) { + props.viewId?.let { + saveViewValue("$it.minHeight", minHeight.value) + saveViewValue("$it.minWidth", minWidth.value) + saveViewValue("$it.maxHeight", maxHeight.value) + saveViewValue("$it.maxWidth", maxWidth.value) + DisposableEffect(it) { + onDispose { + removeViewValue("$it.minHeight") + removeViewValue("$it.minWidth") + removeViewValue("$it.maxHeight") + removeViewValue("$it.maxWidth") + } + } + } + composableNode?.children?.forEach { + PhxLiveView(it, pushEvent, composableNode, null, this) + } + } + } + + @Stable + internal data class Properties( + val contentAlignment: Alignment = Alignment.TopStart, + val propagateMinConstraints: Boolean = false, + val viewId: String? = null, + override val commonProps: CommonComposableProperties = CommonComposableProperties(), + ) : ComposableProperties + + internal object Factory : ComposableViewFactory() { + /** + * Creates a `BoxWithConstraintsView` object based on the attributes of the input + * `Attributes` object. `BoxWithConstraintsView` co-relates to the `BoxWithConstraints` + * composable. + * @param attributes the `Attributes` object to create the `BoxView` object from + * @return a `BoxWithConstraints` object based on the attributes of the input `Attributes` + * object. + */ + override fun buildComposableView( + attributes: ImmutableList, + pushEvent: PushEvent?, + scope: Any?, + ): BoxWithConstraintsView = + BoxWithConstraintsView(attributes.fold(Properties()) { props, attribute -> + when (attribute.name) { + attrContentAlignment -> contentAlignment(props, attribute.value) + attrPropagateMinConstraints -> propagateMinConstraints(props, attribute.value) + attrViewId -> viewId(props, attribute.value) + else -> props.copy( + commonProps = handleCommonAttributes( + props.commonProps, + attribute, + pushEvent, + scope + ) + ) + } + }) + + /** + * The default alignment inside the BoxWithConstraintsView. + * + * ``` + * + * ... + * + * ``` + * @param contentAlignment children's alignment inside the Box. See the supported at + * [org.phoenixframework.liveview.data.constants.AlignmentValues]. + */ + private fun contentAlignment(props: Properties, contentAlignment: String): Properties { + return props.copy( + contentAlignment = alignmentFromString( + contentAlignment, + Alignment.TopStart + ) + ) + } + + /** + * Whether the incoming min constraints should be passed to content. + * + * ``` + * ... + * ``` + * @param value true if the incoming min constraints should be passed to content, false + * otherwise. + */ + private fun propagateMinConstraints(props: Properties, value: String): Properties { + return props.copy(propagateMinConstraints = value.toBoolean()) + } + + /** + * Assigns an id to the view in order to get the lamdba value from its children. + * ``` + * + * ``` + */ + private fun viewId(props: Properties, value: String): Properties { + return props.copy(viewId = value) + } + } +} \ No newline at end of file diff --git a/liveview-android/src/main/java/org/phoenixframework/liveview/ui/view/CrossfadeView.kt b/liveview-android/src/main/java/org/phoenixframework/liveview/ui/view/CrossfadeView.kt index 0a54ec60..bdfac4d5 100644 --- a/liveview-android/src/main/java/org/phoenixframework/liveview/ui/view/CrossfadeView.kt +++ b/liveview-android/src/main/java/org/phoenixframework/liveview/ui/view/CrossfadeView.kt @@ -115,18 +115,65 @@ internal class CrossfadeView private constructor(props: Properties) : } }) + /** + * The AnimationSpec to configure the animation. + * ``` + * + * ``` + */ private fun animationSpec(props: Properties, value: String): Properties { return props.copy(animationSpec = finiteAnimationSpecFromString(value)) } + /** + * An optional label to differentiate from other animations in Android Studio. + * ``` + * + * ``` + */ private fun label(props: Properties, value: String): Properties { return props.copy(label = value) } + /** + * TargetState is a key representing your target layout state. Every time you change a key + * the animation will be triggered. The content called with the old key will be faded out + * while the content called with the new key will be faded in. To use the new target value + * in the children nodes, use the `lambdaValue` expressions like below. + * ``` + * + * + * + * ``` + * In case of the child of `CrossfadeView` needs the parent value in a property, you can do the + * following: + * ``` + * + * + * + * + * ``` + */ private fun targetState(props: Properties, value: String): Properties { return props.copy(targetState = value) } + /** + * Assigns an id to the view in order to get the lamdba value from its children. + * ``` + * + * ``` + */ private fun viewId(props: Properties, value: String): Properties { return props.copy(viewId = value) }