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