小结 Android 中的自定义 View 套路

文章目录
  1. 1. 简要介绍
  2. 2. 完全自定义的组件
    1. 2.1. 继承 onDraw() 和 onMeasure()
  3. 3. 复合控制
  4. 4. 修改存在的视图类型
  5. 5. 参考文献

君子藏器于身,待时而动。

Android 中的自定义 View,是个老生常谈的话题了,从简单到复杂的,应有尽有。原生控件满足不了需求,网上找到的 Demo 有所欠缺时,往往需要我们撸起袖子自己干。不过,所谓万变不离其宗,返璞归真,一般也就那些套路,深入理解套路,并勤加练习,相信随着时间的洗礼,自会更上一层楼。

以下大多内容翻译及加工自官网

简要介绍

Android 在基于基础布局类 ViewViewGroup 之上,提供了一个复杂且强大的组件模型来构建我们的 UI。一开始,平台包括了一系列预构建的 View 和 ViewGroup 子类,称作控件和布局,尤其可以用来构造我们的 UI。

  • 可用控件的部分清单包括 ButtonTextViewEditTextListViewCheckBoxRadioButtonGallerySpinner 和更具有特殊目的的 AutoCompleteTextViewImageSwitcherTextSwitcher
  • 可用的布局如 LinearLayoutFrameLayoutRelativeLayout

当然,若没有满足需求的预构建的控件或布局,我们可以创建自己的 View 子类。若只需要对现有的控件或布局做一些小的修改,则可以继承相应的控件或布局并重写其方法。

创建 View 的子类可以精确地控制到屏幕元素的外观和功能。为了给出一些控制自定义 View 的 idea,下面是一些例子:

  • 可以创建一个完全自定义渲染的 View 类型,如使用 2D 图像渲染的音量控制旋钮,其像一个模拟的电子控制
  • 可以将一组 View 组件组装成新的单一组件,大概如新建一个 ComboBox (一个弹出列表和自由输入文本区域的组合),亦或一个双面板的选择器控制开关 (一个带有列表的左右面板,在不同的列表里重组 item)
  • 可以重写 EditText 组件渲染在屏幕上的方式
  • 可以捕获事件如键压,然后以自定义的方式处理它们

接下来的部分要讲到如何创建自定义 View,然后在应用中使用它们。更多详情,需要参见 View 类。

##基本方式

下面是高层级的综述,关于开始创建自定义 View 组件所需要知道的东西:

  • 以新建的类继承现有的 View 类或其子类
  • 重写父类的一些方法。需要重写的父类方法以 “on” 开头,如 onDraw()onMeasure()onKeyDown()。这和在 Activity 或 ListActivity 中为了生命周期和别的功能连接的 on… 事件很相像
  • 使用新的继承类。一旦完成了,新的继承类可以用来代替其基于的 View 类

注意,在 Activities 里使用的话,继承类可以定义为内部类。这在控制其入口的地方很有用,但不是必须的 (或许是想在应用中新建一个更广使用范围的新的公用 View)。

完全自定义的组件

完全自定义的组件可以用来创建图像元素,以我们希望的样式显示。大概来说,一个图像式的音频壁纸看起来以旧的相似的规格,或者一个唱出的长文本,一个弹性球沿着语句移动,因此可以用卡拉 OK 的机器来唱歌。不管怎样,无论我们怎么组合现有植入的组件,我们想要的是已存在的组件做不到的。

幸运的是,我们可以创建以自己喜欢的方式表现的组件,或许限制我们的只是自己的想象力、屏幕的尺寸和可用的处理电力 (最终我们的应用应该以比桌面工作站更少电力的方式运行)。

好,总结如下,创建完全自定义的组件注意:

  • 要继承的最宽泛的视图,无疑是 View,故而我们应该继承它来创建我们自己的新的超级组件
  • 可以提供一个构造器,其可以从 XML 中获取属性和参数,也可以使用自己动态设置的属性和参数 (或许是音频壁纸的颜色和范围,或者针状物的宽度和阻尼等)
  • 可能想创建自己的事件监听器,性能存储器和修饰符,也可能是组件类中更复杂的行为
  • 当然,要重写 onMeasure(),并且,想让组件展示东西的话,也需要重写 onDraw()。虽然都有自己默认的行为,默认的 onDraw() 什么都不做,但是,默认的 onMeasure() 总会设置一个 100x100 的尺寸,或许不是我们想要的
  • 别的以 on… 开头的方法或许也按要求需要重写

继承 onDraw() 和 onMeasure()

onDraw() 方法提供给我们一个 Canvas,我们可以在它上面实现任何想要的东西:2D 图像、别的标准的或自定义的组件,自带个性风格的 text,或任何能想到的东西。

注意,这个不能应用于 3D 图像,假如想应用于 3D 图像的话,必须继承 SurfaceView,而不是简单地继承 View,在一个独立的线程绘制它。

