0

I'm creating a component based on ConstraintLayout which have two ImageViews and a TextView. The width and height of them are defined in R.attrs.xml. This component will be used in many places with different sizes for each view.

<declare-styleable name="ProfileInfoView">
    <attr name="imageWidth" format="integer"/>
    <attr name="imageHeight" format="integer" />
    <attr name="badgeWidth" format="integer" />
    <attr name="badgeHeight" format="integer" />
</declare-styleable>

This is the component's layout. Pretty simple:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    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/profileInfoContaner"
    android:elevation="0dp"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <de.hdodenhof.circleimageview.CircleImageView
        android:id="@+id/imageView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:src="@drawable/ic_circle"
        app:civ_fill_color="@color/blueDark"
        app:civ_border_width="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
    <TextView
        android:id="@+id/textViewInitialsName"
        style="@style/TextBoldWhite16"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:ellipsize="end"
        android:fontFamily="@font/montserrat_bold"
        android:textSize="18sp"
        android:textColor="@color/white"
        android:gravity="center"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        tools:text="AE" />
    <androidx.appcompat.widget.AppCompatImageView
        android:id="@+id/dependentBadge"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="@dimen/_28sdp"
        android:layout_marginTop="@dimen/_28sdp"
        android:src="@drawable/ic_dependent_badge"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="1.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="1.0" />
</androidx.constraintlayout.widget.ConstraintLayout>

Reading about resizing views I noticed we need to override onMeasure and onLayout and found these links:

Custom View: mastering onMeasure

Measure… Layout… Draw!

To Constraint, Or Not To Constraint?

How to Create a Custom Layout in Android by Extending ViewGroup Class

So I create a class:

class ProfileInfoView @JvmOverloads constructor(
        context: Context,
        attrs: AttributeSet? = null,
        defStyleAttr: Int = 0
) : androidx.constraintlayout.widget.ConstraintLayout(context, attrs, defStyleAttr) {
    private lateinit var maskBitmap: Bitmap
    private var imageWidth: Int = 0
    private var imageHeight: Int = 0
    private var badgeWidth: Int = 0
    private var badgeHeight: Int = 0
    private lateinit var imageRect: RectF
    private lateinit var outerDrawable: Drawable
    private lateinit var badgeDrawable: Drawable
    private var rect = Rect(0,0,0,0)
    private var paint = Paint()
    private val childViews = HashMap<View, Pair<Int, Int>>()

    init {
        val typedArray = context.obtainStyledAttributes(attrs, R.styleable.ProfileInfoView, defStyleAttr, 0)
        imageWidth = typedArray.getDimensionPixelSize(R.styleable.ProfileInfoView_imageWidth, 0)
        imageHeight = typedArray.getDimensionPixelSize(R.styleable.ProfileInfoView_imageHeight, 0)
        badgeWidth = typedArray.getDimensionPixelSize(R.styleable.ProfileInfoView_badgeWidth, 0)
        badgeHeight = typedArray.getDimensionPixelSize(R.styleable.ProfileInfoView_badgeHeight, 0)
        imageRect = RectF(0.0f, 0.0f, imageWidth.toFloat(), imageHeight.toFloat())

        typedArray.recycle()
    }

    val dependentBadgeView: AppCompatImageView get() {
        return profileInfoViewBindingBinding.dependentBadge
    }

    val circleImageView : CircleImageView get() {
        return profileInfoViewBindingBinding.imageView
    }

    val initialNameTextView: TextView get() {
        return profileInfoViewBindingBinding.textViewInitialsName
    }

    private var profileInfoViewBindingBinding: ProfileInfoViewBinding =
            ProfileInfoViewBinding.inflate(LayoutInflater.from(context), this, true)

    private fun showInitialLettersFromProfile(profileName: String) {
        profileInfoViewBindingBinding.textViewInitialsName.visibility = View.VISIBLE
        profileInfoViewBindingBinding.textViewInitialsName.text = StringResources.getInitialLetters(profileName, " ").toUpperCase()
    }

    override fun dispatchDraw(canvas: Canvas) {
        super.dispatchDraw(canvas)
        drawCanvas(canvas)
    }

    //Makes component's background transparent
    private fun drawCanvas(canvas: Canvas) {
        if(canvas.width >0 && canvas.height >0) {
            maskBitmap = Bitmap.createBitmap(canvas.width, canvas.height, Bitmap.Config.ARGB_8888)
            val auxCanvas = Canvas(maskBitmap)
            auxCanvas.drawColor(Color.TRANSPARENT)
            val paint = Paint()
            paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_IN)
            val childRect = Rect(this.left, this.top, this.right, this.bottom)
            auxCanvas.drawRect(childRect, paint)
            canvas.drawBitmap(maskBitmap, 0.0f, 0.0f, null)
        }
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        var maxWidth = 0
        var maxHeight = 0
        var childState = 0

        addViews()

        childViews.forEach {
            val currentView = it.key
            currentView.measure(
                    MeasureSpec.makeMeasureSpec(it.value.first, MeasureSpec.EXACTLY),
                    MeasureSpec.makeMeasureSpec(it.value.second, MeasureSpec.EXACTLY))
            measureChild(it.key, widthMeasureSpec, heightMeasureSpec)

            maxWidth += Math.max(maxWidth, currentView.measuredWidth)
            maxHeight += Math.max(maxHeight, currentView.measuredHeight)

            childState = View.combineMeasuredStates(childState, currentView.measuredState)
        }

        maxHeight = Math.max(maxHeight, suggestedMinimumHeight)
        maxWidth = Math.max(maxWidth, suggestedMinimumWidth)


        setMeasuredDimension(View.resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                View.resolveSizeAndState(maxHeight, heightMeasureSpec, childState shl View.MEASURED_HEIGHT_STATE_SHIFT))
    }

    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
        var curWidth = 0
        var curHeight = 0
        var curLeft = 0
        var curTop = 0
        var maxHeight = 0

        val childLeft = this.paddingLeft;
        val childTop = this.paddingTop;
        val childRight = this.measuredWidth - this.paddingRight;
        val childBottom = this.measuredHeight - this.paddingBottom;
        val childWidth = childRight - childLeft;
        val childHeight = childBottom - childTop;

        childViews.forEach {
            val currentView = it.key
            if (currentView.visibility == View.GONE) return

            //Get the maximum size of the child
            currentView.measure(MeasureSpec.makeMeasureSpec(it.value.first, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(it.value.second, MeasureSpec.EXACTLY))
            curWidth = currentView.measuredWidth
            curHeight = currentView.measuredHeight
            //wrap is reach to the end
            if (curLeft + curWidth >= childRight) {
                curLeft = childLeft
                curTop += maxHeight
                maxHeight = 0
            }
            //do the layout
            currentView.layout(curLeft, curTop, curLeft + curWidth, curTop + curHeight)
            //store the max height
            if (maxHeight < curHeight) maxHeight = curHeight
            curLeft += curWidth
        }
    }

    private fun measureDimension(desiredSize: Int, measureSpec: Int): Int {
        var result: Int
        val specMode = MeasureSpec.getMode(measureSpec)
        val specSize = MeasureSpec.getSize(measureSpec)
        if (specMode == MeasureSpec.EXACTLY) {
            result = specSize
        } else {
            result = desiredSize
            if (specMode == MeasureSpec.AT_MOST) {
                result = Math.min(result, specSize)
            }
        }
        if (result < desiredSize) {
            Log.e("ChartView", "The view is too small, the content might get cut")
        }
        return result
    }

    private fun addViews() {
        childViews[profileInfoViewBindingBinding.imageView] = Pair(imageWidth, imageHeight)
        childViews[profileInfoViewBindingBinding.dependentBadge] = Pair(badgeWidth, badgeHeight)
    }


    fun loadUserData(userImageUrl: String, userName: String, colorIndex: Int) {
        Glide.with(this)
            .asBitmap()
            .load(userImageUrl)
            .listener(
                object: RequestListener<Bitmap> {
                    override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Bitmap>?, isFirstResource: Boolean): Boolean {
                        showInitialLettersFromProfile(userName)
                        circleImageView.circleBackgroundColor = ResourcesExt.getUserColorArray()[colorIndex]
                        return false
                    }

                    override fun onResourceReady(resource: Bitmap?, model: Any?, target: Target<Bitmap>?, dataSource: DataSource?, isFirstResource: Boolean): Boolean {
                        initialNameTextView.visibility = View.GONE
                        return false
                    }
                }
            )
            .diskCacheStrategy(DiskCacheStrategy.NONE)
            .skipMemoryCache(true)
            .centerCrop()
            .placeholder(R.drawable.ic_transparent_circle)
            .thumbnail(0.5f)
            .into(circleImageView)
    }
}

