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"/>