how to set value to a remember val and not recompose - android-jetpack-compose

Blow the code, I want to set value to currentDegree on animate end, but when I do it, it recomposes. How to make it not recompose?
#Composable
fun RotateImageCompose(width: Dp, height: Dp, resId: Int, targetDegree: Float) {
val currentDegree = remember { mutableStateOf(0f) }
var diff = currentDegree.value - targetDegree
.
.
.
val dregree by animateFloatAsState(targetValue = diff, animationSpec = tween(
easing = LinearEasing,
durationMillis = 180
), finishedListener = {
//this code cause the issue
currentDegree.value = it
})
Image(
painter = painterResource(id = resId),
contentDescription = null,
modifier = Modifier
.rotate(dregree)
.size(width, height)
)
}

Just use :
val currentDegree = remember {0f}
That way your value is immutable.
Please look here for a detailed explanation: https://stackoverflow.com/a/66178263/5513788

I save the value outside the compose

Related

How can we make a dynamic rating bar with user interaction enable in android jetpack composable ? I have try different library but no luck [duplicate]

I'm trying to implement a rating bar. I refer to https://gist.github.com/vitorprado/0ae4ad60c296aefafba4a157bb165e60 but I don't understand anything from this code. It works but when I use this code the stars don't have rounded corners. I want to implement something like the following :
I made very basic sample for this, it would give the basic idea for creating rating bar with sample border and filled png files.
#Composable
private fun RatingBar(
modifier: Modifier = Modifier,
rating: Float,
spaceBetween: Dp = 0.dp
) {
val image = ImageBitmap.imageResource(id = R.drawable.star)
val imageFull = ImageBitmap.imageResource(id = R.drawable.star_full)
val totalCount = 5
val height = LocalDensity.current.run { image.height.toDp() }
val width = LocalDensity.current.run { image.width.toDp() }
val space = LocalDensity.current.run { spaceBetween.toPx() }
val totalWidth = width * totalCount + spaceBetween * (totalCount - 1)
Box(
modifier
.width(totalWidth)
.height(height)
.drawBehind {
drawRating(rating, image, imageFull, space)
})
}
private fun DrawScope.drawRating(
rating: Float,
image: ImageBitmap,
imageFull: ImageBitmap,
space: Float
) {
val totalCount = 5
val imageWidth = image.width.toFloat()
val imageHeight = size.height
val reminder = rating - rating.toInt()
val ratingInt = (rating - reminder).toInt()
for (i in 0 until totalCount) {
val start = imageWidth * i + space * i
drawImage(
image = image,
topLeft = Offset(start, 0f)
)
}
drawWithLayer {
for (i in 0 until totalCount) {
val start = imageWidth * i + space * i
// Destination
drawImage(
image = imageFull,
topLeft = Offset(start, 0f)
)
}
val end = imageWidth * totalCount + space * (totalCount - 1)
val start = rating * imageWidth + ratingInt * space
val size = end - start
// Source
drawRect(
Color.Transparent,
topLeft = Offset(start, 0f),
size = Size(size, height = imageHeight),
blendMode = BlendMode.SrcIn
)
}
}
private fun DrawScope.drawWithLayer(block: DrawScope.() -> Unit) {
with(drawContext.canvas.nativeCanvas) {
val checkPoint = saveLayer(null, null)
block()
restoreToCount(checkPoint)
}
}
Usage
Column {
RatingBar(rating = 3.7f, spaceBetween = 3.dp)
RatingBar(rating = 2.5f, spaceBetween = 2.dp)
RatingBar(rating = 4.5f, spaceBetween = 2.dp)
RatingBar(rating = 1.3f, spaceBetween = 4.dp)
}
Result
Also created a library that uses gestures, other png files and vectors as rating items is available here.
RatingBar(
rating = rating,
space = 2.dp,
imageBackground = imageBackground,
imageForeground = imageForeground,
animationEnabled = false,
gestureEnabled = true,
itemSize = 60.dp
) {
rating = it
}
You can pass the custom drawable as icon. check this code.
Replace your RatingStar() function as it is using canvas to draw star, instead pass the custom drawable.
#Composable
private fun starShow(){
val icon = if (isSelected)
//your selected drawable
else
//your unselected drawable
Icon(
painter = painterResource(id = icon),
contentDescription = null,
tint = MyColor.starColor)
}

Bug in default behavior of ScalingLazyColumn (Jetpack Compose Wear OS)

