自定义 View 之 View 类文档

View 类是用户界面组件的基类,一个 View 在屏幕上占据一块矩形区域,并且负责这块区域的内容绘制、事件传递等等工作。ViewGroup 是 View 的子类,用来盛放其他 View。

View 的使用

窗口中的所有 View 都是以单个的树形结构够成的。我们可以通过 Java 代码或者 XML 文件来定义。View 有很多子类,例如 TextView 用来显示文字,ImageView 用来显示图片等等。

一旦你创建了一个 View,还有一些常见操作可以实行:

  • 设置各种属性:比如设置 TextView 控件的文本。View 中提供的属性和设置属性的方法因子类的不同而多样化。需要注意的是,这些属性同样可以在 XML 布局文件中设置。
  • 设置焦点:框架将会处理用户输入中的焦点转移。某个 View 强制获取焦点,需要调用 requestFocus() 方法。
  • 设置各种监听事件:可以对 View 设置监听,对于这些 View,当有些感兴趣的事情发生时,监听器负责告之这些 View。比如,所有的 Views 需要你设置监听来通知获得和失去焦点事件。一些子类提供更多特殊的监听器。比如,Button 控件的点击事件的监听。
  • 设置可见性:你可以调用 setVisibility(int) 方法来隐藏或是显示View。

实现一个自定义 View

为了实现一个自定义 View,通常从覆盖一些 View 的标准方法开始,一般情况下,我们只需要覆盖 onDraw() 方法。

类别(Category)方法(Methods)描述(Description)
构建(Creation)构造函数当我们使用代码创建 View 的时候,需要调用该 View 的构造函数,然后解析并应用布局文件中的各个属性。
onFinishInflate()在 View 创建之后调用,所有的子元素都从 XML 当中 inflate 出来。
是 inflate XML 的最后阶段,即使重写该方法,也必须调用其超类
布局(Layout)onMeasure(int, int)调用该方法以确定该 View 及其子 View 的大小
onLayout(boolean, int, int, int, int)该 View 为其子 View 设置大小和位置时调用
onSizeChanged(int, int, int, int)当该 View 大小发生改变时调用
绘制(Drawing)onDraw(android.graphics.Canvas)当 View 需要展示的时候调用
事件处理(Event processing)onKeyDown(int, KeyEvent)当键盘按下时候调用
onKeyUp(int, KeyEvent)当键盘抬起时调用
onTrackballEvent(MotionEvent)当轨迹球运动时调用
onTouchEvent(MotionEvent)当屏幕被触摸时调用
焦点(Focus)onFocusChanged(boolean, int, android.graphics.Rect)当 View 获取或者失去焦点时调用
onWindowFocusChanged(boolean)当包含 View 的 Window 获取或失去焦点时调用
属性(Attaching)onAttachedToWindow()当 View 连接到 Window 时调用
onDetachedFromWindow()当 View 从 Window 分离时调用
onWindowVisibilityChanged(int)当包含 View 的 Window 的可视性发生改变时调用

IDs

每个 View 都有唯一的 ID 与之相关联,有助于我们在初始化的时候找到该 View,就好像我们在 XML 文件当中通过 android:id="@+id/xxx" 指定,在 Java 文件中通过 findViewById(R.id.xxx) 初始化该组件一样。

View ID 在所有控件树中不必是唯一的,但必须在你搜索的树中是唯一的,例如你可以在每个 Activity 当中都有一个 ID 为 button 的组件,但是在同一个 Activity 当中,只能有一个 ID 为 button 的组件。

不过为了不混淆,最好是 ID 全局唯一

位置

View 是矩形的,每个 View 拥一个相对于左上角的坐标(location)和两个表示宽度和高度的尺寸(dimensions)。坐标和宽度单位都是像素(px)。

可以通过调用 getLeft()getTop() 来获取 View 的位置,前者返回 View 的左坐标(也可以说是 X 坐标),后者返回 View 的顶部坐标(也可以说是 Y 坐标)。这些方法都返回的是 View 相对于其父视图的位置。例如当 getLeft 返回 20 时,这意味着该 View 位于其父容器的左侧边缘右侧 20 像素处。

此外,Android 还提供了几个便利的方法来避免不必要的计算,即 getRight()getBottom(),前者返回 View 的右侧的坐标后者返回 View 的底部的坐标。getRight() = getLeft()+ getWidth()。

size,padding 和 margin

View 的大小用高度和宽度来表示。实际上,View 有两组高度值和宽度值。

第一组被成为测量高度测量宽度,这些值定义了一个 View 在其父容器中的大小(更多细节,请看 Layout)。可以通过调用 getMeasuredWidth()getMeasuredHeight() 获取测量宽度/高度值。

第二组就是简单的宽度和高度,有时又被成为绘制宽度绘制高度。这些值定义了屏幕上显示的实际的长度。这些长度值,可能但是没必要于 measuredWidth 和 measuredHeight 相同。通过调用 getWidth()getHeight() 来获取。

