diff --git a/Crane/README.md b/Crane/README.md index 8525d6afc8..4340151319 100644 --- a/Crane/README.md +++ b/Crane/README.md @@ -34,13 +34,14 @@ interact with the `MapView` seamlessly. ## Hilt -Crane uses [Hilt][hilt] to manage its dependencies. The Hilt's ViewModel extension (with the -`@ViewModelInject` annotation) works perfectly with Compose's ViewModel integration (`viewModel()` +Crane uses [Hilt][hilt] to manage its dependencies. Hilt's ViewModel (with the +`@HiltViewModel` annotation) works perfectly with Compose's ViewModel integration (`viewModel()` composable function) as you can see in the following snippet of code. `viewModel()` will automatically use the factory that Hilt creates for the ViewModel: ``` -class MainViewModel @ViewModelInject constructor( +@HiltViewModel +class MainViewModel @Inject constructor( private val destinationsRepository: DestinationsRepository, @DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher, datesRepository: DatesRepository diff --git a/Crane/app/build.gradle b/Crane/app/build.gradle index 6d0b62b582..b0664e4993 100644 --- a/Crane/app/build.gradle +++ b/Crane/app/build.gradle @@ -91,7 +91,6 @@ android { } composeOptions { - kotlinCompilerVersion Libs.Kotlin.version kotlinCompilerExtensionVersion Libs.AndroidX.Compose.version } @@ -120,11 +119,7 @@ dependencies { implementation Libs.AndroidX.Lifecycle.viewModelKtx implementation Libs.Hilt.android - implementation Libs.Hilt.AndroidX.viewModel - compileOnly Libs.AssistedInjection.dagger kapt Libs.Hilt.compiler - kapt Libs.Hilt.AndroidX.compiler - kapt Libs.AssistedInjection.processor androidTestImplementation Libs.JUnit.junit androidTestImplementation Libs.AndroidX.Test.runner @@ -134,9 +129,6 @@ dependencies { androidTestImplementation Libs.Kotlin.Coroutines.test androidTestImplementation Libs.AndroidX.Compose.uiTest androidTestImplementation Libs.Hilt.android - androidTestImplementation Libs.Hilt.AndroidX.viewModel androidTestImplementation Libs.Hilt.testing kaptAndroidTest Libs.Hilt.compiler - kaptAndroidTest Libs.Hilt.AndroidX.compiler - kaptAndroidTest Libs.AssistedInjection.processor } diff --git a/Crane/app/src/androidTest/java/androidx/compose/samples/crane/CoroutinesTestRule.kt b/Crane/app/src/androidTest/java/androidx/compose/samples/crane/CoroutinesTestRule.kt new file mode 100644 index 0000000000..b3e3d7df9a --- /dev/null +++ b/Crane/app/src/androidTest/java/androidx/compose/samples/crane/CoroutinesTestRule.kt @@ -0,0 +1,42 @@ +/* + * 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 androidx.compose.samples.crane + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestCoroutineDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.junit.rules.TestWatcher +import org.junit.runner.Description + +@OptIn(ExperimentalCoroutinesApi::class) +class CoroutinesTestRule constructor( + val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher() +) : TestWatcher() { + + override fun starting(description: Description?) { + super.starting(description) + Dispatchers.setMain(testDispatcher) + } + + override fun finished(description: Description?) { + super.finished(description) + Dispatchers.resetMain() + testDispatcher.cleanupTestCoroutines() + } +} diff --git a/Crane/app/src/androidTest/java/androidx/compose/samples/crane/calendar/CalendarTest.kt b/Crane/app/src/androidTest/java/androidx/compose/samples/crane/calendar/CalendarTest.kt index 54c317e7be..49dff6ea3f 100644 --- a/Crane/app/src/androidTest/java/androidx/compose/samples/crane/calendar/CalendarTest.kt +++ b/Crane/app/src/androidTest/java/androidx/compose/samples/crane/calendar/CalendarTest.kt @@ -24,7 +24,6 @@ import androidx.compose.samples.crane.calendar.model.DaySelectedStatus.LastDay import androidx.compose.samples.crane.calendar.model.DaySelectedStatus.NoSelected import androidx.compose.samples.crane.calendar.model.DaySelectedStatus.Selected import androidx.compose.samples.crane.data.DatesRepository -import androidx.compose.samples.crane.di.DispatchersModule import androidx.compose.samples.crane.ui.CraneTheme import androidx.compose.ui.test.SemanticsMatcher import androidx.compose.ui.test.assertLabelEquals @@ -35,13 +34,12 @@ import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performScrollTo import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest -import dagger.hilt.android.testing.UninstallModules import org.junit.Before +import org.junit.Ignore import org.junit.Rule import org.junit.Test import javax.inject.Inject -@UninstallModules(DispatchersModule::class) @HiltAndroidTest class CalendarTest { @@ -67,6 +65,7 @@ class CalendarTest { } } + @Ignore("performScrollTo doesn't work with LazyLists: issuetracker.google.com/178483889") @Test fun scrollsToTheBottom() { composeTestRule.onNodeWithContentDescription("January 1").assertExists() diff --git a/Crane/app/src/androidTest/java/androidx/compose/samples/crane/details/DetailsActivityTest.kt b/Crane/app/src/androidTest/java/androidx/compose/samples/crane/details/DetailsActivityTest.kt index d91c364ee6..7f7c4ad2fb 100644 --- a/Crane/app/src/androidTest/java/androidx/compose/samples/crane/details/DetailsActivityTest.kt +++ b/Crane/app/src/androidTest/java/androidx/compose/samples/crane/details/DetailsActivityTest.kt @@ -20,7 +20,6 @@ import androidx.compose.samples.crane.R import androidx.compose.samples.crane.data.DestinationsRepository import androidx.compose.samples.crane.data.ExploreModel import androidx.compose.samples.crane.data.MADRID -import androidx.compose.samples.crane.di.DispatchersModule import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.onNodeWithText @@ -35,7 +34,6 @@ import com.google.android.libraries.maps.model.CameraPosition import com.google.android.libraries.maps.model.LatLng import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest -import dagger.hilt.android.testing.UninstallModules import org.junit.Before import org.junit.Rule import org.junit.Test @@ -44,7 +42,6 @@ import javax.inject.Inject import kotlin.math.pow import kotlin.math.round -@UninstallModules(DispatchersModule::class) @HiltAndroidTest class DetailsActivityTest { diff --git a/Crane/app/src/androidTest/java/androidx/compose/samples/crane/di/TestDispatchersModule.kt b/Crane/app/src/androidTest/java/androidx/compose/samples/crane/di/TestDispatchersModule.kt index 554d87b150..3e37215867 100644 --- a/Crane/app/src/androidTest/java/androidx/compose/samples/crane/di/TestDispatchersModule.kt +++ b/Crane/app/src/androidTest/java/androidx/compose/samples/crane/di/TestDispatchersModule.kt @@ -20,15 +20,18 @@ package androidx.compose.samples.crane.di import dagger.Module import dagger.Provides -import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import dagger.hilt.testing.TestInstallIn import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @OptIn(ExperimentalCoroutinesApi::class) @Module -@InstallIn(SingletonComponent::class) +@TestInstallIn( + components = [SingletonComponent::class], + replaces = [DispatchersModule::class] +) class TestDispatchersModule { @Provides diff --git a/Crane/app/src/androidTest/java/androidx/compose/samples/crane/home/HomeTest.kt b/Crane/app/src/androidTest/java/androidx/compose/samples/crane/home/HomeTest.kt index af4e73489c..6867a6daa1 100644 --- a/Crane/app/src/androidTest/java/androidx/compose/samples/crane/home/HomeTest.kt +++ b/Crane/app/src/androidTest/java/androidx/compose/samples/crane/home/HomeTest.kt @@ -16,18 +16,15 @@ package androidx.compose.samples.crane.home -import androidx.compose.samples.crane.di.DispatchersModule import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest -import dagger.hilt.android.testing.UninstallModules import org.junit.Before import org.junit.Rule import org.junit.Test -@UninstallModules(DispatchersModule::class) @HiltAndroidTest class HomeTest { diff --git a/Crane/app/src/main/java/androidx/compose/samples/crane/base/BaseUserInput.kt b/Crane/app/src/main/java/androidx/compose/samples/crane/base/BaseUserInput.kt index 9091b5730c..e6786df743 100644 --- a/Crane/app/src/main/java/androidx/compose/samples/crane/base/BaseUserInput.kt +++ b/Crane/app/src/main/java/androidx/compose/samples/crane/base/BaseUserInput.kt @@ -102,7 +102,7 @@ fun CraneEditableUserInput( textStyle = if (isHint()) { captionTextStyle.copy(color = AmbientContentColor.current) } else { - MaterialTheme.typography.body1 + MaterialTheme.typography.body1.copy(color = AmbientContentColor.current) }, cursorColor = AmbientContentColor.current ) @@ -125,7 +125,8 @@ private fun CraneBaseUserInput( Icon( modifier = Modifier.preferredSize(24.dp, 24.dp), imageVector = vectorResource(id = vectorImageId), - tint = if (tintIcon()) tint else Color(0x80FFFFFF) + tint = if (tintIcon()) tint else Color(0x80FFFFFF), + contentDescription = null ) Spacer(Modifier.preferredWidth(8.dp)) } diff --git a/Crane/app/src/main/java/androidx/compose/samples/crane/base/CraneDrawer.kt b/Crane/app/src/main/java/androidx/compose/samples/crane/base/CraneDrawer.kt index 7cc6e44d87..56ab2fa4ad 100644 --- a/Crane/app/src/main/java/androidx/compose/samples/crane/base/CraneDrawer.kt +++ b/Crane/app/src/main/java/androidx/compose/samples/crane/base/CraneDrawer.kt @@ -28,6 +28,7 @@ import androidx.compose.runtime.Composable import androidx.compose.samples.crane.R import androidx.compose.samples.crane.ui.CraneTheme import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -36,8 +37,15 @@ private val screens = listOf("Find Trips", "My Trips", "Saved Trips", "Price Ale @Composable fun CraneDrawer(modifier: Modifier = Modifier) { - Column(modifier.fillMaxSize().padding(start = 24.dp, top = 48.dp)) { - Image(imageVector = vectorResource(id = R.drawable.ic_crane_drawer)) + Column( + modifier + .fillMaxSize() + .padding(start = 24.dp, top = 48.dp) + ) { + Image( + imageVector = vectorResource(R.drawable.ic_crane_drawer), + contentDescription = stringResource(R.string.cd_drawer) + ) for (screen in screens) { Spacer(Modifier.preferredHeight(24.dp)) Text(text = screen, style = MaterialTheme.typography.h4) diff --git a/Crane/app/src/main/java/androidx/compose/samples/crane/base/CraneTabs.kt b/Crane/app/src/main/java/androidx/compose/samples/crane/base/CraneTabs.kt index a993c0ae54..2091bb9ebd 100644 --- a/Crane/app/src/main/java/androidx/compose/samples/crane/base/CraneTabs.kt +++ b/Crane/app/src/main/java/androidx/compose/samples/crane/base/CraneTabs.kt @@ -37,6 +37,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.AmbientConfiguration +import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp import androidx.core.os.ConfigurationCompat @@ -51,13 +52,23 @@ fun CraneTabBar( // Separate Row as the children shouldn't have the padding Row(Modifier.padding(top = 8.dp)) { Image( - modifier = Modifier.padding(top = 8.dp).clickable(onClick = onMenuClicked), - imageVector = vectorResource(id = R.drawable.ic_menu) + modifier = Modifier + .padding(top = 8.dp) + .clickable(onClick = onMenuClicked), + imageVector = vectorResource(id = R.drawable.ic_menu), + contentDescription = stringResource(id = R.string.cd_menu) ) Spacer(Modifier.preferredWidth(8.dp)) - Image(imageVector = vectorResource(id = R.drawable.ic_crane_logo)) + Image( + imageVector = vectorResource(id = R.drawable.ic_crane_logo), + contentDescription = null + ) } - children(Modifier.weight(1f).align(Alignment.CenterVertically)) + children( + Modifier + .weight(1f) + .align(Alignment.CenterVertically) + ) } } @@ -82,7 +93,8 @@ fun CraneTabs( var textModifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp) if (selected) { textModifier = - Modifier.border(BorderStroke(2.dp, Color.White), RoundedCornerShape(16.dp)) + Modifier + .border(BorderStroke(2.dp, Color.White), RoundedCornerShape(16.dp)) .then(textModifier) } diff --git a/Crane/app/src/main/java/androidx/compose/samples/crane/base/ExploreSection.kt b/Crane/app/src/main/java/androidx/compose/samples/crane/base/ExploreSection.kt index 7522815bcd..adc17ede30 100644 --- a/Crane/app/src/main/java/androidx/compose/samples/crane/base/ExploreSection.kt +++ b/Crane/app/src/main/java/androidx/compose/samples/crane/base/ExploreSection.kt @@ -28,6 +28,7 @@ import androidx.compose.foundation.layout.preferredHeight import androidx.compose.foundation.layout.preferredSize import androidx.compose.foundation.layout.preferredWidth import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Divider import androidx.compose.material.MaterialTheme @@ -96,11 +97,13 @@ private fun ExploreItem( data = item.imageUrl, fadeIn = true, contentScale = ContentScale.Crop, + contentDescription = null, loading = { Box(Modifier.fillMaxSize()) { Image( modifier = Modifier.preferredSize(36.dp).align(Alignment.Center), - imageVector = vectorResource(id = R.drawable.ic_crane_logo) + imageVector = vectorResource(id = R.drawable.ic_crane_logo), + contentDescription = null ) } } diff --git a/Crane/app/src/main/java/androidx/compose/samples/crane/calendar/Calendar.kt b/Crane/app/src/main/java/androidx/compose/samples/crane/calendar/Calendar.kt index b049a7b383..4eb6347016 100644 --- a/Crane/app/src/main/java/androidx/compose/samples/crane/calendar/Calendar.kt +++ b/Crane/app/src/main/java/androidx/compose/samples/crane/calendar/Calendar.kt @@ -16,10 +16,8 @@ package androidx.compose.samples.crane.calendar -import androidx.compose.foundation.ScrollableColumn import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight @@ -31,6 +29,8 @@ import androidx.compose.foundation.layout.preferredHeightIn import androidx.compose.foundation.layout.preferredSize import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.material.Colors import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface @@ -63,41 +63,13 @@ fun Calendar( onDayClicked: (CalendarDay, CalendarMonth) -> Unit, modifier: Modifier = Modifier ) { - ScrollableColumn(modifier = modifier) { - Spacer(Modifier.preferredHeight(32.dp)) + LazyColumn(modifier) { + item { Spacer(Modifier.preferredHeight(32.dp)) } for (month in calendarYear) { - Month(month = month, onDayClicked = onDayClicked) - Spacer(Modifier.preferredHeight(32.dp)) - } - } -} - -@Composable -private fun Month( - modifier: Modifier = Modifier, - month: CalendarMonth, - onDayClicked: (CalendarDay, CalendarMonth) -> Unit -) { - Column(modifier = modifier) { - MonthHeader( - modifier = Modifier.padding(horizontal = 30.dp), - month = month.name, - year = month.year - ) - - // Expanding width and centering horizontally - val contentModifier = Modifier.fillMaxWidth().wrapContentWidth(Alignment.CenterHorizontally) - DaysOfWeek(modifier = contentModifier) - for (week in month.weeks.value) { - Week( - modifier = contentModifier, - week = week, - month = month, - onDayClicked = { day -> - onDayClicked(day, month) - } - ) - Spacer(Modifier.preferredHeight(8.dp)) + itemsCalendarMonth(month = month, onDayClicked = onDayClicked) + item { + Spacer(Modifier.preferredHeight(32.dp)) + } } } } @@ -128,7 +100,9 @@ private fun Week( val (leftFillColor, rightFillColor) = getLeftRightWeekColors(week, month) Row(modifier = modifier) { - val spaceModifiers = Modifier.weight(1f).preferredHeightIn(max = CELL_SIZE) + val spaceModifiers = Modifier + .weight(1f) + .preferredHeightIn(max = CELL_SIZE) Surface(modifier = spaceModifiers, color = leftFillColor) { Spacer(Modifier.fillMaxHeight()) } @@ -172,7 +146,9 @@ private fun Day( ) { DayStatusContainer(status = day.status) { Text( - modifier = Modifier.fillMaxSize().wrapContentSize(Alignment.Center), + modifier = Modifier + .fillMaxSize() + .wrapContentSize(Alignment.Center), text = day.value, style = MaterialTheme.typography.body1.copy(color = Color.White) ) @@ -227,6 +203,42 @@ private fun DayStatusContainer( } } +private fun LazyListScope.itemsCalendarMonth( + month: CalendarMonth, + onDayClicked: (CalendarDay, CalendarMonth) -> Unit +) { + item { + MonthHeader( + modifier = Modifier.padding(horizontal = 32.dp), + month = month.name, + year = month.year + ) + } + + // Expanding width and centering horizontally + val contentModifier = Modifier + .fillMaxWidth() + .wrapContentWidth(Alignment.CenterHorizontally) + item { + DaysOfWeek(modifier = contentModifier) + } + for (week in month.weeks.value) { + item { + Week( + modifier = contentModifier, + week = week, + month = month, + onDayClicked = { day -> + onDayClicked(day, month) + } + ) + } + item { + Spacer(Modifier.preferredHeight(8.dp)) + } + } +} + private fun DaySelectedStatus.color(theme: Colors): Color = when (this) { DaySelectedStatus.Selected -> theme.secondary else -> Color.Transparent diff --git a/Crane/app/src/main/java/androidx/compose/samples/crane/calendar/CalendarActivity.kt b/Crane/app/src/main/java/androidx/compose/samples/crane/calendar/CalendarActivity.kt index 47b164b416..19ea98363e 100644 --- a/Crane/app/src/main/java/androidx/compose/samples/crane/calendar/CalendarActivity.kt +++ b/Crane/app/src/main/java/androidx/compose/samples/crane/calendar/CalendarActivity.kt @@ -35,6 +35,7 @@ import androidx.compose.samples.crane.calendar.model.CalendarMonth import androidx.compose.samples.crane.calendar.model.DaySelected import androidx.compose.samples.crane.data.CalendarYear import androidx.compose.ui.platform.setContent +import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.viewinterop.viewModel import dagger.hilt.android.AndroidEntryPoint @@ -95,7 +96,10 @@ private fun CalendarContent( }, navigationIcon = { IconButton(onClick = { onBackPressed() }) { - Image(imageVector = vectorResource(id = R.drawable.ic_back)) + Image( + imageVector = vectorResource(R.drawable.ic_back), + contentDescription = stringResource(R.string.cd_back) + ) } }, backgroundColor = MaterialTheme.colors.primaryVariant diff --git a/Crane/app/src/main/java/androidx/compose/samples/crane/calendar/CalendarViewModel.kt b/Crane/app/src/main/java/androidx/compose/samples/crane/calendar/CalendarViewModel.kt index 1cee9585b6..226847561a 100644 --- a/Crane/app/src/main/java/androidx/compose/samples/crane/calendar/CalendarViewModel.kt +++ b/Crane/app/src/main/java/androidx/compose/samples/crane/calendar/CalendarViewModel.kt @@ -18,12 +18,14 @@ package androidx.compose.samples.crane.calendar import androidx.compose.samples.crane.calendar.model.DaySelected import androidx.compose.samples.crane.data.DatesRepository -import androidx.hilt.lifecycle.ViewModelInject import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch +import javax.inject.Inject -class CalendarViewModel @ViewModelInject constructor( +@HiltViewModel +class CalendarViewModel @Inject constructor( private val datesRepository: DatesRepository ) : ViewModel() { diff --git a/Crane/app/src/main/java/androidx/compose/samples/crane/details/DetailsActivity.kt b/Crane/app/src/main/java/androidx/compose/samples/crane/details/DetailsActivity.kt index 05b2c8acd4..c543a1ae82 100644 --- a/Crane/app/src/main/java/androidx/compose/samples/crane/details/DetailsActivity.kt +++ b/Crane/app/src/main/java/androidx/compose/samples/crane/details/DetailsActivity.kt @@ -46,6 +46,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.setContent import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView +import androidx.compose.ui.viewinterop.viewModel import com.google.android.libraries.maps.CameraUpdateFactory import com.google.android.libraries.maps.MapView import com.google.android.libraries.maps.model.LatLng @@ -76,7 +77,7 @@ data class DetailsActivityArg( class DetailsActivity : ComponentActivity() { @Inject - lateinit var viewModelFactory: DetailsViewModel.AssistedFactory + lateinit var viewModelFactory: DetailsViewModelFactory override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -101,10 +102,12 @@ class DetailsActivity : ComponentActivity() { @Composable fun DetailsScreen( args: DetailsActivityArg, - viewModelFactory: DetailsViewModel.AssistedFactory, + viewModelFactory: DetailsViewModelFactory, onErrorLoading: () -> Unit ) { - val viewModel: DetailsViewModel = viewModelFactory.create(args.cityName) + val viewModel: DetailsViewModel = viewModel( + factory = DetailsViewModel.provideFactory(viewModelFactory, args.cityName) + ) val cityDetailsResult = remember(viewModel) { viewModel.cityDetails } if (cityDetailsResult is Result.Success) { diff --git a/Crane/app/src/main/java/androidx/compose/samples/crane/details/DetailsViewModel.kt b/Crane/app/src/main/java/androidx/compose/samples/crane/details/DetailsViewModel.kt index c97ffe1407..3ff8cdff17 100644 --- a/Crane/app/src/main/java/androidx/compose/samples/crane/details/DetailsViewModel.kt +++ b/Crane/app/src/main/java/androidx/compose/samples/crane/details/DetailsViewModel.kt @@ -21,13 +21,9 @@ import androidx.compose.samples.crane.data.DestinationsRepository import androidx.compose.samples.crane.data.ExploreModel import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject -import com.squareup.inject.assisted.dagger2.AssistedModule -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ActivityRetainedComponent -import java.lang.IllegalArgumentException +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject class DetailsViewModel @AssistedInject constructor( private val destinationsRepository: DestinationsRepository, @@ -44,15 +40,10 @@ class DetailsViewModel @AssistedInject constructor( } } - @AssistedInject.Factory - interface AssistedFactory { - fun create(cityName: String): DetailsViewModel - } - @Suppress("UNCHECKED_CAST") companion object { fun provideFactory( - assistedFactory: AssistedFactory, + assistedFactory: DetailsViewModelFactory, cityName: String ): ViewModelProvider.Factory = object : ViewModelProvider.Factory { override fun create(modelClass: Class): T { @@ -62,7 +53,7 @@ class DetailsViewModel @AssistedInject constructor( } } -@InstallIn(ActivityRetainedComponent::class) -@AssistedModule -@Module(includes = [AssistedInject_AssistedInjectModule::class]) -interface AssistedInjectModule +@AssistedFactory +interface DetailsViewModelFactory { + fun create(cityName: String): DetailsViewModel +} diff --git a/Crane/app/src/main/java/androidx/compose/samples/crane/details/MapViewUtils.kt b/Crane/app/src/main/java/androidx/compose/samples/crane/details/MapViewUtils.kt index 90c76a1caf..e2a635c261 100644 --- a/Crane/app/src/main/java/androidx/compose/samples/crane/details/MapViewUtils.kt +++ b/Crane/app/src/main/java/androidx/compose/samples/crane/details/MapViewUtils.kt @@ -19,7 +19,7 @@ package androidx.compose.samples.crane.details import android.os.Bundle import androidx.annotation.FloatRange import androidx.compose.runtime.Composable -import androidx.compose.runtime.onCommit +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.remember import androidx.compose.samples.crane.R import androidx.compose.ui.platform.AmbientContext @@ -44,7 +44,7 @@ fun rememberMapViewWithLifecycle(): MapView { // Makes MapView follow the lifecycle of this composable val lifecycleObserver = rememberMapLifecycleObserver(mapView) val lifecycle = AmbientLifecycleOwner.current.lifecycle - onCommit(lifecycle) { + DisposableEffect(lifecycle) { lifecycle.addObserver(lifecycleObserver) onDispose { lifecycle.removeObserver(lifecycleObserver) diff --git a/Crane/app/src/main/java/androidx/compose/samples/crane/home/LandingScreen.kt b/Crane/app/src/main/java/androidx/compose/samples/crane/home/LandingScreen.kt index 61b1df3245..dd6ce35975 100644 --- a/Crane/app/src/main/java/androidx/compose/samples/crane/home/LandingScreen.kt +++ b/Crane/app/src/main/java/androidx/compose/samples/crane/home/LandingScreen.kt @@ -41,6 +41,6 @@ fun LandingScreen(modifier: Modifier = Modifier, onTimeout: () -> Unit) { delay(SplashWaitTime) currentOnTimeout() } - Image(imageVector = vectorResource(id = R.drawable.ic_crane_drawer)) + Image(vectorResource(id = R.drawable.ic_crane_drawer), contentDescription = null) } } diff --git a/Crane/app/src/main/java/androidx/compose/samples/crane/home/MainActivity.kt b/Crane/app/src/main/java/androidx/compose/samples/crane/home/MainActivity.kt index 1a32a7dc90..1b6925d1bd 100644 --- a/Crane/app/src/main/java/androidx/compose/samples/crane/home/MainActivity.kt +++ b/Crane/app/src/main/java/androidx/compose/samples/crane/home/MainActivity.kt @@ -19,22 +19,20 @@ package androidx.compose.samples.crane.home import android.os.Bundle import androidx.activity.ComponentActivity import androidx.annotation.VisibleForTesting -import androidx.compose.animation.DpPropKey -import androidx.compose.animation.core.FloatPropKey +import androidx.compose.animation.core.MutableTransitionState import androidx.compose.animation.core.Spring.StiffnessLow +import androidx.compose.animation.core.animateDp +import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.spring -import androidx.compose.animation.core.transitionDefinition import androidx.compose.animation.core.tween -import androidx.compose.animation.transition +import androidx.compose.animation.core.updateTransition import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.samples.crane.base.CraneScaffold import androidx.compose.samples.crane.calendar.launchCalendarActivity import androidx.compose.samples.crane.details.launchDetailsActivity @@ -64,16 +62,32 @@ class MainActivity : ComponentActivity() { @Composable fun MainScreen(onExploreItemClicked: OnExploreItemClicked, onDateSelectionClicked: () -> Unit) { CraneScaffold { - var splashShown by remember { mutableStateOf(SplashState.Shown) } - val transition = transition(splashTransitionDefinition, splashShown) + val transitionState = remember { MutableTransitionState(SplashState.Shown) } + val transition = updateTransition(transitionState) + val splashAlpha by transition.animateFloat( + transitionSpec = { tween(durationMillis = 100) } + ) { + if (it == SplashState.Shown) 1f else 0f + } + val contentAlpha by transition.animateFloat( + transitionSpec = { tween(durationMillis = 300) } + ) { + if (it == SplashState.Shown) 0f else 1f + } + val contentTopPadding by transition.animateDp( + transitionSpec = { spring(stiffness = StiffnessLow) } + ) { + if (it == SplashState.Shown) 100.dp else 0.dp + } + Box { LandingScreen( - modifier = Modifier.alpha(transition[splashAlphaKey]), - onTimeout = { splashShown = SplashState.Completed } + modifier = Modifier.alpha(splashAlpha), + onTimeout = { transitionState.targetState = SplashState.Completed } ) MainContent( - modifier = Modifier.alpha(transition[contentAlphaKey]), - topPadding = transition[contentTopPaddingKey], + modifier = Modifier.alpha(contentAlpha), + topPadding = contentTopPadding, onExploreItemClicked = onExploreItemClicked, onDateSelectionClicked = onDateSelectionClicked ) @@ -99,31 +113,3 @@ private fun MainContent( } enum class SplashState { Shown, Completed } - -private val splashAlphaKey = FloatPropKey("Splash alpha") -private val contentAlphaKey = FloatPropKey("Content alpha") -private val contentTopPaddingKey = DpPropKey("Top padding") - -private val splashTransitionDefinition = transitionDefinition { - state(SplashState.Shown) { - this[splashAlphaKey] = 1f - this[contentAlphaKey] = 0f - this[contentTopPaddingKey] = 100.dp - } - state(SplashState.Completed) { - this[splashAlphaKey] = 0f - this[contentAlphaKey] = 1f - this[contentTopPaddingKey] = 0.dp - } - transition { - splashAlphaKey using tween( - durationMillis = 100 - ) - contentAlphaKey using tween( - durationMillis = 300 - ) - contentTopPaddingKey using spring( - stiffness = StiffnessLow - ) - } -} diff --git a/Crane/app/src/main/java/androidx/compose/samples/crane/home/MainViewModel.kt b/Crane/app/src/main/java/androidx/compose/samples/crane/home/MainViewModel.kt index c9f6399e89..c6b2202a28 100644 --- a/Crane/app/src/main/java/androidx/compose/samples/crane/home/MainViewModel.kt +++ b/Crane/app/src/main/java/androidx/compose/samples/crane/home/MainViewModel.kt @@ -21,19 +21,21 @@ import androidx.compose.samples.crane.data.DatesRepository import androidx.compose.samples.crane.data.DestinationsRepository import androidx.compose.samples.crane.data.ExploreModel import androidx.compose.samples.crane.di.DefaultDispatcher -import androidx.hilt.lifecycle.ViewModelInject import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import javax.inject.Inject import kotlin.random.Random const val MAX_PEOPLE = 4 -class MainViewModel @ViewModelInject constructor( +@HiltViewModel +class MainViewModel @Inject constructor( private val destinationsRepository: DestinationsRepository, @DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher, datesRepository: DatesRepository diff --git a/Crane/app/src/main/java/androidx/compose/samples/crane/home/SearchUserInput.kt b/Crane/app/src/main/java/androidx/compose/samples/crane/home/SearchUserInput.kt index c38fc5e942..2fc65bc1ab 100644 --- a/Crane/app/src/main/java/androidx/compose/samples/crane/home/SearchUserInput.kt +++ b/Crane/app/src/main/java/androidx/compose/samples/crane/home/SearchUserInput.kt @@ -16,15 +16,16 @@ package androidx.compose.samples.crane.home -import androidx.compose.animation.ColorPropKey -import androidx.compose.animation.core.transitionDefinition +import androidx.compose.animation.animateColor +import androidx.compose.animation.core.MutableTransitionState import androidx.compose.animation.core.tween -import androidx.compose.animation.transition +import androidx.compose.animation.core.updateTransition import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -39,12 +40,14 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview +enum class PeopleUserInputAnimationState { Valid, Invalid } + class PeopleUserInputState { var people by mutableStateOf(1) private set - var animationState: PeopleUserInputAnimationState = Valid - private set + val animationState: MutableTransitionState = + MutableTransitionState(Valid) fun addPerson() { people = (people % (MAX_PEOPLE + 1)) + 1 @@ -56,7 +59,7 @@ class PeopleUserInputState { if (people > MAX_PEOPLE) Invalid else Valid - if (animationState != newState) animationState = newState + if (animationState.currentState != newState) animationState.targetState = newState } } @@ -67,17 +70,9 @@ fun PeopleUserInput( peopleState: PeopleUserInputState = remember { PeopleUserInputState() } ) { Column { - val validColor = MaterialTheme.colors.onSurface - val invalidColor = MaterialTheme.colors.secondary - val transitionDefinition = - remember(validColor, invalidColor) { - generateTransitionDefinition( - validColor, - invalidColor - ) - } - - val transition = transition(transitionDefinition, peopleState.animationState) + val transitionState = remember { peopleState.animationState } + val tint = tintPeopleUserInput(transitionState) + val people = peopleState.people CraneUserInput( modifier = Modifier.clickable { @@ -86,9 +81,9 @@ fun PeopleUserInput( }, text = if (people == 1) "$people Adult$titleSuffix" else "$people Adults$titleSuffix", vectorImageId = R.drawable.ic_person, - tint = transition[tintKey] + tint = tint.value ) - if (peopleState.animationState == Invalid) { + if (transitionState.targetState == Invalid) { Text( text = "Error: We don't support more than $MAX_PEOPLE people", style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary) @@ -122,6 +117,21 @@ fun DatesUserInput(datesSelected: String, onDateSelectionClicked: () -> Unit) { ) } +@Composable +private fun tintPeopleUserInput( + transitionState: MutableTransitionState +): State { + val validColor = MaterialTheme.colors.onSurface + val invalidColor = MaterialTheme.colors.secondary + + val transition = updateTransition(transitionState) + return transition.animateColor( + transitionSpec = { tween(durationMillis = 300) } + ) { + if (it == Valid) validColor else invalidColor + } +} + @Preview @Composable fun PeopleUserInputPreview() { @@ -129,29 +139,3 @@ fun PeopleUserInputPreview() { PeopleUserInput(onPeopleChanged = {}) } } - -private val tintKey = ColorPropKey(label = "tint") - -enum class PeopleUserInputAnimationState { Valid, Invalid } - -private fun generateTransitionDefinition( - validColor: Color, - invalidColor: Color -) = transitionDefinition { - state(Valid) { - this[tintKey] = validColor - } - state(Invalid) { - this[tintKey] = invalidColor - } - transition(fromState = Valid) { - tintKey using tween( - durationMillis = 300 - ) - } - transition(fromState = Invalid) { - tintKey using tween( - durationMillis = 300 - ) - } -} diff --git a/Crane/app/src/main/java/androidx/compose/samples/crane/ui/Typography.kt b/Crane/app/src/main/java/androidx/compose/samples/crane/ui/Typography.kt index d1e94676af..4e807b73b4 100644 --- a/Crane/app/src/main/java/androidx/compose/samples/crane/ui/Typography.kt +++ b/Crane/app/src/main/java/androidx/compose/samples/crane/ui/Typography.kt @@ -19,17 +19,17 @@ package androidx.compose.samples.crane.ui import androidx.compose.material.Typography import androidx.compose.samples.crane.R import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.font.font -import androidx.compose.ui.text.font.fontFamily import androidx.compose.ui.unit.sp -private val light = font(R.font.raleway_light, FontWeight.W300) -private val regular = font(R.font.raleway_regular, FontWeight.W400) -private val medium = font(R.font.raleway_medium, FontWeight.W500) -private val semibold = font(R.font.raleway_semibold, FontWeight.W600) +private val light = Font(R.font.raleway_light, FontWeight.W300) +private val regular = Font(R.font.raleway_regular, FontWeight.W400) +private val medium = Font(R.font.raleway_medium, FontWeight.W500) +private val semibold = Font(R.font.raleway_semibold, FontWeight.W600) -private val craneFontFamily = fontFamily(fonts = listOf(light, regular, medium, semibold)) +private val craneFontFamily = FontFamily(fonts = listOf(light, regular, medium, semibold)) val captionTextStyle = TextStyle( fontFamily = craneFontFamily, diff --git a/Crane/app/src/main/res/values/strings.xml b/Crane/app/src/main/res/values/strings.xml index 58d56fb370..71db4f4073 100644 --- a/Crane/app/src/main/res/values/strings.xml +++ b/Crane/app/src/main/res/values/strings.xml @@ -16,4 +16,9 @@ --> Crane + + Menu + Back + Loading + Open drawer \ No newline at end of file diff --git a/Crane/buildSrc/src/main/java/com/example/crane/buildsrc/Dependencies.kt b/Crane/buildSrc/src/main/java/com/example/crane/buildsrc/Dependencies.kt index 7b6e11b6fa..f6e484ce85 100644 --- a/Crane/buildSrc/src/main/java/com/example/crane/buildsrc/Dependencies.kt +++ b/Crane/buildSrc/src/main/java/com/example/crane/buildsrc/Dependencies.kt @@ -21,7 +21,7 @@ object Versions { } object Libs { - const val androidGradlePlugin = "com.android.tools.build:gradle:7.0.0-alpha04" + const val androidGradlePlugin = "com.android.tools.build:gradle:7.0.0-alpha05" const val ktLint = "com.pinterest:ktlint:${Versions.ktLint}" object GoogleMaps { @@ -30,12 +30,12 @@ object Libs { } object Accompanist { - private const val version = "0.4.2" + private const val version = "0.5.0" const val coil = "dev.chrisbanes.accompanist:accompanist-coil:$version" } object Kotlin { - private const val version = "1.4.21" + private const val version = "1.4.21-2" const val stdlib = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$version" const val gradlePlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:$version" const val extensions = "org.jetbrains.kotlin:kotlin-android-extensions:$version" @@ -50,7 +50,7 @@ object Libs { object AndroidX { object Compose { const val snapshot = "" - private const val version = "1.0.0-alpha10" + private const val version = "1.0.0-alpha11" const val runtime = "androidx.compose.runtime:runtime:$version" const val runtimeLivedata = "androidx.compose.runtime:runtime-livedata:$version" @@ -80,32 +80,18 @@ object Libs { } object Hilt { - private const val version = "2.30.1-alpha" + private const val version = "2.31.2-alpha" const val gradlePlugin = "com.google.dagger:hilt-android-gradle-plugin:$version" const val android = "com.google.dagger:hilt-android:$version" const val compiler = "com.google.dagger:hilt-compiler:$version" const val testing = "com.google.dagger:hilt-android-testing:$version" - - object AndroidX { - private const val version = "1.0.0-alpha02" - - const val compiler = "androidx.hilt:hilt-compiler:$version" - const val viewModel = "androidx.hilt:hilt-lifecycle-viewmodel:$version" - } } object JUnit { private const val version = "4.13" const val junit = "junit:junit:$version" } - - object AssistedInjection { - private const val version = "0.5.2" - - const val dagger = "com.squareup.inject:assisted-inject-annotations-dagger2:$version" - const val processor = "com.squareup.inject:assisted-inject-processor-dagger2:$version" - } } object Urls { diff --git a/Crane/gradle/wrapper/gradle-wrapper.properties b/Crane/gradle/wrapper/gradle-wrapper.properties index adcbca81af..5dd41103e7 100644 --- a/Crane/gradle/wrapper/gradle-wrapper.properties +++ b/Crane/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Fri Oct 23 09:30:32 CEST 2020 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-rc-1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.1-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/JetNews/app/build.gradle b/JetNews/app/build.gradle index 5b5a909c2b..a722982473 100644 --- a/JetNews/app/build.gradle +++ b/JetNews/app/build.gradle @@ -65,7 +65,6 @@ android { } composeOptions { - kotlinCompilerVersion kotlin_version kotlinCompilerExtensionVersion compose_version } diff --git a/JetNews/app/src/androidTest/java/com/example/jetnews/TestHelper.kt b/JetNews/app/src/androidTest/java/com/example/jetnews/TestHelper.kt index e704c7bcb3..4742db8717 100644 --- a/JetNews/app/src/androidTest/java/com/example/jetnews/TestHelper.kt +++ b/JetNews/app/src/androidTest/java/com/example/jetnews/TestHelper.kt @@ -18,7 +18,7 @@ package com.example.jetnews import android.content.Context import androidx.compose.runtime.remember -import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.junit4.ComposeContentTestRule import androidx.lifecycle.SavedStateHandle import com.example.jetnews.ui.JetnewsApp import com.example.jetnews.ui.NavigationViewModel @@ -26,7 +26,7 @@ import com.example.jetnews.ui.NavigationViewModel /** * Launches the app from a test context */ -fun ComposeTestRule.launchJetNewsApp(context: Context) { +fun ComposeContentTestRule.launchJetNewsApp(context: Context) { setContent { JetnewsApp( TestAppContainer(context), diff --git a/JetNews/app/src/main/java/com/example/jetnews/ui/AppDrawer.kt b/JetNews/app/src/main/java/com/example/jetnews/ui/AppDrawer.kt index 778095d2a6..1de019ca94 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/ui/AppDrawer.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/ui/AppDrawer.kt @@ -40,6 +40,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -82,11 +83,13 @@ private fun JetNewsLogo(modifier: Modifier = Modifier) { Row(modifier = modifier) { Image( imageVector = vectorResource(R.drawable.ic_jetnews_logo), + contentDescription = null, // decorative colorFilter = ColorFilter.tint(MaterialTheme.colors.primary) ) Spacer(Modifier.preferredWidth(8.dp)) Image( imageVector = vectorResource(R.drawable.ic_jetnews_wordmark), + contentDescription = stringResource(R.string.app_name), colorFilter = ColorFilter.tint(MaterialTheme.colors.onSurface) ) } @@ -136,6 +139,7 @@ private fun DrawerButton( ) { Image( imageVector = icon, + contentDescription = null, // decorative colorFilter = ColorFilter.tint(textIconColor), alpha = imageAlpha ) diff --git a/JetNews/app/src/main/java/com/example/jetnews/ui/SwipeToRefresh.kt b/JetNews/app/src/main/java/com/example/jetnews/ui/SwipeToRefresh.kt index 5e04ec06fa..3f5e0cea83 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/ui/SwipeToRefresh.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/ui/SwipeToRefresh.kt @@ -24,7 +24,7 @@ import androidx.compose.material.SwipeableState import androidx.compose.material.rememberSwipeableState import androidx.compose.material.swipeable import androidx.compose.runtime.Composable -import androidx.compose.runtime.onCommit +import androidx.compose.runtime.DisposableEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset @@ -82,8 +82,9 @@ fun SwipeToRefreshLayout( // TODO (https://issuetracker.google.com/issues/164113834): This state->event trampoline is a // workaround for a bug in the SwipableState API. Currently, state.value is a duplicated // source of truth of refreshingState. - onCommit(refreshingState) { + DisposableEffect(refreshingState) { state.animateTo(refreshingState) + onDispose {} } } } diff --git a/JetNews/app/src/main/java/com/example/jetnews/ui/article/ArticleScreen.kt b/JetNews/app/src/main/java/com/example/jetnews/ui/article/ArticleScreen.kt index 996705b543..f13be05592 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/ui/article/ArticleScreen.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/ui/article/ArticleScreen.kt @@ -46,6 +46,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.AmbientContext +import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -134,7 +135,10 @@ fun ArticleScreen( }, navigationIcon = { IconButton(onClick = onBack) { - Icon(Icons.Filled.ArrowBack) + Icon( + imageVector = Icons.Filled.ArrowBack, + contentDescription = stringResource(R.string.cd_navigate_up) + ) } } ) @@ -177,7 +181,10 @@ private fun BottomBar( .fillMaxWidth() ) { IconButton(onClick = onUnimplementedAction) { - Icon(Icons.Filled.FavoriteBorder) + Icon( + imageVector = Icons.Filled.FavoriteBorder, + contentDescription = stringResource(R.string.cd_add_to_favorites) + ) } BookmarkButton( isBookmarked = isFavorite, @@ -185,11 +192,17 @@ private fun BottomBar( ) val context = AmbientContext.current IconButton(onClick = { sharePost(post, context) }) { - Icon(Icons.Filled.Share) + Icon( + imageVector = Icons.Filled.Share, + contentDescription = stringResource(R.string.cd_share) + ) } Spacer(modifier = Modifier.weight(1f)) IconButton(onClick = onUnimplementedAction) { - Icon(vectorResource(R.drawable.ic_text_settings)) + Icon( + imageVector = vectorResource(R.drawable.ic_text_settings), + contentDescription = stringResource(R.string.cd_text_settings) + ) } } } diff --git a/JetNews/app/src/main/java/com/example/jetnews/ui/article/PostContent.kt b/JetNews/app/src/main/java/com/example/jetnews/ui/article/PostContent.kt index 312117d289..da15bc0c67 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/ui/article/PostContent.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/ui/article/PostContent.kt @@ -17,7 +17,6 @@ package com.example.jetnews.ui.article import androidx.compose.foundation.Image -import androidx.compose.foundation.ScrollableColumn import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -29,6 +28,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.preferredHeight import androidx.compose.foundation.layout.preferredSize import androidx.compose.foundation.layout.preferredWidth +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.AmbientContentAlpha import androidx.compose.material.AmbientContentColor @@ -75,27 +76,39 @@ private val defaultSpacerSize = 16.dp @Composable fun PostContent(post: Post, modifier: Modifier = Modifier) { - ScrollableColumn( + LazyColumn( modifier = modifier.padding(horizontal = defaultSpacerSize) ) { - Spacer(Modifier.preferredHeight(defaultSpacerSize)) - PostHeaderImage(post) - Text(text = post.title, style = MaterialTheme.typography.h4) - Spacer(Modifier.preferredHeight(8.dp)) + item { + Spacer(Modifier.preferredHeight(defaultSpacerSize)) + PostHeaderImage(post) + } + item { + Text(text = post.title, style = MaterialTheme.typography.h4) + Spacer(Modifier.preferredHeight(8.dp)) + } post.subtitle?.let { subtitle -> - Providers(AmbientContentAlpha provides ContentAlpha.medium) { - Text( - text = subtitle, - style = MaterialTheme.typography.body2, - lineHeight = 20.sp - ) + item { + Providers(AmbientContentAlpha provides ContentAlpha.medium) { + Text( + text = subtitle, + style = MaterialTheme.typography.body2, + lineHeight = 20.sp + ) + } + Spacer(Modifier.preferredHeight(defaultSpacerSize)) } - Spacer(Modifier.preferredHeight(defaultSpacerSize)) } - PostMetadata(post.metadata) - Spacer(Modifier.preferredHeight(24.dp)) - PostContents(post.paragraphs) - Spacer(Modifier.preferredHeight(48.dp)) + item { + PostMetadata(post.metadata) + Spacer(Modifier.preferredHeight(24.dp)) + } + items(post.paragraphs) { + Paragraph(paragraph = it) + } + item { + Spacer(Modifier.preferredHeight(48.dp)) + } } } @@ -106,7 +119,12 @@ private fun PostHeaderImage(post: Post) { .heightIn(min = 180.dp) .fillMaxWidth() .clip(shape = MaterialTheme.shapes.medium) - Image(image, imageModifier, contentScale = ContentScale.Crop) + Image( + bitmap = image, + contentDescription = null, // decorative + modifier = imageModifier, + contentScale = ContentScale.Crop + ) Spacer(Modifier.preferredHeight(defaultSpacerSize)) } } @@ -117,6 +135,7 @@ private fun PostMetadata(metadata: Metadata) { Row { Image( imageVector = Icons.Filled.AccountCircle, + contentDescription = null, // decorative modifier = Modifier.preferredSize(40.dp), colorFilter = ColorFilter.tint(AmbientContentColor.current), contentScale = ContentScale.Fit @@ -139,13 +158,6 @@ private fun PostMetadata(metadata: Metadata) { } } -@Composable -private fun PostContents(paragraphs: List) { - paragraphs.forEach { - Paragraph(paragraph = it) - } -} - @Composable private fun Paragraph(paragraph: Paragraph) { val (textStyle, paragraphStyle, trailingPadding) = paragraph.type.getTextAndParagraphStyle() diff --git a/JetNews/app/src/main/java/com/example/jetnews/ui/home/HomeScreen.kt b/JetNews/app/src/main/java/com/example/jetnews/ui/home/HomeScreen.kt index 8f03dfe466..7cc8b36297 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/ui/home/HomeScreen.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/ui/home/HomeScreen.kt @@ -16,8 +16,6 @@ package com.example.jetnews.ui.home -import androidx.compose.foundation.ScrollableColumn -import androidx.compose.foundation.ScrollableRow import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -25,6 +23,9 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.preferredSize import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Divider @@ -171,7 +172,10 @@ fun HomeScreen( title = { Text(text = title) }, navigationIcon = { IconButton(onClick = { scaffoldState.drawerState.open() }) { - Icon(vectorResource(R.drawable.ic_jetnews_logo)) + Icon( + imageVector = vectorResource(R.drawable.ic_jetnews_logo), + contentDescription = stringResource(R.string.cd_open_navigation_drawer) + ) } } ) @@ -292,11 +296,11 @@ private fun PostList( val postsPopular = posts.subList(2, 7) val postsHistory = posts.subList(7, 10) - ScrollableColumn(modifier = modifier) { - PostListTopSection(postTop, navigateTo) - PostListSimpleSection(postsSimple, navigateTo, favorites, onToggleFavorite) - PostListPopularSection(postsPopular, navigateTo) - PostListHistorySection(postsHistory, navigateTo) + LazyColumn(modifier = modifier) { + item { PostListTopSection(postTop, navigateTo) } + item { PostListSimpleSection(postsSimple, navigateTo, favorites, onToggleFavorite) } + item { PostListPopularSection(postsPopular, navigateTo) } + item { PostListHistorySection(postsHistory, navigateTo) } } } @@ -305,7 +309,11 @@ private fun PostList( */ @Composable private fun FullScreenLoading() { - Box(modifier = Modifier.fillMaxSize().wrapContentSize(Alignment.Center)) { + Box( + modifier = Modifier + .fillMaxSize() + .wrapContentSize(Alignment.Center) + ) { CircularProgressIndicator() } } @@ -374,8 +382,8 @@ private fun PostListPopularSection( style = MaterialTheme.typography.subtitle1 ) - ScrollableRow(modifier = Modifier.padding(end = 16.dp)) { - posts.forEach { post -> + LazyRow(modifier = Modifier.padding(end = 16.dp)) { + items(posts) { post -> PostCardPopular(post, navigateTo, Modifier.padding(start = 16.dp, bottom = 16.dp)) } } diff --git a/JetNews/app/src/main/java/com/example/jetnews/ui/home/PostCardTop.kt b/JetNews/app/src/main/java/com/example/jetnews/ui/home/PostCardTop.kt index 135d2c3f86..e6b174beb8 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/ui/home/PostCardTop.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/ui/home/PostCardTop.kt @@ -51,7 +51,12 @@ fun PostCardTop(post: Post, modifier: Modifier = Modifier) { .heightIn(min = 180.dp) .fillMaxWidth() .clip(shape = MaterialTheme.shapes.medium) - Image(image, modifier = imageModifier, contentScale = ContentScale.Crop) + Image( + bitmap = image, + contentDescription = null, // decorative + modifier = imageModifier, + contentScale = ContentScale.Crop + ) } Spacer(Modifier.preferredHeight(16.dp)) diff --git a/JetNews/app/src/main/java/com/example/jetnews/ui/home/PostCardYourNetwork.kt b/JetNews/app/src/main/java/com/example/jetnews/ui/home/PostCardYourNetwork.kt index 94a7651c87..6d6e647bb0 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/ui/home/PostCardYourNetwork.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/ui/home/PostCardYourNetwork.kt @@ -55,6 +55,7 @@ fun PostCardPopular( Image( bitmap = image, + contentDescription = null, // decorative contentScale = ContentScale.Crop, modifier = Modifier .preferredHeight(100.dp) diff --git a/JetNews/app/src/main/java/com/example/jetnews/ui/home/PostCards.kt b/JetNews/app/src/main/java/com/example/jetnews/ui/home/PostCards.kt index f71d0ed2d3..f8ae2e63c4 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/ui/home/PostCards.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/ui/home/PostCards.kt @@ -38,6 +38,7 @@ import androidx.compose.runtime.Providers import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.imageResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.example.jetnews.R @@ -72,6 +73,7 @@ fun PostImage(post: Post, modifier: Modifier = Modifier) { Image( bitmap = image, + contentDescription = null, // decorative modifier = modifier .preferredSize(40.dp, 40.dp) .clip(MaterialTheme.shapes.small) @@ -91,7 +93,8 @@ fun PostCardSimple( onToggleFavorite: () -> Unit ) { Row( - modifier = Modifier.clickable(onClick = { navigateTo(Screen.Article(post.id)) }) + modifier = Modifier + .clickable(onClick = { navigateTo(Screen.Article(post.id)) }) .padding(16.dp) ) { PostImage(post, Modifier.padding(end = 16.dp)) @@ -109,7 +112,8 @@ fun PostCardSimple( @Composable fun PostCardHistory(post: Post, navigateTo: (Screen) -> Unit) { Row( - Modifier.clickable(onClick = { navigateTo(Screen.Article(post.id)) }) + Modifier + .clickable(onClick = { navigateTo(Screen.Article(post.id)) }) .padding(16.dp) ) { PostImage( @@ -130,7 +134,10 @@ fun PostCardHistory(post: Post, navigateTo: (Screen) -> Unit) { ) } Providers(AmbientContentAlpha provides ContentAlpha.medium) { - Icon(Icons.Filled.MoreVert) + Icon( + imageVector = Icons.Filled.MoreVert, + contentDescription = stringResource(R.string.cd_more_actions) + ) } } } @@ -147,9 +154,15 @@ fun BookmarkButton( modifier = modifier ) { if (isBookmarked) { - Icon(imageVector = Icons.Filled.Bookmark) + Icon( + imageVector = Icons.Filled.Bookmark, + contentDescription = stringResource(R.string.cd_bookmark) + ) } else { - Icon(imageVector = Icons.Filled.BookmarkBorder) + Icon( + imageVector = Icons.Filled.BookmarkBorder, + contentDescription = stringResource(R.string.cd_bookmark) + ) } } } diff --git a/JetNews/app/src/main/java/com/example/jetnews/ui/interests/InterestsScreen.kt b/JetNews/app/src/main/java/com/example/jetnews/ui/interests/InterestsScreen.kt index d1a4c64c28..890f661a3c 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/ui/interests/InterestsScreen.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/ui/interests/InterestsScreen.kt @@ -17,13 +17,14 @@ package com.example.jetnews.ui.interests import androidx.compose.foundation.Image -import androidx.compose.foundation.ScrollableColumn import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.preferredSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.selection.toggleable import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Divider @@ -48,6 +49,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.imageResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -181,7 +183,10 @@ fun InterestsScreen( title = { Text("Interests") }, navigationIcon = { IconButton(onClick = { scaffoldState.drawerState.open() }) { - Icon(vectorResource(R.drawable.ic_jetnews_logo)) + Icon( + imageVector = vectorResource(R.drawable.ic_jetnews_logo), + contentDescription = stringResource(R.string.cd_open_navigation_drawer) + ) } } ) @@ -287,8 +292,8 @@ private fun TabWithTopics( selectedTopics: Set, onTopicSelect: (String) -> Unit ) { - ScrollableColumn(modifier = Modifier.padding(top = 16.dp)) { - topics.forEach { topic -> + LazyColumn(modifier = Modifier.padding(top = 16.dp)) { + items(topics) { topic -> TopicItem( topic, selected = selectedTopics.contains(topic) @@ -311,14 +316,16 @@ private fun TabWithSections( selectedTopics: Set, onTopicSelect: (TopicSelection) -> Unit ) { - ScrollableColumn { + LazyColumn { sections.forEach { (section, topics) -> - Text( - text = section, - modifier = Modifier.padding(16.dp), - style = MaterialTheme.typography.subtitle1 - ) - topics.forEach { topic -> + item { + Text( + text = section, + modifier = Modifier.padding(16.dp), + style = MaterialTheme.typography.subtitle1 + ) + } + items(topics) { topic -> TopicItem( itemTitle = topic, selected = selectedTopics.contains(TopicSelection(section, topic)) @@ -348,8 +355,9 @@ private fun TopicItem(itemTitle: String, selected: Boolean, onToggle: () -> Unit .padding(horizontal = 16.dp) ) { Image( - image, - Modifier + bitmap = image, + contentDescription = null, // decorative + modifier = Modifier .align(Alignment.CenterVertically) .preferredSize(56.dp, 56.dp) .clip(RoundedCornerShape(4.dp)) diff --git a/JetNews/app/src/main/java/com/example/jetnews/ui/interests/SelectTopicButton.kt b/JetNews/app/src/main/java/com/example/jetnews/ui/interests/SelectTopicButton.kt index d9c44a1adf..fcc711529b 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/ui/interests/SelectTopicButton.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/ui/interests/SelectTopicButton.kt @@ -47,7 +47,10 @@ fun SelectTopicButton( shape = CircleShape, modifier = modifier.preferredSize(36.dp, 36.dp) ) { - Icon(icon) + Icon( + imageVector = icon, + contentDescription = null // toggleable at higher level + ) } } diff --git a/JetNews/app/src/main/java/com/example/jetnews/ui/theme/Type.kt b/JetNews/app/src/main/java/com/example/jetnews/ui/theme/Type.kt index 2191d27ccc..627127aab0 100644 --- a/JetNews/app/src/main/java/com/example/jetnews/ui/theme/Type.kt +++ b/JetNews/app/src/main/java/com/example/jetnews/ui/theme/Type.kt @@ -18,23 +18,23 @@ package com.example.jetnews.ui.theme import androidx.compose.material.Typography import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.font import androidx.compose.ui.text.font.fontFamily import androidx.compose.ui.unit.sp import com.example.jetnews.R -private val Montserrat = fontFamily( - font(R.font.montserrat_regular), - font(R.font.montserrat_medium, FontWeight.W500), - font(R.font.montserrat_semibold, FontWeight.W600) +private val Montserrat = FontFamily( + Font(R.font.montserrat_regular), + Font(R.font.montserrat_medium, FontWeight.W500), + Font(R.font.montserrat_semibold, FontWeight.W600) ) -private val Domine = fontFamily( - fonts = listOf( - font(R.font.domine_regular), - font(R.font.domine_bold, FontWeight.Bold) - ) +private val Domine = FontFamily( + Font(R.font.domine_regular), + Font(R.font.domine_bold, FontWeight.Bold) ) val JetnewsTypography = Typography( diff --git a/JetNews/app/src/main/res/values/strings.xml b/JetNews/app/src/main/res/values/strings.xml index e89be4036d..190659d62d 100644 --- a/JetNews/app/src/main/res/values/strings.xml +++ b/JetNews/app/src/main/res/values/strings.xml @@ -16,4 +16,11 @@ Jetnews Can\'t update latest news Retry + Navigate up + Add to favorites + Share + Text settings + Open navigation drawer + More actions + Bookmark diff --git a/JetNews/build.gradle b/JetNews/build.gradle index 39246c6c94..a3942c5430 100644 --- a/JetNews/build.gradle +++ b/JetNews/build.gradle @@ -15,8 +15,8 @@ */ buildscript { - ext.kotlin_version = '1.4.21' - ext.compose_version = '1.0.0-alpha10' + ext.kotlin_version = '1.4.21-2' + ext.compose_version = '1.0.0-alpha11' ext.coroutines_version = '1.4.2' repositories { @@ -25,7 +25,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.0.0-alpha04' + classpath 'com.android.tools.build:gradle:7.0.0-alpha05' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/JetNews/gradle/wrapper/gradle-wrapper.properties b/JetNews/gradle/wrapper/gradle-wrapper.properties index cfb40e4707..1b1a27664c 100644 --- a/JetNews/gradle/wrapper/gradle-wrapper.properties +++ b/JetNews/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Tue Oct 27 16:21:59 PDT 2020 +#Thu Jan 28 15:37:57 CET 2021 distributionBase=GRADLE_USER_HOME -distributionUrl=https://services.gradle.org/distributions/gradle-6.8-rc-1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/Jetcaster/app/build.gradle b/Jetcaster/app/build.gradle index f0e3b8bf28..c1d1468fdd 100644 --- a/Jetcaster/app/build.gradle +++ b/Jetcaster/app/build.gradle @@ -88,7 +88,6 @@ android { } composeOptions { - kotlinCompilerVersion Libs.Kotlin.version kotlinCompilerExtensionVersion Libs.AndroidX.Compose.version } } diff --git a/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/Home.kt b/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/Home.kt index 545dfbe261..81111916ce 100644 --- a/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/Home.kt +++ b/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/Home.kt @@ -112,11 +112,15 @@ fun HomeAppBar( title = { Row { Image( - imageVector = vectorResource(R.drawable.ic_logo) + imageVector = vectorResource(R.drawable.ic_logo), + contentDescription = null ) Icon( imageVector = vectorResource(R.drawable.ic_text_logo), - modifier = Modifier.padding(start = 4.dp).preferredHeightIn(max = 24.dp) + contentDescription = stringResource(R.string.app_name), + modifier = Modifier + .padding(start = 4.dp) + .preferredHeightIn(max = 24.dp) ) } }, @@ -126,12 +130,18 @@ fun HomeAppBar( IconButton( onClick = { /* TODO: Open search */ } ) { - Icon(Icons.Filled.Search) + Icon( + imageVector = Icons.Filled.Search, + contentDescription = stringResource(R.string.cd_search) + ) } IconButton( onClick = { /* TODO: Open account? */ } ) { - Icon(Icons.Default.AccountCircle) + Icon( + imageVector = Icons.Default.AccountCircle, + contentDescription = stringResource(R.string.cd_account) + ) } } }, @@ -183,7 +193,8 @@ fun HomeContent( } Column( - modifier = Modifier.fillMaxWidth() + modifier = Modifier + .fillMaxWidth() .verticalGradientScrim( color = MaterialTheme.colors.primary.copy(alpha = 0.38f), startYPercentage = 1f, @@ -193,7 +204,12 @@ fun HomeContent( val appBarColor = MaterialTheme.colors.surface.copy(alpha = 0.87f) // Draw a scrim over the status bar which matches the app bar - Spacer(Modifier.background(appBarColor).fillMaxWidth().statusBarsHeight()) + Spacer( + Modifier + .background(appBarColor) + .fillMaxWidth() + .statusBarsHeight() + ) HomeAppBar( backgroundColor = appBarColor, @@ -235,7 +251,11 @@ fun HomeContent( // TODO } HomeCategory.Discover -> { - Discover(Modifier.fillMaxWidth().weight(1f)) + Discover( + Modifier + .fillMaxWidth() + .weight(1f) + ) } } } @@ -284,7 +304,8 @@ fun HomeCategoryTabIndicator( color: Color = MaterialTheme.colors.onSurface ) { Spacer( - modifier.padding(horizontal = 24.dp) + modifier + .padding(horizontal = 24.dp) .preferredHeight(4.dp) .background(color, RoundedCornerShape(topLeftPercent = 100, topRightPercent = 100)) ) @@ -311,7 +332,9 @@ fun FollowedPodcasts( podcastImageUrl = podcast.imageUrl, lastEpisodeDate = lastEpisodeDate, onUnfollowedClick = { onPodcastUnfollowed(podcast.uri) }, - modifier = Modifier.padding(4.dp).fillMaxHeight() + modifier = Modifier + .padding(4.dp) + .fillMaxHeight() ) } } @@ -335,6 +358,7 @@ private fun FollowedPodcastCarouselItem( if (podcastImageUrl != null) { CoilImage( data = podcastImageUrl, + contentDescription = null, contentScale = ContentScale.Crop, loading = { /* TODO do something better here */ }, modifier = Modifier @@ -357,7 +381,8 @@ private fun FollowedPodcastCarouselItem( style = MaterialTheme.typography.caption, maxLines = 1, overflow = TextOverflow.Ellipsis, - modifier = Modifier.padding(top = 8.dp) + modifier = Modifier + .padding(top = 8.dp) .align(Alignment.CenterHorizontally) ) } diff --git a/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/category/PodcastCategory.kt b/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/category/PodcastCategory.kt index 7855024834..72a0ab3eac 100644 --- a/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/category/PodcastCategory.kt +++ b/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/category/PodcastCategory.kt @@ -17,6 +17,7 @@ package com.example.jetcaster.ui.home.category import androidx.compose.foundation.Image +import androidx.compose.foundation.InteractionState import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -32,6 +33,8 @@ import androidx.compose.foundation.layout.preferredSize import androidx.compose.foundation.layout.preferredWidth import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material.AmbientContentAlpha import androidx.compose.material.AmbientContentColor import androidx.compose.material.ContentAlpha @@ -49,6 +52,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Providers import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -143,10 +147,12 @@ fun EpisodeListItem( // If we have an image Url, we can show it using [CoilImage] CoilImage( data = podcast.imageUrl, + contentDescription = null, fadeIn = true, contentScale = ContentScale.Crop, loading = { /* TODO do something better here */ }, - modifier = Modifier.preferredSize(56.dp) + modifier = Modifier + .preferredSize(56.dp) .clip(MaterialTheme.shapes.medium) .constrainAs(image) { end.linkTo(parent.end, 16.dp) @@ -207,12 +213,14 @@ fun EpisodeListItem( Image( imageVector = Icons.Rounded.PlayCircleFilled, + contentDescription = stringResource(R.string.cd_play), contentScale = ContentScale.Fit, colorFilter = ColorFilter.tint(AmbientContentColor.current), modifier = Modifier - .clickable(indication = rememberRipple(bounded = false, radius = 24.dp)) { - /* TODO */ - } + .clickable( + interactionState = remember { InteractionState() }, + indication = rememberRipple(bounded = false, radius = 24.dp) + ) { /* TODO */ } .preferredSize(36.dp) .constrainAs(playIcon) { start.linkTo(parent.start, Keyline1) @@ -258,7 +266,10 @@ fun EpisodeListItem( centerVerticallyTo(playIcon) } ) { - Icon(Icons.Default.PlaylistAdd) + Icon( + imageVector = Icons.Default.PlaylistAdd, + contentDescription = stringResource(R.string.cd_add) + ) } IconButton( @@ -268,7 +279,10 @@ fun EpisodeListItem( centerVerticallyTo(playIcon) } ) { - Icon(Icons.Default.MoreVert) + Icon( + imageVector = Icons.Default.MoreVert, + contentDescription = stringResource(R.string.cd_more) + ) } } } @@ -317,10 +331,13 @@ private fun TopPodcastRowItem( if (podcastImageUrl != null) { CoilImage( data = podcastImageUrl, + contentDescription = null, fadeIn = true, contentScale = ContentScale.Crop, loading = { /* TODO do something better here */ }, - modifier = Modifier.fillMaxSize().clip(MaterialTheme.shapes.medium) + modifier = Modifier + .fillMaxSize() + .clip(MaterialTheme.shapes.medium) ) } @@ -336,7 +353,9 @@ private fun TopPodcastRowItem( style = MaterialTheme.typography.body2, maxLines = 2, overflow = TextOverflow.Ellipsis, - modifier = Modifier.padding(top = 8.dp).fillMaxWidth() + modifier = Modifier + .padding(top = 8.dp) + .fillMaxWidth() ) } } diff --git a/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/discover/Discover.kt b/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/discover/Discover.kt index 9ff89abf3d..6589720671 100644 --- a/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/discover/Discover.kt +++ b/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/discover/Discover.kt @@ -14,6 +14,8 @@ * limitations under the License. */ +@file:Suppress("DEPRECATION") + package com.example.jetcaster.ui.home.discover import androidx.compose.animation.core.FloatPropKey @@ -35,11 +37,11 @@ import androidx.compose.material.Tab import androidx.compose.material.TabPosition import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.emptyContent import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.onCommit import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier @@ -92,7 +94,8 @@ fun Discover( reverse = reverseTransition, offsetPx = transitionOffset ), - modifier = Modifier.fillMaxWidth() + modifier = Modifier + .fillMaxWidth() .weight(1f) ) { category, transitionState -> /** @@ -100,7 +103,8 @@ fun Discover( */ PodcastCategory( categoryId = category.id, - modifier = Modifier.fillMaxSize() + modifier = Modifier + .fillMaxSize() .graphicsLayer { translationX = transitionState[Offset] alpha = transitionState[Alpha] @@ -108,9 +112,10 @@ fun Discover( ) } - onCommit(selectedCategory) { + DisposableEffect(selectedCategory) { // Update our tracking of the previously selected category previousSelectedCategory = selectedCategory + onDispose {} } } } else { @@ -185,6 +190,7 @@ private fun getChoiceChipTransitionDefinition( offsetPx: Float, reverse: Boolean = false ): TransitionDefinition = remember(reverse, offsetPx, duration) { + transitionDefinition { state(ItemTransitionState.Visible) { this[Alpha] = 1f diff --git a/Jetcaster/app/src/main/java/com/example/jetcaster/ui/theme/Type.kt b/Jetcaster/app/src/main/java/com/example/jetcaster/ui/theme/Type.kt index 2f425dfb39..1c407e52cb 100644 --- a/Jetcaster/app/src/main/java/com/example/jetcaster/ui/theme/Type.kt +++ b/Jetcaster/app/src/main/java/com/example/jetcaster/ui/theme/Type.kt @@ -18,17 +18,17 @@ package com.example.jetcaster.ui.theme import androidx.compose.material.Typography import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.font.font -import androidx.compose.ui.text.font.fontFamily import androidx.compose.ui.unit.sp import com.example.jetcaster.R -private val Montserrat = fontFamily( - font(R.font.montserrat_light, FontWeight.Light), - font(R.font.montserrat_regular, FontWeight.Normal), - font(R.font.montserrat_medium, FontWeight.Medium), - font(R.font.montserrat_semibold, FontWeight.SemiBold) +private val Montserrat = FontFamily( + Font(R.font.montserrat_light, FontWeight.Light), + Font(R.font.montserrat_regular, FontWeight.Normal), + Font(R.font.montserrat_medium, FontWeight.Medium), + Font(R.font.montserrat_semibold, FontWeight.SemiBold) ) val JetcasterTypography = Typography( diff --git a/Jetcaster/app/src/main/java/com/example/jetcaster/util/Buttons.kt b/Jetcaster/app/src/main/java/com/example/jetcaster/util/Buttons.kt index 5f861d497d..e4adfbd074 100644 --- a/Jetcaster/app/src/main/java/com/example/jetcaster/util/Buttons.kt +++ b/Jetcaster/app/src/main/java/com/example/jetcaster/util/Buttons.kt @@ -16,8 +16,8 @@ package com.example.jetcaster.util -import androidx.compose.animation.animateAsState -import androidx.compose.animation.core.animateAsState +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.background import androidx.compose.foundation.layout.padding import androidx.compose.material.AmbientContentColor @@ -32,7 +32,9 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import com.example.jetcaster.R @Composable fun ToggleFollowPodcastIconButton( @@ -50,7 +52,11 @@ fun ToggleFollowPodcastIconButton( isFollowed -> Icons.Default.Check else -> Icons.Default.Add }, - tint = animateAsState( + contentDescription = when { + isFollowed -> stringResource(R.string.cd_unfollow) + else -> stringResource(R.string.cd_follow) + }, + tint = animateColorAsState( when { isFollowed -> AmbientContentColor.current else -> Color.Black.copy(alpha = ContentAlpha.high) @@ -58,11 +64,11 @@ fun ToggleFollowPodcastIconButton( ).value, modifier = Modifier .shadow( - elevation = animateAsState(if (isFollowed) 0.dp else 1.dp).value, + elevation = animateDpAsState(if (isFollowed) 0.dp else 1.dp).value, shape = MaterialTheme.shapes.small ) .background( - color = animateAsState( + color = animateColorAsState( when { isFollowed -> MaterialTheme.colors.surface.copy(0.38f) else -> Color.White diff --git a/Jetcaster/app/src/main/java/com/example/jetcaster/util/DynamicTheming.kt b/Jetcaster/app/src/main/java/com/example/jetcaster/util/DynamicTheming.kt index ee5fc3a44d..6ee7e25a92 100644 --- a/Jetcaster/app/src/main/java/com/example/jetcaster/util/DynamicTheming.kt +++ b/Jetcaster/app/src/main/java/com/example/jetcaster/util/DynamicTheming.kt @@ -18,7 +18,7 @@ package com.example.jetcaster.util import android.content.Context import androidx.collection.LruCache -import androidx.compose.animation.animateAsState +import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.Spring import androidx.compose.animation.core.spring import androidx.compose.material.MaterialTheme @@ -61,11 +61,11 @@ fun DynamicThemePrimaryColorsFromImage( content: @Composable () -> Unit ) { val colors = MaterialTheme.colors.copy( - primary = animateAsState( + primary = animateColorAsState( dominantColorState.color, spring(stiffness = Spring.StiffnessLow) ).value, - onPrimary = animateAsState( + onPrimary = animateColorAsState( dominantColorState.onColor, spring(stiffness = Spring.StiffnessLow) ).value diff --git a/Jetcaster/app/src/main/java/com/example/jetcaster/util/ItemSwitcher.kt b/Jetcaster/app/src/main/java/com/example/jetcaster/util/ItemSwitcher.kt index 9363247efe..78223a8635 100644 --- a/Jetcaster/app/src/main/java/com/example/jetcaster/util/ItemSwitcher.kt +++ b/Jetcaster/app/src/main/java/com/example/jetcaster/util/ItemSwitcher.kt @@ -14,6 +14,8 @@ * limitations under the License. */ +@file:Suppress("DEPRECATION") + package com.example.jetcaster.util import androidx.compose.animation.asDisposableClock @@ -22,9 +24,9 @@ import androidx.compose.animation.core.TransitionState import androidx.compose.animation.core.createAnimation import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.invalidate import androidx.compose.runtime.key -import androidx.compose.runtime.onCommit import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.AmbientAnimationClock @@ -72,7 +74,7 @@ fun ItemSwitcher( ) } - onCommit(visible) { + DisposableEffect(visible) { anim.onStateChangeFinished = { _ -> if (key == state.current) { // leave only the current in the list @@ -87,6 +89,8 @@ fun ItemSwitcher( else -> ItemTransitionState.BecomingNotVisible } anim.toState(targetState) + + onDispose { } } children(anim) @@ -118,7 +122,7 @@ private class ItemTransitionInnerState { private data class ItemTransitionItem( val key: T, - val content: ItemTransitionContent + @Suppress("TYPEALIAS_EXPANSION_DEPRECATION") val content: ItemTransitionContent ) private typealias ItemTransitionContent = @Composable (children: @Composable (TransitionState) -> Unit) -> Unit diff --git a/Jetcaster/app/src/main/res/values/strings.xml b/Jetcaster/app/src/main/res/values/strings.xml index fe66f85a95..a9c036bfd0 100644 --- a/Jetcaster/app/src/main/res/values/strings.xml +++ b/Jetcaster/app/src/main/res/values/strings.xml @@ -40,4 +40,12 @@ %1$s • %2$d mins + Search + Account + Add + More + Play + Unfollow + Follow + diff --git a/Jetcaster/buildSrc/src/main/java/com/example/jetcaster/buildsrc/dependencies.kt b/Jetcaster/buildSrc/src/main/java/com/example/jetcaster/buildsrc/dependencies.kt index e84f7b2568..26989d14ae 100644 --- a/Jetcaster/buildSrc/src/main/java/com/example/jetcaster/buildsrc/dependencies.kt +++ b/Jetcaster/buildSrc/src/main/java/com/example/jetcaster/buildsrc/dependencies.kt @@ -21,7 +21,7 @@ object Versions { } object Libs { - const val androidGradlePlugin = "com.android.tools.build:gradle:7.0.0-alpha04" + const val androidGradlePlugin = "com.android.tools.build:gradle:7.0.0-alpha05" const val jdkDesugar = "com.android.tools:desugar_jdk_libs:1.0.9" const val junit = "junit:junit:4.13" @@ -29,13 +29,13 @@ object Libs { const val material = "com.google.android.material:material:1.1.0" object Accompanist { - private const val version = "0.4.2" + private const val version = "0.5.0" const val coil = "dev.chrisbanes.accompanist:accompanist-coil:$version" const val insets = "dev.chrisbanes.accompanist:accompanist-insets:$version" } object Kotlin { - private const val version = "1.4.21" + private const val version = "1.4.21-2" const val stdlib = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$version" const val gradlePlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:$version" const val extensions = "org.jetbrains.kotlin:kotlin-android-extensions:$version" @@ -62,7 +62,7 @@ object Libs { object Compose { private const val snapshot = "" - private const val version = "1.0.0-alpha10" + private const val version = "1.0.0-alpha11" @get:JvmStatic val snapshotUrl: String diff --git a/Jetcaster/gradle/wrapper/gradle-wrapper.properties b/Jetcaster/gradle/wrapper/gradle-wrapper.properties index 99338ec765..fa84b588f8 100644 --- a/Jetcaster/gradle/wrapper/gradle-wrapper.properties +++ b/Jetcaster/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Dec 14 17:32:45 GMT 2020 +#Fri Jan 29 13:40:13 CET 2021 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-rc-1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.1-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/Jetchat/app/build.gradle b/Jetchat/app/build.gradle index 57d599b340..db63367f4e 100644 --- a/Jetchat/app/build.gradle +++ b/Jetchat/app/build.gradle @@ -78,7 +78,6 @@ android { } composeOptions { - kotlinCompilerVersion Libs.Kotlin.version kotlinCompilerExtensionVersion Libs.AndroidX.Compose.version } diff --git a/Jetchat/app/src/androidTest/java/com/example/compose/jetchat/ConversationTest.kt b/Jetchat/app/src/androidTest/java/com/example/compose/jetchat/ConversationTest.kt index db5df6971c..2d31d6bd14 100644 --- a/Jetchat/app/src/androidTest/java/com/example/compose/jetchat/ConversationTest.kt +++ b/Jetchat/app/src/androidTest/java/com/example/compose/jetchat/ConversationTest.kt @@ -16,7 +16,6 @@ package com.example.compose.jetchat -import androidx.activity.ComponentActivity import androidx.compose.runtime.Providers import androidx.compose.runtime.collectAsState import androidx.compose.ui.geometry.Offset @@ -29,7 +28,6 @@ import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performGesture import androidx.compose.ui.test.swipe -import androidx.compose.ui.unit.milliseconds import com.example.compose.jetchat.conversation.AmbientBackPressedDispatcher import com.example.compose.jetchat.conversation.ConversationContent import com.example.compose.jetchat.conversation.ConversationTestTag @@ -50,32 +48,26 @@ class ConversationTest { @get:Rule val composeTestRule = createAndroidComposeRule() - // Note that keeping these references is only safe if the activity is not recreated. - // See: https://issuetracker.google.com/160862278 - private lateinit var activity: ComponentActivity - private val themeIsDark = MutableStateFlow(false) @Before fun setUp() { - composeTestRule.activityRule.scenario.onActivity { newActivity -> - activity = newActivity - // Provide empty insets. We can modify this value as necessary - val windowInsets = WindowInsets() - - // Launch the conversation screen - composeTestRule.setContent { - Providers( - AmbientBackPressedDispatcher provides newActivity.onBackPressedDispatcher, - AmbientWindowInsets provides windowInsets - ) { - JetchatTheme(isDarkTheme = themeIsDark.collectAsState(false).value) { - ConversationContent( - uiState = exampleUiState, - navigateToProfile = { }, - onNavIconPressed = { } - ) - } + // Provide empty insets. We can modify this value as necessary + val windowInsets = WindowInsets() + + // Launch the conversation screen + composeTestRule.setContent { + val onBackPressedDispatcher = composeTestRule.activity.onBackPressedDispatcher + Providers( + AmbientBackPressedDispatcher provides onBackPressedDispatcher, + AmbientWindowInsets provides windowInsets + ) { + JetchatTheme(isDarkTheme = themeIsDark.collectAsState(false).value) { + ConversationContent( + uiState = exampleUiState, + navigateToProfile = { }, + onNavIconPressed = { } + ) } } } @@ -95,7 +87,7 @@ class ConversationTest { this.swipe( start = this.center, end = Offset(this.center.x, this.center.y + 500), - duration = 200.milliseconds + durationMillis = 200 ) } // Check that the jump to bottom button is shown @@ -109,7 +101,7 @@ class ConversationTest { this.swipe( start = this.center, end = Offset(this.center.x, this.center.y + 500), - duration = 200.milliseconds + durationMillis = 200 ) } // Snap scroll to the bottom @@ -126,7 +118,7 @@ class ConversationTest { this.swipe( start = this.center, end = Offset(this.center.x, this.center.y + 500), - duration = 200.milliseconds + durationMillis = 200 ) } // Second, snap to bottom @@ -146,7 +138,7 @@ class ConversationTest { this.swipe( start = this.center, end = Offset(this.center.x, this.center.y + 500), - duration = 200.milliseconds + durationMillis = 200 ) } @@ -161,10 +153,12 @@ class ConversationTest { } private fun findJumpToBottom() = - composeTestRule.onNodeWithText(activity.getString(R.string.jumpBottom)) + composeTestRule.onNodeWithText(composeTestRule.activity.getString(R.string.jumpBottom)) private fun openEmojiSelector() = composeTestRule - .onNodeWithContentDescription(activity.getString(R.string.emoji_selector_bt_desc)) + .onNodeWithContentDescription( + composeTestRule.activity.getString(R.string.emoji_selector_bt_desc) + ) .performClick() } diff --git a/Jetchat/app/src/androidTest/java/com/example/compose/jetchat/NavigationTest.kt b/Jetchat/app/src/androidTest/java/com/example/compose/jetchat/NavigationTest.kt index 6e1267c80e..e935bb4f16 100644 --- a/Jetchat/app/src/androidTest/java/com/example/compose/jetchat/NavigationTest.kt +++ b/Jetchat/app/src/androidTest/java/com/example/compose/jetchat/NavigationTest.kt @@ -16,23 +16,14 @@ package com.example.compose.jetchat -import android.view.View -import androidx.activity.ComponentActivity -import androidx.compose.runtime.Providers import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.navigation.NavController -import androidx.navigation.Navigation +import androidx.navigation.findNavController import androidx.test.espresso.Espresso -import com.example.compose.jetchat.conversation.AmbientBackPressedDispatcher -import com.example.compose.jetchat.conversation.ConversationContent -import com.example.compose.jetchat.data.exampleUiState -import com.example.compose.jetchat.theme.JetchatTheme -import dev.chrisbanes.accompanist.insets.AmbientWindowInsets -import dev.chrisbanes.accompanist.insets.WindowInsets import org.junit.Assert.assertEquals -import org.junit.Before +import org.junit.Ignore import org.junit.Rule import org.junit.Test @@ -44,49 +35,16 @@ class NavigationTest { @get:Rule val composeTestRule = createAndroidComposeRule() - // Note that keeping these references is only safe if the activity is not recreated. - // See: https://issuetracker.google.com/160862278 - private lateinit var navController: NavController - private lateinit var activity: ComponentActivity - - @Before - fun setUp() { - composeTestRule.activityRule.scenario.onActivity { newActivity: NavActivity -> - // Store a reference to the activity. Don't do this if the activity is recreated! - activity = newActivity - val navHostFragment: View = newActivity.findViewById(R.id.nav_host_fragment) - // Store a reference to the navigation controller. - navController = Navigation.findNavController(navHostFragment) - } - - // Provide empty insets. We can modify this value as necessary - val windowInsets = WindowInsets() - - // Start the app - composeTestRule.setContent { - Providers( - AmbientBackPressedDispatcher provides activity.onBackPressedDispatcher, - AmbientWindowInsets provides windowInsets, - ) { - JetchatTheme { - ConversationContent( - uiState = exampleUiState, - navigateToProfile = { }, - onNavIconPressed = { } - ) - } - } - } - } - @Test fun app_launches() { // Check app launches at the correct destination - assertEquals(navController.currentDestination?.id, R.id.nav_home) + assertEquals(getNavController().currentDestination?.id, R.id.nav_home) } @Test + @Ignore("Issue with keyboard sync https://issuetracker.google.com/169235317") fun profileScreen_back_conversationScreen() { + val navController = getNavController() // Navigate to profile composeTestRule.runOnUiThread { navController.navigate(R.id.nav_profile) @@ -95,7 +53,7 @@ class NavigationTest { assertEquals(navController.currentDestination?.id, R.id.nav_profile) // Extra UI check composeTestRule - .onNodeWithText(activity.getString(R.string.textfield_hint)) + .onNodeWithText(composeTestRule.activity.getString(R.string.display_name)) .assertIsDisplayed() // Press back @@ -104,4 +62,8 @@ class NavigationTest { // Check that we're home assertEquals(navController.currentDestination?.id, R.id.nav_home) } + + private fun getNavController(): NavController { + return composeTestRule.activity.findNavController(R.id.nav_host_fragment) + } } diff --git a/Jetchat/app/src/androidTest/java/com/example/compose/jetchat/UserInputTest.kt b/Jetchat/app/src/androidTest/java/com/example/compose/jetchat/UserInputTest.kt index 771372d709..b59ef75636 100644 --- a/Jetchat/app/src/androidTest/java/com/example/compose/jetchat/UserInputTest.kt +++ b/Jetchat/app/src/androidTest/java/com/example/compose/jetchat/UserInputTest.kt @@ -27,6 +27,7 @@ import androidx.compose.ui.test.hasAnyAncestor import androidx.compose.ui.test.hasContentDescription import androidx.compose.ui.test.hasSetTextAction import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onAllNodesWithContentDescription import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick @@ -40,6 +41,7 @@ import com.example.compose.jetchat.theme.JetchatTheme import dev.chrisbanes.accompanist.insets.AmbientWindowInsets import dev.chrisbanes.accompanist.insets.WindowInsets import org.junit.Before +import org.junit.Ignore import org.junit.Rule import org.junit.Test @@ -49,38 +51,36 @@ import org.junit.Test class UserInputTest { @get:Rule - val composeTestRule = createAndroidComposeRule() + val composeTestRule = createAndroidComposeRule() - // Note that keeping these references is only safe if the activity is not recreated. - // See: https://issuetracker.google.com/160862278 - private lateinit var activity: ComponentActivity + private val activity by lazy { composeTestRule.activity } @Before fun setUp() { - composeTestRule.activityRule.scenario.onActivity { newActivity -> - activity = newActivity - // Provide empty insets. We can modify this value as necessary - val windowInsets = WindowInsets() - - // Launch the conversation screen - composeTestRule.setContent { - Providers( - AmbientBackPressedDispatcher provides activity.onBackPressedDispatcher, - AmbientWindowInsets provides windowInsets, - ) { - JetchatTheme { - ConversationContent( - uiState = exampleUiState, - navigateToProfile = { }, - onNavIconPressed = { } - ) - } + + // Provide empty insets. We can modify this value as necessary + val windowInsets = WindowInsets() + + // Launch the conversation screen + val onBackPressedDispatcher = composeTestRule.activity.onBackPressedDispatcher + composeTestRule.setContent { + Providers( + AmbientBackPressedDispatcher provides onBackPressedDispatcher, + AmbientWindowInsets provides windowInsets, + ) { + JetchatTheme { + ConversationContent( + uiState = exampleUiState, + navigateToProfile = { }, + onNavIconPressed = { } + ) } } } } @Test + @Ignore("Issue with keyboard sync https://issuetracker.google.com/169235317") fun emojiSelector_isClosedWithBack() { // Open emoji selector openEmojiSelector() @@ -91,6 +91,15 @@ class UserInputTest { .assertExists() // Press back button Espresso.pressBack() + + // TODO: Workaround for synchronization issue with "back" + // https://issuetracker.google.com/169235317 + composeTestRule.waitUntil(timeoutMillis = 10_000) { + composeTestRule + .onAllNodesWithContentDescription(activity.getString(R.string.emoji_selector_desc)) + .fetchSemanticsNodes().isEmpty() + } + // Check the emoji selector is not displayed assertEmojiSelectorDoesNotExist() } @@ -125,6 +134,7 @@ class UserInputTest { } @Test + @Ignore("Flaky due to https://issuetracker.google.com/169235317") fun sendButton_enableToggles() { // Given an initial state where there's no text in the textfield, // check that the send button is disabled. diff --git a/Jetchat/app/src/debug/AndroidManifest.xml b/Jetchat/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000000..f453702527 --- /dev/null +++ b/Jetchat/app/src/debug/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/Jetchat/app/src/main/java/com/example/compose/jetchat/components/AnimatingFabContent.kt b/Jetchat/app/src/main/java/com/example/compose/jetchat/components/AnimatingFabContent.kt index af6ab7b481..629fbf8cba 100644 --- a/Jetchat/app/src/main/java/com/example/compose/jetchat/components/AnimatingFabContent.kt +++ b/Jetchat/app/src/main/java/com/example/compose/jetchat/components/AnimatingFabContent.kt @@ -17,14 +17,13 @@ package com.example.compose.jetchat.components import androidx.compose.animation.core.FastOutSlowInEasing -import androidx.compose.animation.core.FloatPropKey import androidx.compose.animation.core.LinearEasing -import androidx.compose.animation.core.transitionDefinition +import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.tween -import androidx.compose.animation.transition +import androidx.compose.animation.core.updateTransition import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.layout.Layout @@ -43,17 +42,57 @@ fun AnimatingFabContent( extended: Boolean = true ) { val currentState = if (extended) ExpandableFabStates.Extended else ExpandableFabStates.Collapsed - val transitionDefinition = remember { fabTransitionDefinition() } - val transition = transition( - definition = transitionDefinition, - toState = currentState - ) + val transition = updateTransition(currentState) + + val textOpacity by transition.animateFloat( + transitionSpec = { + if (targetState == ExpandableFabStates.Collapsed) { + tween( + easing = LinearEasing, + durationMillis = (transitionDuration / 12f * 5).roundToInt() // 5 / 12 frames + ) + } else { + tween( + easing = LinearEasing, + delayMillis = (transitionDuration / 3f).roundToInt(), // 4 / 12 frames + durationMillis = (transitionDuration / 12f * 5).roundToInt() // 5 / 12 frames + ) + } + } + ) { progress -> + if (progress == ExpandableFabStates.Collapsed) { + 0f + } else { + 1f + } + } + val fabWidthFactor by transition.animateFloat( + transitionSpec = { + if (targetState == ExpandableFabStates.Collapsed) { + tween( + easing = FastOutSlowInEasing, + durationMillis = transitionDuration + ) + } else { + tween( + easing = FastOutSlowInEasing, + durationMillis = transitionDuration + ) + } + } + ) { progress -> + if (progress == ExpandableFabStates.Collapsed) { + 0f + } else { + 1f + } + } // Using functions instead of Floats here can improve performance, preventing recompositions. IconAndTextRow( icon, text, - { transition[TextOpacity] }, - { transition[FabWidthFactor] }, + { textOpacity }, + { fabWidthFactor }, modifier = modifier ) } @@ -106,47 +145,6 @@ private fun IconAndTextRow( } } -private val FabWidthFactor = FloatPropKey("Width") -private val TextOpacity = FloatPropKey("Text Opacity") - private enum class ExpandableFabStates { Collapsed, Extended } -@Suppress("RemoveExplicitTypeArguments") -private fun fabTransitionDefinition(duration: Int = 200) = - transitionDefinition { - state(ExpandableFabStates.Collapsed) { - this[FabWidthFactor] = 0f - this[TextOpacity] = 0f - } - state(ExpandableFabStates.Extended) { - this[FabWidthFactor] = 1f - this[TextOpacity] = 1f - } - transition( - fromState = ExpandableFabStates.Extended, - toState = ExpandableFabStates.Collapsed - ) { - TextOpacity using tween( - easing = LinearEasing, - durationMillis = (duration / 12f * 5).roundToInt() // 5 out of 12 frames - ) - FabWidthFactor using tween( - easing = FastOutSlowInEasing, - durationMillis = duration - ) - } - transition( - fromState = ExpandableFabStates.Collapsed, - toState = ExpandableFabStates.Extended - ) { - TextOpacity using tween( - easing = LinearEasing, - delayMillis = (duration / 3f).roundToInt(), // 4 out of 12 frames - durationMillis = (duration / 12f * 5).roundToInt() // 5 out of 12 frames - ) - FabWidthFactor using tween( - easing = FastOutSlowInEasing, - durationMillis = duration - ) - } - } +private const val transitionDuration = 200 diff --git a/Jetchat/app/src/main/java/com/example/compose/jetchat/components/JetchatAppBar.kt b/Jetchat/app/src/main/java/com/example/compose/jetchat/components/JetchatAppBar.kt index d71730f7ad..44dfb341ae 100644 --- a/Jetchat/app/src/main/java/com/example/compose/jetchat/components/JetchatAppBar.kt +++ b/Jetchat/app/src/main/java/com/example/compose/jetchat/components/JetchatAppBar.kt @@ -30,6 +30,7 @@ import androidx.compose.material.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -64,6 +65,7 @@ fun JetchatAppBar( navigationIcon = { Image( imageVector = vectorResource(id = R.drawable.ic_jetchat), + contentDescription = stringResource(id = R.string.back), modifier = Modifier .clickable(onClick = onNavIconPressed) .padding(horizontal = 16.dp) diff --git a/Jetchat/app/src/main/java/com/example/compose/jetchat/components/JetchatDrawer.kt b/Jetchat/app/src/main/java/com/example/compose/jetchat/components/JetchatDrawer.kt index 48ee88998d..0586c3da38 100644 --- a/Jetchat/app/src/main/java/com/example/compose/jetchat/components/JetchatDrawer.kt +++ b/Jetchat/app/src/main/java/com/example/compose/jetchat/components/JetchatDrawer.kt @@ -76,10 +76,12 @@ private fun DrawerHeader() { Row(modifier = Modifier.padding(16.dp), verticalAlignment = CenterVertically) { Image( vectorResource(id = R.drawable.ic_jetchat), + contentDescription = null, modifier = Modifier.preferredSize(24.dp) ) Image( vectorResource(id = R.drawable.jetchat_logo), + contentDescription = null, modifier = Modifier.padding(start = 8.dp) ) } @@ -116,7 +118,8 @@ private fun ChatItem(text: String, selected: Boolean, onChatClicked: () -> Unit) Icon( vectorResource(id = R.drawable.ic_jetchat), tint = iconTint, - modifier = Modifier.padding(8.dp) + modifier = Modifier.padding(8.dp), + contentDescription = null ) Providers(AmbientContentAlpha provides ContentAlpha.medium) { Text( @@ -146,7 +149,8 @@ private fun ProfileItem(text: String, @DrawableRes profilePic: Int?, onProfileCl Image( imageResource(id = profilePic), modifier = widthPaddingModifier.then(Modifier.clip(CircleShape)), - contentScale = ContentScale.Crop + contentScale = ContentScale.Crop, + contentDescription = null ) } else { Spacer(modifier = widthPaddingModifier) diff --git a/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/Conversation.kt b/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/Conversation.kt index 6b0479f776..2832266569 100644 --- a/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/Conversation.kt +++ b/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/Conversation.kt @@ -19,7 +19,6 @@ package com.example.compose.jetchat.conversation import androidx.compose.foundation.ClickableText import androidx.compose.foundation.Image import androidx.compose.foundation.ScrollState -import androidx.compose.foundation.ScrollableColumn import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box @@ -38,6 +37,7 @@ import androidx.compose.foundation.layout.preferredWidth import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll import androidx.compose.material.AmbientContentAlpha import androidx.compose.material.AmbientContentColor import androidx.compose.material.ContentAlpha @@ -51,6 +51,7 @@ import androidx.compose.material.icons.outlined.Info import androidx.compose.material.icons.outlined.Search import androidx.compose.runtime.Composable import androidx.compose.runtime.Providers +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -72,6 +73,7 @@ import com.example.compose.jetchat.theme.JetchatTheme import com.example.compose.jetchat.theme.elevatedSurface import dev.chrisbanes.accompanist.insets.navigationBarsWithImePadding import dev.chrisbanes.accompanist.insets.statusBarsPadding +import kotlinx.coroutines.launch /** * Entry point for a conversation screen. @@ -162,7 +164,8 @@ fun ChannelNameBar( modifier = Modifier .clickable(onClick = {}) // TODO: Show not implemented dialog. .padding(horizontal = 12.dp, vertical = 16.dp) - .preferredHeight(24.dp) + .preferredHeight(24.dp), + contentDescription = stringResource(id = R.string.search) ) // Info icon Icon( @@ -170,7 +173,8 @@ fun ChannelNameBar( modifier = Modifier .clickable(onClick = {}) // TODO: Show not implemented dialog. .padding(horizontal = 12.dp, vertical = 16.dp) - .preferredHeight(24.dp) + .preferredHeight(24.dp), + contentDescription = stringResource(id = R.string.info) ) } } @@ -186,14 +190,15 @@ fun Messages( scrollState: ScrollState, modifier: Modifier = Modifier ) { + + val scope = rememberCoroutineScope() Box(modifier = modifier) { - ScrollableColumn( - scrollState = scrollState, - reverseScrollDirection = true, + Column( modifier = Modifier .testTag(ConversationTestTag) .fillMaxWidth() + .verticalScroll(scrollState, reverseScrolling = true) ) { val authorMe = stringResource(id = R.string.author_me) Spacer(modifier = Modifier.preferredHeight(64.dp)) @@ -234,7 +239,9 @@ fun Messages( // Only show if the scroller is not at the bottom enabled = jumpToBottomButtonEnabled, onClicked = { - scrollState.smoothScrollTo(BottomScrollState) + scope.launch { + scrollState.smoothScrollTo(BottomScrollState) + } }, modifier = Modifier.align(Alignment.BottomCenter) ) @@ -275,7 +282,8 @@ fun Message( .clip(CircleShape) .align(Alignment.Top), bitmap = image, - contentScale = ContentScale.Crop + contentScale = ContentScale.Crop, + contentDescription = null, ) } else { // Space under avatar @@ -341,7 +349,11 @@ private val LastChatBubbleShape = RoundedCornerShape(0.dp, 8.dp, 8.dp, 8.dp) @Composable fun DayHeader(dayString: String) { - Row(modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp).preferredHeight(16.dp)) { + Row( + modifier = Modifier + .padding(vertical = 8.dp, horizontal = 16.dp) + .preferredHeight(16.dp) + ) { DayHeaderLine() Providers(AmbientContentAlpha provides ContentAlpha.medium) { Text( @@ -357,7 +369,9 @@ fun DayHeader(dayString: String) { @Composable private fun RowScope.DayHeaderLine() { Divider( - modifier = Modifier.weight(1f).align(Alignment.CenterVertically), + modifier = Modifier + .weight(1f) + .align(Alignment.CenterVertically), color = MaterialTheme.colors.onSurface.copy(alpha = 0.12f) ) } @@ -389,7 +403,8 @@ fun ChatItemBubble( Image( bitmap = imageResource(it), contentScale = ContentScale.Fit, - modifier = Modifier.preferredSize(160.dp) + modifier = Modifier.preferredSize(160.dp), + contentDescription = stringResource(id = R.string.attached_image) ) } } diff --git a/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/JumpToBottom.kt b/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/JumpToBottom.kt index 61155211ab..be9d9c3f46 100644 --- a/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/JumpToBottom.kt +++ b/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/JumpToBottom.kt @@ -16,9 +16,8 @@ package com.example.compose.jetchat.conversation -import androidx.compose.animation.DpPropKey -import androidx.compose.animation.core.transitionDefinition -import androidx.compose.animation.transition +import androidx.compose.animation.core.animateDp +import androidx.compose.animation.core.updateTransition import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.preferredHeight import androidx.compose.material.ExtendedFloatingActionButton @@ -28,23 +27,13 @@ import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowDownward import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.example.compose.jetchat.R -private val bottomOffset = DpPropKey("Bottom Offset") - -private val definition = transitionDefinition { - state(Visibility.GONE) { - this[bottomOffset] = (-32).dp - } - state(Visibility.VISIBLE) { - this[bottomOffset] = 32.dp - } -} - private enum class Visibility { VISIBLE, GONE @@ -60,16 +49,21 @@ fun JumpToBottom( modifier: Modifier = Modifier ) { // Show Jump to Bottom button - val transition = transition( - definition = definition, - toState = if (enabled) Visibility.VISIBLE else Visibility.GONE - ) - if (transition[bottomOffset] > 0.dp) { + val transition = updateTransition(if (enabled) Visibility.VISIBLE else Visibility.GONE) + val bottomOffset by transition.animateDp() { + if (it == Visibility.GONE) { + (-32).dp + } else { + 32.dp + } + } + if (bottomOffset > 0.dp) { ExtendedFloatingActionButton( icon = { Icon( imageVector = Icons.Filled.ArrowDownward, - modifier = Modifier.preferredHeight(18.dp) + modifier = Modifier.preferredHeight(18.dp), + contentDescription = null ) }, text = { @@ -79,7 +73,7 @@ fun JumpToBottom( backgroundColor = MaterialTheme.colors.surface, contentColor = MaterialTheme.colors.primary, modifier = modifier - .offset(x = 0.dp, y = -transition[bottomOffset]) + .offset(x = 0.dp, y = -bottomOffset) .preferredHeight(36.dp) ) } diff --git a/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/MessageFormatter.kt b/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/MessageFormatter.kt index fdd1b9dc16..e0782c95fa 100644 --- a/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/MessageFormatter.kt +++ b/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/MessageFormatter.kt @@ -23,7 +23,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.StringAnnotation -import androidx.compose.ui.text.annotatedString import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontStyle diff --git a/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/UserInput.kt b/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/UserInput.kt index 6b4f3ba4d1..3c7920c090 100644 --- a/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/UserInput.kt +++ b/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/UserInput.kt @@ -22,7 +22,6 @@ import androidx.compose.animation.fadeIn import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ScrollState -import androidx.compose.foundation.ScrollableRow import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -40,6 +39,7 @@ import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll import androidx.compose.material.AmbientContentAlpha import androidx.compose.material.AmbientContentColor import androidx.compose.material.AmbientTextStyle @@ -60,11 +60,13 @@ import androidx.compose.material.icons.outlined.InsertPhoto import androidx.compose.material.icons.outlined.Mood import androidx.compose.material.icons.outlined.Place import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.Providers +import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.onCommit import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.savedinstancestate.savedInstanceState import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -95,6 +97,7 @@ import com.example.compose.jetchat.FunctionalityNotAvailablePopup import com.example.compose.jetchat.R import com.example.compose.jetchat.theme.compositedOnSurface import com.example.compose.jetchat.theme.elevatedSurface +import kotlinx.coroutines.launch enum class InputSelector { NONE, @@ -123,6 +126,7 @@ fun UserInput( scrollState: ScrollState, modifier: Modifier = Modifier, ) { + val scope = rememberCoroutineScope() var currentInputSelector by savedInstanceState { InputSelector.NONE } val dismissKeyboard = { currentInputSelector = InputSelector.NONE } @@ -147,7 +151,9 @@ fun UserInput( onTextFieldFocused = { focused -> if (focused) { currentInputSelector = InputSelector.NONE - scrollState.smoothScrollTo(0f) + scope.launch { + scrollState.smoothScrollTo(0f) + } } textFieldFocusState = focused }, @@ -161,7 +167,9 @@ fun UserInput( // Reset text field and close keyboard textState = TextFieldValue() // Move scroll to bottom - scrollState.smoothScrollTo(0f) + scope.launch { + scrollState.smoothScrollTo(0f) + } dismissKeyboard() }, currentInputSelector = currentInputSelector @@ -199,7 +207,7 @@ private fun SelectorExpanded( // Request focus to force the TextField to lose it val focusRequester = FocusRequester() // If the selector is shown, always request focus to trigger a TextField.onFocusChange. - onCommit { + SideEffect { if (currentSelector == InputSelector.EMOJI) { focusRequester.requestFocus() } @@ -223,7 +231,9 @@ private fun SelectorExpanded( fun FunctionalityNotAvailablePanel() { AnimatedVisibility(visible = true, initiallyVisible = false, enter = fadeIn()) { Column( - modifier = Modifier.preferredHeight(320.dp).fillMaxWidth(), + modifier = Modifier + .preferredHeight(320.dp) + .fillMaxWidth(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, ) { @@ -342,16 +352,16 @@ private fun InputSelectorButton( description: String, selected: Boolean ) { - IconButton( - onClick = onClick, - modifier = Modifier.semantics { contentDescription = description } - ) { + IconButton(onClick = onClick) { Providers(AmbientContentAlpha provides ContentAlpha.medium) { val tint = if (selected) MaterialTheme.colors.primary else AmbientContentColor.current Icon( icon, tint = tint, - modifier = Modifier.padding(12.dp).preferredSize(20.dp) + modifier = Modifier + .padding(12.dp) + .preferredSize(20.dp), + contentDescription = description ) } } @@ -379,10 +389,14 @@ private fun UserInputText( var keyboardController by remember { mutableStateOf(null) } // Show or hide the keyboard - onCommit(keyboardController, keyboardShown) { // Guard side-effects against failed commits + DisposableEffect( + keyboardController, + keyboardShown + ) { // Guard side-effects against failed commits keyboardController?.let { if (keyboardShown) it.showSoftwareKeyboard() else it.hideSoftwareKeyboard() } + onDispose { /* no-op */ } } val a11ylabel = stringResource(id = R.string.textfield_desc) @@ -398,7 +412,10 @@ private fun UserInputText( ) { Surface { Box( - modifier = Modifier.preferredHeight(48.dp).weight(1f).align(Alignment.Bottom) + modifier = Modifier + .preferredHeight(48.dp) + .weight(1f) + .align(Alignment.Bottom) ) { var lastFocusState by remember { mutableStateOf(FocusState.Inactive) } BasicTextField( @@ -455,7 +472,11 @@ fun EmojiSelector( .focusModifier() .semantics { contentDescription = a11yLabel } ) { - Row(modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp)) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp) + ) { ExtendedSelectorInnerButton( text = stringResource(id = R.string.emojis_label), onClick = { selected = EmojiStickerSelector.EMOJI }, @@ -469,7 +490,7 @@ fun EmojiSelector( modifier = Modifier.weight(1f) ) } - ScrollableRow { + Row(modifier = Modifier.verticalScroll(rememberScrollState())) { EmojiTable(onTextAdded, modifier = Modifier.padding(8.dp)) } } diff --git a/Jetchat/app/src/main/java/com/example/compose/jetchat/profile/Profile.kt b/Jetchat/app/src/main/java/com/example/compose/jetchat/profile/Profile.kt index d4a88f36b9..f1526bf889 100644 --- a/Jetchat/app/src/main/java/com/example/compose/jetchat/profile/Profile.kt +++ b/Jetchat/app/src/main/java/com/example/compose/jetchat/profile/Profile.kt @@ -18,12 +18,10 @@ package com.example.compose.jetchat.profile import androidx.compose.foundation.Image import androidx.compose.foundation.ScrollState -import androidx.compose.foundation.ScrollableColumn import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -31,6 +29,7 @@ import androidx.compose.foundation.layout.preferredHeight import androidx.compose.foundation.layout.preferredHeightIn import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.AmbientContentAlpha import androidx.compose.material.ContentAlpha import androidx.compose.material.Divider @@ -49,7 +48,6 @@ import androidx.compose.runtime.key import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.layout.WithConstraints import androidx.compose.ui.platform.AmbientDensity import androidx.compose.ui.res.imageResource import androidx.compose.ui.res.stringResource @@ -60,8 +58,10 @@ import com.example.compose.jetchat.R import com.example.compose.jetchat.components.AnimatingFabContent import com.example.compose.jetchat.components.JetchatAppBar import com.example.compose.jetchat.components.baselineHeight +import com.example.compose.jetchat.data.colleagueProfile import com.example.compose.jetchat.data.meProfile import com.example.compose.jetchat.theme.JetchatTheme +import dev.chrisbanes.accompanist.insets.ProvideWindowInsets import dev.chrisbanes.accompanist.insets.navigationBarsPadding import dev.chrisbanes.accompanist.insets.statusBarsPadding @@ -73,7 +73,9 @@ fun ProfileScreen(userData: ProfileScreenState, onNavIconPressed: () -> Unit = { Column(modifier = Modifier.fillMaxSize()) { JetchatAppBar( // Use statusBarsPadding() to move the app bar content below the status bar - modifier = Modifier.fillMaxWidth().statusBarsPadding(), + modifier = Modifier + .fillMaxWidth() + .statusBarsPadding(), onNavIconPressed = onNavIconPressed, title = { }, actions = { @@ -84,31 +86,32 @@ fun ProfileScreen(userData: ProfileScreenState, onNavIconPressed: () -> Unit = { modifier = Modifier .clickable(onClick = {}) // TODO: Show not implemented dialog. .padding(horizontal = 12.dp, vertical = 16.dp) - .preferredHeight(24.dp) + .preferredHeight(24.dp), + contentDescription = stringResource(id = R.string.more_options) ) } } ) - WithConstraints { - Box(modifier = Modifier.weight(1f)) { - Surface { - ScrollableColumn( - modifier = Modifier.fillMaxSize(), - scrollState = scrollState - ) { - ProfileHeader( - scrollState, - userData - ) - UserInfoFields(userData, maxHeight) - } + BoxWithConstraints(modifier = Modifier.weight(1f)) { + Surface { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(scrollState), + ) { + ProfileHeader( + scrollState, + userData, + this@BoxWithConstraints.maxHeight + ) + UserInfoFields(userData, this@BoxWithConstraints.maxHeight) } - ProfileFab( - extended = scrollState.value == 0f, - userIsMe = userData.isMe(), - modifier = Modifier.align(Alignment.BottomEnd) - ) } + ProfileFab( + extended = scrollState.value == 0f, + userIsMe = userData.isMe(), + modifier = Modifier.align(Alignment.BottomEnd) + ) } } } @@ -147,7 +150,9 @@ private fun NameAndPosition( ) Position( userData, - modifier = Modifier.padding(bottom = 20.dp).baselineHeight(24.dp) + modifier = Modifier + .padding(bottom = 20.dp) + .baselineHeight(24.dp) ) } } @@ -175,23 +180,23 @@ private fun Position(userData: ProfileScreenState, modifier: Modifier = Modifier @Composable private fun ProfileHeader( scrollState: ScrollState, - data: ProfileScreenState + data: ProfileScreenState, + containerHeight: Dp ) { val offset = (scrollState.value / 2) val offsetDp = with(AmbientDensity.current) { offset.toDp() } data.photo?.let { val asset = imageResource(id = it) - val ratioAsset = (asset.width / asset.height.toFloat()).coerceAtLeast(1f) - // TODO: Fix landscape Image( modifier = Modifier - .aspectRatio(ratioAsset) - .preferredHeightIn(max = 320.dp) + .preferredHeightIn(max = containerHeight / 2) + .fillMaxWidth() .padding(top = offsetDp), bitmap = asset, - contentScale = ContentScale.FillWidth + contentScale = ContentScale.Crop, + contentDescription = null ) } } @@ -242,7 +247,10 @@ fun ProfileFab(extended: Boolean, userIsMe: Boolean, modifier: Modifier = Modifi AnimatingFabContent( icon = { Icon( - imageVector = if (userIsMe) Icons.Outlined.Create else Icons.Outlined.Chat + imageVector = if (userIsMe) Icons.Outlined.Create else Icons.Outlined.Chat, + contentDescription = stringResource( + if (userIsMe) R.string.edit_profile else R.string.message + ) ) }, text = { @@ -259,18 +267,42 @@ fun ProfileFab(extended: Boolean, userIsMe: Boolean, modifier: Modifier = Modifi } } -@Preview +@Preview(widthDp = 640, heightDp = 360) @Composable -fun ConvPreview480MeDefault() { - JetchatTheme { - ProfileScreen(meProfile) +fun ConvPreviewLandscapeMeDefault() { + ProvideWindowInsets(consumeWindowInsets = false) { + JetchatTheme { + ProfileScreen(meProfile) + } + } +} + +@Preview(widthDp = 360, heightDp = 480) +@Composable +fun ConvPreviewPortraitMeDefault() { + ProvideWindowInsets(consumeWindowInsets = false) { + JetchatTheme { + ProfileScreen(meProfile) + } + } +} + +@Preview(widthDp = 360, heightDp = 480) +@Composable +fun ConvPreviewPortraitOtherDefault() { + ProvideWindowInsets(consumeWindowInsets = false) { + JetchatTheme { + ProfileScreen(colleagueProfile) + } } } @Preview @Composable fun ProfileFabPreview() { - JetchatTheme { - ProfileFab(extended = true, userIsMe = false) + ProvideWindowInsets(consumeWindowInsets = false) { + JetchatTheme { + ProfileFab(extended = true, userIsMe = false) + } } } diff --git a/Jetchat/app/src/main/java/com/example/compose/jetchat/theme/Typography.kt b/Jetchat/app/src/main/java/com/example/compose/jetchat/theme/Typography.kt index 12ede6b13d..e0f215e1f9 100644 --- a/Jetchat/app/src/main/java/com/example/compose/jetchat/theme/Typography.kt +++ b/Jetchat/app/src/main/java/com/example/compose/jetchat/theme/Typography.kt @@ -18,21 +18,21 @@ package com.example.compose.jetchat.theme import androidx.compose.material.Typography import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.font.font -import androidx.compose.ui.text.font.fontFamily import androidx.compose.ui.unit.sp import com.example.compose.jetchat.R -private val MontserratFontFamily = fontFamily( - font(R.font.montserrat_regular), - font(R.font.montserrat_light, FontWeight.Light), - font(R.font.montserrat_semibold, FontWeight.SemiBold) +private val MontserratFontFamily = FontFamily( + Font(R.font.montserrat_regular), + Font(R.font.montserrat_light, FontWeight.Light), + Font(R.font.montserrat_semibold, FontWeight.SemiBold) ) -private val KarlaFontFamily = fontFamily( - font(R.font.karla_regular), - font(R.font.karla_bold, FontWeight.Bold) +private val KarlaFontFamily = FontFamily( + Font(R.font.karla_regular), + Font(R.font.karla_bold, FontWeight.Bold) ) val JetchatTypography = Typography( diff --git a/Jetchat/app/src/main/res/layout/content_main.xml b/Jetchat/app/src/main/res/layout/content_main.xml index 61198607ea..accf21f538 100644 --- a/Jetchat/app/src/main/res/layout/content_main.xml +++ b/Jetchat/app/src/main/res/layout/content_main.xml @@ -16,12 +16,21 @@ --> - + android:layout_height="match_parent"> + + + + + diff --git a/Jetchat/app/src/main/res/values/strings.xml b/Jetchat/app/src/main/res/values/strings.xml index 7d72a031bd..2da0c8ee41 100644 --- a/Jetchat/app/src/main/res/values/strings.xml +++ b/Jetchat/app/src/main/res/values/strings.xml @@ -60,5 +60,10 @@ Text input Functionality currently not available Grab a beverage and check back later! + Navigate up + Attached image + Search + Information + More options diff --git a/Jetchat/buildSrc/src/main/java/com/example/compose/jetchat/buildsrc/dependencies.kt b/Jetchat/buildSrc/src/main/java/com/example/compose/jetchat/buildsrc/dependencies.kt index a8913d14a3..55245b3140 100644 --- a/Jetchat/buildSrc/src/main/java/com/example/compose/jetchat/buildsrc/dependencies.kt +++ b/Jetchat/buildSrc/src/main/java/com/example/compose/jetchat/buildsrc/dependencies.kt @@ -21,7 +21,7 @@ object Versions { } object Libs { - const val androidGradlePlugin = "com.android.tools.build:gradle:7.0.0-alpha04" + const val androidGradlePlugin = "com.android.tools.build:gradle:7.0.0-alpha05" const val jdkDesugar = "com.android.tools:desugar_jdk_libs:1.0.9" const val junit = "junit:junit:4.13" @@ -29,7 +29,7 @@ object Libs { const val material = "com.google.android.material:material:1.1.0" object Kotlin { - private const val version = "1.4.21" + private const val version = "1.4.21-2" const val stdlib = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$version" const val gradlePlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:$version" const val extensions = "org.jetbrains.kotlin:kotlin-android-extensions:$version" @@ -43,7 +43,7 @@ object Libs { } object Accompanist { - private const val version = "0.4.2" + private const val version = "0.5.0" const val insets = "dev.chrisbanes.accompanist:accompanist-insets:$version" } @@ -53,7 +53,7 @@ object Libs { object Compose { const val snapshot = "" - const val version = "1.0.0-alpha10" + const val version = "1.0.0-alpha11" const val foundation = "androidx.compose.foundation:foundation:$version" const val layout = "androidx.compose.foundation:foundation-layout:$version" diff --git a/Jetchat/gradle/wrapper/gradle-wrapper.properties b/Jetchat/gradle/wrapper/gradle-wrapper.properties index 2f4c55bdf8..e98a756ba7 100644 --- a/Jetchat/gradle/wrapper/gradle-wrapper.properties +++ b/Jetchat/gradle/wrapper/gradle-wrapper.properties @@ -19,4 +19,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-rc-1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.1-bin.zip diff --git a/Jetsnack/app/build.gradle b/Jetsnack/app/build.gradle index 496eafbd84..873806714d 100644 --- a/Jetsnack/app/build.gradle +++ b/Jetsnack/app/build.gradle @@ -71,7 +71,6 @@ android { } composeOptions { - kotlinCompilerVersion Libs.Kotlin.version kotlinCompilerExtensionVersion Libs.AndroidX.Compose.version } } diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/model/Snack.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/model/Snack.kt index 4fc8a6d9e6..7ef141543b 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/model/Snack.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/model/Snack.kt @@ -79,7 +79,7 @@ val snacks = listOf( id = 7L, name = "Ice Cream Sandwich", tagline = "A tag line", - imageUrl = "https://source.unsplash.com/AqorcpZIKnU", + imageUrl = "https://source.unsplash.com/YgYJsFDd4AU", price = 1299 ), Snack( diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Filters.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Filters.kt index 6cf71bbc3a..a2cb49d366 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Filters.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Filters.kt @@ -16,14 +16,15 @@ package com.example.jetsnack.ui.components -import androidx.compose.animation.animateAsState -import androidx.compose.foundation.ScrollableRow +import androidx.compose.animation.animateColorAsState +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.preferredHeight import androidx.compose.foundation.layout.preferredHeightIn -import androidx.compose.foundation.layout.preferredWidth +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.selection.toggleable import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.Icon @@ -37,34 +38,35 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import com.example.jetsnack.R import com.example.jetsnack.model.Filter import com.example.jetsnack.ui.theme.JetsnackTheme @Composable fun FilterBar(filters: List) { - ScrollableRow(modifier = Modifier.preferredHeightIn(min = 56.dp)) { - Spacer(Modifier.preferredWidth(8.dp)) - IconButton( - onClick = { /* todo */ }, - modifier = Modifier.align(Alignment.CenterVertically) - ) { - Icon( - imageVector = Icons.Rounded.FilterList, - tint = JetsnackTheme.colors.brand, - modifier = Modifier.diagonalGradientBorder( - colors = JetsnackTheme.colors.interactiveSecondary, - shape = CircleShape + LazyRow( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + contentPadding = PaddingValues(start = 8.dp, end = 8.dp), + modifier = Modifier.preferredHeightIn(min = 56.dp) + ) { + item { + IconButton(onClick = { /* todo */ }) { + Icon( + imageVector = Icons.Rounded.FilterList, + tint = JetsnackTheme.colors.brand, + contentDescription = stringResource(R.string.label_filters), + modifier = Modifier.diagonalGradientBorder( + colors = JetsnackTheme.colors.interactiveSecondary, + shape = CircleShape + ) ) - ) + } } - - filters.forEach { filter -> - FilterChip( - filter = filter, - modifier = Modifier.align(Alignment.CenterVertically) - ) - Spacer(Modifier.preferredWidth(8.dp)) + items(filters) { filter -> + FilterChip(filter) } } } @@ -76,7 +78,7 @@ fun FilterChip( shape: Shape = MaterialTheme.shapes.small ) { val (selected, setSelected) = filter.enabled - val backgroundColor by animateAsState( + val backgroundColor by animateColorAsState( if (selected) JetsnackTheme.colors.brand else JetsnackTheme.colors.uiBackground ) val border = Modifier.fadeInDiagonalGradientBorder( @@ -84,7 +86,7 @@ fun FilterChip( colors = JetsnackTheme.colors.interactiveSecondary, shape = shape ) - val textColor by animateAsState( + val textColor by animateColorAsState( if (selected) JetsnackTheme.colors.textInteractive else JetsnackTheme.colors.textSecondary ) JetsnackSurface( diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Gradient.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Gradient.kt index a44e362931..ec26fe92f0 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Gradient.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Gradient.kt @@ -16,7 +16,7 @@ package com.example.jetsnack.ui.components -import androidx.compose.animation.animateAsState +import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.ui.Modifier @@ -71,7 +71,7 @@ fun Modifier.fadeInDiagonalGradientBorder( shape: Shape ) = composed { val animatedColors = List(colors.size) { i -> - animateAsState(if (showBorder) colors[i] else colors[i].copy(alpha = 0f)).value + animateColorAsState(if (showBorder) colors[i] else colors[i].copy(alpha = 0f)).value } diagonalGradientBorder( colors = animatedColors, diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/GradientTintedIconButton.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/GradientTintedIconButton.kt index 2ec3044b8b..8366eb1a60 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/GradientTintedIconButton.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/GradientTintedIconButton.kt @@ -29,6 +29,7 @@ import com.example.jetsnack.ui.theme.JetsnackTheme fun JetsnackGradientTintedIconButton( imageVector: ImageVector, onClick: () -> Unit, + contentDescription: String?, modifier: Modifier = Modifier, colors: List = JetsnackTheme.colors.interactiveSecondary ) { @@ -37,6 +38,7 @@ fun JetsnackGradientTintedIconButton( IconButton(onClick = onClick, modifier) { Icon( imageVector = imageVector, + contentDescription = contentDescription, modifier = Modifier.diagonalGradientTint( colors = colors, blendMode = blendMode diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/QuantitySelector.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/QuantitySelector.kt index dc938c29d2..e0f4da57f1 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/QuantitySelector.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/QuantitySelector.kt @@ -62,6 +62,7 @@ fun QuantitySelector( JetsnackGradientTintedIconButton( imageVector = Icons.Outlined.RemoveCircleOutline, onClick = decreaseItemCount, + contentDescription = stringResource(R.string.label_decrease), modifier = Modifier.constrainAs(minus) { centerVerticallyTo(quantity) linkTo(top = parent.top, bottom = parent.bottom) @@ -84,6 +85,7 @@ fun QuantitySelector( JetsnackGradientTintedIconButton( imageVector = Icons.Outlined.AddCircleOutline, onClick = increaseItemCount, + contentDescription = stringResource(R.string.label_increase), modifier = Modifier.constrainAs(plus) { end.linkTo(parent.end) centerVerticallyTo(quantity) diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Snacks.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Snacks.kt index 192d32736e..7f1fb8ddc6 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Snacks.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Snacks.kt @@ -16,8 +16,8 @@ package com.example.jetsnack.ui.components -import androidx.compose.foundation.ScrollableRow import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -29,9 +29,10 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.preferredHeight import androidx.compose.foundation.layout.preferredHeightIn import androidx.compose.foundation.layout.preferredSize -import androidx.compose.foundation.layout.preferredWidth import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.Icon @@ -98,7 +99,8 @@ fun SnackCollection( ) { Icon( imageVector = Icons.Outlined.ArrowForward, - tint = JetsnackTheme.colors.brand + tint = JetsnackTheme.colors.brand, + contentDescription = null ) } } @@ -126,14 +128,20 @@ private fun HighlightedSnacks( val gradientWidth = with(AmbientDensity.current) { (3 * (HighlightCardWidth + HighlightCardPadding).toPx()) } - ScrollableRow( - scrollState = scroll, - modifier = modifier + LazyRow( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(16.dp), + contentPadding = PaddingValues(start = 16.dp, end = 16.dp) ) { - Spacer(modifier = Modifier.preferredWidth(24.dp)) - snacks.forEachIndexed { index, snack -> - HighlightSnackItem(snack, onSnackClick, index, gradient, gradientWidth, scroll.value) - Spacer(modifier = Modifier.preferredWidth(16.dp)) + itemsIndexed(snacks) { index, snack -> + HighlightSnackItem( + snack, + onSnackClick, + index, + gradient, + gradientWidth, + scroll.value + ) } } } @@ -177,6 +185,7 @@ fun SnackItem( SnackImage( imageUrl = snack.imageUrl, elevation = 4.dp, + contentDescription = null, modifier = Modifier.preferredSize(120.dp) ) Text( @@ -230,6 +239,7 @@ private fun HighlightSnackItem( ) SnackImage( imageUrl = snack.imageUrl, + contentDescription = null, modifier = Modifier .preferredSize(120.dp) .align(Alignment.BottomCenter) @@ -258,6 +268,7 @@ private fun HighlightSnackItem( @Composable fun SnackImage( imageUrl: String, + contentDescription: String?, modifier: Modifier = Modifier, elevation: Dp = 0.dp ) { @@ -269,6 +280,7 @@ fun SnackImage( ) { CoilImage( data = imageUrl, + contentDescription = contentDescription, contentScale = ContentScale.Crop, modifier = Modifier.fillMaxSize() ) diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/DestinationBar.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/DestinationBar.kt index 5afa3985b1..c564826aa9 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/DestinationBar.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/DestinationBar.kt @@ -27,9 +27,11 @@ import androidx.compose.material.icons.outlined.ExpandMore import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import com.example.jetsnack.R import com.example.jetsnack.ui.components.JetsnackDivider import com.example.jetsnack.ui.theme.AlphaNearOpaque import com.example.jetsnack.ui.theme.JetsnackTheme @@ -60,7 +62,8 @@ fun DestinationBar(modifier: Modifier = Modifier) { ) { Icon( imageVector = Icons.Outlined.ExpandMore, - tint = JetsnackTheme.colors.brand + tint = JetsnackTheme.colors.brand, + contentDescription = stringResource(R.string.label_select_delivery) ) } } diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/Feed.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/Feed.kt index 4e702fa647..c6ef1de6cf 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/Feed.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/Feed.kt @@ -20,6 +20,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/Home.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/Home.kt index 9cd6109de4..825b8b0a6e 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/Home.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/Home.kt @@ -18,13 +18,12 @@ package com.example.jetsnack.ui.home import androidx.annotation.FloatRange import androidx.annotation.StringRes -import androidx.compose.animation.AnimatedFloatModel import androidx.compose.animation.Crossfade -import androidx.compose.animation.animateAsState -import androidx.compose.animation.animatedFloat +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.SpringSpec -import androidx.compose.animation.core.animateAsState +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope @@ -43,9 +42,10 @@ import androidx.compose.material.icons.outlined.Home import androidx.compose.material.icons.outlined.Search import androidx.compose.material.icons.outlined.ShoppingCart import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.onCommit import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.savedinstancestate.savedInstanceState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -60,7 +60,6 @@ import androidx.compose.ui.layout.MeasureResult import androidx.compose.ui.layout.MeasureScope import androidx.compose.ui.layout.Placeable import androidx.compose.ui.layout.layoutId -import androidx.compose.ui.platform.AmbientAnimationClock import androidx.compose.ui.platform.AmbientConfiguration import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -75,6 +74,7 @@ import com.example.jetsnack.ui.home.cart.Cart import com.example.jetsnack.ui.home.search.Search import com.example.jetsnack.ui.theme.JetsnackTheme import dev.chrisbanes.accompanist.insets.navigationBarsPadding +import kotlinx.coroutines.launch @Composable fun Home(onSnackSelected: (Long) -> Unit) { @@ -132,7 +132,7 @@ private fun JetsnackBottomNav( ) { items.forEach { section -> val selected = section == currentSection - val tint by animateAsState( + val tint by animateColorAsState( if (selected) { JetsnackTheme.colors.iconInteractive } else { @@ -144,7 +144,8 @@ private fun JetsnackBottomNav( icon = { Icon( imageVector = section.icon, - tint = tint + tint = tint, + contentDescription = null ) }, text = { @@ -180,24 +181,27 @@ private fun JetsnackBottomNavLayout( content: @Composable () -> Unit ) { // Track how "selected" each item is [0, 1] - val clock = AmbientAnimationClock.current val selectionFractions = remember(itemCount) { List(itemCount) { i -> - AnimatedFloatModel(if (i == selectedIndex) 1f else 0f, clock) + Animatable(if (i == selectedIndex) 1f else 0f) } } - - // When selection changes, animate the selection fractions - onCommit(selectedIndex) { - selectionFractions.forEachIndexed { index, selectionFraction -> - val target = if (index == selectedIndex) 1f else 0f - if (selectionFraction.targetValue != target) { + val scope = rememberCoroutineScope() + selectionFractions.forEachIndexed { index, selectionFraction -> + val target = if (index == selectedIndex) 1f else 0f + if (selectionFraction.targetValue != target) { + scope.launch { selectionFraction.animateTo(target, animSpec) } } } + // Animate the position of the indicator - val indicatorLeft = animatedFloat(0f) + val indicatorIndex = remember { Animatable(0f) } + val targetIndicatorIndex = selectedIndex.toFloat() + LaunchedEffect(targetIndicatorIndex) { + indicatorIndex.animateTo(targetIndicatorIndex, animSpec) + } Layout( modifier = modifier.preferredHeight(BottomNavHeight), @@ -210,7 +214,7 @@ private fun JetsnackBottomNavLayout( // Divide the width into n+1 slots and give the selected item 2 slots val unselectedWidth = constraints.maxWidth / (itemCount + 1) - val selectedWidth = constraints.maxWidth - (itemCount - 1) * unselectedWidth + val selectedWidth = 2 * unselectedWidth val indicatorMeasurable = measurables.first { it.layoutId == "indicator" } val itemPlaceables = measurables @@ -232,17 +236,12 @@ private fun JetsnackBottomNavLayout( ) ) - // Animate the indicator position - val targetIndicatorLeft = selectedIndex * unselectedWidth.toFloat() - if (indicatorLeft.targetValue != targetIndicatorLeft) { - indicatorLeft.animateTo(targetIndicatorLeft, animSpec) - } - layout( width = constraints.maxWidth, height = itemPlaceables.maxByOrNull { it.height }?.height ?: 0 ) { - indicatorPlaceable.place(x = indicatorLeft.value.toInt(), y = 0) + val indicatorLeft = indicatorIndex.value * unselectedWidth + indicatorPlaceable.place(x = indicatorLeft.toInt(), y = 0) var x = 0 itemPlaceables.forEach { placeable -> placeable.place(x = x, y = 0) @@ -266,7 +265,7 @@ fun JetsnackBottomNavigationItem( contentAlignment = Alignment.Center ) { // Animate the icon/text positions within the item based on selection - val animationProgress by animateAsState(if (selected) 1f else 0f, animSpec) + val animationProgress by animateFloatAsState(if (selected) 1f else 0f, animSpec) JetsnackBottomNavItemLayout( icon = icon, text = text, diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/cart/Cart.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/cart/Cart.kt index e01003a840..4319febfe5 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/cart/Cart.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/cart/Cart.kt @@ -34,6 +34,7 @@ import androidx.compose.foundation.layout.preferredSize import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme @@ -194,6 +195,7 @@ fun CartItem( createVerticalChain(name, tag, priceSpacer, price, chainStyle = ChainStyle.Packed) SnackImage( imageUrl = snack.imageUrl, + contentDescription = null, modifier = Modifier .preferredSize(100.dp) .constrainAs(image) { @@ -227,7 +229,8 @@ fun CartItem( ) { Icon( imageVector = Icons.Filled.Close, - tint = JetsnackTheme.colors.iconSecondary + tint = JetsnackTheme.colors.iconSecondary, + contentDescription = stringResource(R.string.label_remove) ) } Text( @@ -305,7 +308,8 @@ fun SummaryItem( Text( text = stringResource(R.string.cart_subtotal_label), style = MaterialTheme.typography.body1, - modifier = Modifier.weight(1f) + modifier = Modifier + .weight(1f) .wrapContentWidth(Alignment.Start) .alignBy(LastBaseline) ) @@ -319,7 +323,8 @@ fun SummaryItem( Text( text = stringResource(R.string.cart_shipping_label), style = MaterialTheme.typography.body1, - modifier = Modifier.weight(1f) + modifier = Modifier + .weight(1f) .wrapContentWidth(Alignment.Start) .alignBy(LastBaseline) ) diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/search/Categories.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/search/Categories.kt index 79b34a27d2..ba7597f59a 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/search/Categories.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/search/Categories.kt @@ -27,6 +27,7 @@ import androidx.compose.foundation.layout.preferredHeight import androidx.compose.foundation.layout.preferredHeightIn import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.MaterialTheme import androidx.compose.material.Text @@ -120,6 +121,7 @@ private fun SearchCategory( ) SnackImage( imageUrl = category.imageUrl, + contentDescription = null, modifier = Modifier.fillMaxSize() ) } diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/search/Results.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/search/Results.kt index 4c717322d0..bcdbc5b0aa 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/search/Results.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/search/Results.kt @@ -30,6 +30,7 @@ import androidx.compose.foundation.layout.preferredHeight import androidx.compose.foundation.layout.preferredSize import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme @@ -39,8 +40,8 @@ import androidx.compose.material.icons.outlined.Add import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -103,6 +104,7 @@ private fun SearchResult( } SnackImage( imageUrl = snack.imageUrl, + contentDescription = null, modifier = Modifier .preferredSize(100.dp) .constrainAs(image) { @@ -175,7 +177,10 @@ private fun SearchResult( end.linkTo(parent.end) } ) { - Icon(Icons.Outlined.Add) + Icon( + imageVector = Icons.Outlined.Add, + contentDescription = stringResource(R.string.label_add) + ) } } } @@ -192,7 +197,10 @@ fun NoResults( .wrapContentSize() .padding(24.dp) ) { - Image(vectorResource(R.drawable.empty_state_search)) + Image( + painterResource(R.drawable.empty_state_search), + contentDescription = null + ) Spacer(Modifier.preferredHeight(24.dp)) Text( text = stringResource(R.string.search_no_matches, query), diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/search/Search.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/search/Search.kt index 2d5b6a1836..a57a5b352f 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/search/Search.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/search/Search.kt @@ -191,7 +191,8 @@ private fun SearchBar( IconButton(onClick = onClearQuery) { Icon( imageVector = Icons.Outlined.ArrowBack, - tint = JetsnackTheme.colors.iconPrimary + tint = JetsnackTheme.colors.iconPrimary, + contentDescription = stringResource(R.string.label_back) ) } } @@ -225,11 +226,14 @@ private val IconSize = 48.dp private fun SearchHint() { Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxSize().wrapContentSize() + modifier = Modifier + .fillMaxSize() + .wrapContentSize() ) { Icon( imageVector = Icons.Outlined.Search, - tint = JetsnackTheme.colors.textHelp + tint = JetsnackTheme.colors.textHelp, + contentDescription = stringResource(R.string.label_search) ) Spacer(Modifier.preferredWidth(8.dp)) Text( diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/search/Suggestions.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/search/Suggestions.kt index 08eb4f2dc5..a86bdd7bc4 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/search/Suggestions.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/search/Suggestions.kt @@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.preferredHeightIn import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/snackdetail/SnackDetail.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/snackdetail/SnackDetail.kt index 558229db3a..87a3018f42 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/snackdetail/SnackDetail.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/snackdetail/SnackDetail.kt @@ -17,7 +17,6 @@ package com.example.jetsnack.ui.snackdetail import androidx.compose.foundation.ScrollState -import androidx.compose.foundation.ScrollableColumn import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -33,6 +32,7 @@ import androidx.compose.foundation.layout.preferredSize import androidx.compose.foundation.layout.preferredWidth import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.verticalScroll import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme @@ -129,7 +129,8 @@ private fun Up(upPress: () -> Unit) { ) { Icon( imageVector = Icons.Outlined.ArrowBack, - tint = JetsnackTheme.colors.iconInteractive + tint = JetsnackTheme.colors.iconInteractive, + contentDescription = stringResource(R.string.label_back) ) } } @@ -146,7 +147,9 @@ private fun Body( .statusBarsPadding() .preferredHeight(MinTitleOffset) ) - ScrollableColumn(scrollState = scroll) { + Column( + modifier = Modifier.verticalScroll(scroll) + ) { Spacer(Modifier.preferredHeight(GradientScroll)) JetsnackSurface(Modifier.fillMaxWidth()) { Column { @@ -262,6 +265,7 @@ private fun Image( ) { SnackImage( imageUrl = imageUrl, + contentDescription = null, modifier = Modifier.fillMaxSize() ) } diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/theme/Theme.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/theme/Theme.kt index d7d2d1f97c..11a1898f1e 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/theme/Theme.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/theme/Theme.kt @@ -21,10 +21,10 @@ import androidx.compose.material.Colors import androidx.compose.material.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.Providers +import androidx.compose.runtime.SideEffect import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.onCommit import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.runtime.staticAmbientOf @@ -85,7 +85,7 @@ fun JetsnackTheme( val colors = if (darkTheme) DarkColorPalette else LightColorPalette val sysUiController = SysUiController.current - onCommit(sysUiController, colors.uiBackground) { + SideEffect { sysUiController.setSystemBarsColor( color = colors.uiBackground.copy(alpha = AlphaNearOpaque) ) diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/theme/Type.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/theme/Type.kt index 47e7b68fc4..48a369dbb5 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/theme/Type.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/theme/Type.kt @@ -18,22 +18,22 @@ package com.example.jetsnack.ui.theme import androidx.compose.material.Typography import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.font.font -import androidx.compose.ui.text.font.fontFamily import androidx.compose.ui.unit.sp import com.example.jetsnack.R -private val Montserrat = fontFamily( - font(R.font.montserrat_light, FontWeight.Light), - font(R.font.montserrat_regular, FontWeight.Normal), - font(R.font.montserrat_medium, FontWeight.Medium), - font(R.font.montserrat_semibold, FontWeight.SemiBold) +private val Montserrat = FontFamily( + Font(R.font.montserrat_light, FontWeight.Light), + Font(R.font.montserrat_regular, FontWeight.Normal), + Font(R.font.montserrat_medium, FontWeight.Medium), + Font(R.font.montserrat_semibold, FontWeight.SemiBold) ) -private val Karla = fontFamily( - font(R.font.karla_regular, FontWeight.Normal), - font(R.font.karla_bold, FontWeight.Bold) +private val Karla = FontFamily( + Font(R.font.karla_regular, FontWeight.Normal), + Font(R.font.karla_bold, FontWeight.Bold) ) val Typography = Typography( diff --git a/Jetsnack/app/src/main/res/values/strings.xml b/Jetsnack/app/src/main/res/values/strings.xml index 0350109f60..2e93c6ce2e 100644 --- a/Jetsnack/app/src/main/res/values/strings.xml +++ b/Jetsnack/app/src/main/res/values/strings.xml @@ -14,6 +14,7 @@ --> Jetsnack + Back Home @@ -21,11 +22,17 @@ My Cart Profile + + Filters + Select delivery address + Search Jetsnack No matches for “%1s” Try broadening your search %1d items + Add to cart + Perform search Details @@ -46,4 +53,10 @@ Shipping & Handling Total Checkout + Remove item + + + Increase + Decrease + diff --git a/Jetsnack/buildSrc/src/main/java/com/example/jetsnack/buildsrc/Dependencies.kt b/Jetsnack/buildSrc/src/main/java/com/example/jetsnack/buildsrc/Dependencies.kt index b9da6f12a6..6acb362386 100644 --- a/Jetsnack/buildSrc/src/main/java/com/example/jetsnack/buildsrc/Dependencies.kt +++ b/Jetsnack/buildSrc/src/main/java/com/example/jetsnack/buildsrc/Dependencies.kt @@ -21,17 +21,17 @@ object Versions { } object Libs { - const val androidGradlePlugin = "com.android.tools.build:gradle:7.0.0-alpha04" + const val androidGradlePlugin = "com.android.tools.build:gradle:7.0.0-alpha05" const val junit = "junit:junit:4.13" object Accompanist { - private const val version = "0.4.2" + private const val version = "0.5.0" const val coil = "dev.chrisbanes.accompanist:accompanist-coil:$version" const val insets = "dev.chrisbanes.accompanist:accompanist-insets:$version" } object Kotlin { - private const val version = "1.4.21" + private const val version = "1.4.21-2" const val stdlib = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$version" const val gradlePlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:$version" const val extensions = "org.jetbrains.kotlin:kotlin-android-extensions:$version" @@ -49,7 +49,7 @@ object Libs { object Compose { const val snapshot = "" - const val version = "1.0.0-alpha10" + const val version = "1.0.0-alpha11" const val foundation = "androidx.compose.foundation:foundation:${version}" const val layout = "androidx.compose.foundation:foundation-layout:${version}" diff --git a/Jetsnack/gradle/wrapper/gradle-wrapper.properties b/Jetsnack/gradle/wrapper/gradle-wrapper.properties index 089b9f390f..28ff446a21 100644 --- a/Jetsnack/gradle/wrapper/gradle-wrapper.properties +++ b/Jetsnack/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-rc-1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/Jetsurvey/app/build.gradle b/Jetsurvey/app/build.gradle index 8d063a4ca6..2afdb84a17 100644 --- a/Jetsurvey/app/build.gradle +++ b/Jetsurvey/app/build.gradle @@ -75,7 +75,6 @@ android { } composeOptions { - kotlinCompilerVersion Libs.Kotlin.version kotlinCompilerExtensionVersion Libs.AndroidX.Compose.version } diff --git a/Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/signinsignup/SignInSignUp.kt b/Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/signinsignup/SignInSignUp.kt index a344145b2b..2d582a802e 100644 --- a/Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/signinsignup/SignInSignUp.kt +++ b/Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/signinsignup/SignInSignUp.kt @@ -16,7 +16,6 @@ package com.example.compose.jetsurvey.signinsignup -import androidx.compose.foundation.ScrollableColumn import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -27,6 +26,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.preferredHeight import androidx.compose.foundation.layout.preferredWidth import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.AmbientContentAlpha import androidx.compose.material.AmbientTextStyle @@ -66,16 +66,24 @@ fun SignInSignUpScreen( modifier: Modifier = Modifier, content: @Composable() () -> Unit ) { - ScrollableColumn(modifier = modifier) { - Spacer(modifier = Modifier.preferredHeight(44.dp)) - Box(modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp)) { - content() + LazyColumn(modifier = modifier) { + item { + Spacer(modifier = Modifier.preferredHeight(44.dp)) + Box( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp) + ) { + content() + } + Spacer(modifier = Modifier.preferredHeight(16.dp)) + OrSignInAsGuest( + onSignedInAsGuest = onSignedInAsGuest, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp) + ) } - Spacer(modifier = Modifier.preferredHeight(16.dp)) - OrSignInAsGuest( - onSignedInAsGuest = onSignedInAsGuest, - modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp) - ) } } @@ -93,7 +101,10 @@ fun SignInSignUpTopAppBar(topAppBarText: String, onBackPressed: () -> Unit) { }, navigationIcon = { IconButton(onClick = onBackPressed) { - Icon(Icons.Filled.ChevronLeft) + Icon( + imageVector = Icons.Filled.ChevronLeft, + contentDescription = stringResource(id = R.string.back) + ) } }, // We need to balance the navigation icon, so we add a spacer. @@ -124,13 +135,15 @@ fun Email( ) } }, - modifier = Modifier.fillMaxWidth().onFocusChanged { focusState -> - val focused = focusState == FocusState.Active - emailState.onFocusChange(focused) - if (!focused) { - emailState.enableShowErrors() - } - }, + modifier = Modifier + .fillMaxWidth() + .onFocusChanged { focusState -> + val focused = focusState == FocusState.Active + emailState.onFocusChange(focused) + if (!focused) { + emailState.enableShowErrors() + } + }, textStyle = MaterialTheme.typography.body2, isErrorValue = emailState.showErrors(), keyboardOptions = KeyboardOptions.Default.copy(imeAction = imeAction), @@ -160,13 +173,15 @@ fun Password( passwordState.text = it passwordState.enableShowErrors() }, - modifier = modifier.fillMaxWidth().onFocusChanged { focusState -> - val focused = focusState == FocusState.Active - passwordState.onFocusChange(focused) - if (!focused) { - passwordState.enableShowErrors() - } - }, + modifier = modifier + .fillMaxWidth() + .onFocusChanged { focusState -> + val focused = focusState == FocusState.Active + passwordState.onFocusChange(focused) + if (!focused) { + passwordState.enableShowErrors() + } + }, textStyle = MaterialTheme.typography.body2, label = { Providers(AmbientContentAlpha provides ContentAlpha.medium) { @@ -179,11 +194,17 @@ fun Password( trailingIcon = { if (showPassword.value) { IconButton(onClick = { showPassword.value = false }) { - Icon(imageVector = Icons.Filled.Visibility) + Icon( + imageVector = Icons.Filled.Visibility, + contentDescription = stringResource(id = R.string.hide_password) + ) } } else { IconButton(onClick = { showPassword.value = true }) { - Icon(imageVector = Icons.Filled.VisibilityOff) + Icon( + imageVector = Icons.Filled.VisibilityOff, + contentDescription = stringResource(id = R.string.show_password) + ) } } }, @@ -239,7 +260,9 @@ fun OrSignInAsGuest( } OutlinedButton( onClick = onSignedInAsGuest, - modifier = Modifier.fillMaxWidth().padding(top = 20.dp, bottom = 24.dp) + modifier = Modifier + .fillMaxWidth() + .padding(top = 20.dp, bottom = 24.dp) ) { Text(text = stringResource(id = R.string.sign_in_guest)) } diff --git a/Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/signinsignup/WelcomeScreen.kt b/Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/signinsignup/WelcomeScreen.kt index 9a05930987..04047c9633 100644 --- a/Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/signinsignup/WelcomeScreen.kt +++ b/Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/signinsignup/WelcomeScreen.kt @@ -16,7 +16,7 @@ package com.example.compose.jetsurvey.signinsignup -import androidx.compose.animation.core.animateAsState +import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize @@ -70,7 +70,7 @@ fun WelcomeScreen(onEvent: (WelcomeEvent) -> Unit) { with(AmbientDensity.current) { currentOffsetHolder.value.toDp() } val heightDp = with(AmbientDensity.current) { heightWithBranding.toDp() } Surface(modifier = Modifier.fillMaxSize()) { - val offset by animateAsState(targetValue = currentOffsetHolderDp) + val offset by animateDpAsState(targetValue = currentOffsetHolderDp) Column( modifier = Modifier .fillMaxWidth() @@ -149,7 +149,8 @@ private fun Logo( } Image( imageVector = vectorResource(id = assetId), - modifier = modifier + modifier = modifier, + contentDescription = null ) } diff --git a/Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/survey/SurveyQuestions.kt b/Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/survey/SurveyQuestions.kt index 1538400920..4130a4a576 100644 --- a/Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/survey/SurveyQuestions.kt +++ b/Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/survey/SurveyQuestions.kt @@ -19,7 +19,6 @@ package com.example.compose.jetsurvey.survey import androidx.annotation.StringRes import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Image -import androidx.compose.foundation.ScrollableColumn import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -33,6 +32,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.preferredHeight import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.selection.selectable import androidx.compose.material.AmbientContentAlpha import androidx.compose.material.Button @@ -74,78 +74,80 @@ fun Question( onAction: (Int, SurveyActionType) -> Unit, modifier: Modifier = Modifier ) { - ScrollableColumn( + LazyColumn( modifier = modifier, contentPadding = PaddingValues(start = 20.dp, end = 20.dp) ) { - Spacer(modifier = Modifier.preferredHeight(44.dp)) - val backgroundColor = if (MaterialTheme.colors.isLight) { - MaterialTheme.colors.onSurface.copy(alpha = 0.04f) - } else { - MaterialTheme.colors.onSurface.copy(alpha = 0.06f) - } - Row( - modifier = Modifier - .fillMaxWidth() - .background( - color = backgroundColor, - shape = MaterialTheme.shapes.small - ) - ) { - Text( - text = stringResource(id = question.questionText), - style = MaterialTheme.typography.subtitle1, + item { + Spacer(modifier = Modifier.preferredHeight(44.dp)) + val backgroundColor = if (MaterialTheme.colors.isLight) { + MaterialTheme.colors.onSurface.copy(alpha = 0.04f) + } else { + MaterialTheme.colors.onSurface.copy(alpha = 0.06f) + } + Row( modifier = Modifier .fillMaxWidth() - .padding(vertical = 24.dp, horizontal = 16.dp) - ) - } - Spacer(modifier = Modifier.preferredHeight(24.dp)) - if (question.description != null) { - Providers(AmbientContentAlpha provides ContentAlpha.medium) { + .background( + color = backgroundColor, + shape = MaterialTheme.shapes.small + ) + ) { Text( - text = stringResource(id = question.description), - style = MaterialTheme.typography.caption, + text = stringResource(id = question.questionText), + style = MaterialTheme.typography.subtitle1, modifier = Modifier .fillMaxWidth() - .padding(bottom = 24.dp, start = 8.dp, end = 8.dp) + .padding(vertical = 24.dp, horizontal = 16.dp) + ) + } + Spacer(modifier = Modifier.preferredHeight(24.dp)) + if (question.description != null) { + Providers(AmbientContentAlpha provides ContentAlpha.medium) { + Text( + text = stringResource(id = question.description), + style = MaterialTheme.typography.caption, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 24.dp, start = 8.dp, end = 8.dp) + ) + } + } + when (question.answer) { + is PossibleAnswer.SingleChoice -> SingleChoiceQuestion( + possibleAnswer = question.answer, + answer = answer as Answer.SingleChoice?, + onAnswerSelected = { answer -> onAnswer(Answer.SingleChoice(answer)) }, + modifier = Modifier.fillMaxWidth() + ) + is PossibleAnswer.MultipleChoice -> MultipleChoiceQuestion( + possibleAnswer = question.answer, + answer = answer as Answer.MultipleChoice?, + onAnswerSelected = { newAnswer, selected -> + // create the answer if it doesn't exist or + // update it based on the user's selection + if (answer == null) { + onAnswer(Answer.MultipleChoice(setOf(newAnswer))) + } else { + onAnswer(answer.withAnswerSelected(newAnswer, selected)) + } + }, + modifier = Modifier.fillMaxWidth() + ) + is PossibleAnswer.Action -> ActionQuestion( + questionId = question.id, + possibleAnswer = question.answer, + answer = answer as Answer.Action?, + onAction = onAction, + modifier = Modifier.fillMaxWidth() + ) + is PossibleAnswer.Slider -> SliderQuestion( + possibleAnswer = question.answer, + answer = answer as Answer.Slider?, + onAnswerSelected = { onAnswer(Answer.Slider(it)) }, + modifier = Modifier.fillMaxWidth() ) } - } - when (question.answer) { - is PossibleAnswer.SingleChoice -> SingleChoiceQuestion( - possibleAnswer = question.answer, - answer = answer as Answer.SingleChoice?, - onAnswerSelected = { answer -> onAnswer(Answer.SingleChoice(answer)) }, - modifier = Modifier.fillMaxWidth() - ) - is PossibleAnswer.MultipleChoice -> MultipleChoiceQuestion( - possibleAnswer = question.answer, - answer = answer as Answer.MultipleChoice?, - onAnswerSelected = { newAnswer, selected -> - // create the answer if it doesn't exist or - // update it based on the user's selection - if (answer == null) { - onAnswer(Answer.MultipleChoice(setOf(newAnswer))) - } else { - onAnswer(answer.withAnswerSelected(newAnswer, selected)) - } - }, - modifier = Modifier.fillMaxWidth() - ) - is PossibleAnswer.Action -> ActionQuestion( - questionId = question.id, - possibleAnswer = question.answer, - answer = answer as Answer.Action?, - onAction = onAction, - modifier = Modifier.fillMaxWidth() - ) - is PossibleAnswer.Slider -> SliderQuestion( - possibleAnswer = question.answer, - answer = answer as Answer.Slider?, - onAnswerSelected = { onAnswer(Answer.Slider(it)) }, - modifier = Modifier.fillMaxWidth() - ) } } } @@ -318,7 +320,8 @@ private fun PhotoQuestion( CoilImage( data = answer.result.uri, modifier = Modifier.fillMaxSize(), - fadeIn = true + fadeIn = true, + contentDescription = null ) } else { PhotoDefaultImage(modifier = Modifier.padding(horizontal = 86.dp, vertical = 74.dp)) @@ -330,7 +333,7 @@ private fun PhotoQuestion( .padding(vertical = 26.dp), verticalAlignment = Alignment.CenterVertically ) { - Icon(resource) + Icon(imageVector = resource, contentDescription = null) Spacer(modifier = Modifier.width(8.dp)) Text( text = stringResource( @@ -382,7 +385,8 @@ private fun PhotoDefaultImage( } Image( imageVector = vectorResource(id = assetId), - modifier = modifier + modifier = modifier, + contentDescription = null ) } diff --git a/Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/survey/SurveyScreen.kt b/Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/survey/SurveyScreen.kt index f6a186fa7a..f64c5d91f4 100644 --- a/Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/survey/SurveyScreen.kt +++ b/Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/survey/SurveyScreen.kt @@ -16,7 +16,6 @@ package com.example.compose.jetsurvey.survey -import androidx.compose.foundation.ScrollableColumn import androidx.compose.foundation.layout.ConstraintLayout import androidx.compose.foundation.layout.ExperimentalLayout import androidx.compose.foundation.layout.Row @@ -26,6 +25,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.preferredHeight import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.AmbientContentAlpha import androidx.compose.material.Button import androidx.compose.material.ContentAlpha @@ -124,26 +124,28 @@ fun SurveyResultScreen( @Composable private fun SurveyResult(result: SurveyState.Result, modifier: Modifier = Modifier) { - ScrollableColumn(modifier = modifier.fillMaxSize()) { - Spacer(modifier = Modifier.preferredHeight(44.dp)) - Text( - text = result.surveyResult.library, - style = MaterialTheme.typography.h3, - modifier = Modifier.padding(horizontal = 20.dp) - ) - Text( - text = stringResource( - result.surveyResult.result, - result.surveyResult.library - ), - style = MaterialTheme.typography.subtitle1, - modifier = Modifier.padding(20.dp) - ) - Text( - text = stringResource(result.surveyResult.description), - style = MaterialTheme.typography.body1, - modifier = Modifier.padding(horizontal = 20.dp) - ) + LazyColumn(modifier = modifier.fillMaxSize()) { + item { + Spacer(modifier = Modifier.preferredHeight(44.dp)) + Text( + text = result.surveyResult.library, + style = MaterialTheme.typography.h3, + modifier = Modifier.padding(horizontal = 20.dp) + ) + Text( + text = stringResource( + result.surveyResult.result, + result.surveyResult.library + ), + style = MaterialTheme.typography.subtitle1, + modifier = Modifier.padding(20.dp) + ) + Text( + text = stringResource(result.surveyResult.description), + style = MaterialTheme.typography.body1, + modifier = Modifier.padding(horizontal = 20.dp) + ) + } } } @@ -196,7 +198,7 @@ private fun SurveyTopAppBar( .padding(horizontal = 12.dp) .constrainAs(button) { end.linkTo(parent.end) } ) { - Icon(Icons.Filled.Close) + Icon(Icons.Filled.Close, contentDescription = stringResource(id = R.string.close)) } } diff --git a/Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/theme/Typography.kt b/Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/theme/Typography.kt index fb30a47165..88108a24cd 100644 --- a/Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/theme/Typography.kt +++ b/Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/theme/Typography.kt @@ -18,16 +18,19 @@ package com.example.compose.jetsurvey.theme import androidx.compose.material.Typography import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.font -import androidx.compose.ui.text.font.fontFamily import androidx.compose.ui.unit.sp import com.example.compose.jetsurvey.R -val MontserratFontFamily = fontFamily( - font(R.font.montserrat_regular), - font(R.font.montserrat_medium, FontWeight.Medium), - font(R.font.montserrat_semibold, FontWeight.SemiBold) +val MontserratFontFamily = FontFamily( + listOf( + Font(R.font.montserrat_regular), + Font(R.font.montserrat_medium, FontWeight.Medium), + Font(R.font.montserrat_semibold, FontWeight.SemiBold) + ) ) val Typography = Typography( diff --git a/Jetsurvey/app/src/main/res/values/strings.xml b/Jetsurvey/app/src/main/res/values/strings.xml index 2417fde8d0..50ea2ff2a5 100644 --- a/Jetsurvey/app/src/main/res/values/strings.xml +++ b/Jetsurvey/app/src/main/res/values/strings.xml @@ -35,6 +35,10 @@ NEXT PREVIOUS DONE + Back + Close + Show password + Hide password Which Jetpack library are you? diff --git a/Jetsurvey/buildSrc/src/main/java/com/example/compose/jetsurvey/buildsrc/dependencies.kt b/Jetsurvey/buildSrc/src/main/java/com/example/compose/jetsurvey/buildsrc/dependencies.kt index d7a2a88eaf..4650be9f3f 100644 --- a/Jetsurvey/buildSrc/src/main/java/com/example/compose/jetsurvey/buildsrc/dependencies.kt +++ b/Jetsurvey/buildSrc/src/main/java/com/example/compose/jetsurvey/buildsrc/dependencies.kt @@ -21,7 +21,7 @@ object Versions { } object Libs { - const val androidGradlePlugin = "com.android.tools.build:gradle:7.0.0-alpha04" + const val androidGradlePlugin = "com.android.tools.build:gradle:7.0.0-alpha05" const val jdkDesugar = "com.android.tools:desugar_jdk_libs:1.0.9" const val junit = "junit:junit:4.13" @@ -29,12 +29,12 @@ object Libs { const val material = "com.google.android.material:material:1.1.0" object Accompanist { - private const val version = "0.4.2" + private const val version = "0.5.0" const val coil = "dev.chrisbanes.accompanist:accompanist-coil:$version" } object Kotlin { - private const val version = "1.4.21" + private const val version = "1.4.21-2" const val stdlib = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$version" const val gradlePlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:$version" const val extensions = "org.jetbrains.kotlin:kotlin-android-extensions:$version" @@ -53,7 +53,7 @@ object Libs { object Compose { const val snapshot = "" - const val version = "1.0.0-alpha10" + const val version = "1.0.0-alpha11" @get:JvmStatic val snapshotUrl: String diff --git a/Jetsurvey/gradle/wrapper/gradle-wrapper.properties b/Jetsurvey/gradle/wrapper/gradle-wrapper.properties index f3b943a378..efe7b15a16 100644 --- a/Jetsurvey/gradle/wrapper/gradle-wrapper.properties +++ b/Jetsurvey/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu Jun 18 12:28:02 BST 2020 +#Thu Jan 28 22:46:06 GMT 2021 distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.1-bin.zip distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-rc-1-bin.zip +zipStoreBase=GRADLE_USER_HOME diff --git a/Owl/app/build.gradle b/Owl/app/build.gradle index aaee72fb1e..5b3cde23a9 100644 --- a/Owl/app/build.gradle +++ b/Owl/app/build.gradle @@ -70,7 +70,6 @@ android { } composeOptions { - kotlinCompilerVersion Libs.Kotlin.version kotlinCompilerExtensionVersion Libs.AndroidX.Compose.version } diff --git a/Owl/app/src/androidTest/java/com/example/owl/ui/NavigationTest.kt b/Owl/app/src/androidTest/java/com/example/owl/ui/NavigationTest.kt index 48bef31b83..a90448faf8 100644 --- a/Owl/app/src/androidTest/java/com/example/owl/ui/NavigationTest.kt +++ b/Owl/app/src/androidTest/java/com/example/owl/ui/NavigationTest.kt @@ -24,7 +24,6 @@ import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithSubstring import androidx.compose.ui.test.performClick -import androidx.test.platform.app.InstrumentationRegistry import com.example.owl.R import com.example.owl.model.courses import com.example.owl.ui.fakes.ProvideTestImageLoader @@ -45,20 +44,17 @@ class NavigationTest { */ @get:Rule val composeTestRule = createAndroidComposeRule() - lateinit var activity: ComponentActivity private fun startActivity(startDestination: String? = null) { - composeTestRule.activityRule.scenario.onActivity { - activity = it - composeTestRule.setContent { - Providers(AmbientBackDispatcher provides activity.onBackPressedDispatcher) { - ProvideWindowInsets { - ProvideTestImageLoader { - if (startDestination == null) { - NavGraph() - } else { - NavGraph(startDestination) - } + composeTestRule.setContent { + val backDispatcher = composeTestRule.activity.onBackPressedDispatcher + Providers(AmbientBackDispatcher provides backDispatcher) { + ProvideWindowInsets { + ProvideTestImageLoader { + if (startDestination == null) { + NavGraph() + } else { + NavGraph(startDestination) } } } @@ -106,7 +102,7 @@ class NavigationTest { fun coursesToDetailAndBack() { coursesToDetail() composeTestRule.runOnUiThread { - activity.onBackPressed() + composeTestRule.activity.onBackPressed() } // The first course should be shown @@ -114,17 +110,14 @@ class NavigationTest { } private fun getOnboardingFabLabel(): String { - return InstrumentationRegistry.getInstrumentation().targetContext.resources - .getString(R.string.continue_to_courses) + return composeTestRule.activity.resources.getString(R.string.label_continue_to_courses) } private fun getFeaturedCourseLabel(): String { - return InstrumentationRegistry.getInstrumentation().targetContext.resources - .getString(R.string.featured) + return composeTestRule.activity.resources.getString(R.string.featured) } private fun getCourseDesc(): String { - return InstrumentationRegistry.getInstrumentation().targetContext.resources - .getString(R.string.course_desc) + return composeTestRule.activity.resources.getString(R.string.course_desc) } } diff --git a/Owl/app/src/main/java/com/example/owl/ui/common/CourseListItem.kt b/Owl/app/src/main/java/com/example/owl/ui/common/CourseListItem.kt index 0e3792b055..a784baa057 100644 --- a/Owl/app/src/main/java/com/example/owl/ui/common/CourseListItem.kt +++ b/Owl/app/src/main/java/com/example/owl/ui/common/CourseListItem.kt @@ -67,6 +67,7 @@ fun CourseListItem( Row(modifier = Modifier.clickable(onClick = onClick)) { NetworkImage( url = course.thumbUrl, + contentDescription = null, modifier = Modifier.aspectRatio(1f) ) Column( @@ -90,6 +91,7 @@ fun CourseListItem( Icon( imageVector = Icons.Rounded.OndemandVideo, tint = MaterialTheme.colors.primary, + contentDescription = null, modifier = Modifier.preferredSize(iconSize) ) Text( @@ -107,6 +109,7 @@ fun CourseListItem( ) NetworkImage( url = course.instructor, + contentDescription = null, modifier = Modifier .preferredSize(28.dp) .clip(CircleShape) diff --git a/Owl/app/src/main/java/com/example/owl/ui/common/OutlinedAvatar.kt b/Owl/app/src/main/java/com/example/owl/ui/common/OutlinedAvatar.kt index f82067ba64..8f8012ff08 100644 --- a/Owl/app/src/main/java/com/example/owl/ui/common/OutlinedAvatar.kt +++ b/Owl/app/src/main/java/com/example/owl/ui/common/OutlinedAvatar.kt @@ -63,6 +63,7 @@ fun OutlinedAvatar( ) { NetworkImage( url = url, + contentDescription = null, modifier = Modifier .padding(outlineSize) .fillMaxSize() diff --git a/Owl/app/src/main/java/com/example/owl/ui/course/CourseDetails.kt b/Owl/app/src/main/java/com/example/owl/ui/course/CourseDetails.kt index ec5549eb63..d33a17dc5b 100644 --- a/Owl/app/src/main/java/com/example/owl/ui/course/CourseDetails.kt +++ b/Owl/app/src/main/java/com/example/owl/ui/course/CourseDetails.kt @@ -16,11 +16,11 @@ package com.example.owl.ui.course -import androidx.compose.animation.core.animateAsState +import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.Image -import androidx.compose.foundation.ScrollableColumn import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row @@ -32,8 +32,11 @@ import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.preferredHeight import androidx.compose.foundation.layout.preferredSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.AmbientContentAlpha import androidx.compose.material.ContentAlpha @@ -64,7 +67,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.gesture.scrollorientationlocking.Orientation import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.layout.WithConstraints import androidx.compose.ui.platform.AmbientDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource @@ -115,7 +117,7 @@ fun CourseDetails( upPress: () -> Unit ) { PinkTheme { - WithConstraints { + BoxWithConstraints { val sheetState = rememberSwipeableState(SheetState.Closed) val fabSize = with(AmbientDensity.current) { FabSize.toPx() } val dragRange = constraints.maxHeight - fabSize @@ -147,8 +149,8 @@ fun CourseDetails( LessonsSheet( course, openFraction, - constraints.maxWidth.toFloat(), - constraints.maxHeight.toFloat() + this@BoxWithConstraints.constraints.maxWidth.toFloat(), + this@BoxWithConstraints.constraints.maxHeight.toFloat() ) { state -> sheetState.animateTo(state) } @@ -164,10 +166,10 @@ private fun CourseDescription( upPress: () -> Unit ) { Surface(modifier = Modifier.fillMaxSize()) { - ScrollableColumn { - CourseDescriptionHeader(course, upPress) - CourseDescriptionBody(course) - RelatedCourses(course.id, selectCourse) + LazyColumn { + item { CourseDescriptionHeader(course, upPress) } + item { CourseDescriptionBody(course) } + item { RelatedCourses(course.id, selectCourse) } } } } @@ -180,6 +182,7 @@ private fun CourseDescriptionHeader( Box { NetworkImage( url = course.thumbUrl, + contentDescription = null, modifier = Modifier .fillMaxWidth() .scrim(colors = listOf(Color(0x80000000), Color(0x33000000))) @@ -193,11 +196,13 @@ private fun CourseDescriptionHeader( ) { IconButton(onClick = upPress) { Icon( - imageVector = Icons.Rounded.ArrowBack + imageVector = Icons.Rounded.ArrowBack, + contentDescription = stringResource(R.string.label_back) ) } Image( imageVector = vectorResource(id = R.drawable.ic_logo), + contentDescription = null, modifier = Modifier .padding(bottom = 4.dp) .preferredSize(24.dp) @@ -375,8 +380,8 @@ private fun Lessons( .graphicsLayer { alpha = lessonsAlpha } .statusBarsPadding() ) { - val scroll = rememberScrollState() - val appBarElevation by animateAsState(if (scroll.value > 0f) 4.dp else 0.dp) + val scroll = rememberLazyListState() + val appBarElevation by animateDpAsState(if (scroll.isScrolled) 4.dp else 0.dp) val appBarColor = if (appBarElevation > 0.dp) surfaceColor else Color.Transparent TopAppBar( backgroundColor = appBarColor, @@ -396,16 +401,19 @@ private fun Lessons( onClick = { updateSheet(SheetState.Closed) }, modifier = Modifier.align(Alignment.CenterVertically) ) { - Icon(imageVector = Icons.Rounded.ExpandMore) + Icon( + imageVector = Icons.Rounded.ExpandMore, + contentDescription = stringResource(R.string.label_collapse_lessons) + ) } } - ScrollableColumn( - scrollState = scroll, + LazyColumn( + state = scroll, contentPadding = AmbientWindowInsets.current.systemBars.toPaddingValues( top = false ) ) { - lessons.forEach { lesson -> + items(lessons) { lesson -> Lesson(lesson) Divider(startIndent = 128.dp) } @@ -426,7 +434,8 @@ private fun Lessons( ) { Icon( imageVector = Icons.Rounded.PlaylistPlay, - tint = MaterialTheme.colors.onPrimary + tint = MaterialTheme.colors.onPrimary, + contentDescription = stringResource(R.string.label_expand_lessons) ) } } @@ -442,6 +451,7 @@ private fun Lesson(lesson: Lesson) { ) { NetworkImage( url = lesson.imageUrl, + contentDescription = null, modifier = Modifier.preferredSize(112.dp, 64.dp) ) Column( @@ -462,6 +472,7 @@ private fun Lesson(lesson: Lesson) { ) { Icon( imageVector = Icons.Rounded.PlayCircleOutline, + contentDescription = null, modifier = Modifier.preferredSize(16.dp) ) Text( @@ -482,6 +493,9 @@ private fun Lesson(lesson: Lesson) { private enum class SheetState { Open, Closed } +private val LazyListState.isScrolled: Boolean + get() = firstVisibleItemIndex > 0 || firstVisibleItemScrollOffset > 0 + @Preview(name = "Course Details") @Composable private fun CourseDetailsPreview() { diff --git a/Owl/app/src/main/java/com/example/owl/ui/courses/Courses.kt b/Owl/app/src/main/java/com/example/owl/ui/courses/Courses.kt index 838d92a01c..7d0b8ff22c 100644 --- a/Owl/app/src/main/java/com/example/owl/ui/courses/Courses.kt +++ b/Owl/app/src/main/java/com/example/owl/ui/courses/Courses.kt @@ -61,7 +61,7 @@ fun Courses(selectCourse: (Long) -> Unit) { ) { tabs.forEach { tab -> BottomNavigationItem( - icon = { Icon(vectorResource(tab.icon)) }, + icon = { Icon(vectorResource(tab.icon), contentDescription = null) }, label = { Text(stringResource(tab.title).toUpperCase()) }, selected = tab == selectedTab, onClick = { setSelectedTab(tab) }, @@ -94,13 +94,17 @@ fun CoursesAppBar() { modifier = Modifier .padding(16.dp) .align(Alignment.CenterVertically), - imageVector = vectorResource(id = R.drawable.ic_lockup_white) + imageVector = vectorResource(id = R.drawable.ic_lockup_white), + contentDescription = null ) IconButton( modifier = Modifier.align(Alignment.CenterVertically), onClick = { /* todo */ } ) { - Icon(Icons.Filled.AccountCircle) + Icon( + imageVector = Icons.Filled.AccountCircle, + contentDescription = stringResource(R.string.label_profile) + ) } } } diff --git a/Owl/app/src/main/java/com/example/owl/ui/courses/FeaturedCourses.kt b/Owl/app/src/main/java/com/example/owl/ui/courses/FeaturedCourses.kt index 25199acbb6..a504b5c4a4 100644 --- a/Owl/app/src/main/java/com/example/owl/ui/courses/FeaturedCourses.kt +++ b/Owl/app/src/main/java/com/example/owl/ui/courses/FeaturedCourses.kt @@ -16,13 +16,15 @@ package com.example.owl.ui.courses -import androidx.compose.foundation.ScrollableColumn import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ConstraintLayout import androidx.compose.foundation.layout.ExperimentalLayout import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.preferredSize +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.AmbientElevationOverlay import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme @@ -56,7 +58,11 @@ fun FeaturedCourses( selectCourse: (Long) -> Unit, modifier: Modifier = Modifier ) { - ScrollableColumn(modifier = modifier.statusBarsPadding()) { + Column( + modifier = modifier + .verticalScroll(rememberScrollState()) + .statusBarsPadding() + ) { CoursesAppBar() StaggeredVerticalGrid( maxColumnWidth = 220.dp, @@ -95,6 +101,7 @@ fun FeaturedCourse( val (image, avatar, subject, name, steps, icon) = createRefs() NetworkImage( url = course.thumbUrl, + contentDescription = null, modifier = Modifier .aspectRatio(4f / 3f) .constrainAs(image) { @@ -142,6 +149,7 @@ fun FeaturedCourse( Icon( imageVector = Icons.Rounded.OndemandVideo, tint = MaterialTheme.colors.primary, + contentDescription = null, modifier = Modifier .preferredSize(16.dp) .constrainAs(icon) { diff --git a/Owl/app/src/main/java/com/example/owl/ui/courses/MyCourses.kt b/Owl/app/src/main/java/com/example/owl/ui/courses/MyCourses.kt index 5a3896f37f..0d28bceb64 100644 --- a/Owl/app/src/main/java/com/example/owl/ui/courses/MyCourses.kt +++ b/Owl/app/src/main/java/com/example/owl/ui/courses/MyCourses.kt @@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.preferredHeight import androidx.compose.foundation.layout.preferredWidth import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier diff --git a/Owl/app/src/main/java/com/example/owl/ui/courses/SearchCourses.kt b/Owl/app/src/main/java/com/example/owl/ui/courses/SearchCourses.kt index 430a0153ff..b851554f2b 100644 --- a/Owl/app/src/main/java/com/example/owl/ui/courses/SearchCourses.kt +++ b/Owl/app/src/main/java/com/example/owl/ui/courses/SearchCourses.kt @@ -18,11 +18,12 @@ package com.example.owl.ui.courses import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image -import androidx.compose.foundation.ScrollableColumn import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.text.BasicTextField import androidx.compose.material.AmbientContentColor import androidx.compose.material.Icon @@ -37,6 +38,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.tooling.preview.Preview @@ -53,10 +55,10 @@ fun SearchCourses( modifier: Modifier = Modifier ) { val (searchTerm, updateSearchTerm) = remember { mutableStateOf(TextFieldValue("")) } - ScrollableColumn(modifier = modifier.statusBarsPadding()) { - AppBar(searchTerm, updateSearchTerm) + LazyColumn(modifier = modifier.statusBarsPadding()) { + item { AppBar(searchTerm, updateSearchTerm) } val filteredTopics = getTopics(searchTerm.text, topics) - filteredTopics.forEach { topic -> + items(filteredTopics) { topic -> Text( text = topic.name, style = MaterialTheme.typography.h5, @@ -98,6 +100,7 @@ private fun AppBar( TopAppBar(elevation = 0.dp) { Image( imageVector = vectorResource(id = R.drawable.ic_search), + contentDescription = null, modifier = Modifier .padding(16.dp) .align(Alignment.CenterVertically) @@ -119,7 +122,10 @@ private fun AppBar( modifier = Modifier.align(Alignment.CenterVertically), onClick = { /* todo */ } ) { - Icon(Icons.Filled.AccountCircle) + Icon( + imageVector = Icons.Filled.AccountCircle, + contentDescription = stringResource(R.string.label_profile) + ) } } } diff --git a/Owl/app/src/main/java/com/example/owl/ui/onboarding/Onboarding.kt b/Owl/app/src/main/java/com/example/owl/ui/onboarding/Onboarding.kt index ea48a9de6e..a6d99f7544 100644 --- a/Owl/app/src/main/java/com/example/owl/ui/onboarding/Onboarding.kt +++ b/Owl/app/src/main/java/com/example/owl/ui/onboarding/Onboarding.kt @@ -16,10 +16,9 @@ package com.example.owl.ui.onboarding -import androidx.compose.animation.DpPropKey -import androidx.compose.animation.core.FloatPropKey -import androidx.compose.animation.core.transitionDefinition -import androidx.compose.animation.transition +import androidx.compose.animation.core.animateDp +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.updateTransition import androidx.compose.foundation.Image import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement @@ -52,6 +51,7 @@ import androidx.compose.material.icons.rounded.Explore import androidx.compose.material.primarySurface import androidx.compose.runtime.Composable import androidx.compose.runtime.Providers +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -60,8 +60,6 @@ import androidx.compose.ui.draw.scale import androidx.compose.ui.layout.Layout import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.semantics.contentDescription -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 @@ -83,16 +81,15 @@ fun Onboarding(onboardingComplete: () -> Unit) { topBar = { AppBar() }, backgroundColor = MaterialTheme.colors.primarySurface, floatingActionButton = { - val fabLabel = stringResource(id = R.string.continue_to_courses) FloatingActionButton( onClick = onboardingComplete, modifier = Modifier .navigationBarsPadding() - .semantics { - contentDescription = fabLabel - } ) { - Icon(Icons.Rounded.Explore) + Icon( + imageVector = Icons.Rounded.Explore, + contentDescription = stringResource(R.string.label_continue_to_courses) + ) } } ) { innerPadding -> @@ -133,13 +130,17 @@ private fun AppBar() { ) { Image( imageVector = vectorResource(id = OwlTheme.images.lockupLogo), + contentDescription = null, modifier = Modifier.padding(16.dp) ) IconButton( modifier = Modifier.padding(16.dp), onClick = { /* todo */ } ) { - Icon(Icons.Filled.Settings) + Icon( + imageVector = Icons.Filled.Settings, + contentDescription = stringResource(R.string.label_settings) + ) } } } @@ -159,44 +160,44 @@ private fun TopicsGrid(modifier: Modifier = Modifier) { private enum class SelectionState { Unselected, Selected } -private val CornerRadius = DpPropKey("Corner Radius") -private val SelectedAlpha = FloatPropKey("Selected Alpha") -private val CheckScale = FloatPropKey("Check Scale") - -private val TopicSelect = transitionDefinition { - state(SelectionState.Selected) { - this[CornerRadius] = 28.dp - this[SelectedAlpha] = 0.8f - this[CheckScale] = 1f - } - state(SelectionState.Unselected) { - this[CornerRadius] = 0.dp - this[SelectedAlpha] = 0f - this[CheckScale] = 0.6f - } -} - @Composable private fun TopicChip(topic: Topic) { val (selected, onSelected) = remember { mutableStateOf(false) } - val selectionState = transition( - definition = TopicSelect, - toState = if (selected) SelectionState.Selected else SelectionState.Unselected + val transition = updateTransition( + targetState = if (selected) SelectionState.Selected else SelectionState.Unselected ) + val corerRadius by transition.animateDp { state -> + when (state) { + SelectionState.Unselected -> 0.dp + SelectionState.Selected -> 28.dp + } + } + val selectedAlpha by transition.animateFloat { state -> + when (state) { + SelectionState.Unselected -> 0f + SelectionState.Selected -> 0.8f + } + } + val checkScale by transition.animateFloat { state -> + when (state) { + SelectionState.Unselected -> 0.6f + SelectionState.Selected -> 1f + } + } Surface( modifier = Modifier.padding(4.dp), elevation = OwlTheme.elevations.card, - shape = MaterialTheme.shapes.medium.copy(topLeft = CornerSize(selectionState[CornerRadius])) + shape = MaterialTheme.shapes.medium.copy(topLeft = CornerSize(corerRadius)) ) { Row(modifier = Modifier.toggleable(value = selected, onValueChange = onSelected)) { Box { NetworkImage( url = topic.imageUrl, + contentDescription = null, modifier = Modifier .preferredSize(width = 72.dp, height = 72.dp) .aspectRatio(1f) ) - val selectedAlpha = selectionState[SelectedAlpha] if (selectedAlpha > 0f) { Surface( color = pink500.copy(alpha = selectedAlpha), @@ -204,8 +205,9 @@ private fun TopicChip(topic: Topic) { ) { Icon( imageVector = Icons.Filled.Done, + contentDescription = null, tint = MaterialTheme.colors.onPrimary.copy(alpha = selectedAlpha), - modifier = Modifier.scale(selectionState[CheckScale]) + modifier = Modifier.scale(checkScale) ) } } @@ -225,6 +227,7 @@ private fun TopicChip(topic: Topic) { Providers(AmbientContentAlpha provides ContentAlpha.medium) { Icon( imageVector = vectorResource(R.drawable.ic_grain), + contentDescription = null, modifier = Modifier .padding(start = 16.dp) .preferredSize(12.dp) diff --git a/Owl/app/src/main/java/com/example/owl/ui/theme/Type.kt b/Owl/app/src/main/java/com/example/owl/ui/theme/Type.kt index 06d145e1db..b31e5e1c3c 100644 --- a/Owl/app/src/main/java/com/example/owl/ui/theme/Type.kt +++ b/Owl/app/src/main/java/com/example/owl/ui/theme/Type.kt @@ -18,17 +18,17 @@ package com.example.owl.ui.theme import androidx.compose.material.Typography import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.font.font -import androidx.compose.ui.text.font.fontFamily import androidx.compose.ui.unit.em import androidx.compose.ui.unit.sp import com.example.owl.R -private val fonts = fontFamily( - font(R.font.rubik_regular), - font(R.font.rubik_medium, FontWeight.W500), - font(R.font.rubik_bold, FontWeight.Bold) +private val fonts = FontFamily( + Font(R.font.rubik_regular), + Font(R.font.rubik_medium, FontWeight.W500), + Font(R.font.rubik_bold, FontWeight.Bold) ) val typography = typographyFromDefaults( diff --git a/Owl/app/src/main/java/com/example/owl/ui/utils/NetworkImage.kt b/Owl/app/src/main/java/com/example/owl/ui/utils/NetworkImage.kt index 51cd087248..c814b52433 100644 --- a/Owl/app/src/main/java/com/example/owl/ui/utils/NetworkImage.kt +++ b/Owl/app/src/main/java/com/example/owl/ui/utils/NetworkImage.kt @@ -43,6 +43,7 @@ import okhttp3.HttpUrl @Composable fun NetworkImage( url: String, + contentDescription: String?, modifier: Modifier = Modifier, contentScale: ContentScale = ContentScale.Crop, placeholderColor: Color? = MaterialTheme.colors.compositedOnSurface(0.2f) @@ -50,6 +51,7 @@ fun NetworkImage( CoilImage( data = url, modifier = modifier, + contentDescription = contentDescription, contentScale = contentScale, loading = { if (placeholderColor != null) { diff --git a/Owl/app/src/main/res/values/strings.xml b/Owl/app/src/main/res/values/strings.xml index 7d50fece2f..59efe8a368 100644 --- a/Owl/app/src/main/res/values/strings.xml +++ b/Owl/app/src/main/res/values/strings.xml @@ -14,16 +14,21 @@ --> Owl + Back Choose topics that interest you - Continue to courses + Continue to courses + Profile + Settings My Courses Featured Search %1$d / %2$d + Expand lessons sheet + Collapse lessons sheet This video course introduces the photography of structures, including urban and rural buildings, monuments, and less traditional structures. Instruction includes the handling of equipment and methods used to capture building interiors and exteriors. The discussion will be about the handling of distortion, varied light sources, and perspective. diff --git a/Owl/buildSrc/src/main/java/com/example/owl/buildsrc/Dependencies.kt b/Owl/buildSrc/src/main/java/com/example/owl/buildsrc/Dependencies.kt index c9279f2f10..cc80f361d9 100644 --- a/Owl/buildSrc/src/main/java/com/example/owl/buildsrc/Dependencies.kt +++ b/Owl/buildSrc/src/main/java/com/example/owl/buildsrc/Dependencies.kt @@ -21,17 +21,17 @@ object Versions { } object Libs { - const val androidGradlePlugin = "com.android.tools.build:gradle:7.0.0-alpha04" + const val androidGradlePlugin = "com.android.tools.build:gradle:7.0.0-alpha05" const val junit = "junit:junit:4.13" object Accompanist { - private const val version = "0.4.2" + private const val version = "0.5.0" const val coil = "dev.chrisbanes.accompanist:accompanist-coil:$version" const val insets = "dev.chrisbanes.accompanist:accompanist-insets:$version" } object Kotlin { - private const val version = "1.4.21" + private const val version = "1.4.21-2" const val stdlib = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$version" const val gradlePlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:$version" const val extensions = "org.jetbrains.kotlin:kotlin-android-extensions:$version" @@ -46,11 +46,11 @@ object Libs { object AndroidX { const val coreKtx = "androidx.core:core-ktx:1.5.0-beta01" - const val navigation = "androidx.navigation:navigation-compose:1.0.0-alpha05" + const val navigation = "androidx.navigation:navigation-compose:1.0.0-alpha06" object Compose { const val snapshot = "" - const val version = "1.0.0-alpha10" + const val version = "1.0.0-alpha11" const val animation = "androidx.compose.animation:animation:$version" const val foundation = "androidx.compose.foundation:foundation:$version" diff --git a/Owl/gradle/wrapper/gradle-wrapper.properties b/Owl/gradle/wrapper/gradle-wrapper.properties index 089b9f390f..28ff446a21 100644 --- a/Owl/gradle/wrapper/gradle-wrapper.properties +++ b/Owl/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-rc-1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/Rally/app/build.gradle b/Rally/app/build.gradle index 81e6e6cb51..a38598bad4 100644 --- a/Rally/app/build.gradle +++ b/Rally/app/build.gradle @@ -77,7 +77,6 @@ android { } composeOptions { - kotlinCompilerVersion Libs.Kotlin.version kotlinCompilerExtensionVersion Libs.AndroidX.Compose.version } diff --git a/Rally/app/src/androidTest/assets/circle_100.png b/Rally/app/src/androidTest/assets/circle_100.png index 22d19ae60b..9cfb33d2a3 100644 Binary files a/Rally/app/src/androidTest/assets/circle_100.png and b/Rally/app/src/androidTest/assets/circle_100.png differ diff --git a/Rally/app/src/androidTest/java/com/example/compose/rally/AnimatingCircleTests.kt b/Rally/app/src/androidTest/java/com/example/compose/rally/AnimatingCircleTests.kt index d0c49fe38c..fb41b8032b 100644 --- a/Rally/app/src/androidTest/java/com/example/compose/rally/AnimatingCircleTests.kt +++ b/Rally/app/src/androidTest/java/com/example/compose/rally/AnimatingCircleTests.kt @@ -50,6 +50,7 @@ class AnimatingCircleTests { @Test fun circleAnimation_idle_screenshot() { + composeTestRule.mainClock.autoAdvance = true showAnimatedCircle() assertScreenshotMatchesGolden("circle_done", composeTestRule.onRoot()) } @@ -71,18 +72,18 @@ class AnimatingCircleTests { @Test fun circleAnimation_animationDone_screenshot() { - compareTimeScreenshot(1400, "circle_done") + compareTimeScreenshot(1500, "circle_done") } private fun compareTimeScreenshot(timeMs: Long, goldenName: String) { // Start with a paused clock - composeTestRule.clockTestRule.pauseClock() + composeTestRule.mainClock.autoAdvance = false // Start the unit under test showAnimatedCircle() // Advance clock (keeping it paused) - composeTestRule.clockTestRule.advanceClock(timeMs) + composeTestRule.mainClock.advanceTimeBy(timeMs) // Take screenshot and compare with golden image in androidTest/assets assertScreenshotMatchesGolden(goldenName, composeTestRule.onRoot()) diff --git a/Rally/app/src/main/java/com/example/compose/rally/ui/components/CommonUi.kt b/Rally/app/src/main/java/com/example/compose/rally/ui/components/CommonUi.kt index 35102c07ca..0080ac051b 100644 --- a/Rally/app/src/main/java/com/example/compose/rally/ui/components/CommonUi.kt +++ b/Rally/app/src/main/java/com/example/compose/rally/ui/components/CommonUi.kt @@ -117,6 +117,7 @@ private fun BaseRow( Providers(AmbientContentAlpha provides ContentAlpha.medium) { Icon( imageVector = Icons.Filled.ChevronRight, + contentDescription = null, modifier = Modifier .padding(end = 12.dp) .preferredSize(24.dp) diff --git a/Rally/app/src/main/java/com/example/compose/rally/ui/components/DetailsScreen.kt b/Rally/app/src/main/java/com/example/compose/rally/ui/components/DetailsScreen.kt index 7b2cdec146..dde0ce6af0 100644 --- a/Rally/app/src/main/java/com/example/compose/rally/ui/components/DetailsScreen.kt +++ b/Rally/app/src/main/java/com/example/compose/rally/ui/components/DetailsScreen.kt @@ -16,13 +16,14 @@ package com.example.compose.rally.ui.components -import androidx.compose.foundation.ScrollableColumn import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.preferredHeight +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.Card import androidx.compose.material.MaterialTheme import androidx.compose.material.Text @@ -44,14 +45,17 @@ fun StatementBody( circleLabel: String, rows: @Composable (T) -> Unit ) { - ScrollableColumn { + Column(modifier = Modifier.verticalScroll(rememberScrollState())) { Box(Modifier.padding(16.dp)) { val accountsProportion = items.extractProportions { amounts(it) } val circleColors = items.map { colors(it) } AnimatedCircle( accountsProportion, circleColors, - Modifier.preferredHeight(300.dp).align(Alignment.Center).fillMaxWidth() + Modifier + .preferredHeight(300.dp) + .align(Alignment.Center) + .fillMaxWidth() ) Column(modifier = Modifier.align(Alignment.Center)) { Text( diff --git a/Rally/app/src/main/java/com/example/compose/rally/ui/components/RallyAnimatedCircle.kt b/Rally/app/src/main/java/com/example/compose/rally/ui/components/RallyAnimatedCircle.kt index c2fda2b195..cff6f49b70 100644 --- a/Rally/app/src/main/java/com/example/compose/rally/ui/components/RallyAnimatedCircle.kt +++ b/Rally/app/src/main/java/com/example/compose/rally/ui/components/RallyAnimatedCircle.kt @@ -17,13 +17,15 @@ package com.example.compose.rally.ui.components import androidx.compose.animation.core.CubicBezierEasing -import androidx.compose.animation.core.FloatPropKey import androidx.compose.animation.core.LinearOutSlowInEasing -import androidx.compose.animation.core.transitionDefinition +import androidx.compose.animation.core.MutableTransitionState +import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.tween -import androidx.compose.animation.transition +import androidx.compose.animation.core.updateTransition import androidx.compose.foundation.Canvas import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size @@ -33,8 +35,6 @@ import androidx.compose.ui.platform.AmbientDensity import androidx.compose.ui.unit.dp private const val DividerLengthInDegrees = 1.8f -private val AngleOffset = FloatPropKey("angle") -private val Shift = FloatPropKey("shift") /** * A donut chart that animates when loaded. @@ -45,12 +45,43 @@ fun AnimatedCircle( colors: List, modifier: Modifier = Modifier ) { + val currentState = remember { + MutableTransitionState(AnimatedCircleProgress.START) + .apply { targetState = AnimatedCircleProgress.END } + } val stroke = with(AmbientDensity.current) { Stroke(5.dp.toPx()) } - val state = transition( - definition = CircularTransition, - initState = AnimatedCircleProgress.START, - toState = AnimatedCircleProgress.END - ) + val transition = updateTransition(currentState) + val angleOffset by transition.animateFloat( + transitionSpec = { + tween( + delayMillis = 500, + durationMillis = 900, + easing = LinearOutSlowInEasing + ) + } + ) { progress -> + if (progress == AnimatedCircleProgress.START) { + 0f + } else { + 360f + } + } + val shift by transition.animateFloat( + transitionSpec = { + tween( + delayMillis = 500, + durationMillis = 900, + easing = CubicBezierEasing(0f, 0.75f, 0.35f, 0.85f) + ) + } + ) { progress -> + if (progress == AnimatedCircleProgress.START) { + 0f + } else { + 30f + } + } + Canvas(modifier) { val innerRadius = (size.minDimension - stroke.width) / 2 val halfSize = size / 2.0f @@ -59,9 +90,9 @@ fun AnimatedCircle( halfSize.height - innerRadius ) val size = Size(innerRadius * 2, innerRadius * 2) - var startAngle = state[Shift] - 90f + var startAngle = shift - 90f proportions.forEachIndexed { index, proportion -> - val sweep = proportion * state[AngleOffset] + val sweep = proportion * angleOffset drawArc( color = colors[index], startAngle = startAngle + DividerLengthInDegrees / 2, @@ -76,26 +107,3 @@ fun AnimatedCircle( } } private enum class AnimatedCircleProgress { START, END } - -private val CircularTransition = transitionDefinition { - state(AnimatedCircleProgress.START) { - this[AngleOffset] = 0f - this[Shift] = 0f - } - state(AnimatedCircleProgress.END) { - this[AngleOffset] = 360f - this[Shift] = 30f - } - transition(fromState = AnimatedCircleProgress.START, toState = AnimatedCircleProgress.END) { - AngleOffset using tween( - delayMillis = 500, - durationMillis = 900, - easing = CubicBezierEasing(0f, 0.75f, 0.35f, 0.85f) - ) - Shift using tween( - delayMillis = 500, - durationMillis = 900, - easing = LinearOutSlowInEasing - ) - } -} diff --git a/Rally/app/src/main/java/com/example/compose/rally/ui/components/TopAppBar.kt b/Rally/app/src/main/java/com/example/compose/rally/ui/components/TopAppBar.kt index 16bed1e2d3..55bd3d79cf 100644 --- a/Rally/app/src/main/java/com/example/compose/rally/ui/components/TopAppBar.kt +++ b/Rally/app/src/main/java/com/example/compose/rally/ui/components/TopAppBar.kt @@ -16,10 +16,11 @@ package com.example.compose.rally.ui.components -import androidx.compose.animation.animateAsState +import androidx.compose.animation.animateColorAsState import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.tween +import androidx.compose.foundation.InteractionState import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -82,7 +83,7 @@ private fun RallyTab( delayMillis = TabFadeInAnimationDelay ) } - val tabTintColor by animateAsState( + val tabTintColor by animateColorAsState( targetValue = if (selected) color else color.copy(alpha = InactiveTabOpacity), animationSpec = animSpec ) @@ -94,6 +95,7 @@ private fun RallyTab( .selectable( selected = selected, onClick = onSelected, + interactionState = remember { InteractionState() }, indication = rememberRipple( bounded = false, radius = Dp.Unspecified, @@ -101,7 +103,7 @@ private fun RallyTab( ) ) ) { - Icon(imageVector = icon, tint = tabTintColor) + Icon(imageVector = icon, contentDescription = null, tint = tabTintColor) if (selected) { Spacer(Modifier.preferredWidth(12.dp)) Text(text, color = tabTintColor) diff --git a/Rally/app/src/main/java/com/example/compose/rally/ui/overview/OverviewScreen.kt b/Rally/app/src/main/java/com/example/compose/rally/ui/overview/OverviewScreen.kt index de60bb0839..7609b2f403 100644 --- a/Rally/app/src/main/java/com/example/compose/rally/ui/overview/OverviewScreen.kt +++ b/Rally/app/src/main/java/com/example/compose/rally/ui/overview/OverviewScreen.kt @@ -16,7 +16,6 @@ package com.example.compose.rally.ui.overview -import androidx.compose.foundation.ScrollableColumn import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -26,6 +25,8 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.preferredHeight +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.Card import androidx.compose.material.Icon import androidx.compose.material.IconButton @@ -55,7 +56,11 @@ import com.example.compose.rally.ui.components.formatAmount @Composable fun OverviewBody(onScreenChange: (RallyScreen) -> Unit = {}) { - ScrollableColumn(contentPadding = PaddingValues(16.dp)) { + Column( + modifier = Modifier + .padding(16.dp) + .verticalScroll(rememberScrollState()) + ) { AlertCard() Spacer(Modifier.preferredHeight(RallyDefaultPadding)) AccountsCard(onScreenChange) @@ -97,7 +102,9 @@ private fun AlertCard() { @Composable private fun AlertHeader(onClickSeeAll: () -> Unit) { Row( - modifier = Modifier.padding(RallyDefaultPadding).fillMaxWidth(), + modifier = Modifier + .padding(RallyDefaultPadding) + .fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween ) { Text( @@ -133,7 +140,7 @@ private fun AlertItem(message: String) { onClick = {}, modifier = Modifier.align(Alignment.Top) ) { - Icon(Icons.Filled.Sort) + Icon(Icons.Filled.Sort, contentDescription = stringResource(id = R.string.sort)) } } } @@ -239,7 +246,9 @@ private fun BillsCard(onScreenChange: (RallyScreen) -> Unit) { private fun SeeAllButton(onClick: () -> Unit) { TextButton( onClick = onClick, - modifier = Modifier.preferredHeight(44.dp).fillMaxWidth() + modifier = Modifier + .preferredHeight(44.dp) + .fillMaxWidth() ) { Text(stringResource(R.string.see_all)) } diff --git a/Rally/app/src/main/java/com/example/compose/rally/ui/theme/RallyTheme.kt b/Rally/app/src/main/java/com/example/compose/rally/ui/theme/RallyTheme.kt index b402b86c36..6a78117740 100644 --- a/Rally/app/src/main/java/com/example/compose/rally/ui/theme/RallyTheme.kt +++ b/Rally/app/src/main/java/com/example/compose/rally/ui/theme/RallyTheme.kt @@ -23,21 +23,21 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.compositeOver import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.font.font -import androidx.compose.ui.text.font.fontFamily import androidx.compose.ui.unit.em import androidx.compose.ui.unit.sp import com.example.compose.rally.R -private val EczarFontFamily = fontFamily( - font(R.font.eczar_regular), - font(R.font.eczar_semibold, FontWeight.SemiBold) +private val EczarFontFamily = FontFamily( + Font(R.font.eczar_regular), + Font(R.font.eczar_semibold, FontWeight.SemiBold) ) -private val RobotoCondensed = fontFamily( - font(R.font.robotocondensed_regular), - font(R.font.robotocondensed_light, FontWeight.Light), - font(R.font.robotocondensed_bold, FontWeight.Bold) +private val RobotoCondensed = FontFamily( + Font(R.font.robotocondensed_regular), + Font(R.font.robotocondensed_light, FontWeight.Light), + Font(R.font.robotocondensed_bold, FontWeight.Bold) ) /** diff --git a/Rally/app/src/main/res/values/strings.xml b/Rally/app/src/main/res/values/strings.xml index 7f7ee86637..63742cb56b 100644 --- a/Rally/app/src/main/res/values/strings.xml +++ b/Rally/app/src/main/res/values/strings.xml @@ -21,5 +21,6 @@ • • • • • Accounts Bills + Sort SEE ALL diff --git a/Rally/buildSrc/src/main/java/com/example/compose/rally/buildsrc/dependencies.kt b/Rally/buildSrc/src/main/java/com/example/compose/rally/buildsrc/dependencies.kt index 90640065cc..976d2a2f24 100644 --- a/Rally/buildSrc/src/main/java/com/example/compose/rally/buildsrc/dependencies.kt +++ b/Rally/buildSrc/src/main/java/com/example/compose/rally/buildsrc/dependencies.kt @@ -21,7 +21,7 @@ object Versions { } object Libs { - const val androidGradlePlugin = "com.android.tools.build:gradle:7.0.0-alpha04" + const val androidGradlePlugin = "com.android.tools.build:gradle:7.0.0-alpha05" const val jdkDesugar = "com.android.tools:desugar_jdk_libs:1.0.9" const val junit = "junit:junit:4.13" @@ -29,7 +29,7 @@ object Libs { const val material = "com.google.android.material:material:1.1.0" object Kotlin { - private const val version = "1.4.21" + private const val version = "1.4.21-2" const val stdlib = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$version" const val gradlePlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:$version" const val extensions = "org.jetbrains.kotlin:kotlin-android-extensions:$version" @@ -48,7 +48,7 @@ object Libs { object Compose { const val snapshot = "" - const val version = "1.0.0-alpha10" + const val version = "1.0.0-alpha11" const val core = "androidx.compose.ui:ui:$version" const val foundation = "androidx.compose.foundation:foundation:$version" diff --git a/Rally/gradle/wrapper/gradle-wrapper.properties b/Rally/gradle/wrapper/gradle-wrapper.properties index c7948985bf..562ce658c0 100644 --- a/Rally/gradle/wrapper/gradle-wrapper.properties +++ b/Rally/gradle/wrapper/gradle-wrapper.properties @@ -19,4 +19,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https://services.gradle.org/distributions/gradle-6.8-rc-1-bin.zip +distributionUrl=https://services.gradle.org/distributions/gradle-6.8.1-bin.zip