I'm trying to emulate a Music player, and everything works, however using the layout inspector I can see that my image is being recompose every time that the "slider" value changes this is part of my code:
val albumArt by viewModel.albumArt.collectAsStateWithLifecycle() // collect a bitmap
val playbackPosition by viewModel.playbackPosition.collectAsStateWithLifecycle() // this cause a recomposition, it executes every second that is the duration of te song
#Composable
fun MusicAlbumArt(
albumArt: Bitmap?,
modifier: Modifier
) {
val painter = rememberAsyncImagePainter(
albumArt,
contentScale = ContentScale.Crop
)
AnimatedContent(targetState = painter,
transitionSpec = {
fadeIn() with fadeOut()
}) {
Image(
painter = painter,
contentDescription = null,
modifier = modifier,
contentScale = ContentScale.Crop
)
}
}
and this is the main composable:
#Composable
private fun MusicWidget(
playbackPosition: Float,
albumArt: Bitmap?,
){
BoxWithConstraints {
val width by remember { mutableStateOf(this.maxWidth) }
val height by remember { mutableStateOf(this.maxHeight) }
MusicAlbumArt(
modifier = Modifier.fillMaxSize(),
musicViewState = musicViewState,
albumArt = albumArt
)
//
MusicSlider(modifier = Modifier.fillMaxWidth()
}
The playbackPosition executes every second to update the "song duration" but analyzing this in the layout inspector the AnimatedContent is skipped however the Image is recomposed every time, I can't find the wayt to avoid recomposition on the image. What could be the possible options?
Related
I am writing a small gallery app for my cat. It has a button by clicking on which a new PhotoItem is added to the displayed list, but it appears only after phone rotation and I want it to appear on the screen right after button was clicked.
Right now everything is stored in a mutableList inside savedStateHandle.getStateFlow but I also tried regular MutableStateFlow and mutableStateOf and it didn't help. I havent really used jatpack compose and just can't figure what to do (
App
#SuppressLint("UnusedMaterialScaffoldPaddingParameter")
#Composable
fun BebrasPhotosApp() {
val galaryViewModel = viewModel<GalaryViewModel>()
val allPhotos by galaryViewModel.loadedPics.collectAsState()
Scaffold(topBar = { BebraTopAppBar() }, floatingActionButton = {
FloatingActionButton(
onClick = { galaryViewModel.addPicture() },
backgroundColor = MaterialTheme.colors.onBackground
) {
Icon(
imageVector = Icons.Rounded.Add,
contentDescription = "Add Photo",
tint = Color.White,
)
}
}) {
LazyColumn(modifier = Modifier.background(color = MaterialTheme.colors.background)) {
items(allPhotos) {
PhotoItem(bebra = it)
}
}
}
}
ViewModel
class GalaryViewModel(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
val loadedPics = savedStateHandle.getStateFlow(
"pics", initialValue = mutableListOf<Bebra>(
Bebra(R.string.photo_1, R.string.desc_1, R.drawable.bebra_pic_1, R.string.add_desc_1),
Bebra(R.string.photo_2, R.string.desc_2, R.drawable.bebra_pic_2, R.string.add_desc_2),
Bebra(R.string.photo_3, R.string.desc_3, R.drawable.bebra_pic_3, R.string.add_desc_3)
)
)
fun addPicture() {
val additionalBebraPhoto = Bebra(
R.string.photo_placeholder,
R.string.desc_placeholder,
R.drawable.placeholder_cat,
R.string.add_desc_placeholder
)
savedStateHandle.get<MutableList<Bebra>>("pics")!!.add(additionalBebraPhoto)
}
}
PhotoItem
#Composable
fun PhotoItem(bebra: Bebra, modifier: Modifier = Modifier) {
var expanded by remember { mutableStateOf(false) }
Card(elevation = 4.dp, modifier = modifier
.padding(8.dp)
.clickable { expanded = !expanded }) {
Column(
modifier = modifier
.padding(8.dp)
.animateContentSize(
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
)
)
) {
Text(
text = stringResource(id = bebra.PicNumber),
style = MaterialTheme.typography.h1,
modifier = modifier.padding(bottom = 8.dp)
)
Text(
text = stringResource(id = bebra.PicDesc),
style = MaterialTheme.typography.body1,
modifier = modifier.padding(bottom = 8.dp)
)
Image(
painter = painterResource(id = bebra.Picture),
contentDescription = stringResource(id = bebra.PicDesc),
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxWidth()
.height(256.dp)
.clip(RoundedCornerShape(12))
)
if (expanded) {
BebraAdditionalDesc(bebra.additionalDesc)
}
}
}
}
Bebra Data class
data class Bebra(
#StringRes val PicNumber: Int,
#StringRes val PicDesc: Int,
#DrawableRes val Picture: Int,
#StringRes val additionalDesc: Int
)
So, I am also not super familiar with JC, but from first glance it looks like your method, addPicture() - which is called when the user taps on the button, does not update the state, therefore there's no recomposition happening, so the UI does not get updated.
Check:
fun addPicture() {
// ...
savedStateHandle.get<MutableList<Bebra>>("pics")!!.add(additionalBebraPhoto)
}
So here you are basically adding a new item to savedStateHandle, which I assume does not trigger a recomposition.
What I think you need to do, is to update loadedPics, somehow.
However, loadedPics is a StateFlow, to be able to update it you would need a MutableStateFlow.
For simplicity, this is how you would do it if you were operating with a list of strings:
// declare MutableStateFlow that can be updated and trigger recomposition
val _loadedPics = MutableStateFlow(
savedStateHandle.get<MutableList<String>>("pics") ?: mutableListOf()
)
// use this in the JC layout to listen to state changes
val loadedPics: StateFlow<List<String>> = _loadedPics
// addPicture:
val prevList = _loadedPics.value
prevList.add("item")
_loadedPics.value = prevList // triggers recomposition
// here you probably will want to save the item in the
// `savedStateHandle` as you already doing.
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 a list of photos from the internet and I want to load them into a LazyColumn. The problem is that some of them are loading faster than others so the photos appear in random order.
Here is the code:
val state = viewModel.photos.collectAsState()
val lastIndex = state.value.lastIndex
LazyColumn {
itemsIndexed(state.value) { i, photo ->
AsyncImage(
model = photo.urls.regular,
contentDescription = null,
modifier = Modifier.fillMaxWidth(),
)
if (i >= lastIndex-1 && !viewModel.isLoading.value) {
viewModel.getPhotos(6)
}
}
}
Sometimes image with LastIndex-1 occurs first and it triggers viewModel to get new data.
How can I make a handler for loading a list of images? Or maybe any suggestions on how to make a preloading screen which will end after loading all images into LazyColumn
I also have a Coil version too:
val model = ImageRequest.Builder(LocalContext.current)
.data(photo.urls.regular)
.size(Size.ORIGINAL)
.crossfade(true)
.build()
val painter = rememberAsyncImagePainter(model)
Image(
painter = painter,
contentDescription = null,
modifier = Modifier.fillMaxWidth(),
)
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
)
I'm implementing a simple gallery screen using Jetpack Compose which shows all video and image thumbnails on the screen
I have displayed image from file path successfully. However, I've got trouble in showing video thumbnail. How can I do that using Coil?
Here's my code to show image thumbnails:
#Composable
fun ImageLoaderFromLocal(
url: String,
placeHolderResId: Int,
modifier: Modifier,
transformation: Transformation
) {
val painter = rememberImagePainter(data = File(url),
builder = {
placeholder(placeHolderResId)
crossfade(true)
transformations(transformation)
})
Image(
painter = painter,
contentDescription = null,
modifier = modifier,
contentScale = ContentScale.Inside
)
}
According to Coil documentation, you need to add following dependency:
implementation("io.coil-kt:coil-video:$coil_version")
and specify fetcher in the builder:
val context = LocalContext.current
val painter = rememberImagePainter(
data = url,
builder = {
fetcher(VideoFrameUriFetcher(context))
// optionally set frame location
videoFrameMillis(1000)
placeholder(placeHolderResId)
crossfade(true)
transformations(transformation)
}
)
The other answer doesn't really work any more.
so i tried this one and it worked
val context = LocalContext.current
var visible by rememberSaveable { mutableStateOf(false) }
val imageLoader = ImageLoader.Builder(context)
.components {
add(VideoFrameDecoder.Factory())
}.crossfade(true)
.build()
val painter = rememberAsyncImagePainter(
model = "Your file here",
imageLoader = imageLoader,
The painter should be called in the image composable
like this
Image(
painter = painter,
contentDescription = "",
contentScale = ContentScale.Crop,
alignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
)
i hope this help someone