测量 View 的尺寸,应该考虑到 View 的 padding 值。padding 是 View 的上、下、左、右四个边界。padding 用来将 View 偏移一定像素。可以使用 setPadding(int, int, int, int) 或者 setPaddingRelative(int, int, int, int) 来设置 Padding 值。通过调用 getPaddingLeft()getPaddingRight()getPaddingBotton()getPaddingTop()getPaddingStart()getPaddingEnd() 来获取 Padding 值。

View 类本身可以定义 Padding,但是不可以定义 Margin,不过 ViewGroup 可以定义 Margin。

Layout

Layout 可以分为两个部分:测量和布局。测量部分由 measure(int, int) 实现,并由上至下遍历整个 View 树。每个 View 通过递归的方式深度到 View 树的组底层,在测量过程的最后阶段,每个 View 都存储了各自的尺寸。布局部分由 layout(int, int, int, int) 实现,并且也是自上而下的遍历整个 View 树。在这个过程中,每个父 View 利用测量得到的值对其子 View 进行布局。

View 的 measure() 方法返回时,必须设置其 getMeasuredWidth()getMeasuredHeight() 方法的返回值将要被设置。View 测量的 Width 和 Height 值必须遵循其父 View 限定的值。这样保证了在测量过程结时,所有的父 View 都能满足其子 View 的尺寸。父 View 调用 measure() 方法的次数可能比其子 View 调用的次数多一次。例如,父 View 首先会测量其每一个子 View 所需要的大小,然后再调用一次 measure() 来确定所有的子 View 尺寸的大小总和是否太大或太小。

测量通过两个类来传递尺寸。View 使用 View.MeasureSpec 来告诉其父 View 如何被测量和定位。LayoutParams 类用来描述 View 对宽度和高度的要求。对于每个尺寸,它可以指定以下之一:

  • 一个精确的值
  • MATCH_PARENT:这意味着 View 想要和其父级 View(减去 padding 值)一样大。
  • WRAP_CONTENT:这意味着该 View 希望大小适合包含其内容(加上 padding 值)

对于不同的 ViewGroup 的子类,LayoutParams 也有响应的子类。例如,AbsoluteLayout 拥有 LayoutParams 的子类能够添加 X 值和 Y 值。

MeasureSpec 用于将树结构从父对象推送到子对象。MeasureSpec 可以采用以下三种模式:

  • UNSPECIFIED:父级用于确定子 View 所需尺寸。例如,LinearLayout 可以调用其子 View 的 measure(),其高度设置为 UNSPECIFIED,宽度设置为 EXACTLY 240,这样用来确定在宽度为 240 像素时子视图所需的高度。
  • EXACTLY:父视图指定子实体准确的大小。子视图必须使用该尺寸,而且要确保其后代也在该尺寸范围内。
  • AT_MOST:父视图指定一个子实体可用的最大值。子视图必须确保其后代必须在该尺寸范围内。

要初始化一个布局,请调用 requestLayout()。该方法通常由视图在认为其无法满足当前约束时自己调用。

Drawing

绘制是通过遍历整个控件树并且记录任何需要更新的 View 的绘图指令来处理的。在此之后,整个控件树的绘图指令被发送至屏幕,来替换需要更新的区域。

控件树的记录和绘制顺序大致是:父控件先于子控件绘制,同等级控件按照控件树中的顺序绘制。如果我们为一个 View 设置了 Drawable 背景,那么该 View 在调用其 onDraw() 方法之前绘制它。子 View 的绘制顺序可以通过对 ViewGroup 调用 setChildrenDrawingOrderEnabled(boolean)、重写 getChildDrawingOrder(int, int) 和设置 setZ(float) 来改变。

如果想要强行绘制 View,请调用 invalidate()方法

Event Handling and Threading

事件在 View 中最基本的流程如下:

  • 事件传入并分发给适合的 View,View 处理事件并通知相应的监听器
  • 如果在处理事件的过程中,View 的边界可能需要更改,视图将调用 requestLayout() 方法
  • 与之类似的,如果在处理事件的过程中,View 的外观需要更改,则 View 将调用 invalidate() 方法
  • 如果调用了 requestLayout() 或者 invalidate() 中任意一个方法,那么系统框架将会适当的对 View 进行测量、布局和绘制。

注意:整个 View 树是单线程的。在任何 View 中调用任何方法时,必须始终在 UI 线程上。如果您正在其他线程上工作,并且希望该线程更新 View,你可以使用 Handler

Focus Handling

系统通过处理日常焦点移动来响应用户交互,包括删除或隐藏或新增有效视图等改变焦点行为。视图通过 isFocusable() 方法表明是否能够接受焦点,setFocusable(boolean) 方法改变能否接受焦点的状态,isFocusableInTouchMode() 方法表明是否能够接受焦点(触摸模式下)和
setFocusableInTouchMode(boolean) 方法改变能否接受焦点的状态(触摸模式下)。

焦点移动是基于找到给定方向上最相邻的视图的算法。某些情况下,默认算法不能满足开发者的预期行为,需要在布局文件中用 XML 属性明确指出,比如:

