From 6a81198087e91853127cb6bb54bed2f80f47078a Mon Sep 17 00:00:00 2001 From: Jolanda Verhoef Date: Wed, 19 May 2021 11:47:46 +0200 Subject: [PATCH] [Jetnews] Improve RTL layouts --- .../example/jetsnack/ui/components/Grid.kt | 2 +- .../ui/components/QuantitySelector.kt | 46 +++++++++++-------- .../example/jetsnack/ui/components/Snacks.kt | 7 ++- .../java/com/example/jetsnack/ui/home/Home.kt | 19 +++++--- .../com/example/jetsnack/ui/home/cart/Cart.kt | 30 +++--------- .../jetsnack/ui/home/search/Categories.kt | 4 +- .../example/jetsnack/ui/home/search/Search.kt | 4 +- .../jetsnack/ui/snackdetail/SnackDetail.kt | 7 ++- .../java/com/example/jetsnack/ui/utils/Rtl.kt | 40 ++++++++++++++++ 9 files changed, 99 insertions(+), 60 deletions(-) create mode 100644 Jetsnack/app/src/main/java/com/example/jetsnack/ui/utils/Rtl.kt diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Grid.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Grid.kt index 00bcb470b0..26d3151cc4 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Grid.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Grid.kt @@ -57,7 +57,7 @@ fun VerticalGrid( val columnY = Array(columns) { 0 } placeables.forEachIndexed { index, placeable -> val column = index % columns - placeable.place( + placeable.placeRelative( x = column * itemWidth, y = columnY[column] ) 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 0df7ca473b..34ad354284 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 @@ -16,7 +16,9 @@ package com.example.jetsnack.ui.components +import android.content.res.Configuration.UI_MODE_NIGHT_YES import androidx.compose.animation.Crossfade +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.widthIn import androidx.compose.material.ContentAlpha @@ -28,14 +30,15 @@ import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Remove import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.constraintlayout.compose.ChainStyle -import androidx.constraintlayout.compose.ConstraintLayout import com.example.jetsnack.R import com.example.jetsnack.ui.theme.JetsnackTheme @@ -46,9 +49,7 @@ fun QuantitySelector( increaseItemCount: () -> Unit, modifier: Modifier = Modifier ) { - ConstraintLayout(modifier = modifier) { - val (qty, minus, quantity, plus) = createRefs() - createHorizontalChain(qty, minus, quantity, plus, chainStyle = ChainStyle.Packed) + Row(modifier = modifier) { CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) { Text( text = stringResource(R.string.quantity), @@ -56,25 +57,19 @@ fun QuantitySelector( color = JetsnackTheme.colors.textSecondary, modifier = Modifier .padding(end = 18.dp) - .constrainAs(qty) { - start.linkTo(parent.start) - linkTo(top = parent.top, bottom = parent.bottom) - } + .align(Alignment.CenterVertically) ) } JetsnackGradientTintedIconButton( imageVector = Icons.Default.Remove, onClick = decreaseItemCount, contentDescription = stringResource(R.string.label_decrease), - modifier = Modifier.constrainAs(minus) { - centerVerticallyTo(quantity) - linkTo(top = parent.top, bottom = parent.bottom) - } + modifier = Modifier.align(Alignment.CenterVertically) ) Crossfade( targetState = count, modifier = Modifier - .constrainAs(quantity) { baseline.linkTo(qty.baseline) } + .align(Alignment.CenterVertically) ) { Text( text = "$it", @@ -89,16 +84,15 @@ fun QuantitySelector( imageVector = Icons.Default.Add, onClick = increaseItemCount, contentDescription = stringResource(R.string.label_increase), - modifier = Modifier.constrainAs(plus) { - end.linkTo(parent.end) - centerVerticallyTo(quantity) - linkTo(top = parent.top, bottom = parent.bottom) - } + modifier = Modifier.align(Alignment.CenterVertically) ) } } -@Preview +@Preview("Default") +@Preview("Large font", fontScale = 2f) +@Preview("Small font", fontScale = 0.5f) +@Preview("Dark theme", uiMode = UI_MODE_NIGHT_YES) @Composable fun QuantitySelectorPreview() { JetsnackTheme { @@ -107,3 +101,15 @@ fun QuantitySelectorPreview() { } } } + +@Preview("RTL") +@Composable +fun QuantitySelectorPreviewRtl() { + JetsnackTheme { + JetsnackSurface { + CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) { + QuantitySelector(1, {}, {}) + } + } + } +} 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 f8b6816c6a..7d34de8c02 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 @@ -41,6 +41,7 @@ import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.ArrowBack import androidx.compose.material.icons.outlined.ArrowForward import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -58,6 +59,7 @@ import com.example.jetsnack.model.Snack import com.example.jetsnack.model.SnackCollection import com.example.jetsnack.model.snacks import com.example.jetsnack.ui.theme.JetsnackTheme +import com.example.jetsnack.ui.utils.mirroringIcon import com.google.accompanist.coil.rememberCoilPainter private val HighlightCardWidth = 170.dp @@ -100,7 +102,10 @@ fun SnackCollection( modifier = Modifier.align(Alignment.CenterVertically) ) { Icon( - imageVector = Icons.Outlined.ArrowForward, + imageVector = mirroringIcon( + ltrIcon = Icons.Outlined.ArrowForward, + rtlIcon = Icons.Outlined.ArrowBack + ), tint = JetsnackTheme.colors.brand, contentDescription = null ) 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 f8ff78ccf9..45144cc727 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 @@ -270,10 +270,10 @@ private fun JetsnackBottomNavLayout( height = itemPlaceables.maxByOrNull { it.height }?.height ?: 0 ) { val indicatorLeft = indicatorIndex.value * unselectedWidth - indicatorPlaceable.place(x = indicatorLeft.toInt(), y = 0) + indicatorPlaceable.placeRelative(x = indicatorLeft.toInt(), y = 0) var x = 0 itemPlaceables.forEach { placeable -> - placeable.place(x = x, y = 0) + placeable.placeRelative(x = x, y = 0) x += placeable.width } } @@ -311,12 +311,17 @@ private fun JetsnackBottomNavItemLayout( ) { Layout( content = { - Box(Modifier.layoutId("icon"), content = icon) + Box( + modifier = Modifier + .layoutId("icon") + .padding(horizontal = TextIconSpacing), + content = icon + ) val scale = lerp(0.6f, 1f, animationProgress) Box( modifier = Modifier .layoutId("text") - .padding(start = TextIconSpacing) + .padding(horizontal = TextIconSpacing) .graphicsLayer { alpha = animationProgress scaleX = scale @@ -355,9 +360,9 @@ private fun MeasureScope.placeTextAndIcon( val textX = iconX + iconPlaceable.width return layout(width, height) { - iconPlaceable.place(iconX.toInt(), iconY) + iconPlaceable.placeRelative(iconX.toInt(), iconY) if (animationProgress != 0f) { - textPlaceable.place(textX.toInt(), textY) + textPlaceable.placeRelative(textX.toInt(), textY) } } } @@ -376,7 +381,7 @@ private fun JetsnackBottomNavIndicator( ) } -private val TextIconSpacing = 4.dp +private val TextIconSpacing = 2.dp private val BottomNavHeight = 56.dp private val BottomNavLabelTransformOrigin = TransformOrigin(0f, 0.5f) private val BottomNavIndicatorShape = RoundedCornerShape(percent = 50) 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 6e02fa85f0..c6133e4a83 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 @@ -209,13 +209,7 @@ fun CartItem( style = MaterialTheme.typography.subtitle1, color = JetsnackTheme.colors.textSecondary, modifier = Modifier.constrainAs(name) { - linkTo( - start = image.end, - startMargin = 16.dp, - end = remove.start, - endMargin = 16.dp, - bias = 0f - ) + start.linkTo(image.end, margin = 16.dp) } ) IconButton( @@ -238,20 +232,15 @@ fun CartItem( style = MaterialTheme.typography.body1, color = JetsnackTheme.colors.textHelp, modifier = Modifier.constrainAs(tag) { - linkTo( - start = image.end, - startMargin = 16.dp, - end = parent.end, - endMargin = 16.dp, - bias = 0f - ) + start.linkTo(image.end, margin = 16.dp) } ) Spacer( Modifier .height(8.dp) .constrainAs(priceSpacer) { - linkTo(top = tag.bottom, bottom = price.top) + top.linkTo(tag.bottom) + bottom.linkTo(price.top) } ) Text( @@ -259,13 +248,7 @@ fun CartItem( style = MaterialTheme.typography.subtitle1, color = JetsnackTheme.colors.textPrimary, modifier = Modifier.constrainAs(price) { - linkTo( - start = image.end, - end = quantity.start, - startMargin = 16.dp, - endMargin = 16.dp, - bias = 0f - ) + start.linkTo(image.end, margin = 16.dp) } ) QuantitySelector( @@ -279,7 +262,8 @@ fun CartItem( ) JetsnackDivider( Modifier.constrainAs(divider) { - linkTo(start = parent.start, end = parent.end) + start.linkTo(parent.start) + end.linkTo(parent.end) top.linkTo(parent.bottom) } ) 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 e0b7c2fce7..0b18c0f326 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 @@ -138,11 +138,11 @@ private fun SearchCategory( width = constraints.maxWidth, height = constraints.minHeight ) { - textPlaceable.place( + textPlaceable.placeRelative( x = 0, y = (constraints.maxHeight - textPlaceable.height) / 2 // centered ) - imagePlaceable.place( + imagePlaceable.placeRelative( // image is placed to end of text i.e. will overflow to the end (but be clipped) x = textWidth, y = (constraints.maxHeight - imagePlaceable.height) / 2 // centered 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 5c5c84e005..b9ac1cb114 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 @@ -35,7 +35,6 @@ import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.ArrowBack import androidx.compose.material.icons.outlined.Search import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -62,6 +61,7 @@ import com.example.jetsnack.model.SnackRepo import com.example.jetsnack.ui.components.JetsnackDivider import com.example.jetsnack.ui.components.JetsnackSurface import com.example.jetsnack.ui.theme.JetsnackTheme +import com.example.jetsnack.ui.utils.mirroringBackIcon import com.google.accompanist.insets.statusBarsPadding @Composable @@ -190,7 +190,7 @@ private fun SearchBar( if (searchFocused) { IconButton(onClick = onClearQuery) { Icon( - imageVector = Icons.Outlined.ArrowBack, + imageVector = mirroringBackIcon(), tint = JetsnackTheme.colors.iconPrimary, contentDescription = stringResource(R.string.label_back) ) 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 e7c6c88f9f..d08e655a8e 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 @@ -38,8 +38,6 @@ import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.ArrowBack import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.key @@ -74,6 +72,7 @@ import com.example.jetsnack.ui.components.SnackImage import com.example.jetsnack.ui.theme.JetsnackTheme import com.example.jetsnack.ui.theme.Neutral8 import com.example.jetsnack.ui.utils.formatPrice +import com.example.jetsnack.ui.utils.mirroringBackIcon import com.google.accompanist.insets.navigationBarsPadding import com.google.accompanist.insets.statusBarsPadding import kotlin.math.max @@ -133,7 +132,7 @@ private fun Up(upPress: () -> Unit) { ) ) { Icon( - imageVector = Icons.Outlined.ArrowBack, + imageVector = mirroringBackIcon(), tint = JetsnackTheme.colors.iconInteractive, contentDescription = stringResource(R.string.label_back) ) @@ -323,7 +322,7 @@ private fun CollapsingImageLayout( width = constraints.maxWidth, height = imageY + imageWidth ) { - imagePlaceable.place(imageX, imageY) + imagePlaceable.placeRelative(imageX, imageY) } } } diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/utils/Rtl.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/utils/Rtl.kt new file mode 100644 index 0000000000..0bff71cd0a --- /dev/null +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/utils/Rtl.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.jetsnack.ui.utils + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.ArrowBack +import androidx.compose.material.icons.outlined.ArrowForward +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.unit.LayoutDirection + +/** + * Returns the correct icon based on the current layout direction. + */ +@Composable +fun mirroringIcon(ltrIcon: ImageVector, rtlIcon: ImageVector): ImageVector = + if (LocalLayoutDirection.current == LayoutDirection.Ltr) ltrIcon else rtlIcon + +/** + * Returns the correct back navigation icon based on the current layout direction. + */ +@Composable +fun mirroringBackIcon() = mirroringIcon( + ltrIcon = Icons.Outlined.ArrowBack, rtlIcon = Icons.Outlined.ArrowForward +)