自定义View实现一个框状数据展示控件

在写文章之前一直不知道该如何描述这个控件,想了半天称呼其为框状数据展示控件,话不多说,还是直接上图比较简单



其实像这种控件应该也可以通过自定义ViewGroup实现的,不过我这里就是一个自定义的View,话不多说,直接上代码吧!
首先是属性的设置


        
        
        
        
        
        
            
            
            
            
        
        
        
        
        
        
        
        
        

再看一下控件的属性及初始化:

    private float data;
    private int mainColor;
    private int textColor;
    private float margin;
    //绘制框内文字的画笔
    private TextPaint mainTextPaint;
    //绘制框外符号文字的画笔
    private TextPaint symbolTextPaint;
    //绘制方框的画笔
    private Paint framePaint;
    private float textSize;
    private float symbolTextSize;
    private DecimalFormat format;
    private int dataType;
    //数据对应的字符串
    private String dataText;
    private float frameWidth;
    private RectF frameRect;
    private float cornerRadius;

    public DataFrameView(Context context) {
        this(context,null);
    }

    public DataFrameView(Context context, @Nullable @android.support.annotation.Nullable AttributeSet attrs) {
        this(context, attrs,0);

    }

    public DataFrameView(Context context, @Nullable @android.support.annotation.Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);
    }

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

    private void init(Context context, AttributeSet attrs) {
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.DataFrameView);
        data = ta.getFloat(R.styleable.DataFrameView_data,0f);
        mainColor = ta.getColor(R.styleable.DataFrameView_mainColor, context.getResources().getColor(R.color.blue));
        textColor = ta.getColor(R.styleable.DataFrameView_frameTextColor, context.getResources().getColor(R.color.white));
        //PixelUtils.dp2px()将dp转化为px
        margin = ta.getDimension(R.styleable.DataFrameView_margin, PixelUtils.dp2px(2));
        textSize = ta.getDimension(R.styleable.DataFrameView_frameTextSize, PixelUtils.dp2px(14));
        cornerRadius = ta.getDimension(R.styleable.DataFrameView_radius, 0);
        symbolTextSize = textSize;
        dataType = ta.getInt(R.styleable.DataFrameView_dataType, 0);
        ta.recycle();
        mainTextPaint = new TextPaint();
        symbolTextPaint = new TextPaint();
        framePaint = new Paint();
        //初始化文字画笔
        initTextPaint(mainTextPaint,textColor,textSize,true);
        initTextPaint(symbolTextPaint,mainColor,symbolTextSize,false);
        framePaint.setAntiAlias(true);
        framePaint.setStyle(Paint.Style.FILL);
        framePaint.setColor(mainColor);
        //根据data和dataType获取数据字符串
        formatText();
    }

初始化文字画笔的函数:

 private void initTextPaint(TextPaint textPaint, int textColor, float textSize,boolean isCenter) {
        textPaint.setAntiAlias(true);
        textPaint.setTextSize(textSize);
        textPaint.setTextAlign(isCenter?Paint.Align.CENTER : Paint.Align.LEFT);
        textPaint.setTypeface(Typeface.DEFAULT_BOLD);
        textPaint.setColor(textColor);
}

在绘制之前,首先要准备几件事:

  • 获取数据转化后的字符串;
  • 根据数据转化后的字符串的个数方框的个数;
  • 计算出每个方框的宽度;
  • 根据方框的宽度选择是否需要调整框内字体的大小。

首先看是如何获取字符串的:

 //格式化数据
    private void formatText(){
        if(format==null)
           format = dataType == 0?new DecimalFormat("0,000,000.00") : new DecimalFormat("00.00%");
        dataText = format.format(data);
    }

