欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android自定义View的使用及其原理知识点总结

程序员文章站 2022-06-25 11:04:54
在android开发中,系统提供给我们的ui控件是有限的,当我们需要使用一些特殊的控件的时候,只靠系统提供的控件,可能无法达到我们想要的效果,这时,就需要我们自定义一些控件...

在android开发中,系统提供给我们的ui控件是有限的,当我们需要使用一些特殊的控件的时候,只靠系统提供的控件,可能无法达到我们想要的效果,这时,就需要我们自定义一些控件,来完成我们想要的效果了。下面,我就来讲讲自定义控件的那些事。

首先,我来讲讲android的控件架构。android的控件可以被分为两类,分别是viewgroup和view。在viewgroup中可以包含多个view,并且管理他们。控件树就是有这两个部分组成的,控件树的上层负责的是下层控件的绘制和测量以及交互。我们在activity中使用的findviewbyid()方法,就是在控件树中用深度遍历的方法搜索到对应的id的。每一颗控件树的顶部,都有个viewparent对象,他是整棵树的核心,负责调度所有的交互事件。在activity中,我们是使用setcontentview()来加载布局的。每个activity都是包含着一个window对象的,在android中通常是phonewindow,他将一个decorview作为整个窗口的根view,将要显示的内容呈现在window上。decorview又分为两个部分,一个是titleview,一个是contentview。contentview是一个id为content的framelayout,布局文件就是设置在这里面的。而titleview就是我们看到topbar标题栏。这就是activity加载布局文件的过程了。

接下来,我们开始讲自定义控件的使用,下面讲解使用的时候,会夹带着一些原理的分析。自定义控件可以分为三种类型,一种是拓展谷歌提供的系统控件,来达到自己想要的效果。一种是将系统提供的控件组合在一起,作为一个组合控件来使用。还有一种是重新绘制测量一个全新的控件。

一、拓展谷歌提供的系统控件

假如我们要对textview控件进行拓展,首先我们要定义一个类继承textview,选择性的重写它的ondraw()、onmeasure()、ontouchevent()等方法。其中,ondraw()负责对图像的绘制,onmeasure()负责测量位置,ontouchevent()负责设置触摸的事件。当我们想直接绘制出有背景颜色的textview时,可以在类中定义画笔,在ondraw()进行绘制。代码如下:

paint paint1=new paint(); //定义画笔
paint1.setcolor(color.yellow);
paint1.setstyle(paint.style.fill);

然后,通过以下的代码,就可以绘制出一个带矩形框的textview,但是需要在绘制完成后在调用父类的ondraw(),因为是在系统控件上拓展,所以,还要有其原来的功能。

@override
  protected void ondraw(canvas canvas) {
    canvas.drawrect(0,0,getmeasuredwidth(),getmeasuredheight(),paint1);//绘制矩形
    canvas.save();
    super.ondraw(canvas);
    canvas.restore();
  }

使用canvas对象就可以进行绘图了,对canvas的讲解,我将会在下一篇博客讲解。

然后,我们只需要在布局文件中加入自定义的控件即可,在布局文件中,自定义view的名字就是自定义控件类的包名加上类名,假设定义customtextview类继承textview,例子如下:

<com.example.myapplication.view.customtextview
    android:layout_width="wrap_content"
    android:layout_height="match_parent"></com.example.myapplication.view.buttonbtn>

二、将系统提供的控件组合在一起

除了拓展原有的控件以外,我们还可以将控件组合成一个新的控件使用。首先,我们先定义一个新的布局文件,并把imageview和textview加入,代码如下。

<imageview
  android:id="@+id/iv"
  android:layout_width="20dp"
  android:layout_height="20dp"
  android:src="@mipmap/ic_launcher" />
 
<textview
  android:id="@+id/tv"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:layout_margintop="2dp"
  android:text="消息"
  android:textsize="13sp" />

然后我们定义一个类继承linearlayout,在类的构造方法中对控件和布局进行初始化。

public void init(context context) {
    //指定线性布局的显示方式,垂直
    setorientation(vertical);
    //设置用户期望的布局方式
    layoutparams mlayoutparams = new layoutparams(viewgroup.layoutparams.wrap_content, viewgroup.layoutparams.wrap_content);
    setlayoutparams(mlayoutparams);
    setgravity(gravity.center);
    setpadding(4, 4, 4, 4);
    //设置其布局文件
    view mbuttonbtnview = layoutinflater.from(context).inflate(layout.botton_btn_view, this, true);
    mimageview = mbuttonbtnview.findviewbyid(id.iv);
    mtextview = mbuttonbtnview.findviewbyid(id.tv);
  }

接下来,它的使用方法就和拓展控件的方法一样了,直接在布局文件中,加入控件即可。

<com.example.myapplication.view.buttonbtn
    android:layout_width="wrap_content"
    android:layout_height="match_parent"></com.example.myapplication.view.buttonbtn>

