When I'm trying to use LazyVerticalGrid to display a list of images, some grid items have a different size even the images itself have exactly same size 240x178.
I tied to use modifier.fillMaxWidth(), modifier.matchParentSize(), modifier.fillMaxHeight(), modifier.fillMaxWidth(), modifier.fillParentMaxHeight(), modifier.fillParentMaxWidth(), modifier.requiredHeight(imageHeight), modifier.requiredWidth(imageWidth) but nothing had helped me to make images fill all available space without leaving any empty spaces between and some images continue to be not the same size with the others.
Below is my current implementation and I'm using Coil for image loading if its important
#OptIn(ExperimentalFoundationApi::class)
#Composable
fun TreasureGrid(treasures: List<MapTreasure>) {
val configuration = LocalConfiguration.current
val imageWidth = configuration.screenWidthDp.dp / 4
val imageHeight = (imageWidth.times(1.348f))
LazyVerticalGrid(
cells = GridCells.Adaptive(imageWidth)
) {
items(treasures.size) {
TreasureItem(
treasures[it],
Modifier.requiredHeight(imageHeight).requiredWidth(imageWidth)
)
}
}
}
#Composable
fun TreasureItem(mapTreasure: MapTreasure, modifier: Modifier) {
val resId = TMApplication.instance.resources.getIdentifier(
mapTreasure.image,
"drawable",
TMApplication.instance.packageName
)
val matrix = ColorMatrix()
val isOpened = getUser()?.openedTreasures?.contains(mapTreasure.name) == true
if (isOpened.not()) {
matrix.setToSaturation(0F)
}
Box {
AsyncImage(
model = resId,
contentDescription = mapTreasure.description,
modifier = modifier.clickable {
if (isOpened) {
activity?.let { DialogUtils.showTreasureInfoDialog(it, mapTreasure) }
} else {
Toast.makeText(activity, "Treasure not found", Toast.LENGTH_SHORT).show()
}
}.matchParentSize(),
colorFilter = ColorFilter.colorMatrix(matrix)
)
val alpha = if (isOpened) 0f else 1f
AsyncImage(
model = R.drawable.treasure_key,
contentDescription = mapTreasure.description,
modifier = modifier.rotate(90f).scale(0.3f).alpha(alpha)
)
}
}
Any suggestions on how to make every grid item same size and remove the padding around it are highly appreciated
The problem was not related to the code. I found out that the designer gave me images of the same size but with different paddings on each image, so the problem was in the image resource itself.
Related
I have a simple CoilImage implementation in my project where I download an image through an url, and then proceed to move the image state into an Image Composable. That's going smooth so far, but I'm facing a small problem where undesirable paddings are being added to the image. The image composable is inside a column with no fixed height. How can I get rid of these paddings while retaining the image dimension?
I've tried a couple of solutions such as using wrapContentHeight, putting it in a box and clipping it, using contentScale etc, but I just can't seem to get rid of the green spaces.
#Composable
fun HorizontalTextWithObject(enumerationItem: TextWithObjectPresentable) {
Row(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.Top) {
Image(modifier = Modifier, enumerationItem)
}
Column(modifier = Modifier.weight(2f)) {
Title(modifier = Modifier, enumerationItem)
Description(enumerationItem)
}
}
}
#Composable
fun Image(modifier: Modifier,
enumerationItem:TextWithObjectPresentable) {
CoilImage(
{ request },
success = { imageState ->
imageState.imageBitmap?.let {
val width = it.width.dp
val height = ((width / 5) * 3)
Image(bitmap = it, contentDescription = "",
modifier = Modifier
.width(width)
.height(height)
.background(SDColor.green),
)
}
},
)
}
I tried ContentScale.Crop but it is not what I'm looking for as it will just fill the paddings heightwise
I'm having issues understanding how exactly Offset works in Compose, specifically in the PointerScope onTap callback function to track the exact location where the user tapped on a UI element.
Use Case
User is presented an image. Whenever the user taps onto the image, a marker icon is placed on the tapped location.
Code
(You can ignore the button here, it does nothing at this point)
class MainActivity : ComponentActivity() {
//private var editMode = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val offset = remember {
mutableStateOf(Offset.Infinite)
}
MyLayout(offset.value) {
offset.value = it
}
}
}
#Composable
private fun MyLayout(
offset: Offset,
saveLastTap: (Offset) -> Unit
) {
val painter: Painter = painterResource(id = R.drawable.landscape)
Column(Modifier.fillMaxSize()) {
Box(
modifier = Modifier
.weight(0.95f)
.fillMaxSize()
) {
Image(
contentScale = FillBounds,
painter = painter,
contentDescription = "",
modifier = Modifier
.pointerInput(Unit) {
detectTapGestures(
onTap = {
saveLastTap(it)
}
)
}.border(5.dp, Color.Red)
)
if (offset.isFinite) {
PlaceMarkerOnImage(offset = offset)
}
}
Button(
enabled = false,
onClick = { TODO("Button Impl") },
modifier = Modifier.weight(0.05f),
shape = MaterialTheme.shapes.small
) {
Text(text = "Edit Mode")
}
}
}
#Composable
private fun PlaceMarkerOnImage(offset: Offset) {
Image(
painter = painterResource(id = R.drawable.marker),
contentScale = ContentScale.Crop,
contentDescription = "",
modifier = Modifier.offset(offset.x.dp, offset.y.dp)
)
}
}
Outcome
I added a little dot to the screenshots to indicate where i tapped. You see the image is getting placed far off the expected locations
Assumptions
I read a bit about the Offset object and i suspect the difference has something to do with the conversion to .dp which i need to feed the offset to the marker image Modifier.
It might also be related to coordinates being related to it's parent views or something, but since in my example theres nothing else in the UI but the image, i can't grasp this as a possible candidate.
Any help appreciated. Thank you !
I just solved it by replacing
modifier = Modifier.offset( offset.x.dp, offset.y.dp )
with
modifier = Modifier.offset(
x= LocalDensity.current.run{offset.x.toInt().toDp()},
y= LocalDensity.current.run{offset.y.toInt().toDp()} )
Turned out my mistake was that I ignored the properties of dp by just casting the pixel value to dp. Instead, I should call the local device density to correctly determine the amount of pixels for the transformation.
I have tried below code but it reflects nothing in the UI, I'm missing anything here?
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
loadUi()
}
}
#Composable
fun loadUi() {
CraneWrapper {
MaterialTheme {
Image(
(ResourcesCompat.getDrawable(
resources,
R.mipmap.ic_launcher,
null
) as BitmapDrawable).bitmap
)
}
}
}
}
You can use the painterResource function:
Image(painterResource(R.drawable.ic_xxxx),"content description")
The resources with the given id must point to either fully rasterized images (ex. PNG or JPG files) or VectorDrawable xml assets.
It means that this method can load either an instance of BitmapPainter or VectorPainter for ImageBitmap based assets or vector based assets respectively.
Example:
Card(
modifier = Modifier.size(48.dp).tag("circle"),
shape = CircleShape,
elevation = 2.dp
) {
Image(
painterResource(R.drawable.ic_xxxx),
contentDescription = "",
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize()
)
}
Starting at version 1.0.0-beta01:
Image(
painter = painterResource(R.drawable.your_drawable),
contentDescription = "Content description for visually impaired"
)
As imageResourceis not available anymore, the solutions with painterResource are indeed correct, e.g.
Image(painter = painterResource(R.drawable.ic_heart), contentDescription = "content description")
But you can actually still use Bitmap instead of drawable if you need so:
Image(bitmap = bitmap.asImageBitmap())
.asImageBitmap() is an extensions on Bitmap that compose provides and it creates an ImageBitmap from the given Bitmap.
Working in 0.1.0-dev14
Loading drawable in Image could be achieve from this:
Image(
imageResource(id = R.drawable.scene_01),
modifier = Modifier.preferredHeightIn(160.dp, 260.dp)
.fillMaxWidth(),
contentScale = ContentScale.Crop
)
Now, I'm trying to upload drawable in Circle Image that sounds tricky but too easy in JetPack Compose. You can do achieve in this way:
Image(
asset = imageResource(id = R.drawable.scene_01),
modifier = Modifier.drawBackground(
color = Color.Black,
style = Stroke(4f),
shape = CircleShape
).preferredSize(120.dp)
.gravity(Alignment.CenterHorizontally)
.clip(CircleShape),
contentScale = ContentScale.FillHeight
)
Output:
Try This one but if you copy the code and then paste it I don't know why but it won't work so just type it as it is and replace the image id
Image(
painter = painterResource(id = R.drawable.tanjim),
contentDescription = null,
)
version=1.0.0-beta01,use painterResource,imageResource has been deleted.
example
Image(
painterResource(R.drawable.ic_vector_or_png),
contentDescription = null,
modifier = Modifier.requiredSize(50.dp)
)
android developer doc
With version 1.0.0-beta01
It's like below
Image(
painter = painterResource(R.drawable.header),
contentDescription = null,
)
I found that there is a function imageFromResource() in AndroidImage.kt:
fun imageFromResource(res: Resources, resId: Int): Image {
return AndroidImage(BitmapFactory.decodeResource(res, resId))
}
so your code would be:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
loadUi()
}
}
#Composable
fun loadUi() {
CraneWrapper {
MaterialTheme {
val image = imageFromResource(resources, R.mipmap.ic_launcher)
SimpleImage(image)
}
}
}
}
#Composable
fun loadUi() {
val image = +imageResource(R.drawable.header)
CraneWrapper {
MaterialTheme {
Container(expanded = true,height = 180.dp) {
//Use the Clip() function to round the corners of the image
Clip(shape = RoundedCornerShape(8.dp)) {
//call DrawImage() to add the graphic to the app
DrawImage(image)
}
}
}
}
}
I found SimpleImage class from jetpack compose library to load the image but this is a temporary solution, and I didn't find any styling option with this yet.
// TODO(Andrey) Temporary. Should be replaced with our proper Image component when it available
#Composable
fun SimpleImage(
image: Image
) {
// TODO b132071873: WithDensity should be able to use the DSL syntax
WithDensity(block = {
Container(width = image.width.toDp(), height = image.height.toDp()) {
Draw { canvas, _ ->
canvas.drawImage(image, Offset.zero, Paint())
}
}
})
}
I have used it in this way
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
loadUi()
}
}
#Composable
fun loadUi() {
CraneWrapper {
MaterialTheme {
val bitmap = (ResourcesCompat.getDrawable(
resources,
R.mipmap.ic_launcher,
null
) as BitmapDrawable).bitmap
SimpleImage(Image(bitmap))
}
}
}
}
Still, I'm not sure this is the correct way of loading image from drawables.
Google updated their API. For 0.1.0-dev03 loading images is synchronous and is done this way
val icon = +imageResource(R.drawable.ic_xxx)
To draw the image
Container(modifier = Height(100.dp) wraps Expanded) {
DrawImage(icon)
}
Currently the above code relies on you specifying either the exact height or width. It seems that scaling the image is not supported if you want for example 100 dp height and wrap_content instead of Expanded which expands the full width.
Does any one know how to solve this issue? Also is possible to fit the image inside it's container like old way scaleType=fitCenter?
In xml you can use GridLayoutManager.SpanSizeLookup in GridLayoutManager to set the span size on single items (How many columns the item will use in the row, like for example, in a grid of 3 columns I can set the first item to be span size 3 so it will use all the width of the grid), but in Compose I can't find a way to do it, the vertical grid only have a way set the global span count and add items, but not set the span size of an individual item, is there a way to do it?
Jetpack Compose version 1.1.0-beta03 introduced horizontal spans to LazyVerticalGrid.
Example code:
val list by remember { mutableStateOf(listOf("A", "E", "I", "O", "U")) }
LazyVerticalGrid(
cells = GridCells.Fixed(2)
) {
// Spanned Item:
item(
span = {
// Replace "maxCurrentLineSpan" with the number of spans this item should take.
// Use "maxCurrentLineSpan" if you want to take full width.
GridItemSpan(maxCurrentLineSpan)
}
) {
Text("Vowels")
}
// Other items:
items(list) { item ->
Text(item)
}
}
There is no support for this out of the box at present. The way I have solved this for now is to use a LazyColumn then the items are Rows and in each Row you can decide how wide an item is, using weight.
I have implemented and in my case I have headers (full width), and cells of items of equal width (based on how wide the screen is, there could be 1, 2 or 3 cells per row). It's a workaround, but until there is native support from VerticalGrid this is an option.
My solution is here - look for the LazyListScope extensions.
Edit: this is no longer necessary as LazyVerticalGrid supports spans now, here's an example
LazyVerticalGrid(
columns = GridCells.Adaptive(
minSize = WeatherCardWidth,
),
modifier = modifier,
contentPadding = PaddingValues(all = MarginDouble),
horizontalArrangement = Arrangement.spacedBy(MarginDouble),
verticalArrangement = Arrangement.spacedBy(MarginDouble),
) {
state.forecastItems.forEach { dayForecast ->
item(
key = dayForecast.header.id,
span = { GridItemSpan(maxLineSpan) }
) {
ForecastHeader(
state = dayForecast.header,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = MarginDouble),
)
}
items(
items = dayForecast.forecast,
key = { hourForecast -> hourForecast.id }
) { hourForecast ->
ForecastWeatherCard(
state = hourForecast,
modifier = Modifier.fillMaxWidth(),
)
}
}
}
Adapting the code from the answer, I created a more "general" purpose method, It can be used with Adaptive and Fixed, I'm very new with Compose so I accept suggestions
#Composable
fun HeaderGrid(cells: GridCells, content: HeaderGridScope.() -> Unit) {
var columns = 1
var minColumnWidth = 0.dp
when (cells) {
is GridCells.Fixed -> {
columns = cells.count
minColumnWidth = cells.minSize
}
is GridCells.Adaptive -> {
val width = LocalContext.current.resources.displayMetrics.widthPixels
val columnWidthPx = with(LocalDensity.current) { cells.minSize.toPx() }
minColumnWidth = cells.minSize
columns = ((width / columnWidthPx).toInt()).coerceAtLeast(1)
}
}
LazyColumn(modifier = Modifier.fillMaxWidth()){
content(HeaderGridScope(columns, minColumnWidth, this))
}
}
fun <T>HeaderGridScope.gridItems(items: List<T>, content: #Composable (T) -> Unit) {
items.chunked(numColumn).forEach {
listScope.item {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
it.forEach {
content(it)
}
if (it.size < numColumn) {
repeat(numColumn - it.size) {
Spacer(modifier = Modifier.width(columnWidth))
}
}
}
}
}
}
fun HeaderGridScope.header(content: #Composable BoxScope.() -> Unit) {
listScope.item {
Box(
modifier = Modifier
.fillMaxWidth(),
content = content
)
}
}
data class HeaderGridScope(val numColumn: Int, val columnWidth: Dp, val listScope: LazyListScope)
sealed class GridCells {
class Fixed(val count: Int, val minSize: Dp) : GridCells()
class Adaptive(val minSize: Dp) : GridCells()
}
I have tried below code but it reflects nothing in the UI, I'm missing anything here?
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
loadUi()
}
}
#Composable
fun loadUi() {
CraneWrapper {
MaterialTheme {
Image(
(ResourcesCompat.getDrawable(
resources,
R.mipmap.ic_launcher,
null
) as BitmapDrawable).bitmap
)
}
}
}
}
You can use the painterResource function:
Image(painterResource(R.drawable.ic_xxxx),"content description")
The resources with the given id must point to either fully rasterized images (ex. PNG or JPG files) or VectorDrawable xml assets.
It means that this method can load either an instance of BitmapPainter or VectorPainter for ImageBitmap based assets or vector based assets respectively.
Example:
Card(
modifier = Modifier.size(48.dp).tag("circle"),
shape = CircleShape,
elevation = 2.dp
) {
Image(
painterResource(R.drawable.ic_xxxx),
contentDescription = "",
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize()
)
}
Starting at version 1.0.0-beta01:
Image(
painter = painterResource(R.drawable.your_drawable),
contentDescription = "Content description for visually impaired"
)
As imageResourceis not available anymore, the solutions with painterResource are indeed correct, e.g.
Image(painter = painterResource(R.drawable.ic_heart), contentDescription = "content description")
But you can actually still use Bitmap instead of drawable if you need so:
Image(bitmap = bitmap.asImageBitmap())
.asImageBitmap() is an extensions on Bitmap that compose provides and it creates an ImageBitmap from the given Bitmap.
Working in 0.1.0-dev14
Loading drawable in Image could be achieve from this:
Image(
imageResource(id = R.drawable.scene_01),
modifier = Modifier.preferredHeightIn(160.dp, 260.dp)
.fillMaxWidth(),
contentScale = ContentScale.Crop
)
Now, I'm trying to upload drawable in Circle Image that sounds tricky but too easy in JetPack Compose. You can do achieve in this way:
Image(
asset = imageResource(id = R.drawable.scene_01),
modifier = Modifier.drawBackground(
color = Color.Black,
style = Stroke(4f),
shape = CircleShape
).preferredSize(120.dp)
.gravity(Alignment.CenterHorizontally)
.clip(CircleShape),
contentScale = ContentScale.FillHeight
)
Output:
Try This one but if you copy the code and then paste it I don't know why but it won't work so just type it as it is and replace the image id
Image(
painter = painterResource(id = R.drawable.tanjim),
contentDescription = null,
)
version=1.0.0-beta01,use painterResource,imageResource has been deleted.
example
Image(
painterResource(R.drawable.ic_vector_or_png),
contentDescription = null,
modifier = Modifier.requiredSize(50.dp)
)
android developer doc
With version 1.0.0-beta01
It's like below
Image(
painter = painterResource(R.drawable.header),
contentDescription = null,
)
I found that there is a function imageFromResource() in AndroidImage.kt:
fun imageFromResource(res: Resources, resId: Int): Image {
return AndroidImage(BitmapFactory.decodeResource(res, resId))
}
so your code would be:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
loadUi()
}
}
#Composable
fun loadUi() {
CraneWrapper {
MaterialTheme {
val image = imageFromResource(resources, R.mipmap.ic_launcher)
SimpleImage(image)
}
}
}
}
#Composable
fun loadUi() {
val image = +imageResource(R.drawable.header)
CraneWrapper {
MaterialTheme {
Container(expanded = true,height = 180.dp) {
//Use the Clip() function to round the corners of the image
Clip(shape = RoundedCornerShape(8.dp)) {
//call DrawImage() to add the graphic to the app
DrawImage(image)
}
}
}
}
}
I found SimpleImage class from jetpack compose library to load the image but this is a temporary solution, and I didn't find any styling option with this yet.
// TODO(Andrey) Temporary. Should be replaced with our proper Image component when it available
#Composable
fun SimpleImage(
image: Image
) {
// TODO b132071873: WithDensity should be able to use the DSL syntax
WithDensity(block = {
Container(width = image.width.toDp(), height = image.height.toDp()) {
Draw { canvas, _ ->
canvas.drawImage(image, Offset.zero, Paint())
}
}
})
}
I have used it in this way
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
loadUi()
}
}
#Composable
fun loadUi() {
CraneWrapper {
MaterialTheme {
val bitmap = (ResourcesCompat.getDrawable(
resources,
R.mipmap.ic_launcher,
null
) as BitmapDrawable).bitmap
SimpleImage(Image(bitmap))
}
}
}
}
Still, I'm not sure this is the correct way of loading image from drawables.
Google updated their API. For 0.1.0-dev03 loading images is synchronous and is done this way
val icon = +imageResource(R.drawable.ic_xxx)
To draw the image
Container(modifier = Height(100.dp) wraps Expanded) {
DrawImage(icon)
}
Currently the above code relies on you specifying either the exact height or width. It seems that scaling the image is not supported if you want for example 100 dp height and wrap_content instead of Expanded which expands the full width.
Does any one know how to solve this issue? Also is possible to fit the image inside it's container like old way scaleType=fitCenter?