|
@@ -0,0 +1,814 @@
|
|
|
|
+/*
|
|
|
|
+ * Copyright 2021 The Android Open Source Project
|
|
|
|
+ *
|
|
|
|
+ * Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
+ * you may not use this file except in compliance with the License.
|
|
|
|
+ * You may obtain a copy of the License at
|
|
|
|
+ *
|
|
|
|
+ * https://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
+ *
|
|
|
|
+ * Unless required by applicable law or agreed to in writing, software
|
|
|
|
+ * distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
+ * See the License for the specific language governing permissions and
|
|
|
|
+ * limitations under the License.
|
|
|
|
+ */
|
|
|
|
+
|
|
|
|
+package com.example.android.codelab.animation.ui.home
|
|
|
|
+
|
|
|
|
+import androidx.compose.animation.*
|
|
|
|
+import androidx.compose.animation.core.*
|
|
|
|
+import androidx.compose.foundation.BorderStroke
|
|
|
|
+import androidx.compose.foundation.background
|
|
|
|
+import androidx.compose.foundation.border
|
|
|
|
+import androidx.compose.foundation.clickable
|
|
|
|
+import androidx.compose.foundation.gestures.awaitFirstDown
|
|
|
|
+import androidx.compose.foundation.gestures.horizontalDrag
|
|
|
|
+import androidx.compose.foundation.layout.Arrangement
|
|
|
|
+import androidx.compose.foundation.layout.Box
|
|
|
|
+import androidx.compose.foundation.layout.Column
|
|
|
|
+import androidx.compose.foundation.layout.PaddingValues
|
|
|
|
+import androidx.compose.foundation.layout.Row
|
|
|
|
+import androidx.compose.foundation.layout.Spacer
|
|
|
|
+import androidx.compose.foundation.layout.fillMaxSize
|
|
|
|
+import androidx.compose.foundation.layout.fillMaxWidth
|
|
|
|
+import androidx.compose.foundation.layout.height
|
|
|
|
+import androidx.compose.foundation.layout.heightIn
|
|
|
|
+import androidx.compose.foundation.layout.offset
|
|
|
|
+import androidx.compose.foundation.layout.padding
|
|
|
|
+import androidx.compose.foundation.layout.size
|
|
|
|
+import androidx.compose.foundation.layout.width
|
|
|
|
+import androidx.compose.foundation.layout.wrapContentSize
|
|
|
|
+import androidx.compose.foundation.lazy.LazyColumn
|
|
|
|
+import androidx.compose.foundation.lazy.LazyListState
|
|
|
|
+import androidx.compose.foundation.lazy.items
|
|
|
|
+import androidx.compose.foundation.lazy.rememberLazyListState
|
|
|
|
+import androidx.compose.foundation.shape.CircleShape
|
|
|
|
+import androidx.compose.foundation.shape.RoundedCornerShape
|
|
|
|
+import androidx.compose.material.*
|
|
|
|
+import androidx.compose.material.icons.Icons
|
|
|
|
+import androidx.compose.material.icons.filled.AccountBox
|
|
|
|
+import androidx.compose.material.icons.filled.Check
|
|
|
|
+import androidx.compose.material.icons.filled.Edit
|
|
|
|
+import androidx.compose.material.icons.filled.Home
|
|
|
|
+import androidx.compose.material.icons.filled.Info
|
|
|
|
+import androidx.compose.material.icons.filled.Refresh
|
|
|
|
+import androidx.compose.runtime.Composable
|
|
|
|
+import androidx.compose.runtime.LaunchedEffect
|
|
|
|
+import androidx.compose.runtime.derivedStateOf
|
|
|
|
+import androidx.compose.runtime.getValue
|
|
|
|
+import androidx.compose.runtime.key
|
|
|
|
+import androidx.compose.runtime.mutableStateListOf
|
|
|
|
+import androidx.compose.runtime.mutableStateOf
|
|
|
|
+import androidx.compose.runtime.remember
|
|
|
|
+import androidx.compose.runtime.rememberCoroutineScope
|
|
|
|
+import androidx.compose.runtime.setValue
|
|
|
|
+import androidx.compose.runtime.snapshots.SnapshotStateList
|
|
|
|
+import androidx.compose.ui.Alignment
|
|
|
|
+import androidx.compose.ui.Modifier
|
|
|
|
+import androidx.compose.ui.composed
|
|
|
|
+import androidx.compose.ui.draw.clip
|
|
|
|
+import androidx.compose.ui.graphics.Color
|
|
|
|
+import androidx.compose.ui.graphics.vector.ImageVector
|
|
|
|
+import androidx.compose.ui.input.pointer.consumePositionChange
|
|
|
|
+import androidx.compose.ui.input.pointer.pointerInput
|
|
|
|
+import androidx.compose.ui.input.pointer.positionChange
|
|
|
|
+import androidx.compose.ui.input.pointer.util.VelocityTracker
|
|
|
|
+import androidx.compose.ui.res.stringArrayResource
|
|
|
|
+import androidx.compose.ui.res.stringResource
|
|
|
|
+import androidx.compose.ui.semantics.heading
|
|
|
|
+import androidx.compose.ui.semantics.semantics
|
|
|
|
+import androidx.compose.ui.text.style.TextAlign
|
|
|
|
+import androidx.compose.ui.tooling.preview.Preview
|
|
|
|
+import androidx.compose.ui.unit.Dp
|
|
|
|
+import androidx.compose.ui.unit.IntOffset
|
|
|
|
+import androidx.compose.ui.unit.dp
|
|
|
|
+import androidx.compose.ui.unit.sp
|
|
|
|
+import com.example.android.codelab.animation.R
|
|
|
|
+import com.example.android.codelab.animation.ui.Amber600
|
|
|
|
+import com.example.android.codelab.animation.ui.AnimationCodelabTheme
|
|
|
|
+import com.example.android.codelab.animation.ui.Green300
|
|
|
|
+import com.example.android.codelab.animation.ui.Green800
|
|
|
|
+import com.example.android.codelab.animation.ui.Purple100
|
|
|
|
+import com.example.android.codelab.animation.ui.Purple700
|
|
|
|
+import com.google.accompanist.pager.ExperimentalPagerApi
|
|
|
|
+import com.google.accompanist.pager.HorizontalPager
|
|
|
|
+import com.google.accompanist.pager.PagerState
|
|
|
|
+import com.google.accompanist.pager.rememberPagerState
|
|
|
|
+import kotlinx.coroutines.CoroutineScope
|
|
|
|
+import kotlinx.coroutines.coroutineScope
|
|
|
|
+import kotlinx.coroutines.delay
|
|
|
|
+import kotlinx.coroutines.launch
|
|
|
|
+import kotlin.math.absoluteValue
|
|
|
|
+import kotlin.math.roundToInt
|
|
|
|
+
|
|
|
|
+enum class TabPage {
|
|
|
|
+ Home, Work
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ * Shows the entire screen.
|
|
|
|
+ */
|
|
|
|
+@OptIn(ExperimentalPagerApi::class)
|
|
|
|
+@Composable
|
|
|
|
+fun Home() {
|
|
|
|
+ // String resources.
|
|
|
|
+ val allTasks = stringArrayResource(R.array.tasks)
|
|
|
|
+ val allTopics = stringArrayResource(R.array.topics).toList()
|
|
|
|
+
|
|
|
|
+ // The currently selected tab.
|
|
|
|
+// var tabPage by remember { mutableStateOf(TabPage.Home) }
|
|
|
|
+ //val (tabPage, setTabPage) = remember { mutableStateOf(TabPage.Home) }
|
|
|
|
+ val tabPage = rememberPagerState(TabPage.Home.ordinal)
|
|
|
|
+
|
|
|
|
+ // True if the whether data is currently loading.
|
|
|
|
+ var weatherLoading by remember { mutableStateOf(false) }
|
|
|
|
+
|
|
|
|
+ // Holds all the tasks currently shown on the task list.
|
|
|
|
+ val tasks = remember { mutableStateListOf(*allTasks) }
|
|
|
|
+
|
|
|
|
+ // Holds the topic that is currently expanded to show its body.
|
|
|
|
+ var expandedTopic by remember { mutableStateOf<String?>(null) }
|
|
|
|
+
|
|
|
|
+ // True if the message about the edit feature is shown.
|
|
|
|
+ var editMessageShown by remember { mutableStateOf(false) }
|
|
|
|
+
|
|
|
|
+ val loadWeather = suspend {
|
|
|
|
+ if (!weatherLoading) {
|
|
|
|
+ weatherLoading = true
|
|
|
|
+ delay(3000L)
|
|
|
|
+ weatherLoading = false
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // Simulates loading weather data. This takes 3 seconds.
|
|
|
|
+ /*suspend fun loadWeather() {
|
|
|
|
+
|
|
|
|
+ }*/
|
|
|
|
+
|
|
|
|
+ // Shows the message about edit feature.
|
|
|
|
+ val showEditMessage = suspend {
|
|
|
|
+ if (!editMessageShown) {
|
|
|
|
+ editMessageShown = true
|
|
|
|
+ delay(3000L)
|
|
|
|
+ editMessageShown = false
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // Load the weather at the initial composition.
|
|
|
|
+ LaunchedEffect(Unit) {
|
|
|
|
+ loadWeather()
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ val lazyListState = rememberLazyListState()
|
|
|
|
+
|
|
|
|
+ // The background color. The value is changed by the current tab.
|
|
|
|
+ // TODO 1: Animate this color change.
|
|
|
|
+ //val backgroundColor = if (tabPage == TabPage.Home) Purple100 else Green300
|
|
|
|
+ /*val backgroundColor by animateColorAsState(targetValue = if (tabPage == TabPage.Home) Purple100 else Green300,
|
|
|
|
+ animationSpec = tween(durationMillis = 350))*/
|
|
|
|
+
|
|
|
|
+ val transition = updateTransition(tabPage)
|
|
|
|
+ val backgroundColor by transition.animateColor {
|
|
|
|
+ if (it.targetPage == TabPage.Home.ordinal) Purple100 else Green300
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // The coroutine scope for event handlers calling suspend functions.
|
|
|
|
+ val coroutineScope = rememberCoroutineScope()
|
|
|
|
+ Scaffold(
|
|
|
|
+ topBar = {
|
|
|
|
+ HomeTabBar(
|
|
|
|
+ backgroundColor = backgroundColor,
|
|
|
|
+ tabPage = tabPage,
|
|
|
|
+ transition = transition,
|
|
|
|
+ onTabSelected = {
|
|
|
|
+ coroutineScope.launch {
|
|
|
|
+ tabPage.animateScrollToPage(page = it)}
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+// onTabSelected = {tabPage = it }
|
|
|
|
+ )
|
|
|
|
+ },
|
|
|
|
+ backgroundColor = backgroundColor,
|
|
|
|
+ floatingActionButton = {
|
|
|
|
+ HomeFloatingActionButton(
|
|
|
|
+ extended = lazyListState.isScrollingUp(),
|
|
|
|
+ onClick = {
|
|
|
|
+ coroutineScope.launch {
|
|
|
|
+ showEditMessage()
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ )
|
|
|
|
+ }
|
|
|
|
+ ) {
|
|
|
|
+ /*HorizontalPager(count = 2, state = tabPage){
|
|
|
|
+
|
|
|
|
+ }*/
|
|
|
|
+ PageContent(lazyListState,
|
|
|
|
+ weatherLoading,
|
|
|
|
+ coroutineScope,
|
|
|
|
+ loadWeather,
|
|
|
|
+ allTopics,
|
|
|
|
+ expandedTopic,
|
|
|
|
+ tasks,
|
|
|
|
+ allTasks)
|
|
|
|
+
|
|
|
|
+ EditMessage(editMessageShown)
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+@Composable
|
|
|
|
+fun PageContent(
|
|
|
|
+ lazyListState: LazyListState,
|
|
|
|
+ weatherLoading: Boolean,
|
|
|
|
+ coroutineScope: CoroutineScope,
|
|
|
|
+ loadWeather: suspend () -> Unit,
|
|
|
|
+ allTopics: List<String>,
|
|
|
|
+ expandedTopic: String?,
|
|
|
|
+ tasks: SnapshotStateList<String>,
|
|
|
|
+ allTasks: Array<String>,
|
|
|
|
+) {
|
|
|
|
+ var expandedTopic1 = expandedTopic
|
|
|
|
+ LazyColumn(
|
|
|
|
+ contentPadding = PaddingValues(horizontal = 16.dp, vertical = 32.dp),
|
|
|
|
+ state = lazyListState
|
|
|
|
+ ) {
|
|
|
|
+ // Weather
|
|
|
|
+ item { Header(title = stringResource(R.string.weather)) }
|
|
|
|
+ item { Spacer(modifier = Modifier.height(16.dp)) }
|
|
|
|
+ item {
|
|
|
|
+ Surface(
|
|
|
|
+ modifier = Modifier.fillMaxWidth(),
|
|
|
|
+ elevation = 2.dp
|
|
|
|
+ ) {
|
|
|
|
+ if (weatherLoading) {
|
|
|
|
+ LoadingRow()
|
|
|
|
+ } else {
|
|
|
|
+ WeatherRow(onRefresh = {
|
|
|
|
+ coroutineScope.launch {
|
|
|
|
+ loadWeather()
|
|
|
|
+ }
|
|
|
|
+ })
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ item { Spacer(modifier = Modifier.height(32.dp)) }
|
|
|
|
+
|
|
|
|
+ // Topics
|
|
|
|
+ item { Header(title = stringResource(R.string.topics)) }
|
|
|
|
+ item { Spacer(modifier = Modifier.height(16.dp)) }
|
|
|
|
+ items(allTopics) { topic ->
|
|
|
|
+ TopicRow(
|
|
|
|
+ topic = topic,
|
|
|
|
+ expanded = expandedTopic1 == topic,
|
|
|
|
+ onClick = {
|
|
|
|
+ coroutineScope.launch {
|
|
|
|
+ delay(200)
|
|
|
|
+ expandedTopic1 = if (expandedTopic1 == topic) null else topic
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ )
|
|
|
|
+ }
|
|
|
|
+ item { Spacer(modifier = Modifier.height(32.dp)) }
|
|
|
|
+
|
|
|
|
+ // Tasks
|
|
|
|
+ item { Header(title = stringResource(R.string.tasks)) }
|
|
|
|
+ item { Spacer(modifier = Modifier.height(16.dp)) }
|
|
|
|
+ if (tasks.isEmpty()) {
|
|
|
|
+ item {
|
|
|
|
+ TextButton(onClick = { tasks.clear(); tasks.addAll(allTasks) }) {
|
|
|
|
+ Text(stringResource(R.string.add_tasks))
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ items(count = tasks.size) { i ->
|
|
|
|
+ val task = tasks.getOrNull(i)
|
|
|
|
+ if (task != null) {
|
|
|
|
+ key(task) {
|
|
|
|
+ TaskRow(
|
|
|
|
+ task = task,
|
|
|
|
+ onRemove = { tasks.remove(task) }
|
|
|
|
+ )
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ * Shows the floating action button.
|
|
|
|
+ *
|
|
|
|
+ * @param extended Whether the tab should be shown in its expanded state.
|
|
|
|
+ */
|
|
|
|
+// AnimatedVisibility is currently an experimental API in Compose Animation.
|
|
|
|
+@OptIn(ExperimentalAnimationApi::class)
|
|
|
|
+@Composable
|
|
|
|
+private fun HomeFloatingActionButton(
|
|
|
|
+ extended: Boolean,
|
|
|
|
+ onClick: () -> Unit,
|
|
|
|
+) {
|
|
|
|
+ // Use `FloatingActionButton` rather than `ExtendedFloatingActionButton` for full control on
|
|
|
|
+ // how it should animate.
|
|
|
|
+ FloatingActionButton(onClick = onClick) {
|
|
|
|
+ Row(
|
|
|
|
+ modifier = Modifier.padding(horizontal = 16.dp)
|
|
|
|
+ ) {
|
|
|
|
+ Icon(
|
|
|
|
+ imageVector = Icons.Default.Edit,
|
|
|
|
+ contentDescription = null
|
|
|
|
+ )
|
|
|
|
+ // Toggle the visibility of the content with animation.
|
|
|
|
+ // TODO 2-1: Animate this visibility change.
|
|
|
|
+ /*if (extended) {
|
|
|
|
+ Text(
|
|
|
|
+ text = stringResource(R.string.edit),
|
|
|
|
+ modifier = Modifier
|
|
|
|
+ .padding(start = 8.dp, top = 3.dp)
|
|
|
|
+ )
|
|
|
|
+ }*/
|
|
|
|
+ AnimatedVisibility(
|
|
|
|
+ visible = extended,
|
|
|
|
+ /*enter = slideInHorizontally(
|
|
|
|
+ initialOffsetX = {it},
|
|
|
|
+ animationSpec = tween(durationMillis = 200,easing = LinearOutSlowInEasing)),
|
|
|
|
+ exit = slideOutHorizontally(
|
|
|
|
+ targetOffsetX = {it},
|
|
|
|
+ animationSpec = tween(durationMillis = 200,easing = LinearOutSlowInEasing))*/
|
|
|
|
+ ) {
|
|
|
|
+ Text(
|
|
|
|
+ text = stringResource(R.string.edit),
|
|
|
|
+ modifier = Modifier
|
|
|
|
+ .padding(start = 8.dp, top = 3.dp)
|
|
|
|
+ )
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ * Shows a message that the edit feature is not available.
|
|
|
|
+ */
|
|
|
|
+@OptIn(ExperimentalAnimationApi::class)
|
|
|
|
+@Composable
|
|
|
|
+private fun EditMessage(shown: Boolean) {
|
|
|
|
+ // TODO 2-2: The message should slide down from the top on appearance and slide up on
|
|
|
|
+ // disappearance.
|
|
|
|
+ AnimatedVisibility(
|
|
|
|
+ visible = shown,
|
|
|
|
+ enter = fadeIn() + expandVertically(),
|
|
|
|
+ exit = fadeOut() + shrinkVertically(),
|
|
|
|
+ /*enter= slideInVertically(
|
|
|
|
+ initialOffsetY = {-it},
|
|
|
|
+ animationSpec = tween(durationMillis = 200, easing = LinearOutSlowInEasing)
|
|
|
|
+ ),
|
|
|
|
+ exit = slideOutVertically(
|
|
|
|
+ targetOffsetY = {-it},
|
|
|
|
+ animationSpec = tween(durationMillis = 200, easing = FastOutLinearInEasing)
|
|
|
|
+ )*/
|
|
|
|
+ ) {
|
|
|
|
+ Surface(
|
|
|
|
+ modifier = Modifier.fillMaxWidth(),
|
|
|
|
+ color = MaterialTheme.colors.secondary,
|
|
|
|
+ elevation = 4.dp
|
|
|
|
+ ) {
|
|
|
|
+ Text(
|
|
|
|
+ text = stringResource(R.string.edit_message),
|
|
|
|
+ modifier = Modifier.padding(16.dp)
|
|
|
|
+ )
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ * Returns whether the lazy list is currently scrolling up.
|
|
|
|
+ */
|
|
|
|
+@Composable
|
|
|
|
+private fun LazyListState.isScrollingUp(): Boolean {
|
|
|
|
+ var previousIndex by remember(this) { mutableStateOf(firstVisibleItemIndex) }
|
|
|
|
+ var previousScrollOffset by remember(this) { mutableStateOf(firstVisibleItemScrollOffset) }
|
|
|
|
+ return remember(this) {
|
|
|
|
+ derivedStateOf {
|
|
|
|
+ if (previousIndex != firstVisibleItemIndex) {
|
|
|
|
+ previousIndex > firstVisibleItemIndex
|
|
|
|
+ } else {
|
|
|
|
+ previousScrollOffset >= firstVisibleItemScrollOffset
|
|
|
|
+ }.also {
|
|
|
|
+ previousIndex = firstVisibleItemIndex
|
|
|
|
+ previousScrollOffset = firstVisibleItemScrollOffset
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }.value
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ * Shows the header label.
|
|
|
|
+ *
|
|
|
|
+ * @param title The title to be shown.
|
|
|
|
+ */
|
|
|
|
+@Composable
|
|
|
|
+private fun Header(
|
|
|
|
+ title: String,
|
|
|
|
+) {
|
|
|
|
+ Text(
|
|
|
|
+ text = title,
|
|
|
|
+ modifier = Modifier.semantics { heading() },
|
|
|
|
+ style = MaterialTheme.typography.h5
|
|
|
|
+ )
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ * Shows a row for one topic.
|
|
|
|
+ *
|
|
|
|
+ * @param topic The topic title.
|
|
|
|
+ * @param expanded Whether the row should be shown expanded with the topic body.
|
|
|
|
+ * @param onClick Called when the row is clicked.
|
|
|
|
+ */
|
|
|
|
+@OptIn(ExperimentalMaterialApi::class)
|
|
|
|
+@Composable
|
|
|
|
+private fun TopicRow(topic: String, expanded: Boolean, onClick: () -> Unit) {
|
|
|
|
+ TopicRowSpacer(visible = expanded)
|
|
|
|
+ Surface(
|
|
|
|
+ modifier = Modifier
|
|
|
|
+ .fillMaxWidth(),
|
|
|
|
+ elevation = 2.dp,
|
|
|
|
+ onClick = onClick
|
|
|
|
+ ) {
|
|
|
|
+ //val size = animateSizeAsState(targetValue = )
|
|
|
|
+ // TODO 3: Animate the size change of the content.
|
|
|
|
+ Column(
|
|
|
|
+ modifier = Modifier
|
|
|
|
+ .fillMaxWidth()
|
|
|
|
+ .padding(16.dp)
|
|
|
|
+ .animateContentSize(animationSpec = tween(durationMillis = 250))
|
|
|
|
+ ) {
|
|
|
|
+ Row {
|
|
|
|
+ Icon(
|
|
|
|
+ imageVector = Icons.Default.Info,
|
|
|
|
+ contentDescription = null
|
|
|
|
+ )
|
|
|
|
+ Spacer(modifier = Modifier.width(16.dp))
|
|
|
|
+ Text(
|
|
|
|
+ text = topic,
|
|
|
|
+ style = MaterialTheme.typography.body1
|
|
|
|
+ )
|
|
|
|
+ }
|
|
|
|
+ if (expanded) {
|
|
|
|
+ Spacer(modifier = Modifier.height(8.dp))
|
|
|
|
+ Text(
|
|
|
|
+ text = stringResource(R.string.lorem_ipsum),
|
|
|
|
+ textAlign = TextAlign.Justify
|
|
|
|
+ )
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ TopicRowSpacer(visible = expanded)
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ * Shows a separator for topics.
|
|
|
|
+ */
|
|
|
|
+@OptIn(ExperimentalAnimationApi::class)
|
|
|
|
+@Composable
|
|
|
|
+fun TopicRowSpacer(visible: Boolean) {
|
|
|
|
+ AnimatedVisibility(visible = visible) {
|
|
|
|
+ Spacer(modifier = Modifier.height(8.dp))
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ * Shows the bar that holds 2 tabs.
|
|
|
|
+ *
|
|
|
|
+ * @param backgroundColor The background color for the bar.
|
|
|
|
+ * @param tabPage The [TabPage] that is currently selected.
|
|
|
|
+ * @param onTabSelected Called when the tab is switched.
|
|
|
|
+ */
|
|
|
|
+@OptIn(ExperimentalPagerApi::class)
|
|
|
|
+@Composable
|
|
|
|
+private fun HomeTabBar(
|
|
|
|
+ backgroundColor: Color,
|
|
|
|
+ tabPage: PagerState,
|
|
|
|
+ onTabSelected: (tabPage: Int) -> Unit,
|
|
|
|
+ transition: Transition<PagerState>,
|
|
|
|
+) {
|
|
|
|
+ //val coroutineScope = rememberCoroutineScope()
|
|
|
|
+ TabRow(
|
|
|
|
+ selectedTabIndex = tabPage.currentPage,
|
|
|
|
+ backgroundColor = backgroundColor,
|
|
|
|
+ indicator = { tabPositions ->
|
|
|
|
+ HomeTabIndicator(tabPositions, transition)
|
|
|
|
+ }
|
|
|
|
+ ) {
|
|
|
|
+ HomeTab(
|
|
|
|
+ icon = Icons.Default.Home,
|
|
|
|
+ title = stringResource(R.string.home),
|
|
|
|
+ onClick = {
|
|
|
|
+ onTabSelected(TabPage.Home.ordinal)
|
|
|
|
+ /*coroutineScope.launch {
|
|
|
|
+ delay(100)
|
|
|
|
+ }*/
|
|
|
|
+
|
|
|
|
+ }
|
|
|
|
+ )
|
|
|
|
+ HomeTab(
|
|
|
|
+ icon = Icons.Default.AccountBox,
|
|
|
|
+ title = stringResource(R.string.work),
|
|
|
|
+ onClick = {
|
|
|
|
+ onTabSelected(TabPage.Work.ordinal)
|
|
|
|
+ /*coroutineScope.launch {
|
|
|
|
+ delay(100)
|
|
|
|
+ onTabSelected(TabPage.Work)
|
|
|
|
+ }*/
|
|
|
|
+ }
|
|
|
|
+ )
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ * Shows an indicator for the tab.
|
|
|
|
+ *
|
|
|
|
+ * @param tabPositions The list of [TabPosition]s from a [TabRow].
|
|
|
|
+ * @param tabPage The [TabPage] that is currently selected.
|
|
|
|
+ */
|
|
|
|
+@OptIn(ExperimentalPagerApi::class)
|
|
|
|
+@Composable
|
|
|
|
+private fun HomeTabIndicator(
|
|
|
|
+ tabPositions: List<TabPosition>,
|
|
|
|
+ transition: Transition<PagerState>,
|
|
|
|
+) {
|
|
|
|
+
|
|
|
|
+ /*val indicatorLeft = tabPositions[tabPage.ordinal].left
|
|
|
|
+ val indicatorRight = tabPositions[tabPage.ordinal].right
|
|
|
|
+ val color = if (tabPage == TabPage.Home) Purple700 else Green800*/
|
|
|
|
+ // TODO 4: Animate these value changes.
|
|
|
|
+ /*val indicatorLeft by animateDpAsState(targetValue = tabPositions[tabPage.ordinal].left)
|
|
|
|
+ val indicatorRight by animateDpAsState(targetValue = tabPositions[tabPage.ordinal].right)
|
|
|
|
+ val color by animateColorAsState(targetValue = if (tabPage == TabPage.Home) Purple700 else Green800)*/
|
|
|
|
+ //val transition = updateTransition(tabPage, label = "indicator")
|
|
|
|
+ val indicatorLeft by transition.animateDp(transitionSpec = {
|
|
|
|
+// if(TabPage.Home isTransitioningTo TabPage.Work){
|
|
|
|
+ if (this.initialState.targetPage == TabPage.Home.ordinal && this.targetState.targetPage == TabPage.Work.ordinal) {
|
|
|
|
+ spring(stiffness = Spring.StiffnessVeryLow)
|
|
|
|
+ } else {
|
|
|
|
+ spring(stiffness = Spring.StiffnessMedium)
|
|
|
|
+ }
|
|
|
|
+ }, label = "left") { page ->
|
|
|
|
+ tabPositions[page.targetPage].left
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ val indicatorRight by transition.animateDp(transitionSpec = {
|
|
|
|
+ //if(TabPage.Home isTransitioningTo TabPage.Work){
|
|
|
|
+ if (this.initialState.targetPage == TabPage.Home.ordinal && this.targetState.targetPage == TabPage.Work.ordinal) {
|
|
|
|
+ spring(stiffness = Spring.StiffnessMedium)
|
|
|
|
+ } else {
|
|
|
|
+ spring(stiffness = Spring.StiffnessVeryLow)
|
|
|
|
+ }
|
|
|
|
+ }, label = "right") { page ->
|
|
|
|
+ tabPositions[page.targetPage].right
|
|
|
|
+ }
|
|
|
|
+ val color by transition.animateColor(label = "color") {
|
|
|
|
+ if (it.targetPage == TabPage.Home.ordinal) Purple700 else Green800
|
|
|
|
+ }
|
|
|
|
+ /*val indicatorLeft by transition.animateDp { page ->
|
|
|
|
+ tabPositions[page.ordinal].left
|
|
|
|
+ }
|
|
|
|
+ val indicatorRight by transition.animateDp { page ->
|
|
|
|
+ tabPositions[page.ordinal].right
|
|
|
|
+ }
|
|
|
|
+ val color by transition.animateColor { page ->
|
|
|
|
+ if (page == TabPage.Home) Purple700 else Green800
|
|
|
|
+ }*/
|
|
|
|
+
|
|
|
|
+ Box(
|
|
|
|
+ Modifier
|
|
|
|
+ .fillMaxSize()
|
|
|
|
+ .wrapContentSize(align = Alignment.BottomStart)
|
|
|
|
+ .offset(x = indicatorLeft)
|
|
|
|
+ .width(indicatorRight - indicatorLeft)
|
|
|
|
+ .padding(4.dp)
|
|
|
|
+ .fillMaxSize()
|
|
|
|
+ .border(
|
|
|
|
+ BorderStroke(2.dp, color),
|
|
|
|
+ RoundedCornerShape(4.dp)
|
|
|
|
+ )
|
|
|
|
+ )
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ * Shows a tab.
|
|
|
|
+ *
|
|
|
|
+ * @param icon The icon to be shown on this tab.
|
|
|
|
+ * @param title The title to be shown on this tab.
|
|
|
|
+ * @param onClick Called when this tab is clicked.
|
|
|
|
+ * @param modifier The [Modifier].
|
|
|
|
+ */
|
|
|
|
+@Composable
|
|
|
|
+private fun HomeTab(
|
|
|
|
+ icon: ImageVector,
|
|
|
|
+ title: String,
|
|
|
|
+ onClick: () -> Unit,
|
|
|
|
+ modifier: Modifier = Modifier,
|
|
|
|
+) {
|
|
|
|
+ Row(
|
|
|
|
+ modifier = modifier
|
|
|
|
+ .clickable(onClick = onClick)
|
|
|
|
+ .padding(16.dp),
|
|
|
|
+ horizontalArrangement = Arrangement.Center,
|
|
|
|
+ verticalAlignment = Alignment.CenterVertically
|
|
|
|
+ ) {
|
|
|
|
+ Icon(
|
|
|
|
+ imageVector = icon,
|
|
|
|
+ contentDescription = null
|
|
|
|
+ )
|
|
|
|
+ Spacer(modifier = Modifier.width(16.dp))
|
|
|
|
+ Text(text = title)
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ * Shows the weather.
|
|
|
|
+ *
|
|
|
|
+ * @param onRefresh Called when the refresh icon button is clicked.
|
|
|
|
+ */
|
|
|
|
+@Composable
|
|
|
|
+private fun WeatherRow(
|
|
|
|
+ onRefresh: () -> Unit,
|
|
|
|
+) {
|
|
|
|
+ Row(
|
|
|
|
+ modifier = Modifier
|
|
|
|
+ .heightIn(min = 64.dp)
|
|
|
|
+ .padding(16.dp),
|
|
|
|
+ verticalAlignment = Alignment.CenterVertically
|
|
|
|
+ ) {
|
|
|
|
+ Box(
|
|
|
|
+ modifier = Modifier
|
|
|
|
+ .size(48.dp)
|
|
|
|
+ .clip(CircleShape)
|
|
|
|
+ .background(Amber600)
|
|
|
|
+ )
|
|
|
|
+ Spacer(modifier = Modifier.width(16.dp))
|
|
|
|
+ Text(text = stringResource(R.string.temperature), fontSize = 24.sp)
|
|
|
|
+ Spacer(modifier = Modifier.weight(1f))
|
|
|
|
+ IconButton(onClick = onRefresh) {
|
|
|
|
+ Icon(
|
|
|
|
+ imageVector = Icons.Default.Refresh,
|
|
|
|
+ contentDescription = stringResource(R.string.refresh)
|
|
|
|
+ )
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ * Shows the loading state of the weather.
|
|
|
|
+ */
|
|
|
|
+@Composable
|
|
|
|
+private fun LoadingRow() {
|
|
|
|
+ // TODO 5: Animate this value between 0f and 1f, then back to 0f repeatedly.
|
|
|
|
+ val infiniteTransition = rememberInfiniteTransition()
|
|
|
|
+ val alpha by infiniteTransition.animateFloat(initialValue = 0.25f,
|
|
|
|
+ targetValue = 1f,
|
|
|
|
+ animationSpec = InfiniteRepeatableSpec(repeatMode = RepeatMode.Reverse,animation = tween(durationMillis = 800)))
|
|
|
|
+ Row(
|
|
|
|
+ modifier = Modifier
|
|
|
|
+ .heightIn(min = 64.dp)
|
|
|
|
+ .padding(16.dp),
|
|
|
|
+ verticalAlignment = Alignment.CenterVertically
|
|
|
|
+ ) {
|
|
|
|
+ Box(
|
|
|
|
+ modifier = Modifier
|
|
|
|
+ .size(48.dp)
|
|
|
|
+ .clip(CircleShape)
|
|
|
|
+ .background(Color.LightGray.copy(alpha = alpha))
|
|
|
|
+ )
|
|
|
|
+ Spacer(modifier = Modifier.width(16.dp))
|
|
|
|
+ Box(
|
|
|
|
+ modifier = Modifier
|
|
|
|
+ .fillMaxWidth()
|
|
|
|
+ .height(32.dp)
|
|
|
|
+ .background(Color.LightGray.copy(alpha = alpha))
|
|
|
|
+ )
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ * Shows a row for one task.
|
|
|
|
+ *
|
|
|
|
+ * @param task The task description.
|
|
|
|
+ * @param onRemove Called when the task is swiped away and removed.
|
|
|
|
+ */
|
|
|
|
+@Composable
|
|
|
|
+private fun TaskRow(task: String, onRemove: () -> Unit) {
|
|
|
|
+ Surface(
|
|
|
|
+ modifier = Modifier
|
|
|
|
+ .fillMaxWidth()
|
|
|
|
+ .swipeToDismiss(onRemove),
|
|
|
|
+ elevation = 2.dp
|
|
|
|
+ ) {
|
|
|
|
+ Row(
|
|
|
|
+ modifier = Modifier
|
|
|
|
+ .fillMaxWidth()
|
|
|
|
+ .padding(16.dp)
|
|
|
|
+ ) {
|
|
|
|
+ Icon(
|
|
|
|
+ imageVector = Icons.Default.Check,
|
|
|
|
+ contentDescription = null
|
|
|
|
+ )
|
|
|
|
+ Spacer(modifier = Modifier.width(16.dp))
|
|
|
|
+ Text(
|
|
|
|
+ text = task,
|
|
|
|
+ style = MaterialTheme.typography.body1
|
|
|
|
+ )
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ * The modified element can be horizontally swiped away.
|
|
|
|
+ *
|
|
|
|
+ * @param onDismissed Called when the element is swiped to the edge of the screen.
|
|
|
|
+ */
|
|
|
|
+private fun Modifier.swipeToDismiss(
|
|
|
|
+ onDismissed: () -> Unit,
|
|
|
|
+): Modifier = composed {
|
|
|
|
+ // TODO 6-1: Create an Animatable instance for the offset of the swiped element.
|
|
|
|
+ val offsetX = remember { Animatable(0f) } // Add this line
|
|
|
|
+ pointerInput(Unit) {
|
|
|
|
+ // Used to calculate a settling position of a fling animation.
|
|
|
|
+ val decay = splineBasedDecay<Float>(this)
|
|
|
|
+ // Wrap in a coroutine scope to use suspend functions for touch events and animation.
|
|
|
|
+ coroutineScope {
|
|
|
|
+ while (true) {
|
|
|
|
+ // Wait for a touch down event.
|
|
|
|
+ val pointerId = awaitPointerEventScope { awaitFirstDown().id }
|
|
|
|
+ // TODO 6-2: Touch detected; the animation should be stopped.
|
|
|
|
+ offsetX.stop()
|
|
|
|
+ // Prepare for drag events and record velocity of a fling.
|
|
|
|
+ val velocityTracker = VelocityTracker()
|
|
|
|
+ // Wait for drag events.
|
|
|
|
+ awaitPointerEventScope {
|
|
|
|
+ horizontalDrag(pointerId) { change ->
|
|
|
|
+ // TODO 6-3: Apply the drag change to the Animatable offset.
|
|
|
|
+ val horizontalDragOffset = offsetX.value + change.positionChange().x
|
|
|
|
+ launch {
|
|
|
|
+ offsetX.snapTo(horizontalDragOffset)
|
|
|
|
+ }
|
|
|
|
+ // Record the velocity of the drag.
|
|
|
|
+ velocityTracker.addPosition(change.uptimeMillis, change.position)
|
|
|
|
+ // Consume the gesture event, not passed to external
|
|
|
|
+ change.consumePositionChange()
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ // Dragging finished. Calculate the velocity of the fling.
|
|
|
|
+ val velocity = velocityTracker.calculateVelocity().x
|
|
|
|
+ // TODO 6-4: Calculate the eventual position where the fling should settle
|
|
|
|
+ // based on the current offset value and velocity
|
|
|
|
+ val targetOffsetX = decay.calculateTargetValue(offsetX.value, velocity)
|
|
|
|
+ // TODO 6-5: Set the upper and lower bounds so that the animation stops when it
|
|
|
|
+ // reaches the edge.
|
|
|
|
+ offsetX.updateBounds(
|
|
|
|
+ lowerBound = -size.width.toFloat(),
|
|
|
|
+ upperBound = size.width.toFloat()
|
|
|
|
+ )
|
|
|
|
+ launch {
|
|
|
|
+ // TODO 6-6: Slide back the element if the settling position does not go beyond
|
|
|
|
+ // the size of the element. Remove the element if it does.
|
|
|
|
+ if (targetOffsetX.absoluteValue <= size.width) {
|
|
|
|
+ // Not enough velocity; Slide back.
|
|
|
|
+ offsetX.animateTo(targetValue = 0f, initialVelocity = velocity)
|
|
|
|
+ } else {
|
|
|
|
+ // Enough velocity to slide away the element to the edge.
|
|
|
|
+ offsetX.animateDecay(velocity, decay)
|
|
|
|
+ // The element was swiped away.
|
|
|
|
+ onDismissed()
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ .offset {
|
|
|
|
+ // TODO 6-7: Use the animating offset value here.
|
|
|
|
+ IntOffset(offsetX.value.roundToInt(), 0)
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+@OptIn(ExperimentalPagerApi::class)
|
|
|
|
+@Preview
|
|
|
|
+@Composable
|
|
|
|
+private fun PreviewHomeTabBar() {
|
|
|
|
+ val pagerState = rememberPagerState()
|
|
|
|
+ HomeTabBar(
|
|
|
|
+ backgroundColor = Purple100,
|
|
|
|
+ tabPage = pagerState,
|
|
|
|
+ onTabSelected = {},
|
|
|
|
+ transition = updateTransition(targetState = pagerState)
|
|
|
|
+
|
|
|
|
+ )
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+@Preview
|
|
|
|
+@Composable
|
|
|
|
+private fun PreviewHome() {
|
|
|
|
+ AnimationCodelabTheme {
|
|
|
|
+ Home()
|
|
|
|
+ }
|
|
|
|
+}
|