I've below composable with image inside box (which is inside LazyColumn). When I click on box, image scale is changing (which I've verified), but image on UI is not changing. That is, UI stays same, but it should change. What am I missing here?
#Composable
fun ImageContent(url: String) {
var scale by remember { mutableStateOf(1f) }
val state = rememberTransformableState { zoomChange, _, _ ->
scale = (scale * zoomChange).coerceIn(1f, 3f)
}
var expanded by remember { mutableStateOf(false) }
Box(
modifier = Modifier
.fillMaxWidth()
.height(300.dp)
.padding(start = 4.dp, end = 4.dp)
.clip(RoundedCornerShape(16.dp))
.graphicsLayer(scaleX = scale, scaleY = scale)
.transformable(state = state)
.clickable { expanded = !expanded },
) {
Image(
modifier = Modifier.fillMaxSize(),
painter = rememberImagePainter(data = url),
contentScale = if (expanded) ContentScale.Fit else ContentScale.Crop
)
}
}
I didn't mentioned it earlier, but I found the cause of issue and resolved it. transformations function was forcing the ui to not change when content scale change
Image(
modifier = Modifier.fillMaxSize(),
painter = rememberImagePainter(
data = url,
builder = {
placeholder(R.drawable.ic_filled_placeholder)
crossfade(300)
// This line was forcing the ui to not change when content scale change
transformations(RoundedCornersTransformation(16f))
}
),
contentScale = if (expanded) ContentScale.Fit else ContentScale.Crop,
contentDescription = null
)
Related
I want to show a list of elements in which each one is an expanded image on top and a title on the bottom.
The image must expand all of its available width but it must have a fixed height.
If the image is big enough I can do it, but if the image has a portrait aspect ratio it doesn't expand as I would like.
This is what I have right now:
The first image has a portrait aspect ratio, I would like it to expand to the available width but maintaining a fixed height.
This is the code for the list and each element:
#Composable
private fun List(memes: List<Meme>) {
LazyColumn(
modifier = Modifier
.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(12.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
itemsIndexed(memes) { index, meme ->
ListElement(meme, isFirst = index == 0, isLast = index == memes.size - 1)
}
}
}
#Composable
private fun ListElement(meme: Meme, isFirst: Boolean, isLast: Boolean) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(
start = marginLat,
top = if (isFirst) 8.dp else 0.dp,
end = marginLat,
bottom = if (isLast) 8.dp else 0.dp
),
elevation = 4.dp
) {
Column {
Image(
painter = rememberImagePainter(meme.url),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxWidth()
.height(100.dp)
.padding(8.dp)
.clip(RoundedCornerShape(4.dp))
)
Text(meme.name, style = MaterialTheme.typography.h6, modifier = Modifier.padding(8.dp))
}
}
}
use ContentScale.FillWidth
Image(
painter = rememberImagePainter(meme.url),
contentDescription = null,
contentScale = ContentScale.FillWidth,
modifier = Modifier
.fillMaxWidth()
.height(100.dp)
.padding(8.dp)
.clip(RoundedCornerShape(4.dp))
)
I want to display a ripple affect after click on a view and also change it alpha after click on it.
However, the ripple effect only work well if alpha change from 0.5->1, when alpha change from 1->0.5, the ripple effect don't display fully.
fun Greeting2(name: String) {
val isProcessing = remember { mutableStateOf(false) }
Column(
modifier = Modifier
.padding(36.dp)
.alpha(if (isProcessing.value) 0.5f else 1f)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false),
) {
isProcessing.value = !isProcessing.value
}
) {
Image(
painter = painterResource(R.drawable.ic_btn_speak_now),
contentDescription = "",
modifier = Modifier
.width(80.dp)
.height(80.dp)
)
Text(text = "Hello $name!")
}
}
Here is the demo. Any way to achieve both alpha and ripple effect together?
I guess it might be because of the mechanism how the ripple is displayed internally. Maybe it's a sort of clash between the recompositions occuring because of both the changing alpha and the propagating ripple. To fix that, you can just wrap your column in another composable, like so.
#Preview
#Composable
fun Greeting2() {
val name = "Android!" // I used preview so had to remove the parameter
var isProcessing by remember { mutableStateOf(false) }
val alpha by animateFloatAsState(targetValue = if (isProcessing) 0.5f else 1f, animationSpec = keyframes { durationMillis = 1 })
Box(
Modifier
.alpha(alpha)
){
Column(
modifier = Modifier
.padding(36.dp)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false),
) {
isProcessing = !isProcessing
}
) {
Image(
painter = painterResource(R.drawable.ic_launcher_foreground),
contentDescription = "",
modifier = Modifier
.width(80.dp)
.height(80.dp)
)
Text(text = "Hello $name!")
}
}
}
I might have made some modifications but you get the idea. Also, I wrapped in a Box instead of a Surface since it defaults to a background.
By default all the Composables, much like views are defined in form of Rects, i.e., four specific corner points with distinct properties. Now, if I want to implement something like this
(This is just an example, I want to implement this with much complex shapes (PATHS using Canvas))
So, when the user clicks on either of the triangles, the specific codeblock implemented for that triangle should be executed.
I got almost no possible approaches to this, so please inform if anyone does, thanks!
You can accomplish it by creating a custom shape and applying the image that is drawn latest in a Box
#Composable
private fun DividedImage() {
Box(
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
) {
val context = LocalContext.current
val shapeLeft = GenericShape { size: Size, layoutDirection: LayoutDirection ->
val width = size.width
val height = size.height
moveTo(0f, 0f)
lineTo(0f, height)
lineTo(width, height)
close()
}
val modifierLeft = Modifier
.fillMaxWidth()
.height(200.dp)
.graphicsLayer {
clip = true
shape = shapeLeft
}
.clickable {
Toast
.makeText(context, "LEFT", Toast.LENGTH_SHORT)
.show()
}
.border(3.dp, Color.Green)
val modifierRight = Modifier
.fillMaxWidth()
.height(200.dp)
.clickable {
Toast
.makeText(context, "RIGHT", Toast.LENGTH_SHORT)
.show()
}
.border(3.dp, Color.Red)
Image(
modifier = modifierRight,
painter = painterResource(id = R.drawable.landscape2),
contentDescription = null,
contentScale = ContentScale.FillBounds
)
Image(
modifier = modifierLeft,
painter = painterResource(id = R.drawable.landscape1),
contentDescription = null,
contentScale = ContentScale.FillBounds
)
}
}
I have an image and I want to draw dark rectangle over it with a transparent circle, so the result will be something like this:
I have ended up with this code:
Box(modifier = Modifier
.clip(RectangleShape)
.fillMaxSize()
.background(Color.Black)
.pointerInput(Unit) {
detectTransformGestures { centroid, pan, zoom, rotation ->
scale *= zoom
}
}) {
Image(
modifier = Modifier
.align(Alignment.Center)
.graphicsLayer(
scaleX = maxOf(.2f, minOf(5f, scale)),
scaleY = maxOf(.2f, minOf(5f, scale))
),
bitmap = bitmap.asImageBitmap(),
contentDescription = null
)
Canvas(modifier = Modifier.fillMaxSize(), onDraw = {
drawRect(Color.Black.copy(alpha = 0.8f))
drawCircle(
Color.Transparent,
style = Fill,
blendMode = BlendMode.Clear
)
})
}
But it seems like it just draws a dark circle on top of the image instead of clearing darken rectangle...
It would be also super handy if you would suggest how to crop image based on this circle coordinates.
You need to use clipPath in this case.
Canvas(modifier = Modifier.fillMaxSize(), onDraw = {
val circlePath = Path().apply {
addOval(Rect(center, size.minDimension / 2))
}
clipPath(circlePath, clipOp = ClipOp.Difference) {
drawRect(SolidColor(Color.Black.copy(alpha = 0.8f)))
}
})
You don't need to necessarily use clipPath, you can use Porterduff(BlendMode) modes too, which is actually preferred way when you check sample codes or questions for clearing or removing some pixels or manipulating pixels.
It only requires small modifications to your code to work, you can check my answer how to apply Porterduff modes here.
1- adding graphics layer with alpha less than 1f
.graphicsLayer {
alpha = .99f
}
will fix the issue.
Full implementation
Box(
modifier = Modifier
.background(Color.Black)
.fillMaxSize()
) {
Image(
modifier = Modifier.fillMaxSize(),
painter = painterResource(id = R.drawable.landscape),
contentScale = ContentScale.Crop,
contentDescription = null
)
Canvas(
modifier = Modifier
.fillMaxSize()
// ONLY ADD THIS
.graphicsLayer {
alpha = .99f
}
) {
// Destination
drawRect(Color.Black.copy(alpha = 0.8f))
// Source
drawCircle(
color = Color.Transparent,
blendMode = BlendMode.Clear
)
}
}
2- Saving to a layer
with(drawContext.canvas.nativeCanvas) {
val checkPoint = saveLayer(null, null)
// Destination
drawRect(Color.Black.copy(alpha = 0.8f))
// Source
drawCircle(
color = Color.Transparent,
blendMode = BlendMode.Clear
)
restoreToCount(checkPoint)
}
Full Implementation
Canvas(modifier = Modifier
.fillMaxSize()
) {
with(drawContext.canvas.nativeCanvas) {
val checkPoint = saveLayer(null, null)
// Destination
drawRect(Color.Black.copy(alpha = 0.8f))
// Source
drawCircle(
color = Color.Transparent,
blendMode = BlendMode.Clear
)
restoreToCount(checkPoint)
}
}
}
Result
Also you can check here to get familiar with BlendModes, path operations and more about canvas.
I need to implement LazyColumn with top fading edge effect. On Android I use fade gradient for ListView or RecyclerView, but couldn't find any solution for Jetpack Compose!
I tried to modify canvas:
#Composable
fun Screen() {
Box(
Modifier
.fillMaxWidth()
.background(color = Color.Yellow)
) {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.drawWithContent {
val colors = listOf(Color.Transparent, Color.Black)
drawContent()
drawRect(
brush = Brush.verticalGradient(colors),
blendMode = BlendMode.DstIn
)
}
) {
itemsIndexed((1..1000).toList()) { item, index ->
Text(
text = "Item $item: $index value",
modifier = Modifier.padding(12.dp),
color = Color.Red,
fontSize = 24.sp
)
}
}
}
}
But have wrong result:
What you could do is place a Spacer on top of the list, and draw a gradient on that Box. Make the Box small so only a small portion of the list has the overlay. Make the color the same as the background of the screen, and it will look like the content is fading.
val screenBackgroundColor = MaterialTheme.colors.background
Box(Modifier.fillMaxSize()) {
LazyColumn(Modifier.fillMaxSize()) {
//your items
}
//Gradient overlay
Spacer(
Modifier
.fillMaxWidth()
.height(32.dp)
.background(
brush = Brush.verticalGradient(
colors = listOf(
Color.Transparent,
screenBackgroundColor
)
)
)
//.align(Alignment) to control the position of the overlay
)
}
Here's how it would look like:
However, this doesn't seem like quite what you asked for since it seems like you want the actual list content to fade out.
I don't know how you would apply an alpha to only a portion of a view. Perhaps try to dig into the .alpha sources to figure out.
Quick hack which fixes the issue: add .graphicsLayer { alpha = 0.99f } to your modifer
By default Jetpack Compose disables alpha compositing for performance reasons (as explained here; see the "Custom Modifier" section). Without alpha compositing, blend modes which affect transparency (e.g. DstIn) don't have the desired effect. Currently the best workaround is to add .graphicsLayer { alpha = 0.99F } to the modifier on the LazyColumn; this forces Jetpack Compose to enable alpha compositing by making the LazyColumn imperceptibly transparent.
With this change, your code looks like this:
#Composable
fun Screen() {
Box(
Modifier
.fillMaxWidth()
.background(color = Color.Yellow)
) {
LazyColumn(
modifier = Modifier
.fillMaxSize()
// Workaround to enable alpha compositing
.graphicsLayer { alpha = 0.99F }
.drawWithContent {
val colors = listOf(Color.Transparent, Color.Black)
drawContent()
drawRect(
brush = Brush.verticalGradient(colors),
blendMode = BlendMode.DstIn
)
}
) {
itemsIndexed((1..1000).toList()) { item, index ->
Text(
text = "Item $item: $index value",
modifier = Modifier.padding(12.dp),
color = Color.Red,
fontSize = 24.sp
)
}
}
}
}
which produces the correct result
Just a little nudge in the right direction. What this piece of code does is place a Box composable at the top of your LazyColumn with an alpha modifier for fading. You can make multiple of these Box composables in a Column again to create a smoother effect.
#Composable
fun FadingExample() {
Box(
Modifier
.fillMaxWidth()
.requiredHeight(500.dp)) {
LazyColumn(Modifier.fillMaxSize()) {
}
Box(
Modifier
.fillMaxWidth()
.height(10.dp)
.alpha(0.5f)
.background(Color.Transparent)
.align(Alignment.TopCenter)
) {
}
}
}
I optimised the #user3872620 solution. You have just to put this lines below your LazyColumn, VerticalPager.. and just adapt your offset / height, usually offset = height
Box(
Modifier
.fillMaxWidth()
.offset(y= (-10).dp)
.height(10.dp)
.background(brush = Brush.verticalGradient(
colors = listOf(
Color.Transparent,
MaterialTheme.colors.background
)
))
)
You will got this render:
There is the render
This is a very simple implementation of FadingEdgeLazyColumn using AndroidView. Place AndroidView with gradient background applied to the top and bottom of LazyColumn.
#Stable
object GradientDefaults {
#Stable
val Color = androidx.compose.ui.graphics.Color.Black
#Stable
val Height = 30.dp
}
#Stable
sealed class Gradient {
#Immutable
data class Top(
val color: Color = GradientDefaults.Color,
val height: Dp = GradientDefaults.Height,
) : Gradient()
#Immutable
data class Bottom(
val color: Color = GradientDefaults.Color,
val height: Dp = GradientDefaults.Height,
) : Gradient()
}
#Composable
fun FadingEdgeLazyColumn(
modifier: Modifier = Modifier,
gradients: Set<Gradient> = setOf(Gradient.Top(), Gradient.Bottom()),
contentGap: Dp = 0.dp,
state: LazyListState = rememberLazyListState(),
contentPadding: PaddingValues = PaddingValues(0.dp),
reverseLayout: Boolean = false,
verticalArrangement: Arrangement.Vertical =
if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
userScrollEnabled: Boolean = true,
content: LazyListScope.() -> Unit,
) {
val topGradient =
remember(gradients) { gradients.find { it is Gradient.Top } as? Gradient.Top }
val bottomGradient =
remember(gradients) { gradients.find { it is Gradient.Bottom } as? Gradient.Bottom }
ConstraintLayout(modifier = modifier) {
val (topGradientRef, lazyColumnRef, bottomGradientRef) = createRefs()
GradientView(
modifier = Modifier
.constrainAs(topGradientRef) {
top.linkTo(parent.top)
width = Dimension.matchParent
height = Dimension.value(topGradient?.height ?: GradientDefaults.Height)
}
.zIndex(2f),
colors = intArrayOf(
(topGradient?.color ?: GradientDefaults.Color).toArgb(),
Color.Transparent.toArgb()
),
visible = topGradient != null
)
LazyColumn(
modifier = Modifier
.constrainAs(lazyColumnRef) {
top.linkTo(
anchor = topGradientRef.top,
margin = when (topGradient != null) {
true -> contentGap
else -> 0.dp
}
)
bottom.linkTo(
anchor = bottomGradientRef.bottom,
margin = when (bottomGradient != null) {
true -> contentGap
else -> 0.dp
}
)
width = Dimension.matchParent
height = Dimension.fillToConstraints
}
.zIndex(1f),
state = state,
contentPadding = contentPadding,
reverseLayout = reverseLayout,
verticalArrangement = verticalArrangement,
horizontalAlignment = horizontalAlignment,
flingBehavior = flingBehavior,
userScrollEnabled = userScrollEnabled,
content = content
)
GradientView(
modifier = Modifier
.constrainAs(bottomGradientRef) {
bottom.linkTo(parent.bottom)
width = Dimension.matchParent
height = Dimension.value(bottomGradient?.height ?: GradientDefaults.Height)
}
.zIndex(2f),
colors = intArrayOf(
Color.Transparent.toArgb(),
(bottomGradient?.color ?: GradientDefaults.Color).toArgb(),
),
visible = bottomGradient != null
)
}
}
#Composable
private fun GradientView(
modifier: Modifier = Modifier,
#Size(value = 2) colors: IntArray,
visible: Boolean = true,
) {
AndroidView(
modifier = modifier,
factory = { context ->
val gradientBackground = GradientDrawable(
GradientDrawable.Orientation.TOP_BOTTOM,
colors
).apply {
cornerRadius = 0f
}
View(context).apply {
layoutParams = LayoutParams(
LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT
)
background = gradientBackground
visibility = when (visible) {
true -> View.VISIBLE
else -> View.INVISIBLE
}
}
}
)
}