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 + + + GibsonRuitiari +
+ 8BitsLives .❤️ +
+ tamzi
Frank Tamre
- + + michaelbukachi
Michael Bukachi
- - + keithchad @@ -189,13 +196,6 @@ We would endlessly like to thank the following contributors
Beatrice Kinya
- - - - GibsonRuitiari -
- 8BitsLives .❤️ -
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