From 75bf1c3389df01d8c2b308b6d0acda4128b5b9f3 Mon Sep 17 00:00:00 2001 From: Mathieu Date: Sun, 2 Jan 2022 22:53:05 +0100 Subject: [PATCH] Added footer, scrollable conversation and global UI improvements --- .idea/misc.xml | 2 + .../jarvis/android/MainActivity.kt | 77 +++- .../jarvis/android/utils/DotsLoaders.kt | 332 ++++++++++++++++++ ...e_person_24.xml => ic_baseline_mic_24.xml} | 2 +- 4 files changed, 397 insertions(+), 16 deletions(-) create mode 100644 app/src/main/java/ch/mathieubroillet/jarvis/android/utils/DotsLoaders.kt rename app/src/main/res/drawable/{ic_baseline_person_24.xml => ic_baseline_mic_24.xml} (54%) diff --git a/.idea/misc.xml b/.idea/misc.xml index 8ca1baa..6ccae69 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,7 +11,9 @@ + + diff --git a/app/src/main/java/ch/mathieubroillet/jarvis/android/MainActivity.kt b/app/src/main/java/ch/mathieubroillet/jarvis/android/MainActivity.kt index ea90822..35ccfe1 100644 --- a/app/src/main/java/ch/mathieubroillet/jarvis/android/MainActivity.kt +++ b/app/src/main/java/ch/mathieubroillet/jarvis/android/MainActivity.kt @@ -4,9 +4,7 @@ import android.os.Bundle import android.view.WindowManager import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* @@ -16,8 +14,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.font.Font -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -79,9 +75,32 @@ fun Base() { } } +@Composable +fun Footer() { + //We create a row that we align to the bottom center of the parent box + Row( + Modifier + .padding(bottom = 50.dp) + .fillMaxWidth(), + verticalAlignment = Alignment.Bottom, + horizontalArrangement = Arrangement.Center + ) { + //Microphone floating button to manually start/stop listening + FloatingActionButton(onClick = { /*TODO*/ }) { + Icon( + painter = painterResource(id = R.drawable.ic_baseline_mic_24), + contentDescription = "microphone" + ) + } + } +} + @Composable fun MessageFromJarvis(text: String) { + //We create a row to contain the message and the robot image (to look like an sms) Row(Modifier.padding(bottom = 25.dp)) { + + // Adding the robot image as the sender Image( painter = painterResource(id = R.drawable.robot256), contentDescription = "robot", @@ -90,13 +109,13 @@ fun MessageFromJarvis(text: String) { .padding(end = 10.dp) ) + // Adding the message box with the text given in the params Box( modifier = Modifier .fillMaxWidth(fraction = 0.9F) .clip(RoundedCornerShape(15.dp)) .background(color = MaterialTheme.colors.secondaryVariant) .padding(horizontal = 10.dp, vertical = 5.dp) - ) { Text(text = text, fontFamily = productSansFont) } @@ -105,10 +124,13 @@ fun MessageFromJarvis(text: String) { @Composable fun MessageFromUser(text: String) { + //We create a row to contain the user message and we align the row to the right side (to look like a conversation between two people) Row( Modifier .padding(bottom = 25.dp) - .fillMaxWidth(), horizontalArrangement = Arrangement.End) { + .fillMaxWidth(), horizontalArrangement = Arrangement.End + ) { + // The message box with the text Box( modifier = Modifier .fillMaxWidth(fraction = 0.8F) @@ -119,23 +141,48 @@ fun MessageFromUser(text: String) { Text( text = text, fontFamily = productSansFont, - color = if (!isSystemInDarkTheme()) Color.White else Color(15, 15, 15, 255)) + color = if (!isSystemInDarkTheme()) Color.White else Color(15, 15, 15, 255) + ) } } - - } + @Preview(showBackground = true) @Composable fun DefaultPreview() { JarvisComposeTheme { - Column(Modifier.padding(top = 40.dp, start = 20.dp, end = 20.dp)) { - Base() - MessageFromJarvis(text = "Salut, je suis Jarvis! \nPose moi une question et je ferais de mon mieux pour te renseigner.") - MessageFromUser(text = "Quel temps fait-il à Paris en ce moment ?") - MessageFromJarvis(text = "A Paris, il fait actuellement 10 degrés et le ciel est nuageux.") + //We create a main box with basic padding to avoid having stuff too close to every side. + Box( + Modifier + .fillMaxHeight() + .fillMaxWidth() + .padding(horizontal = 20.dp) + .padding(top = 30.dp, bottom = 0.dp) + ) { + // This column regroup the base and all the conversations (everything except the footer) + Column { + Base() + + // This column regroup only the conversations and make them scrollable + Column( + modifier = Modifier + .verticalScroll(rememberScrollState()) + .weight(weight = 1f, fill = false) + ) { + // Basic interaction stuff for demo + MessageFromJarvis(text = "Salut, je suis Jarvis! \nPose moi une question et je ferais de mon mieux pour te renseigner.") + MessageFromUser(text = "Quel temps fait-il à Paris en ce moment ?") + MessageFromJarvis(text = "A Paris, il fait actuellement 10 degrés et le ciel est nuageux.") + } + } + + + // Finally we add the footer to the bottom center of the main box + Column(Modifier.align(Alignment.BottomCenter)) { + Footer() + } } } } \ No newline at end of file diff --git a/app/src/main/java/ch/mathieubroillet/jarvis/android/utils/DotsLoaders.kt b/app/src/main/java/ch/mathieubroillet/jarvis/android/utils/DotsLoaders.kt new file mode 100644 index 0000000..8868dfa --- /dev/null +++ b/app/src/main/java/ch/mathieubroillet/jarvis/android/utils/DotsLoaders.kt @@ -0,0 +1,332 @@ +// Thanks to EugeneTheDev : https://gist.github.com/EugeneTheDev/a27664cb7e7899f964348b05883cbccd + +package ch.mathieubroillet.jarvis.android.utils + +import androidx.compose.animation.core.* +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.scale +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +val dotSize = 20.dp +val delayUnit = 150 + +@Composable +fun DotsPulsing() { + + @Composable + fun Dot( + scale: Float + ) = Spacer( + Modifier + .size(dotSize) + .scale(scale) + .background( + color = MaterialTheme.colors.primary, + shape = CircleShape + ) + ) + + val infiniteTransition = rememberInfiniteTransition() + + @Composable + fun animateScaleWithDelay(delay: Int) = infiniteTransition.animateFloat( + initialValue = 0f, + targetValue = 0f, + animationSpec = infiniteRepeatable( + animation = keyframes { + durationMillis = delayUnit * 4 + 0f at delay with LinearEasing + 1f at delay + delayUnit with LinearEasing + 0f at delay + delayUnit * 2 + } + ) + ) + + val scale1 by animateScaleWithDelay(0) + val scale2 by animateScaleWithDelay(delayUnit) + val scale3 by animateScaleWithDelay(delayUnit * 2) + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + val spaceSize = 2.dp + + Dot(scale1) + Spacer(Modifier.width(spaceSize)) + Dot(scale2) + Spacer(Modifier.width(spaceSize)) + Dot(scale3) + } +} + +@Composable +fun DotsElastic() { + val minScale = 0.6f + + @Composable + fun Dot( + scale: Float + ) = Spacer( + Modifier + .size(dotSize) + .scale(scaleX = minScale, scaleY = scale) + .background( + color = MaterialTheme.colors.primary, + shape = CircleShape + ) + ) + + val infiniteTransition = rememberInfiniteTransition() + + @Composable + fun animateScaleWithDelay(delay: Int) = infiniteTransition.animateFloat( + initialValue = minScale, + targetValue = minScale, + animationSpec = infiniteRepeatable( + animation = keyframes { + durationMillis = delayUnit * 4 + minScale at delay with LinearEasing + 1f at delay + delayUnit with LinearEasing + minScale at delay + delayUnit * 2 + } + ) + ) + + val scale1 by animateScaleWithDelay(0) + val scale2 by animateScaleWithDelay(delayUnit) + val scale3 by animateScaleWithDelay(delayUnit * 2) + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + val spaceSize = 2.dp + + Dot(scale1) + Spacer(Modifier.width(spaceSize)) + Dot(scale2) + Spacer(Modifier.width(spaceSize)) + Dot(scale3) + } +} + +@Composable +fun DotsFlashing() { + val minAlpha = 0.1f + + @Composable + fun Dot( + alpha: Float + ) = Spacer( + Modifier + .size(dotSize) + .alpha(alpha) + .background( + color = MaterialTheme.colors.primary, + shape = CircleShape + ) + ) + + val infiniteTransition = rememberInfiniteTransition() + + @Composable + fun animateAlphaWithDelay(delay: Int) = infiniteTransition.animateFloat( + initialValue = minAlpha, + targetValue = minAlpha, + animationSpec = infiniteRepeatable( + animation = keyframes { + durationMillis = delayUnit * 4 + minAlpha at delay with LinearEasing + 1f at delay + delayUnit with LinearEasing + minAlpha at delay + delayUnit * 2 + } + ) + ) + + val alpha1 by animateAlphaWithDelay(0) + val alpha2 by animateAlphaWithDelay(delayUnit) + val alpha3 by animateAlphaWithDelay(delayUnit * 2) + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + val spaceSize = 2.dp + + Dot(alpha1) + Spacer(Modifier.width(spaceSize)) + Dot(alpha2) + Spacer(Modifier.width(spaceSize)) + Dot(alpha3) + } +} + +@Composable +fun DotsTyping() { + val maxOffset = 10f + + @Composable + fun Dot( + offset: Float + ) = Spacer( + Modifier + .size(dotSize) + .offset(y = -offset.dp) + .background( + color = MaterialTheme.colors.primary, + shape = CircleShape + ) + ) + + val infiniteTransition = rememberInfiniteTransition() + + @Composable + fun animateOffsetWithDelay(delay: Int) = infiniteTransition.animateFloat( + initialValue = 0f, + targetValue = 0f, + animationSpec = infiniteRepeatable( + animation = keyframes { + durationMillis = delayUnit * 4 + 0f at delay with LinearEasing + maxOffset at delay + delayUnit with LinearEasing + 0f at delay + delayUnit * 2 + } + ) + ) + + val offset1 by animateOffsetWithDelay(0) + val offset2 by animateOffsetWithDelay(delayUnit) + val offset3 by animateOffsetWithDelay(delayUnit * 2) + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + modifier = Modifier.padding(top = maxOffset.dp) + ) { + val spaceSize = 2.dp + + Dot(offset1) + Spacer(Modifier.width(spaceSize)) + Dot(offset2) + Spacer(Modifier.width(spaceSize)) + Dot(offset3) + } +} + +@Composable +fun DotsCollision() { + val maxOffset = 30f + val delayUnit = 500 // it's better to use longer delay for this animation + + @Composable + fun Dot( + offset: Float + ) = Spacer( + Modifier + .size(dotSize) + .offset(x = offset.dp) + .background( + color = MaterialTheme.colors.primary, + shape = CircleShape + ) + ) + + val infiniteTransition = rememberInfiniteTransition() + + val offsetLeft by infiniteTransition.animateFloat( + initialValue = 0f, + targetValue = 0f, + animationSpec = infiniteRepeatable( + animation = keyframes { + durationMillis = delayUnit * 3 + 0f at 0 with LinearEasing + -maxOffset at delayUnit / 2 with LinearEasing + 0f at delayUnit + } + ) + ) + val offsetRight by infiniteTransition.animateFloat( + initialValue = 0f, + targetValue = 0f, + animationSpec = infiniteRepeatable( + animation = keyframes { + durationMillis = delayUnit * 3 + 0f at delayUnit with LinearEasing + maxOffset at delayUnit + delayUnit / 2 with LinearEasing + 0f at delayUnit * 2 + } + ) + ) + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + modifier = Modifier.padding(horizontal = maxOffset.dp) + ) { + val spaceSize = 2.dp + + Dot(offsetLeft) + Spacer(Modifier.width(spaceSize)) + Dot(0f) + Spacer(Modifier.width(spaceSize)) + Dot(offsetRight) + } +} + + +@Preview(showBackground = true) +@Composable +fun DotsPreview() = MaterialTheme { + Column(modifier = Modifier.padding(4.dp)) { + val spaceSize = 16.dp + + Text( + text = "Dots pulsing", + style = MaterialTheme.typography.h5 + ) + DotsPulsing() + + Spacer(Modifier.height(spaceSize)) + + Text( + text = "Dots elastic", + style = MaterialTheme.typography.h5 + ) + DotsElastic() + + Spacer(Modifier.height(spaceSize)) + + Text( + text = "Dots flashing", + style = MaterialTheme.typography.h5 + ) + DotsFlashing() + + Spacer(Modifier.height(spaceSize)) + + Text( + text = "Dots typing", + style = MaterialTheme.typography.h5 + ) + DotsTyping() + + Spacer(Modifier.height(spaceSize)) + + Text( + text = "Dots collision", + style = MaterialTheme.typography.h5 + ) + DotsCollision() + } +} diff --git a/app/src/main/res/drawable/ic_baseline_person_24.xml b/app/src/main/res/drawable/ic_baseline_mic_24.xml similarity index 54% rename from app/src/main/res/drawable/ic_baseline_person_24.xml rename to app/src/main/res/drawable/ic_baseline_mic_24.xml index 6bdced2..791b475 100644 --- a/app/src/main/res/drawable/ic_baseline_person_24.xml +++ b/app/src/main/res/drawable/ic_baseline_mic_24.xml @@ -6,5 +6,5 @@ android:tint="?attr/colorControlNormal"> + android:pathData="M12,14c1.66,0 2.99,-1.34 2.99,-3L15,5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5v6c0,1.66 1.34,3 3,3zM17.3,11c0,3 -2.54,5.1 -5.3,5.1S6.7,14 6.7,11L5,11c0,3.41 2.72,6.23 6,6.72L11,21h2v-3.28c3.28,-0.48 6,-3.3 6,-6.72h-1.7z"/>