Android小知识程序员程序员技术栈

自定义控件——初识自定义控件

2016-03-07  本文已影响2482人  阿敏其人

本文出自 “阿敏其人” 简书博客,转载或引用请注明出处。

开发的时候,因为业务需求或者封装需要,我们会进行自定义控件。

说在前面,本篇涉及到一些东西

一、控件的简单分类

简单粗暴来说,控件可以分为2种,View和ViewGroup

View 单独的控件,里面不能存放控件,继承自View。比如Imageview Button TextView。

TextView继承自View.png

ViewGroup 能存放控件的容器,比如FrameLayout、RelativeLayout 和LinearLayout等。

FrameLayout继承自ViewGroup.png

二、自定义控件的分类

自定义控件,类型各有各的分法,本人的对其进行大概如下分类:

既然如此,我们就开始来吧,从自制控件说起。
(本文的最后会附上关于三类自定义控件的博文链接)

三、自制控件

如果用最简单接地气的语言来描述自制控件,那么就是

1、继承自View或者ViewGroup
2、利用 onMeasure(测量)、onLayout(摆放)和onDraw(绘制) 三大步骤弄来弄去把View搞出来。(三者不必同时用到)
3、在利用事件的触发机制等调一调onTouchEvent等方法监听一下,然后利用Scroller或者ViewDragHelper等做做动画之类,合适怎么整就怎么整。
4、写写接口,调调回调,该谁干活谁就干活。

说成一句话:继承自View或者ViewGroup然后测量摆放绘制,然后做做动画,最后写写接口给调用者调用。

小二,来一个最简单的自制控件吧

1、 需要先认识的简单知识

提示:如果觉得琐碎可以直接从三.2的实例看起,最后再回过头来看这个三.1的内容。效果可能更佳。

我们的ViewGroup继承自View,在View里面有这么几个重要方法

首先三个我们已经提到的

1.1、measure和onMeasure(),layout()和onLayout(),draw()和onDraw()

其中,measure确定View的宽高,layout确定View的四个顶点的位置,而draw将View绘制在屏幕上。
measure()方法是final类型的方法,子类无法复写,而layout()和draw()子类就可以复写

measure和onMeasure()

关于measure(),需要分两种情况讨论,
情况1,只是View,那么通过View的measure() 可以完成对View的测量,而measure() 会去调用 onMeasure方法。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  setMeasuredDimension(
      getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
      getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

情况2,是ViewGroup,measure()除了完成对容器自身的测量之外,接着还会遍历去调用子元素(子元素可以使View或者ViewGroup)的measure(),各个子元素再去递归调用自身子元素的measure。注意是递归,递归,递归,重要的说三遍。
在ViewGroup的measure()会去调用onMeasure(),然后我们需要在onMeasure()里面对容器里的孩子进行 孩子.measure() 对孩子进行测量。

layout()和onLayout()
对于layout
如果是View,View调用layout可以指定View的位置
如果是ViewGroup,那么onLayout() 里面进行 孩子.layout 可以精确摆放孩子的位置

draw()和onDraw()

对于draw

这个就 比较简单了,他是按照如下几个流程走的
1、绘制背景 backgroud.draw(canvas)
2、绘制本身( onDraw )
3、绘制孩子( dispatchDraw )
4、绘制装饰 ( onDrawScrollBars )

其实实际开发中我们常用的,关心的也就是onDraw方法。

其实说了这么多,大概就是需要明白的是:

一个View的从无到有需要经过measure(),layout(),draw()三个步骤,measure()会辗转调用measure(),layout()会辗转调用onLayout,draw会转转调用onDraw().

如果是View,那么就写写onDraw,onLayout,至于onMeasure一般不需要。
如果是ViewGroup,那么些在onMeasure里面测量自身后接着进行对孩子的测量,在onLayout里面进行孩子的位置摆放。

.
.

1.2、onFinishInflate方法

我们知道,我们在Xml写的布局文件最终会在通过Pull解析的方式转成代码的。

onFinishInflate的作用,就是在xml加载组件完成后调用的。这个方法一般在自制ViewGroup的时候调用。

    /**
     * Finalize inflating a view from XML.  This is called as the last phase
     * of inflation, after all child views have been added.
     *
     * <p>Even if the subclass overrides onFinishInflate, they should always be
     * sure to call the super method, so that we get called.
     */
    @CallSuper
    protected void onFinishInflate() {
    }

.
.

1.3、ViewGroup的 getChildAt(int position) 方法

Returns the view at the specified position in the group.
返回该组中指定位置的视图。

这个位置按照我们include的顺序排列,索引从0开始。

    /**
     * Returns the view at the specified position in the group.
     *
     * @param index the position at which to get the view from
     * @return the view at the specified position or null if the position
     *         does not exist within the group
     */
    public View getChildAt(int index) {
        if (index < 0 || index >= mChildrenCount) {
            return null;
        }
        return mChildren[index];
    }

.
.

1.4 onMeasure和measure的参数

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        孩子.measure(32位的MeasureSpec宽,32位的MeasureSpec高);
        
    }

