(21) Learning custom controls

Foreword: Recently, some recruitment requirements require to be familiar with the drawing principle of view. I have always wanted to see how the custom control is made. I forgot what I learned a long time ago. Now it seems that the two are actually strongly related, and I am not familiar with it. Drawing principles, custom controls are also futile.


Reference blog: Click to open the link


Demo address: Click to open the link


1. hello world

It is essential to learn something new about hello world. Refer to the blog above to write a custom View of hello world to play with.

demo:

package com.example.demo_21_custom_view;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.View;

public class HelloWorldTextView extends View {
    /**
     * Text to be drawn
     */
    private String mText;
    /**
     * text color
     */
    private int mTextColor;
    /**
     * the size of the text
     */
    private int mTextSize;
    /**
     * Control the range of text drawing when drawing
     */
    private Rect mBound;
    private Paint mPaint;
    public HelloWorldTextView(Context context) {
        this(context, null);
    }

    public HelloWorldTextView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public HelloWorldTextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mText = "hello world";
        mTextColor = Color.BLACK;
        mTextSize = 100;

        mPaint = new Paint();
        mPaint.setTextSize(mTextSize);
        mPaint.setColor(mTextColor);
        //Get the width and height of the drawn text
        mBound = new Rect();
        mPaint.getTextBounds(mText, 0, mText.length(), mBound);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        // draw text
        canvas.drawText(mText, getWidth() / 2 - mBound.width() / 2, getHeight() / 2 + mBound.height() / 2, mPaint);
    }
}

This onDraw method is to display the text in the center of the component.

layout file:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.example.demo_21_custom_view.HelloWorldTextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="20sp"
        android:text=""
        android:background="#ff0000"/>
</LinearLayout>


2. Custom Properties

In fact, many of the above view construction methods are hard-coded and not configured from the configuration file, which requires custom attributes to appear.

2.1 Configuration file

Create a new attrs.xml in the res/values ​​folder as follows:

attrs.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <attr name="mText" format="string" />
    <attr name="mTextColor" format="color" />
    <attr name="mTextSize" format="dimension" />
    <declare-styleable name="MyTextView">
        <attr name="mText"/>
        <attr name="mTextColor"/>
        <attr name="mTextSize"/>
    </declare-styleable>
</resources>

2.2 Layout file reference

Add a custom namespace ( jiatai:xmlns:openxu="http://schemas.android.com/apk/res-auto" ), and use your own namespace to set the attributes you need. It doesn't matter if you try the name, just match the attribute name.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:jiatai="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.example.demo_21_custom_view.HelloWorldTextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="20sp"
        android:text=""
        android:background="#ff0000"
        jiatai:mTextSize="25sp"
        jiatai:mText="hello world attr"
        jiatai:mTextColor ="#0000ff"/>
</LinearLayout

2.3 Modify the constructor

Change properties to read from layout file

public HelloWorldTextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        /*mText = "hello world";
        mTextColor = Color.BLACK;
        mTextSize = 100;*/

        //Get the value of the custom property
        TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.MyTextView, defStyleAttr, 0);
        mText = a.getString(R.styleable.MyTextView_mText);
        mTextColor = a.getColor(R.styleable.MyTextView_mTextColor, Color.BLACK);
        mTextSize = a.getDimension(R.styleable.MyTextView_mTextSize, 100);
        a.recycle(); //Pay attention to recycling

        mPaint = new Paint();
        mPaint.setTextSize(mTextSize);
        mPaint.setColor(mTextColor);
        //Get the width and height of the drawn text
        mBound = new Rect();
        mPaint.getTextBounds(mText, 0, mText.length(), mBound);
    }


2.4 Effects

It can be seen that the font color and string have been changed accordingly


3. Override the onMeasure method

Referring to the previous custom control mentioned in the blog, although the declaration is wrap_content, the actual effect is match_parent. The modification method is to override onMeasure.

