Added footer, scrollable conversation and global UI improvements

This commit is contained in:
Mathieu 2022-01-02 22:53:05 +01:00
parent 2157a7e26f
commit 75bf1c3389
4 changed files with 397 additions and 16 deletions

View File

@ -11,7 +11,9 @@
<entry key="../../../../../layout/compose-model-1641145605520.xml" value="0.8685185185185185" />
<entry key="../../../../../layout/compose-model-1641154451614.xml" value="0.8768518518518519" />
<entry key="../../../../../layout/compose-model-1641155782430.xml" value="0.7886363636363637" />
<entry key="../../../../../layout/compose-model-1641156150634.xml" value="0.33" />
<entry key="app/src/main/res/drawable-v24/ic_launcher_foreground.xml" value="0.5307291666666667" />
<entry key="app/src/main/res/drawable/ic_baseline_more_vert_24.xml" value="0.38981481481481484" />
<entry key="app/src/main/res/drawable/ic_baseline_person_24.xml" value="0.5307291666666667" />
<entry key="app/src/main/res/drawable/ic_launcher_background.xml" value="0.5307291666666667" />
<entry key="app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml" value="0.5307291666666667" />

View File

@ -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)) {
//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()
}
}
}
}

View File

@ -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()
}
}

View File

@ -6,5 +6,5 @@
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M12,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM12,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z"/>
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"/>
</vector>