I'm using ScalingLazyColumn with a very long Text inside as follows:
#Preview(device = Devices.WEAR_OS_SMALL_ROUND, showSystemUi = true)
#Composable
fun Test(modifier: Modifier = Modifier) {
val scalingLazyState = remember { ScalingLazyListState() }
val focusRequester = remember { FocusRequester() }
Scaffold(
modifier = modifier,
positionIndicator = { PositionIndicator(scalingLazyListState = scalingLazyState) }
) {
ScalingLazyColumn(
modifier = Modifier.scrollableColumn(focusRequester, scalingLazyState),
state = scalingLazyState,
) {
item {
Text(
longText,
Modifier
.padding(top = 20.dp, start = 16.dp, end = 16.dp, bottom = 48.dp),
textAlign = TextAlign.Center,
)
}
}
}
}
val longText =
"Take the plunge\n" +
"\n" +
"commit oneself to a course of action about which one is nervous.\n" +
"\n" +
"\"she wondered whether to enter for the race, but decided to take the plunge\"\n" +
"\"They're finally taking the plunge and getting married.\"\n" +
"\n" +
"\n" +
"plunge:\n" +
"jump or dive quickly and energetically.\n" +
"\"our daughters whooped as they plunged into the sea\"\n"
But for some reason when I launch the app the focus goes to the bottom of the text, instead of the beginning, which looks like a bug. I've tried playing with different parameters of ScalingLazyColumn (anchorType, autoCentering, scalingParams) to no avail.
Any idea how to fix it and make the ScalingLazyColumn focus on the beginning of the first element when I launch the app?
Switching off autoCentering is an option, but I would try and avoid it in most cases as it will will make handling getting the padding right on different devices sizes more difficult and often results in being able to over scroll the list items either at the beginning or the end.
I am not sure exactly what you want to achieve when you say that you want the focus to be on the start of the first item but the following should give you what you need.
Set the state initial item to 0
Set the anchor type to ScalingLazyListAnchorType.ItemStart
Remove top padding from your item
Apply an offset to the state initialItem initialCenterItemScrollOffset to shift the start of you item up a little.
Optionally adjust the autoCentering to make sure that the limit of the scrolling matches the initial position selected in the state
#Preview(device = Devices.WEAR_OS_SMALL_ROUND, showSystemUi = true)
#Composable
fun SingleItemSLCWithLongText(modifier: Modifier = Modifier) {
val scalingLazyState = remember { ScalingLazyListState(initialCenterItemIndex = 0, initialCenterItemScrollOffset = 80) }
val focusRequester = remember { FocusRequester() }
Scaffold(
modifier = modifier.background(Color.Black),
positionIndicator = { PositionIndicator(scalingLazyListState = scalingLazyState) }
) {
ScalingLazyColumn(
modifier = Modifier.scrollableColumn(focusRequester, scalingLazyState),
autoCentering = AutoCenteringParams(itemIndex = 0, itemOffset = 80),
state = scalingLazyState,
anchorType = ScalingLazyListAnchorType.ItemStart
) {
item {
Text(
longText,
Modifier
.padding(start = 16.dp, end = 16.dp, bottom = 48.dp),
textAlign = TextAlign.Center,
)
}
}
}
}
Here is a screenshot of how the screen initially looks
Initial screen
This test activity let's you play with all the params to see starting position
https://github.com/google/horologist/blob/a1241ff25b7008f7c1337f4425b98d14ce30d96d/sample/src/main/java/com/google/android/horologist/scratch/ScratchActivity.kt
After a few hours of frustration I finally found a solution.
If you read the documentation for ScalingLazyColumn it says:
"If the developer wants custom control over position and spacing they
can switch off autoCentering and provide contentPadding."
So all you need to do is to just add autoCentering = null in ScalingLazyColumn.
This is a working code where the focus will be in the beginning of the Text:
val scalingLazyState = remember { ScalingLazyListState() }
val focusRequester = remember { FocusRequester() }
Scaffold(
modifier = modifier,
positionIndicator = { PositionIndicator(scalingLazyListState = scalingLazyState) }
) {
ScalingLazyColumn(
modifier = Modifier.scrollableColumn(focusRequester, scalingLazyState),
state = scalingLazyState,
autoCentering = null,
) {
item {
Text(
longText,
Modifier
.padding(top = 20.dp, start = 16.dp, end = 16.dp, bottom = 48.dp),
textAlign = TextAlign.Center,
)
}
}
}
ScalingLazyListState defaults to the center of the second item (index 1). You can tell it to instead start in the first item and even jn the ScalingLazyColumn parameters use the start of items.
val scalingLazyState = remember { ScalingLazyListState(initialCenterItemIndex = 0) }
val focusRequester = remember { FocusRequester() }
Scaffold(
modifier = Modifier,
positionIndicator = { PositionIndicator(scalingLazyListState = scalingLazyState) }
) {
ScalingLazyColumn(
modifier = Modifier.scrollableColumn(focusRequester, scalingLazyState),
state = scalingLazyState,
anchorType = ScalingLazyListAnchorType.ItemStart
) {
item {
Text(
longText,
Modifier
.padding(top = 20.dp, start = 16.dp, end = 16.dp, bottom = 48.dp),
textAlign = TextAlign.Center,
)
}
}
}

