Android自定义View笔记

来源:互联网 发布:结构化查询语言sql 编辑:程序博客网 时间:2024/06/05 09:08

自定义View的分类

  自定义View的有好几种分类,以我目前的阅历我把它分成4种:

  1. 特定的View的子类:Android的API已经为我们提供了不少可以使用的View,如TextView、ImageView、Button等等,但是有时候我们需要在这些基础的View上扩展一些功能,例如在Button里绑定一个TextWatch监测若干个EditText的输入情况时,就是继承Button类,在它的子类进行扩展了。这种自定义View实现难度低,不需要自己支持wrap_content和padding等属性,非常常见。
  2. 特定的ViewGroup子类:Android的API也为我们提供了不少可以使用的ViewGroup,如LinearLayout、RelativeLayout等等,但是有时候我们想把实现同一个需求若干个View组合起来,就可以用这种方式的自定义View来打包了。这种自定义View的实现难度低,也不需要自己处理ViewGroup对每个子View的测量和布局,非常常见。
  3. View的子类:View是一个很基础的父类,有一个空的onDraw()方法,继承它首先就是要实现这个方法,在里面利用Canvas画出自己想要的内容,不然View是不会显示任何东西的,使用这种自定义View主要用于实现一些非常规的图形效果,例如一些动态变化的View等等。这种自定义View的实现难度比较高,除了需要自己重写onDraw(),还要自己支持wrap_content和padding等属性,不过这种View也很常见。
  4. ViewGroup的子类:ViewGroup是用于实现View的组合布局的基础类,直接继承ViewGroup的子类主要是用于实现一些非常规的布局,即不同于官方API给出的LinearLayout等这些的布局。这种这种自定义View的实现难度高,需要处理好ViewGroup和它子View的测量和布局,比较少见。

进入自定义View

  下面是4种自定义View所需的步骤,有一些是必须的,有一些事根据实际需求选择的。

  下面我们来一个一个的来学习,其中重写onDraw()和重写onMeasure()是通过写自定义View的例子学习,重写自身和子类的onMesure()和onLayout()是通过写自定义ViewGroup来学习。

自定义属性

  想要实现自定义的功能,我们有时候就需要一些自己定义的属性,怎么让这些属性可以通过在xml上设置呢?只需要在res/value文件夹里新建一个attrs.xml(名字随便,建立位置对就行):

<?xml version="1.0" encoding="utf-8"?><resources>    <attr name="Color" format="color"/>    <attr name="inVelocityX" format="integer"/>    <attr name="inVelocityY" format="integer"/>    <attr name="Text" format="string"/>    <attr name="TextColor" format="color"/>    <declare-styleable name="BallView">        <attr name="color"/>        <attr name="inVelocityX" />        <attr name="inVelocityY" />        <attr name="Text" />        <attr name="TextColor"/>    </declare-styleable></resources>

  BallView就是我demo里面的自定义View名字,在declare-styleable外面声明一些自定义属性和属性的类型format,在里面申明BallView需要哪些属性(当然也可以直接在declare-styleable里面声明属性的format,这样就不需要在外面声明了,但是这样的话这些属性也不能被另一个自定义View重用)。

关于属性的format有很多种,reference,color,boolean等等,想看全部可以参考这里。

  在attrs.xml声明了属性之后,就可以在View的xml里用了,不过首先要在根ViewGroup里声明变量空间:

<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:cust="http://schemas.android.com/apk/res-auto"    android:id="@+id/activity_main"    android:layout_width="match_parent"    android:layout_height="match_parent">        <scut.com.learncustomview.BallView            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:layout_centerInParent="true"            cust:color="#ff0000"            cust:Text="我是一个球"            cust:TextColor="#ffffff"            cust:TextSize= "34"            cust:inVelocityX="6"            cust:inVelocityY="6"/></RelativeLayout>

  然后我们就要在自定义View里面获取这些属性了,自定义View的构造函数有4个,自定义View必须重写至少一个构造函数:

    public BallView(Context context) {        super(context);    }    public BallView(Context context, AttributeSet attrs) {        super(context, attrs);    }    public BallView(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);    }    //API21之后才使用    public BallView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {        super(context, attrs, defStyleAttr, defStyleRes);    }

  4个构造函数中:如果View是在Java代码里面new的,则调用第一个构造函数;如果是在xml里声明的,则调用第二个构造函数,我们所需要的自定义属性也就是从这个AttributeSet参数传进来的;第三第四个构造函数不会自动调用,一般是在第二个构造主动调用(例如View有style属性的时候)。

