Define Custom Boundaries for a Composable - android

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
)
}
}

Related

How to make all images from API be in one size in Jetpack Compose?

I have the following composable that represents a list model -
#Composable
fun DashboardCard(
modifier: Modifier = Modifier,
model: DashboardCardModel,
onCardClicked: (model: DashboardCardModel) -> Unit
) {
Column(
modifier = modifier
.size(200.dp, 200.dp)
.background(Color.Transparent)
.padding(16.dp)
.clickable {
onCardClicked(model)
},
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceAround
) {
if (model.showDefaultThumbnail) {
AsyncImage(
modifier = Modifier
.size(90.dp)
.clip(RoundedCornerShape(10.dp)),
model = model.thumbnailUrl, contentDescription = ""
)
} else {
Image(
modifier = Modifier
.size(90.dp)
.clip(RoundedCornerShape(10.dp)),
painter = painterResource(id = com.tinytap.R.drawable.tinytap),
contentDescription = ""
)
}
Image(
modifier = Modifier
.size(25.dp)
.padding(top = 10.dp)
.alpha(if (model.isCurrentPostOfInterest) 1f else 0f),
painter = painterResource(id = com.tinytap.R.drawable.post_of_interest),
contentDescription = null
)
Text(
modifier = Modifier.padding(top = 10.dp),
fontSize = 16.sp,
color = Color.White,
text = model.title,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = model.author,
fontSize = 12.sp,
color = Color.LightGray,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
The issue is that I am using a fixed size for both my Image and AsyncImage but sometimes the API gives me images that are very wide, causing me to have the following inconsistency in the UI -
How can I make it that my images show up exactly the same? I tried using all kind of crops but the results ended up messing my image
Putting into consideration that image might be shorter, taller, wider or smaller.
To solve this issue, I recommend you to use Coil here is a sample of code that will solve your issue :
Card(
modifier = Modifier.size(119.dp, 92.dp),
shape = RoundedCornerShape(10.dp)
) {
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(imageURL)
.build(),
placeholder = painterResource(R.drawable.complex_placeholder),
error = painterResource(R.drawable.complex_placeholder),
contentDescription = "complex image",
contentScale = ContentScale.Crop,//The line that will affect your image size and help you solve the problem.
)
}
The above code will always show fully covered box with image for any case, also don't forget to determine size of container (Card in my example '119,92' ).
To know more about different attributes and their effect on your code, select what suits you the best
Check more here (reference of image attached) : Content scaletype fully illustrated

Compose elements not overlapping

I am trying to overlap two different compose elements. I want to show a toast kind of message at the top whenever there is an error message. I don't want to use a third party lib for such an easy use case. I plan to use the toast in every other composable screen for displaying error message. Below is the layout which i want to achieve
So I want to achieve the toast message saying "Invalid PIN, please try again".
#Composable
fun MyToast(title: String) {
Card(
modifier = Modifier
.absoluteOffset(x = 0.dp, y = 40.dp)
.background(
color = MaterialTheme.colors.primaryVariant,
shape = RoundedCornerShape(10.dp)
), elevation = 20.dp
) {
Row(
modifier = Modifier
.background(color = MaterialTheme.colors.primaryVariant)
.padding(12.dp),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(id = R.drawable.error_circle),
contentDescription = title
)
Text(
text = title,
fontFamily = FontFamily(Font(R.font.inter_medium)),
fontSize = 12.sp,
color = MaterialTheme.colors.primary,
modifier = Modifier.padding(horizontal = 10.dp)
)
}
}
}
and my screen composable is as follows
#Composable
fun Registration(navController: NavController, registrationViewModel: RegistrationViewModel) {
Scaffold() {
Box(){
MyToast(
title = "Invalid pin, please try again"
)
Column() {
//my other screen components
}
}
}
I will add the AnimatedVisibility modifier later to MyToast composable. First I need to overlap MyToast over all the other elements and somehow MyToast is just not visible
If you want the child of a Box to overlay/overlap its siblings behind, you should put it at the last part in the code
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Box(
modifier = Modifier.background(Color.Red).size(150.dp)
)
// your Toast
Box(
modifier = Modifier.background(Color.Green).size(80.dp)
)
}
So if I put the green box before the bigger red box like this
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
// your Toast
Box(
modifier = Modifier.background(Color.Green).size(80.dp)
)
Box(
modifier = Modifier.background(Color.Red).size(150.dp)
)
}
the green box will hide behind the red one
You have to solutions, you can either put the Toast in the bottom of your code because order matters in compose:
#Composable
fun Registration(navController: NavController, registrationViewModel: RegistrationViewModel) {
Scaffold() {
Box() {
Column() {
//my other screen components
}
MyToast(
title = "Invalid pin, please try again"
)
}
}
}
Or you can keep it as it is, but add zIndex to the Toast:
#Composable
fun MyToast(title: String) {
Card(
modifier = Modifier
.absoluteOffset(x = 0.dp, y = 40.dp)
.zIndex(10f) // add z index here
.background(
color = MaterialTheme.colors.primaryVariant,
shape = RoundedCornerShape(10.dp)
), elevation = 20.dp
) {
Row(
modifier = Modifier
.background(color = MaterialTheme.colors.primaryVariant)
.padding(12.dp),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(id = R.drawable.error_circle),
contentDescription = title
)
Text(
text = title,
fontFamily = FontFamily(Font(R.font.inter_medium)),
fontSize = 12.sp,
color = MaterialTheme.colors.primary,
modifier = Modifier.padding(horizontal = 10.dp)
)
}
}
}
Note: elevation in Card composable is not the same as elevation in XML so it's not going to make the composable in the top, it will just add a shadow but if you want to give the composable a higher z order use Modifier.zIndex(10f)