很简单的一个方法,只是根据dataType类型做了一下数据的格式化,根据项目需求我这里的位数已经固定了,多余的地方用0填充,也可以根据自己的需求自定义比如说不固定位数。
然后就是计算方框的个数了,其实就是字符串除去,.的字符个数。
接着就是计算每个方框的宽度,只要计算出所有方框的宽度总和再除以方框个数就行了,方框的宽度总和又该如何去求呢?只要将控件的宽度减去所有符号占用的宽度之和以及所有的间隔宽度之和,便是方框的宽度和了,符号占用的宽度只需要调用paintmeasureText就行了,因为每两个字符之间都存在间隔,所以只需要将字符长度减一便是间隔的个数,具体代码如下:

 //计算每个框框的宽度
    private void calculateFrameSize() {
        //方框宽度总和
        float totalFrameWidth = 0f;
        //符号宽度总和
        float totalSymbolWidth = 0f;
        //间隔宽度总和
        float totalMargin = 0f;
        //除符号以外数据长度,也就是方框个数
        int dataCount = 0;
        //算出"."的总宽度
        int pointCount = calculateSymbolCount(dataText,".");
        float pointWidth = symbolTextPaint.measureText(".");
        float totalPointWidth = pointWidth * pointCount;
        if(dataType == 0){
            //算出“,”的总宽度
            int commaCount = calculateSymbolCount(dataText,",");
            float commaWidth = symbolTextPaint.measureText(",");
            float totalCommaWidth = commaWidth * commaCount;
            //符号宽度总和
            totalSymbolWidth = totalCommaWidth + totalPointWidth;
            dataCount = dataText.length() - commaCount - pointCount;
        }else{
            //符号宽度总和
            totalSymbolWidth = totalPointWidth;
            dataCount = dataText.length() - pointCount;
        }
        //为了美观,调整间隔大小
        if(((dataText.length() - 1) * margin) > (getWidth() - totalSymbolWidth)  / 4){
            margin = (getWidth() - totalSymbolWidth) / (4 * (dataText.length() - 1));
        }
        //间隔宽度总和
        totalMargin = (dataText.length() - 1) * margin;
        //方框宽度总合
        totalFrameWidth = getWidth() - totalSymbolWidth - totalMargin;
        //当个方框宽度
        frameWidth = totalFrameWidth / dataCount;
        //方框宽度必须大于0
        if(frameWidth > 0)
            //调整字体大小
            adjustMainTextSize();
    }

计算符号个数的函数:

    private int calculateSymbolCount(String str,String symbol){
        int fromIndex = 0;
        int count = 0;
        while(true){
            int index = str.indexOf(symbol, fromIndex);
            if(-1 != index){
                fromIndex = index + 1;
                count++;
            }else{
                break;
            }
        }
        return count;
    }

接下来就是调整字体大小,保证字体显示不能超出方框,需要了解文字的测量规则,我在自定义View实现一个环形比例图以及相关原理这篇文章中以及写过,这里不再赘述:

    //调整字体大小
    private void adjustMainTextSize(){
        //字体宽度
        float dataWidth;
        if(dataType == 0){
            dataWidth = mainTextPaint.measureText("0");
        }else {
            dataWidth = mainTextPaint.measureText("%");
        }
        Paint.FontMetrics fontMetrics = mainTextPaint.getFontMetrics();
        //获取字体高度
        float dataHeight = fontMetrics.bottom - fontMetrics.top;
        //字体显示不能超出方框
        if(dataWidth > frameWidth || dataHeight > getHeight()){
            textSize-= PixelUtils.dp2px(1);
            mainTextPaint.setTextSize(textSize);
            adjustMainTextSize();
        }
    }

最后就是绘制View了:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //如果方框宽度不大于0就直接返回
        if(frameWidth <= 0)
            return;
        //标记当前绘制的left
        float left = 0;
        for (int i = 0; i < dataText.length(); i++) {
            //获取当前需要绘制的字符
            String sub = dataText.substring(i, i + 1);
            //字符是","或者"."
            if(sub.equals(",") || sub.equals(".")){
                Paint.FontMetrics fontMetrics = symbolTextPaint.getFontMetrics();
                canvas.drawText(sub,left,getHeight() - fontMetrics.bottom,symbolTextPaint);
                left = left + symbolTextPaint.measureText(sub) + margin;
            }
            //字符是数据
            else{
                //画框架
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                    canvas.drawRoundRect(left,0,left+frameWidth,getHeight(),cornerRadius,cornerRadius,framePaint);
                }else {
                    canvas.drawRoundRect(new RectF(left,0,left + frameWidth,getHeight()),cornerRadius,cornerRadius,framePaint);
                }
                //画数据
                Paint.FontMetrics fontMetrics = mainTextPaint.getFontMetrics();
                float subY = getHeight() / 2 + (fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.bottom;
                canvas.drawText(sub,left + frameWidth / 2,subY,mainTextPaint);
                left = left + frameWidth + margin;
            }
        }
    }

