自定义view_自定义viewgroup - CSDN
精华内容
参与话题
  • 自定义View,有这一篇就够了

    万次阅读 多人点赞 2016-06-12 09:54:44
    我的简书同步发布:自定义View,有这一篇就够了 为了扫除学习中的盲点,尽可能多的覆盖Android知识的边边角角,决定对自定义View做一个稍微全面一点的使用方法总结,在内容并没有什么独特,其他大神们的博客上面基本...

    我的简书同步发布:自定义View,有这一篇就够了

    为了扫除学习中的盲点,尽可能多的覆盖Android知识的边边角角,决定对自定义View做一个稍微全面一点的使用方法总结,在内容上面并没有什么独特的地方,其他大神们的博客上面基本上都有讲这方面的内容,如果你对自定义View很熟了,那么就不用往下看啦~。如果对自定义View不是很熟,或者说很多内容忘记了想复习一下,更或者说是从来没用过,欢迎跟我一起重温这方面的知识,或许我的博文更符合你的胃口呢(*^__^*) 嘻嘻……

    1.自定义View

    首先我们要明白,为什么要自定义View?主要是Android系统内置的View无法实现我们的需求,我们需要针对我们的业务需求定制我们想要的View。自定义View我们大部分时候只需重写两个函数:onMeasure()、onDraw()。onMeasure负责对当前View的尺寸进行测量,onDraw负责把当前这个View绘制出来。当然了,你还得写至少写2个构造函数:

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

    1.1.onMeasure

    我们自定义的View,首先得要测量宽高尺寸。为什么要测量宽高尺寸?我在刚学自定义View的时候非常无法理解!因为我当时觉得,我在xml文件中已经指定好了宽高尺寸了,我自定义View中有必要再次获取宽高并设置宽高吗?既然我自定义的View是继承自View类,google团队直接在View类中直接把xml设置的宽高获取,并且设置进去不就好了吗?那google为啥让我们做这样的“重复工作”呢?客官别急,马上给您上茶~

    在学习Android的时候,我们就知道,在xml布局文件中,我们的layout_widthlayout_height参数可以不用写具体的尺寸,而是wrap_content或者是match_parent。其意思我们都知道,就是将尺寸设置为“包住内容”和“填充父布局给我们的所有空间”。这两个设置并没有指定真正的大小,可是我们绘制到屏幕上的View必须是要有具体的宽高的,正是因为这个原因,我们必须自己去处理和设置尺寸。当然了,View类给了默认的处理,但是如果View类的默认处理不满足我们的要求,我们就得重写onMeasure函数啦~。这里举个例子,比如我们希望我们的View是个正方形,如果在xml中指定宽高为wrap_content,如果使用View类提供的measure处理方式,显然无法满足我们的需求~。

    先看看onMeasure函数原型:

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) 

    参数中的widthMeasureSpecheightMeasureSpec是个什么鬼?看起来很像width和height,没错,这两个参数就是包含宽和高的信息。什么?包含?难道还要其他信息?是的!它还包含测量模式,也就是说,一个int整数,里面放了测量模式和尺寸大小。那么一个数怎么放两个信息呢?我们知道,我们在设置宽高时有3个选择:wrap_contentmatch_parent以及指定固定尺寸,而测量模式也有3种:UNSPECIFIEDEXACTLYAT_MOST,当然,他们并不是一一对应关系哈,这三种模式后面我会详细介绍,但测量模式无非就是这3种情况,而如果使用二进制,我们只需要使用2个bit就可以做到,因为2个bit取值范围是[0,3]里面可以存放4个数足够我们用了。那么Google是怎么把一个int同时放测量模式和尺寸信息呢?我们知道int型数据占用32个bit,而google实现的是,将int数据的前面2个bit用于区分不同的布局模式,后面30个bit存放的是尺寸的数据。

    那我们怎么从int数据中提取测量模式和尺寸呢?放心,不用你每次都要写一次移位<<和取且&操作,Android内置类MeasureSpec帮我们写好啦~,我们只需按照下面方法就可以拿到啦:

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

    爱思考的你肯定会问,既然我们能通过widthMeasureSpec拿到宽度尺寸大小,那我们还要测量模式干嘛?测量模式会不会是多余的?请注意:这里的的尺寸大小并不是最终我们的View的尺寸大小,而是父View提供的参考大小。我们看看测量模式,测量模式是干啥用的呢?

    测量模式 表示意思
    UNSPECIFIED 父容器没有对当前View有任何限制,当前View可以任意取尺寸
    EXACTLY 当前的尺寸就是当前View应该取的尺寸
    AT_MOST 当前尺寸是当前View能取的最大尺寸

    而上面的测量模式跟我们的布局时的wrap_contentmatch_parent以及写成固定的尺寸有什么对应关系呢?

    match_parent—>EXACTLY。怎么理解呢?match_parent就是要利用父View给我们提供的所有剩余空间,而父View剩余空间是确定的,也就是这个测量模式的整数里面存放的尺寸。

    wrap_content—>AT_MOST。怎么理解:就是我们想要将大小设置为包裹我们的view内容,那么尺寸大小就是父View给我们作为参考的尺寸,只要不超过这个尺寸就可以啦,具体尺寸就根据我们的需求去设定。

    固定尺寸(如100dp)—>EXACTLY。用户自己指定了尺寸大小,我们就不用再去干涉了,当然是以指定的大小为主啦。

    1.2.动手重写onMeasure函数

    上面讲了太多理论,我们实际操作一下吧,感受一下onMeasure的使用,假设我们要实现这样一个效果:将当前的View以正方形的形式显示,即要宽高相等,并且默认的宽高值为100像素。就可以这些编写:

     private int getMySize(int defaultSize, int measureSpec) {
            int mySize = defaultSize;
    
            int mode = MeasureSpec.getMode(measureSpec);
            int size = MeasureSpec.getSize(measureSpec);
    
            switch (mode) {
                case MeasureSpec.UNSPECIFIED: {//如果没有指定大小,就设置为默认大小
                    mySize = defaultSize;
                    break;
                }
                case MeasureSpec.AT_MOST: {//如果测量模式是最大取值为size
                    //我们将大小取最大值,你也可以取其他值
                    mySize = size;
                    break;
                }
                case MeasureSpec.EXACTLY: {//如果是固定的大小,那就不要去改变它
                    mySize = size;
                    break;
                }
            }
            return mySize;
    }
    
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            int width = getMySize(100, widthMeasureSpec);
            int height = getMySize(100, heightMeasureSpec);
    
            if (width < height) {
                height = width;
            } else {
                width = height;
            }
    
            setMeasuredDimension(width, height);
    }
    

    我们设置一下布局

      <com.hc.studyview.MyView
            android:layout_width="match_parent"
            android:layout_height="100dp"
            android:background="#ff0000" />
    

    看看使用了我们自己定义的onMeasure函数后的效果:
    自定义View

    而如果我们不重写onMeasure,效果则是如下:

    默认size

    1.3.重写onDraw

    上面我们学会了自定义尺寸大小,那么尺寸我们会设定了,接下来就是把我们想要的效果画出来吧~绘制我们想要的效果很简单,直接在画板Canvas对象上绘制就好啦,过于简单,我们以一个简单的例子去学习:假设我们需要实现的是,我们的View显示一个圆形,我们在上面已经实现了宽高尺寸相等的基础上,继续往下做:

      @Override
        protected void onDraw(Canvas canvas) {
            //调用父View的onDraw函数,因为View这个类帮我们实现了一些
            // 基本的而绘制功能,比如绘制背景颜色、背景图片等
            super.onDraw(canvas);
            int r = getMeasuredWidth() / 2;//也可以是getMeasuredHeight()/2,本例中我们已经将宽高设置相等了
            //圆心的横坐标为当前的View的左边起始位置+半径
            int centerX = getLeft() + r;
            //圆心的纵坐标为当前的View的顶部起始位置+半径
            int centerY = getTop() + r;
    
            Paint paint = new Paint();
            paint.setColor(Color.GREEN);
            //开始绘制
            canvas.drawCircle(centerX, centerY, r, paint);
    
    
        }
    

    显示效果

    1.4.自定义布局属性

    如果有些属性我们希望由用户指定,只有当用户不指定的时候才用我们硬编码的值,比如上面的默认尺寸,我们想要由用户自己在布局文件里面指定该怎么做呢?那当然是通我们自定属性,让用户用我们定义的属性啦~

    首先我们需要在res/values/styles.xml文件(如果没有请自己新建)里面声明一个我们自定义的属性:

    <resources>
    
        <!--name为声明的"属性集合"名,可以随便取,但是最好是设置为跟我们的View一样的名称-->
        <declare-styleable name="MyView">
            <!--声明我们的属性,名称为default_size,取值类型为尺寸类型(dp,px等)-->
            <attr name="default_size" format="dimension" />
        </declare-styleable>
    </resources>
    

    接下来就是在布局文件用上我们的自定义的属性啦~

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:hc="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <com.hc.studyview.MyView
            android:layout_width="match_parent"
            android:layout_height="100dp"
            hc:default_size="100dp" />
    
    </LinearLayout>
    

    注意:需要在根标签(LinearLayout)里面设定命名空间,命名空间名称可以随便取,比如hc,命名空间后面取得值是固定的:"http://schemas.android.com/apk/res-auto"

    最后就是在我们的自定义的View里面把我们自定义的属性的值取出来,在构造函数中,还记得有个AttributeSet属性吗?就是靠它帮我们把布局里面的属性取出来:

     private int defalutSize;
      public MyView(Context context, AttributeSet attrs) {
          super(context, attrs);
          //第二个参数就是我们在styles.xml文件中的<declare-styleable>标签
            //即属性集合的标签,在R文件中名称为R.styleable+name
            TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MyView);
    
            //第一个参数为属性集合里面的属性,R文件名称:R.styleable+属性集合名称+下划线+属性名称
            //第二个参数为,如果没有设置这个属性,则设置的默认的值
            defalutSize = a.getDimensionPixelSize(R.styleable.MyView_default_size, 100);
    
            //最后记得将TypedArray对象回收
            a.recycle();
       }
    
    

    最后,把MyView的完整代码附上:

    package com.hc.studyview;
    
    import android.content.Context;
    import android.content.res.TypedArray;
    import android.graphics.Canvas;
    import android.graphics.Color;
    import android.graphics.Paint;
    import android.util.AttributeSet;
    import android.util.Log;
    import android.view.MotionEvent;
    import android.view.View;
    
    /**
     * Package com.hc.studyview
     * Created by HuaChao on 2016/6/3.
     */
    public class MyView extends View {
    
        private int defalutSize;
    
        public MyView(Context context) {
            super(context);
        }
    
        public MyView(Context context, AttributeSet attrs) {
            super(context, attrs);
            //第二个参数就是我们在styles.xml文件中的<declare-styleable>标签
            //即属性集合的标签,在R文件中名称为R.styleable+name
            TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MyView);
            //第一个参数为属性集合里面的属性,R文件名称:R.styleable+属性集合名称+下划线+属性名称
            //第二个参数为,如果没有设置这个属性,则设置的默认的值
            defalutSize = a.getDimensionPixelSize(R.styleable.MyView_default_size, 100);
            //最后记得将TypedArray对象回收
            a.recycle();
        }
    
    
        private int getMySize(int defaultSize, int measureSpec) {
            int mySize = defaultSize;
    
            int mode = MeasureSpec.getMode(measureSpec);
            int size = MeasureSpec.getSize(measureSpec);
    
            switch (mode) {
                case MeasureSpec.UNSPECIFIED: {//如果没有指定大小,就设置为默认大小
                    mySize = defaultSize;
                    break;
                }
                case MeasureSpec.AT_MOST: {//如果测量模式是最大取值为size
                    //我们将大小取最大值,你也可以取其他值
                    mySize = size;
                    break;
                }
                case MeasureSpec.EXACTLY: {//如果是固定的大小,那就不要去改变它
                    mySize = size;
                    break;
                }
            }
            return mySize;
        }
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            int width = getMySize(defalutSize, widthMeasureSpec);
            int height = getMySize(defalutSize, heightMeasureSpec);
    
            if (width < height) {
                height = width;
            } else {
                width = height;
            }
    
            setMeasuredDimension(width, height);
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            //调用父View的onDraw函数,因为View这个类帮我们实现了一些
            // 基本的而绘制功能,比如绘制背景颜色、背景图片等
            super.onDraw(canvas);
            int r = getMeasuredWidth() / 2;//也可以是getMeasuredHeight()/2,本例中我们已经将宽高设置相等了
            //圆心的横坐标为当前的View的左边起始位置+半径
            int centerX = getLeft() + r;
            //圆心的纵坐标为当前的View的顶部起始位置+半径
            int centerY = getTop() + r;
    
            Paint paint = new Paint();
            paint.setColor(Color.GREEN);
            //开始绘制
            canvas.drawCircle(centerX, centerY, r, paint);
    
    
        }
    
    
    }
    
    

    2 自定义ViewGroup

    自定义View的过程很简单,就那几步,可自定义ViewGroup可就没那么简单啦~,因为它不仅要管好自己的,还要兼顾它的子View。我们都知道ViewGroup是个View容器,它装纳child View并且负责把child View放入指定的位置。我们假象一下,如果是让你负责设计ViewGroup,你会怎么去设计呢?

    1.首先,我们得知道各个子View的大小吧,只有先知道子View的大小,我们才知道当前的ViewGroup该设置为多大去容纳它们。

    2.根据子View的大小,以及我们的ViewGroup要实现的功能,决定出ViewGroup的大小

    3.ViewGroup和子View的大小算出来了之后,接下来就是去摆放了吧,具体怎么去摆放呢?这得根据你定制的需求去摆放了,比如,你想让子View按照垂直顺序一个挨着一个放,或者是按照先后顺序一个叠一个去放,这是你自己决定的。

    4.已经知道怎么去摆放还不行啊,决定了怎么摆放就是相当于把已有的空间”分割”成大大小小的空间,每个空间对应一个子View,我们接下来就是把子View对号入座了,把它们放进它们该放的地方去。

    现在就完成了ViewGroup的设计了,我们来个具体的案例:将子View按从上到下垂直顺序一个挨着一个摆放,即模仿实现LinearLayout的垂直布局。

    首先重写onMeasure,实现测量子View大小以及设定ViewGroup的大小:

    
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            //将所有的子View进行测量,这会触发每个子View的onMeasure函数
            //注意要与measureChild区分,measureChild是对单个view进行测量
            measureChildren(widthMeasureSpec, heightMeasureSpec);
    
            int widthMode = MeasureSpec.getMode(widthMeasureSpec);
            int widthSize = MeasureSpec.getSize(widthMeasureSpec);
            int heightMode = MeasureSpec.getMode(heightMeasureSpec);
            int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    
            int childCount = getChildCount();
    
            if (childCount == 0) {//如果没有子View,当前ViewGroup没有存在的意义,不用占用空间
                setMeasuredDimension(0, 0);
            } else {
                //如果宽高都是包裹内容
                if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
                    //我们将高度设置为所有子View的高度相加,宽度设为子View中最大的宽度
                    int height = getTotleHeight();
                    int width = getMaxChildWidth();
                    setMeasuredDimension(width, height);
    
                } else if (heightMode == MeasureSpec.AT_MOST) {//如果只有高度是包裹内容
                    //宽度设置为ViewGroup自己的测量宽度,高度设置为所有子View的高度总和
                    setMeasuredDimension(widthSize, getTotleHeight());
                } else if (widthMode == MeasureSpec.AT_MOST) {//如果只有宽度是包裹内容
                    //宽度设置为子View中宽度最大的值,高度设置为ViewGroup自己的测量值
                    setMeasuredDimension(getMaxChildWidth(), heightSize);
    
                }
            }
        }
        /***
         * 获取子View中宽度最大的值
         */
        private int getMaxChildWidth() {
            int childCount = getChildCount();
            int maxWidth = 0;
            for (int i = 0; i < childCount; i++) {
                View childView = getChildAt(i);
                if (childView.getMeasuredWidth() > maxWidth)
                    maxWidth = childView.getMeasuredWidth();
    
            }
    
            return maxWidth;
        }
    
        /***
         * 将所有子View的高度相加
         **/
        private int getTotleHeight() {
            int childCount = getChildCount();
            int height = 0;
            for (int i = 0; i < childCount; i++) {
                View childView = getChildAt(i);
                height += childView.getMeasuredHeight();
    
            }
    
            return height;
        }

    代码中的注释我已经写得很详细,不再对每一行代码进行讲解。上面的onMeasure将子View测量好了,以及把自己的尺寸也设置好了,接下来我们去摆放子View吧~

     @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
            int count = getChildCount();
            //记录当前的高度位置
            int curHeight = t;
            //将子View逐个摆放
            for (int i = 0; i < count; i++) {
                View child = getChildAt(i);
                int height = child.getMeasuredHeight();
                int width = child.getMeasuredWidth();
                //摆放子View,参数分别是子View矩形区域的左、上、右、下边
                child.layout(l, curHeight, l + width, curHeight + height);
                curHeight += height;
            }
        }
    

    我们测试一下,将我们自定义的ViewGroup里面放3个Button ,将这3个Button的宽度设置不一样,把我们的ViewGroup的宽高都设置为包裹内容wrap_content,为了看的效果明显,我们给ViewGroup加个背景:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <com.hc.studyview.MyViewGroup
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="#ff9900">
    
            <Button
                android:layout_width="100dp"
                android:layout_height="wrap_content"
                android:text="btn" />
    
            <Button
                android:layout_width="200dp"
                android:layout_height="wrap_content"
                android:text="btn" />
    
            <Button
                android:layout_width="50dp"
                android:layout_height="wrap_content"
                android:text="btn" />
    
    
        </com.hc.studyview.MyViewGroup>
    
    </LinearLayout>
    
    

    看看最后的效果吧~

    自定义ViewGroup

    是不是很激动~我们自己也可以实现LinearLayout的效果啦~~~~

    最后附上MyViewGroup的完整源码:

    package com.hc.studyview;
    
    import android.content.Context;
    import android.util.AttributeSet;
    import android.view.MotionEvent;
    import android.view.View;
    import android.view.ViewGroup;
    
    /**
     * Package com.hc.studyview
     * Created by HuaChao on 2016/6/3.
     */
    public class MyViewGroup extends ViewGroup {
        public MyViewGroup(Context context) {
            super(context);
        }
    
        public MyViewGroup(Context context, AttributeSet attrs) {
    
            super(context, attrs);
        }
    
        /***
         * 获取子View中宽度最大的值
         */
        private int getMaxChildWidth() {
            int childCount = getChildCount();
            int maxWidth = 0;
            for (int i = 0; i < childCount; i++) {
                View childView = getChildAt(i);
                if (childView.getMeasuredWidth() > maxWidth)
                    maxWidth = childView.getMeasuredWidth();
    
            }
    
            return maxWidth;
        }
    
        /***
         * 将所有子View的高度相加
         **/
        private int getTotleHeight() {
            int childCount = getChildCount();
            int height = 0;
            for (int i = 0; i < childCount; i++) {
                View childView = getChildAt(i);
                height += childView.getMeasuredHeight();
    
            }
    
            return height;
        }
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            //将所有的子View进行测量,这会触发每个子View的onMeasure函数
            //注意要与measureChild区分,measureChild是对单个view进行测量
            measureChildren(widthMeasureSpec, heightMeasureSpec);
    
            int widthMode = MeasureSpec.getMode(widthMeasureSpec);
            int widthSize = MeasureSpec.getSize(widthMeasureSpec);
            int heightMode = MeasureSpec.getMode(heightMeasureSpec);
            int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    
            int childCount = getChildCount();
    
            if (childCount == 0) {//如果没有子View,当前ViewGroup没有存在的意义,不用占用空间
                setMeasuredDimension(0, 0);
            } else {
                //如果宽高都是包裹内容
                if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
                    //我们将高度设置为所有子View的高度相加,宽度设为子View中最大的宽度
                    int height = getTotleHeight();
                    int width = getMaxChildWidth();
                    setMeasuredDimension(width, height);
    
                } else if (heightMode == MeasureSpec.AT_MOST) {//如果只有高度是包裹内容
                    //宽度设置为ViewGroup自己的测量宽度,高度设置为所有子View的高度总和
                    setMeasuredDimension(widthSize, getTotleHeight());
                } else if (widthMode == MeasureSpec.AT_MOST) {//如果只有宽度是包裹内容
                    //宽度设置为子View中宽度最大的值,高度设置为ViewGroup自己的测量值
                    setMeasuredDimension(getMaxChildWidth(), heightSize);
    
                }
            }
        }
    
        @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
            int count = getChildCount();
            //记录当前的高度位置
            int curHeight = t;
            for (int i = 0; i < count; i++) {
                View child = getChildAt(i);
                int height = child.getMeasuredHeight();
                int width = child.getMeasuredWidth();
                child.layout(l, curHeight, l + width, curHeight + height);
                curHeight += height;
            }
        }
    
    
    }
    

    好啦~自定义View的学习到此结束,是不是发现自定义View如此简单呢?

    展开全文
  • 不知不觉中,带你一步步深入了解View系列的文章已经写到第四篇了,回顾一下,我们一共学习了...现在前半部分的承诺已经如约兑现了,那么今天我就要来兑现后面部分的承诺,讲一讲自定义View的实现方法,同时这也是带

    转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/17357967


    不知不觉中,带你一步步深入了解View系列的文章已经写到第四篇了,回顾一下,我们一共学习了LayoutInflater的原理分析、视图的绘制流程、视图的状态及重绘等知识,算是把View中很多重要的知识点都涉及到了。如果你还没有看过我前面的几篇文章,建议先去阅读一下,多了解一些原理方面的东西。


    之前我有承诺过,会在View这个话题上多写几篇博客,讲一讲View的工作原理,以及自定义View的方法。现在前半部分的承诺已经如约兑现了,那么今天我就要来兑现后面部分的承诺,讲一讲自定义View的实现方法,同时这也是带你一步步深入了解View系列的完结篇。


    一些接触Android不久的朋友对自定义View都有一丝畏惧感,总感觉这是一个比较高级的技术,但其实自定义View并不复杂,有时候只需要简单几行代码就可以完成了。


    如果说要按类型来划分的话,自定义View的实现方式大概可以分为三种,自绘控件、组合控件、以及继承控件。那么下面我们就来依次学习一下,每种方式分别是如何自定义View的。


    一、自绘控件


    自绘控件的意思就是,这个View上所展现的内容全部都是我们自己绘制出来的。绘制的代码是写在onDraw()方法中的,而这部分内容我们已经在 Android视图绘制流程完全解析,带你一步步深入了解View(二) 中学习过了。


    下面我们准备来自定义一个计数器View,这个View可以响应用户的点击事件,并自动记录一共点击了多少次。新建一个CounterView继承自View,代码如下所示:

    public class CounterView extends View implements OnClickListener {
    
    	private Paint mPaint;
    	
    	private Rect mBounds;
    
    	private int mCount;
    	
    	public CounterView(Context context, AttributeSet attrs) {
    		super(context, attrs);
    		mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    		mBounds = new Rect();
    		setOnClickListener(this);
    	}
    
    	@Override
    	protected void onDraw(Canvas canvas) {
    		super.onDraw(canvas);
    		mPaint.setColor(Color.BLUE);
    		canvas.drawRect(0, 0, getWidth(), getHeight(), mPaint);
    		mPaint.setColor(Color.YELLOW);
    		mPaint.setTextSize(30);
    		String text = String.valueOf(mCount);
    		mPaint.getTextBounds(text, 0, text.length(), mBounds);
    		float textWidth = mBounds.width();
    		float textHeight = mBounds.height();
    		canvas.drawText(text, getWidth() / 2 - textWidth / 2, getHeight() / 2
    				+ textHeight / 2, mPaint);
    	}
    
    	@Override
    	public void onClick(View v) {
    		mCount++;
    		invalidate();
    	}
    
    }

    可以看到,首先我们在CounterView的构造函数中初始化了一些数据,并给这个View的本身注册了点击事件,这样当CounterView被点击的时候,onClick()方法就会得到调用。而onClick()方法中的逻辑就更加简单了,只是对mCount这个计数器加1,然后调用invalidate()方法。通过 Android视图状态及重绘流程分析,带你一步步深入了解View(三) 这篇文章的学习我们都已经知道,调用invalidate()方法会导致视图进行重绘,因此onDraw()方法在稍后就将会得到调用。


    既然CounterView是一个自绘视图,那么最主要的逻辑当然就是写在onDraw()方法里的了,下面我们就来仔细看一下。这里首先是将Paint画笔设置为蓝色,然后调用Canvas的drawRect()方法绘制了一个矩形,这个矩形也就可以当作是CounterView的背景图吧。接着将画笔设置为黄色,准备在背景上面绘制当前的计数,注意这里先是调用了getTextBounds()方法来获取到文字的宽度和高度,然后调用了drawText()方法去进行绘制就可以了。


    这样,一个自定义的View就已经完成了,并且目前这个CounterView是具备自动计数功能的。那么剩下的问题就是如何让这个View在界面上显示出来了,其实这也非常简单,我们只需要像使用普通的控件一样来使用CounterView就可以了。比如在布局文件中加入如下代码:

    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
    
        <com.example.customview.CounterView
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:layout_centerInParent="true" />
    
    </RelativeLayout>

    可以看到,这里我们将CounterView放入了一个RelativeLayout中,然后可以像使用普通控件来给CounterView指定各种属性,比如通过layout_width和layout_height来指定CounterView的宽高,通过android:layout_centerInParent来指定它在布局里居中显示。只不过需要注意,自定义的View在使用的时候一定要写出完整的包名,不然系统将无法找到这个View。


    好了,就是这么简单,接下来我们可以运行一下程序,并不停地点击CounterView,效果如下图所示。




    怎么样?是不是感觉自定义View也并不是什么高级的技术,简单几行代码就可以实现了。当然了,这个CounterView功能非常简陋,只有一个计数功能,因此只需几行代码就足够了,当你需要绘制比较复杂的View时,还是需要很多技巧的。


    二、组合控件


    组合控件的意思就是,我们并不需要自己去绘制视图上显示的内容,而只是用系统原生的控件就好了,但我们可以将几个系统原生的控件组合到一起,这样创建出的控件就被称为组合控件。


    举个例子来说,标题栏就是个很常见的组合控件,很多界面的头部都会放置一个标题栏,标题栏上会有个返回按钮和标题,点击按钮后就可以返回到上一个界面。那么下面我们就来尝试去实现这样一个标题栏控件。


    新建一个title.xml布局文件,代码如下所示:

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:background="#ffcb05" >
    
        <Button
            android:id="@+id/button_left"
            android:layout_width="60dp"
            android:layout_height="40dp"
            android:layout_centerVertical="true"
            android:layout_marginLeft="5dp"
            android:background="@drawable/back_button"
            android:text="Back"
            android:textColor="#fff" />
    
        <TextView
            android:id="@+id/title_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:text="This is Title"
            android:textColor="#fff"
            android:textSize="20sp" />
    
    </RelativeLayout>

    在这个布局文件中,我们首先定义了一个RelativeLayout作为背景布局,然后在这个布局里定义了一个Button和一个TextView,Button就是标题栏中的返回按钮,TextView就是标题栏中的显示的文字。


    接下来创建一个TitleView继承自FrameLayout,代码如下所示:

    public class TitleView extends FrameLayout {
    
    	private Button leftButton;
    
    	private TextView titleText;
    
    	public TitleView(Context context, AttributeSet attrs) {
    		super(context, attrs);
    		LayoutInflater.from(context).inflate(R.layout.title, this);
    		titleText = (TextView) findViewById(R.id.title_text);
    		leftButton = (Button) findViewById(R.id.button_left);
    		leftButton.setOnClickListener(new OnClickListener() {
    			@Override
    			public void onClick(View v) {
    				((Activity) getContext()).finish();
    			}
    		});
    	}
    
    	public void setTitleText(String text) {
    		titleText.setText(text);
    	}
    
    	public void setLeftButtonText(String text) {
    		leftButton.setText(text);
    	}
    
    	public void setLeftButtonListener(OnClickListener l) {
    		leftButton.setOnClickListener(l);
    	}
    
    }

    TitleView中的代码非常简单,在TitleView的构建方法中,我们调用了LayoutInflater的inflate()方法来加载刚刚定义的title.xml布局,这部分内容我们已经在 Android LayoutInflater原理分析,带你一步步深入了解View(一) 这篇文章中学习过了。


    接下来调用findViewById()方法获取到了返回按钮的实例,然后在它的onClick事件中调用finish()方法来关闭当前的Activity,也就相当于实现返回功能了。


    另外,为了让TitleView有更强地扩展性,我们还提供了setTitleText()、setLeftButtonText()、setLeftButtonListener()等方法,分别用于设置标题栏上的文字、返回按钮上的文字、以及返回按钮的点击事件。


    到了这里,一个自定义的标题栏就完成了,那么下面又到了如何引用这个自定义View的部分,其实方法基本都是相同的,在布局文件中添加如下代码:

    <RelativeLayout 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" >
    
        <com.example.customview.TitleView
            android:id="@+id/title_view"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" >
        </com.example.customview.TitleView>
    
    </RelativeLayout>

    这样就成功将一个标题栏控件引入到布局文件中了,运行一下程序,效果如下图所示:




    现在点击一下Back按钮,就可以关闭当前的Activity了。如果你想要修改标题栏上显示的内容,或者返回按钮的默认事件,只需要在Activity中通过findViewById()方法得到TitleView的实例,然后调用setTitleText()、setLeftButtonText()、setLeftButtonListener()等方法进行设置就OK了。


    三、继承控件


    继承控件的意思就是,我们并不需要自己重头去实现一个控件,只需要去继承一个现有的控件,然后在这个控件上增加一些新的功能,就可以形成一个自定义的控件了。这种自定义控件的特点就是不仅能够按照我们的需求加入相应的功能,还可以保留原生控件的所有功能,比如 Android PowerImageView实现,可以播放动画的强大ImageView 这篇文章中介绍的PowerImageView就是一个典型的继承控件。


    为了能够加深大家对这种自定义View方式的理解,下面我们再来编写一个新的继承控件。ListView相信每一个Android程序员都一定使用过,这次我们准备对ListView进行扩展,加入在ListView上滑动就可以显示出一个删除按钮,点击按钮就会删除相应数据的功能。


    首先需要准备一个删除按钮的布局,新建delete_button.xml文件,代码如下所示:

    <?xml version="1.0" encoding="utf-8"?>
    <Button xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/delete_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@drawable/delete_button" >
    
    </Button>

    这个布局文件很简单,只有一个按钮而已,并且我们给这个按钮指定了一张删除背景图。


    接着创建MyListView继承自ListView,这就是我们自定义的View了,代码如下所示:

    public class MyListView extends ListView implements OnTouchListener,
    		OnGestureListener {
    
    	private GestureDetector gestureDetector;
    
    	private OnDeleteListener listener;
    
    	private View deleteButton;
    
    	private ViewGroup itemLayout;
    
    	private int selectedItem;
    
    	private boolean isDeleteShown;
    
    	public MyListView(Context context, AttributeSet attrs) {
    		super(context, attrs);
    		gestureDetector = new GestureDetector(getContext(), this);
    		setOnTouchListener(this);
    	}
    
    	public void setOnDeleteListener(OnDeleteListener l) {
    		listener = l;
    	}
    
    	@Override
    	public boolean onTouch(View v, MotionEvent event) {
    		if (isDeleteShown) {
    			itemLayout.removeView(deleteButton);
    			deleteButton = null;
    			isDeleteShown = false;
    			return false;
    		} else {
    			return gestureDetector.onTouchEvent(event);
    		}
    	}
    
    	@Override
    	public boolean onDown(MotionEvent e) {
    		if (!isDeleteShown) {
    			selectedItem = pointToPosition((int) e.getX(), (int) e.getY());
    		}
    		return false;
    	}
    
    	@Override
    	public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
    			float velocityY) {
    		if (!isDeleteShown && Math.abs(velocityX) > Math.abs(velocityY)) {
    			deleteButton = LayoutInflater.from(getContext()).inflate(
    					R.layout.delete_button, null);
    			deleteButton.setOnClickListener(new OnClickListener() {
    				@Override
    				public void onClick(View v) {
    					itemLayout.removeView(deleteButton);
    					deleteButton = null;
    					isDeleteShown = false;
    					listener.onDelete(selectedItem);
    				}
    			});
    			itemLayout = (ViewGroup) getChildAt(selectedItem
    					- getFirstVisiblePosition());
    			RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(
    					LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
    			params.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
    			params.addRule(RelativeLayout.CENTER_VERTICAL);
    			itemLayout.addView(deleteButton, params);
    			isDeleteShown = true;
    		}
    		return false;
    	}
    
    	@Override
    	public boolean onSingleTapUp(MotionEvent e) {
    		return false;
    	}
    
    	@Override
    	public void onShowPress(MotionEvent e) {
    
    	}
    
    	@Override
    	public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
    			float distanceY) {
    		return false;
    	}
    
    	@Override
    	public void onLongPress(MotionEvent e) {
    	}
    	
    	public interface OnDeleteListener {
    
    		void onDelete(int index);
    
    	}
    
    }

    由于代码逻辑比较简单,我就没有加注释。这里在MyListView的构造方法中创建了一个GestureDetector的实例用于监听手势,然后给MyListView注册了touch监听事件。然后在onTouch()方法中进行判断,如果删除按钮已经显示了,就将它移除掉,如果删除按钮没有显示,就使用GestureDetector来处理当前手势。


    当手指按下时,会调用OnGestureListener的onDown()方法,在这里通过pointToPosition()方法来判断出当前选中的是ListView的哪一行。当手指快速滑动时,会调用onFling()方法,在这里会去加载delete_button.xml这个布局,然后将删除按钮添加到当前选中的那一行item上。注意,我们还给删除按钮添加了一个点击事件,当点击了删除按钮时就会回调onDeleteListener的onDelete()方法,在回调方法中应该去处理具体的删除操作。


    好了,自定义View的功能到此就完成了,接下来我们需要看一下如何才能使用这个自定义View。首先需要创建一个ListView子项的布局文件,新建my_list_view_item.xml,代码如下所示:

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:descendantFocusability="blocksDescendants"
        android:orientation="vertical" >
    
        <TextView
            android:id="@+id/text_view"
            android:layout_width="wrap_content"
            android:layout_height="50dp"
            android:layout_centerVertical="true"
            android:gravity="left|center_vertical"
            android:textColor="#000" />
    
    </RelativeLayout>
    然后创建一个适配器MyAdapter,在这个适配器中去加载my_list_view_item布局,代码如下所示:
    public class MyAdapter extends ArrayAdapter<String> {
    
    	public MyAdapter(Context context, int textViewResourceId, List<String> objects) {
    		super(context, textViewResourceId, objects);
    	}
    
    	@Override
    	public View getView(int position, View convertView, ViewGroup parent) {
    		View view;
    		if (convertView == null) {
    			view = LayoutInflater.from(getContext()).inflate(R.layout.my_list_view_item, null);
    		} else {
    			view = convertView;
    		}
    		TextView textView = (TextView) view.findViewById(R.id.text_view);
    		textView.setText(getItem(position));
    		return view;
    	}
    
    }
    到这里就基本已经完工了,下面在程序的主布局文件里面引入MyListView这个控件,如下所示:
    <RelativeLayout 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" >
    
        <com.example.customview.MyListView
            android:id="@+id/my_list_view"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" >
        </com.example.customview.MyListView>
    
    </RelativeLayout>
    最后在Activity中初始化MyListView中的数据,并处理了onDelete()方法的删除逻辑,代码如下所示:
    public class MainActivity extends Activity {
    
    	private MyListView myListView;
    
    	private MyAdapter adapter;
    
    	private List<String> contentList = new ArrayList<String>();
    
    	@Override
    	protected void onCreate(Bundle savedInstanceState) {
    		super.onCreate(savedInstanceState);
    		requestWindowFeature(Window.FEATURE_NO_TITLE);
    		setContentView(R.layout.activity_main);
    		initList();
    		myListView = (MyListView) findViewById(R.id.my_list_view);
    		myListView.setOnDeleteListener(new OnDeleteListener() {
    			@Override
    			public void onDelete(int index) {
    				contentList.remove(index);
    				adapter.notifyDataSetChanged();
    			}
    		});
    		adapter = new MyAdapter(this, 0, contentList);
    		myListView.setAdapter(adapter);
    	}
    
    	private void initList() {
    		contentList.add("Content Item 1");
    		contentList.add("Content Item 2");
    		contentList.add("Content Item 3");
    		contentList.add("Content Item 4");
    		contentList.add("Content Item 5");
    		contentList.add("Content Item 6");
    		contentList.add("Content Item 7");
    		contentList.add("Content Item 8");
    		contentList.add("Content Item 9");
    		contentList.add("Content Item 10");
    		contentList.add("Content Item 11");
    		contentList.add("Content Item 12");
    		contentList.add("Content Item 13");
    		contentList.add("Content Item 14");
    		contentList.add("Content Item 15");
    		contentList.add("Content Item 16");
    		contentList.add("Content Item 17");
    		contentList.add("Content Item 18");
    		contentList.add("Content Item 19");
    		contentList.add("Content Item 20");
    	}
    
    }

    这样就把整个例子的代码都完成了,现在运行一下程序,会看到MyListView可以像ListView一样,正常显示所有的数据,但是当你用手指在MyListView的某一行上快速滑动时,就会有一个删除按钮显示出来,如下图所示:



    点击一下删除按钮就可以将第6行的数据删除了。此时的MyListView不仅保留了ListView原生的所有功能,还增加了一个滑动进行删除的功能,确实是一个不折不扣的继承控件。

    到了这里,我们就把自定义View的几种实现方法全部讲完了,虽然每个例子都很简单,但是万变不离其宗,复杂的View也是由这些简单的原理堆积出来的。经过了四篇文章的学习,相信每个人对View的理解都已经较为深入了,那么带你一步步深入了解View系列的文章就到此结束,感谢大家有耐心看到最后。

    关注我的技术公众号,每天都有优质技术文章推送。关注我的娱乐公众号,工作、学习累了的时候放松一下自己。

    微信扫一扫下方二维码即可关注:

            

    展开全文
  • 对于安卓程序员来说,自定义view简直不要太重要,毕竟有很多功能,譬如圆形头像这些,用单纯的原生非常难以实现,而用自定义view,简直分分钟。在这里,我尝试用最简单方式跟初学者说一下如何自定义一个自己的view~...

    对于安卓程序员来说,自定义view简直不要太重要,毕竟有很多功能,譬如圆形头像这些,用单纯的原生非常难以实现,而用自定义view,简直分分钟。

    在这里,我尝试用最简单方式跟初学者说一下如何自定义一个自己的view~

    首先,最简单最简单的自定义view,有多简单?简单到没有逻辑,没有任何自带属性,只是简单展示,

    譬如,我制作一个圆形图片。。。

    一共只需要两步,

    第一步,创建你的自定义类,让它继承View类,并重写构造方法。

    第二步,重写ondraw()方法。

    比如,我创建了这个自定义类名字叫做circleImageView,如下图所示:

    这里写图片描述

    注意,这里有两个默认的构造器方法,circleImageView(Context context)和circleImageView(Context context,AttributeSet arrts)

    其中circleImageView(Context context)这个方法是你动态new的时候调用的,而circleImageView(Context context,AttributeSet arrts)这个方法是你在xml文件中调用才会调用这个方法。如下图,直接将标签定义为类名便可以了:

    这里写图片描述

    然后第二步,重写ondraw()方法!

    在这里简单说一下,ondraw(),顾名思义就是画画方法,在自定义组件时候,其实内部可以简单分为三个步骤,也就是三大方法,onMeasure,onLayout,ondraw,这些方法在自定义组件中依次运行,先onMeasure获得组件大小,再onLayout知道组件布局位置,最后ondraw将组件渲染画出来,但是其实在自定义组件中onMeasure和onLayout不是必须重写的,所以在这里我先不说,后面再讲,就说这个ondraw方法,这个是必须实现的方法。

    在上面的构造器方法中,我已经在组件初始化的时候便将要显示的图片初始化伟bitmap了

    这里写图片描述

    嗯,就是上面那个方法

    接下来我们要做的就是将这个bitmap截成一个圆形,在这里我写了一个圆形方法,如下图:

    这里写图片描述

    好啦,画圆方法写好啦,最后就是在ondraw里面调用画画方法就好了,如下图

    这里写图片描述

    这样一个自定义组件就写好啦,完整代码如下图

    package com.example.administrator.testmyidea.myTextView;
    
    import android.content.Context;
    import android.graphics.*;
    import android.util.AttributeSet;
    import android.view.View;
    import com.example.administrator.testmyidea.R;
    
    public class circleImageView extends View{
        private Paint mPaint;
        private Bitmap imageBitmap;
        private float circleRadio;
    
        public circleImageView(Context context) {
            super(context);
            mPaint=new Paint();
    
        }
    
        public circleImageView(Context context, AttributeSet attrs) {
            super(context, attrs);
            mPaint=new Paint();
            imageBitmap=BitmapFactory.decodeResource(getResources(),R.drawable.touxiang);
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            canvas.drawBitmap(getCircle(imageBitmap),0,0,mPaint);
        }
    
        //将bitmap转换成圆形bitmap
        public Bitmap getCircle(Bitmap bitmap){
            //circleRadio圆形图片的半径
            float circleRadio=bitmap.getWidth()/2;
            //创建一张新的bitmap,跟传入图片一样宽的正方形bitmap,
            Bitmap b=Bitmap.createBitmap(bitmap.getWidth(),bitmap.getWidth(), Bitmap.Config.ARGB_8888);
            //初始化画布,并将刚才创建的bitmap给这画布,让画布画在这bitmap上面
            Canvas canvas=new Canvas(b);
            //初始化画笔
            Paint p=new Paint();
            //在画布中画一个等宽的圆
            canvas.drawCircle(circleRadio,circleRadio,circleRadio,p);
            //设置画笔属性,让画笔只在哪圆圈中画画,关于画笔属性,可以百度一下,很多,但是非常有用
            p.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
            canvas.drawBitmap(bitmap,0,0,p);
            return  b;
        }
    
    }

    在xml里面代码是这样的

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout
            xmlns:android="http://schemas.android.com/apk/res/android"
            xmlns:tools="http://schemas.android.com/tools"
            xmlns:luo="http://schemas.android.com/apk/res-auto"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            tools:context="com.example.administrator.testmyidea.MainActivity">
        <com.example.administrator.testmyidea.myTextView.circleImageView
                android:layout_centerInParent="true"
                android:layout_width="150dp"
                android:layout_height="150dp"/>
    
    </RelativeLayout>
    

    最后效果:

    这里写图片描述

    诺~自定义的圆形imageview出来啦

    不过这里的是写死图片的,下一节我们说下怎么弄为可在代码动态设置图片,也可以在xml里面加入图片地址的

    展开全文
  • Android 自定义View (一)

    万次阅读 多人点赞 2016-08-10 19:19:53
    很多的Android入门程序猿来说对于Android自定义View,可能都是比较恐惧的,但是这又是高手进阶的必经之路,所有准备在自定义View上面花一些功夫,多写一些文章。先总结下自定义View的步骤: 1、自定义View的属性 2、...

    转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/24252901

    很多的Android入门程序猿来说对于Android自定义View,可能都是比较恐惧的,但是这又是高手进阶的必经之路,所有准备在自定义View上面花一些功夫,多写一些文章。先总结下自定义View的步骤:

    1、自定义View的属性

    2、在View的构造方法中获得我们自定义的属性

    [ 3、重写onMesure ]

    4、重写onDraw

    我把3用[]标出了,所以说3不一定是必须的,当然了大部分情况下还是需要重写的。

    1、自定义View的属性,首先在res/values/  下建立一个attrs.xml , 在里面定义我们的属性和声明我们的整个样式。

    <?xml version="1.0" encoding="utf-8"?>
    <resources>
    
        <attr name="titleText" format="string" />
        <attr name="titleTextColor" format="color" />
        <attr name="titleTextSize" format="dimension" />
    
        <declare-styleable name="CustomTitleView">
            <attr name="titleText" />
            <attr name="titleTextColor" />
            <attr name="titleTextSize" />
        </declare-styleable>
    
    </resources>
    我们定义了字体,字体颜色,字体大小3个属性,format是值该属性的取值类型:

    一共有:string,color,demension,integer,enum,reference,float,boolean,fraction,flag;不清楚的可以google一把。

    然后在布局中声明我们的自定义View

    <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/com.example.customview01"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
    
        <com.example.customview01.view.CustomTitleView
            android:layout_width="200dp"
            android:layout_height="100dp"
            custom:titleText="3712"
            custom:titleTextColor="#ff0000"
            custom:titleTextSize="40sp" />
    
    </RelativeLayout>

    一定要引入 xmlns:custom="http://schemas.android.com/apk/res/com.example.customview01"我们的命名空间,后面的包路径指的是项目的package

    2、在View的构造方法中,获得我们的自定义的样式

    /**
    	 * 文本
    	 */
    	private String mTitleText;
    	/**
    	 * 文本的颜色
    	 */
    	private int mTitleTextColor;
    	/**
    	 * 文本的大小
    	 */
    	private int mTitleTextSize;
    
    	/**
    	 * 绘制时控制文本绘制的范围
    	 */
    	private Rect mBound;
    	private Paint mPaint;
    
    	public CustomTitleView(Context context, AttributeSet attrs)
    	{
    		this(context, attrs, 0);
    	}
    
    	public CustomTitleView(Context context)
    	{
    		this(context, null);
    	}
    
    	/**
    	 * 获得我自定义的样式属性
    	 * 
    	 * @param context
    	 * @param attrs
    	 * @param defStyle
    	 */
    	public CustomTitleView(Context context, AttributeSet attrs, int defStyle)
    	{
    		super(context, attrs, defStyle);
    		/**
    		 * 获得我们所定义的自定义样式属性
    		 */
    		TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CustomTitleView, defStyle, 0);
    		int n = a.getIndexCount();
    		for (int i = 0; i < n; i++)
    		{
    			int attr = a.getIndex(i);
    			switch (attr)
    			{
    			case R.styleable.CustomTitleView_titleText:
    				mTitleText = a.getString(attr);
    				break;
    			case R.styleable.CustomTitleView_titleTextColor:
    				// 默认颜色设置为黑色
    				mTitleTextColor = a.getColor(attr, Color.BLACK);
    				break;
    			case R.styleable.CustomTitleView_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);
    		// mPaint.setColor(mTitleTextColor);
    		mBound = new Rect();
    		mPaint.getTextBounds(mTitleText, 0, mTitleText.length(), mBound);
    
    	}

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

    3、我们重写onDraw,onMesure调用系统提供的:

    @Override
    	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
    	{
    		super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    	}
    
    	@Override
    	protected void onDraw(Canvas canvas)
    	{
    		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);
    	}
    此时的效果是:

    是不是觉得还不错,基本已经实现了自定义View。但是此时如果我们把布局文件的宽和高写成wrap_content,会发现效果并不是我们的预期:


    系统帮我们测量的高度和宽度都是MATCH_PARNET,当我们设置明确的宽度和高度时,系统帮我们测量的结果就是我们设置的结果,当我们设置为WRAP_CONTENT,或者MATCH_PARENT系统帮我们测量的结果就是MATCH_PARENT的长度。

    所以,当设置了WRAP_CONTENT时,我们需要自己进行测量,即重写onMesure方法”:

    重写之前先了解MeasureSpec的specMode,一共三种类型:

    EXACTLY:一般是设置了明确的值或者是MATCH_PARENT

    AT_MOST:表示子布局限制在一个最大值内,一般为WARP_CONTENT

    UNSPECIFIED:表示子布局想要多大就多大,很少使用

    下面是我们重写onMeasure代码:

    	@Override
    	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
    	{
    		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(mTitle, 0, mTitle.length(), mBounds);
    			float textWidth = mBounds.width();
    			int desired = (int) (getPaddingLeft() + textWidth + getPaddingRight());
    			width = desired;
    		}
    
    		if (heightMode == MeasureSpec.EXACTLY)
    		{
    			height = heightSize;
    		} else
    		{
    			mPaint.setTextSize(mTitleTextSize);
    			mPaint.getTextBounds(mTitle, 0, mTitle.length(), mBounds);
    			float textHeight = mBounds.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:custom="http://schemas.android.com/apk/res/com.example.customview01"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
    
        <com.example.customview01.view.CustomTitleView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            custom:titleText="3712"
            android:padding="10dp"
            custom:titleTextColor="#ff0000"
            android:layout_centerInParent="true"
            custom:titleTextSize="40sp" />
    
    </RelativeLayout>

    现在的效果是:


    完全复合我们的预期,现在我们可以对高度、宽度进行随便的设置了,基本可以满足我们的需求。

    当然了,这样下来我们这个自定义View与TextView相比岂不是没什么优势,所有我们觉得给自定义View添加一个事件:

    在构造中添加:

    this.setOnClickListener(new OnClickListener()
    		{
    
    			@Override
    			public void onClick(View v)
    			{
    				mTitleText = randomText();
    				postInvalidate();
    			}
    
    		});

    private String randomText()
    	{
    		Random random = new Random();
    		Set<Integer> set = new HashSet<Integer>();
    		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();
    	}

    下面再来运行:


    我们添加了一个点击事件,每次让它随机生成一个4位的随机数,有兴趣的可以在onDraw中添加一点噪点,然后改写为验证码,是不是感觉很不错。


    好了,各位学习的,打酱油的留个言,顶个呗~


    源码点击此处下载






    展开全文
  • android进阶——自定义View

    万次阅读 2018-08-04 16:02:50
    博客和简书 我的博客:https://blog.csdn.net/qq_24675479 我的简书:https://www.jianshu.com/u/3ff32f5aea98 软件架构 01.自定义View简介 - onMeasure,onDraw,自定义属性 ...02.自定义View入门 - 自定义Te...
  • Android高级进阶之自定义View

    万次阅读 2018-09-14 17:51:36
    在网上看到一张图,花了一天时间,自己尝试着写了一个自定义View,里面涉及到了自定义属性、自定义View padding属性的处理、画笔(Paint)和画布(Canvas)的使用、分辨率适配问题、性能问题等,觉得还是有些东西值...
  • 三种自定义View的实现

    2018-03-29 14:02:11
    自定义控件的实现有三种方式,分别是:组合控件、自绘控件和继承控件。下面将分别对这三种方式进行介绍。 (一)组合控件  组合控件,顾名思义就是将一些小的控件组合起来形成一个新的控件,这些小的控件多...
  • 安卓面试题之---自定义View

    千次阅读 2019-11-25 21:29:53
    ##一,自定义View的分类 -1.继承View或者ViewGroup类,重写onDraw方法,调用invalidate方法重新绘制View(譬如说计数器) - -2.自定义组合控件,即将几种控件组合起来形成一个新的控件,这个新的组合控件就会整合了原来...
  • Android 自定义View (二) 进阶

    万次阅读 多人点赞 2016-08-10 19:22:20
    继续自定义View之旅,前面已经介绍过一个自定义View的基础的例子,Android 自定义View (一),如果你还对自定义View不了解可以去看看。今天给大家带来一个稍微复杂点的例子。 自定义View显示一张图片,下面包含图片...
  • 自定义View简介

    2020-07-26 23:11:15
    文章目录onMeasure()实操:ScrollView + ListView 会显示不全问题onDraw()onTouch()自定义属性 onMeasure() // 获取宽高的模式 int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = ...
  • 自定义view+onTouch

    2016-05-09 11:52:03
    自定义view 布局  xmlns:tools="http://schemas.android.com/tools"  android:layout_width="match_parent"  android:layout_height="match_parent"  android:paddingBottom="@dimen/activity_verti
  • 自定义View(重绘)

    千次阅读 2016-02-19 14:31:46
    这里的定义View是创建全新的View下面有2个实例1.带弧形的 (1)重写onMeasure(),获得View的长和高 (2)根据获得的长和高来画图1.ArcView.javapackage com.example.day0113.ui.view;import android.R.color; import ...
  • android自定义View设置大小的方法

    万次阅读 2012-10-25 02:03:55
    onMeasure使用的是父类的处理方法,如果我们需要解决 自定义View的大小,可以尝试下面的方法 view plaincopy to clipboardprint? @Override  protected void onMeasure (int widthMeasureSpec, int ...
  • 动态给自定义view设置id AndroidStudio会报错: 因为在sdk17(4.2.2)以上可以使用myView.setId(View.generateViewId())才行,为了兼容,里面的id必须使用静态int类型。 解决三种写法: 1、sdk17(4.2.2以上)...
  • Android 自定义View的资源回收

    万次阅读 2015-01-30 14:27:12
    Android默认Bitmap对象是不会帮你回收的,所以当你用自定义View的时候很容易出现OOM,尤其是在Listview里用自定义View的时候,我就遇到过,有个人重写了checkbox实现了IOS的开关控件,结果当我放入listview的时候...
  • Android 自定义View引用xml布局

    千次阅读 2016-11-14 14:22:58
    一参 Context上下文 二参 xml布局文件id 三参 ViewGroup inflate(context, R.layout.activity_main, this);public class CustomView extends ViewGroup { public CustomView(Context context, AttributeSet attrs...
  • android 在xml文件中引用自定义View

    万次阅读 2013-03-13 16:54:26
    在xml中引用自定义view 方法一: android:layout_width="fill_parent" android:layout_height="wrap_content" /> 方法二: <view class="com.test.copytext.CopyText"
  • kotlin 自定义view构造函数写法

    千次阅读 2018-09-20 14:32:12
    class LoadingPager: FrameLayout { constructor(context: Context): super(context){ } constructor(context: Context, attributeSet: AttributeSet): super(context, attributeSet){ } ...
  • Android自定义view生命周期

    千次阅读 2016-07-26 15:38:29
    自定义view的生命周期过程主要分为三个环节,初始准备,监测绘制,销毁 如图所示,一开始view在xml中加载,调用构造函数CustomView(Context contextt, AttributeSet attrs),当从xml中加载完成后,调用o
  • Android自定义View之一:初探实例

    万次阅读 热门讨论 2011-04-11 15:23:00
    Android自定义View实现很简单 继承View,重写构造函数、onDraw,(onMeasure)等函数。   如果自定义的View需要有自定义的属性,需要在values下建立attrs.xml。在其中定义你的属性。  ...
1 2 3 4 5 ... 20
收藏数 511,089
精华内容 204,435
关键字:

自定义view