自定义View基础

Android中的View分为普通View和ViewGroup两种。继承结构如图


image.png

因此自定义View分为 自定义普通View和自定义 ViewGroup两种,由于普通View内部不能放其他View,因此自定义ViewGroup与自定义普通View的区别就在于其要处理子View的放置问题。

1 自定义普通View

自定义普通View只需要继承View,并且重写两个构造函数即可

public class MyView extends View {

    public MyView(Context context) {
        super(context);
    }

    public MyView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }
}

除此之外自定义View还需要重写两个重要方法,onMeasure()与onDraw()方法。

1.1 onMeasure()

onMeasure()方法主要是用来设置控件的宽和高。父View在摆放子View时候会分别调用每个子View的onMeasure()方法来确定它们需要多大的空间。
onMeasure()方法的原型是

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) 

参数是width(宽)和height(高)的MeasureSpec(测量细节),MeasureSpec包含两部分,一部分是宽和高的具体数值,另一部分是测量模式。类型是int,也就是4个字节,32个二进制位。这32个二进制位中有2位用来记录测量模式,其他位用来存储具体的测量尺寸。
可以通过MeasureSpec工具来将这两部分数值分离出来,如下:

int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);

测量模式分为三种:

   /**
         * Measure specification mode: The parent has not imposed any constraint
         * on the child. It can be whatever size it wants.
         */
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;

        /**
         * Measure specification mode: The parent has determined an exact size
         * for the child. The child is going to be given those bounds regardless
         * of how big it wants to be.
         */
        public static final int EXACTLY     = 1 << MODE_SHIFT;

        /**
         * Measure specification mode: The child can be as large as it wants up
         * to the specified size.
         */
        public static final int AT_MOST     = 2 << MODE_SHIFT;

其中MODE_SHIFT=30,也就是左移30位,将测量模式数值放在int类型的最高两位。
UNSPECIFIED(取值0)表示,父View对子View没有限制,子View可以任意取值。
EXACTLY(取值1)表示父View已经确定了具体的尺寸,子View可以根据这个尺寸来设置自己的宽和高。
AT_MOST(取值2)表示父View最大就能提供这么大空间了,子View自己看着办吧,别超过限制的width和height就好。

因此简单的重写onMeasure方法如下。

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width=   MeasureSpec.getSize(widthMeasureSpec);
        int height=  MeasureSpec.getSize(heightMeasureSpec);

        //这里无论测量模式取什么,都设置宽和高为父View提供的一半
        setMeasuredDimension(width/2,height/2);
    }

在xml中声明一个自定义的控件

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    android:orientation="vertical"
    tools:context=".MainActivity">

  <com.example.zhouhong1.tabtest.view.MyView
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      android:background="#ff2266ff"
     />
  
</LinearLayout>

设置width和height都是 match_parent,但是由于重写了onMeasure()方法, 出来的效果是这样:


image.png

1.2 onDraw()

onDraw方法就是去绘制自定义的View,在View中这是个空方法。

     /**
     * Implement this to do your drawing.
     *
     * @param canvas the canvas on which the background will be drawn
     */
    protected void onDraw(Canvas canvas) {
    }

因此在自定义的子View中就可以根据参数中提供的canvas画布去绘制View的样子,或者添加文字。

View中的坐标系是相对于父控件而言,如下图所示:


image.png

因此View的触摸事件MotionEvent中 get 和 getRaw 的区别就在于一个是相对的值,一个是绝对的值。

event.getX();       //触摸点相对于其所在组件坐标系的坐标
event.getY();

event.getRawX();    //触摸点相对于屏幕默认坐标系的坐标
event.getRawY();

在控件的中心绘制一个以宽/2为半径的圆。

    @Override
    protected void onDraw(Canvas canvas) {
        int wid=getMeasuredWidth();
        int height=getMeasuredHeight();
        paint.setColor(Color.RED);
        canvas.drawCircle(wid/2,height/2,wid/2,paint);

    }

效果图是


image.png

画完圆后再写点字

    @Override
    protected void onDraw(Canvas canvas) {
        int wid=getMeasuredWidth();
        int height=getMeasuredHeight();
        paint.setColor(Color.RED);
        canvas.drawCircle(wid/2,height/2,wid/2,paint);

        //添加文字
        String s="我是一个圆";
        //设置画笔颜色,大小
        paint.setColor(Color.WHITE);
        paint.setTextSize(50);
        //宽居中
        float textWidth=  paint.measureText(s);
        float x=wid/2-textWidth/2;

        float y=height/2;
        canvas.drawText(s,x,y,paint);
    }
image.png

2 自定义ViewGroup

ViewGroup是View的子类,因此它也具有onMeasure() ,onDraw()等方法,不过ViewGroup与普通View的区别在于它更多的是作为一种容器来安放子View,因此ViewGroup中onMeasure(),onLayout()方法需要处理子View的测量和摆放。这里通过自定义一个ViewGroup来模仿LinearLayout的垂直摆放和水平摆放子View的功能。