3.1 Corresponding code

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec); //Get the width mode
        int heightMode = MeasureSpec.getMode(heightMeasureSpec); //Get the height mode
        int widthSize = MeasureSpec.getSize(widthMeasureSpec); //Get the width size
        int heightSize = MeasureSpec.getSize(heightMeasureSpec); //Get the height size
        Log.v(TAG, "wide mode:"+widthMode);
        Log.v(TAG, "High Mode:"+heightMode);
        Log.v(TAG, "width size:"+widthSize);
        Log.v(TAG, "Height size:"+heightSize);
        int width;
        int height ;
        if (widthMode == MeasureSpec.EXACTLY) {
            //If match_parent or a specific value, assign it directly
            width = widthSize;
        } else {
            //If it is wrap_content, we need to get the size of the control
            float textWidth = mBound.width(); //The width of the text
            //The width of the control is the width of the text plus the padding on both sides. The padding is the padding value, which is assigned after the constructor is executed.
            width = (int) (getPaddingLeft() + textWidth + getPaddingRight());
            Log.v(TAG, "Text width:"+textWidth + "Control width:"+width);
        }
        //height is the same as width
        if (heightMode == MeasureSpec.EXACTLY) {
            height = heightSize;
        } else {
            float textHeight = mBound.height();
            height = (int) (getPaddingTop() + textHeight + getPaddingBottom());
            Log.v(TAG, "Height of text:"+textHeight + "Height of control:"+height);
        }
        // save the measured width and measured height
        setMeasuredDimension(width, height);
    }

3.2 Corresponding log


3.3 Rendering


4. Implement automatic line wrapping

After studying the code of the reference blog, I refactored and modified a little what I found to be wrong. In fact, the automatic line wrapping is still calculated by onMeasure to calculate the width and height of the control, and all characters are drawn by onDraw.


4.1 Long string branch

private void initTextToLines(int widthSize){
        float textWidth = mBound.width(); //The width of the text
        if(mTextList.size()==0){
            // segment the text
            int padding = getPaddingLeft() + getPaddingRight();
            int specWidth = widthSize - padding; //The maximum width that can display text
            if(textWidth<specWidth){
                // Description one line is enough to display
                lineNum = 1;
                mTextList.add(mText);
            }else{
                // more than one line
                isOneLines = false;
                spLineNum = textWidth/specWidth;
                if((spLineNum+"").contains(".")){
                    lineNum = Integer.parseInt((spLineNum+"").substring(0,(spLineNum+"").indexOf(".") ))+1;
                }else{
                    lineNum = spLineNum;
                }
                int lineLength = (int)(mText.length()/spLineNum);
                Log.v(TAG, "Text:"+mText);
                Log.v(TAG, "Total length of text:"+mText.length());
                Log.v(TAG, "Width of text that can be drawn:"+lineLength);
                Log.v(TAG, "Need to draw:"+lineNum+"Line");
                Log.v(TAG, "lineLength:"+lineLength);
                for(int i = 0; i<lineNum; i++){
                    String lineStr;
                    if(mText.length()<lineLength){
                        lineStr = mText.substring(0, mText.length());
                    }else{
                        lineStr = mText.substring(0, lineLength);
                    }
                    Log.v(TAG, "lineStr:"+lineStr);
                    mTextList.add(lineStr);
                    if(!TextUtils.isEmpty(mText)) {
                        if(mText.length()<lineLength){
                            //mText = mText.substring(0, mText.length());
                        }else{
                            mText = mText.substring(lineLength, mText.length());
                        }
                    }else{
                        break;
                    }
                }
            }
        }
    }