但看起来他们是一个int值,这两个参数可以是宽高的意思,但是不完全是,这宽和高都是32由 32位的二进制码组成的,并不是随随便便穿进去一个int类型的值就完事了的。

既然不能随随便便,那我们应该传什么?——我们应该传一个32位的二进制数。
那么我们计算出或者拿到这个数?—— 使用View类里面的MeasureSpec这个静态内部类。

1.5、 View类里面的静态内部类 MeasureSpec

我们首先需要看一个类,MeasureSpec

MeasureSpec.png

从上图我们可以看到,MeasureSpec这个类给我们提供了4个方法和3个常量

makeMeasureSpec和32位的二进制int值

先看看几个方法

其中,size占30位,mode占2位

这个方法的2个参数
size(大小)怎么指定?
mode怎么确定?

** 利用LayoutParams得到size **

我们的View有多大谁知道?——我们在写xml的布局文件的时候就知道了有多大了。
但是怎么获取文件的在xml布局时的大小呢?
利用View的getLayoutParams()方法可以得到一个LayoutParams类型的值,利用
layoutParams.width 和
layoutParams.height
就可以获得View在布局时的宽高。

** 怎么确定mode **

mode有3种模式,分别是EXACTLY:精确模式;AT_MOST�最大值模式,UNSPECIFIED : 未指定的模式,

MeasureSpec.EXACTLY,精确模式
当我们的 layout_width 和 layout_height 设定为
填充父窗体 match_parent 或者
指定具体数值 比如 android:layout_width="130dp" 时
就可以采用EXACTLY这种模式

.

MeasureSpec.AT_MOST,最大值模式
当我们的 layout_width 和 layout_height 设定为 warp_content 时,控件大小随着内容大小的变化而变化,就是采用AT_MOST这种模式。

.

MeasureSpec.UNSPECIFIED,未指定模式
父容器没有给子布局任何限制,子布局可以任意大小。比较奇怪,少用。

1.6、getMeasuredWidth和getMeasuredHeight

getMeasuredWidth 获取测量后的宽度
getMeasuredHeight 获取测量后的高度

这两个如果孩子没有被measure有效地测量过,那么返回的值是0。

getMeasuredWidth

    /**
     * Like {@link #getMeasuredWidthAndState()}, but only returns the
     * raw width component (that is the result is masked by
     * {@link #MEASURED_SIZE_MASK}).
     *
     * @return The raw measured width of this view.
     */
    public final int getMeasuredWidth() {
        return mMeasuredWidth & MEASURED_SIZE_MASK;
    }

.
.
getMeasuredHeight

    /**
     * Like {@link #getMeasuredHeightAndState()}, but only returns the
     * raw width component (that is the result is masked by
     * {@link #MEASURED_SIZE_MASK}).
     *
     * @return The raw measured height of this view.
     */
    public final int getMeasuredHeight() {
        return mMeasuredHeight & MEASURED_SIZE_MASK;
    }

.
.

2、继承自View的简单demo

上菜之前,我们新建一个名字为MyDiyView 的类,当我们继承自View,会被系统要求写上构造方法。

2.1、重载构造方法

我们一般重载2个或者3个即可

系统提示.png

构造方法

public class MyDiyView extends View {

    // java代码创建视图的时候被调用
    public MyDiyView(Context context) {
        super(context);
    }

