美文网首页我爱编程
自定义View基础

自定义View基础

作者: MacroZH | 来源:发表于2018-05-28 10:40 被阅读0次

    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”值之外的值情况并没有做相关的处理等。

    相关文章

      网友评论

        本文标题:自定义View基础

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