Android custom controls: text controls of adaptive size

demand

Adaptive size text:
Design sketch:

In project development, developers lay out some pages with text according to a set of sizes provided by UI personnel.
Often less consideration will be given to some data limits, resulting in the problem of screen adaptation.
For example:
Text (or numeric) length is variable, such as experience value, number of gold coins, etc. If the page is placed in multiple Textview layouts at the same height, it may cause overlap when the text length of Textview increases.

There are many more examples, and I believe many developers have encountered them.
Today we will write a simple example to solve this problem.

Actual requirements in our project:

1. The text control is displayed in single line and center.
2. Text control has a fixed size by default. If there is too much text, the font size automatically decreases, and the text is still fully displayed without folding.
3. The size of text control decreases, the text remains unchanged, the font size automatically decreases, and the text is still fully displayed without folding.

In fact, 2 and 3 carefully analyze the meaning of:
When the space of the text control is not enough to store the current text, the font size will automatically shrink.

Analysis

In general, many developers inherit TextView and control its properties to achieve results.

Let's take a different approach.
TextView text, in the final analysis, is a View, and the text we see is actually drawn on the View space (i.e., canvas) through a brush.
So let's use the brush to do it.

Maybe developers will worry about the difficulty, but it's not at all difficult, just a few basic knowledge (1. Canvas, 2. Brush, 3. Computation of text rendering) is enough.
The control code we customize is no more than inheriting TextView directly, and you can learn more useful knowledge.

So let's start to analyze:
How to adjust the size of text in a certain space so that it can be fully displayed?
The principle is very simple, that is, to reduce the size of the line, then how small? How to calculate it?

1. The control we want to implement is only a text display, the interface is not complex, so we do not need to use XML, just write a custom class;
2. Custom class inherits View directly;
3. Information available in View or incoming from outside (i.e. known conditions):
    3.1 Text (Outside Input)
    3.2 MaxSize (the default font size we set, because the font size will only shrink, will not increase, so it is also called the maximum font size)
    3.3 The width of PreWidth (available through Paint, Text, MaxSize) that should be displayed with the maximum font size for text
    3.4 Space width canvas Width (i.e. canvas width, which can be obtained in onSizeChanged method of View);
   
    Then, when space is insufficient to display text, the corresponding font size X of canvasWidth should be calculated by known conditions.
    How to calculate it?
    After analysis, the width ratio of large font display to small font display should be the same, so the formula is as follows:
    MaxSize / PreWidth = x / canvasWidth;   
    x = MaxSize * canvasWidth / PreWidth;
    x is the number we need to reset.
    With the font size, we can set the size of the text for the brush, and we can draw the appropriate size of the text through the brush.

4. Drawing text, how to deal with the coordinates of text?
    Here we take the center display as an example:
    We can calculate the position (x,y) of the text to be drawn by the width of the text and the width of the canvas.
    For example:
    Containers (i.e. canvas) are 10 wide, 10 high, 4 wide and 4 high. We think of containers and words as two rectangles, respectively.
    How to make the text rectangle center in the container rectangle, we need to calculate the position of the upper left corner of the text rectangle (x,y), that is, the position of the text drawing.
    In fact, it is also very simple:
    x = container width - text width) / 2 = 1 (0 - 4) / 3 = 3
    y = container height - text height / 2 = 3

    If we use this (x,y) directly to draw text, the result is that the text position is shifted upward, which is not what we want.
    In fact, text rendering is based on baseline. We also need to understand the structure calculation of text rendering.
    (How the text is drawn is described below)

Okay, the basic knowledge points have been analyzed. Let's get familiar with these foundations first.
***

Drawing foundation

The basic knowledge introduced in this chapter only describes the current needs, not particularly detailed, if necessary, please consult the information yourself.
1.Canvas canvas, Paint brush
What we see on the control is actually drawn on the canvas, and drawing is a brush tool.
Canvas has many methods, such as: drawing text, circle, rectangle, line, path, picture, etc.