onMeasure() 的戏份会更多点,其在我们定义的组件和容器之间,是一块重要的渲染协议。onMeasure() 应该被重写,高效且精确地报告其所包括部分的测量。这有点复杂,被 parent 的要求所限制 (是被传递至 onMeasure() 方法的) ,同时,也被计算好宽度和高度的 setMeasuredDimension() 方法所限制。若忘了在重写的 onMeasure() 方法里调用这个方法,会在测量期间抛出个异常。

更高层级的,使用 onMeasure() 方法要注意以下:

  • 被重写的 onMeasure() 方法被调用时带有宽度和高度测量说明 (widthMeasureSpecheightMeasureSpec参数,都是代表尺寸的整型值),应该在生成宽度和高度的尺寸时,作为限制要求来对待。对这些说明限制种类的完整参考,可以在View.onMeasure(int, int)的参考文档中找到,文档中也把整个测量操作解释的很透彻
  • 组件的 onMeasure() 方法应该计算一种测量宽度和高度,其被要求用来渲染组件。并且,它应该保持在传入说明里,尽管它可以选择越过它 (这种情况下,parent 可以选择做的包括如,裁剪、滚动、抛出异常或请求 onMeasure() 重新执行一次,或许有着不同的测量说明)
  • 一旦宽度和高度计算完成,setMeasuredDimension(int width, int height)方法在被调用时,须带有计算好的测量值,没做的话会导致抛出异常

下列表格是 framework 层在 views 上调用时,展现标准方法的总结:

种类 方法 描述
创建 (Creation) Constructors 构造器的一种形式是,当 view 从代码中被创建出来,还有当 view 从布局文件中被 inflate 出来;第二种形式是解析和应用定义在布局文件中的属性
onFinishInflate()) 在 view 和其所有的 children 从 XML 中被 inflate 之后调用
种类 方法 描述
布局 (Layout) onMeasure(int, int)) 用来决定当前 view 和其所有 children 的尺寸时被调用
onLayout(boolean, int,int, int, int)) 当 view 应该分配其所有 children 的尺寸和位置时被调用
onSizeChanged(int, int,int, int)) 当 view 的尺寸已经变化时被调用
种类 方法 描述
绘制 (Drawing) onDraw(Canvas)) 当 view 应该渲染其内容时被调用
种类 方法 描述
事件处理 (Event processing) onKeyDown(int, KeyEvent)) 当一个新的按键事件发生时被调用
onKeyUp(int, KeyEvent)) 当一个按键抬起的事件发生时被调用
onTrackballEvent(MotionEvent)) 当一个跟踪球动作事件发生时被调用
onTouchEvent(MotionEvent)) 当一个触屏动作事件发生时被调用
种类 方法 描述
焦点 (Focus) onFocusChanged(boolean,int, Rect)) 当 view 获取或失去焦点时被调用
onWindowFocusChanged(boolean)) 当窗口包括 view 获取或失去焦点时被调用
种类 方法 描述
依附 (Attaching) onAttachedToWindow()) 当 view 依附窗口时被调用
onDetachedFromWindow()) 当 view 从其所在窗口分离时被调用
onDetachedFromWindow()) 当包含 view 窗口的可见性改变时被调用

复合控制

假如不想创建一个完全自定义的组件,而是试图将一个包括一组已存在控制的复用组件放在一起,然后新建一个满足要求的复合组件 (或复合控制)。概括来说,这将一系列更具原子性的控制 (或 views) 一起放进一个 items 逻辑组里,这可以当一个单一的事物来对待。举个例子,Combo Box可以被认为是一个单线 EditText 域和一个带有依附着 PopupList 连接 Button 的组合。若按下按钮,选中 list 中的某些 item,其会弹出 EditText 编辑框,但是若用户喜欢的话,他们也可以直接在 EditText 编辑框内输入东西。

Android 里,实际上有两种 Views 这样可用:SpinnerAutoCompleteTextView,然而,Combo Box的概念让这个实例很容易来理解。

为了创建一个复合组件:

  • 通常开始于某种 Layout,因此可以创建一个继承于某种 Layout 的类。大概在Combo box的例子里,我们或许会使用呈现水平方向的 LinearLayout。不过,别的布局可以在里面嵌套,因此,复合组件可以任意得复杂和结构化。注意就像在 Activity 里,我们可以使用基于 XML 的声明式的方法创建包括的组件,也可以在 Java 代码里动态地嵌套它们
  • 在新类的构造器里,拿着父类希望的任何参数,然后首先传递至父类的构造器。然后,在新建的组件里,生成别的使用的 views,这是能创建 EditText 域和 PopupList 的地方。注意,也可以将 XML 里自定义的属性和参数拿出来在构造器里使用
  • 也可以在容纳 views 或许产生的事件里创建监听器,举个例子,若一个 list 选中了,创建一个 List Item 点击监听器的监听方法,来更新 EditText 里的内容
  • 或许也创建带有存储器和修饰器的自定义性能,举个例子,允许组件的 EditText 里初始时就带有值,然后在需要时查询所需的内容
  • 在继承某种布局 Layout 的 case 里,不需要重写 onDraw() 和 onMeasure() 方法,因为 layout 其自带的行为也可能运转得很好。但是,我们仍然可以在需要时重写它们
  • 或许要重写 on… 方法,像onKeyDown(),大概是按下某个键,在 combo box 弹出的 list 里选择默认的值