如果想深入了解构造函数,可以参考这里和这里
  所以,我们就可以重写第二个构造函数那里获取我们在xml设定的自定义属性:

    //球的x,y方向速度    private int velocityX = 0,velocityY = 0;    //球的颜色    private int color;    //球里面的文字    private String text;    //文字的颜色    private int textColor;    public BallView(Context context, AttributeSet attrs) {        super(context, attrs);        //获取自定义属性数组        TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.BallView, 0, 0);        int n = a.getIndexCount();        for (int i = 0;i < n;i++){            int attr = a.getIndex(i);            switch (attr){                case R.styleable.BallView_inVelocityX:                    velocityX = a.getInt(attr,0);                    break;                case R.styleable.BallView_inVelocityY:                    velocityY = a.getInt(attr,0);                    break;                case R.styleable.BallView_color:                    color = a.getColor(attr,Color.BLUE);                    break;                case R.styleable.BallView_Text:                    text = a.getString(attr);                    break;                case R.styleable.BallView_TextColor:                    textColor = a.getColor(attr,Color.RED);                    break;            }        }    }

  可以看到输出:

System.out: text:球System.out: textColor:-1System.out: velocityX:3System.out: velocityY:3System.out: color:-65536

重写onMeasure()

  关于重写onMeasure()的解释,我觉得用BallView不合适,于是就另外开了个TestMeasureView进行测试:
  下面是没有重写onMeasure()来支持wrap_content的例子:

public class TestMeasureView extends View {    private Paint paint;    public TestMeasureView(Context context) {        super(context);    }    public TestMeasureView(Context context, AttributeSet attrs) {        super(context, attrs);    }    public TestMeasureView(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);    }    @Override    protected void onDraw(Canvas canvas) {        super.onDraw(canvas);        canvas.drawColor(Color.BLUE);    }}

在xml上使用这个View:

<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:cust="http://schemas.android.com/apk/res-auto"    android:id="@+id/activity_main"    android:layout_width="match_parent"    android:layout_height="match_parent">    <scut.com.learncustomview.TestMeasureView        android:layout_width="wrap_content"        android:layout_height="wrap_content" /></RelativeLayout>