public class MyLinearLayout extends ViewGroup {
    public MyLinearLayout(Context context) {
        super(context);
    }

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

起名为MyLinearLayout。继承自ViewGroup。

2.1 自定义属性

通过自定义属性来设置控件的摆放方式,在res的values文件夹中新建attr.xml文件来存放自定义的属性。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="MyLinearLayout">
        <attr name="orientation1" format="string" />
    </declare-styleable>
</resources>

通过declare-stuleable标签来声明一个自定义属性的集合,name属性指定这个集合的命名,之后需要通过这个命名来找到指定属性。 内部的 attr标签指定了 具体的自定义属性, 这里声明了一个orientation1的属性, format指明属性值的类型,这是指定string类型(orientation ,vertical),format 可以指定许多类型:

reference     引用
color            颜色
boolean       布尔值
dimension   尺寸值
float            浮点值
integer        整型值
string          字符串
enum          枚举值

在使用自定义属性的时候需要指明属性的命名空间,通常在xml文件的根View上添加这一行。

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

myattrs为自定义的名字可以任意起,后面的值在gradle中是固定的,所有自定义的属性都要使用这个命名空间。
之后就可以在自定义的View中使用刚才自定义的属性

    <com.example.macroz.myapplication.view.MyLinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        myattrs:orientation1="vertical"
        >

在第二个构造函数中传入的参数 attrs就是解析xml文件时候的属性集合。现在要从这个attrs中拿到我们刚才定义的属性orientation1来确定空间是垂直摆放还是水平摆放。

    public MyLinearLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        //获取自定义属性的值,TypedArray获取到命名为MyLinearLayout的属性集合
        TypedArray ta=context.obtainStyledAttributes(attrs, R.styleable.MyLinearLayout);
        //从属性集合中拿到orientation1属性的值
        orientation=ta.getString(R.styleable.MyLinearLayout_orientation1);
        ta.recycle();
    }

现在拿到的控件的摆放方向,如果是vertical垂直摆放,那么设置MyLinearLayout的高度为所有子View的高度和,宽度为子View中最长的。
如果是horizontal水平摆放,宽度为子View的宽度和,高度为子View中最高的。

//根据子View的宽度计算当前宽度
 private int calCurWidth(int curWidth,View child)
    {
        if("vertical".equals(orientation))
        {
            if(child.getMeasuredWidth() >curWidth)
                curWidth=child.getMeasuredWidth();
        }else
        {
            curWidth+=child.getMeasuredWidth();
        }
        return  curWidth;
    }
//根据子View的高度计算当前高度
    private  int calCurHeight(int curHeight,View child)
    {
        if("vertical".equals(orientation))
        {
            curHeight+=child.getMeasuredHeight();
        }else
        {
            if(child.getMeasuredHeight()>curHeight)
                curHeight=child.getMeasuredHeight();
        }
        return curHeight;
    }

根据子View的宽高情况来设置MyLinearLayout的宽和高

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        measureChildren(widthMeasureSpec,heightMeasureSpec);
        int count=getChildCount();
        int width=0;
        int height=0;
        for(int i=0;i<count;i++)
        {
            View child=getChildAt(i);
            width=calCurWidth(width,child);
            height=calCurHeight(height,child);
        }
        setMeasuredDimension(width,height);
    }

测量了宽和高之后就是控件的摆放问题。这里自定义的MyLinearLayout只是简单的实现子View的水平摆放和垂直摆放功能,不考虑padding和margin的属性,因此在摆放的时候直接根据子View的大小来摆放。

 @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int count = getChildCount();
        int curLeft = l;
        int curTop = t;
        for (int i = 0; i < count; i++) {
            View view = getChildAt(i);
            view.layout(curLeft, curTop, curLeft + view.getMeasuredWidth(), curTop + view.getMeasuredHeight());
            //垂直摆放
            if ("vertical".equals(orientation)) {

                curTop += view.getMeasuredHeight();
            } else {
                //水平摆放
                curLeft += view.getMeasuredWidth();
            }
        }
    }

接下来使用这个自定义的MyLinearLayout,在它的内部添加两个TextView来测试摆放的效果,并且通过颜色来区分不同的View,蓝色为MyLinearLayout,红色为TextView1,绿色为TextView2

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:myattrs="http://schemas.android.com/apk/res-auto"
    android:id="@+id/root"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >
    <com.example.macroz.myapplication.view.MyLinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#ff0000ff"
        myattrs:orientation1="vertical"
        >
        <TextView
            android:id="@+id/kkk"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="#ffff0000"
            android:text="textView1" />
        <TextView
            android:id="@+id/ttt"
            android:layout_width="wrap_content"
            android:layout_height="50dp"
            android:background="#ff00ff00"
            android:text="textView22"/>
    </com.example.macroz.myapplication.view.MyLinearLayout>
</RelativeLayout>

效果如下:


image.png

之后设置
myattrs:orientation1="horizontal"
效果如下


image.png

注:这里只是简单的演示下效果,这段代码最好不要直接拿来使用,为了演示的简单,代码中有很多问题并没有考虑,比如自定义的orientation1设置成了“vertical” ,“horizontal”值之外的值情况并没有做相关的处理等。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,125评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,293评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,054评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,077评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,096评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,062评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,988评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,817评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,266评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,486评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,646评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,375评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,974评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,621评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,642评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,538评论 2 352

推荐阅读更多精彩内容