总结来说,使用布局 Layout 来作为自定义控制的基础,其有一系列好处,包括:

  • 就像在 Activity 屏幕里一样,可以使用声明式 XML 文件来指定布局,或者可以动态地创建 views,然后在代码中嵌套它们
  • onDraw() 和 onMeasure() 方法 (甚至还有别的 on… 方法) 可能有着合适的行为,因此,我们往往不必多余地来重写这些方法
  • 最后要说的是,我们可以迅速地构造任意复杂的复合 views,然后重用它们,好像它们就是单个组件

修改存在的视图类型

在某些情况下,甚至有一些更简单的有用的方法,来创建自定义 View。若有一个已经和你想要的组件十分相像,则可以简单地继承组件,只重写想要改变行为的方法。用完全自定义的组件,可以做任何想做的事,但是在 View 继承树里,开始于一个特殊的类,可以更自由地获取我们想要的行为。

举个例子,SDK 的 samples 里包括一个记事本应用。其演示了使用 Android 平台的很多方面,里面有继承 EditText 视图来建立一个内衬记事本。这虽然不是一个完美的例子,实现这个的 APIs 或许和早起的预览有所不同,但是其确实有些参考作用。

若没做过的话,可以将 NotePad 样例导入至 Android Studio (或者只是使用提供的链接查看源码)。尤其,注意看 NoteEditor.java 文件里自定义的 MyEditText,注意以下:

  • 定义

    该类以下面的方式来定义,

    1
    public static class MyEditText extends EditText
    • 其在NoteEditorActivity 里,定义为一个内部类,但它是 public 的,因此其可以在NoteEditor之外任何需要的地方,以NoteEditor.MyEditText的方式获取
    • 其是静态static的,意味着它不会生成所谓的假的方法,来允许它从父类中获取数据,反过来,这也意味着,它作为一个分离的类运转,而不是和NoteEditor强相关的东西。若不需要在外部类获取状态 state,以上所说的是一个更优雅的方式,让生成的类很小,允许其从别的类中被使用时很容易
    • 它继承自EditText,即我们选择在这个 case 中要自定义的 View。当我们完成后,新类能够代表一个标准的EditText view
  • 类初始化

    通常,父类构造方法首先被调用。此外,这不是一个默认的构造器,但是一个带有参数的构造器。当 EditText 从 XML 布局中被 inflate 后,其会同时带有这些参数被创建出来,因此,我们的构造器需要拿着它们,并将它们传递至父构造器

  • 重写方法

    这个例子里,只有一个被重写的方法,即onDraw(),但是在我们自建的自定义 View 里,很容易会出现其他需要的方法。

    在 NotePad 样例里,重写onDraw()方法来让我们在EditText的画布 canvas (canvas 被传递至重写的onDraw()方法里) 上。super.onDraw()方法在该方法结束前调用。父类方法应该被调用,但是在这个 case 里,在我们绘制了我们想要的 lines 后,在方法最后调用了。

  • 使用自定义组件

    我们现在有了自己的自定义组件,但是我们怎样使用它呢?在 NotePad 样例里,自定义组件直接在声明式布局里使用,看看res/layout文件夹下的note_editor.xml文件如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <view
    class="com.android.notepad.NoteEditor$MyEditText"
    id="@+id/note"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:background="@android:drawable/empty"
    android:padding="10dip"
    android:scrollbars="vertical"
    android:fadingEdge="vertical" />
    • 自定义组件在 XML 里新建为一个宽泛的 view,类指定使用全包名。注意,我们定义的内部类以 NoteEditor$MyEditText 注解的方式被引用,其也是 Java 里引用内部类的标准作法。

      假如自定义组件不是作为内部类定义的,那么,可以用 XML 元素的名字声明 View 组件,且包括类属性,如:

      1
      2
      3
      <com.android.notepad.MyEditText
      id="@+id/note"
      ... />

      注意,这种情况下,MyEditText是一个分离的类文件。当类在NoteEditor类里嵌套的话,这样不起作用的。

    • 定义中别的属性和参数,被传递至自定义的组件构造器里,然后传递过 EditText 构造器,因此,它们即是将在 EditText view 里使用的同样的参数。注意,把你自己的参数添加进去也是可能的。

以上皆提纲挈领,自定义 View 万变不离其宗。

至此,关于小结 Android 中的自定义 View 套路完毕。

本人才疏学浅,如有疏漏错误之处,望读者中有识之士不吝赐教,谢谢。

1
Email: [email protected] / WeChat: Wolverine623

您也可以关注我个人的微信公众号码农六哥第一时间获得博客的更新通知,或后台留言与我交流

参考文献

1.https://developer.android.com/guide/topics/ui/custom-components