    // xml创建视图并且没有指定StyleAttr是调用 至于attrs,是默认指定了的
    public MyDiyView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    
    //xml创建视图并且指定StyleAttr是调用,一般不需需要
    public MyDiyView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    // 参数多指定了一个,这个方法是API 21以上才有的,所以一般不需要
    public MyDiyView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }
}

只要有一个构造方法就不会报错了,一般我们会写上头两个构造方法。
为什么系统或有这样要求呢,因为这几个构造方法是必不可少的。根据参数的不同,其应用场合的也不同。我们在代码备注里面已经说得很清楚。

现在这个自定义VIew就能用了。只是当前是一片空白。

2.2、给view,鲜艳上色

一篇空白的View我们不需要,我们还是画上一点东西吧

�对于自制控件来说,有三个方法是必须掌握的,分别是
onMeasure (测量)
onLayout (摆放)
onDraw (绘制)

我们当前既然是最简单,直接调用一下用于绘制的onDraw就好

复写一下onDraw方法,发现onDraw里面一个Canvas类型的参数可以拿来用


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
    }

利用Canvas,我们可以画 线条,文字,图形,bitmap等等。我们先来画一个线条。(关于Canvas的相关知识网上很多,不了解的可以自行参考)

Canvas可以画出来很多东西


Canvas可以画出来很多东西.png

我们画一条线看看

MyDiyView


public class MyDiyView extends View {

    private Paint paint;

    // java代码创建视图的时候被调用
    public MyDiyView(Context context) {
        super(context);
        paint = new Paint(); // 此处创建可避免多次创建对象,浪费内存
    }

    // xml创建视图并且没有指定StyleAttr是调用 至于attrs,是默认指定了的
    public MyDiyView(Context context, AttributeSet attrs) {
        super(context, attrs);
        paint = new Paint();
    }


    //xml创建视图并且指定StyleAttr是调用,一般不需需要
    public MyDiyView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        //super.onDraw(canvas);

        paint.setColor(Color.BLUE);
        paint.setStrokeWidth(20); // 笔划大小
        // canvas 是帆布的意思,也可以理解为绘画板
        // 我们现在来画一条线
        /**
         public void drawLine (float startX, float startY, float stopX, float stopY, Paint paint)
         两点连成线
         startX和startY确定开始的点
         stopX和stopY确定结束的点
         */
        canvas.drawLine(30, 30, 120, 70, paint);  // 利用canvas画一条线
        super.onDraw(canvas);

    }
}

顺便佐证下构造函数的作用

佐证1、利用第一个构造函数直接在Java代码创建出来视图


public class MainActivity extends Activity {

    private TextView mTv;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //setContentView(R.layout.activity_main);
        setContentView(new MyDiyView(MainActivity.this));
    }
}

运行界面:

2016-02-28_23-03-33.png

如上已证

佐证2、在xml代码里面调用我们的自定义控件

在MainActivity的activity_main的xml布局里面利用MyDiyView的全路径名,引用我们的自定义控件

activity_main

<?xml version="1.0" encoding="utf-8"?>
<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"
    android:background="#ffffff"
    tools:context="com.amqr.simpleviewtest.MainActivity">

    <com.amqr.simpleviewtest.MyDiyView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!" />
</RelativeLayout>

MainActivity


public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

运行,发现效果如上图的运行效果。

3、继承自ViewGroup的简单demo

3.1、 重载构造方法和复写onLayout

我们新建一个类,继承ViewGroup
发现,系统不单单要需要弄出来几个构造方法,还要求实现复写onLayout方法。这就不报错了。
其实这也可以理解,毕竟是可以存放孩子的容器嘛。

像下面这样就不错报了

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

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

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

    }
    
}

3.2、把View塞进去ViewGroup

接下来我们怎么把View塞进ViewGroup里面呢?假设我们要塞进去的是两个View

1、新建两个xml布局文件,一个放上一个TextView,一个放上我们刚刚写好的自定义控件MyDiyView,在MainActivity的xml中的MyViewGroup里面include这两个布局

item_text

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/mLlDelete"
    android:layout_width="130dp"
    android:layout_height="60dp"
    android:background="#ff0000"
    android:gravity="center"
    android:orientation="vertical">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="文本"
        android:textSize="22dp"
        android:textColor="#ffffff"
        />
