美文网首页
自定义View入门(六) - 绘制文字

自定义View入门(六) - 绘制文字

作者: 黄烨1121 | 来源:发表于2017-11-03 09:58 被阅读0次

    本章目录

    • Part One:自定义View绘制文字

    自定义View无非就是绘制图形和文字,本来觉得文字这块没啥难度,后来想了想,文字摆放位置这块还是有点技术含量在里面。

    Part One:自定义View绘制文字

    文字的绘制步骤跟图形还是一样的,首先创建一个画笔和Context。Context是为了密度转换使用的,比如dp转px或者sp转px之类的,这个都有相应的工具类,知道咋用即可。

        //文字画笔
        private Paint textPaint;
        //为了密度转换,需要一个context
        private Context context;
    

    然后在构造方法里为context初始化,这步写的时候别忘了~

    this.context = context;
    

    接着呢,初始化画笔,这里用DesnsityUtils工具类的sp2px方法,设定画笔的文字大小,其它的都和画图形一样:

            //初始化文字画笔
            textPaint = new Paint();
            textPaint.setAntiAlias(true);
            textPaint.setColor(circleColor);
            textPaint.setStyle(Paint.Style.STROKE);
            textPaint.setTextSize(DensityUtils.sp2px(context, 22));
    

    最后就是在onDraw方法里调用canvas的drawText方法了。

            //在中心点绘制文字
            String text = currentProgress + "%";
            canvas.drawText(text, x, y, textPaint);
    

    这里面有4个参数,text和textPaint好理解,一个是要绘制的文字,一个是绘制文字的画笔,那么x和y分别是啥。

    1. 先来看x
      从注释里可以看出,x是文字的起始x坐标,也就是最左边的位置。
      如果想要文字居中要怎么做的,很简单,用View的x轴中心点,减去文字宽度的一半即可:
            //在中心点绘制文字
            String text = currentProgress + "%";
            //获取文字的宽度,text是文本,然后从0开始到文字结束
            float textWidth = textPaint.measureText(text, 0, text.length());
            canvas.drawText(text, (getWidth() - textWidth) / 2, y, textPaint);
    
    1. 接下来看y
      从注释里知道,y是文字的基线位置的y轴坐标,这个就要设计到FontMetrics了。
      FontMetrics是Paint的一个静态内部类


      FontMetrics.gif

      如上图所示,其中包含5个float值:

    • leading:留给文字音标的距离
    • ascent:从基线到文字最高字母的顶点,值为负数
    • top:从基线到字母最高点加上ascent
    • descent:从基线到字母最低点
    • bottom:从基线到字母最低点加上decent

    咱们要使用的字符都是常规字符,leading没啥用,top和bottom也是,都是为极少数字符预留的。
    另外,需要注意的是,基线位置为0。上面的值为负数就是ascent,下面的值为整数,就是descent。
    来看一张图:


    基线位置.png

    我们文字公式需要的是基线的y轴位置,也就是用(View的中心点 - 2和3这两条线之间距离),就是基线位置。
    |ascent| + descent = (descent + 2和3之间的距离) * 2
    转换一下就是
    (|ascent| - descent) / 2 = 2和3之间的距离。
    最后y轴的基线位置就可以确定了,就是
    getHeight() / 2 + (|ascent| - descent) / 2
    所以,最终我们的文字就可以写成这样:

            //在中心点绘制文字
            String text = currentProgress + "%";
            //获取文字的宽度,text是文本,然后从0开始到文字结束
            float textWidth = textPaint.measureText(text, 0, text.length());
            Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();
            canvas.drawText(text, (getWidth() - textWidth) / 2,
                    getHeight() / 2 + (Math.abs(fontMetrics.ascent) - fontMetrics.descent) / 2, textPaint);
    

    总结一下我们写到现在的代码
    CircleView.java:

    package com.terana.customview;
    
    import android.content.Context;
    import android.content.res.TypedArray;
    import android.graphics.Canvas;
    import android.graphics.Color;
    import android.graphics.Paint;
    import android.graphics.RectF;
    import android.support.annotation.Nullable;
    import android.util.AttributeSet;
    import android.view.View;
    
    public class CircleView extends View{
        //画圆的画笔
        private Paint circlePaint;
        //圆的半径
        private float radius;
        //圆的颜色
        private int circleColor;
        //圆的宽度
        private float strokeWidth;
        //动态圆的颜色
        private int progressColor;
        //动态圆的画笔
        private Paint progressPaint;
        //动态圆的当前进度值
        private int currentProgress;
        //动态圆的范围
        private RectF initRectF;
        //文字画笔
        private Paint textPaint;
        //为了密度转换,需要一个context
        private Context context;
    
        public CircleView(Context context) {
            this(context, null);
        }
    
        public CircleView(Context context, @Nullable AttributeSet attrs) {
            this(context, attrs, 0);
        }
    
        public CircleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            this(context, attrs, defStyleAttr, 0);
        }
    
        public CircleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
            super(context, attrs, defStyleAttr, defStyleRes);
            this.context = context;
            initAttrs(context, attrs);
            initVariables();
        }
    
        private void initAttrs(Context context, AttributeSet attrs) {
            TypedArray typedArray = context.obtainStyledAttributes(attrs,
                    R.styleable.CircleView, 0, 0);//获取TypedArray对象
            radius = typedArray.getDimension(R.styleable.CircleView_radius,
                    100);//获取半径,默认值为100
            strokeWidth = typedArray.getDimension(R.styleable.CircleView_strokeWidth,
                    2);//获取圆环的宽度,默认为2
            circleColor = typedArray.getColor(R.styleable.CircleView_circleColor,
                   Color.BLACK);//获取圆环的颜色,默认为红色
            progressColor = typedArray.getColor(R.styleable.CircleView_progressColor,
                    Color.RED);//获取圆环的颜色,默认为红色
            typedArray.recycle();//TypedArray对象是共享的资源,所以在获取完值之后必须要调用recycle()方法来回收。
        }
    
        private void initVariables() {
            //创建画圆的画笔
            circlePaint = new Paint();
            circlePaint.setAntiAlias(true);//画笔去除锯齿
            circlePaint.setColor(circleColor);//画笔颜色为红色
            circlePaint.setStyle(Paint.Style.STROKE);//画的圆是空心圆,FILL为实心圆
            circlePaint.setStrokeWidth(strokeWidth);//设置圆的线条宽度为2
    
            //创建动态圆的范围
            initRectF = new RectF();
    
            //创建动态圆的画笔
            progressPaint = new Paint();
            progressPaint.setAntiAlias(true);//画笔去除锯齿
            progressPaint.setColor(progressColor);//画笔颜色为红色
            progressPaint.setStyle(Paint.Style.STROKE);//画的圆是空心圆,FILL为实心圆
            progressPaint.setStrokeWidth(strokeWidth);//设置圆的线条宽度为2
    
            //初始化文字画笔
            textPaint = new Paint();
            textPaint.setAntiAlias(true);
            textPaint.setColor(circleColor);
            textPaint.setStyle(Paint.Style.STROKE);
            textPaint.setTextSize(DensityUtils.sp2px(context, 22));
        }
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            //getSuggestedMinimumWidth用于返回View推荐的最小宽度
            int width = getMeasuresize(getSuggestedMinimumWidth(), widthMeasureSpec);
            //getSuggestedMinimumHeight用于返回View推荐的最小高度
            int height = getMeasuresize(getSuggestedMinimumHeight(), heightMeasureSpec);
            setMeasuredDimension(width, height);//必须调用此方法,否则会抛出异常
        }
    
        private int getMeasuresize(int size, int measureSpec) {
            int result = size;
            //从MeasureSpec中获取测量模式
            int specMode = MeasureSpec.getMode(measureSpec);
            //从MeasureSpec中获取测量大小
            int specSize = MeasureSpec.getSize(measureSpec);
            switch (specMode){
                //父容器没有对当前View有任何限制,要多大就多大,这种情况一般用于系统内部,表示一种测量状态。
                case MeasureSpec.UNSPECIFIED:
                    result = size;//用推荐值即可
                    break;
                //父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize的值。
                //对应match_parent和具体的数值。
                case MeasureSpec.EXACTLY:
                    result = specSize;
                    break;
                //父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值。对应wrap_content。
                case MeasureSpec.AT_MOST:
                    result = Math.min(200, specSize);
                    break;
            }
            return result;
        }
    
        @Override
        protected void onSizeChanged(int w, int h, int oldw, int oldh) {
            super.onSizeChanged(w, h, oldw, oldh);
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            int minimum = Math.min(getWidth() / 2, getHeight() / 2);
            radius = radius <= minimum ? radius : minimum;
            //画圆
            canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius - strokeWidth / 2, circlePaint);
    
            //动态圆的总进度
           int totalProgress = 100;
            //获取动态圆的矩形区域
            initRectF.top = getWidth() / 2 - radius + strokeWidth / 2;
            initRectF.left = getHeight() / 2 - radius + strokeWidth / 2;
            initRectF.right = getWidth() / 2 + radius - strokeWidth / 2;
            initRectF.bottom = getHeight() / 2 + radius - strokeWidth / 2;
            //本质其实是画一个圆弧形的矩形
            canvas.drawArc(initRectF, -90,((float) currentProgress / totalProgress)
                    * 360 , false, progressPaint);
    
            //在中心点绘制文字
            String text = currentProgress + "%";
            //获取文字的宽度,text是文本,然后从0开始到文字结束
            float textWidth = textPaint.measureText(text, 0, text.length());
            Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();
            canvas.drawText(text, (getWidth() - textWidth) / 2,
                    getHeight() / 2 + (Math.abs(fontMetrics.ascent) - fontMetrics.descent) / 2, textPaint);
        }
    
        public void setRadius(float mRadius) {
            this.radius = mRadius;
            invalidate();//重绘
        }
    
        public void setCircleColor(int mCircleColor) {
            circlePaint.setColor(mCircleColor);
            invalidate();//重绘
        }
    
        public void setStrokeWidth(float mStrokeWidth) {
            circlePaint.setStrokeWidth(mStrokeWidth);
            invalidate();//重绘
        }
    
        public void updateProgress(int mCurrentProgress) {
            this.currentProgress = mCurrentProgress;
            postInvalidate();
        }
    }
    

    MainActivity.java:

    package com.terana.customview;
    
    import android.os.Bundle;
    import android.support.v7.app.AppCompatActivity;
    
    public class MainActivity extends AppCompatActivity {
        private CircleView circleView;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            initViews();
        }
    
        @Override
        protected void onResume() {
            super.onResume();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    int temp = 0;
                    while(temp <=60){
                        circleView.updateProgress(temp++);
                        try {
                            Thread.sleep(50);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }).start();
        }
    
        private void initViews() {
            circleView = findViewById(R.id.circleView);
        }
    }
    

    DensityUtils.java:

    package com.terana.customview;
    
    import android.content.Context;
    import android.util.TypedValue;
    
    public class DensityUtils
    {
        private DensityUtils()
        {
            /* cannot be instantiated */
            throw new UnsupportedOperationException("cannot be instantiated");
        }
    
        public static int dp2px(Context context, float dpVal)
        {
            return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
                    dpVal, context.getResources().getDisplayMetrics());
        }
    
        public static int sp2px(Context context, float spVal)
        {
            return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
                    spVal, context.getResources().getDisplayMetrics());
        }
    
        public static float px2dp(Context context, float pxVal)
        {
            final float scale = context.getResources().getDisplayMetrics().density;
            return (pxVal / scale);
        }
    
        public static float px2sp(Context context, float pxVal)
        {
            return (pxVal / context.getResources().getDisplayMetrics().scaledDensity);
        }
    
    }
    

    activity_main.xml:

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context="com.terana.customview.MainActivity">
    
        <com.terana.customview.CircleView
            android:id="@+id/circleView"
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:layout_alignParentTop="true"
            android:layout_centerHorizontal="true"
            app:circleColor="#696969"
            app:progressColor="#1E88E5"
            app:radius="44dp"
            app:strokeWidth="6dp" />
    
    </RelativeLayout>
    

    attrs.xml:

    <?xml version="1.0" encoding="utf-8"?>
    <resources>
        <declare-styleable name="CircleView">
            <attr name="circleColor" format="color"/>
            <attr name="radius" format="dimension"/>
            <attr name="strokeWidth" format="dimension"/>
            <attr name="progressColor" format="color"/>
        </declare-styleable>
    </resources>
    

    最终的效果为:


    添加文字.gif

    绘制的部分差不多了,有兴趣的话可以再自行扩充,下一节会说说自定义View的点击事件。

    相关文章

      网友评论

          本文标题:自定义View入门(六) - 绘制文字

          本文链接:https://www.haomeiwen.com/subject/ldnzpxtx.html