自定义View实现TextView

上篇导航:自定义View三大流程理论篇

自定义View实现TextView

TextView是我们常用的View之一,下面我们来自定义一个TextView,实现基本的TextView功能。以下不涉及基本的理论概念,概念篇参见上一篇文章。

准备工作

这里我们初始化了一些要用到的数据,其中有字体的颜色和大小以及文字内容的画笔。

public class MyTextView extends View {
    private float textSize;//文字大小
    private String text = "Hello!";//文字内容
    private int textColor;//字体颜色
    private Paint textPaint;//字体画笔
    private int gravity = Gravity.RIGHT;//文字显示位置

    public MyTextView(Context context) {
        super(context);
        init();
    }

    public MyTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        this.textSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 15, getContext().getResources().getDisplayMetrics());//初始化文字大小
        this.textColor = Color.BLACK;
        this.textPaint = new Paint();
        this.textPaint.setColor(this.textColor);
    }
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
    }
}

重写onMeasure

这里我们一步步来看下面的代码:
首先是通过MeasureSpec获取特定的尺寸信息。
第二步就是要计算出内容所占的大小,也就是文本所占的大小,因为我们的模式可能是AT_MOST和UNSPECIFIED。
第三步是要根据模式来确定当前View的尺寸,我们在上一篇的内容了解到了AT_MOST对应的是wrap_content所以我们就将尺寸改为内容的尺寸,UNSPECIFIED是在当前View作为ScrollView子控件时的模式,我们也要设置当前View为内容所占的尺寸。
第四部也是最后一步,就是设置当前View的尺寸。

/**
 * 获取Text占用的大小
 * @return
 */
private Rect measureTextSize() {
    Paint paint = new Paint();
    Rect textSize = new Rect();
    paint.setTextSize(this.textSize);//注意别忘了设置文字的大小
    paint.getTextBounds(this.text, 0, this.text.length(), textSize);
    return textSize;
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    //【1】获取尺寸和模式
    int wMode = MeasureSpec.getMode(widthMeasureSpec);
    int w = MeasureSpec.getSize(widthMeasureSpec);
    int hMode = MeasureSpec.getMode(heightMeasureSpec);
    int h = MeasureSpec.getSize(heightMeasureSpec);
    //【2】计算内容尺寸(这里的内容指的是文字的大小)
    int contentWidth = 0;
    int contentHeight = 0;
    if (text != null && text.length() > 0) {
        //测量并计算Text的大小
        Rect textSize = measureTextSize();
        contentWidth = textSize.width();
        contentHeight = textSize.height();
    }
    //【3】根据模式设置最终尺寸(这里要处理AT_MOST和UNSPECIFIED)
    if (wMode == MeasureSpec.AT_MOST || wMode == MeasureSpec.UNSPECIFIED) {
        w = contentWidth;
    }
    if (hMode == MeasureSpec.AT_MOST || hMode == MeasureSpec.UNSPECIFIED) {
        h = contentHeight;
    }
    //【4】设置最终的尺寸
    setMeasuredDimension(w, h);
}

onLayout

这里我们可以不重写onLayout函数,因为我们并没有子view需要来布局。

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    super.onLayout(changed, left, top, right, bottom);
}

onDraw

我们这里使用drawText将文本绘制到View中,实现了三种Gravity,具体计算这里不再详述。

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    if (this.text == null || this.text.isEmpty()) {
        return;
    }
    //计算坐标
    Rect rect = measureTextSize();
    int textWidth = rect.width();
    int textHeight = rect.height();
    int x = 0;
    int y = 0;
    if (gravity == Gravity.CENTER) {
        x = getMeasuredWidth() / 2 - textWidth / 2;
        y = (int) (getMeasuredHeight() / 2F + textHeight / 2F);
    } else if (gravity == Gravity.LEFT) {
        y = textHeight;
    } else if (gravity == Gravity.RIGHT) {
        x = getMeasuredWidth() - textWidth;
        y = textHeight;
    }
    //绘制文字
    Paint paint = new Paint();
    paint.setTextSize(this.textSize);
    int descent = paint.getFontMetricsInt().descent;
    canvas.drawText(this.text, x, y - descent, this.textPaint);
}

