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
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.