And I inserted in a layout:

...
<androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/profileTitleContainer"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/_16sdp"
        android:layout_marginBottom="@dimen/_16sdp"
        android:layout_marginStart="@dimen/_16sdp"
        android:layout_marginEnd="@dimen/_16sdp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toTopOf="@+id/checkDependents"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintVertical_bias="0.0">
        <com.component.ProfileInfoView
            android:id="@+id/profileContainer"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:imageWidth="@dimen/_55sdp"
            app:imageHeight="@dimen/_55sdp"
            app:badgeWidth="@dimen/_10sdp"
            app:badgeHeight="@dimen/_10sdp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"/>
            ...

Measuring parent view I believe there are some errors. But the mais issue here is the component is being rendered blank. It occupies space in screen but nothing is shown.

Some posts on SO related with this subject:

How to correctly override onLayout Method in a Viewgroup class

Correctly layout children with onLayout in Custom ViewGroup

Android ViewGroup: what should I do in the onLayout() override?

Even with these answers I can't solve my problem. I don't know why the component insists to be blank.

learner
  • 1,311
  • 3
  • 18
  • 39
  • A couple of things to look at: Check the [documentation](https://developer.android.com/reference/androidx/constraintlayout/widget/ConstraintLayout) about using `match_parent` in _ConstraintLayout_. (Don't do it if you are. Results are unpredictable.) Also, check the Layout Inspector in Android Studio to see if your component's views are making it to display. Are they just missing, empty or of zero size? – Cheticamp Jan 11 '21 at 13:24
  • @Cheticamp The topmost ConstraintLayout from my component, according to LayoutInspector has 0 width and 0 height. Child views has its sizes defined. So, onMeasure should define parent size, because I call measure for all child views and setMeasuredDimensions based on those values. Really doesn't know the issue. – learner Jan 11 '21 at 16:48

0 Answers0