Android compound view in preview not showing height or contents

Viewed 274

I'm creating a custom compound view which extends LinearLayout (also tried ConstraintLayout) which contains 2 child views. When the app is running the view lays out correctly, however in the preview window it shows as 0 height when I use wrap_content. When the view was extending ConstraintLayout it was rendering as match_parent when the view was set to wrap_content.

If I override onMeasure and force a size when isInEditMode it is still showing up as 0 height.

Can someone point out what I'm doing wrong? This is the custom view class:

package com.classdojo.android.nessie

import android.content.Context
import android.util.AttributeSet
import android.util.TypedValue
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.annotation.ColorRes
import androidx.annotation.DimenRes
import androidx.annotation.DrawableRes
import androidx.core.content.res.ResourcesCompat
import androidx.core.view.isVisible
import com.airbnb.paris.utils.getFont
import com.classdojo.android.nessie.icon.Icon
import kotlinx.android.synthetic.main.nessie_button_view.view.*

class NessieButton : LinearLayout {

    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
        init(attrs)
    }
    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
        init(attrs)
    }
    constructor(context: Context) : super(context) {
        init(null)
    }

    enum class Style(
        @DrawableRes
        val backgroundRes: Int,
        @ColorRes
        val textColor: Int
    ) {
        PRIMARY(
            backgroundRes = R.drawable.nessie_button_style_primary,
            textColor = R.color.nessie_button_style_primary_text_color
        ),
        SECONDARY(
            backgroundRes = R.drawable.nessie_button_style_secondary,
            textColor = R.color.nessie_button_style_secondary_text_color
        ),
        TERTIARY(
            backgroundRes = R.drawable.nessie_button_style_tertiary,
            textColor = R.color.nessie_button_style_tertiary_text_color
        ),
        DESTRUCTIVE(
            backgroundRes = R.drawable.nessie_button_style_destructive,
            textColor = R.color.nessie_button_style_destructive_text_color
        ),
        BEYOND(
            backgroundRes = R.drawable.nessie_button_style_beyond,
            textColor = R.color.nessie_button_style_beyond_text_color
        );

        companion object {
            fun from(index: Int) = values().getOrElse(index) { Style.PRIMARY }
        }
    }

    enum class Size(
        @DimenRes
        val horizontalPadding: Int,
        @DimenRes
        val verticalPadding: Int,
        @DimenRes
        val textSize: Int
    ) {
        SMALL(
            horizontalPadding = R.dimen.nessie_default_size_3x,
            verticalPadding = R.dimen.nessie_default_size_2x,
            textSize = R.dimen.nessie_textSizeDetail
        ),
        MEDIUM(
            horizontalPadding = R.dimen.nessie_default_size_3x,
            verticalPadding = R.dimen.nessie_default_size_2x,
            textSize = R.dimen.nessie_textSizeAction
        ),
        LARGE(
            horizontalPadding = R.dimen.nessie_default_size_4x,
            verticalPadding = R.dimen.nessie_default_size_3x,
            textSize = R.dimen.nessie_textSizeAction
        );

        companion object {
            fun from(index: Int) = values().getOrElse(index) { Size.LARGE }
        }
    }

    var text: CharSequence?
        set(value) {
            nessie_button_text_view.text = value
            nessie_button_text_view.isVisible = !value.isNullOrBlank()
        }
        get() = nessie_button_text_view.text

    var icon: Icon? = null
        set(value) {
            field = value
            when (value) {
                null -> {
                    nessie_button_icon_view.isVisible = false
                }
                else -> {
                    nessie_button_icon_view.icon = value
                    nessie_button_icon_view.isVisible = true
                }
            }
        }

    var style: Style = Style.PRIMARY
        set(value) {
            field = value
            setBackgroundResource(value.backgroundRes)
            val color = ResourcesCompat.getColor(resources, value.textColor, null)
            nessie_button_icon_view.iconColor = color
            nessie_button_text_view.setTextColor(color)
        }

    var size: Size = Size.LARGE
        set(value) {
            field = value
            val horizontalPadding = resources.getDimensionPixelSize(value.horizontalPadding)
            val verticalPadding = resources.getDimensionPixelSize(value.verticalPadding)
            setPadding(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding)
            val fontSize = resources.getDimension(value.textSize)
            nessie_button_text_view.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize)
            nessie_button_icon_view.apply {
                iconPixelSize = fontSize.toInt()
                layoutParams.apply {
                    width = fontSize.toInt()
                    height = fontSize.toInt()
                }
            }
        }

    init {
        init(null)
    }

    override fun setEnabled(enabled: Boolean) {
        super.setEnabled(enabled)
        super.setFocusable(enabled)

        val colorStateList = ResourcesCompat.getColorStateList(resources, style.textColor, null)
        val viewState = drawableState
        val color = colorStateList!!.getColorForState(viewState, 0)
        nessie_button_text_view.setTextColor(color)
        nessie_button_icon_view.iconColor = color
    }

    private fun init(attrs: AttributeSet?) {
        orientation = LinearLayout.HORIZONTAL
        gravity = Gravity.CENTER

        inflate(context, R.layout.nessie_button_view, this)

        val styledAttributes = context.obtainStyledAttributes(attrs, R.styleable.nessie_NessieButton)

        try {
            val iconIndex = styledAttributes.getInteger(R.styleable.nessie_NessieButton_nessie_icon, -1)
            icon = when (iconIndex) {
                -1 -> null
                else -> Icon.from(iconIndex)
            }
            text = styledAttributes.getString(R.styleable.nessie_NessieButton_android_text)
            style = Style.from(styledAttributes.getInteger(R.styleable.nessie_NessieButton_nessie_buttonStyle, Style.PRIMARY.ordinal))
            size = Size.from(styledAttributes.getInteger(R.styleable.nessie_NessieButton_nessie_buttonSize, Size.LARGE.ordinal))

            isEnabled = styledAttributes.getBoolean(R.styleable.nessie_NessieButton_android_enabled, true)
        } finally {
            styledAttributes.recycle()
        }

        nessie_button_text_view.typeface = context.getFont(R.font.nessie_proximanova_bold)
    }

    override fun addView(child: View, index: Int, params: ViewGroup.LayoutParams?) {
        if (child.id == R.id.nessie_button_icon_view || child.id == R.id.nessie_button_text_view) {
            super.addView(child, index, params)
        } else {
            throw IllegalArgumentException("Cannot add more views to a NessieButton")
        }
    }
}