得出的结果是这样的:

  这就是为什么View的之类要自己支持wrap_parent的原因了,如果不重写wrap_parent就被当成match_parent。具体原因可以看一下View的Measure过程,这个是必须了解的,下面的图(从链接里面盗的)是关键。

  了解Measure过程之后我们发现我们现在这个TestMeasureView的长宽参数是由父View的测量模式(RelativeLayout的EXACTLY)和自身的参数(wrap_content)决定的(AT_MOST),所以我们就可以重写onMeasure()让View支持wrap_content了,下面网上流传很广的方法:

    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        int hSpeSize = MeasureSpec.getSize(heightMeasureSpec);        int hSpeMode = MeasureSpec.getMode(heightMeasureSpec);        int wSpeSize = MeasureSpec.getSize(widthMeasureSpec);        int wSpeMode = MeasureSpec.getMode(widthMeasureSpec);        int width = wSpeSize;        int height = hSpeSize;        if (wSpeMode == MeasureSpec.AT_MOST){            //在这里实现计算需要wrap_content时需要的宽度,这里我直接当作赋值处理了            width =200;        }        if (hSpeMode == MeasureSpec.AT_MOST){            //在这里实现计算需要wrap_content时需要的高度,这里我直接当作赋值处理了            height = 200;        }        //传入处理后的宽高        setMeasuredDimension(width,height);    }

结果是成功的:

  网上的很多都是这样做,通过判断测量模式是否AT_MOST来判断View的参数是否是wrap_content,然而,通过上面的表我们发现View的AT_MOST模式对应的不只是wrap_content,还有当父View是AT_MOST模式的时候的match_parent,如果我们这样做的话,父View是AT_MOST的时候这个自定义View的match_parent不就失效了吗。
  测试一下,我们把TestMeasureView长宽参数设置为match_parent,然后在外面再包一个模式为AT_MOST的父View(把父View的宽高都设为wrap_content,这样就确保了模式是AT_MOST,UNSPECIFIED因为不会出现在这里可以忽略):

<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:cust="http://schemas.android.com/apk/res-auto"    android:id="@+id/activity_main"    android:layout_width="match_parent"    android:layout_height="match_parent">    <LinearLayout        android:layout_width="wrap_content"        android:layout_height="wrap_content">        <scut.com.learncustomview.TestMeasureView            android:layout_width="match_parent"            android:layout_height="match_parent" />    </LinearLayout></RelativeLayout>

  运行一下,结果果然是match_parent失效:

  所以说看到的东西要思考一下,才能真正地转化为自己的,然后这个怎么解决呢,很简单,直接在onMeasure里面判断参数是否wrap_content就好:

    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        int hSpeSize = MeasureSpec.getSize(heightMeasureSpec);        int hSpeMode = MeasureSpec.getMode(heightMeasureSpec);        int wSpeSize = MeasureSpec.getSize(widthMeasureSpec);        int wSpeMode = MeasureSpec.getMode(widthMeasureSpec);        int width = wSpeSize;        int height = hSpeSize;        if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT){            //在这里实现计算需要wrap_content时需要的宽            width =200;        }        if (getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT){            //在这里实现计算需要wrap_content时需要的高            height =200;        }        //传入处理后的宽高        setMeasuredDimension(width,height);    }

  然后我把参数设回wrap_content(xml就不贴代码了),结果是正确的:

  但是这种方法有一个缺陷,就是可能会将UNSPECIFIED的情况也覆盖掉,但是UNSPECIFIED一般只出现在系统内部的View,不会出现在自定义View,而且当它出现的时候也可以加个判断按情况解决。

重写onDraw()

  这里就是利用onDraw()给出的Canvas画出各种东西了,具体可以参考我之前的笔记。这里是BallView的onMeasure()方法和onDraw(),通过以下代码,可以实现在wrap_content的时候根据字的内容长度画出相应的圆,然后可以根据给出的速度移动,遇到“墙会碰撞”。

    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        int wSpeSize = MeasureSpec.getSize(widthMeasureSpec);        int hSpeSize = MeasureSpec.getSize(heightMeasureSpec);        int width = wSpeSize ;        int height = hSpeSize;        if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT){            //在这里实现计算需要wrap_content时需要的宽高            width = bounds.width();        }else if(getLayoutParams().width != ViewGroup.LayoutParams.MATCH_PARENT){            width = getLayoutParams().width;        }        if (getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT){            //在这里实现计算需要wrap_content时需要的宽高            height =bounds.height();        }else if(getLayoutParams().height != ViewGroup.LayoutParams.MATCH_PARENT){            height = getLayoutParams().height;        }        //计算半径        radius = Math.max(width,height)/2;        //传入处理后的宽高        setMeasuredDimension((int) (radius*2+1), (int) (radius*2+1));    }    @Override    protected void onDraw(Canvas canvas) {        super.onDraw(canvas);        canvas.drawCircle(getWidth()/2,getHeight()/2,radius,paintFill);        //让字体处于球中间        canvas.drawText(text,getWidth()/2,getHeight()/2+bounds.height()/2,paintText);        checkCrashScreen();        offsetLeftAndRight(velocityX);        offsetTopAndBottom(velocityY);        postInvalidateDelayed(10);    }    //检测碰撞,有碰撞就反弹    private void checkCrashScreen(){        if ((getLeft() <= 0 && velocityX < 0)){            velocityX = -velocityX ;        }        if (getRight() >= screenWidth && velocityX > 0){            velocityX = -velocityX ;        }        if ((getTop() <= 0 && velocityY < 0)) {            velocityY = -velocityY ;        }        if (getBottom() >= screenHeight -sbHeight && velocityY > 0){            velocityY = -velocityY ;        }    }

  最后结果:

  

重写自身和子类的onMesure()和onLayout()

  
  上面是以自定义View为例子,这次就以一个自定义ViewGroup做为例子,做一个很简单的可以按照斜向下依次排列View的ViewGroup,类似于LinearLayout。要做一个新的ViewGroup,首先就是要重写它的onMesure()方法,让它可以按照需求测量子View和自身的宽高,还可以在这里支持wrap_content。

onMesure()和onLayout()是干什么的呢?为什么需要重写的是它们?因为View的绘制过程大概是Measure(测量)→Layout(定位)→Draw(绘图)三个过程,至于具体是怎样的呢?可以看工匠若水的这篇文章,看不懂没关系,可以看图。。。

    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);        int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);        // 计算出所有的childView的宽和高        measureChildren(widthMeasureSpec, heightMeasureSpec);        int cCount = getChildCount();        int width = 0;        int height = 0;        //处理WRAP_CONTENT情况,把所有子View的宽高加起来作为自己的宽高        if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT){            for (int i = 0; i < cCount; i++){                View childView = getChildAt(i);                width += childView.getMeasuredWidth();            }        }else {            width = sizeWidth;        }        if (getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT){            for (int i = 0; i < cCount; i++){                View childView = getChildAt(i);                height += childView.getMeasuredHeight();            }        }else {            height =sizeHeight;        }        //传入处理后的宽高        setMeasuredDimension(width,height);    }

  还有通过重写onLayout()把子View一个个排序斜向放好:

    @Override    protected void onLayout(boolean changed, int l, int t, int r, int b) {        int cCount = getChildCount();        int sPointX = 0;        int sPointY = 0;        int cWidth = 0;        int cHeight = 0;        //遍历子View,根据它们的宽高定位        for (int i = 0; i < cCount; i++){            View childView = getChildAt(i);            //这里使用getMeasuredXXX()方法是因为还没layout完,使用getWidth()和getHeight()获取会得不到正确的宽高            cWidth = childView.getMeasuredWidth();            cHeight = childView.getMeasuredHeight();            //定位            childView.layout(sPointX,sPointY,sPointX + cWidth,sPointY + cHeight);            sPointX += cWidth;            sPointY += cHeight;        }    }

  结果:
参数为WRAP_CONTENT的时候,成功地显示了:

<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:cust="http://schemas.android.com/apk/res-auto"    android:id="@+id/activity_main"    android:layout_width="match_parent"    android:layout_height="match_parent"    ><scut.com.learncustomview.InclinedLayout    android:layout_width="wrap_content"    android:layout_height="wrap_content"    android:background="#000fff">        <TextView            android:layout_width="50dp"            android:layout_height="50dp"            android:text="1"            android:background="#fff000"/>        <TextView            android:layout_width="20dp"            android:layout_height="50dp"            android:text="2"            android:background="#00ff00"/>        <TextView            android:layout_width="50dp"            android:layout_height="30dp"            android:text="3"            android:background="#ff0000"/></scut.com.learncustomview.InclinedLayout></RelativeLayout>


还有match_parent的时候:

  这样斜向下排列的ViewGroup就完成了,这些只是最简单的一个demo,用于我们熟悉自定义View的步骤,掌握了这些,复杂的自定义View也可以一步一步地完成了。

发布项目到JCenter

  在github上能看到很多人家做好的自定义View,使用人家造好的轮子是一件很方便的事情,而且很多都不用自己手动下载,只有在AndroidStudio里的Gradle添加依赖项complie ‘XXXX’就行了,这是怎样弄的呢?就是作者把项目上传到JCenter或者Maven了,要怎么做才能上传呢?
    读取上面这两篇博文,按上面的的步骤做
http://blog.csdn.net/lmj623565791/article/details/51148825
http://blog.csdn.net/u012375207/article/details/56840217

  我有几点补充:
1. 注册邮箱不能是qq邮箱,163邮箱(不知道是不是所有中国的都不行),别注册错了企业版,企业版右上角会有个30天倒计时的,点进去再点 Cancel Enterprise (trial)就可以换成个人版了,不用重新注册。
2. 要新建一个module把想要上传的内容装起来,不然可能会出错: Could not get unknown property ‘main’ for SourceSet container.
3. 注意要翻墙。。。
4. compile成功后引用是用封装上传的时候的包名,就像我这个上传的Module里面的包是scut.com.ballviewdemo需要import的时候也是import这个。
5. 最后是我这个项目上传之后的结果:

    compile 'com.yanzhikaijky:LearnCustomView:1.0.1'

参考资料:
(除了上面引用的)
《Android开发艺术探索》——任玉刚

0 0
原创粉丝点击