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