And this is the view layout:

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/root_view"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:gravity="center"
    android:orientation="horizontal"
    android:padding="@dimen/nessie_default_size_2x"
    tools:background="@drawable/nessie_button_primary_background"
    tools:parentTag="android.widget.LinearLayout">

    <com.classdojo.android.nessie.icon.IconImageView
        android:id="@+id/nessie_button_icon_view"
        android:layout_width="@dimen/nessie_default_size_3x"
        android:layout_height="@dimen/nessie_default_size_3x"
        android:layout_marginEnd="@dimen/nessie_default_size" />

    <TextView
        android:id="@+id/nessie_button_text_view"
        style="@style/nessie_action"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:lineHeight="22sp"
        android:textStyle="bold"
        tools:text="My button"
        tools:textColor="@color/nessie_white" />

</merge>
2 Answers

It turns out that it wasn't related to the merge/custom view, but was related to the fact that the editor preview couldn't find the font resource.

Wrapping the instances where the font was loaded in if (!isInEditMode){} resolved the problem.

I knew there was an errors list, but I hadn't used it in a while and couldn't find it. In case someone comes across this question, it's the blue i icon in the top right which shows you the errors hit when rendering the view.

Android preview toolbar

<merge is not a ViewGroup. Hence, the android:attributes on that tag are ignored. This means, that your view does not have an id, width, height, gravity nor does it have padding.

  1. Either add the attributes to where you are using your custom view like so:

     <com.classdojo.android.nessie.NessieButton
         android:id="@+id/root_view"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:gravity="center"
         android:orientation="horizontal" 
         android:padding="@dimen/item_spacing_normal"/>
    
  1. Or, if these attributes should be the same for all NessieButton's, set these properties programatically. Inside your init() methods sounds like the best place.

Be careful with the android:id as well! You probably do not want to set it programatically. But you will almost certainly want to set it, when you actually use your NessieButton.

Related