cleanup the new Compose UI a bit

pull/8/head
geeksville 2020-02-10 10:32:12 -08:00
rodzic 6fed223a35
commit 4a99d0d3ec
6 zmienionych plików z 384 dodań i 291 usunięć

Wyświetl plik

@ -6,6 +6,7 @@ MVP features required for first public alpha
* parcels are busted - something wrong with the Parcelize kotlin magic
* all chat in the app defaults to group chat
* make my android app show mesh state
* add app icon
* when notified phone should automatically download messages
* at connect we might receive messages before finished downloading the nodeinfo. In that case, process those messages later
* use https://codelabs.developers.google.com/codelabs/jetpack-compose-basics/#4 to show service state
@ -14,6 +15,7 @@ MVP features required for first public alpha
* call crashlytics from exceptionReporter!!! currently not logging failures caught there
* show direction and distance on the nodeinfo cards
* test with oldest compatible android in emulator (see below for testing with hardware)
* make playstore entry
# Signal alpha release
Do this "Signal app compatible" release relatively soon after the alpha release of the android app.

Wyświetl plik

@ -12,92 +12,27 @@ import android.os.IBinder
import android.view.Menu
import android.view.MenuItem
import android.widget.Toast
import androidx.annotation.DrawableRes
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.Composable
import androidx.compose.Model
import androidx.compose.mutableStateOf
import androidx.compose.state
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.ui.animation.Crossfade
import androidx.ui.core.Modifier
import androidx.ui.core.Text
import androidx.ui.core.WithDensity
import androidx.ui.core.setContent
import androidx.ui.foundation.Clickable
import androidx.ui.foundation.VerticalScroller
import androidx.ui.foundation.shape.corner.RoundedCornerShape
import androidx.ui.graphics.Color
import androidx.ui.graphics.vector.DrawVector
import androidx.ui.layout.*
import androidx.ui.material.*
import androidx.ui.material.ripple.Ripple
import androidx.ui.material.surface.Surface
import androidx.ui.res.vectorResource
import androidx.ui.tooling.preview.Preview
import androidx.ui.unit.dp
import com.geeksville.android.Logging
import com.geeksville.mesh.ui.MeshApp
import com.geeksville.mesh.ui.TextMessage
import com.geeksville.mesh.ui.UIState
import com.geeksville.util.exceptionReporter
import com.google.firebase.crashlytics.FirebaseCrashlytics
import java.nio.charset.Charset
import java.util.*
// defines the screens we have in the app
sealed class Screen {
object Home : Screen()
object Settings : Screen()
}
@Model
object AppStatus {
var currentScreen: Screen = Screen.Home
}
/**
* Temporary solution pending navigation support.
*/
fun navigateTo(destination: Screen) {
AppStatus.currentScreen = destination
}
class MainActivity : AppCompatActivity(), Logging {
companion object {
const val REQUEST_ENABLE_BT = 10
const val DID_REQUEST_PERM = 11
private val testPositions = arrayOf(
Position(32.776665, -96.796989, 35), // dallas
Position(32.960758, -96.733521, 35), // richardson
Position(32.912901, -96.781776, 35) // north dallas
)
private val testNodes = testPositions.mapIndexed { index, it ->
NodeInfo(
9 + index,
MeshUser("+65087653%02d".format(9 + index), "Kevin Mester$index", "KM$index"),
it,
12345
)
}
data class TextMessage(val date: Date, val from: String, val text: String)
private val testTexts = listOf(
TextMessage(Date(), "+6508675310", "I found the cache"),
TextMessage(Date(), "+6508675311", "Help! I've fallen and I can't get up.")
)
}
/// A map from nodeid to to nodeinfo
private val nodes = mutableStateOf(testNodes.map { it.user!!.id to it }.toMap())
private val messages = mutableStateOf(testTexts)
/// Are we connected to our radio device
private var isConnected = mutableStateOf(false)
private val utf8 = Charset.forName("UTF-8")
@ -169,220 +104,6 @@ class MainActivity : AppCompatActivity(), Logging {
}
}
@Composable
fun composeNodeInfo(it: NodeInfo) {
Text("Node: ${it.user?.longName}")
}
@Composable
fun VectorImageButton(@DrawableRes id: Int, onClick: () -> Unit) {
Ripple(bounded = false) {
Clickable(onClick = onClick) {
VectorImage(id = id)
}
}
}
@Composable
fun VectorImage(
modifier: Modifier = Modifier.None, @DrawableRes id: Int,
tint: Color = Color.Transparent
) {
val vector = vectorResource(id)
WithDensity {
Container(
modifier = modifier + LayoutSize(
vector.defaultWidth,
vector.defaultHeight
)
) {
DrawVector(vector, tint)
}
}
}
@Composable
fun HomeContent() {
Column {
Text(text = "Meshtastic")
Text("Radio connected: ${isConnected.value}")
nodes.value.values.forEach {
composeNodeInfo(it)
}
messages.value.forEach {
Text("Text: ${it.text}")
}
Button(text = "Start scan",
onClick = {
if (bluetoothAdapter != null) {
// Note: We don't want this service to die just because our activity goes away (because it is doing a software update)
// So we use the application context instead of the activity
SoftwareUpdateService.enqueueWork(
applicationContext,
SoftwareUpdateService.startUpdateIntent
)
}
})
Button(text = "send packets",
onClick = { sendTestPackets() })
}
}
@Composable
fun HomeScreen(openDrawer: () -> Unit) {
Column {
TopAppBar(
title = { Text(text = "Meshtastic") },
navigationIcon = {
VectorImageButton(R.drawable.ic_launcher_foreground) {
openDrawer()
}
}
)
VerticalScroller(modifier = LayoutFlexible(1f)) {
HomeContent()
}
}
}
@Composable
fun composeView() {
val (drawerState, onDrawerStateChange) = state { DrawerState.Closed }
MaterialTheme {
ModalDrawerLayout(
drawerState = drawerState,
onStateChange = onDrawerStateChange,
gesturesEnabled = drawerState == DrawerState.Opened,
drawerContent = {
AppDrawer(
currentScreen = AppStatus.currentScreen,
closeDrawer = { onDrawerStateChange(DrawerState.Closed) }
)
/*
// modifier = Spacing(8.dp)
Column() {
*/
}, bodyContent = { AppContent { onDrawerStateChange(DrawerState.Opened) } })
}
}
@Preview
@Composable
fun previewView() {
// It seems modaldrawerlayout not yet supported in preview
HomeContent()
}
@Composable
private fun AppContent(openDrawer: () -> Unit) {
Crossfade(AppStatus.currentScreen) { screen ->
Surface(color = (MaterialTheme.colors()).background) {
when (screen) {
is Screen.Home -> HomeScreen { openDrawer() }
/* is Screen.Interests -> InterestsScreen { openDrawer() }
is Screen.Article -> ArticleScreen(postId = screen.postId) */
}
}
}
}
@Composable
private fun AppDrawer(
currentScreen: Screen,
closeDrawer: () -> Unit
) {
Column(modifier = LayoutSize.Fill) {
Spacer(LayoutHeight(24.dp))
Row(modifier = LayoutPadding(16.dp)) {
VectorImage(
id = R.drawable.ic_launcher_foreground,
tint = (MaterialTheme.colors()).primary
)
Spacer(LayoutWidth(8.dp))
VectorImage(id = R.drawable.ic_launcher_foreground)
}
Divider(color = Color(0x14333333))
DrawerButton(
icon = R.drawable.ic_launcher_foreground,
label = "Home",
isSelected = currentScreen == Screen.Home
) {
navigateTo(Screen.Home)
closeDrawer()
}
/*
DrawerButton(
icon = R.drawable.ic_interests,
label = "Interests",
isSelected = currentScreen == Screen.Interests
) {
navigateTo(Screen.Interests)
closeDrawer()
}
*/
}
}
@Composable
private fun DrawerButton(
modifier: Modifier = Modifier.None,
@DrawableRes icon: Int,
label: String,
isSelected: Boolean,
action: () -> Unit
) {
val colors = MaterialTheme.colors()
val textIconColor = if (isSelected) {
colors.primary
} else {
colors.onSurface.copy(alpha = 0.6f)
}
val backgroundColor = if (isSelected) {
colors.primary.copy(alpha = 0.12f)
} else {
colors.surface
}
Surface(
modifier = modifier + LayoutPadding(
left = 8.dp,
top = 8.dp,
right = 8.dp,
bottom = 0.dp
),
color = backgroundColor,
shape = RoundedCornerShape(4.dp)
) {
Button(onClick = action, style = TextButtonStyle()) {
Row {
VectorImage(
modifier = LayoutGravity.Center,
id = icon,
tint = textIconColor
)
Spacer(LayoutWidth(16.dp))
Text(
text = label,
style = (MaterialTheme.typography()).body2.copy(
color = textIconColor
)
)
}
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -393,7 +114,7 @@ class MainActivity : AppCompatActivity(), Logging {
FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(true)
setContent {
composeView()
MeshApp()
}
// Ensures Bluetooth is available on the device and it is enabled. If not,
@ -432,9 +153,9 @@ class MainActivity : AppCompatActivity(), Logging {
// We only care about nodes that have user info
info.user?.id?.let {
val newnodes = nodes.value.toMutableMap()
val newnodes = UIState.nodes.value.toMutableMap()
newnodes[it] = info
nodes.value = newnodes
UIState.nodes.value = newnodes
}
}
@ -447,16 +168,16 @@ class MainActivity : AppCompatActivity(), Logging {
when (typ) {
MeshProtos.Data.Type.CLEAR_TEXT_VALUE -> {
// FIXME - use the real time from the packet
val modded = messages.value.toMutableList()
val modded = UIState.messages.value.toMutableList()
modded.add(TextMessage(Date(), sender, payload.toString(utf8)))
messages.value = modded
UIState.messages.value = modded
}
else -> TODO()
}
}
RadioInterfaceService.CONNECTCHANGED_ACTION -> {
isConnected.value = intent.getBooleanExtra(EXTRA_CONNECTED, false)
debug("connchange $isConnected")
UIState.isConnected.value = intent.getBooleanExtra(EXTRA_CONNECTED, false)
debug("connchange ${UIState.isConnected.value}")
}
else -> TODO()
}
@ -475,10 +196,10 @@ class MainActivity : AppCompatActivity(), Logging {
// FIXME - do actions for when we connect to the service
debug("did connect")
isConnected.value = m.isConnected
UIState.isConnected.value = m.isConnected
// make some placeholder nodeinfos
nodes.value =
UIState.nodes.value =
m.online.toList().map { it to NodeInfo(0, MeshUser(it, "unknown", "unk")) }.toMap()
}

Wyświetl plik

@ -0,0 +1,201 @@
package com.geeksville.mesh.ui
import androidx.annotation.DrawableRes
import androidx.compose.Composable
import androidx.compose.state
import androidx.ui.animation.Crossfade
import androidx.ui.core.Modifier
import androidx.ui.core.Text
import androidx.ui.foundation.VerticalScroller
import androidx.ui.foundation.shape.corner.RoundedCornerShape
import androidx.ui.graphics.Color
import androidx.ui.layout.*
import androidx.ui.material.*
import androidx.ui.material.surface.Surface
import androidx.ui.tooling.preview.Preview
import androidx.ui.unit.dp
import com.geeksville.mesh.R
@Composable
fun HomeContent() {
Column {
Text(text = "Meshtastic")
Text("Radio connected: ${UIState.isConnected.value}")
UIState.nodes.value.values.forEach {
NodeInfoCard(it)
}
UIState.messages.value.forEach {
Text("Text: ${it.text}")
}
/*
Button(text = "Start scan",
onClick = {
if (bluetoothAdapter != null) {
// Note: We don't want this service to die just because our activity goes away (because it is doing a software update)
// So we use the application context instead of the activity
SoftwareUpdateService.enqueueWork(
applicationContext,
SoftwareUpdateService.startUpdateIntent
)
}
})
Button(text = "send packets",
onClick = { sendTestPackets() }) */
}
}
@Composable
fun HomeScreen(openDrawer: () -> Unit) {
Column {
TopAppBar(
title = { Text(text = "Meshtastic") },
navigationIcon = {
VectorImageButton(R.drawable.ic_launcher_foreground) {
openDrawer()
}
}
)
VerticalScroller(modifier = LayoutFlexible(1f)) {
HomeContent()
}
}
}
@Composable
fun MeshApp() {
val (drawerState, onDrawerStateChange) = state { DrawerState.Closed }
MaterialTheme {
ModalDrawerLayout(
drawerState = drawerState,
onStateChange = onDrawerStateChange,
gesturesEnabled = drawerState == DrawerState.Opened,
drawerContent = {
AppDrawer(
currentScreen = AppStatus.currentScreen,
closeDrawer = { onDrawerStateChange(DrawerState.Closed) }
)
/*
// modifier = Spacing(8.dp)
Column() {
*/
}, bodyContent = { AppContent { onDrawerStateChange(DrawerState.Opened) } })
}
}
@Preview
@Composable
fun previewView() {
// It seems modaldrawerlayout not yet supported in preview
HomeContent()
}
@Composable
private fun AppContent(openDrawer: () -> Unit) {
Crossfade(AppStatus.currentScreen) { screen ->
Surface(color = (MaterialTheme.colors()).background) {
when (screen) {
is Screen.Home -> HomeScreen { openDrawer() }
/* is Screen.Interests -> InterestsScreen { openDrawer() }
is Screen.Article -> ArticleScreen(postId = screen.postId) */
}
}
}
}
@Composable
private fun AppDrawer(
currentScreen: Screen,
closeDrawer: () -> Unit
) {
Column(modifier = LayoutSize.Fill) {
Spacer(LayoutHeight(24.dp))
Row(modifier = LayoutPadding(16.dp)) {
VectorImage(
id = R.drawable.ic_launcher_foreground,
tint = (MaterialTheme.colors()).primary
)
Spacer(LayoutWidth(8.dp))
VectorImage(id = R.drawable.ic_launcher_foreground)
}
Divider(color = Color(0x14333333))
DrawerButton(
icon = R.drawable.ic_launcher_foreground,
label = "Home",
isSelected = currentScreen == Screen.Home
) {
navigateTo(Screen.Home)
closeDrawer()
}
/*
DrawerButton(
icon = R.drawable.ic_interests,
label = "Interests",
isSelected = currentScreen == Screen.Interests
) {
navigateTo(Screen.Interests)
closeDrawer()
}
*/
}
}
@Composable
private fun DrawerButton(
modifier: Modifier = Modifier.None,
@DrawableRes icon: Int,
label: String,
isSelected: Boolean,
action: () -> Unit
) {
val colors = MaterialTheme.colors()
val textIconColor = if (isSelected) {
colors.primary
} else {
colors.onSurface.copy(alpha = 0.6f)
}
val backgroundColor = if (isSelected) {
colors.primary.copy(alpha = 0.12f)
} else {
colors.surface
}
Surface(
modifier = modifier + LayoutPadding(
left = 8.dp,
top = 8.dp,
right = 8.dp,
bottom = 0.dp
),
color = backgroundColor,
shape = RoundedCornerShape(4.dp)
) {
Button(onClick = action, style = TextButtonStyle()) {
Row {
VectorImage(
modifier = LayoutGravity.Center,
id = icon,
tint = textIconColor
)
Spacer(LayoutWidth(16.dp))
Text(
text = label,
style = (MaterialTheme.typography()).body2.copy(
color = textIconColor
)
)
}
}
}
}

Wyświetl plik

@ -0,0 +1,67 @@
package com.geeksville.mesh.ui
import androidx.compose.Composable
import androidx.ui.core.Modifier
import androidx.ui.core.Text
import androidx.ui.foundation.DrawImage
import androidx.ui.layout.Container
import androidx.ui.layout.LayoutPadding
import androidx.ui.layout.LayoutSize
import androidx.ui.layout.Row
import androidx.ui.material.EmphasisLevels
import androidx.ui.material.MaterialTheme
import androidx.ui.material.ProvideEmphasis
import androidx.ui.res.imageResource
import androidx.ui.tooling.preview.Preview
import androidx.ui.unit.dp
import com.geeksville.mesh.NodeInfo
import com.geeksville.mesh.R
@Composable
fun NodeIcon(modifier: Modifier = Modifier.None, node: NodeInfo) {
val image = imageResource(R.drawable.ic_launcher_foreground)
Container(modifier = modifier + LayoutSize(40.dp, 40.dp)) {
DrawImage(image)
}
}
@Composable
fun NodeHeading(node: NodeInfo) {
ProvideEmphasis(emphasis = EmphasisLevels().high) {
Text(node.user?.longName ?: "unknown", style = MaterialTheme.typography().subtitle1)
}
}
/**
* An info card for a node:
*
* on left, the icon for the user (or shortname if that is all we have)
*
* Middle is users fullname
*
* on right a compass rose with a pointer to the user and distance
*
*/
@Composable
fun NodeInfoCard(node: NodeInfo) {
// Text("Node: ${it.user?.longName}")
Row(modifier = LayoutPadding(16.dp)) {
NodeIcon(
modifier = LayoutPadding(left = 0.dp, top = 0.dp, right = 16.dp, bottom = 0.dp),
node = node
)
NodeHeading(node)
// FIXME - show compass instead
NodeIcon(node = node)
}
}
@Preview
@Composable
fun nodeInfoPreview() {
NodeInfoCard(UIState.testNodes[0])
}

Wyświetl plik

@ -0,0 +1,61 @@
package com.geeksville.mesh.ui
import androidx.compose.Model
import androidx.compose.mutableStateOf
import com.geeksville.mesh.MeshUser
import com.geeksville.mesh.NodeInfo
import com.geeksville.mesh.Position
import java.util.*
// defines the screens we have in the app
sealed class Screen {
object Home : Screen()
// object Settings : Screen()
}
@Model
object AppStatus {
var currentScreen: Screen = Screen.Home
}
data class TextMessage(val date: Date, val from: String, val text: String)
/// FIXME - figure out how to merge this staate with the AppStatus Model
object UIState {
private val testPositions = arrayOf(
Position(32.776665, -96.796989, 35), // dallas
Position(32.960758, -96.733521, 35), // richardson
Position(32.912901, -96.781776, 35) // north dallas
)
val testNodes = testPositions.mapIndexed { index, it ->
NodeInfo(
9 + index,
MeshUser("+65087653%02d".format(9 + index), "Kevin Mester$index", "KM$index"),
it,
12345
)
}
val testTexts = listOf(
TextMessage(Date(), "+6508675310", "I found the cache"),
TextMessage(Date(), "+6508675311", "Help! I've fallen and I can't get up.")
)
/// A map from nodeid to to nodeinfo
val nodes = mutableStateOf(testNodes.map { it.user!!.id to it }.toMap())
val messages = mutableStateOf(testTexts)
/// Are we connected to our radio device
var isConnected = mutableStateOf(false)
}
/**
* Temporary solution pending navigation support.
*/
fun navigateTo(destination: Screen) {
AppStatus.currentScreen = destination
}

Wyświetl plik

@ -0,0 +1,41 @@
package com.geeksville.mesh.ui
import androidx.annotation.DrawableRes
import androidx.compose.Composable
import androidx.ui.core.Modifier
import androidx.ui.core.WithDensity
import androidx.ui.foundation.Clickable
import androidx.ui.graphics.Color
import androidx.ui.graphics.vector.DrawVector
import androidx.ui.layout.Container
import androidx.ui.layout.LayoutSize
import androidx.ui.material.ripple.Ripple
import androidx.ui.res.vectorResource
@Composable
fun VectorImageButton(@DrawableRes id: Int, onClick: () -> Unit) {
Ripple(bounded = false) {
Clickable(onClick = onClick) {
VectorImage(id = id)
}
}
}
@Composable
fun VectorImage(
modifier: Modifier = Modifier.None, @DrawableRes id: Int,
tint: Color = Color.Transparent
) {
val vector = vectorResource(id)
WithDensity {
Container(
modifier = modifier + LayoutSize(
vector.defaultWidth,
vector.defaultHeight
)
) {
DrawVector(vector, tint)
}
}
}