三、重写view来实现全新的控件

当系统原生的控件无法满足我们需求时,我们就可以定义一个新的控件来完成需要的功能。创建一个新的控件,需要继承view类,其难点主要在于绘制控件和实现交互。在继承view类时,我们还需要重写它的ondraw(),onmeasure()、ontouchevent()来实现绘制、测量和触摸事件。

ondraw()绘制就是在canvas对象上调用其一系列方法进行绘图,绘制控件的形状。

onmeasure()

下面,我来讲讲onmeasure()。在绘制view之前,我们需要告诉系统我们需要画一个多大的view以及他的位置,这就是onmeasure()进行的了。首先,我们来了解一下测量的三种模式:

exactly:精确值模式,在指定view具体数值的时候会用到。

at_most:最大值模式,将控件设置为"wrap_content"用到,它会根据子控件或者内容变化而变化。

unspecified:绘制控件想要多大就可以多大。

根据以上三种模式,我们就可以在测量的时候判断和使用了。首先,我们重写一个view的onmeasure()方法。再通过使用measurespec类获得控件的测量模式。measurespec使用的是位运算,其高2位为测量的模式,剩下的30位为测量的大小。

@override
  protected void onmeasure(int widthmeasurespec, int heightmeasurespec) {
    int widthmode = measurespec.getmode(widthmeasurespec);
    int widthsize = measurespec.getsize(widthmeasurespec);
 
    if (widthmode == measurespec.exactly) {
 
    } else if (widthmode == measurespec.at_most) {
 
    } else if (widthmode == measurespec.unspecified) {
 
    }
 
  }

以上代码就是通过判断测量模式来给定义控件的大小,这里只是测量了控件的宽度,控件高度的测量也是类似的,就不在做详解。

前面说过,viewgroup是用来管理控件的,当viewgroup的大小为"wrap_content"时,它就会遍历其所有子view,来获得子view的大小,再来设置自身的大小。我们使用过的布局,像relativelayout,linearlayout都是继承viewgroup的,所以他们也是使用这种方法来获得自己的大小的。

ontouchevent()

ontouchevent()就是我们所说的触摸事件,由于android手机是触屏的,所以我们自定义view在触摸屏幕的时候,也需要有一定的处理来完成交互。当重写ontouchevent方法的时候,我们可以看到,需要传入motionevent的对象。我们可以通过这个类来设置触摸的事件,也可以获得触摸点的位置。我们可以通过getaction()来获取触摸事件的行动,来判断是否按下屏幕或者移动。在android的坐标系中,我们都知道android的屏幕在竖屏的时候,以左上角的位置为原点,向右为x轴的正方向,向下为y轴的正方向,知道了这个后,我们就可以通过调用getx()和gety()方法可以获取触摸点的坐标,来完成一些交互操作。

public boolean ontouchevent(motionevent event) {
    float x;
    switch (event.getaction()) {
      case motionevent.action_down:
      {
        x=event.getx();
      }
        break;
      case motionevent.action_move:
        break;
      case motionevent.action_up:
        break;
    }
    return true;
  }

以上就是自定义控件常用重写的方法,通过了重写这几个方法,我们基本就可以实现一个简易的自定义控件了。下面,我们来了解下控件的事件拦截机制的原理。

事件拦截机制分析

我们前面讲过,控件结构是树形结构,一个viewgroup中可能有多个viewgroup或者view,那么,触摸事件是怎么准确的分配给每个view和viewgroup的呢。我们假设有一个viewgroupa,在他的里面嵌套着viewgroupb,而在viewgroupb的里面,又嵌套着一个view。当我们重写viewgroupa类的时候,就需要重写里面的这三个方法:

  1. dispatchtouchevent()
  2. onintercepttouchevent()
  3. ontouchevent()

而在重写view的时候,需要重写两个方法:

  1. dispatchtouchevent()
  2. ontouchevent()

可以根据名字看出,viewgroup中比view多了onintercepttouchevent()方法,这个方法就是事件拦截的核心。在每一个方法中log一下,再点击view的时候,就会发现方法调用的顺序:

首先,调用了viewgroupa类的dispatchtouchevent()和onintercepttouchevent()。

再调用了viewgroupb类的dispatchtouchevent()和onintercepttouchevent()。

再到view的dispatchtouchevent()方法。

这个调用的顺序就是事件传递的顺序,而事件处理的顺序则是:

  • view的ontouchevent()。
  • viewgroupb的ontouchevent()。
  • viewgroupa的ontouchevent()。

由此,可以看出,事件的分发是由上层的viewgroup发布的,再逐层下发。而事件的处理,则是由下层的view处理后,再逐层上传。前面也说过,onintercepttouchevent()是事件拦截的核心,那么,只要设置它的返回值为true,就可以拦截事件,使其不再下发,而ontouchevent()返回false,事件处理后就不会再上传。事件的分发和拦截的流程就大致讲解完成了。