</LinearLayout>

.
.
item_mdv

<?xml version="1.0" encoding="utf-8"?>
<com.amqr.simpleviewtest.MyDiyView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="100dp"
    android:layout_height="60dp">

</com.amqr.simpleviewtest.MyDiyView>

.
.
MyViewGroup里面include这两个布局

<?xml version="1.0" encoding="utf-8"?>
<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"
    android:background="#ffffff"
    tools:context="com.amqr.simpleviewtest.MainActivity">

    <com.amqr.simpleviewtest.MyDiyViewGroup
        android:id="@+id/mMyDiyViewGroup"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <!--这个include的顺序很重要,因为需要和onFinishInflate相结合-->

        <!--TextView,系统自带的-->
        <include layout="@layout/item_text"/>

        <!--自定义控件-->
        <include layout="@layout/item_mdv"/>
    </com.amqr.simpleviewtest.MyDiyViewGroup>
</RelativeLayout>

.
.
2、在onFinishInflate方法中使用getChild(int position)方法加载获取这两个View

public class MyDiyViewGroup extends ViewGroup{
    private View myDiyView;
    private View mViewText;

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

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

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();

        mViewText = getChildAt(0);
        myDiyView = getChildAt(1);
    }
}

到了这里总算把View塞进ViewGroup了。但是此时如果直接运行程序,我们会发现还看到什么效果。

3.3、把让进ViewGroup的View显示出来

这时候,我们就需要利用到onMeasure和onLayout了。

首先进行复写onMeasure,在onMeasure里面进行 孩子.measure(measureSpec的32位的宽,measureSpec的32位的高)

这个32的值的size利用 LayoutParams 得到
至于mode,因为我们xml都是指定大小的,所以用MeasureSpec.EXACTLY

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

        LayoutParams layoutParams = mViewText.getLayoutParams();

        int mViewTextWidth = MeasureSpec.makeMeasureSpec(layoutParams.width,MeasureSpec.EXACTLY);
        int mViewTextHeight = MeasureSpec.makeMeasureSpec(layoutParams.height, MeasureSpec.EXACTLY);
        mViewText.measure(mViewTextWidth,mViewTextHeight);

        LayoutParams lp = myDiyView.getLayoutParams();

        int myDiyViewWidth = MeasureSpec.makeMeasureSpec(lp.width,MeasureSpec.EXACTLY);
        int myDiyViewHeight = MeasureSpec.makeMeasureSpec(lp.height,MeasureSpec.EXACTLY);

        myDiyView.measure(myDiyViewWidth, myDiyViewHeight);

    }

.
.
在onLayout里面进行 孩子.layout(l,t,r,b)

利用 孩子View.getMeasuredWidth和孩子View.getMeasuredHeight获取的孩子的宽和高

因为之前我们已经对孩子进行 孩子View.measure(宽,高)了,所以此时调用getMeasuredWidth和getMeasuredHeight能够顺利获得孩子的宽和高。(如果没进行measure那么这两个获取的值就是0)

为什么不用 孩子View.getWidth() 和 孩子View.getHeight()?

因为在haiziView还没进行 孩子View.layout(l,t,r,b)之前,如果使用 孩子View.getWidth(),那么获取出来的值是0。

摆放孩子的位置

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        int mViewTextWidth = mViewText.getMeasuredWidth();
        int mViewTextHeight = mViewText.getMeasuredHeight();

        Log.d("VG", "mViewText.getMeasuredWidth():  " + mViewText.getMeasuredWidth());
        Log.d("VG", "mViewText.getMeasuredHeight():  " + mViewText.getMeasuredHeight());


        mViewText.layout(0,0,mViewTextWidth,mViewTextHeight);


        int myDiyViewWidth = myDiyView.getMeasuredWidth();
        int myDiyViewHeight = myDiyView.getMeasuredHeight();


        myDiyView.layout(mViewTextWidth, 0, mViewTextWidth + myDiyViewWidth, myDiyViewHeight);
        Log.d("VG", "layout之后  mViewText.getWidth():  " + mViewText.getWidth());

    }

此时运行程序,发现我们这个ViewGroup已经成功显示在屏幕上了。

ViewGroup成功显示.png

