Home > Back-end >  Custom View onDraw is called multiple times
Custom View onDraw is called multiple times

Time:10-11

I am creating a Custom TextView where i want to have a circle background and in the middle i want to have the initials of the text. The code is the below

class CircleTextView(context: Context, attrs: AttributeSet) : AppCompatTextView(context, attrs) {

    private lateinit var circlePaint: Paint
    private lateinit var strokePaint: Paint

    private fun initViews(context: Context, attrs: AttributeSet) {
        circlePaint = Paint()
        strokePaint = Paint()
        circlePaint.apply {
            color = Color.parseColor("#041421")
            style = Paint.Style.FILL
            flags = Paint.ANTI_ALIAS_FLAG
            isDither = true
        }

        strokePaint.apply {
            color = Color.parseColor("#fe440b")
            style = Paint.Style.FILL
            flags = Paint.ANTI_ALIAS_FLAG
            isDither = true
        }
    }

    override fun draw(canvas: Canvas) {
        val diameter: Int
        val h: Int = this.height
        val w: Int = this.width
        diameter = h.coerceAtLeast(w)
        val radius: Int = diameter / 2

        canvas.drawCircle(
            radius.toFloat(), radius.toFloat(), radius.toFloat(), strokePaint
        )
        canvas.drawCircle(
            radius.toFloat(), radius.toFloat(), (radius - 10).toFloat(), circlePaint
        )
        super.draw(canvas)
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        val dimension = widthMeasureSpec.coerceAtLeast(heightMeasureSpec)
        super.onMeasure(dimension, dimension)
    }

    init {
        initViews(context, attrs)
        setWillNotDraw(false)
    }
}

Inside my activity xml

<?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:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/colorPrimary">

    <com.example.ui.views.CircleTextView
        android:id="@ id/circleText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:textColor="@color/colorWhite"
        android:textSize="18sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" /> />

</androidx.constraintlayout.widget.ConstraintLayout>

and in the Kotlin file

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_splash)

        circleText.apply {
            text = "II"
            setPadding(50, 50, 50, 50)
        }
    }

My problem here is that the View seems to gets cropped on the right side of it and also the Text with the letters is not in the Center of the view.

How can i solve that?

enter image description here

CodePudding user response:

The main answer to your question is yes: there is an infinite loop on your onDraw(Canvas) implementation with your calls to setWidth(Int) and setHeight(Int) on the TextView, which internally call invalidate(), which then calls your onDraw(Canvas) again to continue the cycle.

You should move all of your TextView.setXxx(yyy) calls (this.xxx = yyy) outside of your onDraw implementation.

For instance, if you want to force your View to have same width and height, you should override onMeasure instead:

@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // This example will always make the height equivalent to its width
    super.onMeasure(widthMeasureSpec, widthMeasureSpec); 
}

Setting up your Paints (i.e. circlePaint.apply { ... } should be moved to your initViews implementation, as they are unchanging.

Since you're only using 1 text color as well, you should also set that value in your initViews implementation (or, better, just let it be inherited by the AttributeSet used to construct the View).

If you're going to use the TextView's default setText(String) anyway (i.e. this.text = mText), you may as well just set it directly and drop your custom mText entirely:

fun setCustomText(value: String) {
    this.text = value.toSplitLetterCircle()
}

The same goes for your text size customization:

fun setCustomTextSize(value: Float) {
    this.textSize = value
}

Both of the above functions then become obsolete, and can be replaced with the standard TextView setText and setTextSize calls.

To finish, you want to add some number to the radius, which means you actually want the view to be some value larger than it is currently being rendered in. Since you're already using the TextView as your base class, you can use the View's Padding properties to add space in between the edges of your View and the start of the content within, e.g.

// Order is left, top, right, bottom, units are in px
this.setPadding(5, 5, 5, 5)

Which leaves you with:

override fun draw(canvas: Canvas) {
    val h: Int = this.height
    val w: Int = this.width
    diameter = Math.max(h, w)
    val radius: Int = diameter / 2

    canvas.drawCircle(
            (diameter / 2).toFloat(), (diameter / 2).toFloat(), radius.toFloat(), strokePaint
    )

    canvas.drawCircle(
            (diameter / 2).toFloat(), (diameter / 2).toFloat(), (radius - 5).toFloat(), circlePaint
    )

    super.draw(canvas)
}

Since all that's left in your onDraw(Canvas) are calls to draw 2 circles in the background, you can actually replace it entirely with a custom background drawable, like this:

<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="oval">
    <solid android:color="#041421"/>
    <stroke
        android:width="2dp"
        android:color="#FFFFFF"/>
</shape>

Which then means you can delete that custom onDraw(Canvas) implementation entirely

Edit to answer question part 2:

My initial advice to use the padding without accounting for it in onMeasure is incomplete on my part, and could definitely produce the effects you're seeing.

The units passed to onMeasure(int, int) are also not true width/height units, but multiple components (size and mode) together that can be accessed via the MeasureSpec class. This post goes into much more detail about how that all works. This is important, as it's the reason that your View isn't truly appearing as a perfect square, and the same reason the text isn't centered inside the circle you're drawing.

That then causes diameter = h.coerceAtLeast(w) to equal h which, according to the screenshot, is larger than w, which is why your circle exceeds the bounds on the horizontal plane, but not the vertical.

Also note, you can apply the padding directly in your XML layout as well, using the android:padding-related attributes

  • Related