(二十一)学习自定义控件

前言:最近有的招聘要求有要熟悉view的绘制原理,我也一直想看看自定义控件是怎么弄的,好久之前学的都忘了,现在看来其实两者是强相关的,不熟悉绘制原理,自定义控件也是徒劳。


参考博客:点击打开链接


demo地址:点击打开链接


1. hello world

学习什么新东西hello world是必不可少的,参考上面的博客先写个hello world的自定义View玩玩。

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 {
    /**
     * 需要绘制的文字
     */
    private String mText;
    /**
     * 文本的颜色
     */
    private int mTextColor;
    /**
     * 文本的大小
     */
    private int mTextSize;
    /**
     * 绘制时控制文本绘制的范围
     */
    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);
        //获得绘制文本的宽和高
        mBound = new Rect();
        mPaint.getTextBounds(mText, 0, mText.length(), mBound);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        //绘制文字
        canvas.drawText(mText, getWidth() / 2 - mBound.width() / 2, getHeight() / 2 + mBound.height() / 2, mPaint);
    }
}

这个onDraw方法就是将文字显示在组件的中心位置。

布局文件:

<?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. 自定义属性

其实上面view的构造方法有很多是写死的,没有从配置文件中配置,这是就需要自定义属性登场了。

2.1 配置文件

在res/values文件夹下新建attrs.xml,如下所示:

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 布局文件引用

添加自定义的的命名空间(jiatai:xmlns:openxu="http://schemas.android.com/apk/res-auto"),并用自己的命名空间设置自己需要的属性,试了下名称无所谓,属性名称对的上就行。

<?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 修改构造方法

将属性写死改为从布局文件中读取

public HelloWorldTextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        /*mText = "hello world";
        mTextColor = Color.BLACK;
        mTextSize = 100;*/

        //获取自定义属性的值
        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();  //注意回收

        mPaint = new Paint();
        mPaint.setTextSize(mTextSize);
        mPaint.setColor(mTextColor);
        //获得绘制文本的宽和高
        mBound = new Rect();
        mPaint.getTextBounds(mText, 0, mText.length(), mBound);
    }


2.4 效果

可以看出字体颜色和字符串都得到了相应的改变


3. 重写onMeasure方法

参考博客中提及之前的自定义控件虽然声明是wrap_content的但实际效果是match_parent,修改方法就是重写onMeasure。

3.1 对应代码

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);   //获取宽的模式
        int heightMode = MeasureSpec.getMode(heightMeasureSpec); //获取高的模式
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);   //获取宽的尺寸
        int heightSize = MeasureSpec.getSize(heightMeasureSpec); //获取高的尺寸
        Log.v(TAG, "宽的模式:"+widthMode);
        Log.v(TAG, "高的模式:"+heightMode);
        Log.v(TAG, "宽的尺寸:"+widthSize);
        Log.v(TAG, "高的尺寸:"+heightSize);
        int width;
        int height ;
        if (widthMode == MeasureSpec.EXACTLY) {
            //如果match_parent或者具体的值,直接赋值
            width = widthSize;
        } else {
            //如果是wrap_content,我们要得到控件需要多大的尺寸
            float textWidth = mBound.width();   //文本的宽度
            //控件的宽度就是文本的宽度加上两边的内边距。内边距就是padding值,在构造方法执行完就被赋值
            width = (int) (getPaddingLeft() + textWidth + getPaddingRight());
            Log.v(TAG, "文本的宽度:"+textWidth + "控件的宽度:"+width);
        }
        //高度跟宽度处理方式一样
        if (heightMode == MeasureSpec.EXACTLY) {
            height = heightSize;
        } else {
            float textHeight = mBound.height();
            height = (int) (getPaddingTop() + textHeight + getPaddingBottom());
            Log.v(TAG, "文本的高度:"+textHeight + "控件的高度:"+height);
        }
        //保存测量宽度和测量高度
        setMeasuredDimension(width, height);
    }

3.2  对应log


3.3 效果图


4. 实现自动换行

