Android 自定义View基础(二)

接着上一篇《Android 自定义View基础(一)》,这一篇我们真正的来实现一个文字自定义控件:

自定义控件的步骤:

自定义属性

在自定义View的构造函数获取属性

onMesure()

onDraw()

1.自定义属性

在res/values/下创建一个attrs.xml文件,然后在创建需要的属性
代码如下:


<resources>
    <attr name="titleText" format="string">attr>
    <attr name="titleTextColor" format="color">attr>
    <attr name="titleTextSize" format="dimension">attr>

    <declare-styleable name="CustomView01" >
        <attr name="titleText"/>
        <attr name="titleTextColor"/>
        <attr name="titleTextSize"/>
    declare-styleable>
resources>

上面的属性中,我们创建了文字,文字颜色,文字大小三个属性

 <declare-styleable name="CustomView01" >

名字也可以是你随便起,但是建议一般与你创建的自定义类名相同。方便查看和管理。

2.创建CustomView01

先创建一个CustomView01继承自View

public class CustomView01 extends View {
    public CustomView01(Context context) {
        super(context);
    }
    public CustomView01(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
}

上面的两个构造方法中,一个是我们动态创建控件时使用,一个是从xml布局文件中加载时调用。

3.将创建的控件写入到布局中

在activity_main.xml中加入我们创建的属性和控件


<RelativeLayout  xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:custom="http://schemas.android.com/apk/res-auto"
    android:id="@+id/main_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.aofei.myview.MainActivity">
   <com.aofei.myview.CustomView01
       android:layout_width="200dp"
       android:layout_height="100dp"
       custom:titleText="这是测试文字"
       custom:titleTextColor="#ff0000"
       custom:titleTextSize="40sp"
       />
RelativeLayout>

此行代码就是将我们创建的自定义属性导入进当前布局,该代码不必记忆,只要输入app然后就会出现一个appNs的提示,确认后就是

 xmlns:app="http://schemas.android.com/apk/res-auto"

然后可以更改名字,为custom,我们的属性就是以custom 开头,

 xmlns:custom="http://schemas.android.com/apk/res-auto"

到这里我们运行,后发现,界面上并没有我们的创建的控件,为什么呢?这是因为我们的自定Custom中,并没有饮用我们的属性和调用onDraw()进行绘画。

然后我们实现我们调用属性和绘画


public class CustomView01 extends View {

    private String mTitleText;//文本
    private int mTitleTextColor;//文本的颜色
    private int mTitleTextSize;//文本的大小


    private Rect mBound;//绘制时控制文本绘制的范围
    private Paint mPaint;  //绘制文本时的画笔


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

    public CustomView01(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    /**
     * 获取我们的自定义样式
     *
     * @param context
     * @param attrs
     * @param defStyleAttr
     */
    public CustomView01(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        //获取我们所定义的自定义样式属性
        TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CustomView01, defStyleAttr, 0);
        int n = a.getIndexCount();//获取多少个属性
        //遍历所有属性
        for (int i = 0; i < n; i++) {
            int attr = a.getIndex(i);
            //根据从xml对应的值对属性赋值
            switch (attr) {
                case R.styleable.CustomView01_titleText:
                    mTitleText = a.getString(attr);
                    break;
                case R.styleable.CustomView01_titleTextColor:
                    mTitleTextColor = a.getColor(attr, Color.BLACK);//默认颜色为黑色
                    break;
                case R.styleable.CustomView01_titleTextSize:
                    //給文字尺寸赋值,默认设置为16sp,TypeValue也可以把sp转化为px
                    mTitleTextSize = a.getDimensionPixelSize(attr,
                            (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 16,
                                    getResources().getDisplayMetrics()));
                    break;

            }
        }

        a.recycle();//释放资源,这个一定不要忘记!

        //获取绘制文本的宽和高
        mPaint=new Paint();
        mPaint.setTextSize(mTitleTextSize);
        mBound= new Rect();
        mPaint.getTextBounds(mTitleText,0,mTitleText.length(),mBound);
    }
}

我们重写了3个构造方法,默认的布局文件调用的是两个参数的构造方法,所以记得让所有的构造调用我们的三个参数的构造,我们在三个参数的构造中获得自定义属性。

4.我们重写onDraw,onMesure

   @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onDraw(Canvas canvas) {
//        super.onDraw(canvas);   //我们要重写,不必调用继承的View的方法
        mPaint.setColor(Color.YELLOW);
     //背景矩形
     canvas.drawRect(0,0,getMeasuredWidth(),getMeasuredHeight(),mPaint);

        mPaint.setColor(mTitleTextColor);
//画文字
        canvas.drawText(mTitleText,getWidth()/2-mBound.width()/2,getHeight()/2+mBound.height()/2,mPaint);
    }

运行后的结果如下:

Android 自定义View基础(二)_第1张图片

到这里,我们就实现一个类似于TextView的控件
我们在onDraw()中打印一下几个测量的值