BottomNavigationItems padding

Is there any way to remove this padding from the BottomNavigationItem?
Image
If I have very large text, I have to use ResponsiveText to manage this, but that's not what I intended. What I need is that it doesn't have that side padding/margin, both on the left and on the right, in order to occupy as much space as possible.
My code:
#Composable
fun BottomNavBar(
backStackEntryState: State<NavBackStackEntry?>,
navController: NavController,
bottomNavItems: List<NavigationItem>
) {
BottomNavigation(
backgroundColor = DarkGray.copy(alpha = 0.6f),
elevation = Dimen0,
modifier = Modifier
.padding(Dimen10, Dimen20, Dimen10, Dimen20)
.clip(RoundedCornerShape(Dimen13, Dimen13, Dimen13, Dimen13))
) {
bottomNavItems.forEach { item ->
val isSelected = item.route == backStackEntryState.value?.destination?.route
BottomNavigationItem(
icon = {
Icon(
painter = painterResource(id = item.icon.orZero()),
contentDescription = stringResource(id = item.title)
)
},
label = {
ResponsiveText(
text = stringResource(id = item.title),
textStyle = TextStyle14,
maxLines = 1
)
},
selectedContentColor = Color.White,
unselectedContentColor = Color.White,
alwaysShowLabel = true,
selected = isSelected,
onClick = {
navController.navigate(item.route) {
navController.graph.startDestinationRoute?.let { route ->
popUpTo(route = route) {
saveState = true
}
}
launchSingleTop = true
restoreState = true
}
},
modifier = if (isSelected) {
Modifier
.clip(RoundedCornerShape(Dimen13, Dimen13, Dimen13, Dimen13))
.background(color = DarkGray)
} else {
Modifier.background(color = Color.Unspecified)
}
)
}
}
}
Apparently this is currently (I am using compose version '1.2.0-rc03') not possible when using BottomNavigation, as there is padding set for each element in these lines:
.padding(horizontal = BottomNavigationItemHorizontalPadding)
Here is what is said about this value:
/**
* Padding at the start and end of a [BottomNavigationItem]
*/
private val BottomNavigationItemHorizontalPadding = 12.dp
[Solution]
Just copy BottomNavigation from androidx and remov this line:
.padding(horizontal = BottomNavigationItemHorizontalPadding)
However, it is necessary that the first and last elements still have padding, so add the innerHorizontalPaddings parameter to the your CustomBottomNavigation constructor
There are a few more changes, you can see the full code of my CustomBottomNavigation here
Example of usage:
CustomBottomNavigation(
...,
innerHorizontalPaddings = 12.dp
) {
items.forEach { item ->
BottomNavigationItem(
icon = {
Icon(...)
},
label = {
Text(
...
softWrap = false,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.padding(horizontal = 2.dp)
)
},
...
)
}
}
Another solution is to wrap the label in a BoxWithConstraints and draw outside of it:
BottomNavigationItem(
label = {
/**
* Because of [BottomNavigationItemHorizontalPadding] (12.dp), we need to
* think (and draw) outside the box.
*/
BoxWithConstraints {
Text(
modifier = Modifier
.wrapContentWidth(unbounded = true)
.requiredWidth(maxWidth + 24.dp), // 24.dp = the padding * 2
text = "Centered text and clipped at the end if too long",
softWrap = false,
textAlign = TextAlign.Center
)
}
},
...
)
To get a little bit of padding, you can set requiredWidth(maxWidth + 18.dp).
With this solution, you don't need to copy the enire BottomNavigation :)

expand Image as much as screen size jetpack compose

