引言
本篇将介绍我最近在对
Android
项目进行开发时,有关 View 方面的笔记分享。
在阅读本文前,你需要了解:
- View:视图,在 Android 中通常用于展示与用户进行交互的 Ui(User Interface),View 是所有 Android 现有布局中的最基础类,所有与 Ui 相关的视图均继承自 View;
- ViewGroup:视图组,通常由多个继承自 View 或另一 ViewGroup 的控件组成显示;
- Layout:布局,在 Android 中,Layout 是通常存放于 res 文件夹的 layout 目录中的由 XML 编写而成的文件,该文件用于对 Ui 进行绘制,并在 App 运行时进行读取渲染,最终呈现给用户。
本篇不会介绍 Jetpack Compose 技术,有关 Jetpack Compose 的技术与开发文档,请读者自行查阅Google Developer 官网。
View 显示 / 隐藏
背景
在 Android 开发中,有时我们需要通过后端返回的结果,对指定的View进行显示与隐藏。
如有以下场景:在用户首次打开淘宝时,向用户展示商品广告,在延时结束后,进入主页。
这个场景会带有点歧义,毕竟进入主页的方式可以是直接跳转,也可是在主页上将广告隐藏,这里我们默认是第二种操作逻辑。
即:广告页是直接覆盖在主页上,需要等待延时或用户手动关闭后隐藏。
常用方法
Android 开发中通常使用到的 View 显示隐藏值:
- XML设置:
<!-- Layout XML 文件中 -->
<View
android:layout_width="match_parent"
android:layout_height="wrap_content"
...
android:visibility="visible" />
<!-- 变量说明 -->
<!-- Controls the initial visibility of the view. -->
<attr name="visibility">
<!-- Visible on screen; the default value. -->
<enum name="visible" value="0" />
<!-- Not displayed, but taken into account during layout (space is left for it). -->
<enum name="invisible" value="1" />
<!-- Completely hidden, as if the view had not been added. -->
<enum name="gone" value="2" />
</attr>
- 代码设置:
// 可见
view.visibility = View.VISIBLE
// 不可见,占据 Layout 位置,父View 在绘制时依旧会绘制该 子View,并创建其实例。
view.visibility = View.INVISIBLE
// 不可见,不占据 Layout 位置,父View 在绘制时不会绘制该 子View,只会创建其实例。
view.visibility = View.GONE
Kotlin 携程 实现倒计时隐藏
假设 View 的可见度在
XML中初始为 INVISIBLE
,延时3秒后将 View 的可见度设置为 VISIBLE;
CoroutineScope(Dispatchers.Main).launch {
delay(3 * 1000)
view.visibility = View.VISIBLE
}
View.postDelay 实现倒计时隐藏
假设 View 的可见度在
XML中初始为 INVISIBLE
,延时3秒后将 View 的可见度设置为 VISIBLE;
view.apply {
postDelayed({
visibility = View.VISIBLE
}, 3 * 1000)
}
Handler 实现倒计时隐藏
假设 View 的可见度在
XML中初始为 INVISIBLE
,延时3秒后将 View 的可见度设置为 VISIBLE。
Handler(Looper.getMainLooper()).postDelayed({
view.visibility = View.VISIBLE
}, 3 * 1000)
遇到问题
场景:假设 View 的可见度在 XML中初始为 GONE
,延时3秒后将 View 的可见度设置为 VISIBLE。
- 当绘制复杂 Layout 布局时,使用 View.GONE 会发生间歇性的失效问题,失效频率为平均 5次 出现 3次。
尝试的解决方案:将 XML中初始为 GONE
修改为 INVISIBLE,后在代码中进行 VISIBLE 展示。
自定义 View
背景
在 Android 开发中,虽然原生的 Android 内核已经提供了许多开箱即用的控件,但若遇到一些负责的业务场景,通常还是需要开发者自行绘制适用于业务场景的自定义 View。
继承 View
最基础的自定义 View 需要实现两个构造函数方法:
第一构造方法 TestView(context: Context):用于代码使用;
第二构造方法 TestView(context: Context, attr: AttributeSet):用于 XML 中使用;
// 在代码中使用 TestView,会自动调用第一构造方法。
class TestView(context: Context) : View(context) {
// 若在 XML 中使用该自定义组件,则会自动调用该构造方法。
constructor(
context: Context,
attr: AttributeSet
) : this(context) {
// ...
}
}
自定义属性
每个控件都会有属性,自定义控件也是如此,在编写自定义属性时,通常会在 res/values 文件夹下新建一个 attrs.xml 文件用于统一管理,针对多个自定义控件,你也可以选择分开单文件管理。
<!-- attrs.xml -->
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- TestView 自定义属性 -->
<declare-styleable name="TestViewAttr">
<attr name="isShow" format="boolean" />
<attr name="text" format="string" />
<attr name="value" format="integer" />
<attr name="backgroundColor" format="color" />
<attr name="selectedValue">
<flag name="1" value="1" />
<flag name="2" value="2" />
<flag name="3" value="3" />
</attr>
<!-- ... -->
</declare-styleable>
</resources>
使用:
<io.dev.myapplication.TestView
android:layout_width="200dp"
android:layout_height="200dp"
android:background="@color/black"
app:isShow="true"
app:selectedValue="2"
app:text="Hello World"
app:value="10" />
属性值类型 format
- reference:引用资源id
<!-- 属性定义 -->
<attr name="reference" format="reference" />
<!-- 属性使用 -->
<io.dev.myapplication.TestView
...
app:reference="@drawable/图片id" />
- color:颜色值
<!-- 属性定义 -->
<attr name="backgroundColor" format="color" />
<!-- 使用 -->
<io.dev.myapplication.TestView
...
app:backgroundColor="@color/black" />
- boolean:布尔类型值
<!-- 属性定义 -->
<attr name="isShow" format="boolean" />
<!-- 使用 -->
<io.dev.myapplication.TestView
...
app:isShow="true" />
- dimension:尺寸
<!-- 属性定义 -->
<attr name="dimen" format="dimension" />
<!-- 使用 -->
<io.dev.myapplication.TestView
...
app:dimen="100dp" />
- float:浮点数类型值
<!-- 属性定义 -->
<attr name="value" format="float" />
<!-- 使用 -->
<io.dev.myapplication.TestView
...
app:value="2.0" />
- integer:整数类型值
<!-- 属性定义 -->
<attr name="value" format="integer" />
<!-- 使用 -->
<io.dev.myapplication.TestView
...
app:value="100" />
- string:字符串类型值
<!-- 属性定义 -->
<attr name="text" format="string" />
<!-- 使用 -->
<io.dev.myapplication.TestView
...
app:value="Hello World" />
- fraction:百分数类型值
<!-- 属性定义 -->
<attr name="percent" format="fraction" />
<!-- 使用 -->
<io.dev.myapplication.TestView
...
app:percent="80%" />
- enum:枚举类型
若使用枚举类型,则值只能选择枚举列表中的其中一项,不可多选,如:horizontal|vertical。
<!-- 属性定义 -->
<attr name="orientation" format="enum">
<enum name="horizontal" value="horizontal" />
<enum name="vertical" value="vertical" />
</attr>
<!-- 使用 -->
<io.dev.myapplication.TestView
...
app:orientation="vertical" />
- flag:位或运算
位运算类型的属性在使用过程中可以使用多个值。
<!-- 属性定义 -->
<attr name="gravity">
<flag name="top" value="0x01" />
<flag name="bottom" value="0x02" />
<flag name="left" value="0x04" />
<flag name="right" value="0x08" />
<flag name="center_vertical" value="0x16" />
...
</attr>
<!-- 使用 -->
<io.dev.myapplication.TestView
...
app:gravity="bottom|left" />
- 属性混合
<!-- 属性定义 -->
<attr name="gravity">
<attr name="background" format="reference|color" />
</attr>
<!-- 使用 -->
<io.dev.myapplication.TestView
...
app:background="@drawable/图片id" />
<!-- 或 -->
<io.dev.myapplication.TestView
...
app:color="#FFFFFF" />
View 绘制流程
有关 View 的基本绘制,通常使用到 measure(),layout(),draw() 三个函数完成。
函数 | 作用 | 相关方法 |
---|---|---|
measure() | 测量View的宽高 | measure(),setMeasuredDimension(),onMeasure() |
layout() | 计算当前View以及子View的位置 | layout(),onLayout(),setFrame() |
draw() | 视图的绘制工作 | draw(),onDraw() |
Measure()
- MeasureSpec
MeasureSpec 是 View 的内部类,用于封装 View 的尺寸,在 onMeasure() 方法中会根据 MeasureSpec 的值来确定 View 的宽高。
MeasureSpec = mode + size,由于 MeasureSpec 的值类型是 int 类型,一个 int 值类型有 32位,前两位用于表示模式 mode,后 30位 用于表示大小 size。
在 MeasureSpec 的源码中,共有三种 Mode:
/**
* Measure specification mode: The parent has not imposed any constraint
* on the child. It can be whatever size it wants.
*/
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
/**
* Measure specification mode: The parent has determined an exact size
* for the child. The child is going to be given those bounds regardless
* of how big it wants to be.
*/
public static final int EXACTLY = 1 << MODE_SHIFT;
/**
* Measure specification mode: The child can be as large as it wants up
* to the specified size.
*/
public static final int AT_MOST = 2 << MODE_SHIFT;
对于 View来说,MeasureSpec 的 mode 和 size 有如下意义
模式 | 意义 | 对应 |
---|---|---|
EXACTLY | 精准模式,View需要一个精确值,这个值即为MeasureSpec当中的Size | match_parent |
AT_MOST | 最大模式,View的尺寸有一个最大值,View不可以超过MeasureSpec当中的Size值 | wrap_content |
UNSPECIFIED | 无限制,View对尺寸没有任何限制,View设置为多大就应当为多大 | 一般系统内部使用 |
使用方式
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(300, 300)
} else if (widthMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(300, heightSize)
} else if (heightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSize, 300)
}
}
- layout()
- draw()
未完待续…