  @Override
    protected void onDraw(Canvas canvas) {
//        super.onDraw(canvas);   //我们要重写,不必调用继承的View的方法

        mPaint.setColor(Color.YELLOW);
        canvas.drawRect(0,0,getMeasuredWidth(),getMeasuredHeight(),mPaint);


        mPaint.setColor(mTitleTextColor);
        mPaint.setAntiAlias(true);//抗锯齿
        canvas.drawText(mTitleText,getWidth()/2-mBound.width()/2,getHeight()/2+mBound.height()/2,mPaint);
        Log.e(TAG,"getMeasuredWidth()="+getMeasuredWidth()+", getMeasuredHeight()="+getMeasuredHeight());
        Log.e(TAG,"getWidth()="+getWidth()+" ,getHeight="+getHeight());
        Log.e(TAG,"mBound.width()="+mBound.width()+" ,mBound.height()="+mBound.height());
    }

打印结果如下
这里写图片描述

03-09 14:37:47.172 22448-22448/com.aofei.myview E/CustomView01: getMeasuredWidth()=600, getMeasuredHeight()=300
03-09 14:37:47.172 22448-22448/com.aofei.myview E/CustomView01: getWidth()=600 ,getHeight=300
03-09 14:37:47.172 22448-22448/com.aofei.myview E/CustomView01: mBound.width()=354 ,mBound.height()=58

这个就是我们控件的大小。

如果我们将我们的布局文件中的属性更改为wrap_content,

<com.aofei.myview.CustomView01
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       custom:titleText="这是测试文字"
       custom:titleTextColor="#ff0000"
       custom:titleTextSize="20sp"
       />

运行后的结果如下:

Android 自定义View基础(二)_第2张图片
为什么会出现这样的结果呢,看下我们打印的测量尺寸。

03-09 14:44:32.548 22448-22448/com.aofei.myview E/CustomView01: getMeasuredWidth()=1080, getMeasuredHeight()=1536
03-09 14:44:32.548 22448-22448/com.aofei.myview E/CustomView01: getWidth()=1080 ,getHeight=1536
03-09 14:44:32.548 22448-22448/com.aofei.myview E/CustomView01: mBound.width()=354 ,mBound.height()=58

此时的宽高发生了变化,这是系统帮我们测量的尺寸,都是match_parent,是手机的屏幕尺寸了,getWidth()=1080 ,getHeight=1536
getMeasuredWidth()=1080, getMeasuredHeight()=1536,显然,这不是我们想要的效果。所以当我们设置了wrap_content时,我们需要自己进行测量,即重写onMeasure.

MeasureSpec的SpecMode,一共三种类型:

EXACTLY:一般是设置了明确的值或者是MATCH_PARENT
AT_MOST:表示子布局限制在一个最大值内,一般为wrap_content
UNSPECIFIED:表示子布局想要多大就多大,很少使用
下面是我们重写onMeasure代码:

   @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        //重写onMeasure
        int widthMode=MeasureSpec.getMode(widthMeasureSpec);
        int widthSize=MeasureSpec.getSize(widthMeasureSpec);
        int heightMode=MeasureSpec.getMode(heightMeasureSpec);
        int heightSize=MeasureSpec.getSize(heightMeasureSpec);
        int width;
        int height;

        if (widthMode==MeasureSpec.EXACTLY){
            width=widthSize;
        }else {
            //如果不是给出精确的值,我们通过计算文字的长度和距离控件左右的值来确定,我们画多大
            mPaint.setTextSize(mTitleTextSize);
        mPaint.getTextBounds(mTitleText,0,mTitleText.length(),mBound);
            float textWidth=mBound.width();
            int desired = (int) (getPaddingLeft()+textWidth+getPaddingRight());
            width=desired;
        }

        if (heightMode==MeasureSpec.EXACTLY){
            height=heightSize;
        }else {
            mPaint.setTextSize(mTitleTextSize);
            mPaint.getTextBounds(mTitleText,0,mTitleText.length(),mBound);
            float textHeight=mBound.height();
            int desired = (int) (getPaddingTop()+textHeight+getPaddingBottom());
            height=desired;
        }
        setMeasuredDimension(width,height);//设置计算后的宽高
    }

我们修改一下布局文件,如下


<RelativeLayout  xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:custom="http://schemas.android.com/apk/res-auto"
    android:id="@+id/main_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.aofei.myview.MainActivity">
   <com.aofei.myview.CustomView01
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       custom:titleText="3388"
       custom:titleTextColor="#ff0000"
       custom:titleTextSize="20sp"
       android:padding="10dp"
       />
RelativeLayout>

运行后的结果是:

Android 自定义View基础(二)_第3张图片
这样的效果,就是我们想要的,我们可以对高度,宽度随便设置,基本可以满足我们的需求。

接下来我们稍微扩展一下,給这个View添加一个点击事件。在onDraw()最后面添加一个点击事件。

        //再画完后我们給这个控件添加一个点击事件
        this.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                mTitleText=randomText();
                postInvalidate();//重写绘制
            }
        });
 /**
     * 获取一个随机的四个数字的字符串
     * @return
     */
  private  String randomText(){
      Random random = new Random();
      Set set=new HashSet<>();

      while (set.size()<4){
          int randomInt=random.nextInt(10);
          set.add(randomInt);
      }
      StringBuffer sb=new StringBuffer();
      for (Integer i:set) {
          sb.append(""+i);
      }
      return sb.toString();
  }

运行效果如下:
Android 自定义View基础(二)_第4张图片
,这是不是很像我们登陆一些网站的时候验证码的更换。写到这里,就简单实现了一个类似于TextView的自定义控件,不管看博客还是视频,都需要自己动手,永远不要认为自己看懂了,不用写了。

你可能感兴趣的:(Android)