附上完整的MyViewGroup代码

MyViewGroup

public class MyDiyViewGroup extends ViewGroup{

    private View myDiyView;
    private View mViewText;

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

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



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

        LayoutParams layoutParams = mViewText.getLayoutParams();

        int mViewTextWidth = MeasureSpec.makeMeasureSpec(layoutParams.width,MeasureSpec.EXACTLY);
        int mViewTextHeight = MeasureSpec.makeMeasureSpec(layoutParams.height, MeasureSpec.EXACTLY);
        mViewText.measure(mViewTextWidth,mViewTextHeight);

        LayoutParams lp = myDiyView.getLayoutParams();

        int myDiyViewWidth = MeasureSpec.makeMeasureSpec(lp.width,MeasureSpec.EXACTLY);
        int myDiyViewHeight = MeasureSpec.makeMeasureSpec(lp.height,MeasureSpec.EXACTLY);

        myDiyView.measure(myDiyViewWidth, myDiyViewHeight);
        Log.d("VG", "layout之前  mViewText.getWidth():  " + mViewText.getWidth());

    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        int mViewTextWidth = mViewText.getMeasuredWidth();
        int mViewTextHeight = mViewText.getMeasuredHeight();

        Log.d("VG", "mViewText.getMeasuredWidth():  " + mViewText.getMeasuredWidth());
        Log.d("VG", "mViewText.getMeasuredHeight():  " + mViewText.getMeasuredHeight());


        mViewText.layout(0,0,mViewTextWidth,mViewTextHeight);


        int myDiyViewWidth = myDiyView.getMeasuredWidth();
        int myDiyViewHeight = myDiyView.getMeasuredHeight();


        myDiyView.layout(mViewTextWidth, 0, mViewTextWidth + myDiyViewWidth, myDiyViewHeight);
        Log.d("VG", "layout之后  mViewText.getWidth():  " + mViewText.getWidth());

    }


    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();

        mViewText = getChildAt(0);
        myDiyView = getChildAt(1);
    }
}

MainActivity

public class MainActivity extends Activity {
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

activity_main

<?xml version="1.0" encoding="utf-8"?>
<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"
    android:background="#ffffff"
    tools:context="com.amqr.simpleviewtest.MainActivity">

    <com.amqr.simpleviewtest.MyDiyViewGroup
        android:id="@+id/mMyDiyViewGroup"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <!--这个include的顺序很重要,因为需要和onFinishInflate相结合-->

        <!--TextView,系统自带的-->
        <include layout="@layout/item_text"/>

        <!--自定义控件-->
        <include layout="@layout/item_mdv"/>
    </com.amqr.simpleviewtest.MyDiyViewGroup>
</RelativeLayout>

.
.
最后附上证明在layout之前孩子的getWidth是无法获取宽度的log

02-29 02:40:45.952 5073-5073/? D/VG: layout之前  mViewText.getWidth():  0
02-29 02:40:45.952 5073-5073/? D/VG: layout之前  mViewText.getWidth():  0
02-29 02:40:46.024 5073-5073/? D/VG: mViewText.getMeasuredWidth():  260
02-29 02:40:46.024 5073-5073/? D/VG: mViewText.getMeasuredHeight():  120
02-29 02:40:46.024 5073-5073/? D/VG: layout之后  mViewText.getWidth():  260
02-29 02:40:46.032 5073-5073/? D/VG: layout之前  mViewText.getWidth():  260
02-29 02:40:46.036 5073-5073/? D/VG: layout之前  mViewText.getWidth():  260
02-29 02:40:46.036 5073-5073/? D/VG: mViewText.getMeasuredWidth():  260
02-29 02:40:46.036 5073-5073/? D/VG: mViewText.getMeasuredHeight():  120
02-29 02:40:46.036 5073-5073/? D/VG: layout之后  mViewText.getWidth():  260

关于最简单的自制控件到此结束。
下面的链接是本人关于三类自定义控件的相关博文,欢迎链接查看。

自制控件1 开关按钮
自制控件2 —— 自制控件 仿qq侧滑菜单
组合控件1—— 设置框
组合控件2——海贼王选项菜单
拓展控件——拓展TextView

本篇完。

上一篇下一篇

猜你喜欢

热点阅读