preface
This article is for custom view novice, but I hope you had better have a certain theoretical knowledge, or basic concepts, some places may be a brush will not be detailed, detailed length is too long.
This article is copied from Hongyang’s custom View (1). Although nearly 7 years have passed, I think it is still valuable to learn.
The effect
Custom View classification
A brief introduction to custom View classification:
- Combination control, inherit their own
layout
, such as LinearLayout, and then throughLayoutInflater
The advantage of introducing a layout and then handling related events is that you don’t have to focus too much on the drawing mechanism inside the View, and it’s also very scalable. - Inherit from existing system controls, modify or extend certain properties or methods, such as
AppCompatTextView
- Inherited from
view
orviewgroup
This is a bit more complicated, because we have to control the drawing process ourselves, but there is also more room for imagination.
steps
Let’s analyze the effect above:
- Rectangular background with color
- Middle text
Relatively simple, the old hand a little thought already had the train of thought:
- 1. Customize attributes
- 2. Add constructors
- 3. Get custom styles in the construct
- 4. Rewrite the
onDraw
Computational coordinate rendering - 5. Rewrite the
onMeasure
High width measurement - 6. Set click events
Analyze the renderings first, then conceive, and then constantly adjust and optimize.
1. Customize attributes
This step doesn’t have to be written in front of you. Some people might think that you don’t know what attributes you’re going to use in advance, but I’ll leave it up front because it’s a simple example.
- in
res/values/
Let’s create aattrs.xml
File, where we define ourattribute
And declare our wholestyle
<? xml version="1.0" encoding="utf-8"? > <resources> <attr name="randomText" format="string"/>
<attr name="randomTextColor" format="color"/>
<attr name="randomTextSize" format="dimension"/>
<declare-styleable name="RandomTextView" >
<attr name="randomText"/>
<attr name="randomTextColor"/>
<attr name="randomTextSize"/>
</declare-styleable>
</resources>
Copy the code
Format indicates the value. The value can be string, color, demension, INTEGER, enum, reference, float, Boolean, fraction, and flag
- in
xml
References in the layout:
<com.yechaoa.customviews.randomtext.RandomTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="20dp"
app:randomText="1234"
app:randomTextColor="@color/colorAccent"
app:randomTextSize="50sp" />
Copy the code
Note the introduction of namespaces:
xmlns:app="http://schemas.android.com/apk/res-auto"
Copy the code
2. Add constructors
Create a new RandomTextView class, inherit the View, and add three constructors
class RandomTextView : View {
/ / text
private var mRandomText: String
// Text color
private var mRandomTextColor: Int = 0
// Text font size
private var mRandomTextSize: Int = 0
private var paint = Paint()
private var bounds = Rect()
// Call the construction of two arguments
constructor(context: Context) : this(context, null)
// XML calls the two-parameter construct by default, and then calls the three-parameter construct to get the custom attributes in the three-parameter construct
constructor(context: Context, attributeSet: AttributeSet?) : this(context, attributeSet, 0)
constructor(context: Context, attributeSet: AttributeSet? , defStyle:Int) : super(context, attributeSet, defStyle) { ... }... }Copy the code
Note here that all constructors refer to the third constructor, and the first two constructors inherit from this, not super.
The first construct we can create as new, the second is called by default in XML, and the third construct we can get custom attributes.
3. Get custom styles in the construct
constructor(context: Context, attributeSet: AttributeSet? , defStyle:Int) : super(context, attributeSet, defStyle) {
// Get custom attributes
val typedArray = context.theme.obtainStyledAttributes(
attributeSet,
R.styleable.RandomTextView,
defStyle,
0
)
mRandomText = typedArray.getString(R.styleable.RandomTextView_randomText).toString()
mRandomTextColor = typedArray.getColor(R.styleable.RandomTextView_randomTextColor, Color.BLACK)// Black by default
mRandomTextSize = typedArray.getDimensionPixelSize(
R.styleable.RandomTextView_randomTextSize,
TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_SP, 16F, resources.displayMetrics ).toInt()
)
// The collection is done
typedArray.recycle()
paint.textSize = mRandomTextSize.toFloat()
// Returns a text boundary, which is the smallest rectangle containing text, with no "white space", and returns a more accurate text width and height than measureText(). Data is stored in bounds
paint.getTextBounds(mRandomText, 0, mRandomText.length, bounds)
}
Copy the code
Through obtainStyledAttributes access to custom properties, and returns a TypedArray, here in the us in the attrs. The style of the declaration in the XML file (R.s tyleable. RandomTextView), The returned TypedArray contains these properties.
Take your custom View properties, assign them, and then you can paint them.
Then we use the getTextBounds method in paint:
paint.getTextBounds(mRandomText, 0, mRandomText.length, bounds)
Copy the code
The bounds are a Rect rectangle. Why do we do that? We’ll use it later when we calculate the middle of the text.
Ok, let’s start drawing the layout.
4. Rewrite onDraw to calculate coordinate drawing
@SuppressLint("DrawAllocation")
override fun onDraw(canvas: Canvas?). {
super.onDraw(canvas)
/** * When we customize a View, we need to process the padding in onDraw, otherwise it will not work */ when we customize a ViewGroup, the padding of the child View will be processed in onMeasure
/**
* 矩形背景
*/
paint.color = Color.YELLOW
// Calculate the coordinates, since the origin is in the lower left corner of the text, the left side should go to the left if it extends, so it is minus, the right side and bottom side are positive, so it is pluscanvas? .drawRect( (0 - paddingLeft).toFloat(),
(0 - paddingTop).toFloat(),
(measuredWidth + paddingRight).toFloat(),
(measuredHeight + paddingBottom).toFloat(),
paint
)
/** * text */
paint.color = mRandomTextColor
// Note that the coordinate xy is not the upper left corner, but the lower left corner, so the heights are additive. In the custom view, the lower right of the coordinate axis is positive
/ / getWidth equals measuredWidthcanvas? .drawText( mRandomText, (width /2 - bounds.width() / 2).toFloat(),
(height / 2 + bounds.height() / 2).toFloat(),
paint
)
}
Copy the code
The above code displays a rectangular background with a YELLOW color drawn in onDraw, and then draws the text with a custom property color centered on it.
Now, notice the coordinates of the position that we’re computing, so in the custom view, the origin is the top left corner of the view, and in the mathematical coordinate system, the origin (0,0) is in the middle, so there’s a difference.
Second, if there is padding in the XML layout, or if it is expected to use padding, you should also add the padding data when rewriting onDraw, otherwise the padding will not work. If you inherit a ViewGroup, the padding of the child view is handled in an onMeasure.
Here’s what happens:
The width and height of the XML is wrAP_content, so why is it filled with the parent layout?
That’s where onMeasure comes in. Scroll down.
5. Rewrite onMeasure to measure width and height
We can set the view width and height in XML in three ways:
- match_parent
- wrap_content
- Specific data, like 100dp
MeasureSpec: MeasureSpec: MeasureSpec: MeasureSpec: MeasureSpec: MeasureSpec: MeasureSpec
- EXACTLY: usually set to an exact value or MATCH_PARENT
- AT_MOST: indicates that the child layout is limited to a maximum value, usually WARP_CONTENT
- UNSPECIFIED: Indicates that the child layout is as large as it wishes and is rarely used
Since our XML is wrap_content, which corresponds to AT_MOST, the effect will be to fill up the available space in the parent layout, which fills the screen, so our custom view will also fill up the full screen.
What we really want is for the view to wrap itself, not cover the full screen, so we need to do this in onMeasure
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
/** * EXACTLY: MATCH_PARENT * AT_MOST: MATCH_PARENT * AT_MOST: MATCH_PARENT * AT_MOST: MATCH_PARENT * AT_MOST: MATCH_PARENT * AT_MOST: WARP_CONTENT * UNSPECIFIED
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
var width = 0
var height = 0
// If the width is specified, or if the width is not restricted, use the available width. If WARP_CONTENT, use the text width, plus the left and right padding
when (widthMode) {
MeasureSpec.UNSPECIFIED,
MeasureSpec.EXACTLY -> {
width = widthSize + paddingLeft + paddingRight
}
MeasureSpec.AT_MOST -> {
width = bounds.width() + paddingLeft + paddingRight
}
}
// If the height is specified, or if there is no limit on the height, use the available height. If it is WARP_CONTENT, use the text height, plus the padding
when (heightMode) {
MeasureSpec.UNSPECIFIED,
MeasureSpec.EXACTLY -> {
height = heightSize + paddingTop + paddingBottom
}
MeasureSpec.AT_MOST -> {
height = bounds.height() + paddingTop + paddingBottom
}
}
// Save the measured width and height
setMeasuredDimension(width, height)
}
Copy the code
The code above does two things:
- Gets the mode of view width and height
- The width and height are re-measured for different modes
Finally, don’t forget to call setMeasuredDimension to save the newly measured width and height, otherwise it won’t work.
At this point to see the effect is the appearance in the effect drawing.
6. Set click events
Ok, at this point, the view is drawn, but there’s no event, so we add a click event to the construct
constructor(context: Context, attributeSet: AttributeSet? , defStyle:Int) : super(context, attributeSet, defStyle) {
...
/** * add click event */
this.setOnClickListener {
mRandomText = randomText()
/ / update
postInvalidate()
}
}
Copy the code
RandomText method:
/** * Arbitrary number */ depending on the length of the text
private fun randomText(a): String {
val list = mutableListOf<Int> ()for (index in mRandomText.indices) {
list.add(Random.nextInt(10))}val stringBuffer = StringBuffer()
for (i in list) {
stringBuffer.append("" + i)
}
return stringBuffer.toString()
}
Copy the code
After the event is triggered, the text is updated and the view redraws to update the page.
For data retrieval, that is, the changed number, you can either write an onTextChanged interface or write an open method retrieval.
conclusion
In fact, if you look at the effect, it is not as simple as TextView, and TextView can easily achieve the effect of the effect map.
So the focus of this article is not to implement the effect, but to learn to understand the custom View and its drawing process.
Theory to see more also need to practice, it is better to follow two times, understand digestion.
The e comments are very detailed and even a bit verbose.
If it helps you at all, give it a thumbs up
Github
Github.com/yechaoa/Cus…
reference
- Android custom View (1)
- Custom View 1-1 drawing basis