nextFocusDown
nextFocusLeft
nextFocusRight
nextFocusUp

调用 requestFocus() 方法可以让任意指定的视图获取焦点。

Touch Mode

当用户通过键盘去导航用户界面时,需要将焦点方法诸如按钮之类的项目上,以便用户可以看到需要输入的内容。然而,如果是触摸设备,并且用户通过触摸和界面进行交互,则不再需要总是突出显示或者将焦点放在特定视图上。这激发了一种名为“触摸模式”的交互模式。

对于触摸设备,一旦用户触摸到了屏幕,设备就进入到“触摸模式”。只有那些调用 isFocusableInTouchMode() 返回 true 的视图可以获取焦点,比如:文本编辑控件。按钮等可触摸的视图,被触摸时不再获取焦点,只会触发点击监听器。

只要用户点击任意一个方向键,设备将退出“触摸模式”,找到一个 View 给予焦点,那样用户无需触摸屏幕便可于用户界面恢复交互。

触摸模式的状态由Activity保持,调用 isInTouchMode() 方法可以查看设备当前是否处于触摸模式。

Scrolling

系统为 View 滚动其内容提供了基本的支持,包括监听 X 和 Y 轴偏移量和绘制滚动条。查看 scrollBy(int, int),scrollTo(int, int) 和 awakenScrollBars() 获取更多信息。

Tags

和 ID 不同,标签不用于标识 View。标签本质上是提供与 View 相关联的额外信息。通常放在视图内部,用来方便地存储与 View 相关的信息,而不是作为一个独立的结构。

在 XML 布局中,可以将字符串设置给标签。一个标签时,使用 android:tag 属性;多个标签时,使用 <tag> 标签作为子元素。

<View ...
      android:tag="@string/mytag_value" />
<View ...>
    <tag android:id="@+id/mytag"
         android:value="@string/mytag_value" />
</View>

在代码中,可以通过调用 setTag(Object) 或者 setTag(int, Object) 方法设置任意对象给标签。

Themes

默认情况下,视图通过构造函数中传入的 Context 对象获取主题来创建自己。但是,可以在 XML 布局中设置 android:theme 属性或者通过代码向构造函数中传入 ContextThemeWrapper 对象来指定不同的主题。

当在 XML 中使用 android:theme 属性,被指定的主题优先于 Context 主题,而且作用视图自己包括子元素。

下面的例子中,所有的视图使用 Material dark 颜色样式创建。但是,由于被使用的那个覆盖主题只定义了一个属性子集,Context主题(例如Activity的主题)属性 android:colorAccent 的值保留了下来。

    <LinearLayout
             ...
             android:theme="@android:theme/ThemeOverlay.Material.Dark">
         <View ...>
     </LinearLayout>

Properties

View 类公开了一个 ALPHA 属性,以及几个与 transform 相关的属性,如 TRANSLATION_XTRANSLATION_Y,这些属性可以在 XML 文件中或者以 setter / getter 类似方法中使用。这些属性能够设置 View 渲染相关的持续性状态,通常与Animator结合起来展示动画。

Animation

从 Android 3.0 开始,动画视图的首选方法是使用 android.animation 包中的 API。 这些基于 Animator 的类更改了 View 对象的实际属性,例如 alpha 和 translationX。 这种行为与 3.0 之前的基于 Animation 的类相反,不单单只是在显示器上绘制视图。 特别是,ViewPropertyAnimator 类使这些 View 属性的动画化变得非常简单和高效。

如果使用 3.0 之前的 Animation 类来动画渲染视图。 您可以使用 setAnimation(Animation)startAnimation(Animation) 将 Animation 对象附加到 View。 动画可以随着时间的推移改变视图的 scale(缩放), rotation(旋转),translation(位移) 和 alpha(透明度)。 如果动画关联的视图有子视图,那么动画将影响节点往下的整个子树。 动画启动时,框架将重新绘制适当的视图直到动画完成。

Security

应用必须能够验证正在执行的行为合理且有用户的允许,比如:授予请求购买或者点击广告的权限。不幸的是,恶意程序可能会偷偷隐藏带有意图的控件来欺骗用户执行这些行为。作为补救措施,系统提供了触摸过滤机制,提高访问敏感功能的保护。

通过调用 setFilterTouchesWhenObscured(boolean) 方法或者设置 XML 布局中 android:filterTouchesWhenObscured 属性值为 true 来开启触摸过滤机制。开启后,当视图窗口被另外的可见窗口遮挡时,忽略接收到的触摸事件。因此,当 Toast、dialog 或者其它窗口出现在视图窗口之上时,那个视图不会接收到触摸事件。

重写 onFilterTouchEventForSecurity(MotionEvent) 方法实现自己的安全规则可以更精细地控制安全,也可以参考 FLAG_WINDOW_IS_OBSCURED

原文:View

Copyright© 2020-2022 li-xyz 冀ICP备2022001112号-1