ANDROID 八月 30, 2022

Android • View

文章字数 7.9k 阅读约需 14 mins. 阅读次数 0

引言

本篇将介绍我最近在对 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()

未完待续…


0%