litho icon indicating copy to clipboard operation
litho copied to clipboard

[Question] How to properly implement onMeasure

Open xirdneh opened this issue 5 years ago • 0 comments

Version

0.37.0

Issues and Steps to Reproduce

I'm trying to implement @onMeasure for a @MountSpec. The mount spec creates a material button like this:

@MountSpec(isPureRender = true, poolSize = 30)
object OutlinedButtonSpec {
    @OnCreateMountContent
    fun onCreateMountContent(
        context: Context
    ) : MaterialButton {
        return MaterialButton(
            context,
            null,
            R.attr.materialButtonOutlinedStyle
        )
    }

    @OnMount
    fun onMount(
        c: ComponentContext, button: MaterialButton,
        @Prop(resType = ResType.DRAWABLE, optional = true) image: Drawable? = null,
        @Prop(resType = ResType.STRING, optional = true) text: String? = null,
        @Prop(resType = ResType.FLOAT, optional = true) textSize: Float?
    ) {
        button.text = text
        textSize?.let { button.textSize = it }
        button.icon = image
    }
}

I now I could craft a material button by hand but I like deferring styles and other stuff to some libraries. For this mountspec I figure I have to take into consideration the icon and the text of the button, measure those and add whatever padding the button ads and I would have my measures. So, for simplification we'll say that the icon in the button is the tallest element inside the button, which means that the button will always be as tall as the icon. Assuming some padding here and there we can say the button will always be 40dp in height unless specified otherwise.

Now, I tried implementing measuring the width like this:

@OnMeasure
    fun onMeasure(
        c: ComponentContext,
        layout: ComponentLayout,
        widthSpec: Int,
        heightSpec: Int,
        size: Size,
        @Prop(resType = ResType.DRAWABLE, optional = true) image: Drawable? = null,
        @Prop(resType = ResType.STRING, optional = true) text: String? = null
    ){
        //Create material button using a custom class that overrides "invalidate()" to avoid drawing.
        val measurable = MeasurableMaterialButton(
            c.applicationContext,
            null,
            R.attr.materialButtonOutlinedStyle
        )
        //Configure icon and text
        measurable.icon = image
        measurable.text = text

        //Use fbui.textLayoutBuilder to create the text layout for the button so it can be measured later
        val textLayout = TextLayoutBuilder().apply {
            setText(measurable.text)
            setTextSize(measurable.textSize.toInt())
        }.build()
        //If width is unspecified then use the text layout width as the button's width.
        size.width = when (SizeSpec.getMode(widthSpec)) {
            SizeSpec.UNSPECIFIED -> SizeSpec.resolveSize(
                widthSpec, LayoutMeasureUtil.getWidth(textLayout)
            ) 
            else -> SizeSpec.getSize(widthSpec)
        }
        size.height = when (SizeSpec.getMode(heightSpec)) {
            SizeSpec.UNSPECIFIED -> (40 * Resources.getSystem().displayMetrics.density).toInt()
            else-> SizeSpec.getSize(heightSpec)
        }
    }

Expected Behavior

The previous @onMeasure is incomplete since it's not taking into consideration the icon when calculating the width. This is OK because this is just the first step and measuring the text is the hardest part, imo. I would expect for the @onMeasure function to always set the button's width the same as the text in the button but this is not happening and the button always ends up full width. Printing the acutal text layout width does yield some "reasonable" numbers but I'm not sure why the size.width always ends up the same as the screen's width.

What is the correct way to implement @OnMeasure here? am I using fbui.textLayoutBuilder wrong? am I using SizeSpec.resolveSize wrong? It would be nice if litho's documentation had more examples about how to implement @OnMeasure. Thanks!

xirdneh avatar Sep 18 '20 16:09 xirdneh