diff --git a/README.md b/README.md
index 691c4f7..922e968 100644
--- a/README.md
+++ b/README.md
@@ -140,21 +140,28 @@ We would endlessly like to thank the following contributors
Evans Chepsiror
+
|
diff --git a/chai/src/main/java/com/droidconke/chai/atoms/Color.kt b/chai/src/main/java/com/droidconke/chai/atoms/Color.kt
index cc455e7..ed72db0 100644
--- a/chai/src/main/java/com/droidconke/chai/atoms/Color.kt
+++ b/chai/src/main/java/com/droidconke/chai/atoms/Color.kt
@@ -15,7 +15,14 @@
*/
package com.droidconke.chai.atoms
+import androidx.compose.animation.core.*
+import androidx.compose.runtime.*
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.colorspace.ColorSpace
+import androidx.compose.ui.graphics.colorspace.ColorSpaces
+import com.droidconke.chai.utils.chaiAnimationSpec
+import kotlin.math.pow
+import kotlin.reflect.KProperty
/**
* these are the Primary colours from Chai's Design spec document
@@ -43,6 +50,250 @@ val ChaiDarkerGrey = Color(0xFF191d1d)
val ChaiCoal = Color(0xFF20201E)
val ChaiBlack = Color(0xFF000000)
+/**
+ * Defines the colors to be used by the app
+ * This an abstraction that contains all the colors (neutrals,primary & secondary)
+ * needed by the application
+ * A normal class, instead of data class, is used to prevent changing of the colors via copy
+ * as that would lead to breaking of the intended app design system
+ * Note: The color definition ought to be in Hex Color format
+ * * Example usage see ChaiDemo
+ * @param value the color value given by the client, the value is of a type [Color]
+ */
+@Immutable // since we are using an internal constructor, values read from [ChaiColor] won't change after an instance is constructed
+@JvmInline
+value class ChaiColor internal constructor(val value: Color) {
+ /**
+ * Changes the alpha of the ChaiColor
+ * @param alpha alpha in form float to be applied to current ChaiColor
+ * @return an instance of ChaiColor with modified alpha value
+ */
+ fun changeAlpha(alpha: Float) = ChaiColor(value.copy(alpha = alpha))
+
+ companion object {
+ // should not be used in real components but,
+ // can be used as a base value for ChaiColor
+ @Stable
+ internal val Unspecified = ChaiColor(value = Color.Unspecified)
+
+ /* these are the Primary colours from Chai's Design spec document*/
+ @Stable
+ val ChaiBlue = ChaiColor(value = Color(0xFF000CEB))
+ @Stable
+ val ChaiWhite = ChaiColor(value = Color(0xFFFFFFFF))
+
+ /* these are the Secondary colours from Chai's Design spec document */
+ @Stable
+ val ChaiRed = ChaiColor(value = Color(0xFFFF6E4D))
+ @Stable
+ val ChaiTeal = ChaiColor(value = Color(0xFF00e2c3))
+ @Stable
+ val ChaiFadedLime = ChaiColor(value = Color(0xFF7de1c3))
+
+ /* these are the Neutrals from the Chai's Design spec document */
+ @Stable
+ val ChaiLightGrey = ChaiColor(value = Color(0xFFF5F5F5))
+ @Stable
+ val ChaiGrey = ChaiColor(value = Color(0xFFB1B1B1))
+ @Stable
+ val ChaiDarkGrey = ChaiColor(value = Color(0xFF5A5A5A))
+ @Stable
+ val ChaiDarkerGrey = ChaiColor(value = Color(0xFF191d1d))
+ @Stable
+ val ChaiCoal = ChaiColor(value = Color(0xFF20201E))
+ @Stable
+ val ChaiBlack = ChaiColor(value = Color(0xFF000000))
+
+ /**
+ * Typically colors are 3 dimensional planes i.e x,y,z
+ * with the x being red, y being green and z blue
+ * The three combined create a [ColorSpace] i.e a unique representation
+ * of colors that can be created by combining the three pigments
+ * To convert one color to the other, a mapping of these color spaces is needed
+ * from [xyz] plane to another [xyz] plane.
+ * for comprehensive explanation see [full_explanation] (https://en.wikipedia.org/wiki/Color_space)
+ * note: adapted from aosp
+ */
+ private fun multiplyColumn(
+ column: Int,
+ x: Float,
+ y: Float,
+ z: Float,
+ matrix: FloatArray
+ ) = x * matrix[column] + y * matrix[3 + column] + z * matrix[6 + column]
+ private val M1 = floatArrayOf(
+ 0.80405736f,
+ 0.026893456f,
+ 0.04586542f,
+ 0.3188387f,
+ 0.9319606f,
+ 0.26299807f,
+ -0.11419419f,
+ 0.05105356f,
+ 0.83999807f,
+ )
+
+ private val InverseM1 = floatArrayOf(
+ 1.2485008f,
+ -0.032856926f,
+ -0.057883114f,
+ -0.48331892f,
+ 1.1044513f,
+ -0.3194066f,
+ 0.19910365f,
+ -0.07159331f,
+ 1.202023f,
+ )
+ internal val VectorConverter: (colorSpace: ColorSpace) -> TwoWayConverter =
+ { colorSpace ->
+ TwoWayConverter(
+ convertToVector = { chaiColor ->
+ val color by chaiColor
+ val colorXyz = color.convert(
+ colorSpace = ColorSpaces.CieXyz,
+ )
+
+ val x = colorXyz.red
+ val y = colorXyz.green
+ val z = colorXyz.blue
+
+ val l = multiplyColumn(
+ column = 0,
+ x = x,
+ y = y,
+ z = z,
+ matrix = M1,
+ ).pow(
+ x = 1f / 3f,
+ )
+ val a = multiplyColumn(
+ column = 1,
+ x = x,
+ y = y,
+ z = z,
+ matrix = M1,
+ ).pow(
+ x = 1f / 3f,
+ )
+ val b = multiplyColumn(
+ column = 2,
+ x = x,
+ y = y,
+ z = z,
+ matrix = M1,
+ ).pow(
+ x = 1f / 3f,
+ )
+
+ AnimationVector4D(
+ v1 = color.alpha,
+ v2 = l,
+ v3 = a,
+ v4 = b,
+ )
+ },
+ convertFromVector = { vector ->
+ val l = vector.v2.pow(
+ x = 3f,
+ )
+ val a = vector.v3.pow(
+ x = 3f,
+ )
+ val b = vector.v4.pow(
+ x = 3f,
+ )
+
+ val x = multiplyColumn(
+ column = 0,
+ x = l,
+ y = a,
+ z = b,
+ matrix = InverseM1,
+ )
+ val y = multiplyColumn(
+ column = 1,
+ x = l,
+ y = a,
+ z = b,
+ matrix = InverseM1,
+ )
+ val z = multiplyColumn(
+ column = 2,
+ x = l,
+ y = a,
+ z = b,
+ matrix = InverseM1,
+ )
+
+ val colorXyz = Color(
+ alpha = vector.v1.coerceIn(
+ minimumValue = 0f,
+ maximumValue = 1f,
+ ),
+ red = x.coerceIn(
+ minimumValue = -2f,
+ maximumValue = 2f,
+ ),
+ green = y.coerceIn(
+ minimumValue = -2f,
+ maximumValue = 2f,
+ ),
+ blue = z.coerceIn(
+ minimumValue = -2f,
+ maximumValue = 2f,
+ ),
+ colorSpace = ColorSpaces.CieXyz,
+ )
+
+ ChaiColor(
+ value = colorXyz.convert(
+ colorSpace = colorSpace,
+ ),
+ )
+ }
+ )
+ }
+ }
+ operator fun getValue(thisRef: Any?, property: KProperty<*>) = value
+}
+
+/**
+ * Constructs a color animation spec using [tween]
+ * The duration millis is divided by 2 since color animation
+ * more often than not will depend on other animations,
+ * so it needs to run and complete before other animations to ensure a
+ * smooth transition
+ * @receiver animation spec to be used, ideally should be an instance of [TweenSpec]
+ * @return a new instance of animation spec whose duration is divided by 2
+ */
+private fun AnimationSpec.toColorSpec(): AnimationSpec {
+ val tweenSpec = this as TweenSpec ?: return this
+ return tween(
+ durationMillis = tweenSpec.durationMillis / 2,
+ delayMillis = tweenSpec.delay, easing = tweenSpec.easing
+ )
+}
+/**
+ * Animates [ChaiColor] changes from one color to the other
+ * @param targetValue an instance of [ChaiColor]
+ * @param animationSpec animationSpec to be used when color change is detected/happens
+ * @return state object of type [ChaiColor]
+ */
+@Composable
+internal fun animateChaiColorAsState(
+ targetValue: ChaiColor,
+ animationSpec: AnimationSpec = chaiAnimationSpec()
+): State {
+ val converter = remember(key1 = targetValue.value.colorSpace) {
+ (ChaiColor.VectorConverter)(targetValue.value.colorSpace)
+ }
+ return animateValueAsState(
+ targetValue = targetValue,
+ typeConverter = converter,
+ animationSpec = animationSpec.toColorSpec(),
+ finishedListener = null
+ )
+}
/**
* TOBE Replaced
*/
diff --git a/chai/src/main/java/com/droidconke/chai/components/CText.kt b/chai/src/main/java/com/droidconke/chai/components/CText.kt
index 5bc94c6..a8bf384 100644
--- a/chai/src/main/java/com/droidconke/chai/components/CText.kt
+++ b/chai/src/main/java/com/droidconke/chai/components/CText.kt
@@ -15,16 +15,13 @@
*/
package com.droidconke.chai.components
-import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.NonRestartableComposable
+import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.unit.sp
-import com.droidconke.chai.atoms.*
-import com.droidconke.chai.atoms.type.MontserratRegular
-import com.droidconke.chai.atoms.type.MontserratThin
+import com.droidconke.chai.atoms.ChaiColor
+import com.droidconke.chai.utils.AnimateContentChange
/**
* CText:
@@ -40,63 +37,147 @@ import com.droidconke.chai.atoms.type.MontserratThin
* */
/**
- * Title based fonts
- * */
+ * Basic Text Construct, that adheres to the app's design system,
+ * which is to be used by clients whenever they need
+ * a text component in their composables
+ * @param modifier modifier to be applied to the text component
+ * @param style ChaiTextStyle design to be applied, note the design includes
+ * the font-family+weight, color, and text size
+ * @param singleLine whether this text is a single line
+ */
@Composable
-fun CParagraph(dParagraph: String) {
+internal fun ChaiText(
+ modifier: Modifier = Modifier,
+ text: String,
+ style: ChaiTextStyle,
+ singleLine: Boolean = true
+) {
+ val styleAnimationState by animateChaiTextStyleAsState(targetValue = style)
Text(
- text = dParagraph,
- style = TextStyle(
- color = ChaiBlack,
- fontSize = 12.sp,
- fontWeight = FontWeight.W500,
- fontFamily = MontserratRegular,
- ),
- modifier = Modifier.fillMaxWidth()
+ text = text, style = styleAnimationState.asComposedStyle(),
+ modifier = modifier,
+ maxLines = when (singleLine) {
+ true -> 1
+ else -> Int.MAX_VALUE
+ }
)
}
-
+/**
+ * Basic Text Construct that construct an animated text, and adheres to the app's design system,
+ * This is to be used by clients whenever they need a text component in their composables
+ * @param modifier modifier to be applied to the text component
+ * @param style ChaiTextStyle design to be applied, note the design includes
+ * the font-family+weight, color, and text size
+ * @param singleLine whether this text is a single line
+ */
@Composable
-fun CPageTitle(pageTitle: String) {
- Text(
- text = pageTitle,
- style = TextStyle(
- color = ChaiBlue,
- fontSize = 33.sp,
- fontWeight = FontWeight.W300,
- fontFamily = MontserratThin,
-
- ),
- modifier = Modifier.fillMaxWidth()
- )
+internal fun AnimatedChaiText(
+ modifier: Modifier = Modifier,
+ text: String,
+ style: ChaiTextStyle,
+ singleLine: Boolean = true
+) {
+ val styleAnimationState by animateChaiTextStyleAsState(targetValue = style)
+ AnimateContentChange(targetState = text, modifier = modifier) { animatedText ->
+ Text(
+ text = animatedText, style = styleAnimationState.asComposedStyle(),
+ maxLines = when (singleLine) {
+ true -> 1
+ else -> Int.MAX_VALUE
+ }
+ )
+ }
}
+/**
+ * Title based fonts
+ * */
+
@Composable
-fun CSubtitle(dSubtitle: String) {
- Text(
- text = dSubtitle,
- style = TextStyle(
- color = ChaiRed,
- fontSize = 15.sp,
- fontWeight = FontWeight.W700,
- fontFamily = MontserratRegular,
+// directly calls another composable thus we need to tell compiler to skip this during recomposition
+@NonRestartableComposable
+fun ChaiParagraph(
+ modifier: Modifier = Modifier,
+ text: String,
+ color: ChaiColor = ChaiColor.ChaiBlack
+) = ChaiText(
+ modifier = modifier,
+ text = text,
+ style = ChaiTextStyle.CParagraphStyle.change(color = color)
+)
- ),
- modifier = Modifier.fillMaxWidth()
- )
-}
+@Composable
+@NonRestartableComposable
+fun ChaiPageTitle(
+ modifier: Modifier = Modifier,
+ text: String,
+ color: ChaiColor = ChaiColor.ChaiCoal
+) = AnimatedChaiText(
+ text = text,
+ modifier = modifier,
+ style = ChaiTextStyle.CPageTitleStyle.change(color = color)
+)
@Composable
-fun CActionText(cAction: String) {
- Text(
- text = cAction,
- style = TextStyle(
- color = ChaiRed,
- fontSize = 15.sp,
- fontWeight = FontWeight.W700,
- fontFamily = MontserratRegular,
+@NonRestartableComposable
+fun ChaiSubtitle(modifier: Modifier = Modifier, text: String, color: ChaiColor = ChaiColor.ChaiBlack) =
+ ChaiText(text = text, modifier = modifier, style = ChaiTextStyle.CSubtitleStyle.change(color = color))
- ),
- modifier = Modifier.fillMaxWidth()
- )
-}
\ No newline at end of file
+// @Composable
+// fun CParagraph(dParagraph: String) {
+// Text(
+// text = dParagraph,
+// style = TextStyle(
+// color = ChaiBlack,
+// fontSize = 12.sp,
+// fontWeight = FontWeight.W500,
+// fontFamily = MontserratRegular,
+// ),
+// modifier = Modifier.fillMaxWidth()
+// )
+// }
+//
+// @Composable
+// fun CPageTitle(pageTitle: String) {
+// Text(
+// text = pageTitle,
+// style = TextStyle(
+// color = ChaiBlue,
+// fontSize = 33.sp,
+// fontWeight = FontWeight.W300,
+// fontFamily = MontserratThin,
+//
+// ),
+// modifier = Modifier.fillMaxWidth()
+// )
+// }
+//
+// @Composable
+// fun CSubtitle(dSubtitle: String) {
+// Text(
+// text = dSubtitle,
+// style = TextStyle(
+// color = ChaiRed,
+// fontSize = 15.sp,
+// fontWeight = FontWeight.W700,
+// fontFamily = MontserratRegular,
+//
+// ),
+// modifier = Modifier.fillMaxWidth()
+// )
+// }
+//
+// @Composable
+// fun CActionText(cAction: String) {
+// Text(
+// text = cAction,
+// style = TextStyle(
+// color = ChaiRed,
+// fontSize = 15.sp,
+// fontWeight = FontWeight.W700,
+// fontFamily = MontserratRegular,
+//
+// ),
+// modifier = Modifier.fillMaxWidth()
+// )
+// }
\ No newline at end of file
diff --git a/chai/src/main/java/com/droidconke/chai/components/CTextStyle.kt b/chai/src/main/java/com/droidconke/chai/components/CTextStyle.kt
new file mode 100644
index 0000000..90db446
--- /dev/null
+++ b/chai/src/main/java/com/droidconke/chai/components/CTextStyle.kt
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2022 DroidconKE
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.droidconke.chai.components
+
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.State
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.Font
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.ExperimentalUnitApi
+import androidx.compose.ui.unit.TextUnit
+import androidx.compose.ui.unit.TextUnitType
+import androidx.compose.ui.unit.sp
+import com.droidconke.chai.R
+import com.droidconke.chai.atoms.ChaiColor
+import com.droidconke.chai.atoms.animateChaiColorAsState
+import com.droidconke.chai.utils.animateChaiAsState
+import com.droidconke.chai.utils.chaiAnimationSpec
+
+/**
+ * Defines the ChaiDesign system TextStyle
+ * This is the text style to be used by the client/across
+ * the app instead of [TextStyle]. (Maybe implement a custom lint warning to enforce this?)
+ * Example usage see ChaiDemo
+ * @param color which is of type [ChaiColor]
+ * @param size text size
+ * @param weight text weight
+ * @param letterSpacing the spacing of letters
+ * @param textAlign TextAlignment by default is [TextAlign.Center]
+ */
+@Immutable
+class ChaiTextStyle internal constructor(
+ val color: ChaiColor = ChaiColor.ChaiBlack,
+ val size: TextUnit,
+ val weight: FontWeight,
+ val letterSpacing: TextUnit = 0.sp,
+ val fontFamily: FontFamily,
+ val textAlign: TextAlign = TextAlign.Center
+) {
+ /**
+ * Converts an instance of [ChaiTextStyle] into compose [TextStyle]
+ * @return text style
+ */
+ @Stable
+ internal fun asComposedStyle() = TextStyle(
+ color = color.value,
+ fontSize = size, fontWeight = weight, fontFamily = fontFamily,
+ letterSpacing = letterSpacing, textAlign = textAlign
+ )
+
+ companion object {
+ // annotating the variables with @Stable because it is a much stronger contract than val
+ // note: compose compiler takes val to be unstable
+ @Stable
+ private val montserratRegular = FontFamily(Font(R.font.montserrat_regular))
+ @Stable
+ private val montserratThin = FontFamily(Font(R.font.montserrat_thin))
+ @Stable
+ val CActionStyle = ChaiTextStyle(
+ color = ChaiColor.ChaiRed, size = 10.sp,
+ weight = FontWeight.W700, fontFamily = montserratRegular
+ )
+ @Stable
+ val CSubtitleStyle = ChaiTextStyle(
+ color = ChaiColor.ChaiRed, size = 15.sp,
+ fontFamily = montserratRegular, weight = FontWeight.W700
+ )
+ @Stable
+ val CParagraphStyle = ChaiTextStyle(
+ color = ChaiColor.ChaiBlack,
+ size = 12.sp,
+ fontFamily = montserratRegular, weight = FontWeight.W500
+ )
+ @Stable
+ val CPageTitleStyle = ChaiTextStyle(
+ color = ChaiColor.ChaiBlue,
+ size = 33.sp,
+ fontFamily = montserratThin, weight = FontWeight.W300
+ )
+ }
+
+ /**
+ * This method is appropriate for the times when a text style
+ * is constructed but some properties need to be changed
+ * @param color text color
+ * @param weight font weight
+ * @param textAlign alignment of text
+ * @param fontFamily font family used by text
+ * @return an instance of ChaiTextStyle
+ */
+ @Stable
+ internal fun change(
+ color: ChaiColor = this.color,
+ weight: FontWeight = this.weight,
+ textAlign: TextAlign = this.textAlign,
+ fontFamily: FontFamily = this.fontFamily
+ ) = ChaiTextStyle(
+ color = color,
+ weight = weight,
+ letterSpacing = letterSpacing,
+ fontFamily = fontFamily,
+ size = size, textAlign = textAlign
+ )
+}
+
+@OptIn(ExperimentalUnitApi::class)
+@Stable
+private fun Float.toSp() = TextUnit(value = this, type = TextUnitType.Sp)
+/**
+ * A [ChaiTextStyle] properties animator
+ * Currently it animates only the [ChaiTextStyle.color] & [ChaiTextStyle.size] properties
+ * @param targetValue the [ChaiTextStyle] to be animated
+ * @param animationSpec animation spec to be used during the animation
+ * @return [targetValue] returns an animated [ChaiTextStyle] in form of [State]
+ */
+@Suppress("UNCHECKED_CAST")
+@Composable
+internal fun animateChaiTextStyleAsState(
+ targetValue: ChaiTextStyle,
+ animationSpec: AnimationSpec = chaiAnimationSpec()
+): State {
+ val targetColorAnimationState = animateChaiColorAsState(
+ targetValue = targetValue.color,
+ animationSpec = animationSpec as AnimationSpec
+ )
+ val targetSizeAnimationState = animateFloatAsState(
+ targetValue = targetValue.size.value,
+ animationSpec = animationSpec as AnimationSpec
+ )
+ return animateChaiAsState(
+ initialValue = targetValue,
+ animationStates = listOf(targetColorAnimationState, targetSizeAnimationState),
+ targetBuilder = { animatedValues ->
+ val (color, size) = animatedValues
+ ChaiTextStyle(
+ color = color as ChaiColor, size = (size as Float).toSp(),
+ weight = targetValue.weight, letterSpacing = targetValue.letterSpacing, textAlign = targetValue.textAlign,
+ fontFamily = targetValue.fontFamily
+ )
+ }
+ )
+}
\ No newline at end of file
diff --git a/chai/src/main/java/com/droidconke/chai/icons/ChaiIcon.kt b/chai/src/main/java/com/droidconke/chai/icons/ChaiIcon.kt
new file mode 100644
index 0000000..c40bfca
--- /dev/null
+++ b/chai/src/main/java/com/droidconke/chai/icons/ChaiIcon.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2022 DroidconKE
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.droidconke.chai.icons
+
+import androidx.annotation.DrawableRes
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.Stable
+import com.droidconke.chai.R
+
+/**
+ * An abstraction that defines the icons to be used instead of
+ * accessing icon resources directly
+ * @param drawableId icon drawable resource id
+ */
+@Immutable
+@JvmInline
+value class ChaiIcon private constructor(@DrawableRes val drawableId: Int) {
+ companion object {
+ @Stable
+ val About = ChaiIcon(drawableId = R.drawable.about_icon)
+ @Stable
+ val FeedIcon = ChaiIcon(drawableId = R.drawable.feed_icon)
+ @Stable
+ val HomeIcon = ChaiIcon(drawableId = R.drawable.home_icon)
+ @Stable
+ val SessionsIcon = ChaiIcon(drawableId = R.drawable.sessions_icon)
+ @Stable
+ val BackArrow = ChaiIcon(drawableId = R.drawable.ic_back_arrow)
+ @Stable
+ val GoogleIcon = ChaiIcon(drawableId = R.drawable.ic_google_logo_icon)
+ }
+}
\ No newline at end of file
diff --git a/chai/src/main/java/com/droidconke/chai/images/Images.kt b/chai/src/main/java/com/droidconke/chai/images/Images.kt
index c51d0ef..a97e838 100644
--- a/chai/src/main/java/com/droidconke/chai/images/Images.kt
+++ b/chai/src/main/java/com/droidconke/chai/images/Images.kt
@@ -13,4 +13,47 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.droidconke.chai.images
\ No newline at end of file
+package com.droidconke.chai.images
+
+import androidx.compose.foundation.Image
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.NonRestartableComposable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.res.painterResource
+import com.droidconke.chai.atoms.ChaiColor
+import com.droidconke.chai.icons.ChaiIcon
+import com.droidconke.chai.modifier.chaiClickable
+
+/**
+ * An image component that uses icons
+ *
+ * @param icon to be used which is a drawable
+ * @param tint tint color to be applied to the icon
+ * @param contentDescription content description of the icon
+ * @param rippleEnabled whether ripple animation is enabled
+ * @param onClick on click listener attached to this image component
+ */
+@Composable
+@NonRestartableComposable
+fun ChaiImage(
+ modifier: Modifier = Modifier,
+ icon: ChaiIcon?,
+ tint: ChaiColor? = null,
+ contentDescription: String? = null,
+ rippleEnabled: Boolean = true,
+ onClick: (() -> Unit)? = null
+) {
+ if (icon == null) return
+ Image(
+ modifier = modifier.chaiClickable(
+ rippleEnabled = rippleEnabled,
+ onClick = onClick
+ ),
+ painter = painterResource(id = icon.drawableId),
+ contentDescription = contentDescription,
+ colorFilter = tint.toColorFilter()
+ )
+}
+
+private fun ChaiColor?.toColorFilter() = this?.run { ColorFilter.tint(color = value) }
\ No newline at end of file
diff --git a/chai/src/main/java/com/droidconke/chai/modifier/clickable.kt b/chai/src/main/java/com/droidconke/chai/modifier/clickable.kt
new file mode 100644
index 0000000..9ed941e
--- /dev/null
+++ b/chai/src/main/java/com/droidconke/chai/modifier/clickable.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2022 DroidconKE
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.droidconke.chai.modifier
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.material.ripple.rememberRipple
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import com.droidconke.chai.atoms.ChaiColor
+
+/**
+ * Modifier that can be used to make components clickable
+ * @param onClick the listener to be used when component is clicked
+ * @param rippleColor ripple color to be used when component is clicked
+ * @param rippleEnabled whether or not ripple animation/effect is enabled
+ * by default it is
+ * @return clickable modifier
+ */
+@Stable
+internal fun Modifier.chaiClickable(
+ rippleEnabled: Boolean = true,
+ rippleColor: ChaiColor? = null,
+ onClick: (() -> Unit)?
+) = when (onClick != null) {
+ true -> composed {
+ clickable(
+ onClick = onClick,
+ indication = rememberRipple(
+ color = rippleColor?.value ?: ChaiColor.Unspecified.value,
+ ).takeIf {
+ rippleEnabled
+ },
+ interactionSource = remember { MutableInteractionSource() }
+ )
+ }
+ else -> this
+}
\ No newline at end of file
diff --git a/chai/src/main/java/com/droidconke/chai/utils/AnimateContent.kt b/chai/src/main/java/com/droidconke/chai/utils/AnimateContent.kt
new file mode 100644
index 0000000..91cddf1
--- /dev/null
+++ b/chai/src/main/java/com/droidconke/chai/utils/AnimateContent.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2022 DroidconKE
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.droidconke.chai.utils
+
+import androidx.compose.animation.*
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.FiniteAnimationSpec
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.IntSize
+
+/**
+ * Function which animates content change whenever state of a composable state
+ *@param targetState composable's state to watch for changes
+ *@param content content to be shown by AnimatedContent container
+ *@param animationSpec animation spec to be used by default chaiAnimationSpec is used
+ */
+@Suppress("UNCHECKED_CAST")
+@OptIn(ExperimentalAnimationApi::class)
+@Composable
+internal fun AnimateContentChange(
+ modifier: Modifier = Modifier,
+ targetState: T,
+ animationSpec: AnimationSpec = chaiAnimationSpec(),
+ content: @Composable AnimatedVisibilityScope.(animatedTargetState: T) -> Unit
+) {
+ AnimatedContent(
+ targetState = targetState, modifier = modifier, content = content,
+ transitionSpec = {
+ fadeIn(
+ animationSpec = animationSpec as FiniteAnimationSpec,
+ ) with fadeOut(
+ animationSpec = animationSpec as FiniteAnimationSpec,
+ ) using SizeTransform(
+ clip = false,
+ sizeAnimationSpec = { _, _ ->
+ animationSpec as FiniteAnimationSpec
+ },
+ )
+ }
+ )
+}
\ No newline at end of file
diff --git a/chai/src/main/java/com/droidconke/chai/utils/animateAsState.kt b/chai/src/main/java/com/droidconke/chai/utils/animateAsState.kt
new file mode 100644
index 0000000..dc7b981
--- /dev/null
+++ b/chai/src/main/java/com/droidconke/chai/utils/animateAsState.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2022 DroidconKE
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.droidconke.chai.utils
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.snapshotFlow
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+
+private fun State.toFlow() = snapshotFlow { this }
+
+/**
+ * Animates Chai's design properties, and returns a new instance
+ * of Chai's design property in form of a [State]
+ * @param initialValue initial value of the design resource/property must be of type ChaiDesign eg ChaiIcon
+ * @param animationStates a list of animation states which are created from [animate*AsState] functions
+ * @param targetBuilder a function that takes in a list of animated design properties and returns a single
+ * value of type ChaiDesign (of type [T]) eg ChaiIcon wrapped in [State]
+ * @return an animated state which is an instance ChaiDesign wrapped in [State]
+ */
+@Composable
+internal inline fun animateChaiAsState(
+ initialValue: T,
+ animationStates: List>,
+ crossinline targetBuilder: (animatedValues: List) -> T
+): State {
+ val animationFlows: List> = animationStates.map(State<*>::toFlow)
+ // (combine scales linearly; find a better option probably)?
+ return combine(flows = animationFlows) { _animationFlows ->
+ targetBuilder(
+ _animationFlows.mapIndexed { index, flow ->
+ (flow as State<*>).value
+ ?: throw NullPointerException(
+ "animation of the individual element at index $index of type " +
+ "is null hence cannot be animated"
+ )
+ }
+ )
+ }.collectAsState(initial = initialValue)
+}
\ No newline at end of file
diff --git a/chai/src/main/java/com/droidconke/chai/icons/Icons.kt b/chai/src/main/java/com/droidconke/chai/utils/spec.kt
similarity index 52%
rename from chai/src/main/java/com/droidconke/chai/icons/Icons.kt
rename to chai/src/main/java/com/droidconke/chai/utils/spec.kt
index 6728353..53d3ede 100644
--- a/chai/src/main/java/com/droidconke/chai/icons/Icons.kt
+++ b/chai/src/main/java/com/droidconke/chai/utils/spec.kt
@@ -13,4 +13,22 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.droidconke.chai.icons
\ No newline at end of file
+package com.droidconke.chai.utils
+
+import androidx.compose.animation.core.FastOutSlowInEasing
+import androidx.compose.animation.core.tween
+import androidx.compose.runtime.Stable
+
+/**
+ * Default duration time to be used in ChaiDesign system
+ */
+const val ChaiDefaultAnimationMillis = 250
+/**
+ * Basic/default animation spec to be used by ChaiDesign system
+ * @return tween animation spec
+ */
+@Stable
+internal fun chaiAnimationSpec() = tween(
+ durationMillis = ChaiDefaultAnimationMillis,
+ easing = FastOutSlowInEasing
+)
\ No newline at end of file
diff --git a/chaidemo/src/main/java/com/droidconke/chaidemo/ChaiDemo.kt b/chaidemo/src/main/java/com/droidconke/chaidemo/ChaiDemo.kt
index 5859fe7..124cf03 100644
--- a/chaidemo/src/main/java/com/droidconke/chaidemo/ChaiDemo.kt
+++ b/chaidemo/src/main/java/com/droidconke/chaidemo/ChaiDemo.kt
@@ -22,7 +22,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.droidconke.chai.ChaiDCKE22Theme
-import com.droidconke.chai.atoms.ChaiWhite
+import com.droidconke.chai.atoms.ChaiColor
import com.droidconke.chai.components.*
import com.droidconke.chai.utils.BreathingSpace13
import com.droidconke.chai.utils.BreathingSpace26
@@ -35,15 +35,15 @@ fun ChaiDemo() {
Column(
Modifier
.fillMaxSize()
- .background(color = ChaiWhite)
+ .background(color = ChaiColor.ChaiWhite.value)
.padding(horizontal = 13.dp, vertical = 5.dp)
) {
BreathingSpace26()
- CPageTitle("Chai Demo")
+ ChaiPageTitle(text ="Chai Demo" )
SeparatorSpace()
- CSubtitle("A catalog of the chai design system elements")
+ ChaiSubtitle(text="A catalog of the chai design system elements")
SeparatorSpace()
- CParagraph("Check the code that is with each view")
+ ChaiParagraph(text="Check the code that is with each view")
BreathingSpace13()
}
}
diff --git a/chaidemo/src/main/java/com/droidconke/chaidemo/screens/TextDemo.kt b/chaidemo/src/main/java/com/droidconke/chaidemo/screens/TextDemo.kt
index c0e4900..82dc05f 100644
--- a/chaidemo/src/main/java/com/droidconke/chaidemo/screens/TextDemo.kt
+++ b/chaidemo/src/main/java/com/droidconke/chaidemo/screens/TextDemo.kt
@@ -19,15 +19,20 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.FloatingActionButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.droidconke.chai.ChaiDCKE22Theme
+import com.droidconke.chai.atoms.ChaiColor
import com.droidconke.chai.atoms.ChaiWhite
-import com.droidconke.chai.components.CPageTitle
-import com.droidconke.chai.components.CParagraph
-import com.droidconke.chai.components.CSubtitle
+import com.droidconke.chai.components.*
+import com.droidconke.chai.icons.ChaiIcon
+import com.droidconke.chai.images.ChaiImage
import com.droidconke.chai.utils.BreathingSpace13
import com.droidconke.chai.utils.BreathingSpace26
import com.droidconke.chai.utils.SeparatorSpace
@@ -40,16 +45,19 @@ fun TextScreen() {
Column(
Modifier
.fillMaxSize()
- .background(color = ChaiWhite)
+ .background(color = ChaiColor.ChaiWhite.value)
.padding(horizontal = 13.dp, vertical = 5.dp)
) {
BreathingSpace26()
- CPageTitle("Welcome Message")
+ ChaiPageTitle(text="Welcome Message")
SeparatorSpace()
- CSubtitle("dcke 2022 welcome remarks as subtitle")
+ ChaiSubtitle(text="dcke 2022 welcome remarks as subtitle")
SeparatorSpace()
- CParagraph("Welcome to droidconKE 2022. Lorem something something")
+ ChaiParagraph(text="Welcome to droidconKE 2022. Lorem something something")
BreathingSpace13()
+ ChaiImage(modifier=Modifier,icon=ChaiIcon.FeedIcon,tint = ChaiColor.ChaiDarkGrey)
+ BreathingSpace13()
+ IconButton(onClick = { }) { Icon(painter = painterResource(id = ChaiIcon.FeedIcon.drawableId), contentDescription = null)}
}
}
}
\ No newline at end of file
|