研究了下参考博客的代码,稍微重构并修改了下我发现有误的地方,自动换行其实还是由onMeasure算出控件的宽和高,由onDraw画出所有字符。


4.1 过长字符串分行

 private void initTextToLines(int widthSize){
        float textWidth = mBound.width();   //文本的宽度
        if(mTextList.size()==0){
            //将文本分段
            int padding = getPaddingLeft() + getPaddingRight();
            int specWidth = widthSize - padding; //能够显示文本的最大宽度
            if(textWidth<specWidth){
                //说明一行足矣显示
                lineNum = 1;
                mTextList.add(mText);
            }else{
                //超过一行
                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, "文本:"+mText);
                Log.v(TAG, "文本总长度:"+mText.length());
                Log.v(TAG, "能绘制文本的宽度:"+lineLength);
                Log.v(TAG, "需要绘制:"+lineNum+"行");
                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 根据行数和字符串宽度设置组件高和宽

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);   //获取宽的模式
        int heightMode = MeasureSpec.getMode(heightMeasureSpec); //获取高的模式
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);   //获取宽的尺寸
        int heightSize = MeasureSpec.getSize(heightMeasureSpec); //获取高的尺寸
        Log.v(TAG, "宽的模式:"+widthMode);
        Log.v(TAG, "高的模式:"+heightMode);
        Log.v(TAG, "宽的尺寸:"+widthSize);
        Log.v(TAG, "高的尺寸:"+heightSize);

        initTextToLines(widthSize);

        int width;
        int height ;
        if (widthMode == MeasureSpec.EXACTLY) {
            //如果match_parent或者具体的值,直接赋值
            width = widthSize;
        } else {
            //如果是wrap_content,我们要得到控件需要多大的尺寸
            float textWidth = mBound.width();
            if(isOneLines){
                //控件的宽度就是文本的宽度加上两边的内边距。内边距就是padding值,在构造方法执行完就被赋值
                width = (int) (getPaddingLeft() + textWidth + getPaddingRight());
            }else{
                //如果是多行,说明控件宽度应该填充父窗体
                width = widthSize;
            }
            Log.v(TAG, "文本的宽度:"+textWidth + "控件的宽度:"+width);
        }
        //高度跟宽度处理方式一样
        if (heightMode == MeasureSpec.EXACTLY) {
            height = heightSize;
        } else {
            float textHeight = mBound.height();
            if(isOneLines){
                height = (int) (getPaddingTop() + textHeight + getPaddingBottom());
            }else{
                //如果是多行
                height = (int) (getPaddingTop() + textHeight*lineNum + getPaddingBottom());;
            }

            Log.v(TAG, "文本的高度:"+textHeight + "控件的高度:"+height);
        }
        //保存测量宽度和测量高度
        setMeasuredDimension(width, height);
    }


4.3 绘制多行字符串

@Override
    protected void onDraw(Canvas canvas) {
        //绘制文字
        //canvas.drawText(mText, getWidth() / 2 - mBound.width() / 2, getHeight() / 2 + mBound.height() / 2, mPaint);
        //绘制文字
        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 效果


4.5 改为靠左对齐

改一下onDraw画字符串的起始位置就好了,不要中间,要边界,即getPaddingLeft()

@Override
    protected void onDraw(Canvas canvas) {
        //绘制文字
        //canvas.drawText(mText, getWidth() / 2 - mBound.width() / 2, getHeight() / 2 + mBound.height() / 2, mPaint);
        //绘制文字
        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);
        }
    }
效果:


4.6 改为靠右对齐

将x改成靠左对齐时离右边的距离即可,即改一下onDraw方法x的设定,改为

(getWidth() - mBound.width() - getPaddingLeft())

效果:


emmm,还是能玩得起来的嘛



5. 总结

今天主要就是学习下自定义控件大致怎么做,不想深入到源码层面,简单看来就是onMeasure算出控件高和宽,onDraw进行控件具体内容的绘制。这其实都是就控件本身讨论的,没有将控件放置于布局中进行讨论,控件位于何地如何确定也未提及,待续。。。

猜你喜欢

转载自blog.csdn.net/sinat_20059415/article/details/79776177