I have a LazyColumn and some childs in it like below
LazyColumn(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(space = 16.dp)
) {
item {
Child(
modifier = Modifier,
firstImage = fakeImage,
secondImage = fakeImage,
onImageClick = {}
)
}
item {
Child(
modifier = Modifier,
firstImage = fakeImage,
secondImage = fakeImage,
onImageClick = {}
)
}
}
here is what is inside of TwoPiecesLayout
#ExperimentalMaterialApi
#Composable
fun Child(
modifier: Modifier,
firstImage: Image,
secondImage: Image,
onImageClick: (Image) -> Unit
) {
val height = (LocalConfiguration.current.screenWidthDp / 2) - 56
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(space = 16.dp)
) {
ImageCell(
modifier = Modifier
.weight(
weight = 1F
)
.height(
height = height.dp
),
image = firstImage,
onImageClick = {
onImageClick(firstImage)
}
)
ImageCell(
modifier = Modifier
.weight(
weight = 3F
)
.height(
height = height.dp
),
image = secondImage,
onImageClick = {
onImageClick(secondImage)
}
)
}
}
when every of Images in Child clicked I want to expand their size as much as screen's size just like the material design choreography
https://storage.cloud.google.com/non-spec-apps/mio-direct-embeds/videos/FADE.mp4
how can I do this?
This is not just for image, with basically any Composable, you can apply this method
var expanded by remember { mutableStateOf (false) }
val animF by animateFloatAsState(
initialState = 0.25f,
targetState = if (expanded) 1f else 0.25f
)
MyComposable(
Modifier.fillMaxSize(animF) // pass the animated Fraction here
.clickable { expanded = !expanded }
)
This will oscillate between 0.25 of the entire screen to 1f of the same, upon clicking the Composable.
See? Super-easy, barely an inconvenience.

how can we create a circular checkbox in jetpack compose?

It is usually possible to assign different shapes to a composable using a modifier, but this is not done in this composable.
I want the part marked in the image to be a circle
You can see the code I wrote below
#Composable
fun StandardCheckbox(
text: String = "",
checked: Boolean,
onCheckedChange: ((Boolean) -> Unit)?,
) {
Row(
Modifier.padding(horizontal = SpaceMedium)
) {
Checkbox(
modifier = Modifier
.clip(CircleShape),
checked = checked,
onCheckedChange = onCheckedChange,
enabled = true,
colors = CheckboxDefaults.colors(
checkedColor = MaterialTheme.colors.primary,
checkmarkColor = MaterialTheme.colors.onPrimary,
uncheckedColor = MaterialTheme.colors.onBackground.copy(alpha = 0.3f)
)
)
Spacer(modifier = Modifier.width(SpaceSmall))
Text(
text = text,
color = MaterialTheme.colors.primary,
modifier = Modifier.clickable {
if (onCheckedChange != null) {
onCheckedChange(!checked)
}
}
)
}
}
In order to achieve a circular checkbox with a native experience, and retain the body color and click ripple effect, and keep it simple, IconButton is the best choice.
#Composable
fun CircleCheckbox(selected: Boolean, enabled: Boolean = true, onChecked: () -> Unit) {
val color = MaterialTheme.colorScheme
val imageVector = if (selected) Icons.Filled.CheckCircle else Icons.Outlined.Circle
val tint = if (selected) color.primary.copy(alpha = 0.8f) else color.white.copy(alpha = 0.8f)
val background = if (selected) color.white else Color.Transparent
IconButton(onClick = { onChecked() },
modifier = Modifier.offset(x = 4.dp, y = 4.dp),
enabled = enabled) {
Icon(imageVector = imageVector, tint = tint,
modifier = Modifier.background(background, shape = CircleShape),
contentDescription = "checkbox")
}
}
The code below is from CheckboxImpl composable
Canvas(modifier.wrapContentSize(Alignment.Center).requiredSize(CheckboxSize)) {
val strokeWidthPx = floor(StrokeWidth.toPx())
drawBox(
boxColor = boxColor,
borderColor = borderColor,
radius = RadiusSize.toPx(),
strokeWidth = strokeWidthPx
)
drawCheck(
checkColor = checkColor,
checkFraction = checkDrawFraction,
crossCenterGravitation = checkCenterGravitationShiftFraction,
strokeWidthPx = strokeWidthPx,
drawingCache = checkCache
)
}
drawBox will always draw a rounded rectangle. It can't be be customised.
To implement the circular checkbox you need to write a custom Composable and draw Circle instead of Rectangle. You can use RadioButton and CheckboxImpl composable as reference.

Resources