通过xml设置属性

在上边我们的属性是定死的,我们没办法直接通过xml来设置,下面我们就来说一下如何通过xml来设置自定义的属性。

Step1:

在values目录下新建一个Values Resource File文件,名字可以随便起。右击values–>New–>Values Resource File。

自定义View实现TextView_第1张图片

Step2:

在刚才建立好的xml文件中写需要的属性。建立好xml后默认是有一个根节点resources,我们在根节点下建立一个declare-styleable并添加一个属性为name,这个name就是我们View的名字。然后再这个节点下添加我们的attr节点,也就是属性节点,需要名称和类型,常用类型有dimension、int、string、color、enum,使用enum需要在其节点下声明枚举值。


<resources>
    <declare-styleable name="MyTextView">
        <attr name="textSize" format="dimension">attr>
        <attr name="text" format="string">attr>
        <attr name="gravity" format="enum">
            <enum name="CENTER" value="1">enum>
            <enum name="LEFT" value="2">enum>
            <enum name="RIGHT" value="3">enum>
        attr>
        <attr name="textColor" format="color">attr>
    declare-styleable>
resources>

Step3:

第三部就是在构造函数中获取定义的属性值了。

这里我们将原来init函数中的添加默认值的代码给注释掉了(因为我们要从xml属性中获取),还添加了三个内部的方法,从下看第一个就是将sp单位转换px单位的方法,第二个是根据我们定义的枚举值转换成Gravity对象对应的值,getAttrs就是获取我们在xml中定义的属性值了。这个getAttrs函数我们在带有AttributeSet的构造函数中进行调用。这个AttributeSet里边就包含了所有我们在xml中设置的属性。

public MyTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        getAttrs(attrs);
        init();
    }

    private void getAttrs(AttributeSet attributeSet) {
        TypedArray typedArray = getContext().obtainStyledAttributes(attributeSet, R.styleable.MyTextView);//第二个参数是xml中定义的名称
        //以下使用的R.styleable.MyTextView_xxxx是自动生成的,是declare-styleable的name+下划线+attr的name
        this.textSize = typedArray.getDimensionPixelSize(R.styleable.MyTextView_textSize, sp2px(12));//解析dimension类型
        this.text = typedArray.getString(R.styleable.MyTextView_text);//解析String类型
        this.textColor = typedArray.getColor(R.styleable.MyTextView_textColor, Color.BLACK);//解析Color类型
        this.gravity = parseGravity(typedArray.getInt(R.styleable.MyTextView_gravity, 1));//解析枚举类型
        typedArray.recycle();
    }

    /**
     * 将枚举值解析为Gravity
     *
     * @param val
     * @return
     */
    private int parseGravity(int val) {
        if (val == 1) {
            return Gravity.CENTER;
        } else if (val == 2) {
            return Gravity.LEFT;
        }
        return Gravity.RIGHT;
    }

    /**
     * 将sp单位转换为像素单位
     *
     * @param spValue
     * @return
     */
    private int sp2px(int spValue) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, spValue, getContext().getResources().getDisplayMetrics());
    }

    private void init() {
//        this.textSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 12, getContext().getResources().getDisplayMetrics());//初始化文字大小
//        this.textColor = Color.BLACK;
        this.textPaint = new Paint();
        this.textPaint.setColor(this.textColor);
        this.textPaint.setTextSize(this.textSize);
    }

Step4:

在xml中使用。

<com.example.customview.MyTextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="#FF00"
    app:text="blog.lost520.cn"
    app:textColor="#FFFF"
    app:textSize="30sp" />

效果图:

自定义View实现TextView_第2张图片

完整代码

public class MyTextView extends View {
    private float textSize;//文字大小
    private String text = "Hello";//文字内容
    private int textColor;//字体颜色
    private Paint textPaint;//字体画笔
    private int gravity = Gravity.CENTER;//文字显示位置

    public MyTextView(Context context) {
        super(context);
        init();
    }