向外暴露一些修改属性的接口,我这里只添加了一个修改数据的方法:

    public void setNewData(float data) {
        int oldSize = this.dataText.length();
        this.data = data;
        formatText();
        int newSize = this.dataText.length();
        if(newSize > oldSize)
            calculateFrameSize();
        invalidate();
    }

最后贴一下完整代码:

public class DataFrameView extends View {

    private float data;
    private int mainColor;
    private int textColor;
    private float margin;
    private TextPaint mainTextPaint;
    private TextPaint symbolTextPaint;
    private Paint framePaint;
    private float textSize;
    private float symbolTextSize;
    private DecimalFormat format;
    private int dataType;
    private String dataText;
    private float frameWidth;
    private RectF frameRect;
    private float cornerRadius;

    public DataFrameView(Context context) {
        this(context,null);
    }

    public DataFrameView(Context context, @Nullable @android.support.annotation.Nullable AttributeSet attrs) {
        this(context, attrs,0);

    }

    public DataFrameView(Context context, @Nullable @android.support.annotation.Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);
    }

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

    private void init(Context context, AttributeSet attrs) {
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.DataFrameView);
        data = ta.getFloat(R.styleable.DataFrameView_data,0f);
        mainColor = ta.getColor(R.styleable.DataFrameView_mainColor, context.getResources().getColor(R.color.blue));
        textColor = ta.getColor(R.styleable.DataFrameView_frameTextColor, context.getResources().getColor(R.color.white));
        margin = ta.getDimension(R.styleable.DataFrameView_margin, PixelUtils.dp2px(2));
        textSize = ta.getDimension(R.styleable.DataFrameView_frameTextSize, PixelUtils.dp2px(14));
        cornerRadius = ta.getDimension(R.styleable.DataFrameView_radius, 0);
        symbolTextSize = textSize;
        dataType = ta.getInt(R.styleable.DataFrameView_dataType, 0);
        ta.recycle();
        mainTextPaint = new TextPaint();
        symbolTextPaint = new TextPaint();
        framePaint = new Paint();
        initTextPaint(mainTextPaint,textColor,textSize,true);
        initTextPaint(symbolTextPaint,mainColor,symbolTextSize,false);
        framePaint.setAntiAlias(true);
        framePaint.setStyle(Paint.Style.FILL);
        framePaint.setColor(mainColor);
        formatText();
    }

    private void initTextPaint(TextPaint textPaint, int textColor, float textSize,boolean isCenter) {
        textPaint.setAntiAlias(true);
        textPaint.setTextSize(textSize);
        textPaint.setTextAlign(isCenter?Paint.Align.CENTER : Paint.Align.LEFT);
        textPaint.setTypeface(Typeface.DEFAULT_BOLD);
        textPaint.setColor(textColor);
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //如果方框宽度不大于0就直接返回
        if(frameWidth <= 0)
            return;
        //标记当前绘制的left
        float left = 0;
        for (int i = 0; i < dataText.length(); i++) {
            //获取当前需要绘制的字符
            String sub = dataText.substring(i, i + 1);
            //字符是","或者"."
            if(sub.equals(",") || sub.equals(".")){
                Paint.FontMetrics fontMetrics = symbolTextPaint.getFontMetrics();
                canvas.drawText(sub,left,getHeight() - fontMetrics.bottom,symbolTextPaint);
                left = left + symbolTextPaint.measureText(sub) + margin;
            }
            //字符是数据
            else{
                //画框架
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                    canvas.drawRoundRect(left,0,left+frameWidth,getHeight(),cornerRadius,cornerRadius,framePaint);
                }else {
                    canvas.drawRoundRect(new RectF(left,0,left + frameWidth,getHeight()),cornerRadius,cornerRadius,framePaint);
                }
                //画数据
                Paint.FontMetrics fontMetrics = mainTextPaint.getFontMetrics();
                float subY = getHeight() / 2 + (fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.bottom;
                canvas.drawText(sub,left + frameWidth / 2,subY,mainTextPaint);
                left = left + frameWidth + margin;
            }
        }
    }


    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if(getWidth()!=0 && getWidth() !=0)
            calculateFrameSize();
    }

    //计算每个框框的宽度
    private void calculateFrameSize() {
        //方框宽度总和
        float totalFrameWidth = 0f;
        //符号宽度总和
        float totalSymbolWidth = 0f;
        //间隔宽度总和
        float totalMargin = 0f;
        //除符号以外数据长度,也就是方框个数
        int dataCount = 0;
        //算出"."的总宽度
        int pointCount = calculateSymbolCount(dataText,".");
        float pointWidth = symbolTextPaint.measureText(".");
        float totalPointWidth = pointWidth * pointCount;
        if(dataType == 0){
            //算出“,”的总宽度
            int commaCount = calculateSymbolCount(dataText,",");
            float commaWidth = symbolTextPaint.measureText(",");
            float totalCommaWidth = commaWidth * commaCount;
            //符号宽度总和
            totalSymbolWidth = totalCommaWidth + totalPointWidth;
            dataCount = dataText.length() - commaCount - pointCount;
        }else{
            //符号宽度总和
            totalSymbolWidth = totalPointWidth;
            dataCount = dataText.length() - pointCount;
        }
        //为了美观,调整间隔大小
        if(((dataText.length() - 1) * margin) > (getWidth() - totalSymbolWidth)  / 4){
            margin = (getWidth() - totalSymbolWidth) / (4 * (dataText.length() - 1));
        }
        //间隔宽度总和
        totalMargin = (dataText.length() - 1) * margin;
        //方框宽度总合
        totalFrameWidth = getWidth() - totalSymbolWidth - totalMargin;
        //当个方框宽度
        frameWidth = totalFrameWidth / dataCount;
        //方框宽度必须大于0
        if(frameWidth > 0)
            //调整字体大小
            adjustMainTextSize();
    }

    //调整字体大小
    private void adjustMainTextSize(){
        //字体宽度
        float dataWidth;
        if(dataType == 0){
            dataWidth = mainTextPaint.measureText("0");
        }else {
            dataWidth = mainTextPaint.measureText("%");
        }
        Paint.FontMetrics fontMetrics = mainTextPaint.getFontMetrics();
        //获取字体高度
        float dataHeight = fontMetrics.bottom - fontMetrics.top;
        //字体显示不能超出方框
        if(dataWidth + 10 > frameWidth || dataHeight + 10 > getHeight()){
            textSize-= PixelUtils.dp2px(1);
            mainTextPaint.setTextSize(textSize);
            adjustMainTextSize();
        }
    }

    //格式化数据
    private void formatText(){
        if(format==null)
           format = dataType == 0?new DecimalFormat("0,000,000.00") : new DecimalFormat("00.00%");
        dataText = format.format(data);

    }

    private int calculateSymbolCount(String str,String symbol){
        int fromIndex = 0;
        int count = 0;
        while(true){
            int index = str.indexOf(symbol, fromIndex);
            if(-1 != index){
                fromIndex = index + 1;
                count++;
            }else{
                break;
            }
        }
        return count;
    }

    public void setNewData(float data) {
        int oldSize = this.dataText.length();
        this.data = data;
        formatText();
        int newSize = this.dataText.length();
        if(newSize > oldSize)
            calculateFrameSize();
        invalidate();
    }
}

如果有不正确的地方欢迎指正!

你可能感兴趣的:(自定义View实现一个框状数据展示控件)