Canvas(The method of drawing text provided by Canvas:
    public void drawText(char[] text, int index, int count, float x, float y, Paint paint) {
        throw new RuntimeException("Stub!");
    }

    public void drawText(String text, float x, float y, Paint paint) {
        throw new RuntimeException("Stub!");
    }

    public void drawText(String text, int start, int end, float x, float y, Paint paint) {
        throw new RuntimeException("Stub!");
    }

    public void drawText(CharSequence text, int start, int end, float x, float y, Paint paint) {
        throw new RuntimeException("Stub!");
    }

Brushes can set the desired effects, such as color, font, font size, line thickness, etc.

Paint(Brushes) Some of the main ways to set text attributes:
    //Setting text size
    public void setTextSize(float textSize) {
        throw new RuntimeException("Stub!");
    }
    //Set color
    public void setColor(int color) {
        throw new RuntimeException("Stub!");
    }
    //Setting Text Spacing
    public void setLetterSpacing(float letterSpacing) {
        throw new RuntimeException("Stub!");
    }
    //Setting text alignment
    public void setTextAlign(Paint.Align align) {
        throw new RuntimeException("Stub!");
    }
   //Set text type: such as Song style, boldface, square font, etc.
    public Typeface setTypeface(Typeface typeface) {
        throw new RuntimeException("Stub!");
    }
    //Compute the width of the text (according to the size of the font set, the content of the text to be drawn, calculate and return the width that the text should occupy)
    public float measureText(String text) {
        throw new RuntimeException("Stub!");
    }
    ...
    //There are many ways to set up text, you can find it by yourself!!!

Specifically, refer to this article:
https://www.jianshu.com/p/f69873371763
https://blog.csdn.net/ttjay000/article/details/73876973

2. Text rendering
Want to draw the text accurately on the screen.
FontMetrics and its top, ascent, descent, bottom and leading five member variables need to be understood.

If we want to start drawing text at the top-left corner of the canvas (i.e. (0,0), when we finish drawing, we see the parts below Baseline as shown above, which we can't see.

The reason is very simple. Text drawing is based on Baseline. If we directly set the position of drawing to (0,0), that is, the position of Baseline to (0,0), as shown in the figure above, the text above Baseline must be drawn on the control, that is, y is negative, so we can not see the content above Baseline.

So, how can we draw the text where we want it to be? In fact, it's very simple. We just need to move Baseline downward, but how much?
Looking at the figure above, we only need to get a few attributes above the text (ascent, etc.) to calculate it.

Specific reference can be made to this article:
https://www.cnblogs.com/tianzhijiexian/p/4297664.html

code implementation

Custom control class: AutoTextView

package iwangzhe.customautosizetextview;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;
import android.widget.TextView;

/**
 * Class: AutoTextView
 * Author: qxc
 * Date: 2018/4/3.
 */
public class AutoTextView extends View {
    private Context context;//context
    private int canvasWidth;//Canvas width
    private int canvasHeight;//Canvas height
    private Paint paint;//Paint brush

    private int maxTextSize = 50;//The default font (maximum font), unit SP, if not imported, we default to 50sp, you can modify it by yourselves.
    private String text = "";//Text content imported from the outside world
    public AutoTextView(Context context) {
        super(context);
        this.context = context;
        initPaint();//Initialize Brush Properties
    }

    public AutoTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.context = context;
        initPaint();//Initialize Brush Properties
    }

    //Initialize Brush Properties
    private void initPaint(){
        //Setting Anti-aliasing
        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        //Set Brush Width
        paint.setStrokeWidth(1);
    }

    /**
     * Setting text (for external calls)
     * @param text text
     */
    public AutoTextView setText(String text){
        this.text = text;
        return this;
    }

    /**
     * Set the maximum text number supported (for external calls)
     * @param size Text font size
     */
    public AutoTextView setMaxTextSize(int size){
        this.maxTextSize = size;
        return this;
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        //This method is called every time the appearance changes.
        this.canvasWidth = getWidth();//Get Canvas Width
        this.canvasHeight = getHeight();//Get canvas height
    }

    @Override
    protected void onDraw(Canvas canvas) {
        //Every time you redraw, draw the text message that is passed in
        drawText(canvas);
    }

    //Draw text
    private void drawText(Canvas canvas){
        //According to the width of the canvas, get the appropriate font size (that is, the font size that can display the full current width can only be small but not large compared with maxsize).
        float textSize = getRightSize();
        //Set the appropriate font size for the brush
        paint.setTextSize(sp2px(textSize));

        // Calculate the X-axis coordinates of the starting point drawn by Baseline in half the width of the canvas - half the width of the text
        int x = (int) (canvasWidth / 2 - paint.measureText(text) / 2);

        //int x = 0;
        // Calculate the Y coordinates drawn by Baseline in half the height of the canvas - half the total height of the text
        int y = (int) ((canvasHeight / 2) - ((paint.descent() + paint.ascent()) / 2));

        //Draw text
        canvas.drawText(text,x,y,paint);
    }

    //Get the right number
    private float getRightSize(){
        paint.setTextSize(sp2px(maxTextSize));
        paint.setTextAlign(Paint.Align.LEFT);
        //Based on the maximum value, the width of the current text occupation is calculated.
        float preWidth = paint.measureText(text);
        //If the width of the text is smaller than that of the canvas, it means that the current font is returned directly without scaling.
        if(preWidth<canvasWidth){
            return maxTextSize;
        }
        //Given the current text size, text occupancy width, canvas width, calculate the appropriate font size, and return
        return maxTextSize*canvasWidth/preWidth;
    }
    
    //Pixels are needed for actual rendering. Here we provide the method of sp to px.
    private int sp2px(float spValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (spValue * scale + 0.5f);
    }
}

Caller class and layout file: MainActivity
Use SeekBar to simulate text control width changes (i.e., container size changes)

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="iwangzhe.customautosizetextview.MainActivity">

    <iwangzhe.customautosizetextview.AutoTextView
        android:layout_width="match_parent"
        android:background="#eeeeee"
        android:layout_height="50dp"
        android:layout_marginTop="20dp"
        android:id="@+id/atv" />

    <SeekBar
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@+id/atv"
        android:layout_marginTop="50dp"
        android:progress="100"
        android:id="@+id/sb" />
</RelativeLayout>
package iwangzhe.customautosizetextview;
import android.content.Context;
import android.content.res.Configuration;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.RelativeLayout;
import android.widget.SeekBar;

public class MainActivity extends AppCompatActivity {
    SeekBar sb;
    AutoTextView atv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        initView();
        initEvent();
    }

    void initView(){
        sb = (SeekBar) findViewById(R.id.sb);
        sb.setProgress(100);
        atv = (AutoTextView) findViewById(R.id.atv);
        //Setting up test data
        atv.setText("One two three four five six seven eight nine ten").setMaxTextSize(50);
    }

    void initEvent(){
        //Use SeekBar to simulate text control width changes (i.e., container size changes)
        sb.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
            @Override
            public void onProgressChanged(SeekBar seekBar, int i, boolean b) {
                RelativeLayout.LayoutParams linearParams = (RelativeLayout.LayoutParams) atv.getLayoutParams();
                linearParams.height = dip2px(MainActivity.this, 50);
                linearParams.width = i * sb.getWidth()/100;
                atv.setLayoutParams(linearParams);
            }

            @Override
            public void onStartTrackingTouch(SeekBar seekBar) {

            }

            @Override
            public void onStopTrackingTouch(SeekBar seekBar) {

            }
        });
    }

    public static int dip2px(Context context, float dpValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5f);
    }
}

Conclusion:
This control is very simple to use, but the technical point is very practical. It will be used in the customized control of dynamic report later.
If you want to add text font, bold, underscore and other styles, please refer to this example, expand it by yourself.
This article is mainly to let you know the process of using the canvas brush to customize the control. If you want to use it in your own project, please adjust and optimize it according to your needs.

Demo Download Address:
(In order to reduce the size of Demo, I deleted the files under the build. After you get the rebuild code, you can do it.)
https://pan.baidu.com/s/1Fu81a-Efki_MpTDkfls0JA

Keywords: Android xml less encoding

Added by FredFredrickson2 on Sat, 12 Oct 2019 03:46:12 +0300