galex
02/10/2022, 8:36 AM@Composable
outside of setContent {}
so that I could get the size of one child Composable
within?
I don’t care doing that in the UI thread right now, just want to know if it’s possible!Stylianos Gakis
02/10/2022, 9:08 AMgalex
02/10/2022, 9:43 AMsetContent {}
, like can I trigger this layout
to compose outside of the UI hierarchy?Stylianos Gakis
02/10/2022, 12:26 PMlayout{}
and doing everything inside that block.
One term I’ve heard thrown around in this slack has been “SubComposition” which I don’t know what it is but maybe this potentially helps solve your problem in some way?
And another less optimal way I’ve seen people do something like this has been by laying it out normally and using onGloballyPositioned
to get data about it’s place/size?
If none of the above, I am not quite sure myself, maybe someone else might have another idea. Or maybe you should reconsider what you’re trying to achieve and go about it with a different approach?galex
02/10/2022, 12:44 PMAdam Powell
02/10/2022, 3:28 PMModifier.onSizeChanged
is likely to be an efficient answer to what you're trying to do here. Alternatively, we've worked with the Coil library authors to help with this case in the past as well, that might be an easy drop-in solutiongalex
02/10/2022, 4:16 PMvar imageSize: IntSize? = null
ComposeView(context).apply {
compositionContext = Recomposer(<http://Dispatchers.IO|Dispatchers.IO>)
setContent {
HouzzTheme {
AdItem(ad = safeAd) { imageSize = it }
}
}
measure(ViewMeasureUtils.exact(width), ViewMeasureUtils.exact(height))
layout(0, 0, width, width)
}
delay(5000)
logger().d("HomeFeedComposedAdFetcher", "ImageSize = $imageSize")
Adam Powell
02/10/2022, 4:19 PMgalex
02/10/2022, 4:22 PMAdam Powell
02/10/2022, 7:23 PMwidth
and height
in the snippet above?galex
02/10/2022, 8:00 PMLazyColumn
would get in its onSizeChanged
modifier:
).onSizeChanged {
homeFeedViewModel.screenSize = it
}
Ugly, I know, but I have to make this work at any price.
I thought this was working but got lots of null
at the time I am trying to run the snippet above, so I just used constants to go forward for now:
val width = 1080
val height = 2400
Not pretty as well, I agree….2022-02-10 22:27:20.733 16222-16222/com.some.app D/fillDescriptor: called with safeSize = 1080 x 1865
Now, the question is why I can’t get that value out of the callback of the Composable, as it also uses internally onSizeChanged
to notify us of its size:
fun AdItem(
ad: Ad,
modifier: Modifier = Modifier,
onImageSizeChanged: ((IntSize) -> Unit)? = null,
) {
SomeImage(
placeHolder = defaultPlaceholder(),
imageDescriptor = ad.AdSpace.image1Descriptor(),
modifier = Modifier
.fillMaxWidth()
.height(375.dp)
.onSizeChanged {
onImageSizeChanged?.invoke(it)
}
)
}
And the snippet, again:
var imageSize: IntSize? = null
ComposeView(context).apply {
compositionContext = Recomposer(Dispatchers.Main)
setContent {
HouzzTheme {
AdItem(ad = safeAd) { imageSize = it }
}
}
measure(ViewMeasureUtils.exact(width), ViewMeasureUtils.exact(height))
layout(0, 0, width, width)
}
Does the composition happen only following a lifecycle event maybe? I thought it would measure and layout itself when the ComposeView
does 🤔class VagabondComposeView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AbstractComposeView(context, attrs, defStyleAttr) {
private val content = mutableStateOf<(@Composable () -> Unit)?>(null)
@Suppress("RedundantVisibilityModifier")
protected override var shouldCreateCompositionOnAttachedToWindow: Boolean = false
private set
@Composable
override fun Content() {
content.value?.invoke()
}
fun setContent(content: @Composable () -> Unit) {
this.content.value = content
createComposition()
}
}
My Composable
AdItem
is still not composed…Adam Powell
02/11/2022, 2:32 PMrunRecomposeAndApplyChanges
on the recomposer, but I still think this is overkill for the problem space. If you know what the size of your container is in your main composition there should be no need for this parallel universegalex
02/13/2022, 8:47 AM<com.company.app.views.MyImageView
android:id="@+id/image"
style="@style/image_bg_gray"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
So the width depends on the parent layout which has a parent layout which has another parent layout.
I’ve made the equivalent in Compose like this:
CustomImage(
placeHolder = defaultPlaceholder(),
imageDescriptor = someData.imageDescriptor(), // image descriptor is url + enum for a size
modifier = Modifier
.fillMaxWidth()
.onSizeChanged {
onImageSizeChanged?.invoke(it)
}
)
I think you’re getting my point, I can’t just say “its the width of the list minus 4 dp for margins and that’ll be the final size…” I wish!
Nothing here would beat getting it calculated by the view itself, and using that exact size to prefetch that image way in advance, when the data is loaded, to be ready for the image to show itself directly when the user scrolls.
Let’s be clear here, I also think it’s overkill but that’s the requirement set up for this, sadly.AndroidView
so that it matches what we calculated… I’m sad I have to do so just for knowing in advance the size of a Composable
😢Adam Powell
02/13/2022, 4:28 PMfun Modifier.invisible() = layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
layout(placeable.width, placeable.height) {
// Do not place
}
}
LazyColumn
in a Box
with an instance of your item you're trying to phantom-measure in that box along with it. Give it a fillMaxWidth
, your desired fixed/minimum height if needed, and one of these or something like it. The key thing this makes use of is that if a layout or layout modifier doesn't place a measured item, it's not attached to the layout node treeonSizeChanged
will still fireINVISIBLE
- it still measures but it doesn't draw and you can't interact with itif
and omit it after you have what you need, but if you want to be aware of any resizes that happen you might want to have it stick aroundPreCompose.Eager
or similar?galex
02/14/2022, 1:51 PMLazyColumn
.
Using your snippet I’ve come to build it in a modifier, which I think looks quite good:
fun Modifier.prefetchAdsImages(
scope: CoroutineScope,
ads: List<Ad>,
) = composed {
// remember the types of Ads we already calculated the size to not recompose that again
val composedAdTypes by remember { mutableStateOf(mutableListOf<AdType>()) }
// run on all the ads
ads.forEach { ad ->
// only when not already composed we'll compose
if (!composedAdTypes.contains(ad.Type)) {
composedAdTypes.add(ad.Type)
// setup a basic layout to be able to calculate the size of the image inside the adItem
Box(modifier = Modifier.fillMaxSize()) {
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(listOf(ad)) { item ->
AdItem(
ad = item,
imageModifier = Modifier
.onSizeSet { size ->
scope.launchOnBackground {
AndroidCore
.services()
.imageLoaderTaskManager()
.prefetch(ad.AdSpace.image1Descriptor(), size.width, size.height, false, null)
}
}
)
}
}
}
}
}
this
}
onSizeSet {}
is just a modifier to not get a size change twice
fun Modifier.onSizeSet(
onSizeSet: (IntSize) -> Unit
) = composed {
var isSet by remember { mutableStateOf(false) }
return@composed onSizeChanged {
if (!isSet && it.width > 0 && it.height > 0) {
isSet = true
onSizeSet(it)
}
}
}
What do you think?
Maybe there’s a perf issue with this that I don’t see yet?Stylianos Gakis
02/14/2022, 2:56 PMval composedAdTypes by remember { mutableStateOf(mutableListOf<AdType>()) }
it may not be doing what you want it to. Please read this article, to see why.Adam Powell
02/14/2022, 3:04 PMModifier.composed
- the behavior of this will not be what you want and is more or less undefined. We'll likely use the new applier inference feature to make the snippet above a compiler error. Using a LazyColumn
in your speculative layout will have the same issues you're facing in your original problem - things that don't fit won't be composed because the layout is lazy.galex
02/14/2022, 6:42 PMAdam Powell
02/15/2022, 4:51 AM