From 39e8a265134817692d96becb112014f13cdd4cf5 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 29 Jul 2026 14:26:25 -0700 Subject: [PATCH] Update Material3 implementation to use TransformingLazyColumn and Screen/AppScaffolds. # Conflicts: # ChatApp/wear/src/main/kotlin/com/example/chatapp/wear/uicomponents/WearChatScreen.kt --- ChatApp/gradle/libs.versions.toml | 2 + ChatApp/wear/build.gradle.kts | 1 + .../com/example/chatapp/wear/MainActivity.kt | 43 +++++----- .../uicomponents/WearAppFunctionsScreen.kt | 81 ++++++++++-------- .../wear/uicomponents/WearCallScreen.kt | 35 ++++---- .../wear/uicomponents/WearChatScreen.kt | 79 +++++++++++------- .../wear/uicomponents/WearRecipientsScreen.kt | 82 ++++++++++++------- 7 files changed, 197 insertions(+), 126 deletions(-) diff --git a/ChatApp/gradle/libs.versions.toml b/ChatApp/gradle/libs.versions.toml index 82d99a5..bbaa77b 100644 --- a/ChatApp/gradle/libs.versions.toml +++ b/ChatApp/gradle/libs.versions.toml @@ -23,6 +23,7 @@ navigation3 = "1.1.4" kotlinxSerialization = "1.11.0" #noinspection UnusedVersionCatalogEntry - Accessed dynamically ktlint = "1.2.1" +composeNavigation3 = "1.6.2" [libraries] androidx-appfunctions = { module = "androidx.appfunctions:appfunctions", version.ref = "appfunctions" } @@ -58,6 +59,7 @@ androidx-navigation3-runtime = { group = "androidx.navigation3", name = "navigat androidx-navigation3-ui = { group = "androidx.navigation3", name = "navigation3-ui", version.ref = "navigation3" } androidx-lifecycle-viewmodel-navigation3 = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-navigation3", version.ref = "lifecycleRuntimeKtx" } kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerialization" } +androidx-compose-navigation3 = { group = "androidx.wear.compose", name = "compose-navigation3", version.ref = "composeNavigation3" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } diff --git a/ChatApp/wear/build.gradle.kts b/ChatApp/wear/build.gradle.kts index 4ff1d88..abc726d 100644 --- a/ChatApp/wear/build.gradle.kts +++ b/ChatApp/wear/build.gradle.kts @@ -60,6 +60,7 @@ android { dependencies { implementation(project(":shared")) + implementation(libs.androidx.compose.navigation3) implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.activity.compose) diff --git a/ChatApp/wear/src/main/kotlin/com/example/chatapp/wear/MainActivity.kt b/ChatApp/wear/src/main/kotlin/com/example/chatapp/wear/MainActivity.kt index 27ac848..418a88c 100644 --- a/ChatApp/wear/src/main/kotlin/com/example/chatapp/wear/MainActivity.kt +++ b/ChatApp/wear/src/main/kotlin/com/example/chatapp/wear/MainActivity.kt @@ -19,14 +19,15 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.remember import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.rememberNavBackStack import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator import androidx.navigation3.ui.NavDisplay +import androidx.wear.compose.material3.AppScaffold import androidx.wear.compose.material3.MaterialTheme +import androidx.wear.compose.navigation3.rememberSwipeDismissableSceneStrategy import com.example.chatapp.wear.uicomponents.WearAppFunctionsScreen import com.example.chatapp.wear.uicomponents.WearCallScreen import com.example.chatapp.wear.uicomponents.WearChatScreen @@ -57,23 +58,25 @@ class MainActivity : ComponentActivity() { @Composable fun WearApp() { - val backStack = remember { mutableStateListOf(Screen.Recipients) } + val backStack = rememberNavBackStack(Screen.Recipients) MaterialTheme { - NavDisplay( - backStack = backStack, - onBack = { - if (backStack.size > 1) { - backStack.removeAt(backStack.size - 1) - } - }, - entryDecorators = - listOf( - rememberSaveableStateHolderNavEntryDecorator(), - rememberViewModelStoreNavEntryDecorator(), - ), - entryProvider = - entryProvider { + AppScaffold { + val swipeDismissableSceneStrategy = rememberSwipeDismissableSceneStrategy() + + NavDisplay( + backStack = backStack, + onBack = { + if (backStack.size > 1) { + backStack.removeAt(backStack.size - 1) + } + }, + entryDecorators = + listOf( + rememberSaveableStateHolderNavEntryDecorator(), + rememberViewModelStoreNavEntryDecorator(), + ), + entryProvider = entryProvider { entry { WearRecipientsScreen( onRecipientClick = { id -> backStack.add(Screen.Chat(id)) }, @@ -96,6 +99,8 @@ fun WearApp() { ) } }, - ) + sceneStrategies = listOf(swipeDismissableSceneStrategy), + ) + } } -} +} \ No newline at end of file diff --git a/ChatApp/wear/src/main/kotlin/com/example/chatapp/wear/uicomponents/WearAppFunctionsScreen.kt b/ChatApp/wear/src/main/kotlin/com/example/chatapp/wear/uicomponents/WearAppFunctionsScreen.kt index 8132906..5044f17 100644 --- a/ChatApp/wear/src/main/kotlin/com/example/chatapp/wear/uicomponents/WearAppFunctionsScreen.kt +++ b/ChatApp/wear/src/main/kotlin/com/example/chatapp/wear/uicomponents/WearAppFunctionsScreen.kt @@ -15,58 +15,73 @@ */ package com.example.chatapp.wear.uicomponents -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.wear.compose.foundation.lazy.ScalingLazyColumn +import androidx.wear.compose.foundation.lazy.TransformingLazyColumn import androidx.wear.compose.foundation.lazy.items +import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState import androidx.wear.compose.material3.ListHeader import androidx.wear.compose.material3.MaterialTheme +import androidx.wear.compose.material3.ScreenScaffold +import androidx.wear.compose.material3.SurfaceTransformation import androidx.wear.compose.material3.SwitchButton import androidx.wear.compose.material3.Text +import androidx.wear.compose.material3.lazy.rememberTransformationSpec +import androidx.wear.compose.material3.lazy.transformedHeight import com.example.chatapp.AppFunctionsViewModel @Composable fun WearAppFunctionsScreen(viewModel: AppFunctionsViewModel = hiltViewModel()) { + val transformationSpec = rememberTransformationSpec() + val scrollState = rememberTransformingLazyColumnState() val uiState by viewModel.uiState.collectAsStateWithLifecycle() - ScalingLazyColumn( - modifier = Modifier.fillMaxSize(), - ) { - item { - ListHeader { - Text("App Functions") - } - } - - if (uiState.functions.isEmpty()) { + ScreenScaffold( + scrollState = scrollState, + ) { contentPadding -> + TransformingLazyColumn( + state = scrollState, + contentPadding = contentPadding, + ) { item { - Text( - text = "No functions found.", - style = MaterialTheme.typography.bodyMedium, - ) + ListHeader ( + modifier = Modifier.fillMaxWidth().transformedHeight(this, transformationSpec), + transformation = SurfaceTransformation(transformationSpec) + ){ + Text("App Functions") + } } - } else { - items(uiState.functions) { function -> - SwitchButton( - modifier = Modifier.fillMaxWidth(), - checked = function.isEnabled, - onCheckedChange = { enabled -> - viewModel.toggleFunction(function.id, enabled) - }, - label = { - val shortName = function.id.substringAfterLast("#").substringAfterLast(".") - Text(text = shortName) - }, - secondaryLabel = { - Text(text = function.description ?: function.id) - }, - ) + + if (uiState.functions.isEmpty()) { + item { + Text( + text = "No functions found.", + style = MaterialTheme.typography.bodyMedium, + ) + } + } else { + items(uiState.functions) { function -> + SwitchButton( + transformation = SurfaceTransformation(transformationSpec), + modifier = Modifier.fillMaxWidth().transformedHeight(this, transformationSpec), + checked = function.isEnabled, + onCheckedChange = { enabled -> + viewModel.toggleFunction(function.id, enabled) + }, + label = { + val shortName = function.id.substringAfterLast("#").substringAfterLast(".") + Text(text = shortName) + }, + secondaryLabel = { + Text(text = function.description ?: function.id) + }, + ) + } } } } -} +} \ No newline at end of file diff --git a/ChatApp/wear/src/main/kotlin/com/example/chatapp/wear/uicomponents/WearCallScreen.kt b/ChatApp/wear/src/main/kotlin/com/example/chatapp/wear/uicomponents/WearCallScreen.kt index c2f1733..b661a64 100644 --- a/ChatApp/wear/src/main/kotlin/com/example/chatapp/wear/uicomponents/WearCallScreen.kt +++ b/ChatApp/wear/src/main/kotlin/com/example/chatapp/wear/uicomponents/WearCallScreen.kt @@ -31,6 +31,7 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.wear.compose.material3.Button import androidx.wear.compose.material3.ButtonDefaults +import androidx.wear.compose.material3.ScreenScaffold import androidx.wear.compose.material3.Text import com.example.chatapp.ChatViewModel @@ -52,23 +53,25 @@ fun WearCallScreen( viewModel.startCall() } - Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - Text(text = "Calling...") - Spacer(modifier = Modifier.height(8.dp)) - Text(text = activeCall?.name ?: viewModel.recipient.name) - Spacer(modifier = Modifier.height(16.dp)) - Button( - onClick = { - viewModel.endCall() - onEndCall() - }, - colors = ButtonDefaults.buttonColors(containerColor = Color.Red), + ScreenScaffold() { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, ) { - Text("End") + Text(text = "Calling...") + Spacer(modifier = Modifier.height(8.dp)) + Text(text = activeCall?.name ?: viewModel.recipient.name) + Spacer(modifier = Modifier.height(16.dp)) + Button( + onClick = { + viewModel.endCall() + onEndCall() + }, + colors = ButtonDefaults.buttonColors(containerColor = Color.Red), + ) { + Text("End") + } } } } diff --git a/ChatApp/wear/src/main/kotlin/com/example/chatapp/wear/uicomponents/WearChatScreen.kt b/ChatApp/wear/src/main/kotlin/com/example/chatapp/wear/uicomponents/WearChatScreen.kt index 5b21082..c5305fc 100644 --- a/ChatApp/wear/src/main/kotlin/com/example/chatapp/wear/uicomponents/WearChatScreen.kt +++ b/ChatApp/wear/src/main/kotlin/com/example/chatapp/wear/uicomponents/WearChatScreen.kt @@ -15,20 +15,25 @@ */ package com.example.chatapp.wear.uicomponents -import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.wear.compose.foundation.lazy.ScalingLazyColumn +import androidx.wear.compose.foundation.lazy.TransformingLazyColumn import androidx.wear.compose.foundation.lazy.items +import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState import androidx.wear.compose.material3.Button import androidx.wear.compose.material3.ListHeader +import androidx.wear.compose.material3.ScreenScaffold +import androidx.wear.compose.material3.SurfaceTransformation import androidx.wear.compose.material3.MaterialTheme import androidx.wear.compose.material3.Text import androidx.wear.compose.material3.TitleCard +import androidx.wear.compose.material3.lazy.rememberTransformationSpec +import androidx.wear.compose.material3.lazy.transformedHeight import com.example.chatapp.ChatViewModel import com.example.chatapp.util.linkifyString @@ -44,38 +49,56 @@ fun WearChatScreen( ), onCallClick: () -> Unit, ) { + val transformationSpec = rememberTransformationSpec() + val scrollState = rememberTransformingLazyColumnState() val uiState by viewModel.uiState.collectAsStateWithLifecycle() val recipient = viewModel.recipient - ScalingLazyColumn( - modifier = Modifier.fillMaxSize(), - ) { - item { - Button( - onClick = onCallClick, - ) { - Text(text = "📞") + ScreenScaffold( + scrollState = scrollState, + ) { contentPadding -> + TransformingLazyColumn( + state = scrollState, + contentPadding = contentPadding, + ) { + item { + Button( + onClick = onCallClick, + modifier = Modifier.transformedHeight(this, transformationSpec), + transformation = SurfaceTransformation(transformationSpec) + ) { + Text(text = "📞") + } } - } - - item { - ListHeader { - Text(text = recipient.name) + item { + ListHeader( + modifier = + Modifier + .fillMaxWidth() + .transformedHeight(this, transformationSpec), + transformation = SurfaceTransformation(transformationSpec) + ) { Text(text = recipient.name) } } - } - - items(uiState.messages.reversed()) { message -> - TitleCard( - onClick = { }, - title = { Text(text = if (message.isInbound) message.senderName ?: "Sender" else "Me") }, - ) { - val linkColor = MaterialTheme.colorScheme.primary - val annotatedText = - remember(message.content, linkColor) { - linkifyString(text = message.content, linkColor = linkColor) - } - Text(text = annotatedText) + items(uiState.messages.reversed()) { message -> + TitleCard( + onClick = { }, + title = { + Text( + text = if (message.isInbound) message.senderName ?: "Sender" else "Me" + ) + }, + modifier = Modifier.transformedHeight(this, transformationSpec), + transformation = SurfaceTransformation(transformationSpec) + ) { + val linkColor = MaterialTheme.colorScheme.primary + val annotatedText = + remember(message.content, linkColor) { + linkifyString(text = message.content, linkColor = linkColor) + } + Text(text = annotatedText) + } } } + } } diff --git a/ChatApp/wear/src/main/kotlin/com/example/chatapp/wear/uicomponents/WearRecipientsScreen.kt b/ChatApp/wear/src/main/kotlin/com/example/chatapp/wear/uicomponents/WearRecipientsScreen.kt index 8120ec7..e3a2dd7 100644 --- a/ChatApp/wear/src/main/kotlin/com/example/chatapp/wear/uicomponents/WearRecipientsScreen.kt +++ b/ChatApp/wear/src/main/kotlin/com/example/chatapp/wear/uicomponents/WearRecipientsScreen.kt @@ -15,16 +15,21 @@ */ package com.example.chatapp.wear.uicomponents -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import androidx.wear.compose.foundation.lazy.ScalingLazyColumn +import androidx.wear.compose.foundation.lazy.TransformingLazyColumn import androidx.wear.compose.foundation.lazy.items +import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState import androidx.wear.compose.material3.Button import androidx.wear.compose.material3.ListHeader +import androidx.wear.compose.material3.ListHeaderDefaults +import androidx.wear.compose.material3.ScreenScaffold +import androidx.wear.compose.material3.SurfaceTransformation import androidx.wear.compose.material3.Text +import androidx.wear.compose.material3.lazy.rememberTransformationSpec +import androidx.wear.compose.material3.lazy.transformedHeight import com.example.chatapp.RecipientsViewModel @Composable @@ -33,41 +38,58 @@ fun WearRecipientsScreen( onRecipientClick: (String) -> Unit, onSettingsClick: () -> Unit, ) { + val transformationSpec = rememberTransformationSpec() + val scrollState = rememberTransformingLazyColumnState() val recipients = viewModel.recipients val groups = viewModel.groups - ScalingLazyColumn( - modifier = Modifier.fillMaxSize(), - ) { - item { - ListHeader { - Text(text = "Chats") + ScreenScaffold( + scrollState = scrollState, + ) { contentPadding -> + TransformingLazyColumn( + state = scrollState, + contentPadding = contentPadding + ) { + item { + ListHeader( + modifier = + Modifier + .fillMaxWidth() + .transformedHeight(this, transformationSpec) + .minimumVerticalContentPadding( + ListHeaderDefaults.minimumTopListContentPadding + ), + transformation = SurfaceTransformation(transformationSpec) + ) { Text(text = "Chats") } } - } - items(groups) { group -> - Button( - modifier = Modifier.fillMaxWidth(), - onClick = { onRecipientClick(group.id) }, - label = { Text(text = group.name) }, - secondaryLabel = { Text(text = "Group") }, - ) - } + items(groups) { group -> + Button( + modifier = Modifier.fillMaxWidth().transformedHeight(this, transformationSpec), + onClick = { onRecipientClick(group.id) }, + label = { Text(text = group.name) }, + secondaryLabel = { Text(text = "Group") }, + transformation = SurfaceTransformation(transformationSpec) + ) + } - items(recipients) { recipient -> - Button( - modifier = Modifier.fillMaxWidth(), - onClick = { onRecipientClick(recipient.id) }, - label = { Text(text = recipient.name) }, - ) - } + items(recipients) { recipient -> + Button( + modifier = Modifier.fillMaxWidth().transformedHeight(this, transformationSpec), + onClick = { onRecipientClick(recipient.id) }, + label = { Text(text = recipient.name) }, + transformation = SurfaceTransformation(transformationSpec) + ) + } - item { - Button( - modifier = Modifier.fillMaxWidth(), - onClick = onSettingsClick, - label = { Text(text = "Settings") }, - ) + item { + Button( + modifier = Modifier.fillMaxWidth().transformedHeight(this, transformationSpec), + transformation = SurfaceTransformation(transformationSpec), + onClick = onSettingsClick, + label = { Text(text = "Settings") }, + ) + } } } }