4.2 Set the component height and width according to the number of lines and string width

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec); //Get the width mode
        int heightMode = MeasureSpec.getMode(heightMeasureSpec); //Get the height mode
        int widthSize = MeasureSpec.getSize(widthMeasureSpec); //Get the width size
        int heightSize = MeasureSpec.getSize(heightMeasureSpec); //Get the height size
        Log.v(TAG, "wide mode:"+widthMode);
        Log.v(TAG, "High Mode:"+heightMode);
        Log.v(TAG, "width size:"+widthSize);
        Log.v(TAG, "Height size:"+heightSize);

        initTextToLines(widthSize);

        int width;
        int height ;
        if (widthMode == MeasureSpec.EXACTLY) {
            //If match_parent or a specific value, assign it directly
            width = widthSize;
        } else {
            //If it is wrap_content, we need to get the size of the control
            float textWidth = mBound.width();
            if(isOneLines){
                //The width of the control is the width of the text plus the padding on both sides. The padding is the padding value, which is assigned after the constructor is executed.
                width = (int) (getPaddingLeft() + textWidth + getPaddingRight());
            }else{
                //If it is multi-line, it means that the width of the control should fill the parent form
                width = widthSize;
            }
            Log.v(TAG, "Text width:"+textWidth + "Control width:"+width);
        }
        //height is the same as width
        if (heightMode == MeasureSpec.EXACTLY) {
            height = heightSize;
        } else {
            float textHeight = mBound.height();
            if(isOneLines){
                height = (int) (getPaddingTop() + textHeight + getPaddingBottom());
            }else{
                //if it is multiple lines
                height = (int) (getPaddingTop() + textHeight*lineNum + getPaddingBottom());;
            }

            Log.v(TAG, "Height of text:"+textHeight + "Height of control:"+height);
        }
        // save the measured width and measured height
        setMeasuredDimension(width, height);
    }


4.3 Drawing multi-line strings

@Override
    protected void onDraw(Canvas canvas) {
        // draw text
        //canvas.drawText(mText, getWidth() / 2 - mBound.width() / 2, getHeight() / 2 + mBound.height() / 2, mPaint);
        // draw text
        for(int i = 0; i<mTextList.size(); i++){
            mPaint.getTextBounds(mTextList.get(i), 0, mTextList.get(i).length(), mBound);
            Log.v(TAG, "mBound.h:"+mBound.height());
            Log.v(TAG, "在X:" + (getWidth() / 2 - mBound.width() / 2)+"  Y:"+(getPaddingTop() + (mBound.height() *i))+"  绘制:"+mTextList.get(i));
            canvas.drawText(mTextList.get(i), (getWidth() / 2 - mBound.width() / 2), (getPaddingTop() + (mBound.height() *i)), mPaint);
        }
    }


4.4 Effects


4.5 Change to left-aligned

Just change the starting position of the onDraw drawing string, not the middle, but the border, that is, getPaddingLeft()

@Override
    protected void onDraw(Canvas canvas) {
        // draw text
        //canvas.drawText(mText, getWidth() / 2 - mBound.width() / 2, getHeight() / 2 + mBound.height() / 2, mPaint);
        // draw text
        for(int i = 0; i<mTextList.size(); i++){
            mPaint.getTextBounds(mTextList.get(i), 0, mTextList.get(i).length(), mBound);
            Log.v(TAG, "mBound.h:"+mBound.height());
            Log.v(TAG, "在X:" + (getWidth() / 2 - mBound.width() / 2)+"  Y:"+(getPaddingTop() + (mBound.height() *i))+"  绘制:"+mTextList.get(i));
            canvas.drawText(mTextList.get(i), (getPaddingLeft()), (getPaddingTop() + (mBound.height() *i)), mPaint);
        }
    }
Effect:


4.6 Change to right alignment

Change x to the distance from the right when aligning to the left, that is, change the setting of the onDraw method x to

(getWidth() - mBound.width() - getPaddingLeft())

Effect:


hmm, can you still play it?



5. Summary

Today, I mainly learn how to do custom controls in general. I don't want to go deep into the source code level. In a simple way, onMeasure calculates the height and width of the control, and onDraw draws the specific content of the control. In fact, this is all discussed on the control itself, without discussing the placement of the control in the layout, and how to determine where the control is located is not mentioned, to be continued. . .

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325852214&siteId=291194637