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 ownlayout, such as LinearLayout, and then throughLayoutInflaterThe 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 asAppCompatTextView
  • Inherited fromvieworviewgroupThis 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 theonDrawComputational coordinate rendering
  • 5. Rewrite theonMeasureHigh 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.

  • inres/values/Let’s create aattrs.xmlFile, where we define ourattributeAnd 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

  • inxmlReferences 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