Composable fillMaxSize and rotation not working

How can I rotate composables and still make them fill their parent?
For example, I have a Column filling the Screen with two Boxes taking up half the size.
The size of the boxes seems to be calculated before the rotation and not after.
Column(
modifier = Modifier
.fillMaxSize()
.padding(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Box(
modifier = Modifier
.rotate(90f)
.fillMaxSize()
.weight(1f)
.background(Color.Red),
contentAlignment = Alignment.Center
) {
Text("1", fontSize = 100.sp)
}
Box(
modifier = Modifier
.rotate(90f)
.fillMaxSize()
.weight(1f)
.background(Color.Blue),
contentAlignment = Alignment.Center
) {
Text("2", fontSize = 100.sp)
}
}
Edit after Thracians comment:
This looks better but the maxHeight I get seems wrong, I can see in the layout inspector that the size of the BoxWithConstraints is right
Column(
modifier = Modifier
.fillMaxSize()
.padding(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
BoxWithConstraints(modifier = Modifier.fillMaxSize().weight(1f)) {
Box(
modifier = Modifier
.rotate(90f)
.width(maxHeight)
.height(maxWidth)
.background(Color.Red),
contentAlignment = Alignment.Center
) {
Text("1", fontSize = 100.sp)
}
}
BoxWithConstraints(modifier = Modifier.fillMaxSize().weight(1f)) {
Box(
modifier = Modifier
.rotate(90f)
.width(maxHeight)
.height(maxWidth)
.background(Color.Blue),
contentAlignment = Alignment.Center
) {
Text("2", fontSize = 100.sp)
}
}
Modifier.rotate is
#Stable
fun Modifier.rotate(degrees: Float) =
if (degrees != 0f) graphicsLayer(rotationZ = degrees) else this
Modifier.graphicsLayer{} does not change dimensions and physical position of a Composable, and doesn't trigger recomposition which is very good for animating or changing visual presentation.
You can also see in my question here even i change scale and position green rectangle, position in parent is not changing.
A [Modifier.Element] that makes content draw into a draw layer. The
draw layer can be * invalidated separately from parents. A
[graphicsLayer] should be used when the content * updates
independently from anything above it to minimize the invalidated
content. * * [graphicsLayer] can also be used to apply effects to
content, such as scaling ([scaleX], [scaleY]), * rotation
([rotationX], [rotationY], [rotationZ]), opacity ([alpha]), shadow
However any Modifier after Modifier.graphicsLayer is applied based on new scale, translation or rotation. Easiest to see is drawing border before and after graphicsLayer.
Column(modifier = Modifier.fillMaxSize()) {
val context = LocalContext.current
Row(modifier = Modifier.fillMaxSize()) {
Box(
modifier = Modifier
.size(100.dp, 200.dp)
.border(2.dp, Color.Red)
.zIndex(1f)
.clickable {
Toast
.makeText(context, "Before Layer", Toast.LENGTH_SHORT)
.show()
}
.graphicsLayer {
translationX = 300f
rotationZ = 90f
}
.border(2.dp, Color.Green)
.clickable {
Toast
.makeText(context, "After Layer", Toast.LENGTH_SHORT)
.show()
}
)
Box(
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.background(Color.Yellow)
)
}
}
If you check this example you will see that even after we rotate Composable on left position of Box with yellow background doesn't change because we don't change actual form of Composable on left side.
#Composable
private fun MyComposable() {
Column(modifier = Modifier.fillMaxSize()) {
Spacer(modifier = Modifier.height(300.dp))
Column(modifier = Modifier.fillMaxSize()) {
var angle by remember { mutableStateOf(0f) }
LaunchedEffect(key1 = Unit) {
delay(2000)
angle = 90f
}
Box(
modifier = Modifier
.fillMaxSize()
.weight(1f)
.background(Color.Red)
.rotate(angle),
contentAlignment = Alignment.Center
) {
Text("1", fontSize = 100.sp)
}
Spacer(modifier =Modifier.height(8.dp))
Box(
modifier = Modifier
.fillMaxSize()
.weight(1f)
.background(Color.Blue)
.rotate(angle),
contentAlignment = Alignment.Center
) {
Text("2", fontSize = 100.sp)
}
}
}
}
Added Spacer to have uneven width and height for Boxes for demonstration.
As i posted in example gif order of rotate determines what we rotate.
modifier = Modifier
.fillMaxSize()
.weight(1f)
.background(Color.Blue)
.rotate(angle)
This sets the size, it never rotates parent but the content or child because we set size and background before rotation. This answer works if width of the child is not greater than height of the parent.
If child has Modifier.fillSize() or child's width is bigger than parent's height when we rotate as in the image left below. So we need to scale it back to parent after rotation since we didn't change parents dimensions.
#Composable
private fun MyComposable2() {
var angle by remember { mutableStateOf(0f) }
LaunchedEffect(key1 = Unit) {
delay(2000)
angle = 90f
}
Column(modifier = Modifier.fillMaxSize()) {
Spacer(modifier = Modifier.height(100.dp))
BoxWithConstraints(
modifier = Modifier
.fillMaxSize()
.padding(8.dp)
) {
val width = maxWidth
val height = maxHeight / 2
val newWidth = if (angle == 0f) width else height
val newHeight = if (angle == 0f) height else width
Column(modifier = Modifier.fillMaxSize()) {
Box(
modifier = Modifier
.border(3.dp, Color.Red)
.size(width, height)
.graphicsLayer {
rotationZ = angle
scaleX = newWidth/width
scaleY = newHeight/height
}
.border(5.dp, Color.Yellow),
contentAlignment = Alignment.Center
) {
Image(
modifier = Modifier
.border(4.dp, getRandomColor())
.fillMaxSize(),
painter = painterResource(id = R.drawable.landscape1),
contentDescription = "",
contentScale = ContentScale.FillBounds
)
}
Box(
modifier = Modifier
.border(3.dp, Color.Red)
.graphicsLayer {
rotationZ = angle
scaleX = newWidth/width
scaleY = newHeight/height
}
.size(width, height)
.border(5.dp, Color.Yellow),
contentAlignment = Alignment.Center
) {
Image(
modifier = Modifier
.border(4.dp, getRandomColor())
.fillMaxSize(),
painter = painterResource(id = R.drawable.landscape1),
contentDescription = "",
contentScale = ContentScale.FillBounds
)
}
}
}
}
}
on left we don't scale only rotate as in question
on right we scale child into parent based on height/width ratio

Jetpack Compose: expand image with restricted height

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))
)

Changing image content scale do not change UI in jetpack compose

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
)

Categories

Resources