    public MyTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        getAttrs(attrs);
        init();
    }

    private void getAttrs(AttributeSet attributeSet) {
        TypedArray typedArray = getContext().obtainStyledAttributes(attributeSet, R.styleable.MyTextView);//第二个参数是xml中定义的名称
        //以下使用的R.styleable.MyTextView_xxxx是自动生成的,是declare-styleable的name+下划线+attr的name
        this.textSize = typedArray.getDimensionPixelSize(R.styleable.MyTextView_textSize, sp2px(12));//解析dimension类型
        this.text = typedArray.getString(R.styleable.MyTextView_text);//解析String类型
        this.textColor = typedArray.getColor(R.styleable.MyTextView_textColor, Color.BLACK);//解析Color类型
        this.gravity = parseGravity(typedArray.getInt(R.styleable.MyTextView_gravity, 1));//解析枚举类型
        typedArray.recycle();
    }

    /**
     * 将枚举值解析为Gravity
     *
     * @param val
     * @return
     */
    private int parseGravity(int val) {
        if (val == 1) {
            return Gravity.CENTER;
        } else if (val == 2) {
            return Gravity.LEFT;
        }
        return Gravity.RIGHT;
    }

    /**
     * 将sp单位转换为像素单位
     *
     * @param spValue
     * @return
     */
    private int sp2px(int spValue) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, spValue, getContext().getResources().getDisplayMetrics());
    }

    private void init() {
//        this.textSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 12, getContext().getResources().getDisplayMetrics());//初始化文字大小
//        this.textColor = Color.BLACK;
        this.textPaint = new Paint();
        this.textPaint.setColor(this.textColor);
        this.textPaint.setTextSize(this.textSize);
    }

    /**
     * 获取Text占用的大小
     *
     * @return
     */
    private Rect measureTextSize() {
        Paint paint = new Paint();
        Rect textSize = new Rect();
        paint.setTextSize(this.textSize);
        paint.getTextBounds(this.text, 0, this.text.length(), textSize);
        return textSize;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //【1】获取尺寸和模式
        int wMode = MeasureSpec.getMode(widthMeasureSpec);
        int w = MeasureSpec.getSize(widthMeasureSpec);
        int hMode = MeasureSpec.getMode(heightMeasureSpec);
        int h = MeasureSpec.getSize(heightMeasureSpec);
        //【2】计算内容尺寸(这里的内容指的是文字的大小)
        int contentWidth = 0;
        int contentHeight = 0;
        if (text != null && text.length() > 0) {
            //测量并计算Text的大小
            Rect textSize = measureTextSize();
            contentWidth = textSize.width();
            contentHeight = textSize.height();
        }
        //【3】根据模式设置最终尺寸(这里要处理AT_MOST和UNSPECIFIED)
        if (wMode == MeasureSpec.AT_MOST || wMode == MeasureSpec.UNSPECIFIED) {
            w = contentWidth;
        }
        if (hMode == MeasureSpec.AT_MOST || hMode == MeasureSpec.UNSPECIFIED) {
            h = contentHeight;
        }
        //【4】设置最终的尺寸
        setMeasuredDimension(w, h);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (this.text == null || this.text.isEmpty()) {
            return;
        }
        //计算坐标
        Rect rect = measureTextSize();
        int textWidth = rect.width();
        int textHeight = rect.height();
        int x = 0;
        int y = 0;
        if (gravity == Gravity.CENTER) {
            x = getMeasuredWidth() / 2 - textWidth / 2;
            y = (int) (getMeasuredHeight() / 2F + textHeight / 2F);
        } else if (gravity == Gravity.LEFT) {
            y = textHeight;
        } else if (gravity == Gravity.RIGHT) {
            x = getMeasuredWidth() - textWidth;
            y = textHeight;
        }
        //绘制文字
        Paint paint = new Paint();
        paint.setTextSize(this.textSize);
        int descent = paint.getFontMetricsInt().descent;
        canvas.drawText(this.text, x, y - descent, this.textPaint);
    }
}

最后

以上的属性只能通过xml来定义,如果想要通过代码来设置,直接为变量添加get和set方法即可。

下一章导航:自定义View实现LinearLayout

你可能感兴趣的:(Android)