Android self-drawing control

Posted by brianjw on Wed, 24 Jul 2019 18:47:12 +0200

During the development process, we need to use some custom View s, which can generally be divided into three categories:

(1) Inherit Class View - Basic View after the general inheritance system, add/reset some custom properties, such as two-end aligned TextView;

(2) Combination Class View - Combining several basic views of the system to form a new View, such as EditText with end band * Empty, is to combine EditText and ImageView;

(3) When self-drawing View, a special design control, cannot be implemented in either way, we need to consider self-drawing for processing. This article will focus on the implementation of such View.

 

Here we illustrate by customizing a circular Button:

Steps to customize View:

(1) Properties of a custom View;

(2) Obtain the attribute value of the View in the construction method of the custom View;

(3) Rewrite the method of measuring dimensions onMeasure(int, int); (Whether it is necessary to rewrite according to specific requirements);

(4) Rewrite the drawing method onDraw(Canvas c);

Use the properties of the custom View in the layout XML file.

 

1. Properties of Custom View:

Create a new attrs.xml property file under the directory res/values.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!--name Is the class name of the custom control-->
    <declare-styleable name="DCircleButton" parent="android.widget.Button">
        <attr name="txtSize" format="dimension"/>
        <attr name="text" format="string"/>
        <attr name="txtColor" format="color"/>
        <attr name="txtBackgroundColor" format="color"/>
    </declare-styleable>
</resources>

 

Custom attributes are in two steps:
(1) Define the theme style of the control;
(2) Define attribute names and types.

If the xml file above is the theme style of the custom control DCirclebutton, and the properties are defined in the theme style, some people may be confused about what attribute units are behind the format field?The IDE will automatically prompt you if you are using AS development, basically including the following:
dimension (font size) string (string) color (color) Boolean (boolean type) float (floating point type) integer (enmu (enumeration) fraction (percentage), etc.

2. Get the attribute values in the construction method and draw them

Step 1: Inherit View and implement (as prompted by AS) the following four.

public DCircleButton(Context context) {
    super(context);
}
public DCircleButton(Context context, @Nullable AttributeSet attrs) {
    super(context, attrs);
}
public DCircleButton(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public DCircleButton(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    super(context, attrs, defStyleAttr, defStyleRes);
}

In the second step, rewrite the four constructs so that they progressively proceed:

public DCircleButton(Context context) {
    super(context, null);
}
public DCircleButton(Context context, AttributeSet attrs) {
    super(context, attrs, 0);
}
public DCircleButton(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr, 0);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public DCircleButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    super(context, attrs, defStyleAttr, defStyleRes);
}

In the third step, we get the attribute values in the last method:

private void initAttrs(Context context, AttributeSet attrs) {
    TypedArray tArr = context.obtainStyledAttributes(attrs, R.styleable.DCircleButton);
    if (null != tArr) {
        txtColor = tArr.getColor(R.styleable.DCircleButton_txtColor, Color.BLACK); // Get Text Color
        txtSize = tArr.getDimensionPixelSize(R.styleable.DCircleButton_txtSize, 18);  // Get Text Size
        txt = tArr.getString(R.styleable.DCircleButton_text); // Get Text Content
        backgroundColor = tArr.getColor(R.styleable.DCircleButton_txtBackgroundColor, Color.GRAY); // Get text background color
        tArr.recycle();
    }
}

Step 4, Draw

/** Font color**/
private int txtColor;
/** Font Background Color**/
private int backgroundColor;
/** Font Size**/
private int txtSize;
/** Button Text **/
private String txt;
/** Circle radius**/
private float mDrawableRadius;

/** Font Background Brush**/
private Paint mBackgroundPaint;
/** Font Brush**/
private Paint mTxtPaint;

public DCircleButton(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    initAttrs(context, attrs);
    init();
}
/** Initialization**/
private void init() {
    mBackgroundPaint = new Paint();
    mBackgroundPaint.setColor(backgroundColor);

    mTxtPaint = new Paint();
    mTxtPaint.setTextAlign(Paint.Align.CENTER);
    mTxtPaint.setColor(txtColor);
    mTxtPaint.setTextSize(txtSize);
}

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    mDrawableRadius = Math.min(getWidth() >> 1, getHeight() >> 1);
    canvas.drawCircle(getWidth() >> 1, getHeight() >> 1, mDrawableRadius, mBackgroundPaint);
    if (null != txt)
        canvas.drawText(txt, getWidth() >> 1, getHeight() >> 1, mTxtPaint);
}

3. Application in Layout

<dinn.circle.button.DCircleButton
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:text="I am a button"
    app:txtBackgroundColor="@android:color/holo_orange_dark"
    app:txtColor="@android:color/white"
    app:txtSize="20sp" />

4. Running results

 

At this point it is discovered that the button is full of screen, but the dimension property we set in the layout is "wrap_content".It's really because we have an onMeasure method in the process of customizing the View that hasn't been overridden.

5. Rewrite onMeasure to control View size

When you do not override the onMeasure method, the system calls the default onMeasure method.

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}

The purpose of this method is to measure the size of the control.Actually, when the Android system loads the layout, the system measures the size of each child View to tell the parent View how much space I need, and then the parent View decides how much space to allocate to the child View based on its size.

So from the above effect, when you set the size of the View in the layout to "wrap_content", the system actually measures the size to be "match_parent".Why is this so?

That has to start with MeasureSpec's specMode l mode.There are three modes:

MeasureSpec.EXACTLY: The parent view expects the size of the child view to be the size specified in the specSize; typically, an explicit value or MATCH_PARENT is set.

MeasureSpec.AT_MOST: The size of the subview is at most the size in the specSize; it means that the sublayout is limited to a maximum, usually WARP_CONTENT.

MeasureSpec.UNSPECIFIED: The parent view does not impose any restrictions on the child view, and the child view can get any size it wants; it represents how big the child layout wants and is rarely used.

 

Let's see how the system source super.onMeasure(widthMeasureSpec, heightMeasureSpec); is implemented:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
    }
    return result;
}

As you can see from the code getDefaultSize() method above, the original MeasureSpec.AT_MOST and MasureSpec.EXACTLY went on the same branch, that is, the parent view expects the size of the child view to be the size specified in the specSize.

The default is to populate the entire parent layout.Therefore, whether your layout size is "wrap_content" or "match_parent" the effect fills the entire parent layout.So what do I want the effect of "wrap_content"?Then only the onMeasure method can be overridden.

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // Measurement mode
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    // The parent layout expects the size of the child layout,If a fixed value is set inside the layout,This takes the minimum of the fixed and parent layout size values in the layout.
    // If set to match_parent,Then take the size of the parent layout
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);

    int width;
    int height;
    Rect mBounds = new Rect();
    if (widthMode == MeasureSpec.EXACTLY) {
        width = widthSize;
    } else {
        mTxtPaint.setTextSize(txtSize);
        mTxtPaint.getTextBounds(txt, 0, txt.length(), mBounds);
        float textWidth = mBounds.width();
        int desired = (int) (getPaddingLeft() + textWidth + getPaddingRight());
        width = desired;
    }

    if (heightMode == MeasureSpec.EXACTLY) {
        height = heightSize;
    } else {
        height = width;
    }
    // Last call to parent method,hold View The size tells the parent layout.
    setMeasuredDimension(width, height);
}

The ultimate effect of this is as follows:

Topics: PHP Android Attribute xml encoding