Android 开发从入门到实战

第1章 Android开发环境搭建

本章介绍了如何在个人电脑上搭建Android开发环境,主要包括:Android开发的发展历史是怎样的、

Android Studio的开发环境是如何搭建的、如何创建并编译App工程、如何运行和调试App。

.1 Android开发简介

本节介绍Android开发的历史沿革,包括Android的发展历程和Android Studio的发展历程两个方面。

Android的发展历程

安卓(Android)是一种基于Linux内核(不包含GNU组件)的自由及开放源代码的操作系统。主要使用于移动设备,如智能手机和平板电脑,由美国Google公司和开放手机联盟领导及开发。Android操作系统最初由Andy Rubin开发,主要支持手机。

2005年8月由Google收购注资。

2007年11月,Google与84家硬件制造商、软件开发商及电信营运商组建开放手机联盟共同研发改 良Android系统,并发布了Android的源代码。

第一部Android智能手机发布于2008年10月,由 HTC 公司制造。Android逐渐扩展到平板电脑及其他领域上,如电视、数码相机、游戏机、智能手表、车载大屏、智能家居等,并逐渐成为了人们 日常生活中不可或缺的系统软件。

2011年第一季度,Android在全球的市场份额首次超过塞班系统,跃居全球第一。

2013年的第四季度,Android平台手机的全球市场份额已经达到78.1%。2013年09月24日谷歌开 发的操作系统Android在迎来了5岁生日,全世界采用这款系统的设备数量已经达到10亿台。

2019年,谷歌官方宣布全世界有25亿活跃的Android设备,还不包含大多数中国设备。

Android几乎每年都要发布一个大版本,技术的更新迭代非常之快,表1-1展示了Android几个主要版本的发布时间。

表1-1 Android主要版本的发布时间

Android 版本号对应 API发布时间
Android 13332022年2月
Android 12312021年10月
Android 11302020年9月
Android 10292019年8月
Android 9282018年8月
Android 826/272017年8月
Android 724/252016年8月
Android 6232015年9月
Android 521/222014年6月
Android 4.419/202013年9月

Android Studio的发展历程

虽然Android基于Linux内核,但是Android手机的应用App主要采用Java语言开发。为了吸引众多的Java 程序员,早期的App开发工具使用Eclipse,通过给Eclipse安装ADT插件,使之支持开发和调试App。然 而Eclipse毕竟不是专门的App开发环境,运行速度也偏慢,因此谷歌公司在2013年5月推出了全新的

Android开发环境—Android Studio。Android Studio基于IntelliJ IDEA演变而来,既保持了IDEA方便快捷的特点,又增加了Android开发的环境支持。自2015年之后,谷歌公司便停止了ADT的版本更新,转而重点打造自家的Android Studio,数年升级换代下来,Android Studio的功能愈加丰富,性能也愈高效,使得它逐步成为主流的App开发环境。表1-2展示了、几个主要版本的发布时间。

表1-2 Android Studio主要版本的发布时间

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9ggD3FCh-1676725567985)(media/b73ea5c9db031e20d1c26712ea4cb91c.jpeg)]

.2 搭建Android Studio开发环境

本节介绍在电脑上搭建Android Studio开发环境的过程和步骤,首先说明用作开发机的电脑应当具备哪些基本配置,接着描述了Android Studio的安装和配置详细过程,然后叙述了如何下载Android开发需要的SDK组件及相关工具。

开发机配置要求

工欲善其事,必先利其器。要想保证Android Studio的运行速度,开发用的电脑配置就要跟上。现在一般用笔记本电脑开发App,下面是对电脑硬件的基本要求:

  1. 内存要求至少8GB,越大越好。

  2. CPU要求1.5GHz以上,越快越好。

  3. 硬盘要求系统盘剩余空间10GB以上,越大越好。

  4. 要求带无线网卡与USB插槽。

    下面是对操作系统的基本要求(以Windows为例)。

  5. 必须是64位系统,不能是32位系统。

  6. Windows系统至少为Windows 7,推荐Windows 10,不支持Windows XP。

    下面是对网络的基本要求:

  7. 最好连接公众网,因为校园网可能无法访问国外的网站。

  8. 下载速度至少每秒1MB,越快越好。因为Android Studio安装包大小为1GB左右,还需要另外下载几百MB的SDK,所以网络带宽一定要够大,否则下载文件都要等很久。

安装Android Studio

Android Studio的官方下载页面是https://developer.android.google.cn/studio/index.html,单击网页中央的DOWNLOAD按钮即可下载Android Studio的安装包。或者下拉网页找到“Android Studio

downloads”区域,选择指定操作系统对应的Android Studio安装包。

双击下载完成的Android Studio安装程序,弹出安装向导对话框,如图1-1所示。直接单击Next按钮, 进入下一页的组件选择对话框,如图1-2所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V0gb6jiL-1676725567986)(media/6432a27cbcff4cdbd33741bb1e73c25e.jpeg)]

图1-1 Android Studio的安装向导

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-isNusdWy-1676725567987)(media/9c2e8b71090ea16d1651d89f85e857d3.jpeg)]

图1-2 勾选Android Studio的安装组件

勾选Android Studio和Android Virtual Device两个选项,然后单击Next按钮,进入下一页的安装路径对话框,如图1-3所示。建议将Android Studio安装在除系统盘外的其他磁盘(比如E盘),然后单击Next 按钮,进入下一页的开始菜单设置对话框,如图1-4所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i98PIjUB-1676725567988)(media/098e96ae1d2b28979c560cec67ed925c.jpeg)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yIjGMPm9-1676725567989)(media/1db170a4c60d58a228f4b1d7074ea26a.jpeg)]图1-3 选择Android Studio的安装目录

图1-4 设置Android Studio的开始菜单

单击右下角的Install按钮,跳到下一页的安装过程对话框,耐心等待安装操作,安装过程的界面如图1-5 所示。单击Next按钮进入完成对话框,如图1-6所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V9KXhkPj-1676725567990)(media/074fddff638717caa3274c69cae004d0.jpeg)]

图1-5 Android Studio的安装过程对话框

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qDSv4y44-1676725567991)(media/566d114f533830aeb1496abbbfe6eb66.jpeg)]

图1-6 Android Studio的完成安装对话框

勾选完成对话框的“Start Android Studio”选项,再单击右下角的Finish按钮,结束安装操作的同时启动Android Studio。稍等片刻Android Studio启动之后会打开如图1-7所示的配置向导对话框。单击Next 按钮进入下一页的安装类型对话框,如图1-8所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hwHYnF4C-1676725567992)(media/7582df06ba0b402039120a1186c81906.jpeg)]

图1-7 Android Studio的配置向导对话框

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RjjRiS19-1676725567992)(media/5a33944fe7fb5f1edef3b139e423555e.jpeg)]

图1-8 Android Studio的安装类型对话框

这里保持Standard选项,单击Next按钮,跳到下一页的界面,如图1-9所示。选中右边的Light主题,表 示开发界面采取白底黑字,然后单击Next按钮,跳到下一页的设置确认对话框,如图1-10所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jcruG4Ew-1676725567993)(media/3ca8c350cf124d25819e675409f6d343.jpeg)]

图1-9 Android Studio的对话框

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tReD18pQ-1676725567994)(media/990013d249b437e8265a13f8e5f4d434.jpeg)]

图1-10 Android Studio的设置确认对话框

设置确认对话框列出了需要下载哪些工具及其大小,确认完毕后继续单击Next按钮,跳到下一页的组件下载对话框,如图1-11所示。耐心等待组件下载操作,全部下载完成后,该对话框提示成功更新,如图1-12所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x6MnUTjR-1676725567994)(media/ef186f2b82605f6174e0a3a94caa4cb1.jpeg)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-71r4ugAQ-1676725567995)(media/b89fb044f3f19a0dc615792d620ed25d.jpeg)]图1-11 Android Studio的组件下载对话框

图1-12 Android Studio的更新完成对话框

单击对话框右下角的Finish按钮,完成安装配置工作,同时打开Android Studio欢迎界面,如图1-13所示。单击第一项的“Start a new Android Studio project”即可开始你的Android开发之旅。

另外注意,配置过程可能发生如下错误提示:

  1. 第一次打开Android Studio可能会报Unable to access Android SDK add-on list错误信息,这个界面不用理会,单击Cancel按钮即可。进入Android Studio主界面后,依次选择菜单File→Project Structure→SDK Location,在弹出的对话框中设置SDK的路径。设置完毕后再打开Android Studio就不会报错了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-COXOKPwZ-1676725567995)(media/8f6c06daff09687d705e959fa064ec9e.jpeg)]

图1-13 Android Studio的欢迎界面

  1. 已经按照安装步骤正确安装,运行Android Studio时却总是打不开。这时请检查电脑上是否开启了防火墙,建议关闭系统防火墙及所有杀毒软件的防火墙。关闭了防火墙后再重新打开Android Studio重试。

下载Android的SDK

Android Studio只提供了App的开发环境界面,编译App源码还需另外下载Android官方的SDK,上一小节中的图1-10便展示了初始下载安装的SDK工具包。SDK全称为Software Development Kit,意即软件开发工具包,它可将App源码编译为可执行的App应用。随着Android版本的更新换代,SDK也需时常在 线升级,接下来介绍如何下载最新的SDK。

在Android Studio主界面,依次选择菜单Tools→SDK Manager,或者在Android Studio右上角中单击图标,如图1-14所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UXz8h60n-1676725567996)(media/dfc23813c49d1716608804160e811449.jpeg)]

图1-14 打开SDK Manager的图标栏

此时弹出SDK Manager的管理界面,窗口右边是SDK安装配置区域,初始画面如图1-15所示。注意Android SDK Location一栏,可单击右侧的Edit链接,进而选择SDK下载后的保存路径。其下的三个选项卡默认显示SDK Platforms,也就是各个SDK平台的版本列表,勾选每个列表项左边的复选框,表示需要下载该版本的SDK平台,然后单击OK按钮即可自动下载并安装SDK。也可单击中间SDK Tools选项

卡,此时会切换到SDK工具的管理列表,如图1-16所示。在这个工具管理界面,能够在线升级编译工具

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xg57ljeW-1676725567996)(media/ff905ba739fd84a6c473acd2f1e8c77e.jpeg)]Build Tools、平台工具Platform Tools,以及开发者需要的其他工具。

图1-15 SDK平台的管理列表

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yNoz2eks-1676725567998)(media/2f0f8f513bad89ca9146f22eb5bcd715.jpeg)]

图1-16 SDK工具的管理列表

SDK下载完成,可以到“我的电脑”中打开Android SDK Location指定的SDK保存路径,发现下面还有十几个目录,其中比较重要的几个目录说明如下:

build-tools目录,存放各版本Android的编译工具。

emulator目录,存放模拟器的管理工具。

platforms目录,存放各版本Android的资源文件与内核JAR包android.jar。

platform-tools目录,存放常用的开发辅助工具,包括客户端驱动程序adb.exe、数据库管理工具sqlite3.exe,等等。

sources目录,存放各版本Android的SDK源码。

.3 创建并编译App工程

本节介绍使用Android Studio创建并编译App工程的过程和步骤,首先叙述了如何通过Android Studio 创建新的App项目,接着描述了如何导入已有的App工程(包括导入项目和导入模块两种方式),然后阐述了如何手工编译App工程。

创建新项目

在“1.2.2 安装Android Studio”小节最后一步出来的图1-13中,单击第一项的Start a new Android Studio project会创建初始的新项目。如果要创建另外的新项目,也可在打开Android Studio之后,依次选择菜单File→New→New Project。以上两种创建方式都会弹出如图1-17所示的项目创建对话框,在该对话框中选择第一行第四列的“Empty Activity”,单击Next按钮跳到下一个配置对话框如图1-18所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Gc2dpI2Y-1676725567998)(media/4edcf452cc0ff18d7feb6dca8f189c39.jpeg)]

图1-17 创建新项目

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I1OUkE98-1676725567998)(media/84e825303af62ea48ee5de2d5d499eff.jpeg)]

图1-18 指定目标设备

在配置对话框的Name栏输入应用名称,在Package Name栏输入应用的包名,在Save Location栏输入或者选择项目工程的保存目录,在Language下拉框中选择编码语言为Java,在Minimun SDK下拉框中选择最低支持到“API19:Android 4.4(KitKat)”,Minimun SDK下方的文字提示当前版本支持设备的市场份额为98.1%。下面有个复选框“User legacy android.support libraries”,如果勾选表示采用旧的

support支持库,如果不勾选表示采用新的androidx库,因为Android官方不再更新旧的support库,所 以此处无须勾选,默认采用新的androidx库就可以了。

然后单击Finish按钮完成配置操作,Android Studio便自动创建规定配置的新项目了。稍等片刻, Android Studio将呈现刚创建好的项目页面,如图1-19所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uFqeMWZ4-1676725567998)(media/84f46a70eba56f32050ca4c02415c1aa.jpeg)]

图1-19 刚刚创建的新项目页面

工程创建完毕后,Android Studio自动打开activity_main.xml与MainActivity.java,并默认展示

MainActivity.java的源码。MainActivity.java上方的标签表示该文件的路径结构,注意源码左侧有一列标签,从上到下依次是Project、Resource Manager、Structure、Build Variants、Favorites。单击

Project标签,左侧会展开小窗口表示该项目工程的目录结构,如图1-20所示。单击Structure标签,左 侧会展开小窗口表示该代码的内部方法结构,如图1-21所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-s6TymkGu-1676725568000)(media/fddea816b9e33b9966e46fc3b45057ef.jpeg)]

图1-20 新项目的工程结构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Rnawve3e-1676725568000)(media/00531e3e3eb4f35ceca1c12fdf574ce0.jpeg)]

导入已有的工程

图1-21 MainActivity的方法结构

本书提供了所有章节的示例源码,为方便学习,读者可将本书源码直接导入Android Studio。根据App

工程的组织形式,有两种源码导入方式,分别是导入整个项目,以及导入某个模块,简要说明如下。

导入整个项目

以本书源码MyApp为例,依次选择菜单File→Open,或者依次选择菜单File→New→Import Project, 均会弹出如图1-22所示的文件对话框。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yDWbPva6-1676725568001)(media/8e48e82b1e4f8f79aa2540fdbc2df454.jpeg)]

图1-22 打开App项目的文件对话框

在文件对话框中选中待导入的项目路径,再单击对话框下方的OK按钮。此时文件对话框关闭,弹出另一 个如图1-23所示的确认对话框。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3cxxdV5o-1676725568001)(media/4e1f7a3fdc71766652f76b1024115b07.jpeg)]

图1-23 是否开启新窗口的确认对话框

确认对话框右下角有3个按钮,分别是This Window、New Window和Cancel,其中This Window按钮表

示在当前窗口打开该项目,New Window按钮表示在新窗口打开该项目,Cancel按钮表示取消打开操作。此处建议单击New Window按钮,即可在新窗口打开App项目。

导入某个模块

如果读者已经创建了自己的项目,想在当前项目导入某章的源码,应当通过Module方式导入模块源码。依次选择菜单File→New→Import Module,弹出如图1-24所示的导入对话框。

单击Source Directory输入框右侧的文件夹图标,弹出如图1-25所示的文件对话框。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8V9gEGS3-1676725568002)(media/af2b1232b1a955f709133969ed4c9ccd.jpeg)]

图1-24 导入模块的对话框

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zZ3WQHXL-1676725568003)(media/3a9e23c9c87762c8d917c23eefe4816b.jpeg)]

图1-25 选择模块的文件对话框

在文件对话框中选择待导入的模块路径,再单击对话框下方的OK按钮,回到如图1-26所示的导入对话框。

可见导入对话框已经自动填上了待导入模块的完整路径,单击对话框右下角的Finish按钮完成导入操 作。然后Android Studio自动开始模块的导入和编译动作,等待导入结束即可在Android Studio左上角的项目结构图中看到导入的chapter02模块,如图1-27所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4wROPsts-1676725568003)(media/34580403528043b7ef83b1a247b11aee.jpeg)]

图1-26 填写模块路径的对话框

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VLek8rSX-1676725568003)(media/e02e2bf53657db077f74a1a2231b01b1.jpeg)]

图1-27 成功导入模块之后的项目结构图

编译App工程

Android Studio跟IDEA一样,被改动的文件会自动保存,无须开发者手工保存。它还会自动编译最新的代码,如果代码有误,编辑界面会标红提示出错了。但是有时候可能因为异常关闭的缘故,造成 Android Studio的编译文件发生损坏,此时需要开发者手动重新编译,手动编译有以下3种途径:

  1. 依次选择菜单Build→Make Project,该方式会编译整个项目下的所有模块。

  2. 依次选择菜单Build→Make Module ***,该方式会编译指定名称的模块。

  3. 先选择菜单Build→Clean Project,再选择菜单Build→Rebuild Project,表示先清理当前项目,再对整个项目重新编译。

    不管是编译项目还是编译模块,编译结果都展示在Android Studio主界面下方的Build窗口中,如图1-28

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kEsBueht-1676725568004)(media/39ff861b7779742af54eb559dfdb6b10.jpeg)]所示。

    图1-28 App工程的编译结果窗口

    由编译结果可知,当前项目编译耗时2分29秒,共发现了1个警告,未发现错误。

.4 运行和调试App

本节介绍使用Android Studio运行和调试App的过程,首先叙述了如何创建Android Studio内置的模拟器,接着描述了如何在刚创建的模拟器上运行测试App,然后阐述了如何在Android Studio中查看App 的运行日志。

创建内置模拟器

所谓模拟器,指的是在电脑上构造一个演示窗口,模拟手机屏幕的App运行效果。App通过编译之后, 只说明代码没有语法错误,若想验证App能否正确运行,还得让它在Android设备上跑起来。这个设备可以是真实手机,也可以是电脑里的模拟器。依次选择菜单Run→Run (也可按快捷键Shift+F10),或者选择菜单Run→Run…,在弹出的小窗中选择待运行的模块名称,Android Studio会判断当前是否存在已经连接的设备,如果已有连接上的设备就在该设备上安装测试App。

因为一开始没有任何已连上的设备,所以运行App会报错“Error running :No target device found.”, 意思是未找到任何目标设备。此时要先创建一个模拟器,依次选择菜单Tools→AVD Manager,或者在Android Studio右上角的按钮中单击图标,如图1-29所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oPnwn9fK-1676725568004)(media/251f159f2be252bf97d7f1d2df7f4abe.jpeg)]

图1-29 打开AVD Manager的图标栏此时Android Studio打开模拟器的创建窗口,如图1-30所示。

单击创建窗口中的Create Virtual Device按钮,弹出如图1-31所示的硬件选择对话框。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3yMQtbPu-1676725568005)(media/da5e7b46149d2dd9439e5663b8b0d767.jpeg)]

图1-30 模拟器的创建窗口

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qppE9Ycq-1676725568005)(media/dcd21fad86eeb4a87d00386a1cbe9404.jpeg)]

图1-31 硬件选择对话框

在对话框的左边列表单击Phone表示选择手机,在中间列表选择某个手机型号如Pixel 2,然后单击对话框右下角的Next按钮,跳到下一页的系统镜像选择对话框如图1-32所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TAt4zvL1-1676725568005)(media/c5c4d7d1b9a4f1dca6dc55190a0aac4b.jpeg)]

图1-32 系统镜像选择对话框

看到镜像列表顶端的发布名称叫R,对应的API级别为30,它正是Android 11的系统镜像。单击R右边的

Download链接,弹出如图1-33所示的许可授权对话框。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K4hqsgcP-1676725568006)(media/30575137617234ec577fe4294a56fb47.jpeg)]

图1-33 许可授权对话框

单击许可授权对话框的Accept选项,表示接受上述条款,再单击Next按钮跳到下一页的镜像下载对话框,如图1-34所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tVrOOeSj-1676725568006)(media/fcc5f9c8fbe0131fc2ef948d53d3abc8.jpeg)]

图1-34 镜像下载对话框

等待镜像下载完成,单击右下角的Finish按钮,返回到如图1-35所示的系统镜像选择对话框。

此时R右边的Download链接消失,说明电脑中已经存在该版本的Android镜像。于是选中R这行,再单 击Next按钮,跳到模拟器的配置对话框如图1-36所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BE4OeaGJ-1676725568007)(media/cbbefc9e7fdcfcb60ec45837d0eefed2.jpeg)]

图1-35 系统镜像选择对话框

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y8ubpHHU-1676725568007)(media/2842506c2242b15db3988d1ffb6f3cb7.jpeg)]

图1-36 模拟器的配置对话框

配置对话框左上方的AVD Name用于填写模拟器的名称,这里保持默认名称不动,单击对话框右下角的

Finish按钮完成创建操作。一会儿对话框关闭,回到如图1-37所示的模拟器列表对话框,可见多了个名为Pixel 2 API 30的模拟器,且该模拟器基于Android 11(API 30)。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aRmrGxw5-1676725568008)(media/fbd5705dafdfba4bb34a16bd0511465e.jpeg)]

图1-37 模拟器的列表对话框

在模拟器上运行App

模拟器创建完成后,回到Android Studio的主界面,即可在顶部工具栏的下拉框中发现多了个“Pixel 2 API 30”,它正是上一小节创建好的模拟器,如图1-38所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TryEUrZW-1676725568008)(media/fde144e8205d1f471a670d1b9e021426.jpeg)]

图1-38 顶部工具栏出现刚创建的模拟器

重新选择菜单Run→Run ‘app’,也可直接单击“Pixel 2 API 30”右侧的三角运行按钮,Android Studio便开始启动名为“Pixel 2 API 30”的模拟器,如图1-39所示。等待模拟器启动完毕,出现模拟器的开机画面如图1-40所示。再过一会儿,模拟器自动打开如图1-41所示的App界面。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wDgFodQF-1676725568008)(media/c25690054ad1a71666b8c2f7d0a644da.jpeg)]

图1-39 模拟器正在启动

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TWlwRKL9-1676725568009)(media/4b8c15f329913f9d689eaba1f0a79be3.jpeg)]

图1-40 模拟器的开机画面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h2NpoWO2-1676725568009)(media/afa15259c7c28a3f603e1534e0b53162.jpeg)]

图1-41 模拟器运行App

可见模拟器屏幕左上角的应用名称为MyApp,页面内容为Hello World!它正是刚才想要运行的测试

App,说明已经在模拟器上成功运行App了。

观察App的运行日志

虽然在模拟器上能够看到App的运行,却无法看到App的调试信息。以前写Java代码的时候,通过

System.out.println可以很方便地向IDEA的控制台输出日志,当然Android Studio也允许查看App的运行日志,只是Android不使用System.out.println,而是采用Log工具打印日志。

有别于System.out.println,Log工具将各类日志划分为5个等级,每个等级的重要性是不一样的,这些 日志等级按照从高到低的顺序依次说明如下:

Log.e:表示错误信息,比如可能导致程序崩溃的异常。Log.w:表示警告信息。

Log.i: 表 示 一 般 消 息 。 Log.d:表示调试信息,可把程序运行时的变量值打印出来,方便跟踪调试。Log.v:表示冗余信息。

一般而言,日常开发使用Log.d即可,下面是给App添加日志信息的代码例子:

(完整代码见app\src\main\java\com\example\app\MainActivity.java)

重新运行测试App,等模拟器刷新App界面后,单击Android Studio底部的“Logcat”标签,此时主界面下方弹出一排日志窗口,如图1-42所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aUYV0OVM-1676725568010)(media/73106d1c58cb5540000910889404023c.jpeg)]

图1-42 Android Studio的日志查看窗口

日志窗口的顶部是一排条件筛选框,从左到右依次为:测试设备的名称(如“Pixel_2_API_30”)、测试

App的包名(例如只显示com.example.myapp的日志)、查看日志的级别(例如只显示级别不低于Debug即Log.d的日志)、日志包含的字符串(例如只显示包含MainActivity的日志),还有最后一个是 筛选控制选项(其中“Show only selected application”表示只显示选中的应用日志,而“No Filters”则表示不过滤任何条件)。一排条件筛选之后,logcat窗口只显示一行“D/MainActivity:我看到你了”,说明成 功捕获前面代码调用Log.d的日志信息。

.5 小结

本章主要介绍了Android开发环境的搭建过程,包括:Android开发简介(Android的发展历程、Android Studio的发展历程)、搭建Android Studio开发环境(开发机配置要求、安装Android Studio、下载Android的SDK)、创建并编译App工程(创建新项目、导入已有的工程、编译App工 程)、运行和调试App(创建内置模拟器、在模拟器上运行App、观察App的运行日志)。

通过本章的学习,读者应该掌握Android Studio的基本操作技能,能够使用自己搭建的Android Studio

环境创建简单的App工程,并在模拟器上成功运行测试App。

.6 课后练习题

一、填空题
  1. Android是基于 的移动端开源操作系统。
  2. Android系统是由 公司推出的。3.Android 11对应的API编号是 。

4.App除了在手机上运行,还能在电脑的 上运行。5.Android Studio创建模拟器的管理工具名为 。

二、判断题(正确打√,错误打×)

1.第一部Android手机由诺基亚制造。( ) 2.Android Studio由Eclipse演变而来。( ) 3.Android Studio只能在64位操作系统上运行。( )

  1. 运行App指的是运行某个模块,而非运行某个项目。( )
  2. App可以在电脑上直接运行。( )
三、选择题
  1. 智能手机的两大操作系统是( )。

A.Android B.iOS C.Symbian D.Windows

  1. 下列哪些设备可以运行Android系统( )。
  2. 智能手机B.平板电脑C.智能电视D.车载大屏
  3. Android提供的App专用开发工具包名为( )。

A.JDK B.NDK C.SDK D.SSH

  1. Android App开发主要使用的编程语言是( )。

    A.C/C++

  2. Java C.Python D.Swift

  3. 打印调试级别的日志方法名为( )。

A.Log.e B.Log.w C.Log.i

D.Log.d

四、简答题

请列出导入App工程的几种方式。

五、动手练习

请上机实验搭建App的开发环境,主要步骤说明如下:

  1. 下载并安装Android Studio的最新版本。
  2. 创建一个新的App项目“Hello World”。
  3. 使用Android Studio创建一个模拟器。
  4. 在模拟器上安装并运行第二步创建的App,观察能否看到“Hello World”字样。

第2章 Android App开发基础

本章介绍基于Android系统的App开发常识,包括以下几个方面:App开发与其他软件开发有什么不一 样,App工程是怎样的组织结构又是怎样配置的,App开发的前后端分离设计是如何运作实现的,App的活动页面是如何创建又是如何跳转的。

.1 App的开发特点

本节介绍了App开发与其他软件开发不一样的特点,例如:App能在哪些操作系统上运行、App开发用到了哪些编程语言、App能操作哪些数据库等,搞清楚了App的开发运行环境,才能有的放矢不走弯路。

App的运行环境

App是在手机上运行的一类应用软件,而应用软件依附于操作系统,无论电脑还是手机,刚开机都会显 示桌面,这个桌面便是操作系统的工作台。个人电脑的操作系统主要有微软的Windows和苹果的Mac

OS,智能手机流行的操作系统也有两种,分别是安卓手机的Android和苹果手机的iOS。本书讲述的App

开发为Android上的应用开发,Android系统基于Linux内核,但不等于Linux系统,故App应用无法在

Linux系统上运行。

Android Studio是谷歌官方推出的App开发环境,它提供了三种操作系统的安装包,分别是Windows、

Mac和Linux。这就产生一个问题:开发者可以在电脑上安装Android Studio,并使用Android Studio开发App项目,但是编译出来的App在电脑上跑不起来。这种情况真是令人匪夷所思的,通常学习C语言、

Java或者Python,都能在电脑的开发环境直接观看程序运行过程,就算是J2EE开发,也能在浏览器通过 网页观察程序的运行结果。可是安卓的App应用竟然没法在电脑上直接运行,那该怎样验证App的界面 展示及其业务逻辑是否正确呢?

为了提供App开发的功能测试环境,一种办法是利用Android Studio创建内置的模拟器,然后启动内置模拟器,再在模拟器上运行App应用,详细步骤参见第一章的“1.4.2 在模拟器上运行App”。

另一种办法是使用真实手机测试App,该办法在实际开发中更为常见。由于模拟器本身跑在电脑上面, 占用电脑的CPU和内存,会拖累电脑的运行速度;况且模拟器仅仅是模拟而已,无法完全验证App的所有功能,因此最终都得通过真机测试才行。

利用真机调试要求具备以下5个条件:

使用数据线把手机连到电脑上

手机的电源线拔掉插头就是数据线。数据线长方形的一端接到电脑的USB接口,即可完成手机与电脑的连接。

在电脑上安装手机的驱动程序

一般电脑会把手机当作USB存储设备一样安装驱动,大多数情况会自动安装成功。如果遇到少数情况安装失败,需要先安装手机助手,由助手软件下载并安装对应的手机驱动。

打开手机的开发者选项并启用USB调试

手机出厂后默认关闭开发者选项,需要开启开发者选项才能调试App。打开手机的设置菜单,进入“系统”

→“关于手机”→“版本信息”页面,这里有好几个版本项,每个版本项都使劲点击七、八下,总会有某个版本点击后出现“你将开启开发者模式”的提示。继续点击该版本开启开发者模式,然后退出并重新进入设 置页面,此时就能在“系统”菜单下找到“开发者选项”或“开发人员选项”了。进入“开发者选项”页面,启用

“开发者选项”和“USB调试”两处开关,允许手机通过USB接口安装调试应用。

将连接的手机设为文件传输模式,并允许计算机进行USB调试

手机通过USB数据线连接电脑后,屏幕弹出如图2-1所示的选择列表,请求选择某种USB连接方式。这里记得选中“传输文件”,因为充电模式不支持调试App。

选完之后手机桌面弹出如图2-2所示的确认窗口,提示开发者是否允许当前计算机进行USB调试。这里勾选“始终允许使用这台计算机进行调试”选项,再点击右下角的确定按钮,允许计算机在手机上调试App。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3JZkcQGn-1676725568012)(media/47499d9d8590caf2f15bf58ddac0c1ce.jpeg)]

图2-1 USB连接方式选择列表

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EAb8KjvA-1676725568015)(media/69cbb5122fff706c30dedfd3bd8e712d.jpeg)]

图2-2 USB调试的确认对话框

手机要能正常使用

锁屏状态下,Android Studio向手机安装App的行为可能会被拦截,所以要保证手机处于解锁状态,才能顺利通过电脑安装App到手机上。

有的手机还要求插入SIM卡才能调试App,还有的手机要求登录会员才能调试App,总之如果遇到无法安装的问题,各种情况都尝试一遍才好。

经过以上步骤,总算具备通过电脑在手机上安装App的条件了。马上启动Android Studio,在顶部中央的执行区域看到已连接的手机信息,如图2-3所示。此时的设备信息提示这是一台华为手机,单击手机名 称右边的三角运行按钮,接下来就是等待Android Studio往手机上安装App了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w3Eea9Js-1676725568015)(media/4aec558463d6328b2e5e0158e0dae394.jpeg)]

图2-3 找到已连接的真机设备

App的开发语言

基于安卓系统的App开发主要有两大技术路线,分别是原生开发和混合开发。原生开发指的是在移动平 台上利用官方提供的编程语言(例如Java、Kotlin等)、开发工具包(SDK)、开发环境(Android

Studio)进行App开发;混合开发指的是结合原生与H5技术开发混合应用,也就是将部分App页面改成内嵌的网页,这样无须升级App、只要覆盖服务器上的网页,即可动态更新App页面。

不管是原生开发还是混合开发,都要求掌握Android Studio的开发技能,因为混合开发本质上依赖于原生开发,如果没有原生开发的皮,哪里还有混合开发的毛呢?单就原生开发而言,又涉及多种编程语 言,包括Java、Kotlin、C/C++、XML等,详细说明如下。

Java

Java是Android开发的主要编程语言,在创建新项目时,弹出如图2-4所示的项目配置对话框,看见

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yeyjeWUV-1676725568015)(media/ca28929ab93d5a6235ba641373cb0137.jpeg)]Language栏默认选择了Java,表示该项目采用Java编码。

图2-4 创建新项目时候的项目配置页面(Java)

虽然Android开发需要Java环境,但没要求电脑上必须事先安装JDK,因为Android Studio已经自带了JRE。依次选择菜单File→Project Structure,弹出如图2-5所示的项目结构对话框。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aAAPPpaJ-1676725568015)(media/644a4cce64eb772fb725f12f48a486ad.jpeg)]

图2-5 项目结构的配置窗口

单击项目结构对话框左侧的SDK Location,对话框右边从上到下依次排列着“Android SDK location”、“Android NDK location”、“JDK location”,其中下方的JDK location提示位于Android Studio安装路径的

JRE目录下,它正是Android Studio自带的Java运行环境。

可是Android Studio自带的JRE看不出来基于Java哪个版本,它支不支持最新的Java版本呢?其实Android Studio自带的JRE默认采用Java 7编译,如果在代码里直接书写Java 8语句就会报错,比如Java 8 引入了Lambda表达式,下面代码通过Lambda表达式给整型数组排序:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OdtHWNnn-1676725568017)(media/823105e1ca72d18b6f985e08242445b9.jpeg)]倘若由Android Studio编译上面代码,结果提示出错“Lambda expressions are not supported at language level ‘7’”,意思是Java 7不支持Lambda表达式,错误信息如图2-6所示。

图2-6 不支持Lambda表达式的出错提示

原来Android Studio果真默认支持Java 7而非Java 8,但Java 8增添了诸多新特性,其拥趸与日俱增,有的用户习惯了Java 8,能否想办法让Android Studio也支持Java 8呢?当然可以,只要略施小计便可,依次选择菜单File→Project Structure,在弹出的项目结构对话框左侧单击Modules,此时对话框如图2-7 所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RIgJlIUz-1676725568017)(media/c6a33dd01511a299338ecbede9bf43a8.jpeg)]

图2-7 模块的属性设置对话框

对话框右侧的Properties选项卡,从上到下依次排列着“Compile Sdk Version”、“Build Tool Version”、“NDK Version”、“Source Compatibility”、“Target Compatibility”,这5项分别代表:编译的SDK版本、构建工具的版本、编译C/C++代码的NDK版本、源代码兼容性、目标兼容性,其中后面两项用来设置

Java代码的兼容版本。单击“Source Compatibility”右边的下拉箭头按钮,弹出如图2-8所示的下拉列表。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HIotqoF7-1676725568018)(media/e85e6a230c9f70f06c591d4e0c950514.jpeg)]

图2-8 源码兼容性的Java版本选择列表

从下拉列表中看到,Android Studio自带的JRE支持Java 6、Java 7、Java 8三种版本。单击选中列表项的“1.8(Java 8)”,并在“Target Compatibility”栏也选择“1.8(Java 8)”,然后单击窗口下方的OK按钮, 就能将编译模块的Java版本改成Java 8了。

Kotlin

Kotlin是谷歌官方力推的又一种编程语言,它与Java同样基于JVM(Java Virtual Machine,即Java虚拟机),且完全兼容Java语言。创建新项目时,在Language栏下拉可选择Kotlin,此时项目结构对话框如 图2-9所示。

一旦在创建新项目时选定Kotlin,该项目就会自动加载Kotlin插件,并将Kotlin作为默认的编程语言。不 过本书讲述的App开发采用Java编程,未涉及Kotlin编程。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JS5TJFua-1676725568019)(media/578b3e0686ceb5c83153a03cbfb78b46.jpeg)]

图2-9 创建新项目时的项目配置对话框(Kotlin)

3.C/C++

不管是Java还是Kotlin,它们都属于解释型语言,这类语言在运行之时才将程序翻译成机器语言,故而执 行效率偏低。虽然现在手机配置越来越高,大多数场景的App运行都很流畅,但是涉及图像与音视频处 理等复杂运算的场合,解释型语言的性能瓶颈便暴露出来。

编译型语言在首次编译时就将代码编译为机器语言,后续运行无须重新编译,直接使用之前的编译文件 即可,因此执行效率比解释型语言高。C/C++正是编译型语言的代表,它能够有效弥补解释型语言的性 能缺憾,借助于JNI技术(Java Native Interface,即Java原生接口),Java代码允许调用C/C++编写的程序。事实上,Android的SDK开发包内部定义了许多JNI接口,包括图像读写在内的底层代码均由 C/C++编写,再由外部通过封装好的Java方法调用。

不过Android系统的JNI编程属于高级开发内容,初学者无须关注JNI开发,也不要求掌握C/C++。

4.XML

XML全称为Extensible Markup Language,即可扩展标记语言,严格地说,XML并非编程语言,只是一种标记语言。它类似于HTML,利用各种标签表达页面元素,以及各元素之间的层级关系及其排列组

合。每个XML标签都是独立的控件对象,标签内部的属性以“android:”打头,表示这是标准的安卓属性,各属性分别代表控件的某种规格。比如下面是以XML书写的文本控件:

上面的标签名称为TextView,翻译过来叫文本视图,该标签携带4个属性,说明如下:

id:控件的编号。

layout_width:控件的布局宽度,wrap_content表示刚好包住该控件的内容。

layout_height:控件的布局高度,wrap_content表示刚好包住该控件的内容。text:控件的文本,也就是文本视图要显示什么文字。

综合起来,以上XML代码所表达的意思为:这是一个名为tv_hello的文本视图,显示的文字内容是“Hello World!”,它的宽度和高度都要刚好包住这些文字。

以上就是Android开发常见的几种编程语言,本书选择了Java路线而非Kotlin路线,并且定位安卓初学者 教程,因此读者需要具备Java和XML基础。

App连接的数据库

在学习Java编程的时候,基本会学到数据库操作,通过JDBC连接数据库进行记录的增删改查,这个数据 库可能是MySQL,也可能是Oracle,还可能是SQL Server。然而手机应用不能直接操作上述几种数据库,因为数据库软件也得像应用软件那样安装到操作系统上,比如MySQL提供了Windows系统的安装包,也提供了Linux系统的安装包,可是它没有提供Android系统的安装包呢,所以MySQL没法在Android系统上安装,手机里面的App也就不能直连MySQL。

既然MySQL、Oracle这些企业数据库无法在手机安装,那么App怎样管理业务方面的数据记录呢?其实

Android早已内置了专门的数据库名为SQLite,它遵循关系数据库的设计理念,SQL语法类似于

MySQL。不同之处在于,SQLite无须单独安装,因为它内嵌到应用进程当中,所以App无须配置连接信 息,即可直接对其增删改查。由于SQLite嵌入到应用程序,省去了配置数据库服务器的开销,因此它又被归类为嵌入式数据库。

可是SQLite的数据库文件保存在手机上,开发者拿不到用户的手机,又该如何获取App存储的业务数

据?比如用户的注册信息、用户的购物记录,等等。如果像Java Web那样,业务数据统一保存在后端的数据库服务器,开发者只要登录数据库服务器,就能方便地查询导出需要的记录信息。

手机端的App,连同程序代码及其内置的嵌入式数据库,其实是个又独立又完整的程序实体,它只负责 手机上的用户交互与信息处理,该实体被称作客户端。而后端的Java Web服务,包括Web代码和数据库服务器,同样构成另一个单独运行的程序实体,它只负责后台的业务逻辑与数据库操作,该实体被称作 服务端。客户端与服务端之前通过HTTP接口通信,每当客户端觉得需要把信息发给服务端,或者需要从服务端获取信息时,客户端便向服务端发起HTTP请求,服务端收到客户端的请求之后,根据规则完成数据处理,并将处理结果返回给客户端。这样客户端经由HTTP接口并借服务端之手,方能间接读写后端的数据库服务器(如MySQL),具体的信息交互过程如图2-10所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OdQTyrJy-1676725568019)(media/755697d8d6bd7a9e97978e3a2b6d0d50.jpeg)]

图2-10 客户端与服务端分别操作的数据库

由此看来,一个具备用户管理功能的App系统,实际上并不单单只是手机上的一个应用,还包括与其对 应的Java Web服务。手机里的客户端App,面向的是手机用户,App与用户之间通过手机屏幕交互;而后端的服务程序,面向的是手机App,客户端与服务端之间通过HTTP接口交互。客户端和服务端这种多对一的架构关系如图2-11所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-97hkaQxc-1676725568020)(media/74972ff3316645fa9942ac1f20900faa.jpeg)]

图2-11 客户端与服务端的多对一架构关系图

总结一下,手机App能够直接操作内置的SQLite数据库,但不能直接操作MySQL这种企业数据库。必须 事先搭建好服务端程序(如Java Web),然后客户端与服务端通过HTTP接口通信,再由服务端操作以

MySQL为代表的数据库服务器。

.2 App的工程结构

本节介绍App工程的基本结构及其常用配置,首先描述项目和模块的区别,以及工程内部各目录与配置 文件的用途说明;其次阐述两种级别的编译配置文件build.gradle,以及它们内部的配置信息说明;再次讲述运行配置文件AndroidManifest.xml的节点信息及其属性说明。

App工程目录结构

App工程分为两个层次,第一个层次是项目,依次选择菜单File→New→New Project即可创建新项目。另一个层次是模块,模块依附于项目,每个项目至少有一个模块,也能拥有多个模块,依次选择菜单File→New→New Module即可在当前项目创建新模块。一般所言的“编译运行App”,指的是运行某个模块,而非运行某个项目,因为模块才对应实际的App。单击Android Studio左上角竖排的Project标签, 可见App工程的项目结构如图2-12所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TaCof1Vo-1676725568021)(media/649139d5a998f91f4aef0b211b321653.jpeg)]

图2-12 App工程的项目结构图

从图2-12中看到,该项目下面有两个分类:一个是app(代表app模块);另一个是Gradle Scripts。其中,app下面又有3个子目录,其功能说明如下:

  1. manifests子目录,下面只有一个XML文件,即AndroidManifest.xml,它是App的运行配置文件。

  2. java子目录,下面有3个com.example.myapp包,其中第一个包存放当前模块的Java源代码,后面两个包存放测试用的Java代码。

  3. res子目录,存放当前模块的资源文件。res下面又有4个子目录:

    drawable目录存放图形描述文件与图片文件。

    layout目录存放App页面的布局文件。

    mipmap目录存放App的启动图标。

    values目录存放一些常量定义文件,例如字符串常量strings.xml、像素常量dimens.xml、颜色常 量colors.xml、样式风格定义styles.xml等。

    Gradle Scripts下面主要是工程的编译配置文件,主要有:

  4. build.gradle,该文件分为项目级与模块级两种,用于描述App工程的编译规则。

  5. proguard-rules.pro,该文件用于描述Java代码的混淆规则。

  6. gradle.properties,该文件用于配置编译工程的命令行参数,一般无须改动。

  7. settings.gradle,该文件配置了需要编译哪些模块。初始内容为include ‘:app’,表示只编译app模块。

  8. local.properties,项目的本地配置文件,它在工程编译时自动生成,用于描述开发者电脑的环境 配置,包括SDK的本地路径、NDK的本地路径等。

编译配置文件build.gradle

新创建的App项目默认有两个build.gradle,一个是Project项目级别的build.gradle;另一个是Module 模块级别的build.gradle。

项目级别的build.gradle指定了当前项目的总体编译规则,打开该文件在buildscript下面找到

repositories和dependencies两个节点,其中repositories节点用于设置Android Studio插件的网络仓库地址,而dependencies节点用于设置gradle插件的版本号。由于官方的谷歌仓库位于国外,下载速度 相对较慢,因此可在repositories节点添加阿里云的仓库地址,方便国内开发者下载相关插件。修改之后的buildscript节点内容如下所示:

模块级别的build.gradle对应于具体模块,每个模块都有自己的build.gradle,它指定了当前模块的详细 编译规则。下面给chapter02模块的build.gradle补充文字注释,方便读者更好地理解每个参数的用途。

(完整代码见chapter02\build.gradle)

为啥这两种编译配置文件的扩展名都是Gradle呢?这是因为它们采用了Gradle工具完成编译构建操作。

Gradle工具的版本配置在gradle\wrapper\gradle-wrapper.properties,也可以依次选择菜单File→Project Structure→Project,在弹出的设置页面中修改Gradle Version。注意每个版本的Android

Studio都有对应的Gradle版本,只有二者的版本正确对应,App工程才能成功编译。比如Android Studio 4.1对应的Gradle版本为6.5,更多的版本对应关系见https://developer.android.google.cn/studi o/releases/gradle-plugin#updating-plugin。

运行配置文件AndroidManifest.xml

AndroidManifest.xml指定了App的运行配置信息,它是一个XML描述文件,初始内容如下所示:

(完整代码见chapter02\src\main\AndroidManifest.xml)

可见AndroidManifest.xml的根节点为manifest,它的package属性指定了该App的包名。manifest下 面有个application节点,它的各属性说明如下:

android:allowBackup,是否允许应用备份。允许用户备份系统应用和第三方应用的apk安装包和 应用数据,以便在刷机或者数据丢失后恢复应用,用户即可通过adb backup和adb restore来进行对应用数据的备份和恢复。为true表示允许,为false则表示不允许。 android:icon, 指 定 App 在 手 机 屏 幕 上 显 示 的 图 标 。 android:label,指定App在手机屏幕上显示的名称。

android:roundIcon,指定App的圆角图标。

android:supportsRtl,是否支持阿拉伯语/波斯语这种从右往左的文字排列顺序。为true表示支 持 , 为 false 则 表 示 不 支 持 。 android:theme,指定App的显示风格。

注意到application下面还有个activity节点,它是活动页面的注册声明,只有在AndroidManifest.xml中正确配置了activity节点,才能在运行时访问对应的活动页面。初始配置的MainActivity正是App的默认 主页,之所以说该页面是App主页,是因为它的activity节点内部还配置了以下的过滤信息:

其中action节点设置的android.intent.action.MAIN表示该页面是App的入口页面,启动App时会最先打开该页面。而category节点设置的android.intent.category.LAUNCHER决定了是否在手机屏幕上显示

App图标,如果同时有两个activity节点内部都设置了android.intent.category.LAUNCHER,那么桌面就 会显示两个App图标。以上的两种节点规则可能一开始不太好理解,读者只需记住默认主页必须同时配 置这两种过滤规则即可。

.3 App的设计规范

本节介绍了App工程的源码设计规范,首先App将看得见的界面设计与看不见的代码逻辑区分开,然后利用XML标记描绘应用界面,同时使用Java代码书写程序逻辑,从而形成App前后端分离的设计规约, 有利于提高App集成的灵活性。

界面设计与代码逻辑

手机的功能越来越强大,某种意义上相当于微型电脑,比如打开一个电商App,仿佛是在电脑上浏览网 站。网站分为用户看得到的网页,以及用户看不到的Web后台;App也分为用户看得到的界面,以及用 户看不到的App后台。虽然Android允许使用Java代码描绘界面,但不提倡这么做,推荐的做法是将界面 设计从Java代码剥离出来,通过单独的XML文件定义界面布局,就像网站使用HTML文件定义网页那样。 直观地看,网站的前后端分离设计如图2-13所示,App的前后端分离设计如图2-14所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OkFu2sem-1676725568021)(media/2213c60204460144f45a06f5342d5635.jpeg)]

图2-13 网站的前后端分离设计

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rlFUehMV-1676725568021)(media/3e919deeb62569baed6b3685070bc0f4.jpeg)]

图2-14 App的前后端分离设计

把界面设计与代码逻辑分开,不仅参考了网站的Web前后端分离,还有下列几点好处。

  1. 使用XML文件描述App界面,可以很方便地在Android Studio上预览界面效果。比如新创建的App 项目,默认首页布局为activity_main.xml,单击界面右上角的Design按钮,即可看到如图2-15所示的预 览界面。

如果XML文件修改了Hello World的文字内容,立刻就能在预览区域观看最新界面。倘若使用Java代码描绘界面,那么必须运行App才能看到App界面,无疑费时许多。

  1. 一个界面布局可以被多处代码复用,比如看图界面,既能通过商城购物代码浏览商品图片,也能通 过商品评价代码浏览买家晒单。

  2. 反过来,一段Java代码也可能适配多个界面布局,比如手机有竖屏与横屏两种模式,默认情况App 采用同一套布局,然而在竖屏时很紧凑的界面布局(见图2-16),切换到横屏往往变得松垮乃至变形

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Fk5pPfHP-1676725568022)(media/d033c8f7c242d99694d823b2f0c4fd30.jpeg)](见图2-17)。

    图2-15 XML文件的预览界面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H5SlXwhs-1676725568022)(media/eafcdfa2d80bc1f9051aee201f53630d.png)]

图2-16 竖屏时候的界面布局

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KEWG3gQz-1676725568023)(media/eae15b10429b57b0211883d7f32bce44.png)]

图2-17 横屏时候的界面布局

鉴于竖屏与横屏遵照一样的业务逻辑,仅仅是屏幕方向不同,若要调整的话,只需分别给出竖屏时候的 界面布局,以及横屏时候的界面布局。因为用户多数习惯竖屏浏览,所以res/layout目录下放置的XML 文件默认为竖屏规格,另外在res下面新建名为layout-land的目录,用来存放横屏规格的XML文件。

land是landscape的缩写,意思是横向,Android把layout-land作为横屏XML的专用布局目录。然后在layout-land目录创建与原XML同名的XML文件,并重新编排界面控件的展示方位,调整后的横屏界面如 图2-18所示,从而有效适配了屏幕的水平方向。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cmkGMuSe-1676725568025)(media/6138494119fa4b9a8021b992fd2c68e4.png)]

图2-18 采用另一个XML文件的横屏布局

总的来说,界面设计与代码逻辑分离的好处多多,后续的例程都由XML布局与Java代码两部分组成。

利用XML标记描绘应用界面

在前面“2.1.2 App的开发语言”末尾,给出了安卓控件的XML定义例子,如下所示:

注意到TextView标签以“<”开头,以“/>”结尾,为何尾巴多了个斜杆呢?要是没有斜杆,以左右尖括号包 裹标签名称,岂不更好?其实这是XML的标记规范,凡是XML标签都由标签头与标签尾组成,标签头以左右尖括号包裹标签名称,形如“”;标签尾在左尖括号后面插入斜杆,以此同标签头区分开,形如“”。标签头允许在标签名称后面添加各种属性取值,而标签尾不允许添加任何属性,因此上述TextView标签的完整XML定义是下面这样的:

考虑到TextView仅仅是个文本视图,其标签头和标签尾之间不会插入其他标记,所以合并它的标签头和标签尾,也就是让TextView标签以“/>”结尾,表示该标签到此为止。

然而不是所有情况都能采取简化写法,简写只适用于TextView控件这种末梢节点。好比一棵大树,大树先有树干,树干分岔出树枝,一些大树枝又分出小树枝,树枝再长出末端的树叶。一个界面也是先有根 节点(相当于树干),根节点下面挂着若干布局节点(相当于树枝),布局节点下面再挂着控件节点

(相当于树叶)。因为树叶已经是末梢了,不会再包含其他节点,所以末梢节点允许采用“/>”这种简写 方式。

譬如下面是个XML文件的布局内容,里面包含了根节点、布局节点,以及控件节点:

(完整代码见chapter02\src\main\res\layout\activity_main.xml)

上面的XML内容,最外层的LinearLayout标签为该界面的根节点,中间的LinearLayout标签为布局节 点,最内层的TextView为控件节点。由于根节点和布局节点都存在下级节点,因此它们要有配对的标签头与标签尾,才能将下级节点包裹起来。根节点其实是特殊的布局节点,它的标签名称可以跟布局节点 一样,区别之处在于下列两点:

  1. 每个界面只有一个根节点,却可能有多个布局节点,也可能没有中间的布局节点,此时所有控件节 点都挂在根节点下面。
  2. 根节点必须配备“xmlns:android=“http://schemas.android.com/apk/res/android””,表示指定

XML内部的命名空间,有了这个命名空间,Android Studio会自动检查各节点的属性名称是否合法,如果不合法就提示报错。至于布局节点就不能再指定命名空间了。

有了根节点、布局节点、控件节点之后,XML内容即可表达丰富多彩的界面布局,因为每个界面都能划分为若干豆腐块,每个豆腐块再细分为若干控件罢了。三种节点之外,尚有“”这类注释标记,它的作用 是包裹注释性质的说明文字,方便其他开发者理解此处的XML含义。

使用Java代码书写程序逻辑

在XML文件中定义界面布局,已经明确是可行的了,然而这只是静态界面,倘若要求在App运行时修改文字内容,该当如何是好?倘若是动态变更网页内容,还能在HTML文件中嵌入JavaScript代码,由js片 段操作Web控件。但Android的XML文件仅仅是布局标记,不能再嵌入其他语言的代码了,也就是说, 只靠XML文件自身无法动态刷新某个控件。

XML固然表达不了复杂的业务逻辑,这副重担就得交给App后台的Java代码了。Android Studio每次创建新项目,除了生成默认的首页布局activity_main.xml之外,还会生成与其对应的代码文件MainActivity.java。赶紧打开MainActivity.java,看看里面有什么内容,该Java文件中MainActivity类的 内容如下所示:

可见MainActivity.java的代码内容很简单,只有一个MainActivity类,该类下面只有一个onCreate方 法。注意onCreate内部的setContentView方法直接引用了布局文件的名字activity_main,该方法的意

思是往当前活动界面填充activity_main.xml的布局内容。现在准备在这里改动,把文字内容改成中文。 首先打开activity_main.xml,在TextView节点下方补充一行android:id=“@+id/tv_hello”,表示给它起 个名字编号;然后回到MainActivity.java,在setContentView方法下面补充几行代码,具体如下:

(完整代码见chapter02\src\main\java\com\example\chapter02\MainActivity.java)

新增的两行代码主要做了这些事情:先调用findViewById方法,从布局文件中取出名为tv_hello的

TextView控件;再调用控件对象的setText方法,为其设置新的文字内容。

代码补充完毕,重新运行测试App,发现应用界面变成了如图2-19所示的样子。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aPXe4IlC-1676725568026)(media/e1621b5a64d9ed970d43581062bec44d.jpeg)]

图2-19 修改控件文本后的界面效果可见使用Java代码成功修改了界面控件的文字内容。

.4 App的活动页面

本节介绍了App活动页面的基本操作,首先手把手地分三步创建新的App页面,接着通过活动创建菜单快速生成页面源码,然后说明了如何在代码中跳到新的活动页面。

创建新的App页面

每次创建新的项目,都会生成默认的activity_main.xml和MainActivity.java,它们正是App首页对应的

XML文件和Java代码。若要增加新的页面,就得由开发者自行操作了,完整的页面创建过程包括3个步骤:创建XML文件、创建Java代码、注册页面配置,分别介绍如下:

创建XML文件

在Android Studio左上方找到项目结构图,右击res目录下面的layout,弹出如图2-20所示的右键菜单。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fb1POGNw-1676725568027)(media/172f19c09435d8ce57b42a9d88f36362.jpeg)]

图2-20 通过右键菜单创建XML文件

在右键菜单中依次选择New→XML→Layout XML File,弹出如图2-21所示的XML创建对话框。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8WUuvCnT-1676725568027)(media/8ae1b99f7f691172b062328b79be7af0.jpeg)]

图2-21 XML文件的创建窗口

在XML创建对话框的Layout File Name输入框中填写XML文件名,例如activity_main2,然后单击窗口右下角的Finish按钮。之后便会在layout目录下面看到新创建的XML文件activity_main2.xml,双击它即可打开该XML的编辑窗口,再往其中填写详细的布局内容。

创建Java代码

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NhGEIeMv-1676725568028)(media/0e7287c01062bfbaecac3d35c42e1d17.jpeg)]同样在Android Studio左上方找到项目结构图,右击java目录下面的包名,弹出如图2-22所示的右键菜单。

图2-22 通过右键菜单创建Java代码

在右键菜单中依次选择New→Java Class,弹出如图2-23所示的代码创建窗口。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TKmmWNsF-1676725568028)(media/d53f8834c9279906223271ea9d3abf52.jpeg)]

图2-23 Java代码的创建窗口

在代码创建窗口的Name输入框中填写Java类名,例如Main2Activity,然后单击窗口下方的OK按钮。之 后便会在Java包下面看到新创建的代码文件Main2Activity,双击它即可打开代码编辑窗口,再往其中填 写如下代码,表示加载来自activity_main2的页面布局。

(完整代码见chapter02\src\main\java\com\example\chapter02\Main2Activity.java)

注册页面配置

创建好了页面的XML文件及其Java代码,还得在项目中注册该页面,打开AndroidManifest.xml,在

application节点内部补充如下一行配置:

添加了上面这行配置,表示给该页面注册身份,否则App运行时打开页面会提示错误“activity not

found”。如果activity的标记头与标记尾中间没有其他内容,则节点配置也可省略为下面这样:

完成以上3个步骤后,才算创建了一个合法的新页面。

快速生成页面源码

上一小节经过创建XML文件、创建Java代码、注册页面配置3个步骤,就算创建好了一个新页面,没想到区区一个页面也这么费事,怎样才能提高开发效率呢?其实Android Studio早已集成了快速创建页面的功能,只要一个对话框就能完成所有操作。

仍旧在项目结构图中,右击java目录下面的包名,弹出如图2-24所示的右键菜单。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7hDE7rF3-1676725568029)(media/5284853d7227da9bd02b26187e052111.jpeg)]

图2-24 通过右键菜单创建活动页面

右键菜单中依次选择New→Activity→Empty Activity,弹出如图2-25所示的页面创建对话框。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1D3RIXGj-1676725568029)(media/b85d25b884eca84b98453c8a3fb585c2.jpeg)]

图2-25 活动页面的创建窗口

在页面创建对话框的Activity Name输入框中填写页面的Java类名(例如Main2Activity),此时下方的Layout Name输入框会自动填写对应的XML文件名(例如activity_main2),单击对话框右下角的Finish 按钮,完成新页面的创建动作。

回到Android Studio左上方的项目结构图,发现res的layout目录下多了个activity_main2.xml,同时

java目录下多了个Main2Activity,并且Main2Activity代码已经设定了加载activity_main2布局。接着打 开AndroidManifest.xml,找到application节点发现多了下面这行配置:

检查结果说明,只要填写一个创建页面对话框,即可实现页面创建的3个步骤。

跳到另一个页面

一旦创建好新页面,就得在合适的时候跳到该页面,假设出发页面为A,到达页面为B,那么跳转动作是 从A跳到B。由于启动App会自动打开默认主页MainActivity,因此跳跃的起点理所当然在MainActivity, 跳跃的终点则为目标页面的Activity。这种跳转动作翻译为Android代码,格式形如“startActivity(new

Intent(源页面.this, 目标页面.class));”。如果目标页面名为Main2Activity,跳转代码便是下面这样的:

因为跳转动作通常发生在当前页面,也就是从当前页面跳到其他页面,所以不产生歧义的话,可以使用

this指代当前页面。简化后的跳转代码如下所示:

接下来做个实验,准备让App启动后在首页停留3秒,3秒之后跳到新页面Main2Activity。此处的延迟处 理功能,用到了Handler工具的postDelayed方法,该方法的第一个参数为待处理的Runnable任务对 象,第二个参数为延迟间隔(单位为毫秒)。为此在MainActivity.java中补充以下的跳转处理代码:

(完整代码见chapter02\src\main\java\com\example\chapter02\MainActivity.java)

运行测试App,刚打开的App界面如图2-26所示,过了3秒发生跳转事件的App界面如图2-27所示,可见 成功跳到了新页面。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P3AG1gzJ-1676725568031)(media/77afa8c6d29232519e324232b9ba3916.jpeg)]

图2-26 跳转之前的App界面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FtYWhYUx-1676725568031)(media/ff58802a55b0c7bfdbe318e1f44775eb.jpeg)]

图2-27 跳转之后的App界面

当然,以上的跳转代码有些复杂,比如:Intent究竟是什么东西?为何在onResume方法中执行跳转动作?Handler工具的处理机制是怎样的?带着这些疑问,后续章节将会逐渐展开,一层一层拨开Android 开发的迷雾。

.5 小结

本章主要介绍了App开发必须事先掌握的基础知识,包括App的开发特点(App的运行环境、App的开发语言、App访问的数据库)、App的工程结构(App工程的目录结构、编译配置文件build.gradle、运行 配置文件AndroidManifest.xml)、App的设计规范(界面设计与代码逻辑、利用XML标记描绘应用界 面、使用Java代码书写程序逻辑)、App的活动页面(创建新的App页面、快速生成页面源码、跳转到另一个页面)。

通过本章的学习,读者应该了解App开发的基本概念,并且熟悉App工程的组织形式,同时掌握使用

Android Studio完成一些简单操作。

.6 课后练习题

一、填空题
  1. 除了开启开发者选项之外,还需打开手机上的 _ 开关,然后才能在手机上调试App。
  2. App开发的两大技术路线包括 _和混合开发。
  3. App工程的编译配置文件名为 _。
  4. Android Studio使用 _ 工具完成App工程的构建操作。
  5. 在Java代码中调用 _ 方法能够跳到新的App页面。
二、判断题(正确打√,错误打×)
  1. Android Studio默认支持到Java 8。( )
  2. Kotlin语言也能用于App开发。( )
  3. App属于服务端程序。( )
  4. 一个App项目可以包含多个App模块。( )
  5. App工程的图片资源放在layout目录下。( )
三、选择题
  1. 通过( )可以连接手机和电脑。

A.HDM接口B.光驱

C.USB接口D.音频接口

  1. 如果手机无法安装调试App,可能是哪个原因造成的( )。
    1. 处于锁屏状态
    2. 未 插 SIM 卡 C. 未 登 录 会 员 D.选择了充电模式
  2. App可以直接连接的数据库是( )。
    1. MySQL
    2. Oracle C.SQLite D.SQL Server
  3. App界面布局采用的文件格式是( )。

A.CSS B.FXML C.HTML D.XML

  1. 下面的( )属性表示TextView标签的控件编号。
    1. id
    2. layout_width C.layout_height D.text
四、简答题

请简要描述App开发过程中分离界面设计与代码逻辑的好处。

五、动手练习

请上机实验修改App工程的XML文件和Java代码,并使用真机调试App,主要步骤说明如下:

  1. 创建一个新的App项目。
  2. 修改项目级别的build.gradle,添加阿里云的仓库地址。
  3. 创建一个名为Main2Activity的新页面(含XML文件与Java代码)。
  4. 在该页面的XML文件中添加一个TextView标签,文本内容为“你好,世界!”。
  5. 在MainActivity的Java代码中添加页面跳转代码,从当前页跳到Main2Activity。
  6. 把App安装到手机上并运行,观察能否看到“你好,世界!”字样。

第3章 简单控件

本章介绍了App开发常见的几类简单控件的用法,主要包括:显示文字的文本视图、容纳视图的常用布 局、响应点击的按钮控件、显示图片的图像视图等。然后结合本章所学的知识,演示了一个实战项目“简 单计算器”的设计与实现。

.1 文本显示

本节介绍了如何在文本视图TextView上显示规定的文本,包括:怎样在XML文件和Java代码中设置文本 内容,尺寸的大小有哪些单位、又该怎样设置文本的大小,颜色的色值是如何表达的、又该怎样设置文 本的颜色。

设置文本的内容

在前一章的“2.3.3 使用Java代码书写程序逻辑”小节,给出了设置文本内容的两种方式,一种是在XML 文件中通过属性android:text设置文本,比如下面这样:

(完整代码见chapter03\src\main\res\layout\activity_text_view.xml)

另一种是在Java代码中调用文本视图对象的setText方法设置文本,比如下面这样:

(完整代码见chapter03\src\main\java\com\example\chapter03\TextViewActivity.java)

在XML文件中设置文本的话,把鼠标移到“你好,世界”上方时,Android Studio会弹出如图3-1所示的提示框。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IRGZEmbP-1676725568031)(media/717716ae12f2c7057fb4b41ba9be1f1a.jpeg)]

图3-1 XML文件提示字符串硬编码

看到提示内容为“Hardcoded string “你好,世界”, should use @string resouce”,意思说这几个字是硬编码的字符串,建议使用来自@string的资源。原来Android Studio不推荐在XML布局文件里直接写字符串,因为可能有好几个页面都显示“你好,世界”,若想把这句话换成“你吃饭了吗?”,就得一个一个XML 文件改过去,无疑费时费力。故而Android Studio推荐把字符串放到专门的地方管理,这个名为@string 的地方位于res/values目录下的strings.xml,打开该文件发现它的初始内容如下所示:

看来strings.xml定义了一个名为“app_name”的字符串常量,其值为“chapter03”。那么在此添加新的字符串定义,字符串名为“hello”,字符串值为“你好,世界”,添加之后的strings.xml内容如下所示:

添加完新的字符串定义,回到XML布局文件,将android:text属性值改为“@string/字符串名”这般,也就 是“@string/hello”,修改之后的TextView标签示例如下:

然后把鼠标移到“你好,世界”上方,此时Android Studio不再弹出任何提示了。

若要在Java代码中引用字符串资源,则调用setText方法时填写形如“R.string.字符串名”的参数,就本例 而言填入“R.string.hello”,修改之后的Java代码示例如下:

至此不管XML文件还是Java代码都从strings.xml引用字符串资源,以后想把“你好,世界”改为其他文字 的话,只需改动strings.xml一个地方即可。

设置文本的大小

TextView允许设置文本内容,也允许设置文本大小,在Java代码中调用setTextSize方法,即可指定文本大小,就像以下代码这样:

(完整代码见chapter03\src\main\java\com\example\chapter03\TextSizeActivity.java)

这里的大小数值越大,则看到的文本也越大;大小数值越小,则看到的文本也越小。在XML文件中则通 过属性android:textSize指定文本大小,可是如果给TextView标签添加“android:textSize=“30””,数字马 上变成红色如图3-2所示,鼠标移过去还会提示错误“Cannot resolve symbol ‘30’”,意思是无法解析“30” 这个符号。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3zc5T6MS-1676725568031)(media/e2f0e34ef37de133b9dc1bcc0eb1c664.jpeg)]

图3-2 textSize属性值只填数字时报错

原来文本大小存在不同的字号单位,XML文件要求在字号数字后面写明单位类型,常见的字号单位主要有px、dp、sp 3种,分别介绍如下。

px

px是手机屏幕的最小显示单位,它与设备的显示屏有关。一般来说,同样尺寸的屏幕(比如6英寸手机),如果看起来越清晰,则表示像素密度越高,以px计量的分辨率也越大。

dp

dp有时也写作dip,指的是与设备无关的显示单位,它只与屏幕的尺寸有关。一般来说,同样尺寸的屏 幕以dp计量的分辨率是相同的,比如同样是6英寸手机,无论它由哪个厂家生产,其分辨率换算成dp单 位都是一个大小。

sp

sp的原理跟dp差不多,但它专门用来设置字体大小。手机在系统设置里可以调整字体的大小(小、标 准、大、超大)。设置普通字体时,同数值dp和sp的文字看起来一样大;如果设置为大字体,用dp设置的文字没有变化,用sp设置的文字就变大了。

字体大小采用不同单位的话,显示的文字大小各不相同。例如,30px、30dp、30sp这3个字号,在不同手机上的显示大小有所差异。有的手机像素密度较低,一个dp相当于两个px,此时30px等同于15dp; 有的手机像素密度较高,一个dp相当于3个px,此时30px等同于10dp。假设某个App的内部文本使用字 号30px,则该App安装到前一部手机的字体大小为15dp,安装到后一部手机的字体大小为10dp,显然后一部手机显示的文本会更小。

至于dp与sp之间的区别,可通过以下实验加以观察。首先创建测试活动页面,该页面的XML文件分别声明30px、30dp、30sp这3个字号的TextView控件,布局内容如下所示:

(完整代码见chapter03\src\main\res\layout\activity_text_size.xml)

接着打开手机的设置菜单,依次选择“显示”→“字体与显示大小”,确认当前的字体为标准大小,如图3-3 所示。然后在手机上运行测试App,进入测试页面看到的文字效果如图3-4所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z2okDAZh-1676725568032)(media/b3cf4df3a5e475db9ac277f852e013b8.jpeg)]

图3-3 系统默认字体是标准大小

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q8irs4zk-1676725568032)(media/547180e792182926a220a25d2eddf51e.jpeg)]

3-4 标准字体时的演示界面

回到设置菜单的字体页面,将字体大小调整为大号,如图3-5所示。再次进入测试页面看到的文字效果如 图3-6所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Sxor9n10-1676725568032)(media/31668d35a9e3e8fd8b84bbdbae6abecd.jpeg)]

图3-5 把系统字体改为大号

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QfSCZNLY-1676725568034)(media/25f98a04e13ff03508ab3766956c4389.jpeg)]

图3-6 大号字体时的演示界面

对照图3-4和图3-6,发现字号单位30px和30dp的文字大小不变,而30sp的文字随着系统字体一起变大 了。

既然XML文件要求android:textSize必须指定字号单位,为什么Java代码调用setTextSize只填数字不填 单位呢?其实查看SDK源码,找到setTextSize方法的实现代码如下所示:

原来纯数字的setTextSize方法,内部默认字号单位为sp(COMPLEX_UNIT_SP),这也从侧面印证了之前的说法:sp才是Android推荐的字号单位。

补充
名称解释
px(Pixel像素)也称为图像元素,是作为图像构成的基本单元,单个像素的大小并不固定,跟随 屏幕大小和像素数量的关系变化,一个像素点为1px。
Resolution (分辨率)是指屏幕的垂直和水平方向的像素数量,如果分辨率是 1920*1080 ,那就是垂直方向有 1920 个像素,水平方向有 1080 个像素。
Dpi(像素密度)是指屏幕上每英寸(1英寸 = 2.54 厘米)距离中有多少个像素点。
Density(密度)是指屏幕上每平方英寸(2.54 ^ 2 平方厘米)中含有的像素点数量。
Dip / dp (设备独立像素)也可以叫做dp,长度单位,同一个单位在不同的设备上有不同的显示效果,具体效果根据设备的密度有关,详细的公式请看下面 。

计算规则

我们以一个 4.95 英寸 1920 * 1080 的 nexus5 手机设备为例:

Dpi :

1. 计算直角边像素数量: 1920^2+1080^2=2202^2(勾股定理)。

2. 计算 DPI:2202 / 4.95 = 445。

3. 得到这个设备的 DPI 为 445 (每英寸的距离中有 445 个像素)。

Density

上面得到每英寸中有 445 像素,那么 density 为每平方英寸中的像素数量,应该为: 445^2=198025。

Dip

所有显示到屏幕上的图像都是以 px 为单位,Dip 是我们开发中使用的长度单位,最后他也需要转换成

px,计算这个设备上 1dip 等于多少 px:

px = dip x dpi /160

根据换算关系:

320 x 480分辨率,3.6寸的手机:dpi为160,1dp=1px

实验一

相同分辨率,不同大小的手机AB:

代号分辨率尺寸dpidp
手机A320x4803.6寸1601dp=1px
手机B320x4807.2寸801dp=0.5px

假如AB都设置一个宽度为100dp的TextView:

代号TextView宽度手机宽度比例关系
手机A100px320px10/32
手机B50px320px5/32

得出结论:

对于相同分辨率的手机,屏幕越大,同DP的组件占用屏幕比例越小。

如图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-maBVlxvz-1676725568034)(media/ba8ee3b05d58f354db79195035fd81e8.png)]

实验二

相同大小,不同分辨率的手机AB:

代号分辨率尺寸dpidp
手机A320x4803.6寸1601dp=1px
手机B640x9603.6寸3201dp=2px

假如AB都设置一个宽度为100dp的TextView:

代号TextView宽度手机宽度比例关系
手机A100px320px10/32
手机B200px640px10/32

得出结论:

对于相同尺寸的手机,即使分辨率不同,同DP的组件占用屏幕比例也相同。

如图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7YOVLKvT-1676725568034)(media/aaddc4e4522cbd465865ede7b0624e71.png)]

综上:

dp的UI效果只在相同尺寸的屏幕上相同,如果屏幕尺寸差异过大,则需要重做dp适配。

这也是平板需要单独做适配的原因,可见dp不是比例

设置文本的颜色

除了设置文字大小,文字颜色也经常需要修改,毕竟Android默认的灰色文字不够醒目。在Java代码中调用setTextColor方法即可设置文本颜色,具体在Color类中定义了12种颜色,详细的取值说明见表3-1。

表3-1 颜色类型的取值说明

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LkdrnDAF-1676725568035)(media/56b583f37e5f676a1ef2da270ae90c21.jpeg)]

比如以下代码便将文本视图的文字颜色改成了绿色:

(完整代码见chapter03\src\main\java\com\example\chapter03\TextColorActivity.java)

可是XML文件无法引用Color类的颜色常量,为此Android制定了一套规范的编码标准,将色值交由透明度alpha和RGB三原色(红色red、绿色green、蓝色blue)联合定义。该标准又有八位十六进制数与六 位十六进制数两种表达方式,例如八位编码FFEEDDCC中,FF表示透明度,EE表示红色的浓度,DD表示 绿色的浓度,CC表示蓝色的浓度。透明度为FF表示完全不透明,为00表示完全透明。RGB三色的数值越大,表示颜色越浓,也就越暗;数值越小,表示颜色越淡,也就越亮。RGB亮到极致就是白色,暗到极 致就是黑色。

至于六位十六进制编码,则有两种情况,它在XML文件中默认不透明(等价于透明度为FF),而在代码中默认透明(等价于透明度为00)。以下代码给两个文本视图分别设置六位色值与八位色值,注意添加

0x前缀表示十六进制数:

运行测试App,发现tv_code_six控件的文本不见了(其实是变透明了),而tv_code_eight控件的文本显 示正常的绿色。

在XML文件中可通过属性android:textColor设置文字颜色,但要给色值添加井号前缀“#”,设定好文本颜 色的TextView标签示例如下:

(完整代码见chapter03\src\main\res\layout\activity_text_color.xml)

就像字符串资源那样,Android把颜色也当作一种资源,打开res/values目录下的colors.xml,发现里面 已经定义了3种颜色:

那么先在resources节点内部补充如下的绿色常量定义:

然后回到XML布局文件,把android:textColor的属性值改为“@color/颜色名称”,也就是

android:textColor=“@color/green”,修改之后的标签TextView如下所示:

不仅文字颜色,还有背景颜色也会用到上述的色值定义,在XML文件中通过属性android:background设 置控件的背景颜色。Java代码则有两种方式设置背景颜色,倘若色值来源于Color类或十六进制数,则调用setBackgroundColor方法设置背景;倘若色值来源于colors.xml中的颜色资源,则调用

setBackgroundResource方法,以“R.color.颜色名称”的格式设置背景。下面是两种方式的背景设定代码例子:

注意属性android:background和setBackgroundResource方法,它俩用来设置控件的背景,不单单是背景颜色,还包括背景图片。在设置背景图片之前,先将图片文件放到res/drawable***目录(以

drawable开头的目录,不仅仅是drawable目录),然后把android:background的属性值改为

“@drawable/不含扩展名的图片名称”,或者调用setBackgroundResource方法填入“R.drawable.不含扩 展名的图片名称”。

.2 视图基础

本节介绍视图的几种基本概念及其用法,包括如何设置视图的宽度和高度,如何设置视图的外部间距和 内部间距,如何设置视图的外部对齐方式和内部对齐方式,等等。

设置视图的宽高

手机屏幕是块长方形区域,较短的那条边叫作宽,较长的那条边叫作高。App控件通常也是长方形状, 控件宽度通过属性android:layout_width表达,控件高度通过属性android:layout_height表达,宽高的取值主要有下列3种:

  1. match_parent:表示与上级视图保持一致。上级视图的尺寸有多大,当前视图的尺寸就有多大。
  2. wrap_content:表示与内容自适应。对于文本视图来说,内部文字需要多大的显示空间,当前视图就要占据多大的尺寸。但最宽不能超过上级视图的宽度,一旦超过就要换行;最高不能超过上级视图 的高度,一旦超过就会隐藏。
  3. 以dp为单位的具体尺寸,比如300dp,表示宽度或者高度就是这么大。

在XML文件中采用以上任一方式均可设置视图的宽高,但在Java代码中设置宽高就有点复杂了,首先确保XML中的宽高属性值为wrap_content,这样才允许在代码中修改宽高。接着打开该页面对应的Java代 码,依序执行以下3个步骤:

步骤一,调用控件对象的getLayoutParams方法,获取该控件的布局参数,参数类型为

ViewGroup.LayoutParams。

步骤二,布局参数的width属性表示宽度,height属性表示高度,修改这两个属性值,即可调整控件的宽高。

步骤三,调用控件对象的setLayoutParams方法,填入修改后的布局参数使之生效。

不过布局参数的width和height两个数值默认是px单位,需要将dp单位的数值转换为px单位的数值,然 后才能赋值给width属性和height属性。下面是把dp大小转为px大小的方法代码:

(完整代码见chapter03\src\main\java\com\example\chapter03\util\Utils.java)

有了上面定义的公共方法dip2px,就能将某个dp数值转换成px数值,比如准备把文本视图的宽度改为

300dp,那么调整宽度的Java代码示例如下:

(完整代码见chapter03\src\main\java\com\example\chapter03\ViewBorderActivity.java)

接下来通过演示页面并观察几种尺寸设置方式的界面效果,主要通过背景色区分当前视图的宽高范围, 详细的XML文件内容如下所示:

(完整代码见chapter03\src\main\res\layout\activity_view_border.xml)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DbGXlVA0-1676725568035)(media/593bcaf9f122f76fb679c01df8ad4f5c.jpeg)]运行测试App,打开演示界面如图3-7所示,依据背景色判断文本视图的边界,可见wrap_content方式刚好包住了文本内容,match_parent方式扩展到了与屏幕等宽,而300dp的宽度介于前两者之间(安卓 手机的屏幕宽度基本为360dp)。

图3-7 设置控件宽度的几种方式效果

设置视图的间距

在上一小节末尾的XML文件中,每个TextView标签都携带新的属性

android:layout_marginTop=“5dp”,该属性的作用是让当前视图与上方间隔一段距离。同理,

android:layout_marginLeft让当前视图与左边间隔一段距离,android:layout_marginRight让当前视图 与右边间隔一段距离,android:layout_marginBottom让当前视图与下方间隔一段距离。如果上下左右 都间隔同样的距离,还能使用android:layout_margin一次性设置四周的间距。

layout_margin不单单用于文本视图,还可用于所有视图,包括各类布局和各类控件。因为不管布局还是控件,它们统统由视图基类View派生而来,而layout_margin正是View的一个通用属性,所以View的 子子孙孙都能使用layout_margin。在View的大家族中,视图组ViewGroup尤为特殊,它既是View的子类,又是各类布局的基类。布局下面能容纳其他视图,而控件却不行,这正源自ViewGroup的组装特 性。View、ViewGroup、控件、布局四者的继承关系如图3-8所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c71MLbun-1676725568036)(media/e55db9fd087564bd441f0550d1a89171.jpeg)]

图3-8 视图家族的依赖继承关系

除了layout_margin之外,padding也是View的一个通用属性,它用来设置视图的内部间距,并且padding也提供了paddingTop、paddingBottom、paddingLeft、paddingRight四个方向的距离属性。 同样是设置间距,layout_margin指的是当前视图与外部视图(包括上级视图和平级视图)之间的距

离,而padding指的是当前视图与内部视图(包括下级视图和内部文本)之间的距离。为了观察外部间距和内部间距的差异,接下来做个实验,看看layout_margin与padding究竟有什么区别。

首先创建新的活动页面,并给该页面的XML文件填入以下的布局内容:

(完整代码见chapter03\src\main\res\layout\activity_view_margin.xml)

上面的XML文件有两层视图嵌套,第一层是蓝色背景布局里面放黄色背景布局,第二层是黄色背景布局里面放红色背景视图。中间层的黄色背景布局,同时设置了20dp的layout_margin,以及60dp的

padding,其中padding是layout_margin的三倍宽(60/20=3)。接着运行测试App,看到的演示界面如图3-9所示。

从效果图可见,外面一圈间隔较窄,里面一圈间隔较宽,表示20dp的layout_margin位于外圈,而60dp 的padding位于内圈。这种情况印证了:layout_margin指的是当前图层与外部图层的距离,而padding 指的是当前图层与内部图层的距离。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wZHDMe14-1676725568036)(media/84de0c4e148a026b6f0a07359ebf7e11.jpeg)]

图3-9 两种间距方式的演示效果

设置视图的对齐方式

App界面上的视图排列,默认靠左朝上对齐,这也符合日常的书写格式。然而页面的排版不是一成不变 的,有时出于美观或者其他原因,要将视图排列改为朝下或靠右对齐,为此需要另外指定视图的对齐方 式。在XML文件中通过属性android:layout_gravity可以指定当前视图的对齐方向,当属性值为top时表 示视图朝上对齐,为bottom时表示视图朝下对齐,为left时表示视图靠左对齐,为right时表示视图靠右对齐。如果希望视图既朝上又靠左,则用竖线连接top与left,此时属性标记为

android:layout_gravity=“top|left”;如果希望视图既朝下又靠右,则用竖线连接bottom与right,此时属性标记为android:layout_gravity=“bottom|right”。

注意layout_gravity规定的对齐方式,指的是当前视图往上级视图的哪个方向对齐,并非当前视图的内部对齐。若想设置内部视图的对齐方向,则需由当前视图的android:gravity指定,该属性一样拥有top、bottom、left、right 4种取值及其组合。它与layout_gravity的不同之处在于:layout_gravity设定了当前视图相对于上级视图的对齐方式,而gravity设定了下级视图相对于当前视图的对齐方式;前者决定了当前视图的位置,而后者决定了下级视图的位置。

为了进一步分辨layout_gravity与gravity的区别,接下来做个实验,对某个布局视图同时设置android:layout_gravity和android:gravity属性,再观察内外视图的对齐情况。下面便是实验用的XML文件例子:

(完整代码见chapter03\src\main\res\layout\activity_view_gravity.xml)

运行测试App,打开演示界面如图3-10所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Fzs1TMF7-1676725568037)(media/b2e744b2e6940b961121a9896e4df232.jpeg)]

图3-10 两种对齐方式的演示效果

由效果图可见,第一个子布局朝下,并且它的内部视图靠左;而第二个子布局朝上,并且它的内部视图 靠右。对比XML文件中的layout_gravity和gravity取值,证明了二者的对齐情况正如之前所言:

layout_gravity决定当前视图位于上级视图的哪个方位,而gravity决定了下级视图位于当前视图的哪个方 位。

.3 常用布局

本节介绍常见的几种布局用法,包括在某个方向上顺序排列的线性布局,参照其他视图的位置相对排列 的相对布局,像表格那样分行分列显示的网格布局,以及支持通过滑动操作拉出更多内容的滚动视图。

线性布局LinearLayout

前几个小节的例程中,XML文件用到了LinearLayout布局,它的学名为线性布局。顾名思义,线性布局 像是用一根线把它的内部视图串起来,故而内部视图之间的排列顺序是固定的,要么从左到右排列,要 么从上到下排列。在XML文件中,LinearLayout通过属性android:orientation区分两种方向,其中从左到右排列叫作水平方向,属性值为horizontal;从上到下排列叫作垂直方向,属性值为vertical。如果

LinearLayout标签不指定具体方向,则系统默认该布局为水平方向排列,也就是默认

android:orientation=“horizontal”。

下面做个实验,让XML文件的根节点挂着两个线性布局,第一个线性布局采取horizontal水平方向,第二个线性布局采取vertical垂直方向。然后每个线性布局内部各有两个文本视图,通过观察这些文本视图的排列情况,从而检验线性布局的显示效果。详细的XML文件内容如下所示:

(完整代码见chapter03\src\main\res\layout\activity_linear_layout.xml)

运行测试App,进入如图3-11所示的演示页面,可见horizontal为横向排列,vertical为纵向排列,说明

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yq7uOYkV-1676725568038)(media/03677983bca43724d9e2fc63c3df9b27.jpeg)]android:orientation的方向属性确实奏效了。

图3-11 线性布局的方向排列

除了方向之外,线性布局还有一个权重概念,所谓权重,指的是线性布局的下级视图各自拥有多大比例 的宽高。比如一块蛋糕分给两个人吃,可能两人平均分,也可能甲分三分之一,乙分三分之二。两人平 均分的话,先把蛋糕切两半,然后甲分到一半,乙分到另一半,此时甲乙的权重比为1:1。甲分三分之 一、乙分三分之二的话,先把蛋糕平均切成三块,然后甲分到一块,乙分到两块,此时甲乙的权重比为

1:2。就线性布局而言,它自身的尺寸相当于一整块蛋糕,它的下级视图们一起来分这个尺寸蛋糕,有的 视图分得多,有的视图分得少。分多分少全凭每个视图分到了多大的权重,这个权重在XML文件中通过 属性android:layout_weight来表达。

把线性布局看作蛋糕的话,分蛋糕的甲乙两人就相当于线性布局的下级视图。假设线性布局平均分为左 右两块,则甲视图和乙视图的权重比为1:1,意味着两个下级视图的layout_weight属性都是1。不过视图 有宽高两个方向,系统怎知layout_weight表示哪个方向的权重呢?所以这里有个规定,一旦设置了

layout_weight属性值,便要求layout_width填0dp或者layout_height填0dp。如果layout_width填

0dp,则layout_weight表示水平方向的权重,下级视图会从左往右分割线性布局;如果layout_height填

0dp,则layout_weight表示垂直方向的权重,下级视图会从上往下分割线性布局。

按照左右均分的话,线性布局设置水平方向horizontal,且甲乙两视图的layout_width都填0dp,

layout_weight都填1,此时横排的XML片段示例如下:

(完整代码见chapter03\src\main\res\layout\activity_linear_layout.xml)

按照上下均分的话,线性布局设置垂直方向vertical,且甲乙两视图的layout_height都填0dp,

layout_weight都填1,此时竖排的XML片段示例如下:

把上面两个片段放到新页面的XML文件,其中第一个是横排区域采用红色背景(色值为ff0000),第二个是竖排区域采用青色背景(色值为00ffff)。重新运行测试App,打开演示界面如图3-12所示,可见横排区域平均分为左右两块,竖排区域平均分为上下两块。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PE3apN5c-1676725568038)(media/814f7a26d9db7ffb5f789bf984237144.jpeg)]

图3-12 线性布局的权重分割

相对布局RelativeLayout

线性布局的下级视图是顺序排列着的,另一种相对布局的下级视图位置则由其他视图决定。相对布局名 为RelativeLayout,因为下级视图的位置是相对位置,所以得有具体的参照物才能确定最终位置。如果 不设定下级视图的参照物,那么下级视图默认显示在RelativeLayout内部的左上角。

用于确定下级视图位置的参照物分两种,一种是与该视图自身平级的视图;另一种是该视图的上级视图

(也就是它归属的RelativeLayout)。综合两种参照物,相对位置在XML文件中的属性名称说明见表3-

2。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9LPzD3ak-1676725568039)(media/d315875e29cf84d6cfac436187ec6849.jpeg)]表3-2 相对位置的属性取值说明

为了更好地理解上述相对属性的含义,接下来使用RelativeLayout及其下级视图进行布局来看看实际效 果图。下面是演示相对布局的XML文件例子:

(完整代码见chapter03\src\main\res\layout\activity_relative_layout.xml)

<TextView

android:id=“@+id/tv_center” android:layout_width=“wrap_content” android:layout_height=“wrap_content” android:layout_centerInParent=“true” android:background=“#ffffff”

android:text=“我在中间”

android:textSize=“11sp” android:textColor=“#000000” />

<TextView

android:id=“@+id/tv_center_horizontal” android:layout_width=“wrap_content” android:layout_height=“wrap_content” android:layout_centerHorizontal=“true” android:background=“#eeeeee”

android:text=“我在水平中间”

android:textSize=“11sp” android:textColor=“#000000” />

<TextView

android:id=“@+id/tv_center_vertical” android:layout_width=“wrap_content” android:layout_height=“wrap_content” android:layout_centerVertical=“true” android:background=“#eeeeee”

android:text=“我在垂直中间”

android:textSize=“11sp” android:textColor=“#000000” />

<TextView

android:id=“@+id/tv_parent_left” android:layout_width=“wrap_content” android:layout_height=“wrap_content” android:layout_alignParentLeft=“true” android:background=“#eeeeee”

android:text=“我跟上级左边对齐”

android:textSize=“11sp” android:textColor=“#000000” />

<TextView

android:id=“@+id/tv_parent_right” android:layout_width=“wrap_content” android:layout_height=“wrap_content” android:layout_alignParentRight=“true” android:background=“#eeeeee”

android:text=“我跟上级右边对齐”

android:textSize=“11sp” android:textColor=“#000000” />

<TextView

android:id=“@+id/tv_parent_top” android:layout_width=“wrap_content” android:layout_height=“wrap_content” android:layout_alignParentTop=“true” android:background=“#eeeeee” android:text=“我跟上级顶部对齐” android:textSize=“11sp”

android:textColor=“#000000” />

<TextView

android:id=“@+id/tv_parent_bottom” android:layout_width=“wrap_content” android:layout_height=“wrap_content” android:layout_alignParentBottom=“true” android:background=“#eeeeee”

android:text=“我跟上级底部对齐”

android:textSize=“11sp” android:textColor=“#000000” />

<TextView

android:id=“@+id/tv_left_center” android:layout_width=“wrap_content” android:layout_height=“wrap_content” android:layout_toLeftOf=“@+id/tv_center” android:layout_alignTop=“@+id/tv_center” android:background=“#eeeeee”

android:text=“我在中间左边”

android:textSize=“11sp” android:textColor=“#000000” />

<TextView

android:id=“@+id/tv_right_center” android:layout_width=“wrap_content” android:layout_height=“wrap_content” android:layout_toRightOf=“@+id/tv_center” android:layout_alignBottom=“@+id/tv_center” android:background=“#eeeeee” android:text=" 我 在 中 间 右 边 " android:textSize=“11sp” android:textColor=“#000000” />

<TextView

android:id=“@+id/tv_above_center” android:layout_width=“wrap_content” android:layout_height=“wrap_content” android:layout_above=“@+id/tv_center” android:layout_alignLeft=“@+id/tv_center” android:background=“#eeeeee” android:text=" 我 在 中 间 上 面 " android:textSize=“11sp” android:textColor=“#000000” />

<TextView

android:id=“@+id/tv_below_center” android:layout_width=“wrap_content” android:layout_height=“wrap_content” android:layout_below=“@+id/tv_center” android:layout_alignRight=“@+id/tv_center” android:background=“#eeeeee”

android:text=“我在中间下面”

android:textSize=“11sp” android:textColor=“#000000” />

</RelativeLayout>

上述XML文件的布局效果如图3-13所示,RelativeLayout的下级视图都是文本视图,控件上的文字说明 了所处的相对位置,具体的控件显示方位正如XML属性中描述的那样。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OaA48kE8-1676725568040)(media/c1ac22f7fe503aed7a807a52d18ea1f8.jpeg)]

图3-13 相对布局的相对位置效果

网格布局GridLayout

虽然线性布局既能在水平方向排列,也能在垂直方向排列,但它不支持多行多列的布局方式,只支持单 行(水平排列)或单列(垂直排列)的布局方式。若要实现类似表格那样的多行多列形式,可采用网格 布局GridLayout。

网格布局默认从左往右、从上到下排列,它先从第一行从左往右放置下级视图,塞满之后另起一行放置 其余的下级视图,如此循环往复直至所有下级视图都放置完毕。为了判断能够容纳几行几列,网格布局 新增了android:columnCount与android:rowCount两个属性,其中columnCount指定了网格的列数, 即每行能放多少个视图;rowCount指定了网格的行数,即每列能放多少个视图。

下面是运用网格布局的XML布局样例,它规定了一个两行两列的网格布局,且内部容纳四个文本视图。

XML文件内容如下所示:

(完整代码见chapter03\src\main\res\layout\activity_grid_layout.xml)

在一个新建的活动页面加载上述布局,运行App观察到的界面如图3-14所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PzZccOoa-1676725568040)(media/df697d945b12932bd2acde1f5ebfce1b.jpeg)]

图3-14 网格布局的视图分布情况

由图3-14可见,App界面的第一行分布着浅红色背景与橙色背景的文本视图,第二行分布着绿色背景与深紫色背景的文本视图,说明利用网格布局实现了多行多列的效果。

滚动视图ScrollView

手机屏幕的显示空间有限,常常需要上下滑动或左右滑动才能拉出其余页面内容,可惜一般的布局节点 都不支持自行滚动,这时就要借助滚动视图了。与线性布局类似,滚动视图也分为垂直方向和水平方向 两类,其中垂直滚动视图名为ScrollView,水平滚动视图名为HorizontalScrollView。这两个滚动视图的使用并不复杂,主要注意以下3点:

  1. 垂直方向滚动时,layout_width属性值设置为match_parent,layout_height属性值设置为

    wrap_content。

  2. 水平方向滚动时,layout_width属性值设置为wrap_content,layout_height属性值设置为

    match_parent。

  3. 滚动视图节点下面必须且只能挂着一个子布局节点,否则会在运行时报错Caused by: java.lang.IllegalStateException:ScrollView can host only one direct child。

    下面是垂直滚动视图ScrollView和水平滚动视图HorizontalScrollView的XML例子:

    (完整代码见chapter03\src\main\res\layout\activity_scroll_view.xml)

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width=“match_parent” android:layout_height=“match_parent”

android:orientation=“vertical”>

<!-- HorizontalScrollView是水平方向的滚动视图,当前高度为200dp -->

<HorizontalScrollView android:layout_width=“wrap_content” android:layout_height=“200dp”>

<!-- 水平方向的线性布局,两个子视图的颜色分别为青色和黄色 -->

<LinearLayout

android:layout_width=“wrap_content” android:layout_height=“match_parent” android:orientation=“horizontal”>

<View

android:layout_width=“300dp” android:layout_height=“match_parent” android:background=“#aaffff” />

<View

android:layout_width=“300dp” android:layout_height=“match_parent” android:background=“#ffff00” />

</LinearLayout>

</HorizontalScrollView>

<!-- ScrollView是垂直方向的滚动视图,当前高度为自适应 -->

<ScrollView

android:layout_width=“match_parent” android:layout_height=“wrap_content”>

<!-- 垂直方向的线性布局,两个子视图的颜色分别为绿色和橙色 -->

<LinearLayout

android:layout_width=“match_parent” android:layout_height=“wrap_content” android:orientation=“vertical”>

<View

android:layout_width=“match_parent” android:layout_height=“400dp” android:background=“#00ff00” />

<View

android:layout_width=“match_parent” android:layout_height=“400dp” android:background=“#ffffaa” />

</LinearLayout>

</ScrollView>

</LinearLayout>

运行测试App,可知ScrollView在纵向滚动,而HorizontalScrollView在横向滚动。

有时ScrollView的实际内容不够,又想让它充满屏幕,怎么办呢?如果把layout_height属性赋值为match_parent,结果还是不会充满,正确的做法是再增加一行属性android:fillViewport(该属性为true 表示允许填满视图窗口),属性片段举例如下:

.4 按钮触控

本节介绍了按钮控件的常见用法,包括:如何设置大小写属性与点击属性,如何响应按钮的点击事件和 长按事件,如何禁用按钮又该如何启用按钮,等等。

按钮控件Button

除了文本视图之外,按钮Button也是一种基础控件。因为Button是由TextView派生而来,所以文本视图 拥有的属性和方法,包括文本内容、文本大小、文本颜色等,按钮控件均能使用。不同的是,Button拥 有默认的按钮背景,而TextView默认无背景;Button的内部文本默认居中对齐,而TextView的内部文本 默认靠左对齐。此外,按钮还要额外注意textAllCaps与onClick两个属性,分别介绍如下:

textAllCaps属性

对于TextView来说,text属性设置了什么文本,文本视图就显示什么文本。但对于Button来说,不管 text属性设置的是大写字母还是小写字母,按钮控件都默认转成大写字母显示。比如在XML文件中加入下面的Button标签:

编译运行后的App界面,按钮上显示全大写的“HELLO WORLD”,而非原来大小写混合的“Hello World”。显然这个效果不符合预期,为此需要给Button标签补充textAllCaps属性,该属性默认为true表 示全部转为大写,如果设置为false则表示不转为大写。于是在布局文件添加新的Button标签,该标签补充了android:textAllCaps=“false”,具体内容如下所示:

(完整代码见chapter03\src\main\res\layout\activity_button_style.xml)

再次运行App,此时包含新旧按钮的界面如图3-15所示,可见textAllCaps属性果然能够控制大小写转 换。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jtLFQYWF-1676725568040)(media/ca47606b28904d426c849ee42d2d76b2.jpeg)]

图3-15 保持英文大小写的按钮控件

onClick属性

按钮之所以成为按钮,是因为它会响应按下动作,就手机而言,按下动作等同于点击操作,即手指轻触 屏幕然后马上松开。每当点击按钮之时,就表示用户确认了某个事项,接下来轮到App接着处理了。

onClick属性便用来接管用户的点击动作,该属性的值是个方法名,也就是当前页面的Java代码存在这么 一个方法:当用户点击按钮时,就自动调用该方法。

譬如下面的Button标签指定了onClick属性值为doClick,表示点击该按钮会触发Java代码中的doClick方法:

(完整代码见chapter03\src\main\res\layout\activity_button_style.xml)

与之相对应,页面所在的Java代码需要增加doClick方法,方法代码示例如下:

(完整代码见chapter03\src\main\java\com\example\chapter03\ButtonStyleActivity.java)

然后编译运行,并在App界面上点击新加的按钮,点击前后的界面如图3-16和图3-17所示,其中图3-16 为点击之前的界面,图3-17为点击之后的界面。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Er3v2YAa-1676725568041)(media/04138638ae383183ee2d420f514ebc4d.jpeg)]

图3-16 按钮点击之前的界面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aIXWBruW-1676725568041)(media/89b76ee4b0a43643d949806149b27917.jpeg)]

图3-17 按钮点击之后的界面

比较图3-16和图3-17的文字差异,可见点击按钮之后确实调用了doClick方法。

点击事件和长按事件

虽然按钮控件能够在XML文件中通过onClick属性指定点击方法,但是方法的名称可以随便叫,既能叫

doClick也能叫doTouch,甚至叫它doA或doB都没问题,这样很不利于规范化代码,倘若以后换了别人 接手,就不晓得doA或doB是干什么用的。因此在实际开发中,不推荐使用Button标签的onClick属性, 而是在代码中给按钮对象注册点击监听器。

所谓监听器,意思是专门监听控件的动作行为,它平时无所事事,只有控件发生了指定的动作,监听器 才会触发开关去执行对应的代码逻辑。点击监听器需要实现接口View.OnClickListener,并重写onClick 方法补充点击事件的处理代码,再由按钮调用setOnClickListener方法设置监听器对象。比如下面的代 码给按钮控件btn_click_single设置了一个点击监听器:

(完整代码见chapter03\src\main\java\com\example\chapter03\ButtonClickActivity.java)

上面的点击监听器名为MyOnClickListener,它的定义代码示例如下:

接着运行App,点击按钮之后的界面如图3-18所示,可见点击动作的确触发了监听器的onClick方法。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WbK5ohjS-1676725568042)(media/a704c4e906853894899cc42ecbcd92e2.jpeg)]

图3-18 点击了单独的点击监听器

如果一个页面只有一个按钮,单独定义新的监听器倒也无妨,可是如果存在许多按钮,每个按钮都定义 自己的监听器,那就劳民伤财了。对于同时监听多个按钮的情况,更好的办法是注册统一的监听器,也 就是让当前页面实现接口View.OnClickListener,如此一来,onClick方法便写在了页面代码之内。因为是统一的监听器,所以onClick内部需要判断是哪个按钮被点击了,也就是利用视图对象的getId方法检查控件编号,完整的onClick代码举例如下:

(完整代码见chapter03\src\main\java\com\example\chapter03\ButtonClickActivity.java)

当然该页面的onCreate内部别忘了调用按钮对象的setOnClickListener方法,把按钮的点击监听器设置 成当前页面,设置代码如下所示:

重新运行App,点击第二个按钮之后的界面如图3-19所示,可见当前页面的onClick方法也正确执行了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VITRw6BY-1676725568043)(media/421897484f6bb1d97b6748474abdee62.jpeg)]

图3-19 点击了公共的点击监听器

除了点击事件,Android还设计了另外一种长按事件,每当控件被按住超过500毫秒之后,就会触发该控件的长按事件。若要捕捉按钮的长按事件,可调用按钮对象的setOnLongClickListener方法设置长按监 听器。具体的设置代码示例如下:

(完整代码见chapter03\src\main\java\com\example\chapter03\ButtonLongclickActivity.java)

以上代码把长按监听器设置到当前页面,意味着该页面需要实现对应的长按接口

View.OnLongClickListener,并重写长按方法onLongClick,下面便是重写后的onLongClick代码例子:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q7u2PkOg-1676725568043)(media/f7d2aee36ac52f29c07fa359faad29fd.jpeg)]再次运行App,长按按钮之后的界面如图3-20所示,说明长按事件果然触发了onLongClick方法。

图3-20 长按了公共的长按监听器

值得注意的是,点击监听器和长按监听器不局限于按钮控件,其实它们都来源于视图基类View,凡是从

View派生而来的各类控件,均可注册点击监听器和长按监听器。譬如文本视图TextView,其对象也能调 用setOnClickListener方法与setOnLongClickListener方法,此时TextView控件就会响应点击动作和长 按动作。因为按钮存在按下和松开两种背景,便于提示用户该控件允许点击,但文本视图默认没有按压 背景,不方便判断是否被点击,所以一般不会让文本视图处理点击事件和长按事件。

禁用与恢复按钮

尽管按钮控件生来就是给人点击的,可是某些情况希望暂时禁止点击操作,譬如用户在注册的时候,有 的网站要求用户必须同意指定条款,而且至少浏览10秒之后才能点击注册按钮。那么在10秒之前,注册按钮应当置灰且不能点击,等过了10秒之后,注册按钮才恢复正常。在这样的业务场景中,按钮先后拥 有两种状态,即不可用状态与可用状态,它们在外观和功能上的区别如下:

  1. 不可用按钮:按钮不允许点击,即使点击也没反应,同时按钮文字为灰色。
  2. 可用按钮:按钮允许点击,点击按钮会触发点击事件,同时按钮文字为正常的黑色。

从上述的区别说明可知,不可用与可用状态主要有两点差异:其一,是否允许点击;其二,按钮文字的 颜色。就文字颜色而言,可在布局文件中使用textColor属性设置颜色,也可在Java代码中调用

setTextColor方法设置颜色。至于是否允许点击,则需引入新属性android:enabled,该属性值为true时 表示启用按钮,即允许点击按钮;该属性值为false时表示禁用按钮,即不允许点击按钮。在Java代码

中,则可通过setEnabled方法设置按钮的可用状态(true表示启用,false表示禁用)。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wd5ecoMx-1676725568044)(media/731062d4cf3d7acff94e5d74f0170174.jpeg)]接下来通过一个例子演示按钮的启用和禁用操作。为了改变测试按钮的可用状态,需要额外添加两个控 制按钮,分别是“启用测试按钮”和“禁用测试按钮”,加起来一共3个按钮控件,注意“测试按钮”默认是灰色文本。测试界面的布局效果如图3-21所示。

图3-21 测试按钮尚未启用时的界面与图3-21对应的布局文件内容如下所示:

(完整代码见chapter03\src\main\res\layout\activity_button_enable.xml)

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width=“match_parent” android:layout_height=“match_parent”

android:orientation=“vertical”>

<LinearLayout

android:layout_width=“match_parent” android:layout_height=“wrap_content” android:orientation=“horizontal”>

<Button

android:id=“@+id/btn_enable” android:layout_width=“0dp” android:layout_height=“wrap_content” android:layout_weight=“1” android:text=" 启 用 测 试 按 钮 " android:textColor=“#000000” android:textSize=“17sp” />

<Button

android:id=“@+id/btn_disable” android:layout_width=“0dp” android:layout_height=“wrap_content” android:layout_weight=“1”

android:text=“禁用测试按钮”

android:textColor=“#000000” android:textSize=“17sp” />

</LinearLayout>

<Button

android:id=“@+id/btn_test” android:layout_width=“match_parent” android:layout_height=“wrap_content” android:enabled=“false”

android:text=“测试按钮”

android:textColor=“#888888” android:textSize=“17sp” />

<TextView

android:id=“@+id/tv_result” android:layout_width=“match_parent” android:layout_height=“wrap_content” android:paddingLeft=“5dp”

android:text=“这里查看测试按钮的点击结果”

android:textColor=“#000000” android:textSize=“17sp” />

</LinearLayout>

然后在Java代码中给3个按钮分别注册点击监听器,注册代码如下所示:

(完整代码见chapter03\src\main\java\com\example\chapter03\ButtonEnableActivity.java)

同时重写页面的onClick方法,分别处理3个按钮的点击事件,修改之后的onClick代码示例如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bUqZgcN4-1676725568045)(media/71e72e8d3f4b4ea25fd157886c36fdda.jpeg)]最后编译运行App,点击了“启用测试按钮”之后,原本置灰的测试按钮btn_test恢复正常的黑色文本,点击该按钮发现界面有了反应,具体效果如图3-22所示。

图3-22 测试按钮已经启用后的界面

对比图3-21和图3-22,观察按钮启用前后的外观及其是否响应点击动作,即可知晓禁用按钮和启用按钮两种模式的差别。

.5 图像显示

本节介绍了与图像显示有关的几种控件用法,包括:专门用于显示图片的图像视图以及若干缩放类型效 果,支持显示图片的按钮控件——图像按钮,如何在按钮控件上同时显示文本和图标等。

图像视图ImageView

显示文本用到了文本视图TextView,显示图像则用到图像视图ImageView。由于图像通常保存为单独的 图片文件,因此需要先把图片放到res/drawable目录,然后再去引用该图片的资源名称。比如现在有张苹果图片名为apple.png,那么XML文件通过属性android:src设置图片资源,属性值格式形如

“@drawable/不含扩展名的图片名称”。添加了src属性的ImageView标签示例如下:

(完整代码见chapter03\src\main\res\layout\activity_image_scale.xml)

若想在Java代码中设置图像视图的图片资源,可调用ImageView控件的setImageResource方法,方法参数格式形如“R.drawable.不含扩展名的图片名称”。仍以上述的苹果图片为例,给图像视图设置图片资源的代码例子如下所示:

(完整代码见chapter03\src\main\java\com\example\chapter03\ImageScaleActivity.java)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cz61tFsR-1676725568045)(media/fd13388ffd8e30c4b9640c23b1fe09bf.jpeg)]运行测试App,展示图片的界面效果如图3-23所示。

图3-23 图像视图显示苹果图片

观察效果图发现苹果图片居中显示,而非文本视图里的文字那样默认靠左显示,这是怎么回事?原来

ImageView本身默认图片居中显示,不管图片有多大抑或有多小,图像视图都会自动缩放图片,使之刚好够着ImageView的边界,并且缩放后的图片保持原始的宽高比例,看起来图片很完美地占据视图中

央。这种缩放类型在XML文件中通过属性android:scaleType定义,即使图像视图未明确指定该属性,系 统也会默认其值为fitCenter,表示让图像缩放后居中显示。添加了缩放属性的ImageView标签如下所

示:

在Java代码中可调用setScaleType方法设置图像视图的缩放类型,其中fitCenter对应的类型为

ScaleType.FIT_CENTER,设置代码示例如下:

除了居中显示,图像视图还提供了其他缩放类型,详细的缩放类型取值说明见表3-3。

表3-3 缩放类型的取值说明

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sY0B8NPj-1676725568047)(media/3dd087f9907a1d4cb061660fc4a157bb.jpeg)]

注意居中显示fitCenter是默认的缩放类型,它的图像效果如之前的图3-23所示。其余缩放类型的图像显示效果分别如图3-24到图3-29所示,其中图3-24为centerCrop的效果图,图3-25为centerInside的效果图,图3-26为center的效果图,图3-27为fitXY的效果图,图3-28为fitStart的效果图,图3-29为fitEnd的效果图。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7lUECHDR-1676725568048)(media/2a0bc9e12c4a10fae1eca823f7a6c146.jpeg)]

图3-24 缩放类型为centerCrop的效果图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MLbjkfP2-1676725568048)(media/ac9191b35df3822843a972c95f7dc7d8.jpeg)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zk6iXEBS-1676725568048)(media/4b716c33f54fc50ae47430802502cc24.jpeg)]图3-25 缩放类型为centerInside的效果图

图3-26 缩放类型为center的效果图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JfX5JHpQ-1676725568050)(media/c5b2c23e0a32552201f6fab33e41cdc1.jpeg)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l5FtFUKd-1676725568051)(media/238e30b0c9221d9f0b38fa2191cbe6cd.jpeg)]图3-27 缩放类型为fitXY的效果图

图3-28 缩放类型为fitStart的效果图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6aQLHLcw-1676725568051)(media/ec3902213d62d59d31f9b765f398811f.jpeg)]

图3-29 缩放类型为fitEnd的效果图

注意到centerInside和center的显示效果居然一模一样,这缘于它们的缩放规则设定。表面上

fitCenter、centerInside、center三个类型都是居中显示,且均不越过图像视图的边界。它们之间的区 别在于:fitCenter既允许缩小图片、也允许放大图片,centerInside只允许缩小图片、不允许放大图标,而center自始至终保持原始尺寸(既不允许缩小图片、也不允许放大图片)。因此,当图片尺寸大于视图宽高,centerInside与fitCenter都会缩小图片,此时它俩的显示效果相同;当图片尺寸小于视图 宽高,centerInside与center都保持图片大小不变,此时它俩的显示效果相同。

图像按钮ImageButton

常见的按钮控件Button其实是文本按钮,因为按钮上面只能显示文字,不能显示图片,ImageButton才 是显示图片的图像按钮。虽然ImageButton号称图像按钮,但它并非继承Button,而是继承了

ImageView,所以凡是ImageView拥有的属性和方法,ImageButton统统拿了过来,区别在于

ImageButton有个按钮背景。

尽管ImageButton源自ImageView,但它毕竟是个按钮呀,按钮家族常用的点击事件和长按事件,

ImageButton全都没落下。不过ImageButton和Button之间除了名称不同,还有下列差异:

Button既可显示文本也可显示图片(通过setBackgroundResource方法设置背景图片),而

ImageButton只能显示图片不能显示文本。

ImageButton上的图像可按比例缩放,而Button通过背景设置的图像会拉伸变形,因为背景图采取

fitXY方式,无法按比例缩放。

Button只能靠背景显示一张图片,而ImageButton可分别在前景和背景显示图片,从而实现两张图 片叠加的效果。

从上面可以看出,Button与ImageButton各有千秋,通常情况使用Button就够用了。但在某些场合,比 如输入法打不出来的字符,以及特殊字体显示的字符串,就适合先切图再放到ImageButton。举个例

子,数学常见的开平方运算,由输入法打出来的运算符号为“√”,但该符号缺少右上角的一横,正确的开 平方符号是带横线的,此时便需要通过ImageButton显示这个开方图片。

不过使用ImageButton得注意,图像按钮默认的缩放类型为center(保持原始尺寸不缩放图片),而非 图像视图默认的fitCenter,倘若图片尺寸较大,那么图像按钮将无法显示整个图片。为避免显示不完整的情况,XML文件中的ImageButton标签必须指定fitCenter的缩放类型,详细的标签内容示例如下:

(完整代码见chapter03\src\main\res\layout\activity_image_button.xml)

运行测试App,打开演示界面如图3-30所示,可见图像按钮正确展示了开平方符号。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UdFyIzMd-1676725568052)(media/8268a530b78adbe4340aaad4ec978493.jpeg)]

图3-30 显示开平方符号的图像按钮

同时展示文本与图像

现在有了Button可在按钮上显示文字,又有ImageButton可在按钮上显示图像,照理说绝大多数场合都 够用了。然而现实项目中的需求往往捉摸不定,例如客户要求在按钮文字的左边加一个图标,这样按钮 内部既有文字又有图片,乍看之下Button和ImageButton都没法直接使用。若用LinearLayout对

ImageView和TextView组合布局,虽然可行,XML文件却变得冗长许多。

其实有个既简单又灵活的办法,要想在文字周围放置图片,使用按钮控件Button就能实现。原来Button 悄悄提供了几个与图标有关的属性,通过这些属性即可指定文字旁边的图标,以下是有关的图标属性说 明。

drawableTop:指定文字上方的图片。drawableBottom:指定文字下方的图片。drawableLeft:指定文字左边的图片。drawableRight:指定文字右边的图片。drawablePadding:指定图片与文字的间距。

譬如下面是个既有文字又有图标的Button标签例子:

(完整代码见chapter03\src\main\res\layout\activity_image_text.xml)

以上的Button标签通过属性android:drawableTop设置了文字上边的图标,若想变更图标所处的位置, 只要把drawableTop换成对应方向的属性即可。各方向的图文混排按钮效果分别如图3-31到图3-34所示,其中图3-31为指定了drawableTop的按钮界面,图3-32为指定了drawableBottom的按钮界面,图3-33为指定了drawableLeft的按钮界面,图3-34为指定了drawableRight的按钮界面。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k5XdAwMD-1676725568052)(media/750867c648029e93c10a48a4c3286324.jpeg)]

图3-31 指定了drawableTop的按钮界面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eaVBpku8-1676725568053)(media/276089e1429a7ea38797890892113bf7.jpeg)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fwGoW9sg-1676725568055)(media/8eee6f0c2325465bdfb03575cb9bca86.jpeg)]图3-32 指定了drawableBottom的按钮界面

图3-33 指定了drawableLeft的按钮界面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0RY2fFHO-1676725568055)(media/8c7a5a31477eee4be0530635e4c614c7.jpeg)]

图3-34 指定了drawableRight的按钮界面

.6 实战项目:计算器

本章虽然只学了一些Android的简单控件,但是只要活学善用这些布局和控件,也能够做出实用的App。接下来让我们尝试设计并实现一个简单计算器。

需求描述

计算器是人们日常生活中最常用的工具之一,无论在电脑上还是手机上,都少不了计算器的身影。以

Windows系统自带的计算器为例,它的界面简洁且十分实用,如图3-35所示。

计算器的界面分为两大部分,第一部分是上方的计算表达式,既包括用户的按键输入,也包括计算结果 数字;第二部分是下方的各个按键,例如:从0到9的数字按钮、加减乘除与等号、正负号按钮、小数点 按钮、求倒数按钮、平方按钮、开方按钮,以及退格、清空、取消等控制按钮。通过这些按键操作,能 够实现整数和小数的四则运算,以及求倒数、求平方、求开方等简单运算。

界面设计

上一小节介绍的Windows计算器,它主要由上半部分的计算结果与下半部分的计算按钮两块区域组成, 据此可创建一个界面相似的计算器App,同样由计算结果和计算按钮两部分组成,如图3-36所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N9G6LVUg-1676725568056)(media/0eb2061f034cd4209ebca6e2187d58df.jpeg)]

图3-35 Windwos系统自带的计算器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cDiS958h-1676725568056)(media/6c9692d13cf88d14c63da7f9d6ac76e3.jpeg)]

图3-36 计算器App的效果图按照计算器App的效果图,大致分布着下列Android控件:

线性布局LinearLayout:因为计算器界面整体从上往下布局,所以需要垂直方向的LinearLayout。

网格布局GridLayout:计算器下半部分的几排按钮,正好成五行四列表格分布,适合采用

GridLayout。

滚动视图ScrollView:虽然计算器界面不宽也不高,但是以防万一,最好还是加个垂直方向的

ScrollView。

文本视图TextView:很明显顶部标题“简单计算器”就是TextView,且文字居中显示;标题下面的计 算结果也需要使用TextView,且文字靠右靠下显示。

按钮Button:几乎所有的数字与运算符按钮都采用了Button控件。

图像按钮ImageButton:开根号的运算符“√”虽然能够打出来,但是右上角少了数学课本上的一横, 所以该按钮要显示一张标准的开根号图片,这用到了ImageButton。

关键代码

App同用户交互的过程中,时常要向用户反馈一些信息,例如:点错了按钮、输入了非法字符等等,诸 如此类。对于这些一句话的提示,Android设计了Toast控件,用于展示短暂的提示文字。Toast的用法 很简单,只需以下一行代码即可弹出提示小窗:

上面代码用到了两个方法,分别是makeText和show,其中show方法用来展示提示窗,而makeText方 法用来构建提示文字的模板。makeText的第一个参数为当前页面的实例,倘若当前页面名为MainActivity的话,这里就填MainActivity.this,当然如果不引发歧义的话,直接填this也可以;第二个参数为准备显示的提示文本;第三个参数规定了提示窗的驻留时长,为Toast.LENGTH_SHORT表示停留

2秒后消失,为Toast.LENGTH_LONG表示停留3.5秒后消失。

对于计算器来说,有好几种情况需要提示用户,比如“被除数不能为零”、“开根号的数值不能小于零”、

“不能对零求倒数”等等,这时就能通过Toast控件弹窗提醒用户。Toast弹窗的展示效果如图3-37所示, 此时App发现了被除数为零的情况。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cDtArS5Z-1676725568057)(media/9adf04319353227fb68222514f15b215.jpeg)]

图3-37 Toast弹窗效果

对于简单计算来说,每次运算至少需要两个操作数,比如加减乘除四则运算就要求有两个操作数,求倒 数、求平方、求开方只要求一个操作数;并且每次运算过程有且仅有一个运算符(等号不计在内),故 而计算器App得事先声明下列几个字符串变量:

用户在计算器界面每输入一个按键,App都要进行下列两项操作:

输入按键的合法性校验

在开展计算之前,务必检查用户的按键是否合法,因为非法按键将导致不能正常运算。非法的按键输入 包括但不限于下列情况:

  1. 被除数不能为零。

  2. 开根号的数值不能小于零。

  3. 不能对零求倒数。

  4. 一个数字不能有两个小数点。

  5. 如果没输入运算符,就不能点击等号按钮。

  6. 如果没输入操作数,也不能点击等号按钮。

    比如点击等号按钮之时,App的逻辑校验代码示例如下:

    (完整代码见chapter03\src\main\java\com\example\chapter03\CalculatorActivity.java)

执行运算并显示计算结果

合法性校验通过,方能继续接下来的业务逻辑,倘若用户本次未输入与计算有关的按钮(例如等号、求 倒数、求平方、求开方),则计算器只需拼接操作数或者运算符;倘若用户本次输入了与计算有关的按 钮(例如等号、求倒数、求平方、求开方),则计算器立即执行运算操作并显示计算结果。以加减乘除 四则运算为例,它们的计算代码例子如下所示:

完成合法性校验与运算处理之后,计算器App的编码基本结束了。运行计算器App,执行各种运算的界 面效果如图3-38和图3-39所示。其中图3-38为执行乘法运算8×9=?的计算器界面,图3-39为先对8做开方 再给开方结果加上60的计算器界面。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ztck79Rp-1676725568057)(media/089024e2a834d86d6c7d3d3ceb310464.jpeg)]

图3-38 执行乘法运算的计算器界面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iIE6VVeq-1676725568058)(media/95897c6c2a876f0ecf2cc8e5acc63205.jpeg)]

图3-39 先执行开方运算再执行加法运算

.7 小结

本章主要介绍了App开发中常见简单控件的用法,包括:在文本视图上显示文本(设置文本的内容、大 小和颜色)、修改视图的基本属性(设置视图的宽高、间距和对齐方式)、运用各种布局排列控件(线 性布局、相对布局、网格布局、滚动视图)、处理按钮的触控事件(按钮控件的点击、长按、禁用与恢 复)、在图像控件上显示图片(图像视图、图像按钮、同时展示文本与图像)。最后设计了一个实战项 目“简单计算器”,在该项目的App编码中用到了前面介绍的大部分控件和布局,从而加深了对所学知识的理解。

通过本章的学习,读者应该能掌握以下4种开发技能:

  1. 学会在文本控件上正确展示文字。
  2. 学会在图像控件上正确展示图片。
  3. 学会正确处理按钮的点
  4. 学会在常见布局上排列组合多个控件。

.8 课后练习题

一、填空题

  1. res/values目录下存放字符串定义的资源文件名为。

  2. 指的是与设备无关的显示单位。

  3. Android的色值由alpha透明度和 _ 三原色联合定义。

  4. 线性布局利用属性layout_weight设置下级控件的尺寸权重时,要将下级控件的宽高设置为 _。

  5. 按钮控件被按住超过 _ 之后,会触发长按事件。

    二、判断题(正确打√,错误打×)

  6. Android的控件类都由ViewGroup派生而来。( )

  7. 线性布局LinearLayout默认下级控件在水平方向排列。( )

  8. 在相对布局内部,如果不设定下级视图的参照物,那么下级视图默认显示在布局中央。( )

  9. 滚动视图ScrollView默认下级布局在水平方向排列。( )

  10. 按钮控件上的英文默认显示大写字母。( )

    三、选择题

  11. Java代码中,setTextSize方法默认的字号单位是( )。

A.dp B.px C.sp D.dip

  1. 网格布局GridLayout指定网格行数的属性名称是( )。

A.columnCount B.rowCount

C.gridCount D.cellCount

  1. 图像视图采取缩放类型( )的时候,图像可能会被拉伸变形。

A.FIT_CENTER B.CENTER_CROP C.FIT_XY D.FIT_START

  1. 图像按钮ImageButton由( )派生而来。
    1. Button
  2. ImageView C.View D.ViewGroup
  3. 在按钮控件中,把图片放在文本右边的属性名称是( )。

A.drawableTop B.drawableBottom C.drawableLeft D.drawableRight

四、简答题

  1. 请简要描述layout_margin和padding之间的区别。

  2. 请简要描述layout_gravity和gravity之间的区别。

    五、动手练习

    请上机实验本章的计算器项目,要求实现加、减、乘、除、求倒数、求平方根等简单运算。

第4章 活动Activity

本章介绍Android 4大组件之一Activity的基本概念和常见用法。主要包括如何正确地启动和停止活动页面、如何在两个活动之间传递各类消息、如何在意图之外给活动添加额外的信息,等等。

.1 启停活动页面

本节介绍如何正确地启动和停止活动页面,首先描述活动页面的启动方法与结束方法,用户看到的页面 就是开发者塑造的活动;接着详细分析活动的完整生命周期,以及每个周期方法的发生场景和流转过 程;然后描述活动的几种启动模式,以及如何在代码中通过启动标志控制活动的跳转行为。

Activity的启动和结束

在第2章的“2.4.3 跳到另一个页面”一节中,提到通过startActivity方法可以从当前页面跳到新页面,具体格式如“startActivity(new Intent(源页面.this, 目标页面.class));”。由于当时尚未介绍按钮控件,因此只好延迟3秒后才自动调用startActivity方法。现在有了按钮控件,就能利用按钮的点击事件去触发页面跳转,譬如以下代码便在重写后的点击方法onClick中执行页面跳转动作。

(完整代码见chapter04\src\main\java\com\example\chapter04\ActStartActivity.java)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LnXw1lLV-1676725568059)(media/198a4f455556926570827cb7299d51a0.jpeg)]以上代码中的startActivity方法,清楚标明了从当前页面跳到新的ActFinishActivity页面。之所以给新页面取名ActFinishActivity,是为了在新页面中演示如何关闭页面。众所周知,若要从当前页面回到上一个 页面,点击屏幕底部的返回键即可实现,但不是所有场景都使用返回键。比如页面左上角的箭头图标经 常代表着返回动作,况且有时页面上会出现“完成”按钮,无论点击箭头图标还是点击完成按钮,都要求 马上回到上一个页面。包含箭头图标与“完成”按钮的演示界面如图4-1所示。

图4-1 箭头图标与完成按钮

既然点击某个图标或者点击某个按钮均可能触发返回动作,就需要App支持在某个事件发生时主动返回 上一页。回到上一个页面其实相当于关闭当前页面,因为最开始由A页面跳到B页面,一旦关闭了B页 面,App应该展示哪个页面呢?当然是展示跳转之前的A页面了。在Java代码中,调用finish方法即可关 闭当前页面,前述场景要求点击箭头图标或完成按钮都返回上一页面,则需给箭头图标和完成按钮分别

注册点击监听器,然后在onClick方法中调用finish方法。下面便是添加了finish方法的新页面代码例子:

(完整代码见chapter04\src\main\java\com\example\chapter04\ActFinishActivity.java)

另外,所谓“打开页面”或“关闭页面”沿用了浏览网页的叫法,对于App而言,页面的真实名称是“活动”—

Activity。打开某个页面其实是启动某个活动,所以有startActivity方法却无openActivity方法;关闭某个页面其实是结束某个活动,所以有finish方法却无close方法。

Activity的生命周期

App引入活动的概念而非传统的页面概念,这是有原因的,单从字面意思理解,页面更像是静态的,而 活动更像是动态的。犹如花开花落那般,活动也有从含苞待放到盛开再到凋零的生命过程。每次创建新 的活动页面,自动生成的Java代码都给出了onCreate方法,该方法用于执行活动创建的相关操作,包括 加载XML布局、设置文本视图的初始文字、注册按钮控件的点击监听,等等。onCreate方法所代表的创建动作,正是一个活动最开始的行为,除了onCreate,活动还有其他几种生命周期行为,它们对应的方法说明如下:

onCreate:创建活动。此时会把页面布局加载进内存,进入了初始状态。

onStart:开启活动。此时会把活动页面显示在屏幕上,进入了就绪状态。

onResume:恢复活动。此时活动页面进入活跃状态,能够与用户正常交互,例如允许响应用户的点击动作、允许用户输入文字等。

onPause:暂停活动。此时活动页面进入暂停状态(也就是退回就绪状态),无法与用户正常交互。

onStop: 停 止 活 动 。 此 时 活 动 页 面 将 不 在 屏 幕 上 显 示 。 onDestroy:销毁活动。此时回收活动占用的系统资源,把页面从内存中清除掉。

onRestart:重启活动。处于停止状态的活动,若想重新开启的话,无须经历onCreate的重复创建 过程,而是走onRestart的重启过程。

onNewIntent:重用已有的活动实例。

上述的生命周期方法,涉及复杂的App运行状态,更直观的活动状态切换过程如图4-2所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yzSqzM4q-1676725568059)(media/ec3f4beaadf376c77ad07ae83cd290f2.jpeg)]

图4-2 活动的状态变迁

如果一个Activity已经启动过,并且存在当前应用的Activity任务栈中,启动模式为singleTask,

singleInstance或singleTop(此时已在任务栈顶端),那么在此启动或回到这个Activity的时候,不会创建新的实例,也就是不会执行onCreate方法,而是执行onNewIntent方法。

Activity的启动模式

上一小节提到,从第一个活动跳到第二个活动,接着结束第二个活动就能返回第一个活动,可是为什么 不直接返回桌面呢?这要从Android的内核设计说起了,系统给每个正在运行的App都分配了活动栈,栈里面容纳着已经创建且尚未销毁的活动信息。鉴于栈是一种先进后出、后进先出的数据结构,故而后面 入栈的活动总是先出栈,假设3个活动的入栈顺序为:活动A→活动B→活动C,则它们的出栈顺序将变 为:活动C→活动B→活动A,可见活动C结束之后会返回活动B,而不是返回活动A或者别的地方。

假定某个App分配到的活动栈大小为3,该App先后打开两个活动,此时活动栈的变动情况如图4-7所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qIGtLZMd-1676725568060)(media/d6d09f28476d5e77924671eceeda17f8.jpeg)]

图4-7 两个活动先后入栈

然后按下返回键,依次结束已打开的两个活动,此时活动栈的变动情况如图4-8所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-op1BVqSO-1676725568061)(media/b9b857876f88afd10c1f1b78af0d73cf.jpeg)]

图4-8 两个活动依次出栈

结合图4-7与图4-8的入栈与出栈流程,即可验证结束活动之时的返回逻辑了。

不过前述的出入栈情况仅是默认的标准模式,实际上Android允许在创建活动时指定该活动的启动模 式,通过启动模式控制活动的出入栈行为。App提供了两种办法用于设置活动页面的启动模式,其一是 修改AndroidManifest.xml,在指定的activity节点添加属性android:launchMode,表示本活动以哪个

启动模式运行。其二是在代码中调用Intent对象的setFlags方法,表明后续打开的活动页面采用该启动标 志。下面分别予以详细说明。

1.在配置文件中指定启动模式

打开AndroidManifest.xml,给activity节点添加属性android:launchMode,属性值填入standard表示 采取标准模式,当然不添加属性的话默认就是标准模式。具体的activity节点配置内容示例如下:

其中launchMode属性的几种取值说明见表4-1。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WxmZ3Sq5-1676725568063)(media/8e557250fa6a738d3071dd303c4ca2d8.jpeg)]表4-2 代码中的启动标志取值说明

接下来举两个例子阐述启动模式的实际应用:在两个活动之间交替跳转、登录成功后不再返回登录页 面,分别介绍如下。

在两个活动之间交替跳转

假设活动A有个按钮,点击该按钮会跳到活动B;同时活动B也有个按钮,点击按钮会跳到活动A;从首页打开活动A之后,就点击按钮在活动A与活动B之间轮流跳转。此时活动页面的跳转流程为:首页→活动

A→活动B→活动A→活动B→活动A→活动B→……多次跳转之后想回到首页,正常的话返回流程是这样的:……→活动B→活动A→活动B→活动A→活动B→活动A→首页,注意每个箭头都代表按一次返回键,

可见要按下许多次返回键才能返回首页。其实在活动A和活动B之间本不应该重复返回,因为回来回去总 是这两个页面有什么意义呢?照理说每个活动返回一次足矣,同一个地方返回两次已经是多余的了,再 返回应当回到首页才是。也就是说,不管过去的时候怎么跳转,回来的时候应该按照这个流程:……→活动B→活动A→首页,或者按照这个流程:……→活动A→活动B→首页,总之已经返回了的页面,决不再返回第二次。

对于不允许重复返回的情况,可以设置启动标志FLAG_ACTIVITY_CLEAR_TOP,即使活动栈里面存在待跳转的活动实例,也会重新创建该活动的实例,并清除原实例上方的所有实例,保证栈中最多只有该活 动的唯一实例,从而避免了无谓的重复返回。于是活动A内部的跳转代码就改成了下面这般:

(完整代码见chapter04\src\main\java\com\example\chapter04\JumpFirstActivity.java)

当然活动B内部的跳转代码也要设置同样的启动标志:

(完整代码见chapter04\src\main\java\com\example\chapter04\JumpSecondActivity.java)

这下两个活动的跳转代码都设置了FLAG_ACTIVITY_CLEAR_TOP,运行测试App发现多次跳转之后,每个活动仅会返回一次而已。

登录成功后不再返回登录页面

很多App第一次打开都要求用户登录,登录成功再进入App首页,如果这时按下返回键,发现并没有回 到上一个登录页面,而是直接退出App了,这又是什么缘故呢?原来用户登录成功后,App便记下用户 的登录信息,接下来默认该用户是登录状态,自然不必重新输入用户名和密码了。既然默认用户已经登 录,哪里还需要回到登录页面?不光登录页面,登录之前的其他页面包括获取验证码、找回密码等页面 都不应回去,每次登录成功之后,整个App就焕然一新仿佛忘记了有登录页面这回事。

对于回不去的登录页面情况,可以设置启动标志FLAG_ACTIVITY_CLEAR_TASK,该标志会清空当前活动栈里的所有实例。不过全部清空之后,意味着当前栈没法用了,必须另外找个活动栈才行,也就是同时 设置启动标志FLAG_ACTIVITY_NEW_TASK,该标志用于开辟新任务的活动栈。于是离开登录页面的跳转代码变成下面这样:

(完整代码见chapter04\src\main\java\com\example\chapter04\LoginInputActivity.java)

运行测试App,登录成功进入首页之后,点击返回键果然没回到登录页面。

默认启动模式 standard

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JMspB7uq-1676725568063)(media/a691dd830722c61a437d1f1aa8060653.png)]该模式可以被设定,不在 manifest 设定时候,Activity 的默认模式就是 standard。在该模式下,启动的 Activity 会依照启动顺序被依次压入 Task 栈中:

栈顶复用模式 singleTop

在该模式下,如果栈顶 Activity 为我们要新建的 Activity(目标Activity),那么就不会重复创建新的Activity。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y0U1Nes7-1676725568064)(media/8c89d6a20050715f4e1582cb6e2ec3f5.png)]

应用场景

适合开启渠道多、多应用开启调用的 Activity,通过这种设置可以避免已经创建过的 Activity 被重复创建,多数通过动态设置使用。

栈内复用模式 singleTask

与 singleTop 模式相似,只不过 singleTop 模式是只是针对栈顶的元素,而 singleTask 模式下,如果

task 栈内存在目标 Activity 实例,则将 task 内的对应 Activity 实例之上的所有 Activity 弹出栈,并将对应 Activity 置于栈顶,获得焦点。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wCQOjd1h-1676725568064)(media/4d229de9b33f239d03a0bda0b96d5323.png)]

应用场景

程序主界面:我们肯定不希望主界面被创建多次,而且在主界面退出的时候退出整个 App 是最好的效果。

耗费系统资源的Activity:对于那些及其耗费系统资源的 Activity,我们可以考虑将其设为 singleTask

模式,减少资源耗费。

全局唯一模式 singleInstance

在该模式下,我们会为目标 Activity 创建一个新的 Task 栈,将目标 Activity 放入新的 Task,并让目标

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1nTu3HbY-1676725568064)(media/d1d0a54550c89302b8eb1160e084031c.png)]Activity获得焦点。新的 Task 有且只有这一个 Activity 实例。 如果已经创建过目标 Activity 实例,则不会创建新的 Task,而是将以前创建过的 Activity 唤醒。

看一个示例,Activity3 设置为singleInstance,Activity1 和 Activity2 默认(standard),下图程序流程中,黄色的代表 Background 的Task,蓝色的代表 Foreground 的Task。返回时会先把 Foreground 的

Task 中的 Activity 弹出,直到 Task 销毁,然后才将 Background的 Task 唤到前台,所以最后将

Activity3 销毁之后,会直接退出应用。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-f2SFuGVz-1676725568065)(media/789b181345641971c28bc95fdacb4b83.jpeg)]

动态设置启动模式

在上述所有情况,都是我们在Manifest中通过 launchMode 属性设置的,这个被称为静态设置,动态设置是通过 Java 代码设置的。

通过 Intent 动态设置 Activity启动模式

如果同时有动态和静态设置,那么动态的优先级更高。接下来我们来看一下如何动态的设置 Activity 启动模式。

FLAG_ACTIVITY_NEW_TASK

对应 Flag

此 Flag 跟 singleInstance 很相似,在给目标 Activity 设立此 Flag 后,会根据目标 Activity 的 affinity 进行匹配,如果已经存在与其affinity 相同的 task,则将目标 Activity 压入此 Task。反之没有的话,则新建一个 task,新建的 task 的 affinity 值与目标 Activity 相同,然后将目标 Activity 压入此栈。

但它与 singleInstance 有不同的点,两点需要注意的地方:

新的 Task 没有说只能存放一个目标 Activity,只是说决定是否新建一个 Task,而 singleInstance

模式下新的 Task 只能放置一个目标 Activity。

在同一应用下,如果 Activity 都是默认的 affinity,那么此 Flag 无效,而 singleInstance 默认情况也会创建新的 Task。

FLAG_ACTIVITY_SINGLE_TOP

该模式比较简单,对应Flag如下:

此 Flag 与静态设置中的 singleTop 效果相同

FLAG_ACTIVITY_CLEAR_TOP

这个模式对应的Flag如下:

当设置此 Flag 时,目标 Activity 会检查 Task 中是否存在此实例,如果没有则添加压入栈。如果有,就将位于 Task 中的对应 Activity 其上的所有 Activity 弹出栈,此时有以下两种情况:

如果同时设置 Flag_ACTIVITY_SINGLE_TOP ,则直接使用栈内的对应 Activity。

没有设置,则将栈内的对应 Activity 销毁重新创建。按位或运算符

运算规则:0|0=0 0|1=1 1|0=1 1|1=1

总结:参加运算的两个对象只要有一个为1,其值为1。

例如:3|5即 0000 0011| 0000 0101 = 0000 0111,因此,3|5的值得7

.2 在活动之间传递消息

本节介绍如何在两个活动之间传递各类消息,首先描述Intent的用途和组成部分,以及显式Intent和隐式

Intent的区别;接着阐述结合Intent和Bundle向下一个活动页面发送数据,再在下一个页面中解析收到 的请求数据;然后叙述从下一个活动页面返回应答数据给上一个页面,并由上一个页面解析返回的应答 数据。

显式Intent和隐式Intent

上一小节的Java代码,通过Intent对象设置活动的启动标志,这个Intent究竟是什么呢?Intent的中文名 是意图,意思是我想让你干什么,简单地说,就是传递消息。Intent是各个组件之间信息沟通的桥梁, 既能在Activity之间沟通,又能在Activity与Service之间沟通,也能在Activity与Broadcast之间沟通。总 而言之,Intent用于Android各组件之间的通信,它主要完成下列3部分工作:

  1. 标明本次通信请求从哪里来、到哪里去、要怎么走。

  2. 发起方携带本次通信需要的数据内容,接收方从收到的意图中解析数据。

  3. 发起方若想判断接收方的处理结果,意图就要负责让接收方传回应答的数据内容。 为了做好以上工作,就要给意图配上必需的装备,Intent的组成部分见表4-3。

    表4-3 Intent组成元素的列表说明

元素名称设置方法说明与用途
ComponentsetComponent组件,它指定意图的来源与目标
ActionsetAction动作,它指定意图的动作行为
DatasetData即Uri,它指定动作要操纵的数据路径
CategoryaddCategory类别,它指定意图的操作类别
TypesetType数据类型,它指定消息的数据类型
ExtrasputExtras扩展信息,它指定装载的包裹信息
FlagssetFlags标志位,它指定活动的启动标志

指定意图对象的目标有两种表达方式,一种是显式Intent,另一种是隐式Intent。

显式Intent,直接指定来源活动与目标活动,属于精确匹配

在构建一个意图对象时,需要指定两个参数,第一个参数表示跳转的来源页面,即“来源Activity.this”; 第二个参数表示待跳转的页面,即“目标Activity.class”。具体的意图构建方式有如下3种:

  1. 在Intent的构造函数中指定,示例代码如下:
  2. 调用意图对象的setClass方法指定,示例代码如下:
  3. 调用意图对象的setComponent方法指定,示例代码如下:
隐式Intent,没有明确指定要跳转的目标活动,只给出一个动作字符串让系统自动匹配,属于模糊 匹配

通常App不希望向外部暴露活动名称,只给出一个事先定义好的标记串,这样大家约定俗成、按图索骥 就好,隐式Intent便起到了标记过滤作用。这个动作名称标记串,可以是自己定义的动作,也可以是已有的系统动作。常见系统动作的取值说明见表4-4。

表4-4 常见系统动作的取值说明

Intent 类的系统动作常量名系统动作的常量值说明
ACTION_MAINandroid.intent.action.MAINApp启动时的入口
ACTION_VIEWandroid.intent.action.VIEW向用户显示数据
ACTION_SENDandroid.intent.action.SEND分享内容
ACTION_CALLandroid.intent.action.CALL直接拨号
ACITON_DIALandroid.intent.action.DIAL准备拨号
ACTION_SENDTOandroid.intent.action.SENDTO发送短信
ACTION_ANSWERandroid.intent.action.ANSWER接听电话

动作名称既可以通过setAction方法指定,也可以通过构造函数Intent(String action)直接生成意图对象。当然,由于动作是模糊匹配,因此有时需要更详细的路径,比如仅知道某人住在天通苑小区,并不能直 接找到他家,还得说明他住在天通苑的哪一期、哪栋楼、哪一层、哪一个单元。Uri和Category便是这样的路径与门类信息,Uri数据可通过构造函数Intent(String action, Uri uri)在生成对象时一起指定,也可 通过setData方法指定(setData这个名字有歧义,实际相当于setUri);Category可通过addCategory 方法指定,之所以用add而不用set方法,是因为一个意图允许设置多个Category,方便一起过滤。

下面是一个调用系统拨号程序的代码例子,其中就用到了Uri:

(完整代码见chapter04\src\main\java\com\example\chapter04\ActionUriActivity.java)

隐式Intent还用到了过滤器的概念,把不符合匹配条件的过滤掉,剩下符合条件的按照优先顺序调用。譬如创建一个App模块,AndroidManifest.xml里的intent-filter就是配置文件中的过滤器。像最常见的首页活动MainAcitivity,它的activity节点下面便设置了action和category的过滤条件。其中

android.intent.action.MAIN表示App的入口动作,而android.intent.category.LAUNCHER表示在桌面上显示App图标,配置样例如下:

向下一个Activity发送数据

上一小节提到,Intent对象的setData方法只指定到达目标的路径,并非本次通信所携带的参数信息,真正的参数信息存放在Extras中。Intent重载了很多种putExtra方法传递各种类型的参数,包括整型、双 精度型、字符串等基本数据类型,甚至Serializable这样的序列化结构。只是调用putExtra方法显然不好 管理,像送快递一样大小包裹随便扔,不但找起来不方便,丢了也难以知道。所以Android引入了

Bundle概念,可以把Bundle理解为超市的寄包柜或快递收件柜,大小包裹由Bundle统一存取,方便又 安全。

Bundle内部用于存放消息的数据结构是Map映射,既可添加或删除元素,还可判断元素是否存在。开发者若要把Bundle数据全部打包好,只需调用一次意图对象的putExtras方法;若要把Bundle数据全部取 出来,也只需调用一次意图对象的getExtras方法。Bundle对象操作各类型数据的读写方法说明见表4- 5。

表4-5 Bundle对各类型数据的读写方法说明

数据类型读方法写方法
整型数getIntputInt
浮点数getFloatputFloat
双精度数getDoubleputDouble
布尔值getBooleanputBoolean
字符串getStringputString
字符串数组getStringArrayputStringArray
字符串列表getStringArrayListputStringArrayList
可序列化结构getSerializableputSerializable

接下来举个在活动之间传递数据的例子,首先在上一个活动使用包裹封装好数据,把包裹塞给意图对 象,再调用startActivity方法跳到意图指定的目标活动。完整的活动跳转代码示例如下:

(完整代码见chapter04\src\main\java\com\example\chapter04\ActSendActivity.java)

然后在下一个活动中获取意图携带的快递包裹,从包裹取出各参数信息,并将传来的数据显示到文本视 图。下面便是目标活动获取并展示包裹数据的代码例子:

(完整代码见chapter04\src\main\java\com\example\chapter04\ActReceiveActivity.java)

代码编写完毕,运行测试App,打开上一个页面如图4-9所示。单击页面上的发送按钮跳到下一个页面如图4-10所示,根据展示文本可知正确获得了传来的数据。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5rNa2PHG-1676725568066)(media/387dc24c3751d7e0c6587cfa335d8b78.jpeg)]

图4-9 上一个页面将要发送数据

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1OGBnzqp-1676725568066)(media/d4faca80d5964d66888a74ebbaf95bd4.jpeg)]

图4-10 下一个页面收到传来的数据

向上一个Activity返回数据

数据传递经常是相互的,上一个页面不但把请求数据发送到下一个页面,有时候还要处理下一个页面的 应答数据,所谓应答发生在下一个页面返回到上一个页面之际。如果只把请求数据发送到下一个页面, 上一个页面调用startActivity方法即可;如果还要处理下一个页面的应答数据,此时就得分多步处理,详 细步骤说明如下:

步骤一,上一个页面打包好请求数据,调用startActivityForResult方法执行跳转动作,表示需要处理下 一个页面的应答数据,该方法的第二个参数表示请求代码,它用于标识每个跳转的唯一性。跳转代码示 例如下:

(完整代码见chapter04\src\main\java\com\example\chapter04\ActRequestActivity.java)

步骤二,下一个页面接收并解析请求数据,进行相应处理。接收代码示例如下:

(完整代码见chapter04\src\main\java\com\example\chapter04\ActResponseActivity.java)

步骤三,下一个页面在返回上一个页面时,打包应答数据并调用setResult方法返回数据包裹。setResult 方法的第一个参数表示应答代码(成功还是失败),第二个参数为携带包裹的意图对象。返回代码示例 如下:

(完整代码见chapter04\src\main\java\com\example\chapter04\ActResponseActivity.java)

步骤四,上一个页面重写方法onActivityResult,该方法的输入参数包含请求代码和结果代码,其中请求 代码用于判断这次返回对应哪个跳转,结果代码用于判断下一个页面是否处理成功。如果下一个页面处 理成功,再对返回数据解包操作,处理返回数据的代码示例如下:

(完整代码见chapter04\src\main\java\com\example\chapter04\ActRequestActivity.java)

结合上述的活动消息交互步骤,运行测试App打开第一个活动页面如图4-11所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cgymDCza-1676725568067)(media/6dbd914b0a59c58a74682fd55faca57c.jpeg)]

图4-11 跳转之前的第一个页面

点击传送按钮跳到第二个活动页面如图4-12所示,可见第二个页面收到了请求数据。然后点击第二个页 面的返回按钮,回到第一个页面如图4-13所示,可见第一个页面成功收到了第二个页面的应答数据。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cB2fkMnh-1676725568067)(media/59e21f481360c097739e0d54a4f6d387.jpeg)]

图4-12 跳转到第二个页面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w3zcF6MY-1676725568068)(media/d0dabb694d78f7487c616d271ecfc05e.jpeg)]

图4-13 返回到第一个页面

.3 为活动补充附加信息

本节介绍如何在意图之外给活动添加额外的信息,首先可以把字符串参数放到字符串资源文件中,待

App运行之时再从资源文件读取字符串值;接着还能在AndroidManifest.xml中给指定活动配置专门的 元数据,App运行时即可获取对应活动的元数据信息;然后利用元数据的resource属性配置更复杂的

XML定义,从而为App注册在长按桌面之时弹出的快捷菜单。

利用资源文件配置字符串

利用Bundle固然能在页面跳转的时候传送数据,但这仅限于在代码中传递参数,如果要求临时修改某个参数的数值,就得去改Java代码。然而直接修改Java代码有两个弊端:

  1. 代码文件那么多,每个文件又有许多行代码,一下子还真不容易找到修改的地方。
  2. 每次改动代码都得重新编译,让Android Studio编译的功夫也稍微费点时间。

有鉴于此,对于可能手工变动的参数,通常把参数名称与参数值的对应关系写入配置文件,由程序在运 行时读取配置文件,这样只需修改配置文件就能改变对应数据了。res\values目录下面的strings.xml就 用来配置字符串形式的参数,打开该文件,发现里面已经存在名为app_name的字符串参数,它配置的是当前模块的应用名称。现在可于app_name下方补充一行参数配置,参数名称叫作“weather_str”,参 数值则为“晴天”,具体的配置内容如下所示:

接着打开活动页面的Java代码,调用getString方法即可根据“R.string.参数名称”获得指定参数的字符串值。获取代码示例如下:

(完整代码见chapter04\src\main\java\com\example\chapter04\ReadStringActivity.java)

上面的getString方法来自于Context类,由于页面所在的活动类AppCompatActivity追根溯源来自

Context这个抽象类,因此凡是活动页面代码都能直接调用getString方法。

然后在onCreate方法中调用showStringResource方法,运行测试App,打开读取页面如图4-14所示, 可见从资源文件成功读到了字符串。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FrC8B8zH-1676725568068)(media/7e5583e3f9368e3751b8330411e9ce1f.jpeg)]

图4-14 从资源文件读取字符串

利用元数据传递配置信息

尽管资源文件能够配置字符串参数,然而有时候为安全起见,某个参数要给某个活动专用,并不希望其 他活动也能获取该参数,此时就不方便到处使用getString了。好在Activity提供了元数据(Metadata) 的概念,元数据是一种描述其他数据的数据,它相当于描述固定活动的参数信息。打开

AndroidManifest.xml,在测试活动的activity节点内部添加meta-data标签,通过属性name指定元数据的名称,通过属性value指定元数据的值。仍以天气为例,添加meta-data标签之后的activity节点如下所 示:

元数据的value属性既可直接填字符串,也可引用strings.xml已定义的字符串资源,引用格式形如

“@string/字符串的资源名称”。下面便是采取引用方式的activity节点配置:

配置好了activity节点的meta-data标签,再回到Java代码获取元数据信息,获取步骤分为下列3步:

调用getPackageManager方法获得当前应用的包管理器。

调用包管理器的getActivityInfo方法获得当前活动的信息对象。

活动信息对象的metaData是Bundle包裹类型,调用包裹对象的getString即可获得指定名称的参数 值。

把上述3个步骤串起来,得到以下的元数据获取代码:

(完整代码见chapter04\src\main\java\com\example\chapter04\MetaDataActivity.java)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KZZAVwr8-1676725568069)(media/e91c9f946d5213b32e7f2a8b6e38a6e1.jpeg)]然后在onCreate方法中调用showMetaData方法,重新运行App观察到的界面如图4-15所示,可见成功 获得AndroidManifest.xml配置的元数据。

图4-15 从配置文件读取元数据

给应用页面注册快捷方式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8QUU2So1-1676725568070)(media/20cca237f1ea52aba27e022fe3805c05.jpeg)]元数据不单单能传递简单的字符串参数,还能传送更复杂的资源数据,从Android 7.1开始新增的快捷方式便用到了这点,譬如在手机桌面上长按支付宝图标,会弹出如图4-16所示的快捷菜单。

图4-16 支付宝的快捷菜单

点击菜单项“扫一扫”,直接打开支付宝的扫码页面;点击菜单项“付钱”,直接打开支付宝的付款页面;点击菜单项“收钱”,直接打开支付宝的收款页面。如此不必打开支付宝首页,即可迅速跳转到常用的App页面,这便是所谓的快捷方式。

那么Android 7.1又是如何实现快捷方式的呢?那得再琢磨琢磨元数据了。原来元数据的meta-data标签除了前面说到的name属性和value属性,还拥有resource属性,该属性可指定一个XML文件,表示元数 据想要的复杂信息保存于XML数据之中。借助元数据以及指定的XML配置,方可完成快捷方式功能,具体的实现过程说明如下:

首先打开res/values目录下的strings.xml,在resources节点内部添加下述的3组(每组两个,共6个)字符串配置,每组都代表一个菜单项,每组又分为长名称和短名称,平时优先展示长名称,当长名称放不 下时才展示短名称。这3组6个字符串的配置定义示例如下:

接着在res目录下创建名为xml的文件夹,并在该文件夹创建shortcuts.xml,这个XML文件用来保存3组 菜单项的快捷方式定义,文件内容如下所示:

(完整代码见chapter04\src\main\res\xml\shortcuts.xml)

<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">

<shortcut

android:shortcutId=“first” android:enabled=“true” android:icon=“@mipmap/ic_launcher” android:shortcutShortLabel=“@string/first_short” android:shortcutLongLabel=“@string/first_long”>

<!-- targetClass指定了点击该项菜单后要打开哪个活动页面 -->

<intent

android:action=“android.intent.action.VIEW” android:targetPackage=“com.example.chapter04” android:targetClass=“com.example.chapter04.ActStartActivity” />

<categories android:name=“android.shortcut.conversation”/>

</shortcut>

<shortcut

android:shortcutId=“second” android:enabled=“true” android:icon=“@mipmap/ic_launcher” android:shortcutShortLabel=“@string/second_short” android:shortcutLongLabel=“@string/second_long”>

<!-- targetClass指定了点击该项菜单后要打开哪个活动页面 -->

<intent

android:action=“android.intent.action.VIEW” android:targetPackage=“com.example.chapter04” android:targetClass=“com.example.chapter04.JumpFirstActivity” />

<categories android:name=“android.shortcut.conversation”/>

</shortcut>

<shortcut

由上述的XML例子中看到,每个shortcut节点都代表了一个菜单项,该节点的各属性说明如下:

shortcutId:快捷方式的编号。

enabled:是否启用快捷方式。true表示启用,false表示禁用。icon: 快 捷 菜 单 左 侧 的 图 标 。 shortcutShortLabel:快捷菜单的短标签。

shortcutLongLabel:快捷菜单的长标签。优先展示长标签的文本,长标签放不下时才展示短标签 的文本。

以上的节点属性仅仅指明了每项菜单的基本规格,点击菜单项之后的跳转动作还要由shortcut内部的

intent节点定义,该节点主要有targetPackage与targetClass两个属性需要修改,其中targetPackage属 性固定为当前App的包名,而targetClass属性描述了菜单项对应的活动类完整路径。

然后打开AndroidManifest.xml,找到MainActivity所在的activity节点,在该节点内部补充如下的元数据配置,其中name属性为android.app.shortcuts,而resource属性为@xml/shortcuts:

这行元数据的作用,是告诉App首页有个快捷方式菜单,其资源内容参见位于xml目录下的

shortcuts.xml。完整的activity节点配置示例如下:

然后把测试应用安装到手机上,回到桌面长按应用图标,此时图标下方弹出如图4-17所示的快捷菜单列 表。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7sOPgP3P-1676725568070)(media/28eeebea9d1568f4df3ec4d7cefe7926.jpeg)]

图4-17 测试应用的快捷菜单

点击其中一个菜单项,果然跳到了配置的活动页面,证明元数据成功实现了类似支付宝的快捷方式。

.4 小结

本章主要介绍了活动组件—Activity的常见用法,包括:正确启停活动页面(Activity的启动和结束、

Activity的生命周期、Activity的启动模式)、在活动之间传递消息(显式Intent和隐式Intent、向下一个

Activity发送数据、向上一个Activity返回数据)、为活动补充附加信息(利用资源文件配置字符串、利 用元数据传递配置信息、给应用页面注册快捷方式)。

通过本章的学习,我们应该能掌握以下3种开发技能:

  1. 理解活动的生命周期过程,并学会正确启动和结束活动。
  2. 理解意图的组成结构,并利用意图在活动之间传递消息。
  3. 理解元数据的概念,并通过元数据配置参数信息和注册快捷菜单。

.5 课后练习题

一、填空题
  1. 打开一个新页面,新页面的生命周期方法依次为onCreate→
  2. 关闭现有的页面,现有页面的生命周期方法依次为onPause→
  3. Intent意图对象的 _ **_**方法用于指定意图的动作行为。
  4. 上一个页面要在**_** _方法中处理下一个页面返回的数据。
  5. 在AndroidManifest.xml的activity节点添加 标签,表示给该活动设置元数据信息。
二、判断题(正确打√,错误打×)
  1. 活动页面处于就绪状态时,允许用户在界面上输入文字。( )
  2. 设置了启动标志Intent.FLAG_ACTIVITY_SINGLE_TOP之后,当栈顶为待跳转的活动实例之时,会重 用栈顶的实例。( )
  3. 隐式Intent直接指定来源活动与目标活动,它属于精确匹配。( )
  4. 调用startActivity方法也能获得下个页面返回的意图数据。( )
  5. 在桌面长按应用图标,会弹出该应用的快捷方式菜单(如果有配置的话)。( )
三、选择题
  1. 在当前页面调用( )方法会回到上一个页面。

A.finish B.goback C.return D.close

  1. 从A页面跳到B页面,再从B页面返回A页面,此时A页面会先执行( )方法。

A.onCreate B.onRestart C.onResume D.onStart

  1. 栈是一种( )的数据结构。
    1. 先进先出
    2. 先进后出C.后进先出D.后进后出
  2. Bundle内部用于存放消息的数据结构是( )。

A.Deque B.List C.Map D.Set

5.( )不是meta-data标签拥有的属性。

A.id B.name C.resource D.value

四、简答题

请简要描述意图Intent主要完成哪几项工作。

五、动手练习

请上机实验下列3项练习:

  1. 创建两个活动页面,分别模拟注册页面和完成页面,先从注册页面跳到完成页面,但是在完成页面按 返回键,不能回到注册页面(因为注册成功之后无需重新注册)。
  2. 创建两个活动页面,从A页面携带请求数据跳到B页面,B页面应当展示A页面传来的信息;然后B页面向A页面返回应答数据,A页面也要展示B页面返回的信息。
  3. 实现类似支付宝的快捷方式,也就是在手机桌面上长按App图标,会弹出快捷方式的菜单列表,点击 某项菜单便打开对应的活动页面。

第5章 中级控件

本章介绍App开发常见的几类中级控件的用法,主要包括:如何定制几种简单的图形、如何使用几种选 择按钮、如何高效地输入文本、如何利用对话框获取交互信息等,然后结合本章所学的知识,演示了一 个实战项目“找回密码”的设计与实现。

.1 图形定制

本节介绍Android图形的基本概念和几种常见图形的使用办法,包括:形状图形的组成结构及其具体用法、九宫格图片(点九图片)的制作过程及其适用场景、状态列表图形的产生背景及其具体用法。

图形Drawable

Android把所有能够显示的图形都抽象为Drawable类(可绘制的)。这里的图形不止是图片,还包括色块、画板、背景等。

包含图片在内的图形文件放在res目录的各个drawable目录下,其中drawable目录一般保存描述性的

XML文件,而图片文件一般放在具体分辨率的drawable目录下。例如:

drawable-ldpi里面存放低分辨率的图片(如240×320),现在基本没有这样的智能手机了。drawable-mdpi里面存放中等分辨率的图片(如320×480),这样的智能手机已经很少了。drawable-hdpi里面存放高分辨率的图片(如480×800),一般对应4英寸~4.5英寸的手机(但不 绝对,同尺寸的手机有可能分辨率不同,手机分辨率就高不就低,因为分辨率低了屏幕会有模糊的 感觉)。

drawable-xhdpi里面存放加高分辨率的图片(如720×1280),一般对应5英寸~5.5英寸的手机。drawable-xxhdpi里面存放超高分辨率的图片(如1080×1920),一般对应6英寸~6.5英寸的手 机。

drawable-xxxhdpi里面存放超超高分辨率的图片(如1440×2560),一般对应7英寸以上的平板电 脑。

基本上,分辨率每加大一级,宽度和高度就要增加二分之一或三分之一像素。如果各目录存在同名图

片,Android就会根据手机的分辨率分别适配对应文件夹里的图片。在开发App时,为了兼容不同的手机屏幕,在各目录存放不同分辨率的图片,才能达到最合适的显示效果。例如,在drawable-hdpi放了一 张背景图片bg.png(分辨率为480×800),其他目录没放,使用分辨率为480×800的手机查看该App界 面没有问题,但是使用分辨率为720×1280的手机查看该App会发现背景图片有点模糊,原因是Android 为了让bg.png适配高分辨率的屏幕,强行把bg.png拉伸到了720×1280,拉伸的后果是图片变模糊了。

在XML布局文件中引用图形文件可使用“@drawable/不含扩展名的文件名称”这种形式,如各视图的background属性、ImageView和ImageButton的src属性、TextView和Button四个方向的drawable*** 系列属性都可以引用图形文件。

形状图形

Shape图形又称形状图形,它用来描述常见的几何形状,包括矩形、圆角矩形、圆形、椭圆等。用好形状图形可以让App页面不再呆板,还可以节省美工不少工作量。

形状图形的定义文件放在drawable目录下,它是以shape标签为根节点的XML描述文件。根节点下定义 了6个节点,分别是:size(尺寸)、stroke(描边)、corners(圆角)、solid(填充)、padding

(间隔)、gradient(渐变),各节点的属性值主要是长宽、半径、角度以及颜色等。下面是形状图形各个节点及其属性的简要说明。

shape(形状)

shape是形状图形文件的根节点,它描述了当前是哪种几何图形。下面是shape节点的常用属性说明。shape:字符串类型,表示图形的形状。形状类型的取值说明见表5-1。

表5-1 形状类型的取值说明

形状类型说明
rectangle矩形。默认值
oval椭圆。此时corners节点会失效
line直线。此时必须设置stroke节点,不然会报错
ring圆环
size(尺寸)

size是shape的下级节点,它描述了形状图形的宽高尺寸。若无size节点,则表示宽高与宿主视图一样大小。下面是size节点的常用属性说明。

height:像素类型,图形高度。width:像素类型,图形宽度。

stroke(描边)

stroke是shape的下级节点,它描述了形状图形的描边规格。若无stroke节点,则表示不存在描边。下 面是stroke节点的常用属性说明。

color: 颜 色 类 型 , 描 边 的 颜 色 。 dashGap:像素类型,每段虚线之间的间隔。

dashWidth:像素类型,每段虚线的宽度。若dashGap和dashWidth有一个值为0,则描边为实 线。

width:像素类型,描边的厚度。

corners(圆角)

corners是shape的下级节点,它描述了形状图形的圆角大小。若无corners节点,则表示没有圆角。下 面是corners节点的常用属性说明。

bottomLeftRadius: 像 素 类 型 , 左 下 圆 角 的 半 径 。 bottomRightRadius: 像 素 类 型 , 右 下 圆 角 的 半 径 。 topLeftRadius: 像 素 类 型 , 左 上 圆 角 的 半 径 。 topRightRadius: 像 素 类 型 , 右 上 圆 角 的 半 径 。 radius:像素类型,4个圆角的半径(若有上面4个圆角半径的定义,则不需要radius定义)。

solid(填充)

solid是shape的下级节点,它描述了形状图形的填充色彩。若无solid节点,则表示无填充颜色。下面是

solid节点的常用属性说明。

color:颜色类型,内部填充的颜色。

padding(间隔)

padding是shape的下级节点,它描述了形状图形与周围边界的间隔。若无padding节点,则表示四周不 设间隔。下面是padding节点的常用属性说明。

top:像素类型,与上方的间隔。bottom:像素类型,与下方的间隔。left:像素类型,与左边的间隔。right:像素类型,与右边的间隔。

gradient(渐变)

gradient是shape的下级节点,它描述了形状图形的颜色渐变。若无gradient节点,则表示没有渐变效 果。下面是gradient节点的常用属性说明。

angle:整型,渐变的起始角度。为0时表示时钟的9点位置,值增大表示往逆时针方向旋转。例如,值为90表示6点位置,值为180表示3点位置,值为270表示0点/12点位置。

type:字符串类型,渐变类型。渐变类型的取值说明见表5-2。

表5-2 渐变类型的取值说明

渐变类型说明
linear线性渐变,默认值
radial放射渐变,起始颜色就是圆心颜色
sweep滚动渐变,即一个线段以某个端点为圆心做360度旋转

centerX:浮点型,圆心的X坐标。当android:type="linear"时不可用。centerY:浮点型,圆心的Y坐标。当android:type="linear"时不可用。

gradientRadius:整型,渐变的半径。当android:type="radial"时需要设置该属性。centerColor: 颜 色 类 型 , 渐 变 的 中 间 颜 色 。 startColor: 颜 色 类 型 , 渐 变 的 起 始 颜 色 。 endColor:颜色类型,渐变的终止颜色。

useLevel:布尔类型,设置为true为无渐变色、false为有渐变色。

在实际开发中,形状图形主要使用3个节点:stroke(描边)、corners(圆角)和solid(填充)。至于

shape根节点的属性一般不用设置(默认矩形即可)。

接下来演示一下形状图形的界面效果,首先右击drawable目录,并依次选择右键菜单的New→Drawable resource file,在弹窗中输入文件名称再单击OK按钮,即可自动生成一个XML描述文件。往该文件填入下面的圆角矩形内容定义:

(完整代码见chapter05\src\main\res\drawable\shape_rect_gold.xml)

接着创建一个测试页面,并在页面的XML文件中添加名为v_content的View标签,再给Java代码补充以下 的视图背景设置代码:

(完整代码见chapter05\src\main\java\com\example\chapter05\DrawableShapeActivity.java)

然后运行测试App,观察到对应的形状图形如图5-1所示。该形状为一个圆角矩形,内部填充色为土黄色,边缘线为灰色。

再来一个椭圆的XML描述文件示例如下:

(完整代码见chapter05\src\main\res\drawable\shape_oval_rose.xml)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oqTsZIBU-1676725568071)(media/16696a97a1c3a9557b03222139a58a69.jpeg)]把前述的视图对象v_content背景改为R.drawable.shape_oval_rose,运行App观察到对应的形状图形如图5-2所示。该形状为一个椭圆,内部填充色为玫红色,边缘线为灰色。

图5-1 圆角矩形效果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6HPOEnr5-1676725568071)(media/39724ab18d58044e9c40d8f5feef0ad8.jpeg)]

九宫格图片

图5-2 椭圆图形效果

将某张图片设置成视图背景时,如果图片尺寸太小,则系统会自动拉伸图片使之填满背景。可是一旦图 片拉得过大,其画面容易变得模糊,如图5-3所示,上面按钮的背景图片被拉得很宽,此时左右两边的边 缘线既变宽又变模糊了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kCIyjFOS-1676725568072)(media/35c97d9214e63d5981f68c82ff43d010.jpeg)]

图5-3 普通图片与九宫格图片的拉伸效果对比

为了解决这个问题,Android专门设计了点九图片。点九图片的扩展名是png,文件名后面常带有“.9”字样。因为该图片划分了3×3的九宫格区域,所以得名点九图片,也叫九宫格图片。如果背景是一个形状 图形,其stroke节点的width属性已经设置了固定数值(如1dp),那么无论该图形被拉到多大,描边宽度始终是1dp。点九图片的实现原理与之类似,即拉伸图形时,只拉伸内部区域,不拉伸边缘线条。

为了演示九宫格图片的展示效果,要利用Android Studio制作一张点九图片。首先在drawable目录下找到待加工的原始图片button_pressed_orig.png,右击它弹出右键菜单如图5-4所示。

选择右键菜单下面的“Create 9-Patch files…”,并在随后弹出的对话框中单击OK按钮。接着drawable目录自动生成一个名为“button_pressed_orig.9.png”的图片,双击该文件,主界面右侧弹出如图5-5所示的 点九图片的加工窗口。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QMM8jU8T-1676725568072)(media/79b0081437ec2caa5428088cc75feda0.jpeg)]

图5-4 点九图片的制作菜单路径

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XleuCAhN-1676725568073)(media/fe699e01199eb3543d5c849f1f70f0f5.jpeg)]

图5-5 点九图片的加工窗口界面

注意图5-5的左侧窗口是图片加工区域,右侧窗口是图片预览区域,从上到下依次是纵向拉伸预览、横向 拉伸预览、两方向同时拉伸预览。在左侧窗口图片四周的马赛克处单击会出现一个黑点,把黑点左右或 上下拖动会拖出一段黑线,不同方向上的黑线表示不同的效果。

如图5-6所示,界面上边的黑线指的是水平方向的拉伸区域。水平方向拉伸图片时,只有黑线区域内的图 像会拉伸,黑线以外的图像保持原状,从而保证左右两侧的边框厚度不变。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2OuQ2ftO-1676725568074)(media/55cc397ddf564c470a74d03aeb07f4ea.jpeg)]

图5-6 点九图片上边的边缘线

如图5-7所示,界面左边的黑线指的是垂直方向的拉伸区域。垂直方向拉伸图片时,只有黑线区域内的图 像会拉伸,黑线以外的图像保持原状,从而保证上下两侧的边框厚度不变。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KZGGXdPm-1676725568074)(media/e06e206534bf50327a4e9db5f9850d89.jpeg)]

图5-7 点九图片左边的边缘线

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rKny34oR-1676725568075)(media/855f443429108d2bd956c508d6ace452.jpeg)]如图5-8所示,界面下边的黑线指的是该图片作为控件背景时,控件内部的文字左右边界只能放在黑线区 域内。这里Horizontal Padding的效果就相当于android:paddingLeft与android:paddingRight。

图5-8 点九图片下边的边缘线

如图5-9所示,界面右边的黑线指的是该图片作为控件背景时,控件内部的文字上下边界只能放在黑线区 域内。这里Vertical Padding的效果就相当于android:paddingTop与android:paddingBottom。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l5rUJ8O4-1676725568075)(media/c5ccee0f9f61f788c09bc339173ee01a.jpeg)]

图5-9 点九图片右边的边缘线

尤其注意,如果点九图片被设置为视图背景,且该图片指定了Horizontal Padding和Vertical Padding, 那么视图内部将一直与视图边缘保持固定间距,无论怎么调整XML文件和Java代码都无法缩小间隔,缘由是点九图片早已在水平和垂直方向都设置了padding。

状态列表图形

常见的图形文件一般为静态图形,但有时会用到动态图形,比如按钮控件的背景在正常情况下是凸起 的,在按下时是凹陷的,从按下到弹起的过程,用户便晓得点击了该按钮。根据不同的触摸情况变更图 形状态,这种情况用到了Drawable的一个子类StateListDrawable(状态列表图形),它在XML文件中规定了不同状态时候所呈现的图形列表。

接下来演示一下状态列表图形的界面效果,右击drawable目录,并依次选择右键菜单的New→Drawable resource file,在弹窗中输入文件名称再单击OK按钮,即可自动生成一个XML描述文件。往该文件填入下面的状态列表图形定义:

(完整代码见chapter05\src\main\res\drawable\btn_nine_selector.xml)

上述XML文件的关键点是state_pressed属性,该属性表示按下状态,值为true表示按下时显示

button_pressed图像,其余情况显示button_normal图像。

为方便理解,接下来做个实验,首先将按钮控件的background属性设置为@drawable/btn_nine_selector,然后在屏幕上点击该按钮,观察发现按下时候的界面如图5-10所示, 而松开时候的界面如图5-11所示,可见按下与松开果然显示不同的图片。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8PR2uH9g-1676725568076)(media/1e138a27593a5da2393b79f383fbd305.jpeg)]

图5-10 按下按钮时的背景样式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1Vv886qC-1676725568076)(media/ebe8c725f0ff9422eae0afe1aedc284e.jpeg)]

图5-11 松开按钮时的背景样式

状态列表图形不仅用于按钮控件,还可用于其他拥有多种状态的控件,这取决于开发者在XML文件中指定了哪种状态类型。各种状态类型的取值说明详见表5-3。

表5-3 状态类型的取值说明

状态类型的属性名称说明适用的控件
state_pressed是否按下按钮Button
state_checked是否勾选复选框CheckBox、单选按钮RadioButton
state_focused是否获取焦点文本编辑框EditText
state_selected是否选中各控件通用

.2 选择按钮

本节介绍几个常用的特殊控制按钮,包括:如何使用复选框CheckBox及其勾选监听器、如何使用开关按钮Switch、如何借助状态列表图形实现仿iOS的开关按钮、如何使用单选按钮RadioButton和单选组

RadioGroup及其选中监听器。

复选框CheckBox

在学习复选框之前,先了解一下CompoundButton。在Android体系中,CompoundButton类是抽象的 复合按钮,因为是抽象类,所以它不能直接使用。实际开发中用的是CompoundButton的几个派生类, 主要有复选框CheckBox、单选按钮RadioButton以及开关按钮Switch,这些派生类均可使用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-j8LQ6Ewb-1676725568077)(media/847092e9f4e170914c44f14a38821c51.jpeg)]CompoundButton的属性和方法。加之CompoundButton本身继承了Button类,故以上几种按钮同时 具备Button的属性和方法,它们之间的继承关系如图5-12所示。

图5-12 复合按钮的继承关系

CompoundButton在XML文件中主要使用下面两个属性。

checked:指定按钮的勾选状态,true表示勾选,false则表示未勾选。默认为未勾选。button:指定左侧勾选图标的图形资源,如果不指定就使用系统的默认图标。

CompoundButton在Java代码中主要使用下列4种方法。

setChecked: 设 置 按 钮 的 勾 选 状 态 。 setButtonDrawable:设置左侧勾选图标的图形资源。setOnCheckedChangeListener:设置勾选状态变化的监听器。isChecked:判断按钮是否勾选。

复选框CheckBox是CompoundButton一个最简单的实现控件,点击复选框将它勾选,再次点击取消勾 选。复选框对象调用setOnCheckedChangeListener方法设置勾选监听器,这样在勾选和取消勾选时就 会触发监听器的勾选事件。

接下来演示复选框的操作过程,首先编写活动页面的XML文件如下所示:

(完整代码见chapter05\src\main\res\layout\activity_check_box.xml)

接着编写对应的Java代码,主要是如何处理勾选监听器,具体代码如下所示:

(完整代码见chapter05\src\main\java\com\example\chapter05\CheckBoxActivity.java)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fsxmhFCH-1676725568077)(media/6cb1abbbeff86b774d93eda9c9f039d0.jpeg)]然后运行测试App,一开始的演示界面如图5-13所示,此时复选框默认未勾选。首次点击复选框,此时复选框的图标及文字均发生变化,如图5-14所示;再次点击复选框,此时复选框的图标及文字又发生变 化,如图5-15所示;可见先后触发了勾选与取消勾选事件。

图5-13 初始的复选框界面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h2DbYhpw-1676725568078)(media/890f78973f4a10655be635bcbaaf18d6.jpeg)]

图5-14 首次点击后的复选框

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xzIhoPnI-1676725568079)(media/f08536b315b1fc85dfb584c2273ea89b.jpeg)]

开关按钮Switch

图5-15 再次点击后的复选框

Switch是开关按钮,它像一个高级版本的CheckBox,在选中与取消选中时可展现的界面元素比复选框丰 富。Switch控件新添加的XML属性说明如下:

textOn:设置右侧开启时的文本。textOff:设置左侧关闭时的文本。track:设置开关轨道的背景。thumb:设置开关标识的图标。

虽然开关按钮是升级版的复选框,但它在实际开发中用得不多。原因之一是大家觉得Switch的默认界面不够大气,如图5-16和图5-17所示,小巧的开关图标显得有些拘谨;原因之二是大家觉得iPhone的界面很漂亮,无论用户还是客户,都希望App实现iOS那样的控件风格,于是iOS的开关按钮UISwitch就成了 安卓开发者仿照的对象。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9uaUrdJ5-1676725568080)(media/bcc56d7322752dc4a105dcedaa7a772d.jpeg)]

图5-16 Switch控件的“关”状态

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W0X5ENjO-1676725568081)(media/7a3d308e20d4b4b45a266704c16a285b.jpeg)]

图5-17 Switch控件的“开”状态

现在要让Android实现类似iOS的开关按钮,主要思路是借助状态列表图形,首先创建一个图形专用的

XML文件,给状态列表指定选中与未选中时候的开关图标,如下所示:

(完整代码见chapter05\src\main\res\drawable\switch_selector.xml)

然后把CheckBox标签的background属性设置为@drawable/switch_selector,同时将button属性设置 为@null。完整的CheckBox标签内容示例如下:

(完整代码见chapter05\src\main\res\layout\activity_switch_ios.xml)

为什么这里修改background属性,而不直接修改button属性呢?因为button属性有局限,无论多大的图片,都只显示一个小小的图标,可是小小的图标一点都不大气,所以这里必须使用background属性, 要它有多大就能有多大,这才够炫够酷。

最后看看这个仿iOS开关按钮的效果,分别如图5-18和图5-19所示。这下开关按钮脱胎换骨,又圆又鲜艳,比原来的Switch好看了很多。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lgTNBaGX-1676725568082)(media/650855b67bff3321f6095f2e3650b8fd.jpeg)]

图5-18 仿iOS按钮的“关”状态

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5in2Ra2H-1676725568082)(media/c0dc49da518836afd8ae05634d514e9a.jpeg)]

图5-19 仿iOS按钮的“开”状态

单选按钮RadioButton

所谓单选按钮,指的是在一组按钮中选择其中一项,并且不能多选,这要求有个容器确定这组按钮的范 围,这个容器便是单选组RadioGroup。单选组实质上是个布局,同一组RadioButton都要放在同一个

RadioGroup节点下。RadioGroup提供了orientation属性指定下级控件的排列方向,该属性为

horizontal时,单选按钮在水平方向排列;该属性为vertical时,单选按钮在垂直方向排列。

RadioGroup下面除了RadioButton,还可以挂载其他子控件(如TextView、ImageView等)。如此看 来,单选组相当于特殊的线性布局,它们主要有以下两个区别:

  1. 单选组多了管理单选按钮的功能,而线性布局不具备该功能。

  2. 如果不指定orientation属性,那么单选组默认垂直排列,而线性布局默认水平排列。下面是RadioGroup在Java代码中的3个常用方法。

    check:选中指定资源编号的单选按钮。getCheckedRadioButtonId:获取已选中单选按钮的资源编号。setOnCheckedChangeListener:设置单选按钮勾选变化的监听器。

与CheckBox不同的是,RadioButton默认未选中,点击后显示选中,但是再次点击不会取消选中。只有 点击同组的其他单选按钮时,原来选中的单选按钮才会取消选中。另需注意,单选按钮的选中事件不是 由RadioButton处理,而是由RadioGroup处理。

接下来演示单选按钮的操作过程,首先编写活动页面的XML文件如下所示:

(完整代码见chapter05\src\main\res\layout\ activity_radio_horizontal.xml)

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width=“match_parent” android:layout_height=“match_parent”

android:orientation=“vertical” android:padding=“5dp” >

<TextView

android:layout_width=“match_parent” android:layout_height=“wrap_content” android:text=" 请 选 择 您 的 性 别 " android:textColor=“@color/black” android:textSize=“17sp” />

<RadioGroup

android:id=“@+id/rg_sex” android:layout_width=“match_parent” android:layout_height=“wrap_content” android:orientation=“horizontal” >

<RadioButton

android:id=“@+id/rb_male” android:layout_width=“0dp” android:layout_height=“wrap_content” android:layout_weight=“1” android:checked=“false”

android:text=“男”

android:textColor=“@color/black” android:textSize=“17sp” />

<RadioButton

android:id=“@+id/rb_female” android:layout_width=“0dp” android:layout_height=“wrap_content” android:layout_weight=“1” android:checked=“false”

android:text=“女”

android:textColor=“@color/black” android:textSize=“17sp” />

</RadioGroup>

<TextView

android:id=“@+id/tv_sex” android:layout_width=“match_parent” android:layout_height=“wrap_content” android:textColor=“@color/black” android:textSize=“17sp” />

</LinearLayout>

接着编写对应的Java代码,主要是如何处理选中监听器,具体代码如下所示:

// 该页面实现了接口OnCheckedChangeListener,意味着要重写选中监听器的onCheckedChanged方法

public class RadioHorizontalActivity extends AppCompatActivity implements RadioGroup.OnCheckedChangeListener {

private TextView tv_sex; // 声明一个文本视图对象

@Override

protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_radio_horizontal);

// 从布局文件中获取名叫tv_sex的文本视图

tv_sex = findViewById(R.id.tv_sex);

// 从布局文件中获取名叫rg_sex的单选组

RadioGroup rg_sex = findViewById(R.id.rg_sex);

// 设置单选监听器,一旦点击组内的单选按钮,就触发监听器的onCheckedChanged方法

rg_sex.setOnCheckedChangeListener(this);

}

// 在用户点击组内的单选按钮时触发

@Override

public void onCheckedChanged(RadioGroup group, int checkedId) { if (checkedId == R.id.rb_male) {

tv_sex.setText(“哇哦,你是个帅气的男孩”);

} else if (checkedId == R.id.rb_female) {

tv_sex.setText(“哇哦,你是个漂亮的女孩”);

}

}

}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7vVJWYXI-1676725568083)(media/93285bfa97f2d97bef9c540f3bceaa41.jpeg)]然后运行测试App,一开始的演示界面如图5-20所示,此时两个单选按钮均未选中。先点击左边的单选按钮,此时左边按钮显示选中状态,如图5-21所示;再点击右边的单选按钮,此时右边按钮显示选中状 态,同时左边按钮取消选中,如图5-22所示;可见果然实现了组内只能选中唯一按钮的单选功能。

图5-20 初始的单选按钮界面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PebtiWx4-1676725568083)(media/313a0dfecd8ef784bbfc1fbd74b92a17.jpeg)]

图5-21 选中左边按钮的单选界面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KZ236ssq-1676725568083)(media/73095366650e7783118f1b32c601288b.jpeg)]

图5-22 选中右边按钮的单选界面

.3 文本输入

本节介绍如何在编辑框EditText上高效地输入文本,包括:如何改变编辑框的控件外观,如何利用焦点变更监听器提前校验输入位数,如何利用文本变化监听器自动关闭软键盘。

编辑框EditText

编辑框EditText用于接收软键盘输入的文字,例如用户名、密码、评价内容等,它由文本视图派生而来,除了TextView已有的各种属性和方法,EditText还支持下列XML属性。

inputType:指定输入的文本类型。输入类型的取值说明见表5-4,若同时使用多种文本类型,则可使用竖线“|”把多种文本类型拼接起来。

maxLength:指定文本允许输入的最大长度。hint: 指 定 提 示 文 本 的 内 容 。 textColorHint:指定提示文本的颜色。

输入类型说明
text文本
textPassword文本密码。显示时用圆点“·”代替
number整型数
numberSigned带符号的数字。允许在开头带负号“-”
numberDecimal带小数点的数字
numberPassword数字密码。显示时用圆点“·”代替
datetime时间日期格式。除了数字外,还允许输入横线、斜杆、空格、冒号
date日期格式。除了数字外,还允许输入横线“-”和斜杆“/”
time时间格式。除了数字外,还允许输入冒号“:”

接下来通过XML布局观看编辑框界面效果,演示用的XML文件内容如下:

(完整代码见chapter05\src\main\res\layout\activity_edit_simple.xml)

运行测试App,进入初始的编辑框页面如图5-23所示。然后往用户名编辑框输入文字,输满10个字后发现不能再输入,于是切换到密码框继续输,直到输满8位密码,此时编辑框页面如图5-24所示。

根据以上图示可知编辑框的各属性正常工作,不过编辑框有根下划线,未输入时显示灰色,正在输入时 显示红色,这种效果是怎么实现的呢?其实下划线没用到新属性,而用了已有的背景属性background; 至于未输入与正在输入两种情况的颜色差异,乃是因为使用了状态列表图形,编辑框获得焦点时(正在 输入)显示红色的下划线,其余时候显示灰色下划线。当然EditText默认的下划线背景不甚好看,下面 将利用状态列表图形将编辑框背景改为更加美观的圆角矩形。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6JQ1nDso-1676725568084)(media/1503ef70a9dc840b6e28189a32d2dc34.jpeg)]

图5-23 初始的编辑框样式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BhWukrwR-1676725568085)(media/1f8aaad256c81df904f5f5b21dd78cf3.jpeg)]

图5-24 输入文字的编辑框样式

首先编写圆角矩形的形状图形文件,它的XML定义文件示例如下:

(完整代码见chapter05\src\main\res\drawable\shape_edit_normal.xml)

上述的shape_edit_normal.xml定义了一个灰色的圆角矩形,可在未输入时展示该形状。正在输入时候 的形状要改为蓝色的圆角矩形,其中轮廓线条的色值从aaaaaa(灰色)改成0000ff(蓝色),具体定义放在shape_edit_focus.xml。

接着编写编辑框背景的状态列表图形文件,主要在selector节点下添加两个item,一个item设置了获得焦点时刻(android:state_focused=“true”)的图形为@drawable/shape_edit_focus;另一个item设置 了图形@drawable/shape_edit_normal但未指定任何状态,表示其他情况都展示该图形。完整的状态列 表图形定义示例如下:

(完整代码见chapter05\src\main\res\drawable\editext_selector.xml)

然后编写测试页面的XML布局文件,一共添加3个EditText标签,第一个EditText采用默认的编辑框背 景;第二个EditText将background属性值设为@null,此时编辑框不显示任何背景;第三个EditText将

background属性值设为@drawable/editext_selector,其背景由editext_selector.xml所定义的状态列 表图形决定。详细的XML文件内容如下所示:

(完整代码见chapter05\src\main\res\layout\activity_edit_border.xml)

最后运行测试App,更换背景之后的编辑框界面如图5-25所示,可见第三个编辑框的背景成功变为了圆角矩形边框。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6hmO5Hs9-1676725568085)(media/5df9cb803c22bd8f163ecd746e13acc8.jpeg)]

图5-25 更换背景后的编辑框样式

焦点变更监听器

虽然编辑框EditText提供了maxLength属性,用来设置可输入文本的最大长度,但是它没提供对应的

minLength属性,也就无法设置可输入文本的最小长度。譬如手机号码为固定的11位数字,用户必须输满11位才是合法的,然而编辑框不会自动检查手机号码是否达到11位,即使用户少输一位只输入十位数字,编辑框依然认为这是合法的手机号。比如图5-26所示的登录页面,有手机号码编辑框,有密码编辑 框,还有登录按钮。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RaJ10hm0-1676725568086)(media/bbefaa7df5e1858a31dd368977799b75.jpeg)]

图5-26 简单的登录界面

既然编辑框不会自动校验手机号是否达到11位,势必要求代码另行检查。一种想法是在用户点击登录按 钮时再判断,不过通常此时已经输完手机号与密码,为啥不能在输入密码之前就判断手机号码的位数 呢?早点检查可以帮助用户早点发现错误,特别是表单元素较多的时候,更能改善用户的使用体验。就 上面的登录例子而言,手机号编辑框下方为密码框,那么能否给密码框注册点击事件,以便在用户准备 输入密码时就校验手机号的位数呢?

然而实际运行App却发现,先输入手机号码再输入密码,一开始并不会触发密码框的点击事件,再次点 击密码框才会触发点击事件。缘由是编辑框比较特殊,要点击两次后才会触发点击事件,因为第一次点 击只触发焦点变更事件,第二次点击才触发点击事件。编辑框的所谓焦点,直观上就看那个闪动的光 标,哪个编辑框有光标,焦点就落在哪里。光标在编辑框之间切换,便产生了焦点变更事件,所以对于 编辑框来说,应当注册焦点变更监听器,而非注册点击监听器。

焦点变更监听器来自于接口View.OnFocusChangeListener,若想注册该监听器,就要调用编辑框对象 的setOnFocusChangeListener方法,即可在光标切换之时(获得光标和失去光标)触发焦点变更事 件。下面是给密码框注册焦点变更监听器的代码例子:

(完整代码见chapter05\src\main\java\com\example\chapter05\EditFocusActivity.java)

以上代码把焦点变更监听器设置到当前页面,则需让活动页面实现接口

View.OnFocusChangeListener,并重写该接口定义的onFocusChange方法,判断如果是密码框获得焦 点,就检查输入的手机号码是否达到11位。具体的焦点变更处理方法如下所示:

改好代码重新运行App,当手机号不足11位时点击密码框,界面底部果然弹出了相应的提示文字,如图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-skDtBpXz-1676725568086)(media/666eb61afd3eb8f5a4c85c0d2db809ee.jpeg)]5-27所示,并且光标仍然留在手机号码编辑框,说明首次点击密码框的确触发了焦点变更事件。

图5-27 编辑框触发了焦点变更监听器

文本变化监听器

输入法的软键盘往往会遮住页面下半部分,使得“登录”“确认”“下一步”等按钮看不到了,用户若想点击这些按钮还得再点一次返回键才能关闭软键盘。为了方便用户操作,最好在满足特定条件时自动关闭软键 盘,比如手机号码输入满11位后自动关闭软键盘,又如密码输入满6位后自动关闭软键盘,等等。达到 指定位数便自动关闭键盘的功能,可以再分解为两个独立的功能点,一个是如何关闭软键盘,另一个是 如何判断已输入的文字达到指定位数,分别说明如下。

如何关闭软键盘

诚然按下返回键就会关闭软键盘,但这是系统自己关闭的,而非开发者在代码中关闭。因为输入法软键 盘由系统服务INPUT_METHOD_SERVICE管理,所以关闭软键盘也要由该服务处理,下面是使用系统服 务关闭软键盘的代码例子:

(完整代码见chapter05\src\main\java\com\example\chapter05\util\ViewUtil.java)

注意上述代码里面的视图对象v,虽然控件类型为View,但它必须是EditText类型才能正常关闭软键盘。

如何判断已输入的文字达到指定位数

该功能点要求实时监控当前已输入的文本长度,这个监控操作用到文本监听器接口TextWatcher,该接口提供了3个监控方法,具体说明如下:

beforeTextChanged:在文本改变之前触发。onTextChanged:在文本改变过程中触发。afterTextChanged:在文本改变之后触发。

具体到编码实现,需要自己写个监听器实现TextWatcher接口,再调用编辑框对象的addTextChangedListener方法注册文本监听器。监听操作建议在afterTextChanged方法中完成,如果同时监听11位的手机号码和6位的密码,一旦输入文字达到指定长度就关闭键盘,则详细的监听器代码 如下所示:

(完整代码见chapter05\src\main\java\com\example\chapter05\EditHideActivity.java)

写好文本监听器代码,还要给手机号码编辑框和密码编辑框分别注册监听器,注册代码示例如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w8Zo5fb2-1676725568086)(media/7e01c5daa865e47ccacfb204a3eee840.jpeg)]然后运行测试App,先输入手机号码的前10位,因为还没达到11位,所以软键盘依然展示,如图5-28所示。接着输入最后一位手机号,总长度达到11位,于是软键盘自动关闭,如图5-29所示。

图5-28 输入10位手机号码

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pN1yfHoB-1676725568087)(media/300bf7b920b5d2e671c8bb96f2331165.jpeg)]

图5-29 输入11位手机号码

.4 对话框

本节介绍几种常用的对话框控件,包括:如何使用提醒对话框处理不同的选项,如何使用日期对话框获 取用户选择的日期,如何使用时间对话框获取用户选择的时间。

提醒对话框AlertDialog

AlertDialog名为提醒对话框,它是Android中最常用的对话框,可以完成常见的交互操作,例如提示、 确认、选择等功能。由于AlertDialog没有公开的构造方法,因此必须借助建造器AlertDialog.Builder才 能完成参数设置,AlertDialog.Builder的常用方法说明如下。

setIcon:设置对话框的标题图标。setTitle:设置对话框的标题文本。setMessage:设置对话框的内容文本。

setPositiveButton:设置肯定按钮的信息,包括按钮文本和点击监听器。setNegativeButton:设置否定按钮的信息,包括按钮文本和点击监听器。setNeutralButton:设置中性按钮的信息,包括按钮文本和点击监听器,该方法比较少用。

通过AlertDialog.Builder设置完对话框参数,还需调用建造器的create方法才能生成对话框实例。最后 调用对话框实例的show方法,在页面上弹出提醒对话框。

下面是构建并显示提醒对话框的Java代码例子:

(完整代码见chapter05\src\main\java\com\example\chapter05\AlertDialogActivity.java)

提醒对话框的弹窗效果如图5-30所示,可见该对话框有标题和内容,还有两个按钮。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UReYhRcP-1676725568087)(media/f03f5646cd9cc1b6302dfce6b76b430f.jpeg)]

图5-30 提醒对话框的效果图

点击不同的对话框按钮会触发不同的处理逻辑。例如,图5-31为点击“我再想想”按钮后的页面,图5-32 为点击“残忍卸载”按钮后的页面。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-22pG0gBi-1676725568088)(media/12c108b02a4021405df43e3fdabc44b2.jpeg)]

图5-31 点击“我再想想”的截图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yotmHsbT-1676725568088)(media/393323869238f9d1db27cb815eaefebb.jpeg)]

图5-32 点击“残忍卸载”的截图

日期对话框DatePickerDialog

虽然EditText提供了inputType="date"的日期输入,但是很少有人会手工输入完整日期,况且EditText 还不支持“ 年 ** 月 **日”这样的中文日期,所以系统提供了专门的日期选择器DatePicker,供用户选择具体的年月日。不过,DatePicker并非弹窗模式,而是在当前页面占据一块区域,并且不会自动关闭。按习惯来说,日期控件应该弹出对话框,选择完日期就要自动关闭对话框。因此,很少直接在界面上显 示DatePicker,而是利用已经封装好的日期选择对话框DatePickerDialog。

DatePickerDialog相当于在AlertDialog上装载了DatePicker,编码时只需调用构造方法设置当前的年、月、日,然后调用show方法即可弹出日期对话框。日期选择事件则由监听器OnDateSetListener负责响 应,在该监听器的onDateSet方法中,开发者获取用户选择的具体日期,再做后续处理。特别注意

onDateSet的月份参数,它的起始值不是1而是0。也就是说,一月份对应的参数值为0,十二月份对应的参数值为11,中间月份的数值以此类推。

在界面上内嵌显示DatePicker的效果如图5-33所示,其中,年、月、日通过上下滑动选择。单独弹出日 期对话框的效果如图5-34所示,其中年、月、日按照日历风格展示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pXmHfOso-1676725568090)(media/f2c64986120430185e75204dab71bd0b.jpeg)]

图5-33 日期选择器的截图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zEScIdvB-1676725568092)(media/8e5e3e095420e0631c7f5b4141eba6e4.jpeg)]

图5-34 日期对话框的截图

下面是使用日期对话框的Java代码例子,包括弹出日期对话框和处理日期监听事件:

(完整代码见chapter05\src\main\java\com\example\chapter05\DatePickerActivity.java)

@Override

public void onClick(View v) {

if (v.getId() == R.id.btn_date) {

// 获取日历的一个实例,里面包含了当前的年月日

Calendar calendar = Calendar.getInstance();

// 构建一个日期对话框,该对话框已经集成了日期选择器。

// DatePickerDialog的第二个构造参数指定了日期监听器DatePickerDialog dialog = new DatePickerDialog(this, this,

calendar.get(Calendar.YEAR), // 年份calendar.get(Calendar.MONTH), // 月份calendar.get(Calendar.DAY_OF_MONTH)); // 日子

dialog.show(); // 显示日期对话框

}

}

// 一旦点击日期对话框上的确定按钮,就会触发监听器的onDateSet方法

@Override

public void onDateSet(DatePicker view, int year, int monthOfYear, int dayOfMonth) {

// 获取日期对话框设定的年月份

String desc = String.format(“您选择的日期是%d年%d月%d日”, year, monthOfYear + 1, dayOfMonth);

tv_date.setText(desc);

}

}

时间对话框TimePickerDialog

既然有了日期选择器,还得有对应的时间选择器。同样,实际开发中也很少直接用TimePicker,而是用封装好的时间选择对话框TimePickerDialog。该对话框的用法类似DatePickerDialog,不同之处主要有两个:

  1. 构造方法传的是当前的小时与分钟,最后一个参数表示是否采取24小时制,一般为true表示小时的数值范围为0~23;若为false则表示采取12小时制。
  2. 时间选择监听器为OnTimeSetListener,对应需要实现onTimeSet方法,在该方法中可获得用户选择的小时和分钟。

在界面上内嵌显示TimePicker的效果如图5-35所示,其中,小时与分钟可通过上下滑动选择。单独弹出 时间对话框的效果如图5-36所示,其中小时与分钟按照钟表风格展示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G9c70CoK-1676725568092)(media/f614e68f5661d273a5bcc4559dc97b28.jpeg)]

图5-35 时间选择器的截图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZEGlk3mO-1676725568093)(media/e06350833aea437c7b4c5b94e1f5c76f.jpeg)]

图5-36 时间对话框的截图

下面是使用时间对话框的Java代码例子,包括弹出时间对话框和处理时间监听事件:

(完整代码见chapter05\src\main\java\com\example\chapter05\TimePickerActivity.java)

// 该页面类实现了接口OnTimeSetListener,意味着要重写时间监听器的onTimeSet方法

public class TimePickerActivity extends AppCompatActivity implements View.OnClickListener, TimePickerDialog.OnTimeSetListener {

private TextView tv_time; // 声明一个文本视图对象

private TimePicker tp_time; // 声明一个时间选择器对象

@Override

protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_time_picker); tv_time = findViewById(R.id.tv_time);

// 从布局文件中获取名叫tp_time的时间选择器

tp_time = findViewById(R.id.tp_time); findViewById(R.id.btn_time).setOnClickListener(this);

}

@Override

public void onClick(View v) {

if (v.getId() == R.id.btn_time) {

// 获取日历的一个实例,里面包含了当前的时分秒

Calendar calendar = Calendar.getInstance();

// 构建一个时间对话框,该对话框已经集成了时间选择器。

// TimePickerDialog的第二个构造参数指定了时间监听器TimePickerDialog dialog = new TimePickerDialog(this, this,

calendar.get(Calendar.HOUR_OF_DAY), // 小时calendar.get(Calendar.MINUTE), // 分钟true); // true表示24小时制,false表示12小时制

dialog.show(); // 显示时间对话框

}

}

// 一旦点击时间对话框上的确定按钮,就会触发监听器的onTimeSet方法

@Override

public void onTimeSet(TimePicker view, int hourOfDay, int minute) {

// 获取时间对话框设定的小时和分钟

String desc = String.format(“您选择的时间是%d时%d分”, hourOfDay, minute); tv_time.setText(desc);

}

}

.5 实战项目:找回密码

在移动互联网时代,用户是每家IT企业最宝贵的资源,对于App而言,吸引用户注册并登录是万分紧要 之事,因为用户登录之后才有机会产生商品交易。登录校验通常是用户名+密码组合,可是每天总有部分 用户忘记密码,为此要求App提供找回密码的功能,如何简化密码找回步骤,同时兼顾安全性,就是一 个值得认真思考的问题。

需求描述

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0B0eRdb4-1676725568094)(media/7bd967e4d1bc5cd4dcd171eb58113ad0.jpeg)]各家电商App的登录页面大同小异,要么是用户名与密码组合登录,要么是手机号码与验证码组合登 录,若是做好一点的,则会提供找回密码与记住密码等功能。先来看一下登录页面是什么样,因为有两 种组合登录方式,所以登录页面也分成两个效果图。如图5-37所示,这是选中密码登录时的界面;如图5-38所示,这是选中验证码登录时的界面。

图5-37 选中密码登录方式时的界面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hTkg3BLT-1676725568094)(media/636b3888f35ffce2b1503bf2cd1c8a5b.jpeg)]

图5-38 选中验证码登录时的界面

从以上两个登录效果图可以看到,密码登录与验证码登录的界面主要存在以下几点区别:

  1. 密码输入框和验证码输入框的左侧标题以及输入框内部的提示语各不相同。
  2. 如果是密码登录,则需要支持找回密码;如果是验证码登录,则需要支持向用户手机发送验证码。
  3. 密码登录可以提供记住密码功能,而验证码的数值每次都不一样,无须也没法记住验证码。

对于找回密码功能,一般直接跳到找回密码页面,在该页面输入和确认新密码,并校验找回密码的合法 性(通过短信验证码检查),据此勾勒出密码找回页面的轮廓概貌,如图5-39所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dVGCzr3H-1676725568095)(media/25b08fd108cdd41e3ce8cda19ab40b82.jpeg)]

图5-39 找回密码的界面效果

在找回密码的操作过程当中,为了更好地增强用户体验,有必要在几个关键节点处提醒用户。比如成功 发送验证码之后,要及时提示用户注意查收短信,这里暂且做成提醒对话框的形式,如图5-40所示。又 比如密码登录成功之后,也要告知用户已经修改成功登录,注意继续后面的操作,登录成功的提示弹窗 如图5-41所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MHZei4f3-1676725568097)(media/b60d31ac5069be7078afd9dface141f2.jpeg)]

图5-40 发送验证码的提醒对话框

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OHFwzYaS-1676725568097)(media/1f4041cbf368a40b168b966a5c546db0.jpeg)]

图5-41 登录成功后的提醒对话框

真是想不到,原来简简单单的一个登录功能,就得考虑这么多的需求场景。可是仔细想想,这些需求场 景都是必要的,其目的是为了让用户能够更加便捷地顺利登录。正所谓“台上十分钟,台下十年功”,每 个好用的App背后,都离不开开发者十年如一日的辛勤工作。

界面设计

用户登录与找回密码界面看似简单,用到的控件却不少。按照之前的界面效果图,大致从上到下、从左 到右分布着下列Android控件:

单选按钮RadioButton:用来区分是密码登录还是验证码登录。

文本视图TextView:输入框左侧要显示此处应该输入什么信息。 编辑框EditText:用来输入手机号码、密码和验证码。

复选框CheckBox:用于判断是否记住密码。

按钮Button:除了“登录”按钮,还有“忘记密码”和“获取验证码”两个按钮。

线性布局LinearLayout:整体界面从上往下排列,用到了垂直方向的线性布局。

相对布局RelativeLayout:忘记密码的按钮与密码输入框是叠加的,且“忘记密码”与上级视图右对 齐。

单选组RadioGroup:密码登录和验证码登录这两个单选按钮,需要放在单选组之中。

提醒对话框AlertDialog:为了演示方便,获取验证码与登录成功都通过提醒对话框向用户反馈结果。

另外,由于整个登录模块由登录页面和找回密码页面组成,因此这两个页面之间需要进行数据交互,也 就是在页面跳转之时传递参数。譬如,从登录页面跳到找回密码页面,要携带唯一标识的手机号码作为 请求参数,不然密码找回页面不知道要给哪个手机号码修改密码。同时,从找回密码页面回到登录页 面,也要将修改之后的新密码作为应答参数传回去,否则登录页面不知道密码被改成什么了。

关键代码

为了方便读者更好更快地完成登录页面与找回密码页面,下面列举几个重要功能的代码片段:

关于自动清空错误的密码

这里有个细微的用户体验问题:用户会去找回密码,肯定是发现输入的密码不对;那么修改密码后回到 登录页面,如果密码框里还是刚才的错误密码,用户只能先清空错误密码,然后才能输入新密码。一个App要想让用户觉得好用,就得急用户之所急,想用户之所想,像刚才那个错误密码的情况,应当由

App在返回登录页面时自动清空原来的错误密码。

自动清空密码框的操作,放在onActivityResult方法中处理是个办法,但这样有个问题,如果用户直接按 返回键回到登录页面,那么onActivityResult方法发现数据为空便不做处理。因此应该这么处理:判断当 前是否为返回页面动作,只要是从找回密码页面返回到当前页面,则不管是否携带应答参数,都要自动 清空密码输入框。对应的Java代码则为重写登录页面的onRestart方法,在该方法中强制清空密码。这样 一来,不管用户是修改密码完成回到登录页,还是点击返回键回到登录页,App都会自动清空密码框

了。

下面是重写onRestart方法之后的代码例子:

(完整代码见chapter05\src\main\java\com\example\chapter05\LoginMainActivity.java)

关于自动隐藏输入法面板

在输入手机号码或者密码的时候,屏幕下方都会弹出输入法面板,供用户按键输入数字和字母。但是输 入法面板往往占据屏幕下方大块空间,很是碍手碍脚,用户输入完11位的手机号码时,还得再按一下返 回键来关闭输入法面板,接着才能继续输入密码。理想的做法是:一旦用户输完11位手机号码,App就要自动隐藏输入法。同理,一旦用户输完6位密码或者6位验证码,App也要自动隐藏输入法。要想让

App具备这种智能的判断功能,就得给文本编辑框添加监听器,只要当前编辑框输入文本长度达到11位或者和6位,App就自动隐藏输入法面板。

下面是实现自动隐藏软键盘的监听器代码例子:

(完整代码见chapter05\src\main\java\com\example\chapter05\LoginMainActivity.java)

关于密码修改的校验操作

由于密码对于用户来说是很重要的信息,因此必须认真校验新密码的合法性,务必做到万无一失才行。 具体的密码修改校验可分作下列4个步骤:

  1. 新密码和确认输入的新密码都要是6位数字。
    1. 新密码和确认输入的新密码必须保持一致。

    2. 用户输入的验证码必须和系统下发的验证码一致。

    3. 密码修改成功,携带修改后的新密码返回登录页面。

      根据以上的校验步骤,对应的代码逻辑示例如下:

      (完整代码见chapter05\src\main\java\com\example\chapter05\LoginForgetActivity.java)

.6 小结

本章主要介绍了App开发的中级控件的相关知识,包括:定制简单的图形(图形的基本概念、形状图

形、九宫格图片、状态列表图形)、操纵几种选择按钮(复选框CheckBox、开关按钮Switch、单选按钮RadioButton)、高效地输入文本(编辑框EditText、焦点变更监听器、文本变化监听器)、获取对话框 的选择结果(提醒对话框AlertDialog、日期对话框DatePickerDialog、时间对话框

TimePickerDialog)。最后设计了一个实战项目“找回密码”,在该项目的App编码中用到了前面介绍的 大部分控件,从而加深了对所学知识的理解。

通过本章的学习,我们应该能掌握以下4种开发技能:

  1. 学会定制几种简单的图形。
  2. 学会操纵常见的选择按钮。
  3. 学会高效且合法地输入文本。
  4. 学会通过对话框获取用户选项。

.7 课后练习题

一、填空题
  1. 图形描述文件的扩展名是 。

  2. 形状图形shape的下级节点 描述了形状图形的宽高尺寸。3.由复合按钮CompoundButton派生而来的控件包括 和Switch。4.EditText的属性 可指定文本允许输入的最大长度。

    5.输入法软键盘由系统服务 管理。

二、判断题(正确打√,错误打×)
  1. 形状图形可以描述圆角矩形的定义。( )
  2. 单选组RadioGroup默认内部控件在水平方向排列。( )
  3. 首次点击编辑框,就会触发它的点击事件。( )
  4. 提醒对话框AlertDialog支持同时设置3个按钮。( )
  5. 时间对话框会显示当前的时、分、秒。( )
三、选择题
  1. 状态列表图形的( )属性用于描述是否按下的图形列表。

A.state_pressed B.state_checked C.state_focused D.state_selected

  1. 在一组按钮中只选择其中一个按钮,应当选用( )控件。

A.Button B.CheckBox C.RadioButton D.Switch

  1. 若想让编辑框EditText输入数字密码,则要将inputType属性设置为( )。

A.text B.textPassword C.number D.numberPassword

  1. 若想在编辑框的文本改变之后补充处理,应当在( )方法中增加代码。

A.beforeTextChanged B.onTextChanged C.afterTextChanged D.构造

  1. 日期选择对话框上能够看到哪些时间单位( )。

A.年份B.月份C.日期D.星期

四、简答题

请简要描述九宫格图片的作用。

五、动手练习

请上机实验本章的找回密码项目,其中登录操作支持“用户名+密码”和“手机号+验证码”两种方式,同时支持通过验证码重置密码。

第6章 数据存储

本章介绍Android 4种存储方式的用法,包括共享参数SharedPreferences、数据库SQLite、存储卡文件、App的全局内存,另外介绍Android重要组件—应用Application的基本概念与常见用法。最后,结 合本章所学的知识演示实战项目“购物车”的设计与实现。

.1 共享参数SharedPreferences

本节介绍Android的键值对存储方式——共享参数SharedPreferences的使用方法,包括:如何将数据保 存到共享参数,如何从共享参数读取数据,如何使用共享参数实现登录页面的记住密码功能,如何利用 设备浏览器找到共享参数文件。

共享参数的用法

SharedPreferences是Android的一个轻量级存储工具,它采用的存储结构是Key-Value的键值对方式, 类似于Java的Properties,二者都是把Key-Value的键值对保存在配置文件中。不同的是,Properties的文件内容形如Key=Value,而SharedPreferences的存储介质是XML文件,且以XML标记保存键值对。保存共享参数键值对信息的文件路径为:/data/data/应用包名/shared_prefs/文件名.xml。下面是一个 共享参数的XML文件例子:

基于XML格式的特点,共享参数主要用于如下场合:

  1. 简单且孤立的数据。若是复杂且相互关联的数据,则要保存于关系数据库。
  2. 文本形式的数据。若是二进制数据,则要保存至文件。
  3. 需要持久化存储的数据。App退出后再次启动时,之前保存的数据仍然有效。

实际开发中,共享参数经常存储的数据包括:App的个性化配置信息、用户使用App的行为信息、临时需要保存的片段信息等。

共享参数对数据的存储和读取操作类似于Map,也有存储数据的put方法,以及读取数据的get方法。调用getSharedPreferences方法可以获得共享参数实例,获取代码示例如下:

由以上代码可知,getSharedPreferences方法的第一个参数是文件名,填share表示共享参数的文件名 是share.xml;第二个参数是操作模式,填MODE_PRIVATE表示私有模式。

往共享参数存储数据要借助于Editor类,保存数据的代码示例如下:

(完整代码见chapter06\src\main\java\com\example\chapter06\ShareWriteActivity.java)

从共享参数读取数据相对简单,直接调用共享参数实例的get * * * 方法即可读取键值,注意 get***方法的第二个参数表示默认值,读取数据的代码示例如下:

(完整代码见chapter06\src\main\java\com\example\chapter06\ShareReadActivity.java)

下面通过测试页面演示共享参数的存取过程,先在编辑页面录入用户注册信息,点击保存按钮把数据提 交至共享参数,如图6-1所示。再到查看页面浏览用户注册信息,App从共享参数中读取各项数据,并将注册信息显示在页面上,如图6-2所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uOPAS0xh-1676725568099)(media/92a7975d521e05308ae0ddbdf21b853c.jpeg)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iUwbP0Yq-1676725568100)(media/3bcd65a8552d34a7a8779acc25a3f691.jpeg)]图6-1 把注册信息写入共享参数

图6-2 从共享参数读取注册信息

实现记住密码功能

上一章末尾的实战项目,登录页面下方有一个“记住密码”复选框,当时只是为了演示控件的用法,并未 真正记住密码。因为用户退出后重新进入登录页面,App没有回忆起上次的登录密码。现在利用共享参 数改造该项目,使之实现记住密码的功能。

改造内容主要有下列3处:

  1. 声明一个共享参数对象,并在onCreate中调用getSharedPreferences方法获取共享参数的实例。

  2. 登录成功时,如果用户勾选了“记住密码”,就使用共享参数保存手机号码与密码。也就是在

    loginSuccess方法中增加以下代码:

(完整代码见chapter06\src\main\java\com\example\chapter06\LoginShareActivity.java)

  1. 再次打开登录页面时,App从共享参数读取手机号码与密码,并自动填入编辑框。也就是在

    onCreate方法中增加以下代码:

代码修改完毕,只要用户上次登录成功时勾选“记住密码”,下次进入登录页面后App就会自动填写上次登录的手机号码与密码。具体的效果如图6-3和图6-4所示。其中,图6-3为用户首次登录成功的界面,此时勾选了“记住密码”;图6-4为用户再次进入登录的界面,因为上次登录成功时已经记住密码,所以这次页 面会自动填充保存的登录信息。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NXR2s1Mj-1676725568100)(media/a79e0281271bb04e296ba2720c8475b1.jpeg)]

图6-3 将登录信息保存到共享参数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Iu3igPpZ-1676725568101)(media/c0a25577de4878aa5f87d461dc026979.jpeg)]

图6-4 从共享参数读取登录信息

利用设备浏览器寻找共享参数文件

前面的“6.1.1 共享参数的基本用法”提到,参数文件的路径为“/data/data/应用包名/shared_prefs/* * *

.xml”,然而使用手机自带的文件管理器却找不到该路径,data下面只有空目录而已。这是因为手机厂商加了层保护,不让用户查看App的核心文件,否则万一不小心误删了,App岂不是运行报错了?当然作 为开发者,只要打开了手机的USB调试功能,还是有办法拿到测试应用的数据文件。首先打开Android Studio,依次选择菜单Run→Run ‘***’,把测试应用比如chapter06安装到手机上。接着单击Android

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IFqRXonI-1676725568107)(media/b5e8e2aff06fba4358adfc01c8394ace.jpeg)]Studio左下角的logcat标签,找到已连接的手机设备和测试应用,如图6-5所示。

图6-5 Android Studio找到已连接的设备

注意到logcat窗口的右边,也就是Android Studio右下角有个竖排标签“Device File Explorer”,翻译过来叫设备文件浏览器。单击该标签按钮,此时主界面右边弹出名为“Device File Explorer”的窗口,如图6-6 所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ybx3C6Dt-1676725568108)(media/6608547b778938b6869275c06f5a50f3.jpeg)]

图6-6 设备文件浏览器的窗口

在图6-6的窗口中依次展开各级目录,进到/data/data/com.example.chapter06/shared_prefs目录,在该目录下看到了参数文件share.xml。右击share.xml,并在右键菜单中选择“Save As”,把该文件保存到电脑中,之后就能查看详细的文件内容了。不仅参数文件,凡是保存在“/data/data/应用包名/”下面的所有文件,均可利用设备浏览器导出至电脑,下一节将要介绍的数据库db文件也可按照以上步骤导出。

.2 数据库SQLite

本节介绍Android的数据库存储方式—SQLite的使用方法,包括:SQLite用到了哪些SQL语法,如何使用 数据库管理器操纵SQLite,如何使用数据库帮助器简化数据库操作等,以及如何利用SQLite改进登录页面的记住密码功能。

SQL的基本语法

SQL本质上是一种编程语言,它的学名叫作“结构化查询语言”(全称为Structured Query Language,简称SQL)。不过SQL语言并非通用的编程语言,它专用于数据库的访问和处理,更像是一种操作命令, 所以常说SQL语句而不说SQL代码。标准的SQL语句分为3类:数据定义、数据操纵和数据控制,但不同的数据库往往有自己的实现。

SQLite是一种小巧的嵌入式数据库,使用方便、开发简单。如同MySQL、Oracle那样,SQLite也采用

SQL语句管理数据,由于它属于轻型数据库,不涉及复杂的数据控制操作,因此App开发只用到数据定 义和数据操纵两类SQL。此外,SQLite的SQL语法与通用的SQL语法略有不同,接下来介绍的两类SQL语 法全部基于SQLite。

数据定义语言

数据定义语言全称Data Definition Language,简称DDL,它描述了怎样变更数据实体的框架结构。就

SQLite而言,DDL语言主要包括3种操作:创建表格、删除表格、修改表结构,分别说明如下。

  1. 创建表格

表格的创建动作由create命令完成,格式为“CREATE TABLE IF NOT EXISTS 表格名称(以逗号分隔的各字段定义);”。以用户信息表为例,它的建表语句如下所示:

上面的SQL语法与其他数据库的SQL语法有所出入,相关的注意点说明见下:

①SQL语句不区分大小写,无论是create与table这类关键词,还是表格名称、字段名称,都不区分大小 写。唯一区分大小写的是被单引号括起来的字符串值。

②为避免重复建表,应加上IF NOT EXISTS关键词,例如CREATE TABLE IF NOT EXISTS 表格名称……

③SQLite支持整型INTEGER、长整型LONG、字符串VARCHAR、浮点数FLOAT,但不支持布尔类型。布尔类型的数据要使用整型保存,如果直接保存布尔数据,在入库时SQLite会自动将它转为0或1,其中0 表示false,1表示true。

④建表时需要唯一标识字段,它的字段名为id。创建新表都要加上该字段定义,例如id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL 。

  1. 删除表格

表格的删除动作由drop命令完成,格式为“DROP TABLE IF EXISTS 表格名称;”。下面是删除用户信息表的SQL语句例子:

  1. 修改表结构

表格的修改动作由alter命令完成,格式为“ALTER TABLE 表格名称 修改操作;”。不过SQLite只支持增加字段,不支持修改字段,也不支持删除字段。对于字段增加操作,需要在alter之后补充add命令, 具体格式如“ALTER TABLE 表格名称 ADD COLUMN 字段名称 字段类型;”。下面是给用户信息表增加手机号字段的SQL语句例子:

注意,SQLite的ALTER语句每次只能添加一列字段,若要添加多列,就得分多次添加。

数据操纵语言

数据操纵语言全称Data Manipulation Language,简称DML,它描述了怎样处理数据实体的内部记录。表格记录的操作类型包括添加、删除、修改、查询4类,分别说明如下:

  1. 添加记录

    记录的添加动作由insert命令完成,格式为“INSERT INTO 表格名称(以逗号分隔的字段名列表)

    VALUES (以逗号分隔的字段值列表);”。下面是往用户信息表插入一条记录的SQL语句例子:

  2. 删除记录

记录的删除动作由delete命令完成,格式为“DELETE FROM 表格名称 WHERE 查询条件;”,其中查询条件的表达式形如“字段名=字段值”,多个字段的条件交集通过“AND”连接,条件并集通过“OR”连接。 下面是从用户信息表删除指定记录的SQL语句例子:

  1. 修改记录

记录的修改动作由update命令完成,格式为“UPDATE 表格名称 SET 字段名=字段值 WHERE 查询条件;”。下面是对用户信息表更新指定记录的SQL语句例子:

  1. 查询记录

记录的查询动作由select命令完成,格式为“SELECT 以逗号分隔的字段名列表 FROM 表格名称WHERE 查询条件;”。如果字段名列表填星号“*”,则表示查询该表的所有字段。下面是从用户信息表查询指定记录的SQL语句例子:

查询操作除了比较字段值条件之外,常常需要对查询结果排序,此时要在查询条件后面添加排序条件, 对应的表达式为“ORDER BY 字段名 ASC或者DESC”,意指对查询结果按照某个字段排序,其中ASC 代表升序,DESC代表降序。下面是查询记录并对结果排序的SQL语句例子:

如果读者之前不熟悉SQL语法,建议下载一个SQLite管理软件,譬如SQLiteStudio,先在电脑上多加练 习SQLite的常见操作语句。

数据库管理器SQLiteDatabase

SQL语句毕竟只是SQL命令,若要在Java代码中操纵SQLite,还需专门的工具类。SQLiteDatabase便是

Android提供的SQLite数据库管理器,开发者可以在活动页面代码调用openOrCreateDatabase方法获取数据库实例,参考代码如下:

(完整代码见chapter06\src\main\java\com\example\chapter06\DatabaseActivity.java)

首次运行测试App,调用openOrCreateDatabase方法会自动创建数据库,并返回该数据库的管理器实 例,创建结果如图6-7所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sUncyJYE-1676725568111)(media/49fc535a1d19185116d0b01fea9184c6.jpeg)]

图6-7 创建数据库的结果提示

获得数据库实例之后,就能对该数据库开展各项操作了。数据库管理器SQLiteDatabase提供了若干操作 数据表的API,常用的方法有3类,列举如下:

管理类,用于数据库层面的操作

openDatabase:打开指定路径的数据库。isOpen: 判 断 数 据 库 是 否 已 打 开 。 close: 关 闭 数 据 库 。 getVersion:获取数据库的版本号。setVersion:设置数据库的版本号。

事务类,用于事务层面的操作

beginTransaction: 开 始 事 务 。 setTransactionSuccessful:设置事务的成功标志。endTransaction:结束事务。执行本方法时,系统会判断之前是否调用了

setTransactionSuccessful方法,如果之前已调用该方法就提交事务,如果没有调用该方法就回滚事务。

数据处理类,用于数据表层面的操作

execSQL:执行拼接好的SQL控制语句。一般用于建表、删表、变更表结构。delete:删除符合条件的记录。

update: 更 新 符 合 条 件 的 记 录 信 息 。 insert: 插 入 一 条 记 录 。 query:执行查询操作,并返回结果集的游标。

rawQuery:执行拼接好的SQL查询语句,并返回结果集的游标。

在实际开发中,比较经常用到的是查询语句,建议先写好查询操作的select语句,再调用rawQuery方法 执行查询语句。

数据库帮助器SQLiteOpenHelper

由于SQLiteDatabase存在局限性,一不小心就会重复打开数据库,处理数据库的升级也不方便;因此

Android提供了数据库帮助器SQLiteOpenHelper,帮助开发者合理使用SQLite。

SQLiteOpenHelper的具体使用步骤如下:

步骤一,新建一个继承自SQLiteOpenHelper的数据库操作类,按提示重写onCreate和onUpgrade两个 方法。其中,onCreate方法只在第一次打开数据库时执行,在此可以创建表结构;而onUpgrade方法在 数据库版本升高时执行,在此可以根据新旧版本号变更表结构。

步骤二,为保证数据库安全使用,需要封装几个必要方法,包括获取单例对象、打开数据库连接、关闭 数据库连接,说明如下:

获取单例对象:确保在App运行过程中数据库只会打开一次,避免重复打开引起错误。

打开数据库连接:SQLite有锁机制,即读锁和写锁的处理;故而数据库连接也分两种,读连接可调用getReadableDatabase方法获得,写连接可调用getWritableDatabase获得。

关闭数据库连接:数据库操作完毕,调用数据库实例的close方法关闭连接。

步骤三, 提供对表记录增加、删除、修改、查询的操作方法。

能被SQLite直接使用的数据结构是ContentValues类,它类似于映射Map,也提供了put和get方法存取 键值对。区别之处在于:ContentValues的键只能是字符串,不能是其他类型。ContentValues主要用于 增加记录和更新记录,对应数据库的insert和update方法。

记录的查询操作用到了游标类Cursor,调用query和rawQuery方法返回的都是Cursor对象,若要获取全 部的查询结果,则需根据游标的指示一条一条遍历结果集合。Cursor的常用方法可分为3类,说明如

下:

游标控制类方法,用于指定游标的状态

close: 关 闭 游 标 。 isClosed:判断游标是否关闭。isFirst:判断游标是否在开头。isLast:判断游标是否在末尾。

游标移动类方法,把游标移动到指定位置

moveToFirst:移动游标到开头。moveToLast:移动游标到末尾。moveToNext:移动游标到下一条记录。moveToPrevious:移动游标到上一条记录。move:往后移动游标若干条记录。moveToPosition:移动游标到指定位置的记录。

获取记录类方法,可获取记录的数量、类型以及取值

getCount:获取结果记录的数量。getInt:获取指定字段的整型值。getLong:获取指定字段的长整型值。getFloat:获取指定字段的浮点数值。getString:获取指定字段的字符串值。getType:获取指定字段的字段类型。

鉴于数据库操作的特殊性,不方便单独演示某个功能,接下来从创建数据库开始介绍,完整演示一下数 据库的读写操作。用户注册信息的演示页面包括两个,分别是记录保存页面和记录读取页面,其中记录 保存页面通过insert方法向数据库添加用户信息,完整代码见chapter06\src\main\java\com\example\chapter06\SQLiteWriteActivity.java;而记录读取页面通过

query方法从数据库读取用户信息,完整代码见chapter06\src\main\java\com\example\chapter06\SQLiteReadActivity.java。

运行测试App,先打开记录保存页面,依次录入并将两个用户的注册信息保存至数据库,如图6-8和图6-

9所示。再打开记录读取页面,从数据库读取用户注册信息并展示在页面上,如图6-10所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-31C2G4uT-1676725568111)(media/fc5b21eaf2fc5e56fa42c30accb84ba3.jpeg)]

图6-8 第一条注册信息保存到数据库

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cof2f5CS-1676725568113)(media/96fcb2462744e675e31b81d306b2ed1b.jpeg)]

图6-9 第二条注册信息保存到数据库

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hX1crfJr-1676725568114)(media/601295ff1609f3b79a17dcb25e454d0f.jpeg)]

图6-10 从数据库读取了两条注册信息

上述演示页面主要用到了数据库记录的添加、查询和删除操作,对应的数据库帮助器关键代码如下所 示,尤其关注里面的insert、delete、update和query方法:

(完整代码见chapter06\src\main\java\com\example\chapter06\database\UserDBHelper.java)

return mHelper;

}

// 打开数据库的读连接

public SQLiteDatabase openReadLink() { if (mDB == null || !mDB.isOpen()) {

mDB = mHelper.getReadableDatabase();

}

return mDB;

}

// 打开数据库的写连接

public SQLiteDatabase openWriteLink() { if (mDB == null || !mDB.isOpen()) {

mDB = mHelper.getWritableDatabase();

}

return mDB;

}

// 关闭数据库连接

public void closeLink() {

if (mDB != null && mDB.isOpen()) { mDB.close();

mDB = null;

}

}

// 创建数据库,执行建表语句

public void onCreate(SQLiteDatabase db) { Log.d(TAG, “onCreate”);

String drop_sql = "DROP TABLE IF EXISTS " + TABLE_NAME + “;”;

Log.d(TAG, “drop_sql:” + drop_sql); db.execSQL(drop_sql);

String create_sql = “CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (”

+ “_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,”

+ “name VARCHAR NOT NULL,” + “age INTEGER NOT NULL,”

+ “height INTEGER NOT NULL,” + “weight FLOAT NOT NULL,”

+ “married INTEGER NOT NULL,” + “update_time VARCHAR NOT NULL”

//演示数据库升级时要先把下面这行注释

+ “,phone VARCHAR” + “,password VARCHAR”

+ “);”;

Log.d(TAG, “create_sql:” + create_sql); db.execSQL(create_sql); // 执行完整的SQL语句

}

// 升级数据库,执行表结构变更语句

public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { Log.d(TAG, “onUpgrade oldVersion=” + oldVersion + “, newVersion=” +

newVersion);

if (newVersion > 1) {

//Android的ALTER命令不支持一次添加多列,只能分多次添加

String alter_sql = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " +

“phone VARCHAR;”;

Log.d(TAG, “alter_sql:” + alter_sql); db.execSQL(alter_sql);

alter_sql = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + "password

VARCHAR;";

Log.d(TAG, “alter_sql:” + alter_sql);

db.execSQL(alter_sql); // 执行完整的SQL语句

}

}

// 根据指定条件删除表记录

public int delete(String condition) {

// 执行删除记录动作,该语句返回删除记录的数目

return mDB.delete(TABLE_NAME, condition, null);

}

// 删除该表的所有记录

public int deleteAll() {

// 执行删除记录动作,该语句返回删除记录的数目

return mDB.delete(TABLE_NAME, “1=1”, null);

}

// 往该表添加一条记录

public long insert(UserInfo info) {

List<UserInfo> infoList = new ArrayList<UserInfo>(); infoList.add(info);

return insert(infoList);

}

// 往该表添加多条记录

public long insert(List<UserInfo> infoList) { long result = -1;

for (int i = 0; i < infoList.size(); i++) { UserInfo info = infoList.get(i);

List<UserInfo> tempList = new ArrayList<UserInfo>();

// 如果存在同名记录,则更新记录

// 注意条件语句的等号后面要用单引号括起来

if (info.name != null && info.name.length() > 0) {

String condition = String.format(“name=‘%s’”, info.name); tempList = query(condition);

if (tempList.size() > 0) { update(info, condition); result = tempList.get(0).rowid; continue;

}

}

// 如果存在同样的手机号码,则更新记录

if (info.phone != null && info.phone.length() > 0) {

String condition = String.format(“phone=‘%s’”, info.phone); tempList = query(condition);

if (tempList.size() > 0) { update(info, condition); result = tempList.get(0).rowid; continue;

}

}

// 不存在唯一性重复的记录,则插入新记录ContentValues cv = new ContentValues(); cv.put(“name”, info.name);

cv.put(“age”, info.age); cv.put(“height”, info.height); cv.put(“weight”, info.weight); cv.put(“married”, info.married); cv.put(“update_time”, info.update_time);

cv.put(“phone”, info.phone); cv.put(“password”, info.password);

// 执行插入记录动作,该语句返回插入记录的行号

result = mDB.insert(TABLE_NAME, “”, cv);

if (result == -1) { // 添加成功则返回行号,添加失败则返回-1 return result;

}

}

return result;

}

// 根据条件更新指定的表记录

public int update(UserInfo info, String condition) { ContentValues cv = new ContentValues(); cv.put(“name”, info.name);

cv.put(“age”, info.age); cv.put(“height”, info.height); cv.put(“weight”, info.weight); cv.put(“married”, info.married); cv.put(“update_time”, info.update_time); cv.put(“phone”, info.phone); cv.put(“password”, info.password);

// 执行更新记录动作,该语句返回更新的记录数量

return mDB.update(TABLE_NAME, cv, condition, null);

}

public int update(UserInfo info) {

// 执行更新记录动作,该语句返回更新的记录数量

return update(info, “rowid=” + info.rowid);

}

// 根据指定条件查询记录,并返回结果数据列表

public List<UserInfo> query(String condition) {

String sql = String.format(“select rowid,_id,name,age,height,weight,married,update_time,” +

“phone,password from %s where %s;”, TABLE_NAME, condition); Log.d(TAG, "query sql: " + sql);

List<UserInfo> infoList = new ArrayList<UserInfo>();

// 执行记录查询动作,该语句返回结果集的游标

Cursor cursor = mDB.rawQuery(sql, null);

// 循环取出游标指向的每条记录

while (cursor.moveToNext()) { UserInfo info = new UserInfo();

info.rowid = cursor.getLong(0); // 取出长整型数

info.xuhao = cursor.getInt(1); // 取出整型数info.name = cursor.getString(2); // 取出字符串info.age = cursor.getInt(3); // 取出整型数info.height = cursor.getLong(4); // 取出长整型数info.weight = cursor.getFloat(5); // 取出浮点数

//SQLite没有布尔型,用0表示false,用1表示true

info.married = (cursor.getInt(6) == 0) ? false : true; info.update_time = cursor.getString(7); // 取出字符串info.phone = cursor.getString(8); // 取出字符串info.password = cursor.getString(9); // 取出字符串infoList.add(info);

}

cursor.close(); // 查询完毕,关闭数据库游标

return infoList;

优化记住密码功能

在“6.1.2 实现记住密码功能”中,虽然使用共享参数实现了记住密码功能,但是该方案只能记住一个用户的登录信息,并且手机号码跟密码没有对应关系,如果换个手机号码登录,前一个用户的登录信息就 被覆盖了。真正的记住密码功能应当是这样的:先输入手机号码,然后根据手机号码匹配保存的密码, 一个手机号码对应一个密码,从而实现具体手机号码的密码记忆功能。

现在运用数据库技术分条存储各用户的登录信息,并支持根据手机号查找登录信息,从而同时记住多个 手机号的密码。具体的改造主要有下列3点:

  1. 声明一个数据库的帮助器对象,然后在活动页面的onResume方法中打开数据库连接,在onPasue 方法中关闭数据库连接,示例代码如下:

    (完整代码见chapter06\src\main\java\com\example\chapter06\LoginSQLiteActivity.java)

  2. 登录成功时,如果用户勾选了“记住密码”,就将手机号码及其密码保存至数据库。也就是在

    loginSuccess方法中增加如下代码:

  3. 再次打开登录页面,用户输入手机号再点击密码框的时候,App根据手机号到数据库查找登录信 息,并将记录结果中的密码填入密码框。其中根据手机号码查找登录信息,要求在帮助器代码中添加以 下方法,用于找到指定手机的登录密码:

此外,上面第3点的点击密码框触发查询操作,用到了编辑框的焦点变更事件,有关焦点变更监听器的详 细用法参见第5章的“5.3.2 焦点变更监听器”。就本案例而言,光标切到密码框触发焦点变更事件,具体处理逻辑要求重写监听器的onFocusChange方法,重写后的方法代码如下所示:

重新运行测试App,先打开登录页面,勾选“记住密码”,并确保本次登录成功。然后再次进入登录页面, 输入手机号码后光标还停留在手机框,如图6-11所示。接着点击密码框,光标随之跳到密码框,此时密 码框自动填入了该号码对应的密码串,如图6-12所示。由效果图可见,这次实现了真正意义上的记住密 码功能。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ulgdXdOn-1676725568115)(media/0ca49e3f8145c14ac9e783e7bfd6382f.jpeg)]

图6-11 光标在手机号码框

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SaD83wrA-1676725568115)(media/2beed75b99e4837e1115451e547fd1a8.jpeg)]

图6-12 光标在密码输入框

.3 存储卡的文件操作

本节介绍Android的文件存储方式—在存储卡上读写文件,包括:公有存储空间与私有存储空间有什么区别、如何利用存储卡读写文本文件、如何利用存储卡读写图片文件等。

私有存储空间与公共存储空间

为了更规范地管理手机存储空间,Android从7.0开始将存储卡划分为私有存储和公共存储两大部分,也就是分区存储方式,系统给每个App都分配了默认的私有存储空间。App在私有空间上读写文件无须任 何授权,但是若想在公共空间读写文件,则要在AndroidManifest.xml里面添加下述的权限配置。

但是即使App声明了完整的存储卡操作权限,系统仍然默认禁止该App访问公共空间。打开手机的系统设置界面,进入到具体应用的管理页面,会发现该应用的存储访问权限被禁止了,如图6-13所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ffmTDJuR-1676725568116)(media/fd1ac7de784b3c0f269aa94dd7ae3d4c.jpeg)]

图6-13 系统设置页面里的存储访问权限开关

当然图示的禁止访问只是不让访问存储卡的公共空间,App自身的私有空间依旧可以正常读写。这缘于

Android把存储卡分成了两块区域,一块是所有应用均可访问的公共空间,另一块是只有应用自己才可 访问的专享空间。虽然Android给每个应用都分配了单独的安装目录,但是安装目录的空间很紧张,所 以Android在存储卡的“Android/data”目录下给每个应用又单独建了一个文件目录,用来保存应用自己 需要处理的临时文件。这个目录只有当前应用才能够读写文件,其他应用是不允许读写的。由于私有空 间本身已经加了访问权限控制,因此它不受系统禁止访问的影响,应用操作自己的文件目录自然不成问 题。因为私有的文件目录只有属主应用才能访问,所以一旦属主应用被卸载,那么对应的目录也会被删 掉。

既然存储卡分为公共空间和私有空间两部分,它们的空间路径获取也就有所不同。若想获取公共空间的 存储路径,调用的是Environment.getExternalStoragePublicDirectory方法;若想获取应用私有空间的 存储路径,调用的是getExternalFilesDir方法。下面是分别获取两个空间路径的代码例子:

(完整代码见chapter06\src\main\java\com\example\chapter06\FilePathActivity.java)

该例子运行之后获得的路径信息如图6-14所示,可见应用的私有空间路径位于“存储卡根目录/Android/data/应用包名/files/Download”这个目录中。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-04W1y9mb-1676725568117)(media/73cff7dbcc3607c4a4cd9c24b0242f9b.jpeg)]

图6-14 公共存储与私有存储的目录路径

在存储卡上读写文本文件

文本文件的读写借助于文件IO流FileOutputStream和FileInputStream。其中,FileOutputStream用于 写文件,FileInputStream用于读文件,它们读写文件的代码例子如下:

(完整代码见chapter06\src\main\java\com\example\chapter06\util\FileUtil.java)

接着分别创建写文件页面和读文件页面,其中写文件页面调用saveText方法保存文本,完整代码见

chapter06\src\main\java\com\example\chapter06\FileWriteActivity.java;而读文件页面调用

readText方法从指定路径的文件中读取文本内容,完整代码见chapter06\src\main\java\com\example\chapter06\FileReadActivity.java。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fHVfHxKS-1676725568118)(media/ca4fa734f12858d7b631c1cfe5251e76.jpeg)]然后运行测试App,先打开文本写入页面,录入注册信息后保存为私有目录里的文本文件,此时写入界 面如图6-15所示。再打开文本读取页面,App自动在私有目录下找到文本文件列表,并展示其中一个文件的文本内容,此时读取界面如图6-16所示。

图6-15 将注册信息保存到文本文件

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lMB2q2TD-1676725568118)(media/ccd651be97859052a17935ef3746736b.jpeg)]

图6-16 从文本文件读取注册信息

在存储卡上读写图片文件

文本文件读写可以转换为对字符串的读写,而图片文件保存的是图像数据,需要专门的位图工具Bitmap 处理。位图对象依据来源不同又分成3种获取方式,分别对应位图工厂BitmapFactory的下列3种方法:

decodeResource:从指定的资源文件中获取位图数据。例如下面代码表示从资源文件huawei.png

获取位图对象:

decodeFile:从指定路径的文件中获取位图数据。注意从Android 10开始,该方法只适用于私有目录下的图片,不适用公共空间下的图片。

decodeStream:从指定的输入流中获取位图数据。比如使用IO流打开图片文件,此时文件输入流 对象即可作为decodeStream方法的入参,相应的图片读取代码如下:

(完整代码见chapter06\src\main\java\com\example\chapter06\util\FileUtil.java)

得到位图对象之后,就能在图像视图上显示位图。图像视图ImageView提供了下列方法显示各种来源的图片:

setImageResource:设置图像视图的图片资源,该方法的入参为资源图片的编号,形如

“R.drawable.去掉扩展名的图片名称”。

setImageBitmap:设置图像视图的位图对象,该方法的入参为Bitmap类型。

setImageURI:设置图像视图的路径对象,该方法的入参为Uri类型。字符串格式的文件路径可通过 代码“Uri.parse(file_path)”转换成路径对象。

读取图片文件的花样倒是挺多,把位图数据写入图片文件却只有一种,即通过位图对象的compress方法将位图数据压缩到文件输出流。具体的图片写入代码如下所示:

接下来完整演示一遍图片文件的读写操作,首先创建图片写入页面,从某个资源图片读取位图数据,再 把位图数据保存为私有目录的图片文件,相关代码示例如下:

(完整代码见chapter06\src\main\java\com\example\chapter06\ImageWriteActivity.java)

然后创建图片读取页面,从私有目录找到图片文件,并挑出一张在图像视图上显示,相关代码示例如 下:

(完整代码见chapter06\src\main\java\com\example\chapter06\ImageReadActivity.java)

运行测试App,先打开图片写入页面,点击保存按钮把资源图片保存到存储卡,此时写入界面如图6-17 所示。再打开图片读取页面,App自动在私有目录下找到图片文件列表,并展示其中一张图片,此时读 取界面如图6-18所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i8DtV7iu-1676725568119)(media/8bc1246487a3cba5be8c54fa2ab14844.jpeg)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TwTTrWzb-1676725568119)(media/34da8916873cac7663d926d1b99105ea.jpeg)]图6-17 把资源图片保存到存储卡

图6-18 从存储卡读取图片文件

.4 应用组件Application

本节介绍Android重要组件Application的基本概念和常见用法。首先说明Application的生命周期贯穿了

App的整个运行过程,接着利用Application实现App全局变量的读写,然后阐述了如何借助App实例来 操作Room数据库框架。

Application的生命周期

Application是Android的一大组件,在App运行过程中有且仅有一个Application对象贯穿应用的整个生 命周期。打开AndroidManifest.xml,发现activity节点的上级正是application节点,不过该节点并未指定name属性,此时App采用默认的Application实例。

注意到每个activity节点都指定了name属性,譬如常见的name属性值为.MainActivity,让人知晓该

activity的入口代码是MainActivity.java。现在尝试给application节点加上name属性,看看其庐山真面 目,具体步骤说明如下:

  1. 打开AndroidManifest.xml,给application节点加上name属性,表示application的入口代码是

    MainApplication.java。修改后的application节点示例如下:

    (完整代码见chapter06\src\main\AndroidManifest.xml)

  2. 在Java代码的包名目录下创建MainApplication.java,要求该类继承Application,继承之后可供重写的方法主要有以下3个。

    onCreate:在App启动时调用。

    onTerminate:在App终止时调用(按字面意思)。onConfigurationChanged:在配置改变时调用,例如从竖屏变为横屏。

光看字面意思的话,与生命周期有关的方法是onCreate和onTerminate,那么重写这两个方法,并在重 写后的方法中打印日志,修改后的Java代码如下所示:

(完整代码见chapter06\src\main\java\com\example\chapter06\MainApplication.java)

  1. 运行测试App,在logcat窗口观察应用日志。但是只在启动一开始看到MainApplication的

onCreate日志(该日志先于MainActivity的onCreate日志),却始终无法看到它的onTerminate日志, 无论是自行退出App还是强行杀掉App,日志都不会打印onTerminate。

无论你怎么折腾,这个onTerminate日志都不会出来。Android明明提供了这个方法,同时提供了关于 该方法的解释,说明文字如下:This method is for use in emulated process environments.It will never be called on a production Android device, where processes are removed by simply killing them; no user code (including this callback) is executed when doing so。这段话的意思是:该方法供模拟环境使用,它在真机上永远不会被调用,无论是直接杀进程还是代码退出;执行该操作时,不会 执行任何用户代码。

现在很明确了,onTerminate方法就是个摆设,中看不中用。如果读者想在App退出前回收系统资源, 就不能指望onTerminate方法的回调了。

利用Application操作全局变量

C/C++有全局变量的概念,因为全局变量保存在内存中,所以操作全局变量就是操作内存,显然内存的 读写速度远比读写数据库或读写文件快得多。所谓全局,指的是其他代码都可以引用该变量,因此全局 变量是共享数据和消息传递的好帮手。不过Java没有全局变量的概念,与之比较接近的是类里面的静态成员变量,该变量不但能被外部直接引用,而且它在不同地方引用的值是一样的(前提是在引用期间不 能改动变量值),所以借助静态成员变量也能实现类似全局变量的功能。

根据上一小节的介绍可知,Application的生命周期覆盖了App运行的全过程。不像短暂的Activity生命周 期,一旦退出该页面,Activity实例就被销毁。因此,利用Application的全生命特性,能够在Application实例中保存全局变量。

适合在Application中保存的全局变量主要有下面3类数据:

  1. 会频繁读取的信息,例如用户名、手机号码等。

  2. 不方便由意图传递的数据,例如位图对象、非字符串类型的集合对象等。

  3. 容易因频繁分配内存而导致内存泄漏的对象,例如Handler处理器实例等。要想通过Application实现全局内存的读写,得完成以下3项工作:

  4. 编写一个继承自Application的新类MainApplication。该类采用单例模式,内部先声明自身类的一 个静态成员对象,在创建App时把自身赋值给这个静态对象,然后提供该对象的获取方法getInstance。 具体实现代码示例如下:

    (完整代码见chapter06\src\main\java\com\example\chapter06\MainApplication.java)

  5. 在活动页面代码中调用MainApplication的getInstance方法,获得它的一个静态对象,再通过该对 象访问MainApplication的公共变量和公共方法。

  6. 不要忘了在AndroidManifest.xml中注册新定义的Application类名,也就是给application节点增 加android:name属性,其值为.MainApplication。

接下来演示如何读写内存中的全局变量,首先分别创建写内存页面和读内存页面,其中写内存页面把用 户的注册信息保存到全局变量infoMap,完整代码见chapter06\src\main\java\com\example\chapter06\AppWriteActivity.java;而读内存页面从全局变量

infoMap读取用户的注册信息,完整代码见

chapter06\src\main\java\com\example\chapter06\AppReadActivity.java。

然后运行测试App,先打开内存写入页面,录入注册信息后保存至全局变量,此时写入界面如图6-19所示。再打开内存读取页面,App自动从全局变量获取注册信息,并展示拼接后的信息文本,此时读取界 面如图6-20所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-auNyr3Ox-1676725568120)(media/f7544c8084241d7958a9a15b623b4ff8.jpeg)]

图6-19 注册信息保存到全局内存

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tvUTNmxu-1676725568120)(media/e088337452244e7839475cc17ea9a16b.jpeg)]

图6-20 从全局内存读取注册信息

利用Room简化数据库操作

虽然Android提供了数据库帮助器,但是开发者在进行数据库编程时仍有诸多不便,比如每次增加一张新表,开发者都得手工实现以下代码逻辑:

  1. 重写数据库帮助器的onCreate方法,添加该表的建表语句。
  2. 在插入记录之时,必须将数据实例的属性值逐一赋给该表的各字段。
  3. 在查询记录之时,必须遍历结果集游标,把各字段值逐一赋给数据实例。
  4. 每次读写操作之前,都要先开启数据库连接;读写操作之后,又要关闭数据库连接。

上述的处理操作无疑存在不少重复劳动,数年来引得开发者叫苦连连。为此各类数据库处理框架纷纷涌 现,包括GreenDao、OrmLite、Realm等,可谓百花齐放。眼见SQLite渐渐乏人问津,谷歌公司干脆整 了个自己的数据库框架—Room,该框架同样基于SQLite,但它通过注解技术极大地简化了数据库操

作,减少了原来相当一部分编码工作量。

由于Room并未集成到SDK中,而是作为第三方框架提供,因此要修改模块的build.gradle文件,往

dependencies节点添加下面两行配置,表示导入指定版本的Room库:

导入Room库之后,还要编写若干对应的代码文件。以录入图书信息为例,此时要对图书信息表进行增删改查,则具体的编码过程分为下列5个步骤:

编写图书信息表对应的实体类

假设图书信息类名为BookInfo,且它的各属性与图书信息表的各字段一一对应,那么要给该类添加

“@Entity”注解,表示该类是Room专用的数据类型,对应的表名称也叫BookInfo。如果BookInfo表的

name字段是该表的主键,则需给BookInfo类的name属性添加“@PrimaryKey”与“@NonNull”两个注解,表示该字段是个非空的主键。下面是BookInfo类的定义代码例子:

(完整代码见chapter06\src\main\java\com\example\chapter06\entity\BookInfo.java)

编写图书信息表对应的持久化类

所谓持久化,指的是将数据保存到磁盘而非内存,其实等同于增删改等SQL语句。假设图书信息表的持 久化类名叫作BookDao,那么该类必须添加“@Dao”注解,内部的记录查询方法必须添加“@Query”注解,记录插入方法必须添加“@Insert”注解,记录更新方法必须添加“@Update”注解,记录删除方法必须 添加“@Delete”注解(带条件的删除方法除外)。对于记录查询方法,允许在@Query之后补充具体的查询语句以及查询条件;对于记录插入方法与记录更新方法,需明确出现重复记录时要采取哪种处理策

略。下面是BookDao类的定义代码例子:

(完整代码见chapter06\src\main\java\com\example\chapter06\dao\BookDao.java)

编写图书信息表对应的数据库类

因为先有数据库然后才有表,所以图书信息表还得放到某个数据库里,这个默认的图书数据库要从

RoomDatabase派生而来,并添加“@Database”注解。下面是数据库类BookDatabase的定义代码例子:

(完整代码见chapter06\src\main\java\com\example\chapter06\database\BookDatabase.java)

在自定义的Application类中声明图书数据库的唯一实例

为了避免重复打开数据库造成的内存泄漏问题,每个数据库在App运行过程中理应只有一个实例,此时 要求开发者自定义新的Application类,在该类中声明并获取图书数据库的实例,并将自定义的

Application类设为单例模式,保证App运行之时有且仅有一个应用实例。下面是自定义Application类的 代码例子:

(完整代码见chapter06\src\main\java\com\example\chapter06\MainApplication.java)

public class MainApplication extends Application { private final static String TAG = “MainApplication”;

private static MainApplication mApp; // 声明一个当前应用的静态实例

// 声明一个公共的信息映射对象,可当作全局变量使用

public HashMap<String, String> infoMap = new HashMap<String, String>();

private BookDatabase bookDatabase; // 声明一个书籍数据库对象

// 利用单例模式获取当前应用的唯一实例

public static MainApplication getInstance() { return mApp;

}

@Override

public void onCreate() { super.onCreate(); Log.d(TAG, “onCreate”);

mApp = this; // 在打开应用时对静态的应用实例赋值

// 构建书籍数据库的实例

bookDatabase = Room.databaseBuilder(mApp, BookDatabase.class,“BookInfo”)

.addMigrations() // 允许迁移数据库(发生数据库变更时,Room默认删除原数据库再创建新数据库。如此一来原来的记录会丢失,故而要改为迁移方式以便保存原有记录)

.allowMainThreadQueries() // 允许在主线程中操作数据库(Room默认不能在主

线程中操作数据库)

}

.build();

// 获取书籍数据库的实例

public BookDatabase getBookDB(){ return bookDatabase;

}

}

在操作图书信息表的地方获取数据表的持久化对象

持久化对象的获取代码很简单,只需下面一行代码就够了:

完成以上5个编码步骤之后,接着调用持久化对象的queryXXX、insertXXX、updateXXX、deleteXXX等 方法,就能实现图书信息的增删改查操作了。例程的图书信息演示页面有两个,分别是记录保存页面和 记录读取页面,其中记录保存页面通过insertOneBook方法向数据库添加图书信息,完整代码见chapter06\src\main\java\com\example\chapter06\RoomWriteActivity.java;而记录读取页面通过

queryAllBook方法从数据库读取图书信息,完整代码见

chapter06\src\main\java\com\example\chapter06\RoomReadActivity.java。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-53iA2w3w-1676725568121)(media/d22b282f27ba9b82ea49423c0efb1d6c.jpeg)]运行测试App,先打开记录保存页面,依次录入两本图书信息并保存至数据库,如图6-21和图6-22所示。再打开记录读取页面,从数据库读取图书信息并展示在页面上,如图6-23所示。

图6-21 第一本图书信息保存到数据库

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8am5t0Rk-1676725568127)(media/564e4605679fd2042c6ad94ecd20c284.jpeg)]

图6-22 第二本图书信息保存到数据库

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r4ObsUac-1676725568127)(media/426b65b202c2306b207ffbfbc14f8998.jpeg)]

图6-23 从数据库读取了两本图书信息

.5 实战项目:购物车

购物车的应用面很广,凡是电商App都可以看到它的身影,之所以选择购物车作为本章的实战项目,除 了它使用广泛的特点,更因为它用到了多种存储方式。现在就让我们开启电商购物车的体验之旅吧。

需求描述

电商App的购物车可谓是司空见惯了,以京东商城的购物车为例,一开始没有添加任何商品,此时空购 物车如图6-24所示,而且提示去逛秒杀商场;加入几件商品之后,购物车页面如图6-25所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Qpd9SyXg-1676725568129)(media/56471efe6113dd90558f02fdf041e181.jpeg)]

图6-24 京东App购物车的初始页面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QmMrCIMw-1676725568129)(media/6c26d5f7c2f814f60b3d6fcfea93c96d.jpeg)]

图6-25 京东App购物车加了几件商品

可见购物车除了底部有个结算行,其余部分主要是已加入购物车的商品列表,然后每个商品行左边是商 品小图,右边是商品名称及其价格。

据此仿照本项目的购物车功能,第一次进入购物车页面,购物车里面是空的,同时提示去逛手机商场, 如图6-26所示。接着去商场页面选购手机,随便挑了几部手机加入购物车,再返回购物车页面,即可看 到购物车的商品列表,如图6-27所示,有商品图片、名称、数量、单价、总价等等信息。当然购物车并不仅仅只是展示待购买的商品,还要支持最终购买的结算操作、支持清空购物车等功能。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VE7EvRUt-1676725568130)(media/9eaede3abc559857b879083595fd443f.jpeg)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LGfedMFa-1676725568130)(media/bc9a9ef54fba7c5c2b923b2ca29014fe.jpeg)]图6-26 首次打开购物车页面

图6-27 选购商品后的购物车

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-axEaEsT7-1676725568130)(media/a1074138eb39ca620fced47d586a109f.jpeg)]购物车的存在感很强,不仅仅在购物车页面才能看到购物车。往往在商场页面,甚至商品详情页面,都 会看到某个角落冒出购物车图标。一旦有新商品加入购物车,购物车图标上的商品数量立马加一。当 然,用户也能点击购物车图标直接跳到购物车页面。商场页面除了商品列表之外,页面右上角还有一个 购物车图标,如图6-28所示,有时这个图标会在页面右下角。商品详情页面通常也有购物车图标,如图6-29所示,倘使用户在详情页面把商品加入购物车,那么图标上的数字也会加一。

图6-28 手机商场页面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0J0Aqb2F-1676725568132)(media/2a32af896a068ecf2db10c39a82dea0d.jpeg)]

图6-29 手机详情页面

至此大概过了一遍购物车需要实现的基本功能,提需求总是很简单的,真正落到实处还得开发者发挥想 象力,把购物车做成一个功能完备的模块。

界面设计

首先找找看,购物车使用了哪些Android控件:

线性布局LinearLayout:购物车界面从上往下排列,用到了垂直方向的线性布局。 网格布局GridLayout:商场页面的陈列橱柜,允许分行分列展示商品。

相对布局RelativeLayout:页面右上角的购物车图标,图标右上角又有数字标记,按照指定方位排 列控件正是相对布局的拿手好戏。

其他常见控件尚有文本视图TextView、图像视图ImageView,按钮控件Button等。

然后考虑一下购物车的存储功能,到底采取了哪些存储方式:

数据库SQLite:最直观的肯定是数据库了,购物车里的商品列表一定是放在SQLite中,增删改查都 少不了它。

全局内存:购物车图标右上角的数字表示购物车中的商品数量,该数值建议保存在全局内存中,这 样不必每次都到数据库中执行count操作。

存储卡文件:通常商品图片来自于电商平台的服务器,此时往往引入图片缓存机制,也就是首次访 问先将网络图片保存到存储卡,下次访问时直接从存储卡获取缓存图片,从而提高图片的加载速 度。

共享参数SharedPreferences:是否首次访问网络图片,这个标志位推荐放在共享参数中,因为它 需要持久化存储,并且只有一个参数信息。

真是想不到,一个小小的购物车,竟然用到了好几种存储方式。

关键代码

为了读者更好更快地完成购物车项目,下面列举几个重要功能的代码片段。

关于页面跳转

因为购物车页面允许直接跳到商场页面,并且商场页面也允许跳到购物车页面,所以如果用户在这两个 页面之间来回跳转,然后再按返回键,结果发现返回的时候也是在两个页面间往返跳转。出现问题的缘 由在于:每次启动活动页面都往活动栈加入一个新活动,那么返回出栈之时,也只好一个一个活动依次 退出了。

解决该问题的办法参见第4章的“4.1.3 Activity的启动模式”,对于购物车的活动跳转需要指定启动标志FLAG_ACTIVITY_CLEAR_TOP,表示活动栈有且仅有该页面的唯一实例,如此即可避免多次返回同一页面的情况。比如从购物车页面跳到商场页面,此时活动跳转的代码示例如下:

又如从商场页面跳到购物车页面,此时活动跳转的代码示例如下:

关于商品图片的缓存

通常商品图片由后端服务器提供,App打开页面时再从服务器下载所需的商品图。可是购物车模块的多 个页面都会展示商品图片,如果每次都到服务器请求图片,显然既耗时间又耗流量非常不经济。因此

App都会缓存常用的图片,一旦从服务器成功下载图片,便在手机存储卡上保存图片文件。然后下次界 面需要加载商品图片时,就先从存储卡寻找该图片,如果找到就读取图片的位图信息,如果没找到就再 到服务器下载图片。

以上的缓存逻辑是最简单的二级图片缓存,实际开发往往使用更高级的三级缓存机制,即“运行内存→存 储卡→网络下载”。当然就初学者而言,先从掌握最简单的二级缓存开始,也就是“存储卡→网络下载”。 按照二级缓存机制,可以设计以下的缓存处理逻辑:

  1. 先判断是否为首次访问网络图片。

  2. 如果是首次访问网络图片,就先从网络服务器下载图片。

  3. 把下载完的图片数据保存到手机的存储卡。

  4. 往数据库中写入商品记录,以及商品图片的本地存储路径。

  5. 更新共享参数中的首次访问标志。

    按照上述的处理逻辑,编写的图片加载代码示例如下:

    (完整代码见chapter06\src\main\java\com\example\chapter06\ShoppingCartActivity.java)

关于各页面共同的标题栏

注意到购物车、手机商场、手机详情三个页面顶部都有标题栏,而且这三个标题栏风格统一,既然如 此,能否把它做成公共的标题栏呢?当然App界面支持局部的公共布局,以购物车的标题栏为例,公共 布局的实现过程包括以下两个步骤:

步骤一,首先定义标题栏专用的布局文件,包含返回箭头、文字标题、购物车图标、商品数量表等,具 体内容如下所示:

(完整代码见chapter06\src\main\res\layout\title_shopping.xml)

步骤二,然后在购物车页面的布局文件中添加如下一行include标签,表示引入title_shopping.xml的布 局内容:

(完整代码见chapter06\src\main\res\layout\activity_shopping_cart.xml)

之后重新运行测试App,即可发现购物车页面的顶部果然出现了公共标题栏,商场页面、详情页面的公 共标题栏可参考购物车页面的include标签。

关于商品网格的单元布局

商场页面的商品列表,呈现三行二列的表格布局,每个表格单元的界面布局雷同,都是商品名称在上、 商品图片居中、商品价格与添加按钮在下,看起来跟公共标题栏的处理有些类似。但后者为多个页面引 用同一个标题栏,是多对一的关系;而前者为一个商场页面引用了多个商品网格,是一对多的关系。因 此二者的实现过程不尽相同,就商场网格而言,它的单元复用分为下列3个步骤:

步骤一,在商场页面的布局文件中添加GridLayout节点,如下所示:

(完整代码见chapter06\src\main\res\layout\activity_shopping_channel.xml)

步骤二,为商场网格编写统一的商品信息布局,XML文件内容示例如下:

(完整代码见chapter06\src\main\res\layout\item_goods.xml)

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id=“@+id/ll_item”

android:layout_width=“wrap_content” android:layout_height=“wrap_content” android:layout_gravity=“center” android:gravity=“center” android:background=“@color/white” android:orientation=“vertical”>

<TextView

android:id=“@+id/tv_name” android:layout_width=“match_parent” android:layout_height=“wrap_content” android:gravity=“center” android:textColor=“@color/black” android:textSize=“17sp” />

<ImageView

android:id=“@+id/iv_thumb” android:layout_width=“180dp” android:layout_height=“150dp” android:scaleType=“fitCenter” />

<LinearLayout

android:layout_width=“match_parent” android:layout_height=“45dp” android:orientation=“horizontal”>

<TextView

android:id=“@+id/tv_price” android:layout_width=“0dp” android:layout_height=“match_parent” android:layout_weight=“2” android:gravity=“center” android:textColor=“@color/red” android:textSize=“15sp” />

<Button

android:id=“@+id/btn_add” android:layout_width=“0dp” android:layout_height=“match_parent” android:layout_weight=“3” android:gravity=“center”

android:text=“加入购物车”

android:textColor=“@color/black” android:textSize=“15sp” />

</LinearLayout>

步骤三,在商场页面的Java代码中,先利用下面代码获取布局文件item_goods.xml的根视图:

再从根视图中依据控件ID分别取出网格单元的各控件对象:

然后就能按照寻常方式操纵这些控件对象了,下面便是给网格布局加载商品的代码例子:

(完整代码见chapter06\src\main\java\com\example\chapter06\ShoppingChannelActivity.java)

弄好了商场页面的网格单元,购物车页面的商品行也可照此办理,不同之处在于购物车页面的商品行使 用线性布局而非网格布局,其余实现过程依然分成上述3个步骤。

.6 小结

本章主要介绍了Android常用的几种数据存储方式,包括共享参数SharedPreferences的键值对存取、数 据库SQLite的关系型数据存取、存储卡的文件读写操作(含文本文件读写和图片文件读写)、App全局 内存的读写,以及为实现全局内存而学习的Application组件的生命周期及其用法。最后设计了一个实战项目“购物车”,通过该项目的编码进一步复习巩固本章几种存储方式的使用。

通过本章的学习,我们应该能够掌握以下4种开发技能:

  1. 学会使用共享参数存取键值对数据。
  2. 学会使用SQLite存取数据库记录。
  3. 学会使用存储卡读写文本文件和图片文件。
  4. 学会应用组件Application的用法。

课后练习题

一、填空题

  1. SharedPreferences采用的存储结构是 的键值对方式。

  2. Android可以直接操作的数据库名为 。

  3. 是Android提供的SQLite数据库管理器。

  4. 数据库记录的修改动作由 命令完成。

  5. 为了确保在App运行期间只有唯一的Application实例,可以采取 模式实现。

    二、判断题(正确打√,错误打×)

  6. 共享参数只能保存字符串类型的数据。( )

  7. SQLite可以直接读写布尔类型的数据。( )

  8. 从Android 7.0开始,系统默认禁止App访问公共存储空间。( )

  9. App在私有空间上读写文件无须任何授权。( )

  10. App终止时会调用Application的onTerminate方法。( )

    三、选择题

    1.( )不是持久化的存储方式。

A.共享参数B.数据库C. 文 件 D.全局变量

  1. DDL语言包含哪些数据库操作( )。
    1. 创建表格
    2. 删除表格C.清空表格D.修改表结构
  2. 调用( )方法会返回结果集的Cursor对象。

A.update B.insert C.query D.rawQuery

  1. 位图工厂BitmapFactory的( )方法支持获取图像数据。

A.decodeStream B.decodeFile C.decodeImage D.decodeResource

  1. 已知某个图片文件的存储卡路径,可以调用( )方法将它显示到图像视图上。

A.setImageBitmap B.setImageFile C.setImageURI

D.setImageResource

四、简答题

请简要描述共享参数与数据库两种存储方式的主要区别。

五、动手练习

  1. 请上机实验完善找回密码项目的记住密码功能,分别采用以下两种存储方式:
  2. 使用共享参数记住上次登录成功时输入的用户名和密码。
  3. 使用SQLite数据库记住用户名对应的密码,也就是根据用户名自动填写密码。
  4. 请上机实验本章的购物车项目,要求实现下列功能:
  5. 往购物车添加商品。
  6. 自动计算购物车中所有商品的总金额。
  7. 移除购物车里的某个商品。
  8. 清空购物车。

第7章 内容共享

本章介绍Android不同应用之间共享内容的具体方式,主要包括:如何利用内容组件在应用之间共享数据,如何使用内容组件获取系统的通讯信息,如何借助文件提供器在应用之间共享文件等。

.1 在应用之间共享数据

本节介绍Android 4大组件之一ContentProvider的基本概念和常见用法。首先说明如何使用内容提供器封装内部数据的外部访问接口,接着阐述如何使用内容解析器通过外部接口操作内部数据。

通过ContentProvider封装数据

Android号称提供了4大组件,分别是活动Activity、广播Broadcast、服务Service和内容提供器

ContentProvider。其中内容提供器涵盖与内部数据存取有关的一系列组件,完整的内容组件由内容提供器ContentProvider、内容解析器ContentResolver、内容观察器ContentObserver三部分组成。

ContentProvider给App存取内部数据提供了统一的外部接口,让不同的应用之间得以互相共享数据。像 上一章提到的SQLite可操作应用自身的内部数据库;上传和下载功能可操作后端服务器的文件;而

ContentProvider可操作当前设备其他应用的内部数据,它是一种中间层次的数据存储形式。

在实际编码中,ContentProvider只是服务端App存取数据的抽象类,开发者需要在其基础上实现一个完 整的内容提供器,并重写下列数据库管理方法。

onCreate:创建数据库并获得数据库连接。insert:插入数据。

delete:删除数据。update:更新数据。

query:查询数据,并返回结果集的游标。getType:获取内容提供器支持的数据类型。

这些方法看起来是不是很像SQLite?没错,ContentProvider作为中间接口,本身并不直接保存数据, 而是通过SQLiteOpenHelper与SQLiteDatabase间接操作底层的数据库。所以要想使用

ContentProvider,首先得实现SQLite的数据库帮助器,然后由ContentProvider封装对外的接口。以封装用户信息为例,具体步骤主要分成以下3步。

编写用户信息表的数据库帮助器

这个数据库帮助器就是常规的SQLite操作代码,实现过程参见上一章的“6.2.3 数据库帮助器

SQLiteOpenHelper”,完整代码参见

chapter07\src\main\java\com\example\chapter07\database\UserDBHelper.java。

编写内容提供器的基础字段类

该类需要实现接口BaseColumns,同时加入几个常量定义。详细代码示例如下:

(完整代码见chapter07\src\main\java\com\example\chapter07\provider\UserInfoContent.java)

通过右键菜单创建内容提供器

右击App模块的包名目录,在弹出的右键菜单中依次选择New→Other→Content Provider,打开如图7-

1所示的组件创建对话框。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oRIkT4gu-1676725568133)(media/b10adfdfbcf3cceb1692054633464bf8.jpeg)]

图7-1 内容提供器的组件创建对话框

在创建对话框的Class Name一栏填写内容提供器的名称,比如UserInfoProvider;在URI Authorities一栏填写URI的授权串,比如“com.example.chapter07.provider.UserInfoProvider”;然后单击对话框右 下角的Finish按钮,完成提供器的创建操作。

上述创建过程会自动修改App模块的两处地方,一处是往AndroidManifest.xml添加内容提供器的注册 配置,配置信息示例如下:

另一处是在包名目录下生成名为UserInfoProvider.java的代码文件,打开一看发现该类继承了

ContentProvider,并且提示重写onCreate、insert、delete、query、update、getType等方法,以便 对数据进行增删改查等操作。这个提供器代码显然只有一个框架,还需补充详细的实现代码,为此重写

onCreate方法,在此获取用户信息表的数据库帮助器实例,其他insert、delete、query等方法也要加入 对应的数据库操作代码,修改之后的内容提供器代码如下所示:

(完整代码见chapter07\src\main\java\com\example\chapter07\provider\UserInfoProvider.java)

static { // 往Uri匹配器中添加指定的数据路径

uriMatcher.addURI(UserInfoContent.AUTHORITIES, “/user”, USER_INFO);

}

// 创建ContentProvider时调用,可在此获取具体的数据库帮助器实例

@Override

public boolean onCreate() {

userDB = UserDBHelper.getInstance(getContext(), 1); return true;

}

// 插入数据

@Override

public Uri insert(Uri uri, ContentValues values) {

if (uriMatcher.match(uri) == USER_INFO) { // 匹配到了用户信息表

// 获取SQLite数据库的写连接

SQLiteDatabase db = userDB.getWritableDatabase();

// 向指定的表插入数据,返回记录的行号

long rowId = db.insert(UserInfoContent.TABLE_NAME, null, values); if (rowId > 0) { // 判断插入是否执行成功

// 如果添加成功,就利用新记录的行号生成新的地址

Uri newUri = ContentUris.withAppendedId(UserInfoContent.CONTENT_URI, rowId);

// 通知监听器,数据已经改变

getContext().getContentResolver().notifyChange(newUri, null);

}

db.close(); // 关闭SQLite数据库连接

}

return uri;

}

// 根据指定条件删除数据

@Override

public int delete(Uri uri, String selection, String[] selectionArgs) { int count = 0;

if (uriMatcher.match(uri) == USER_INFO) { // 匹配到了用户信息表

// 获取SQLite数据库的写连接

SQLiteDatabase db = userDB.getWritableDatabase();

// 执行SQLite的删除操作,并返回删除记录的数目

count = db.delete(UserInfoContent.TABLE_NAME, selection, selectionArgs);

db.close(); // 关闭SQLite数据库连接

}

return count;

}

// 根据指定条件查询数据库

@Override

public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {

Cursor cursor = null;

if (uriMatcher.match(uri) == USER_INFO) { // 匹配到了用户信息表

// 获取SQLite数据库的读连接

SQLiteDatabase db = userDB.getReadableDatabase();

// 执行SQLite的查询操作

cursor = db.query(UserInfoContent.TABLE_NAME, projection, selection, selectionArgs, null, null,

sortOrder);

经过以上3个步骤之后,便完成了服务端App的接口封装工作,接下来再由其他App去访问服务端App的数据。

通过ContentResolver访问数据

上一小节提到了利用ContentProvider封装服务端App的数据,如果客户端App想访问对方的内部数据, 就要借助内容解析器ContentResolver。内容解析器是客户端App操作服务端数据的工具,与之对应的内 容提供器则是服务端的数据接口。在活动代码中调用getContentResolver方法,即可获取内容解析器的 实例。

ContentResolver提供的方法与ContentProvider一一对应,比如insert、delete、query、update、

getType等,甚至连方法的参数类型都雷同。以添加操作为例,针对前面UserInfoProvider提供的数据 接口,下面由内容解析器调用insert方法,使之往内容提供器插入一条用户信息,记录添加代码如下所示:

(完整代码见chapter07\src\main\java\com\example\chapter07\ContentWriteActivity.java)

至于删除操作就更简单了,只要下面一行代码就删除了所有记录:

查询操作稍微复杂一些,调用query方法会返回游标对象,这个游标正是SQLite的游标Cursor,详细用法参见上一章的“6.2.3 数据库帮助器SQLiteOpenHelper”。query方法的输入参数有好几个,具体说明如下(依参数顺序排列)。

uri:Uri 类 型 , 指 定 本 次 操 作 的 数 据 表 路 径 。 projection:字符串数组类型,指定将要查询的字段名称列表。selection: 字 符 串 类 型 , 指 定 查 询 条 件 。 selectionArgs:字符串数组类型,指定查询条件中的参数取值列表。sortOrder:字符串类型,指定排序条件。

下面是调用query方法从内容提供器查询所有用户信息的代码例子:

(完整代码见chapter07\src\main\java\com\example\chapter07\ContentReadActivity.java)

// 显示所有的用户记录

private void showAllUser() {

List<UserInfo> userList = new ArrayList<UserInfo>();

// 通过内容解析器从指定Uri中获取用户记录的游标

Cursor cursor = getContentResolver().query(UserInfoContent.CONTENT_URI, null, null, null, null);

// 循环取出游标指向的每条用户记录

while (cursor.moveToNext()) { UserInfo user = new UserInfo(); user.name =

cursor.getString(cursor.getColumnIndex(UserInfoContent.USER_NAME)); user.age =

cursor.getInt(cursor.getColumnIndex(UserInfoContent.USER_AGE)); user.height =

cursor.getInt(cursor.getColumnIndex(UserInfoContent.USER_HEIGHT)); user.weight =

cursor.getFloat(cursor.getColumnIndex(UserInfoContent.USER_WEIGHT)); userList.add(user); // 添加到用户信息列表

}

cursor.close(); // 关闭数据库游标

String contactCount = String.format(“当前共找到%d个用户”, userList.size()); tv_desc.setText(contactCount);

ll_list.removeAllViews(); // 移除线性布局下面的所有下级视图

for (UserInfo user : userList) { // 遍历用户信息列表

String contactDesc = String.format(“姓名为%s,年龄为%d,身高为%d,体重为%f\n”,

user.name, user.age, user.height,

user.weight);

TextView tv_contact = new TextView(this); // 创建一个文本视图tv_contact.setText(contactDesc); tv_contact.setTextColor(Color.BLACK); tv_contact.setTextSize(17);

int pad = Utils.dip2px(this, 5);

tv_contact.setPadding(pad, pad, pad, pad); // 设置文本视图的内部间距

ll_list.addView(tv_contact); // 把文本视图添加至线性布局

}

}

接下来分别演示通过内容解析器添加和查询用户信息的过程,其中记录添加页面为ContentWriteActivity.java,记录查询页面为ContentReadActivity.java。运行测试App,先打开记录添 加页面,输入用户信息后点击添加按钮,由内容解析器执行插入操作,此时添加界面如图7-2所示。接着 打开记录查询页面,内容解析器自动执行查询操作,并将查到的用户信息一一显示出来,此时查询界面 如图7-3所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EvfdAsyz-1676725568134)(media/9dbd87d7c2acb54ec1a07cf4a2a26a86.jpeg)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lsSel1Es-1676725568136)(media/0f3757c4fee354ec1ca845d65c5c11d5.jpeg)]图7-2 通过内容解析器添加用户信息

图7-3 通过内容解析器查询用户信息对比添加页面和查询页面的用户信息,可知成功查到了新增的用户记录。

.2 使用内容组件获取通讯信息

本节介绍了使用内容组件获取通讯信息的操作办法,包括:如何在App运行的时候动态申请权限(访问 通讯信息要求获得相应授权),如何利用内容解析器读写联系人信息,如何利用内容观察器监听收到的 短信内容等。

运行时动态申请权限

上一章的“6.3.1 公共存储空间与私有存储空间”提到,App若想访问存储卡的公共空间,就要在

AndroidManifest.xml里面添加下述的权限配置。

然而即使App声明了完整的存储卡操作权限,从Android 7.0开始,系统仍然默认禁止该App访问公共空间,必须到设置界面手动开启应用的存储卡权限才行。尽管此举是为用户隐私着想,可是人家咋知道要 手工开权限呢?就算用户知道,去设置界面找到权限开关也颇费周折。为此Android支持在Java代码中处理权限,处理过程分为3个步骤,详述如下:

检查App是否开启了指定权限

权限检查需要调用ContextCompat的checkSelfPermission方法,该方法的第一个参数为活动实例,第二个参数为待检查的权限名称,例如存储卡的写权限名为

Manifest.permission.WRITE_EXTERNAL_STORAGE。注意checkSelfPermission方法的返回值,当它为

PackageManager.PERMISSION_GRANTED时表示已经授权,否则就是未获授权。

请求系统弹窗,以便用户选择是否开启权限

一旦发现某个权限尚未开启,就得弹窗提示用户手工开启,这个弹窗不是开发者自己写的提醒对话框, 而是系统专门用于权限申请的对话框。调用ActivityCompat的requestPermissions方法,即可命令系统自动弹出权限申请窗口,该方法的第一个参数为活动实例,第二个参数为待申请的权限名称数组,第三 个参数为本次操作的请求代码。

判断用户的权限选择结果

然而上面第二步的requestPermissions方法没有返回值,那怎么判断用户到底选了开启权限还是拒绝权 限呢?其实活动页面提供了权限选择的回调方法onRequestPermissionsResult,如果当前页面请求弹出 权限申请窗口,那么该页面的Java代码必须重写onRequestPermissionsResult方法,并在该方法内部处理用户的权限选择结果。

具体到编码实现上,前两步的权限校验和请求弹窗可以合并到一块,先调用checkSelfPermission方法检 查某个权限是否已经开启,如果没有开启再调用requestPermissions方法请求系统弹窗。合并之后的检 查方法代码示例如下,此处代码支持一次检查一个权限,也支持一次检查多个权限:

(完整代码见chapter07\src\main\java\com\example\chapter07\util\PermissionUtil.java)

// 检查某个权限。返回true表示已启用该权限,返回false表示未启用该权限

public static boolean checkPermission(Activity act, String permission, int requestCode) {

return checkPermission(act, new String[]{permission}, requestCode);

}

// 检查多个权限。返回true表示已完全启用权限,返回false表示未完全启用权限

public static boolean checkPermission(Activity act, String[] permissions, int requestCode) {

boolean result = true;

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {

int check = PackageManager.PERMISSION_GRANTED;

// 通过权限数组检查是否都开启了这些权限

for (String permission : permissions) {

check = ContextCompat.checkSelfPermission(act, permission); if (check != PackageManager.PERMISSION_GRANTED) {

break; // 有个权限没有开启,就跳出循环

}

}

if (check != PackageManager.PERMISSION_GRANTED) {

// 未开启该权限,则请求系统弹窗,好让用户选择是否立即开启权限ActivityCompat.requestPermissions(act, permissions, requestCode); result = false;

}

}

return result;

}

注意到上面代码有判断安卓版本号,只有系统版本大于Android 6.0(版本代号为M),才执行后续的权限校验操作。这是因为从Android 6.0开始引入了运行时权限机制,在Android 6.0之前,只要App在

AndroidManifest.xml中添加了权限配置,则系统会自动给App开启相关权限;但在Android 6.0之后, 即便事先添加了权限配置,系统也不会自动开启权限,而要开发者在App运行时判断权限的开关情况, 再据此动态申请未获授权的权限。

回到活动页面代码,一方面增加权限校验入口,比如点击某个按钮后触发权限检查操作,其中

Manifest.permission.WRITE_EXTERNAL_STORAGE表示存储卡权限,入口代码如下:

(完整代码见chapter07\src\main\java\com\example\chapter07\MainActivity.java)

另一方面还要重写活动的onRequestPermissionsResult方法,在方法内部校验用户的选择结果,若用户 同意授权,就执行后续业务;若用户拒绝授权,只能提示用户无法开展后续业务了。重写后的方法代码 如下所示:

以上代码为了简化逻辑,将结果校验操作封装为PermissionUtil的checkGrant方法,该方法遍历授权结 果数组,依次检查每个权限是否都得到授权了。详细的方法代码如下所示:

代码都改好后,运行测试App,由于一开始App默认未开启存储卡权限,因此点击按钮btn_file_write触 发了权限校验操作,弹出如图7-4所示的存储卡权限申请窗口。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zSzCsvoS-1676725568137)(media/932f738ed1307447a977e519eb09bb48.jpeg)]

图7-4 App运行时弹出权限申请窗口

点击弹窗上的“始终允许”,表示同意赋予存储卡读写权限,然后系统自动给App开启了存储卡权限,并执行后续处理逻辑,也就是跳到了FileWriteActivity页面,在该页面即可访问公共空间的文件了。但在Android 10系统中,即使授权通过,App仍然无法访问公共空间,这是因为Android 10默认开启沙箱模式,不允许直接使用公共空间的文件路径,此时要修改AndroidManifest.xml,给application节点添加 如下的requestLegacyExternalStorage属性:

从Android 11开始,为了让应用升级时也能正常访问公共空间,还得修改AndroidManifest.xml,给

application节点添加如下的preserveLegacyExternalStorage属性,表示暂时关闭沙箱模式:

除了存储卡的读写权限,还有部分权限也要求运行时动态申请,这些权限名称的取值说明见表7-1。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z1Bt3Co9-1676725568139)(media/e3a2f3f1130c437fdfd8118983a8aab3.jpeg)]表7-1 权限名称的取值说明

利用ContentResolver读写联系人

在实际开发中,普通App很少会开放数据接口给其他应用访问,作为服务端接口的ContentProvider基本 用不到。内容组件能够派上用场的情况,往往是App想要访问系统应用的通讯数据,比如查看联系人、 短信、通话记录,以及对这些通讯数据进行增、删、改、查。

访问系统的通讯数据之前,得先在AndroidManifest.xml添加相应的权限配置,常见的通讯权限配置主 要有下面几个:

当然,从Android 6.0开始,上述的通讯权限默认是关闭的,必须在运行App的时候动态申请相关权限, 详细的权限申请过程参见上一小节的“7.2.1 运行时动态申请权限”。

尽管系统允许App通过内容解析器修改联系人列表,但操作过程比较烦琐,因为一个联系人可能有多个 电话号码,还可能有多个邮箱,所以系统通讯录将其设计为3张表,分别是联系人基本信息表、联系号码 表、联系邮箱表,于是每添加一位联系人,就要调用至少三次insert方法。下面是往手机通讯录添加联 系人信息的代码例子:

(完整代码见chapter07\src\main\java\com\example\chapter07\util\CommunicationUtil.java)

// 往手机通讯录添加一个联系人信息(包括姓名、电话号码、电子邮箱)

public static void addContacts(ContentResolver resolver, Contact contact) {

// 构建一个指向系统联系人提供器的Uri对象

Uri raw_uri = Uri.parse(“content://com.android.contacts/raw_contacts”); ContentValues values = new ContentValues(); // 创建新的配对

// 往 raw_contacts 添加联系人记录,并获取添加后的联系人编号

long contactId = ContentUris.parseId(resolver.insert(raw_uri, values));

// 构建一个指向系统联系人数据的Uri对象

Uri uri = Uri.parse(“content://com.android.contacts/data”); ContentValues name = new ContentValues(); // 创建新的配对name.put(“raw_contact_id”, contactId); // 往配对添加联系人编号

// 往配对添加“姓名”的数据类型

name.put(“mimetype”, “vnd.android.cursor.item/name”); name.put(“data2”, contact.name); // 往配对添加联系人的姓名resolver.insert(uri, name); // 往提供器添加联系人的姓名记录ContentValues phone = new ContentValues(); // 创建新的配对phone.put(“raw_contact_id”, contactId); // 往配对添加联系人编号

// 往配对添加“电话号码”的数据类型

phone.put(“mimetype”, “vnd.android.cursor.item/phone_v2”); phone.put(“data1”, contact.phone); // 往配对添加联系人的电话号码phone.put(“data2”, “2”); // 联系类型。1表示家庭,2表示工作resolver.insert(uri, phone); // 往提供器添加联系人的号码记录ContentValues email = new ContentValues(); // 创建新的配对email.put(“raw_contact_id”, contactId); // 往配对添加联系人编号

// 往配对添加“电子邮箱”的数据类型

email.put(“mimetype”, “vnd.android.cursor.item/email_v2”); email.put(“data1”, contact.email); // 往配对添加联系人的电子邮箱

同理,联系人读取代码也分成3个步骤,先查出联系人的基本信息,再依次查询联系人号码和联系人邮 箱,详细代码参见CommunicationUtil.java的readAllContacts方法。

接下来演示联系人信息的访问过程,分别创建联系人的添加页面和查询页面,其中添加页面的完整代码 见chapter07\src\main\java\com\example\chapter07\ContactAddActivity.java,查询页面的完整代码 见chapter07\src\main\java\com\example\chapter07\ContactReadActivity.java。首先在添加页面输 入联系人信息,点击添加按钮调用addContacts方法写入联系人数据,此时添加界面如图7-5所示。然后打开联系人查询页面,App自动调用readAllContacts方法查出所有的联系人,并显示联系人列表如图7-

6所示,可见刚才添加的联系人已经成功写入系统的联系人列表,而且也能正确读取最新的联系人信息。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YXT9h40d-1676725568140)(media/804ceb29151ff4608eb287e5ee7b64fc.jpeg)]

图7-5 联系人的添加界面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9r2w0lXS-1676725568141)(media/ac14153248217689815e8dece3fbbe60.jpeg)]

图7-6 联系人的查询界面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IRFusLon-1676725568142)(media/373436dd7871e0ba5f6a79bb42affcc6.jpeg)]raw_contacts 表:

data表:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1ivbVwLW-1676725568143)(media/22086a17b1ba67bca5eb65d0c238b811.jpeg)]记录了用户的通讯录所有数据,包括手机号,显示名称等,但是里面的mimetype_id表示不同的数据类型,这与表mimetypes表中的id相对应,raw_contact_id 与下面的 raw_contacts表中的 id 相对应。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pc4ou9ew-1676725568143)(media/6bd773872ceea57c94d937fbf7951486.jpeg)]mimetypes表:
  1. 利用ContentObserver监听短信

ContentResolver获取数据采用的是主动查询方式,有查询就有数据,没查询就没数据。然而有时不但要获取以往的数据,还要实时获取新增的数据,最常见的业务场景是短信验证码。电商App经常在用户 注册或付款时发送验证码短信,为了替用户省事,App通常会监控手机刚收到的短信验证码,并自动填 写验证码输入框。这时就用到了内容观察器ContentObserver,事先给目标内容注册一个观察器,目标内容的数据一旦发生变化,就马上触发观察器的监听事件,从而执行开发者预先定义的代码。

内容观察器的用法与内容提供器类似,也要从ContentObserver派生一个新的观察器,然后通过ContentResolver对象调用相应的方法注册或注销观察器。下面是内容解析器与内容观察器之间的交互 方法说明。

registerContentObserver:内容解析器要注册内容观察器。unregisterContentObserver: 内 容 解 析 器 要 注 销 内 容 观 察 器 。 notifyChange:通知内容观察器发生了数据变化,此时会触发观察器的onChange方法。

notifyChange的调用时机参见“7.1.1 通过ContentProvider封装数据”的insert代码。

为了让读者更好理解,下面举一个实际应用的例子。手机号码的每月流量限额由移动运营商指定,以中 国移动为例,只要将流量校准短信发给运营商客服号码(如发送18到10086),运营商就会回复用户本月的流量数据,包括月流量额度、已使用流量、未使用流量等信息。手机App只需监控10086发来的短信内容,即可自动获取当前号码的流量详情。

下面是利用内容观察器实现流量校准的关键代码片段:

(完整代码见chapter07\src\main\java\com\example\chapter07\MonitorSmsActivity.java)

initSmsObserver();

}

@Override

public void onClick(View v) {

if (v.getId() == R.id.btn_check_flow) {

//查询数据流量,移动号码的查询方式为发送短信内容“18”给“10086”

//电信和联通号码的短信查询方式请咨询当地运营商客服热线

//跳到系统的短信发送页面,由用户手工发短信

//sendSmsManual(“10086”, “18”);

//无需用户操作,自动发送短信sendSmsAuto(“10086”, “18”);

} else if (v.getId() == R.id.tv_check_flow) { AlertDialog.Builder builder = new AlertDialog.Builder(this);

builder.setTitle(“收到流量校准短信”);

builder.setMessage(mCheckResult); builder.setPositiveButton(“确定”, null); builder.create().show();

}

}

// 跳到系统的短信发送页面,由用户手工编辑与发送短信

public void sendSmsManual(String phoneNumber, String message) {

Intent intent = new Intent(Intent.ACTION_SENDTO, Uri.parse(“smsto:” + phoneNumber));

intent.putExtra(“sms_body”, message); startActivity(intent);

}

// 短信发送事件

private String SENT_SMS_ACTION = “com.example.storage.SENT_SMS_ACTION”;

// 短信接收事件

private String DELIVERED_SMS_ACTION = “com.example.storage.DELIVERED_SMS_ACTION”;

// 无需用户操作,由App自动发送短信

public void sendSmsAuto(String phoneNumber, String message) {

// 以下指定短信发送事件的详细信息

Intent sentIntent = new Intent(SENT_SMS_ACTION); sentIntent.putExtra(“phone”, phoneNumber); sentIntent.putExtra(“message”, message);

PendingIntent sentPI = PendingIntent.getBroadcast(this, 0, sentIntent, PendingIntent.FLAG_UPDATE_CURRENT);

// 以下指定短信接收事件的详细信息

Intent deliverIntent = new Intent(DELIVERED_SMS_ACTION); deliverIntent.putExtra(“phone”, phoneNumber); deliverIntent.putExtra(“message”, message);

PendingIntent deliverPI = PendingIntent.getBroadcast(this, 1, deliverIntent, PendingIntent.FLAG_UPDATE_CURRENT);

// 获取默认的短信管理器

SmsManager smsManager = SmsManager.getDefault();

// 开始发送短信内容。要确保打开发送短信的完全权限,不是那种还需提示的不完整权限

smsManager.sendTextMessage(phoneNumber, null, message, sentPI, deliverPI);

}

private Handler mHandler = new Handler(Looper.myLooper()); // 声明一个处理器对象

private SmsGetObserver mObserver; // 声明一个短信获取的观察器对象

private static Uri mSmsUri; // 声明一个系统短信提供器的Uri对象

private static String[] mSmsColumn; // 声明一个短信记录的字段数组

// 初始化短信观察器

private void initSmsObserver() {

//mSmsUri = Uri.parse(“content://sms/inbox”);

//Android5.0之后似乎无法单独观察某个信箱,只能监控整个短信mSmsUri = Uri.parse(“content://sms”); // 短信数据的提供器路径

mSmsColumn = new String[]{“address”, “body”, “date”}; // 短信记录的字段数组

// 创建一个短信观察器对象

mObserver = new SmsGetObserver(this, mHandler);

// 给指定Uri注册内容观察器,一旦发生数据变化,就触发观察器的onChange方法

getContentResolver().registerContentObserver(mSmsUri, true, mObserver);

}

// 在页面销毁时触发

protected void onDestroy() { super.onDestroy();

getContentResolver().unregisterContentObserver(mObserver); // 注销内容观察

}

// 定义一个短信获取的观察器

private static class SmsGetObserver extends ContentObserver { private Context mContext; // 声明一个上下文对象

public SmsGetObserver(Context context, Handler handler) { super(handler);

mContext = context;

}

的短信

// 观察到短信的内容提供器发生变化时触发

public void onChange(boolean selfChange) { String sender = “”, content = “”;

// 构建一个查询短信的条件语句,移动号码要查找10086发来的短信

String selection = String.format(“address=‘10086’ and date>%d”, System.currentTimeMillis() - 1000 * 60 * 1); // 查找最近一分钟

// 通过内容解析器获取符合条件的结果集游标

Cursor cursor = mContext.getContentResolver().query( mSmsUri, mSmsColumn, selection, null, " date desc");

// 循环取出游标所指向的所有短信记录

while (cursor.moveToNext()) {

sender = cursor.getString(0); // 短信的发送号码content = cursor.getString(1); // 短信内容Log.d(TAG, “sender=”+sender+“, content=”+content); break;

content);

}

cursor.close(); // 关闭数据库游标

mCheckResult = String.format(“发送号码:%s\n短信内容:%s”, sender,

// 依次解析流量校准短信里面的各项流量数值,并拼接流量校准的结果字符串

String flow = String.format(“流量校准结果如下:总流量为:%s;已使用:%s” +

“;剩余流量:%s”, findFlow(content, “总流量为”), findFlow(content, “已使用”), findFlow(content, “剩余”));

if (tv_check_flow != null) { // 离开该页面后就不再显示流量信息

tv_check_flow.setText(flow); // 在文本视图显示流量校准结果

}

super.onChange(selfChange);

运行测试App,点击校准按钮发送流量校准短信,接着收到如图7-7所示的短信内容。同时App监听刚收到的流量短信,从中解析得到当前的流量数值,并展示在界面上如图7-8所示。可见通过内容观察器实时 获取了最新的短信记录。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KstO0z98-1676725568144)(media/132c57e3b754b218cf41166a84b681d6.jpeg)]

图7-7 用户收到的短信内容

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DhXZFcuK-1676725568145)(media/9ae02039f6d0ec38b397ce696c4fa39c.jpeg)]

图7-8 内容观察器监听短信并解析出流量信息总结一下系统开放给普通应用访问的常用URI,详细的URI取值说明见表7-2。

表7-2 常用的系统URI取值说明

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Wi0tZM09-1676725568145)(media/864ec918322b1fc9abc82b8a7c1ea5de.jpeg)]

.3 在应用之间共享文件

本节介绍了Android在应用间共享文件的几种方式,包括:如何使用系统相册发送带图片的彩信,如何从相册媒体库获取图片并借助FileProvider发送彩信,如何在媒体库中查找APK文件并借助FileProvider 安装应用。

使用相册图片发送彩信

不同应用之间可以共享数据,当然也能共享文件,比如系统相册保存着用户拍摄的照片,这些照片理应 分享给其他App使用。举个例子,短信只能发送文本,而彩信允许同时发送文本和图片,彩信的附件图 片就来自系统相册。现在准备到系统相册挑选照片,测试页面的Java代码先增加以下两行代码,分别声明一个路径对象和选择照片的请求码:

接着在选取按钮的点击方法中加入下面代码,表示打开系统相册选择照片:

(完整代码见chapter07\src\main\java\com\example\chapter07\SendMmsActivity.java)

上面的跳转代码期望接收照片选择结果,于是重写当前活动的onActivityResult方法,调用返回意图的

getData方法获得选中照片的路径对象,重写后的方法代码如下所示:

这下拿到了相册照片的路径对象,既能把它显示到图像视图,也能将它作为图片附件发送彩信了。由于 普通应用无法自行发送彩信,必须打开系统的信息应用才行,于是编写页面跳转代码,往意图对象塞入 详细的彩信数据,包括彩信发送的目标号码、标题、内容,以及Uri类型的图片附件。详细的跳转代码示 例如下:

运行测试App,刚打开的活动页面如图7-9所示,在各行编辑框中依次填写彩信的目标号码、标题、内容,再到系统相册选取照片,填好的界面效果如图7-10所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Su78sBnb-1676725568146)(media/9ed9da811a273fc6fed4e326bf20df76.jpeg)]

图7-9 初始的彩信发送界面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qNatjhaQ-1676725568147)(media/92fba77f10b10dc0001ef95edbbebdf4.jpeg)]

图7-10 填好的彩信发送界面之后点击发送按钮,屏幕下方弹出如图7-11所示的应用选择窗口。

先点击信息图标,表示希望跳到信息应用,再点击“仅此一次”按钮,此时打开信息应用界面如图7-12所示。可见信息发送界面已经自动填充收件人号码、信息标题和内容,以及图片附件,只待用户轻点右下 角的飞鸽传书图标,就能将彩信发出去了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a3VExUAm-1676725568147)(media/37a43504102e57b58aa5a0dcff06a58e.jpeg)]

图7-11 选择使用哪个应用发送彩信

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OwEP8r4u-1676725568147)(media/ff802cb2f431698b19584dda546d5346.jpeg)]

图7-12 信息应用的待发送彩信

借助FileProvider发送彩信

通过系统相册固然可以获得照片的路径对象,却无法知晓更多的详细信息,例如照片名称、文件大小、 文件路径等信息,也就无法进行个性化的定制开发。为了把更多的文件信息开放出来,Android设计了专门的媒体共享库,允许开发者通过内容组件从中获取更详细的媒体信息。

图片所在的相册媒体库路径为MediaStore.Images.Media.EXTERNAL_CONTENT_URI,通过内容解析器 即可从媒体库依次遍历得到图片列表详情。为便于代码管理,首先要声明如下的对象变量:

(完整的ImageInfo代码见

chapter07\src\main\java\com\example\chapter07\bean\ImageInfo.java)

然后使用内容解析器查询媒体库的图片信息,简单起见只挑选文件大小最小的前6张图片,图片列表加载 代码示例如下:

(完整代码见chapter07\src\main\java\com\example\chapter07\ProviderMmsActivity.java)

注意到以上代码获得了字符串格式的文件路径,而彩信发送应用却要求Uri类型的路径对象,原本可以通 过代码“Uri.parse(path)”将字符串转换为Uri对象,但是从Android 7.0开始,系统不允许其他应用直接访问老格式的路径,必须使用文件提供器FileProvider才能获取合法的Uri路径,相当于A应用申明了共享某 个文件,然后B应用方可访问该文件。为此需要重头配置FileProvider,详细的配置步骤说明如下。

首先在res目录新建xml文件夹,并在该文件夹中创建file_paths.xml,再往XML文件填入以下内容,表示 定义几个外部文件目录:

接着打开AndroidManifest.xml,在application节点内部添加下面的provider标签,表示声明当前应用的内容提供器组件,添加后的标签配置示例如下:

上面的provider有两处地方允许修改,一处是authorities属性,它规定了授权字符串,这是每个提供器 的唯一标识;另一处是元数据的resource属性,它指明了文件提供器的路径资源,也就是刚才定义的

file_paths.xml。

回到活动页面的源码,在发送彩信之前添加下述代码,目的是根据字符串路径构建Uri对象,注意针对

Android 7.0以上的兼容处理。

(完整代码见ProviderMmsActivity.java的sendMms方法)

由以上代码可见,Android 7.0开始调用FileProvider的getUriForFile方法获得Uri对象,该方法的第二个参数为文件提供器的授权字符串,第三个参数为File类型的文件对象。

运行测试App,页面会自动加载媒体库的前6张图片,另外手工输入对方号码、彩信标题、彩信内容等信息,填好的发送界面如图7-13所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nLZhN3XU-1676725568148)(media/1d1bf1540ccbe5d121ace97904cf8abe.jpeg)]

图7-13 填好信息的彩信发送界面

点击页面下方的某张图片,表示选中该图片作为彩信附件,此时界面下方弹出如图7-14所示的应用选择 窗口。选中信息图标再点击“仅此一次”按钮,即可跳到如图7-15所示的系统信息发送页面了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i0U9a9fV-1676725568148)(media/37a43504102e57b58aa5a0dcff06a58e.jpeg)]

图7-14 选择使用哪个应用发送彩信

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NE38UofO-1676725568149)(media/b152e3b93962852369dc3a7164692808.jpeg)]

图7-15 信息应用的待发送彩信

借助FileProvider安装应用

除了发送彩信需要文件提供器,安装应用也需要FileProvider。不单单彩信的附件图片能到媒体库中查询,应用的APK安装包也可在媒体库找到。查找安装包依然借助于内容解析器,具体的实现过程和查询图片类似,比如事先声明如下的对象变量:

(完整的ApkInfo代码见chapter07\src\main\java\com\example\chapter07\bean\ApkInfo.java)

再通过内容解析器到媒体库查找安装包列表,具体的加载代码示例如下:

(完整代码见chapter07\src\main\java\com\example\chapter07\ProviderApkActivity.java)

找到安装包之后,通常还要获取它的包名、版本名称、版本号等信息,此时可调用应用包管理器的

getPackageArchiveInfo方法,从安装包文件中提取PackageInfo包信息。包信息对象的packageName 属性值为应用包名,versionName属性值为版本名称,versionCode属性值为版本号。下面是利用弹窗 展示包信息的代码例子:

有了安装包的文件路径之后,就能打开系统自带的安装程序执行安装操作了,此时一样要把安装包的Uri 对象传过去。应用安装的详细调用代码如下所示:

注意,从Android 8.0开始,安装应用需要申请权限REQUEST_INSTALL_PACKAGES,于是打开AndroidManifest.xml,补充下面的权限申请配置:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OwKTntV3-1676725568149)(media/6e8a7f5fec494b284ba958a66380e4ac.jpeg)]这下大功告成,编译运行App,打开测试页面自动加载安装包列表的界面如图7-16所示。点击某项安装包,弹出如图7-17所示的确认对话框。

图7-16 安装包列表的发现界面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0p4Q7IQZ-1676725568150)(media/ea036e1d0455e5681224a733ccdc0403.jpeg)]

图7-17 安装应用的提示对话框

点击确认对话框的“是”按钮,便跳到了如图7-18所示的应用安装界面,点击“允许”按钮之后,剩下的安装操作就交给系统程序了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YGO5Xhnq-1676725568150)(media/09910760d8777422b1ce574f434c65b2.jpeg)]

图7-18 跳转到系统的应用安装界面

.4 小结

本章主要介绍内容组件—ContentProvider的常见用法,包括:在应用之间共享数据(通过

ContentProvider封装数据、通过ContentResolver访问数据)、使用内容组件获取通讯信息(运行时动 态申请权限、利用ContentResolver读写联系人、利用ContentObserver监听短信)、在应用之间共享 文件(使用相册照片发送彩信、借助FileProvider发送彩信、借助FileProvider安装应用)。

通过本章的学习,我们应该能掌握以下4种开发技能:

  1. 学会利用ContentProvider在应用之间共享数据。
  2. 学会在App运行过程中动态申请权限。
  3. 学会使用内容组件获取系统的通讯信息。
  4. 学会利用FileProvider在应用之间共享文件。

.5 课后练习题

一、填空题
  1. 在AndroidManifest.xml里面声明内容提供器的标签名称是。
  2. 在活动代码中调用getContentResolver方法,得到的是 实例。
  3. Manifest.permission.READ_CONTACTS表示权限。
  4. MediaStore.Images.Media.DATA保存了媒体库中图片文件的。
二、判断题(正确打√,错误打×)
  1. ContentProvider属于中间接口,本身并不直接保存数据。( )
  2. 内容解析器ContentResolver是客户端App操作服务端数据的工具。( )
  3. 只要调用ContentResolver的一次insert方法,就能向通讯录写入一条联系人数据。( )
  4. 内容观察器ContentObserver能够实时获取新增的数据。( )
  5. 短信和彩信都只能发送文本内容。( )
三、选择题
  1. 内容组件由哪3个部分组成?( ) A.ContentProvider B.ContentObserver

C.FileProvider D.ContentResolver

  1. App读取短信需要申请( )权限。

A.SEND_SMS B.RECEIVE_SMS C.READ_SMS D.READ_CONTACTS

  1. content://mms是( )的内容路径。

A.彩信B.短信C.飞信D.微信

  1. FileProvider的getUriForFile方法返回的数据是( )类型。

A.File B.String C.Uri D.URL

  1. 安卓App安装包的文件扩展名是( )。

A.APP B.APK C.EXE D.IPA

四、简答题

请简要描述App运行时申请动态权限的几个步骤。

五、动手练习

请上机实验下列3项练习:

  1. 使用内容解析器读写系统通讯录里的联系人信息。
  2. 使用内容观察器监听运营商客服号码回复的流量短信,并从中获得用户的流量数据。
  3. 利用内容解析器从系统媒体库获得图片列表,并借助文件提供器向目标号码发送彩信。

第8章 高级控件

本章介绍了App开发常用的一些高级控件用法,主要包括:如何使用下拉框及其适配器、如何使用列表 类视图及其适配器、如何使用翻页类视图及其适配器、如何使用碎片及其适配器等。然后结合本章所学 的知识,演示了一个实战项目“记账本”的设计与实现。

.1 下拉列表

本节介绍下拉框的用法以及适配器的基本概念,结合对下拉框Spinner的使用说明分别阐述数组适配器

ArrayAdapter、简单适配器SimpleAdapter的具体用法与展示效果。

下拉框Spinner

Spinner是下拉框控件,它用于从一串列表中选择某项,其功能类似于单选按钮的组合。下拉列表的展示方式有两种,一种是在当前下拉框的正下方弹出列表框,此时要把spinnerMode属性设置为

dropdown,下面是XML文件中采取下拉模式的Spinner标签例子:

另一种是在页面中部弹出列表对话框,此时要把spinnerMode属性设置为dialog,下面是XML文件中采 取对话框模式的Spinner标签例子:

此外,在Java代码中,Spinner还可以调用下列4个方法。

setPrompt:设置标题文字。注意对话框模式才显示标题,下拉模式不显示标题。setAdapter: 设 置 列 表 项 的 数 据 适 配 器 。 setSelection:设置当前选中哪项。注意该方法要在setAdapter方法后调用。setOnItemSelectedListener:设置下拉列表的选择监听器,该监听器要实现接口

OnItemSelectedListener。

下面是初始化下拉框,并设置选择监听器的代码例子:

(完整代码见chapter08\src\main\java\com\example\chapter08\SpinnerDropdownActivity.java)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jIOIQqlg-1676725568151)(media/4f8bbe7ce64984e0a461831650bbbfc8.jpeg)]接下来观察两种下拉列表的界面效果,运行测试App,一开始的下拉框如图8-1所示。

图8-1 下拉框控件的初始界面

在下拉模式页面(SpinnerDropdownActivity.java)单击下拉框,六大行星的列表框在下拉框正下方展 开,如图8-2所示。点击某项后,列表框消失,同时下拉框文字变为刚选中的行星名称。再打开对话框模 式页面(SpinnerDialogActivity),单击下拉框会在页面中央弹出六大行星的列表对话框,如图8-3所 示。点击某项后,对话框消失,同时下拉框文字也变为刚选中的行星名称。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r1QNJJHy-1676725568151)(media/e8c12697c60fd8c5979b6369bdeff1a8.jpeg)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AQE7SkCV-1676725568152)(media/14a8ea7eaa50f749044eff4892eb8cc7.jpeg)]图8-2 下拉模式的列表框

图8-3 对话框模式的列表框

数组适配器ArrayAdapter

上一小节在演示下拉框控件时,调用了setAdapter方法设置列表适配器。这个适配器好比一组数据的加工流水线,你丢给它一大把糖果(六大行星的原始数据),适配器先按顺序排列糖果(对应行星数组starArray),然后拿来制作好的包装盒(对应每个列表项的布局文件item_select.xml),把糖果往里面 一塞,出来的便是一个个精美的糖果盒(界面上排布整齐的列表框)。这个流水线可以做得很复杂,也 可以做得简单一些,最简单的流水线就是之前演示用到的数组适配器ArrayAdapter。

ArrayAdapter主要用于每行列表只展示文本的情况,实现过程分成下列3个步骤: 步骤一,编写列表项的XML文件,内部布局只有一个TextView标签,示例如下:

(完整代码见chapter08\src\main\res\layout\item_select.xml)

步骤二,调用ArrayAdapter的构造方法,填入待展现的字符串数组,以及列表项的包装盒,即XML文件

R.layout.item_select。构造方法的调用代码示例如下。

步骤三,调用下拉框控件的setAdapter方法,传入第二步得到的适配器实例,代码如下:

经过以上3个步骤,先由ArrayAdapter明确原料糖果的分拣过程与包装方式,再由下拉框调用

setAdapter方法发出开工指令,适配器便会把一个个包装好的糖果盒输出到界面。

简单适配器SimpleAdapter

ArrayAdapter只能显示文本列表,显然不够美观,有时还想给列表加上图标,比如希望显示六大行星的天文影像。这时简单适配器SimpleAdapter就派上用场了,它允许在列表项中同时展示文本与图片。

SimpleAdapter的实现过程略微复杂,因为它的原料需要更多信息。例如,原料不但有糖果,还有贺 卡,这样就得把一大袋糖果和一大袋贺卡送进流水线,适配器每次拿一颗糖果和一张贺卡,把糖果与贺 卡按规定塞进包装盒。对于SimpleAdapter的构造方法来说,第2个参数Map容器放的是原料糖果与贺卡,第3个参数放的是包装盒,第4个参数放的是糖果袋与贺卡袋的名称,第5个参数放的是包装盒里塞 糖果的位置与塞贺卡的位置。

下面是下拉框控件使用简单适配器的示例代码:

(完整代码见chapter08\src\main\java\com\example\chapter08\SpinnerIconActivity.java)

以上代码中,简单适配器使用的包装盒名为R.layout.item_simple,它的布局内容如下:

(完整代码见chapter08\src\main\res\layout\item_simple.xml)

运行测试App,一开始的下拉框如图8-4所示,可见默认选项既有图标又有文字。然后单击下拉框,页面中央弹出六大行星的列表对话框,如图8-5所示,可见列表框的各项也一齐展示了行星的图标及其名称。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xZBIcwag-1676725568153)(media/16e364e82b3544dcdc37c5019dca3395.jpeg)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V0nqvR1c-1676725568153)(media/a2675a220cccbbb1fba418b29eced9fa.jpeg)]图8-4 采用简单适配器的初始下拉框

图8-5 采用简单适配器的列表对话框

.2 列表类视图

本节介绍列表类视图怎样结合基本适配器展示视图阵列,包括:基本适配器BaseAdapter的用法、列表视图ListView的用法及其常见问题的解决、网格视图GridView的用法及其拉伸模式说明。

基本适配器BaseAdapter

由上一节的介绍可知,数组适配器适用于纯文本的列表数据,简单适配器适用于带图标的列表数据。然 而实际应用常常有更复杂的列表,比如每个列表项存在3个以上的控件,这种情况即便是简单适配器也很 吃力,而且不易扩展。为此Android提供了一种适应性更强的基本适配器BaseAdapter,该适配器允许开发者在别的代码文件中编写操作代码,大大提高了代码的可读性和可维护性。

从BaseAdapter派生的数据适配器主要实现下面5种方法。

构造方法:指定适配器需要处理的数据集合。getCount: 获 取 列 表 项 的 个 数 。 getItem: 获 取 列 表 项 的 数 据 。 getItemId:获取列表项的编号。

getView:获取每项的展示视图,并对每项的内部控件进行业务处理。

下面以下拉框控件为载体,演示如何操作BaseAdapter,具体的编码过程分为3步:

步骤一,编写列表项的布局文件,示例代码如下:

(完整代码见chapter08\src\main\res\layout\item_list.xml)

步骤二,写个新的适配器继承BaseAdapter,实现对列表项的管理操作,示例代码如下:

(完整代码见

chapter08\src\main\java\com\example\chapter08\adapter\PlanetBaseAdapter.java)

package com.example.chapter08.adapter;

import android.content.Context; import android.view.LayoutInflater; import android.view.View;

import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.ImageView; import android.widget.TextView;

import com.example.chapter08.R;

import com.example.chapter08.bean.Planet;

import java.util.List;

public class PlanetBaseAdapter extends BaseAdapter { private Context mContext; // 声明一个上下文对象

private List<Planet> mPlanetList; // 声明一个行星信息列表

// 行星适配器的构造方法,传入上下文与行星列表

public PlanetBaseAdapter(Context context, List<Planet> planet_list) { mContext = context;

mPlanetList = planet_list;

}

// 获取列表项的个数

public int getCount() { return mPlanetList.size();

}

// 获取列表项的数据

public Object getItem(int arg0) { return mPlanetList.get(arg0);

}

// 获取列表项的编号

public long getItemId(int arg0) { return arg0;

}

// 获取指定位置的列表项视图

public View getView(final int position, View convertView, ViewGroup parent)

{

ViewHolder holder;

if (convertView == null) { // 转换视图为空

holder = new ViewHolder(); // 创建一个新的视图持有者

// 根据布局文件item_list.xml生成转换视图对象

convertView =

LayoutInflater.from(mContext).inflate(R.layout.item_list, null); holder.iv_icon = convertView.findViewById(R.id.iv_icon); holder.tv_name = convertView.findViewById(R.id.tv_name); holder.tv_desc = convertView.findViewById(R.id.tv_desc); convertView.setTag(holder); // 将视图持有者保存到转换视图当中

} else { // 转换视图非空

// 从转换视图中获取之前保存的视图持有者

holder = (ViewHolder) convertView.getTag();

}

步骤三,在页面代码中创建该适配器实例,并交给下拉框设置,示例代码如下:

(完整代码见chapter08\src\main\java\com\example\chapter08\BaseAdapterActivity.java)

运行测试App,一开始的下拉框如图8-6所示,可见默认选项有图标有标题还有内容。然后单击下拉框, 页面中央弹出六大行星的列表对话框,如图8-7所示,可见列表框的各项也一齐展示了行星的图标、名称 及其详细描述。因为对列表项布局item_list.xml使用了单独的适配器代码PlanetBaseAdapter,所以即 使多加几个控件也不怕麻烦了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qlUWM2bZ-1676725568153)(media/38ff91d85e704349a88332e025b66c5f.jpeg)]

图8-6 采用基本适配器的初始下拉框

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rNdrELYO-1676725568154)(media/1a1cef752effdcf51fcdef5b348977a9.jpeg)]

图8-7 采用基本适配器的列表对话框

列表视图ListView

上一小节给下拉框控件设置了基本适配器,然而列表效果只在弹出对话框中展示,一旦选中某项,回到 页面时又只显示选中的内容。这么丰富的列表信息没展示在页面上实在是可惜,也许用户对好几项内容 都感兴趣。若想在页面上直接显示全部列表信息,就要引入新的列表视图ListView。列表视图允许在页面上分行展示相似的数据列表,例如新闻列表、商品列表、图书列表等,方便用户浏览与操作。

ListView同样通过setAdapter方法设置列表项的数据适配器,但操作列表项的时候,它不使用

setOnItemSelectedListener方法,而是调用setOnItemClickListener方法设置列表项的点击监听器

OnItemClickListener,有时也调用setOnItemLongClickListener方法设置列表项的长按监听器

OnItemLongClickListener。在点击列表项或者长按列表项之时,即可触发监听器对应的事件处理方 法。除此之外,列表视图还新增了几个属性与方法,详细说明见表8-1。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nOWnThPR-1676725568155)(media/8cb044875b58f72dad093e64b96070ca.jpeg)]表8-1 列表视图新增的属性与方法说明

在XML文件中添加ListView很简单,只要以下几行就声明了一个列表视图:

往列表视图填充数据也很容易,先利用基本适配器实现列表适配器,再调用setAdapter方法设置适配器对象。下面是使用列表视图在界面上展示行星列表的代码例子:

(完整代码见chapter08\src\main\java\com\example\chapter08\ListViewActivity.java)

其中列表项的点击事件和长按事件的处理方法代码如下所示:

(完整代码见chapter08\src\main\java\com\example\chapter08\adapter\PlanetListAdapter.java)

运行App后打开包含列表视图的测试页面,行星列表的界面效果如图8-8所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VK6eqXRs-1676725568155)(media/82b79eda21b6ffcece2661a72815bf2b.jpeg)]

图8-8 采用基本适配器的列表视图

由图8-8可见,列表视图在各项之间默认展示灰色的分隔线,点击或长按某项时会显示默认的灰色水波背 景。若想修改分隔线样式或按压背景,则需调整ListView的对应属性,调整时候的注意点说明如下:

修改列表视图的分隔线样式

修改分隔线样式要在XML文件中同时设置divider(分隔图片)与dividerHeight(分隔高度)两个属性, 并且遵循下列两条规则:

  1. divider属性设置为@null时,不能再将dividerHeight属性设置为大于0的数值,因为这会导致最后 一项没法完全显示,底部有一部分被掩盖了。原因是列表高度为wrap_content时,系统已按照没有分隔线的情况计算列表高度,此时dividerHeight占用了n-1块空白分隔区域,使得最后一项被挤到背影里面去了。
  2. 通过代码设置的话,务必先调用setDivider方法再调用setDividerHeight方法。如果先调用

setDividerHeight后调用setDivider,分隔线高度就会变成分隔图片的高度,而不是setDividerHeight设置的高度。XML布局文件则不存在divider属性和dividerHeight属性的先后顺序问题。

下面的代码示范了如何在代码中正确设置分隔线,以及如何正确去掉分隔线:

(完整代码见ListViewActivity.java的refreshListView方法)

修改列表项的按压背景

若想取消按压列表项之时默认的水波背景,可在布局文件中设置也可在代码中设置,两种方式的注意点 说明如下:

  1. 在布局文件中取消按压背景的话,直接将listSelector属性设置为@null并不合适,因为尽管设为

    @null,按压列表项时仍出现橙色背景。只有把listSelector属性设置为透明色才算真正取消背景,此时

    listSelector的属性值如下所示(事先在colors.xml中定义好透明色):

  2. 在代码中取消按压背景的话,调用setSelector方法不能设置null值,因为null值会在运行时报空指 针异常。正确的做法是先从资源文件获得透明色的图形对象,再调用setSelector方法设置列表项的按压状态图形,设置按压背景的代码如下所示:

列表视图除了以上两处属性修改,实际开发还有两种用法要特别小心,一种是列表视图的高度问题,另 一种是列表项的点击问题,分别叙述如下。

列表视图的高度问题

在XML文件中,如果ListView后面还有其他平级的控件,就要将ListView的高度设为0dp,同时权重设为

1,确保列表视图扩展到剩余的页面区域;如果ListView的高度设置为wrap_content,系统就只给列表 视图预留一行高度,如此一来只有列表的第一项会显示,其他项不显示,这显然不是我们所期望的。因 此建议列表视图的尺寸参数按照如下方式设置:

列表项的点击问题

通常只要调用setOnItemClickListener方法设置点击监听器,点击列表项即可触发列表项的点击事件, 但是如果列表项中存在编辑框或按钮(含Button、ImageButton、Checkbox等),点击列表项就无法 触发点击事件了。缘由在于编辑框和按钮这类控件会抢占焦点,因为它们要么等待用户输入、要么等待 用户点击,按道理用户点击按钮确实应该触发按钮的点击事件,而非触发列表项的点击事件,可问题是 用户点击列表项的其余区域,也由于焦点被抢占的缘故导致触发不了列表项的点击事件。

为了规避焦点抢占的问题,列表视图允许开发者自行设置内部视图的焦点抢占方式,该方式在XML文件 中由descendantFocusability属性指定,在代码中由setDescendantFocusability方法设置,详细的焦点 抢占方式说明见表8-2。

表8-2 列表视图的焦点抢占方式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ldcuZRJ6-1676725568155)(media/e428049c5d1288dc1e1ce70be27967a3.jpeg)]

注意焦点抢占方式不是由ListView设置,而是由列表项的根布局设置,也就是item_***.xml的根节点。 完整的演示代码见本章源码中的ListFocusActivity.java、PlanetListWithButtonAdapter.java,以及列 表项的布局文件item_list_with_button.xml。自行指定焦点抢占方式的界面效果如图8-9所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8aRwuqS3-1676725568156)(media/1c2d45754e3cf5290679971e52179409.jpeg)]

图8-9 列表项包含按钮控件的列表视图

在图8-9所示的界面上选择方式“不让子控件处理”(FOCUS_BLOCK_DESCENDANTS),之后点击列表项除按钮之外的区域,才会弹出列表项点击事件的提示。

接下来我们不妨改写第6章实战项目的购物车页面,将商品列表改为列表视图实现,从而把列表项的相关 操作剥离到单独的适配器代码,有利于界面代码的合理解耦。改造完毕的购物车效果如图8-10所示

(完整代码见chapter08\src\main\java\com\example\chapter08\ShoppingCartActivity.java)。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OhmpBaen-1676725568156)(media/cbcc67ddb2bd0d3c21cbd8242065627e.jpeg)]

图8-10 利用列表视图改造购物车界面

网格视图GridView

除了列表视图,网格视图GridView也是常见的列表类视图,它用于分行分列显示表格信息,比列表视图更适合展示物品清单。除了沿用列表视图的3个方法setAdapter、setOnItemClickListener、setOnItemLongClickListener,网格视图还新增了部分属性与方法,新属性与新方法的说明见表8-3。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y5j3uLY2-1676725568157)(media/883ea33e4325967580911d7d8146aaed.jpeg)]表8-3 网格视图新增的属性与方法说明

表8-4 网格视图拉伸模式的取值说明

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oub5ZBS8-1676725568161)(media/e92ae29ca64467f1298efc4ba94dcbf1.jpeg)]

在XML文件中添加GridView需要指定列的数目,以及空隙的拉伸模式,示例如下:

网格视图的按压背景与焦点抢占问题类似于列表视图,此外还需注意网格项的拉伸模式,因为同一行的 网格项可能占不满该行空间,多出来的空间就由拉伸模式决定怎么分配。接下来做个实验,看看各种拉 伸模式分别呈现什么样的界面效果。实验之前先给网格视图设置青色背景,通过观察背景的覆盖区域, 即可知晓网格项之间的空隙分布。

下面是演示网格视图拉伸模式的代码片段:

(完整代码见chapter08\src\main\java\com\example\chapter08\GridViewActivity.java)

运行测试App,一开始的行星网格界面如图8-11所示,此时网格视图没有分隔线。点击界面顶部的下拉框,并选择“不拉伸NO_STRETCH”,此时每行的网格项紧挨着,多出来的空隙排在当前行的右边,如图8-12所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6gATNbbi-1676725568161)(media/053a77d78f368a69e7d997514e3f17bb.jpeg)]拉伸模式选择“拉伸列宽(COLUMN_WIDTH)”,此时行星网格界面如图8-13所示,可见每个网格的宽度都变宽了。拉伸模式选择“列间空隙(STRETCH_SPACING)”,此时行星网格界面如图8-14所示,可见 多出来的空隙位于网格项中间。

图8-11 没有分隔线效果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pwoqO6BJ-1676725568161)(media/2cbe87dbe0c47cb7ac59d73f111fd84a.jpeg)]

图8-12 拉伸模式为NO_STRETCH

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mdwdFs3O-1676725568162)(media/64c79445dfa97bde711a5a2248561c5d.jpeg)]

图8-13 拉伸模式为COLUMN_WIDTH

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MSOIl7Jw-1676725568162)(media/f615ecd7ea5ad5d311adc210972e1cf1.jpeg)]

图8-14 拉伸模式为STRETCH_SPACING

拉伸模式选择“左右空隙(SPACING_UNIFORM)”,此时行星网格界面如图8-15所示,可见空隙同时出 现在网格项的左右两边。拉伸模式选择“使用padding显示全部分隔线”,此时行星网格界面如图8-16所示,可见网格视图的内外边界都显示了分隔线。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Zen7MRWX-1676725568163)(media/6664cb76c84cca1a42d1a2d6b978e191.jpeg)]

图8-15 拉伸模式为SPACING_UNIFORM

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AS9ikGtT-1676725568163)(media/8cd4b926bc8233533d1516593fa2b7ba.jpeg)]

图8-16 使用padding显示全部分隔线

接下来继续在实战中运用网格视图,上一节的列表视图已经成功改造了购物车的商品列表,现在使用网 格视图改造商品频道页面,六部手机正好做成三行两列的GridView。采用网格视图改造的商品频道页面效果如图8-17所示(完整代码见chapter08\src\main\java\com\example\chapter08\ShoppingChannelActivity.java)。

.3 翻页类视图

本节介绍翻页类视图的相关用法,包括:翻页视图ViewPager如何搭配翻页适配器PagerAdapter、如何 搭配翻页标签栏PagerTabStrip,最后结合实战演示了如何使用翻页视图实现简单的启动引导页。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tC5ENWG5-1676725568164)(media/ccd934ea6529af97f774665c3b7424d8.jpeg)]

图8-17 使用网格视图改造后的商品频道页面

翻页视图ViewPager

上一节介绍的列表视图与网格视图,一个分行展示,另一个分行又分列,其实都是在垂直方向上下滑 动。有没有一种控件允许页面在水平方向左右滑动,就像翻书、翻报纸一样呢?为了实现左右滑动的翻

页功能,Android提供了相应的控件—翻页视图ViewPager。对于ViewPager来说,一个页面就是一个项

(相当于ListView的一个列表项),许多个页面组成了ViewPager的页面项。

既然明确了翻页视图的原理类似列表视图和网格视图,它们的用法也很类似。例如,列表视图和网格视 图使用基本适配器BaseAdapter,翻页视图则使用翻页适配器PagerAdapter;列表视图和网格视图使用 列表项的点击监听器OnItemClickListener,翻页视图则使用页面变更监听器OnPageChangeListener监 听页面切换事件。

下面是翻页视图3个常用方法的说明。

setAdapter:设置页面项的适配器。适配器用的是PagerAdapter及其子类。setCurrentItem:设置当前页码,也就是要显示哪个页面。

addOnPageChangeListener:添加翻页视图的页面变更监听器。该监听器需实现接口

OnPageChangeListener下的3个方法,具体说明如下。

onPageScrollStateChanged:在页面滑动状态变化时触发。onPageScrolled:在页面滑动过程中触发。onPageSelected:在选中页面时,即滑动结束后触发。

在XML文件中添加ViewPager时注意指定完整路径的节点名称,示例如下:

由于翻页视图包含了多个页面项,因此要借助翻页适配器展示每个页面。翻页适配器的实现原理与基本 适配器类似,从PagerAdapter派生的翻页适配器主要实现下面6个方法。

构造方法:指定适配器需要处理的数据集合。

getCount: 获 取 页 面 项 的 个 数 。 isViewFromObject:判断当前视图是否来自指定对象,返回view == object即可。instantiateItem:实例化指定位置的页面,并将其添加到容器中。destroyItem: 从 容 器 中 销 毁 指 定 位 置 的 页 面 。 getPageTitle:获得指定页面的标题文本,有搭配翻页标签栏时才要实现该方法。

以商品信息为例,翻页适配器需要通过构造方法传入商品列表,再由instantiateItem方法实例化视图对 象并添加至容器,详细的翻页适配器代码示例如下::

(完整代码见

chapter08\src\main\java\com\example\chapter08\adapter\ImagePagerAdapater.java)

public class ImagePagerAdapater extends PagerAdapter {

// 声明一个图像视图列表

private List<ImageView> mViewList = new ArrayList<ImageView>();

// 声明一个商品信息列表

private List<GoodsInfo> mGoodsList = new ArrayList<GoodsInfo>();

// 图像翻页适配器的构造方法,传入上下文与商品信息列表

public ImagePagerAdapater(Context context, List<GoodsInfo> goodsList) { mGoodsList = goodsList;

// 给每个商品分配一个专用的图像视图

for (int i = 0; i < mGoodsList.size(); i++) {

ImageView view = new ImageView(context); // 创建一个图像视图对象

view.setLayoutParams(new LayoutParams( LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));

view.setImageResource(mGoodsList.get(i).pic); mViewList.add(view); // 把该商品的图像视图添加到图像视图列表

}

}

// 获取页面项的个数

public int getCount() { return mViewList.size();

}

// 判断当前视图是否来自指定对象

public boolean isViewFromObject(View view, Object object) { return view == object;

}

// 从容器中销毁指定位置的页面

public void destroyItem(ViewGroup container, int position, Object object) { container.removeView(mViewList.get(position));

}

接着回到活动页面代码,给翻页视图设置上述的翻页适配器,代码如下所示:

(完整代码见chapter08\src\main\java\com\example\chapter08\ViewPagerActivity.java)

由于监听器OnPageChangeListener多数情况只用到onPageSelected方法,很少用到onPageScrollStateChanged和onPageScrolled两个方法,因此Android又提供了简化版的页面变更监听 器名为SimpleOnPageChangeListener,新的监听器仅需实现onPageSelected方法。给翻页视图添加简 化版监听器的代码示例如下:

然后运行测试App,初始的翻页界面如图8-18所示,此时整个页面只显示第一部手机。用手指从右向左活动页面,滑到一半的界面如图8-19所示,可见第一部手机逐渐向左隐去,而第二部手机逐渐从右边拉 出。继续向左活动一段距离再松开手指,此时滑动结束的界面如图8-20所示,可见整个页面完全显示第 二部手机了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eCaaqe2I-1676725568177)(media/39dfa15fb1639b92fd463c2139fabb73.jpeg)]

图8-18 初始的翻页视图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KuyszMCL-1676725568179)(media/eb97ddf96d8598a0f1c49d38a582977a.jpeg)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h9ZXgCrq-1676725568180)(media/46fc217f3b73e2221ab68569df0381e0.jpeg)]图8-19 滑到一半的翻页视图

图8-20 滑动结束的翻页视图

翻页标签栏PagerTabStrip

尽管翻页视图实现了左右滑动,可是没滑动的时候看不出这是个翻页视图,而且也不晓得当前滑到了哪 个页面。为此Android提供了翻页标签栏PagerTabStrip,它能够在翻页视图上方显示页面标题,从而方 便用户的浏览操作。PagerTabStrip类似选项卡效果,文本下面有横线,点击左右选项卡即可切换到对应页面。给翻页视图引入翻页标签栏只需下列两个步骤:

步骤一,在XML文件的ViewPager节点内部添加PagerTabStrip节点,示例如下:

(完整代码见chapter08\src\main\res\layout\activity_pager_tab.xml)

步骤二,在翻页适配器的代码中重写getPageTitle方法,在不同位置返回对应的标题文本,示例代码如 下:

(完整代码见

chapter08\src\main\java\com\example\chapter08\adapter\ImagePagerAdapater.java)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6fOFdt03-1676725568181)(media/358aa60d76b91d4091ec300b890877ee.jpeg)]完成上述两步骤之后,重新运行测试App,即可观察翻页标签栏的界面效果。如图8-21和图8-22所示, 这是翻到不同页面的翻页视图,可见界面正上方是当前页面的标题,左上方文字是左边页面的标题,右 上方文字是右边页面的标题。

图8-21 翻页标签栏的界面效果1

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JhAwLMEt-1676725568182)(media/1d884b472f87598f5e9a4af5babdffe8.jpeg)]

图8-22 翻页标签栏的界面效果2

另外,若想修改翻页标签栏的文本样式,必须在Java代码中调用setTextSize和setTextColor方法才行, 因为PagerTabStrip不支持在XML文件中设置文本大小和文本颜色,只能在代码中设置文本样式,具体的 设置代码如下所示:

(完整代码见chapter08\src\main\java\com\example\chapter08\PagerTabActivity.java)

简单的启动引导页

翻页视图的使用范围很广,当用户安装一个新应用时,首次启动大多出现欢迎页面,这个引导页要往右 翻好几页,才会进入应用主页。这种启动引导页就是通过翻页视图实现的。

下面就来动手打造你的第一个App启动欢迎页吧!翻页技术的核心在于页面项的XML布局及其适配器, 因此首先要设计页面项的布局。一般来说,引导页由两部分组成,一部分是背景图;另一部分是页面下 方的一排圆点,其中高亮的圆点表示当前位于第几页。启动引导页的界面效果如图8-23与图8-24所示。其中,图8-23为欢迎页面的第一页,此时第一个圆点高亮显示;图8-24为右翻到了第二页,此时第二个圆点高亮显示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vhp537Vj-1676725568182)(media/66c9ded4a709fe44842d77e011b58369.jpeg)]

图8-23 欢迎页的第一页

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uSbgYG3J-1676725568183)(media/ba67a593419999b54d1bb5bcad75353b.jpeg)]

图8-24 欢迎页的第二页

除了背景图与一排圆点之外,最后一页往往有个按钮,它便是进入应用主页的入口。于是页面项的XML 文件至少包含3个控件:引导页的背景图(采用ImageView)、底部的一排圆点(采用RadioGroup)、 最后一页的入口按钮(采用Button),XML内容示例如下:

(完整代码见chapter08\src\main\res\layout\item_launch.xml)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qyDfcWlz-1676725568183)(media/2f668cd8ed8dcf82f31fc28c5364062d.jpeg)]根据上面的XML文件,引导页的最后两页如图8-25与图8-26所示。其中,图8-25是第三页,此时第三个圆点高亮显示;图8-26是最后一页,只有该页才会显示入口按钮。

图8-25 欢迎页的第三页

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TFIKMr6s-1676725568184)(media/9038278ef248805f15f783a1cdfe2979.jpeg)]

图8-26 欢迎页的最后一页

写好了页面项的XML布局,还得编写启动引导页的适配器代码,主要完成3项工作:

  1. 根据页面项的XML文件构造每页的视图。

  2. 让当前页码的圆点高亮显示。

  3. 如果翻到了最后一页,就显示中间的入口按钮。下面是启动引导页对应的翻页适配器代码例子:

    (完整代码见

    chapter08\src\main\java\com\example\chapter08\adapter\LaunchSimpleAdapter.java)

RadioButton radio = new RadioButton(context); // 创建一个单选按钮

radio.setLayoutParams(new LayoutParams( LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));

radio.setButtonDrawable(R.drawable.launch_guide); // 设置单选按钮的

图标

radio.setPadding(10, 10, 10, 10); // 设置单选按钮的四周间距

rg_indicate.addView(radio); // 把单选按钮添加到页面底部的单选组

}

// 当前位置的单选按钮要高亮显示,比如第二个引导页就高亮第二个单选按钮

((RadioButton) rg_indicate.getChildAt(i)).setChecked(true);

// 如果是最后一个引导页,则显示入口按钮,以便用户点击按钮进入主页

if (i == imageArray.length - 1) { btn_start.setVisibility(View.VISIBLE); btn_start.setOnClickListener(new OnClickListener() {

@Override

public void onClick(View v) {

// 这里要跳到应用主页

Toast.makeText(context, “欢迎您开启美好生活”, Toast.LENGTH_SHORT).show();

}

});

}

mViewList.add(view); // 把该图片对应的页面添加到引导页的视图列表

}

}

// 获取页面项的个数

public int getCount() { return mViewList.size();

}

// 判断当前视图是否来自指定对象

public boolean isViewFromObject(View view, Object object) { return view == object;

}

// 从容器中销毁指定位置的页面

public void destroyItem(ViewGroup container, int position, Object object) { container.removeView(mViewList.get(position));

}

// 实例化指定位置的页面,并将其添加到容器中

public Object instantiateItem(ViewGroup container, int position) { container.addView(mViewList.get(position));

return mViewList.get(position);

}

}

.4 碎片Fragment

本节介绍碎片的概念及其用法,包括:通过静态注册方式使用碎片、通过动态注册方式使用碎片(需要 配合碎片适配器FragmentPagerAdapter),并分析两种注册方式的碎片生命周期,最后结合实战演示 了如何使用碎片改进启动引导页。

碎片的静态注册

碎片Fragment是个特别的存在,它有点像报纸上的专栏,看起来只占据页面的一小块区域,但是这一区域有自己的生命周期,可以自行其是,仿佛独立王国;并且该区域只占据空间不扰乱业务,添加之后不 影响宿主页面的其他区域,去除之后也不影响宿主页面的其他区域。

每个碎片都有对应的XML布局文件,依据其使用方式可分为静态注册与动态注册两类。静态注册指的是 在XML文件中直接放置fragment节点,类似于一个普通控件,可被多个布局文件同时引用。静态注册一般用于某个通用的页面部件(如Logo条、广告条等),每个活动页面均可直接引用该部件。

下面是碎片页对应的XML文件内容,看起来跟列表项与网格项的布局文件差不多。

(完整代码见chapter08\src\main\res\layout\fragment_static.xml)

下面是与上述XML布局对应的碎片代码,除了继承自Fragment与入口方法onCreateView两点,其他地 方类似活动页面代码。

(完整代码见chapter08\src\main\java\com\example\chapter08\fragment\StaticFragment.java)

若想在活动页面的XML文件中引用上面定义的StaticFragment,可以直接添加一个fragment节点,但需 注意下列两点:

  1. fragment节点必须指定id属性,否则App运行会报错。

  2. fragment节点必须通过name属性指定碎片类的完整路径。 下面是在布局文件中引用碎片的XML例子。

    (完整代码见chapter08\src\main\res\layout\activity_fragment_static.xml)

运行测试App,可见碎片所在界面如图8-27所示。此时碎片区域仿佛一个视图,其内部控件同样可以接收点击事件。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-22JCfgVp-1676725568185)(media/cfca10285ce2e69834a64eef30d593c6.jpeg)]

图8-27 静态注册的碎片效果

另外,介绍一下碎片在静态注册时的生命周期,像活动的基本生命周期方法onCreate、onStart、

onResume、onPause、onStop、onDestroy,碎片同样也有,而且还多出了下面5个生命周期方法。

onAttach: 与 活 动 页 面 结 合 。 onCreateView:创建碎片视图。onActivityCreated:在活动页面创建完毕后调用。onDestroyView: 回 收 碎 片 视 图 。 onDetach:与活动页面分离。

至于这些周期方法的先后调用顺序,观察日志最简单明了。下面是打开活动页面时的日志信息,此时碎 片的onCreate方法先于活动的onCreate方法,而碎片的onStart与onResume均在活动的同名方法之 后。

下面是退出活动页面时的日志信息,此时碎片的onPause、onStop、onDestroy都在活动的同名方法之 前。

总结一下,在静态注册时,除了碎片的创建操作在页面创建之前,其他操作没有僭越页面范围。就像老 实本分的下级,上级开腔后才能说话,上级要做总结性发言前赶紧闭嘴。

碎片的动态注册

碎片拥有两种使用方式,也就是静态注册和动态注册。相比静态注册,实际开发中动态注册用得更多。 静态注册是在XML文件中直接添加fragment节点,而动态注册迟至代码执行时才动态添加碎片。动态生成的碎片基本给翻页视图使用,要知道ViewPager和Fragment可是一对好搭档。

要想在翻页视图中使用动态碎片,关键在于适配器。在“8.3.1 翻页视图ViewPager”小节演示翻页功能时,用到了翻页适配器PagerAdapter。如果结合使用碎片,翻页视图的适配器就要改用碎片适配器

FragmentPagerAdapter。与翻页适配器相比,碎片适配器增加了getItem方法用于获取指定位置的碎 片,同时去掉了isViewFromObject、instantiateItem、destroyItem三个方法,用起来更加容易。下面 是一个碎片适配器的实现代码例子。

(完整代码见

chapter08\src\main\java\com\example\chapter08\adapter\MobilePagerAdapter.java)

上面的适配器代码在getItem方法中不调用碎片的构造方法,却调用了newInstance方法,目的是给碎片 对象传递参数信息。由newInstance方法内部先调用构造方法创建碎片对象,再调用setArguments方法 塞进请求参数,然后在onCreateView中调用getArguments方法才能取出请求参数。下面是在动态注册 时传递请求参数的碎片代码例子:

(完整代码见

chapter08\src\main\java\com\example\chapter08\fragment\DynamicFragment.java)

// 获取该碎片的一个实例

public static DynamicFragment newInstance(int position, int image_id, String desc) {

DynamicFragment fragment = new DynamicFragment(); // 创建该碎片的一个实例

Bundle bundle = new Bundle(); // 创建一个新包裹bundle.putInt(“position”, position); // 往包裹存入位置序号bundle.putInt(“image_id”, image_id); // 往包裹存入图片的资源编号bundle.putString(“desc”, desc); // 往包裹存入商品的文字描述fragment.setArguments(bundle); // 把包裹塞给碎片

return fragment; // 返回碎片实例

}

// 创建碎片视图

public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

mContext = getActivity(); // 获取活动页面的上下文

if (getArguments() != null) { // 如果碎片携带有包裹,就打开包裹获取参数信息mPosition = getArguments().getInt(“position”, 0); // 从包裹取出位置序号mImageId = getArguments().getInt(“image_id”, 0); // 从包裹取出图片的资源

编号

mDesc = getArguments().getString(“desc”); // 从包裹取出商品的文字描述

}

// 根据布局文件fragment_dynamic.xml生成视图对象

mView = inflater.inflate(R.layout.fragment_dynamic, container, false); ImageView iv_pic = mView.findViewById(R.id.iv_pic);

TextView tv_desc = mView.findViewById(R.id.tv_desc); iv_pic.setImageResource(mImageId); tv_desc.setText(mDesc);

Log.d(TAG, “onCreateView position=” + mPosition); return mView; // 返回该碎片的视图对象

}

}

现在有了适用于动态注册的适配器与碎片对象,还需要一个活动页面展示翻页视图及其搭配的碎片适配 器。下面便是动态注册用到的活动页面代码。

(完整代码见chapter08\src\main\java\com\example\chapter08\FragmentDynamicActivity.java)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PygRWTHe-1676725568187)(media/9b90db5c8205494890936aec07b3e80a.jpeg)]运行测试App,初始的碎片界面如图8-28所示,此时默认展示第一个碎片,包含商品图片和商品描述。接着一路滑到最后一页如图8-29所示,此时展示了最后一个碎片,可见总体界面效果类似于“8.3.2 翻页标签栏PagerTabStrip”那样。

图8-28 翻到第一个碎片界面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5athtcCb-1676725568188)(media/17a313b68e87f1f0b6f44d25f58f9016.jpeg)]

图8-29 翻到最后一个碎片界面

接下来观察动态注册时候的碎片生命周期。按惯例分别在活动代码与碎片代码内部补充生命周期的日 志,然后观察App运行日志。下面是打开活动页面时的日志信息:

下面是退出活动页面时的日志信息:

日志搜集完毕,分析其中的奥妙,总结一下主要有以下3点:

  1. 动态注册时,碎片的onCreate方法在活动的onCreate方法之后,其余方法的先后顺序与静态注册 时保持一致。

  2. 注意onActivityCreated方法,无论是静态注册还是动态注册,该方法都在活动的onCreate方法之 后,可见该方法的确在页面创建之后才调用。

  3. 最重要的一点,进入第一个碎片之际,实际只加载了第一页和第二页,并没有加载所有碎片页,这 正是碎片动态注册的优点。无论当前位于哪一页,系统都只会加载当前页及相邻的左右两页,总共加载 不超过3页。一旦发生页面切换,相邻页面就被加载,非相邻页面就被回收。这么做的好处是节省了宝贵 的系统资源,只有用户正在浏览与将要浏览的碎片页才会加载,避免所有碎片页一起加载造成资源浪

    费,后者正是普通翻页视图的缺点。

改进的启动引导页

接下来将碎片用于实战,对“8.3.3 简单的启动引导页”加以改进。与之前相比,XML文件不变,改动的都是Java代码。下面是用于启动引导页的碎片适配器代码:

(完整代码见

chapter08\src\main\java\com\example\chapter08\adapter\LaunchImproveAdapter.java)

以上的碎片适配器代码倒是简单,原来与视图控件有关的操作都挪到碎片代码当中了,下面是每个启动 页的碎片代码例子:

(完整代码见chapter08\src\main\java\com\example\chapter08\fragment\LaunchFragment.java)

public class LaunchFragment extends Fragment { protected View mView; // 声明一个视图对象protected Context mContext; // 声明一个上下文对象private int mPosition; // 位置序号

private int mImageId; // 图片的资源编号

private int mCount = 4; // 引导页的数量

// 获取该碎片的一个实例

public static LaunchFragment newInstance(int position, int image_id) { LaunchFragment fragment = new LaunchFragment(); // 创建该碎片的一个实例Bundle bundle = new Bundle(); // 创建一个新包裹bundle.putInt(“position”, position); // 往包裹存入位置序号bundle.putInt(“image_id”, image_id); // 往包裹存入图片的资源编号fragment.setArguments(bundle); // 把包裹塞给碎片

return fragment; // 返回碎片实例

}

// 创建碎片视图

public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

mContext = getActivity(); // 获取活动页面的上下文

if (getArguments() != null) { // 如果碎片携带有包裹,就打开包裹获取参数信息mPosition = getArguments().getInt(“position”, 0); // 从包裹获取位置序号mImageId = getArguments().getInt(“image_id”, 0); // 从包裹获取图片的资源

编号

}

// 根据布局文件item_launch.xml生成视图对象

mView = inflater.inflate(R.layout.item_launch, container, false); ImageView iv_launch = mView.findViewById(R.id.iv_launch); RadioGroup rg_indicate = mView.findViewById(R.id.rg_indicate); Button btn_start = mView.findViewById(R.id.btn_start); iv_launch.setImageResource(mImageId); // 设置引导页的全屏图片

// 每个页面都分配一个对应的单选按钮

for (int j = 0; j < mCount; j++) {

经过碎片改造后的启动引导页,其界面效果跟“8.3.3 简单的启动引导页”是一样的。尽管看不出界面上的差异,但引入碎片之后至少有以下两个好处。

  1. 加快启动速度。因为动态注册的碎片,一开始只会加载前两个启动页,对比原来加载所有启动页

    (至少4页),无疑大幅减少了加载页的数量,从而提升了启动速度。

  2. 降低代码耦合。把视图操作剥离到单独的碎片代码,不与适配器代码混合在一起,方便后继的代码 维护工作。

.5 实战项目:记账本

人云:你不理财,财不理你。从工作开始,年轻人就要好好管理自己的个人收支。每年的收入减去支 出,剩下的结余才是进一步发展的积累资金。记账本便是管理日常收支的好帮手,一个易用的记账本

App有助于合理安排个人资金。

需求描述

好用的记账本必须具备两项基本功能,一项是记录新账单,另一项是查看账单列表。其中账单的记录操 作要求用户输入账单的明细要素,包括账单的发生时间、账单的收支类型(收入还是支出)、账单的交 易金额、账单的事由描述等,据此勾勒简易的账单添加界面如图8-30所示。账单列表页通常分月展示, 每页显示单个月份的账单数据,还要支持在不同月份之间切换。每月的账单数据按照时间从上往下排 列,每行的账单明细则需依次展示账单日期、事由描述、交易金额等信息,然后列表末尾展示当月的账 单合计情况(总共收入多少、总共支出多少)。根据这些要求描绘的账单列表界面原型如图8-31所示。

账单的填写功能对应数据库记录的添加操作,账单的展示功能对应数据库记录的查询操作,数据库记录 还有修改和删除操作,分别对应账单的编辑功能和删除功能。账单的编辑页面原型如图8-32所示,至于 删除操作则由如图8-33所示的提示窗控制,点击“是”按钮表示确定删除,点击“否”按钮表示取消删除。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4VLbprdn-1676725568189)(media/c24492b69d73193b8f46c21995be324a.jpeg)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oaXyAWgv-1676725568189)(media/abc3f8369393e00bcae870ce77130278.jpeg)]图8-30 账单填写页面

图8-31 账单列表页面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1AXJn1G4-1676725568191)(media/255e9e208a2076fc1719f4633dd4e310.jpeg)]

图8-32 账单编辑页面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-glOWp7Od-1676725568191)(media/659ad9b4e2a64355235d00146dc13545.jpeg)]

界面设计

图8-33 删除账单的提示窗

除了文本视图、按钮、编辑框、单选按钮等简单控件之外,记账本还用到了下列控件以及相关的适配 器:

翻页视图ViewPager:每页一个月份,一年12个月,支持左右滑动,用到了ViewPager。翻页标签栏PagerTabStrip:每个账单页上方的月份标题来自PagerTabStrip。

碎片适配器FragmentPagerAdapter:把12个月份的Fragment组装到ViewPager中,用到了碎片 适配器。

碎片Fragment:12个月份对应12个账单页,每页都是一个碎片Fragment。列表视图ListView:每月的账单明细从上往下排列,采用了ListView。

基本适配器BaseAdapter:每行的账单项依次展示账单日期、事由描述、交易金额等信息,需要列表视图搭档基本适配器。

提醒对话框AlertDialog:删除账单项的提示窗用到了AlertDialog。

日期选择对话框DatePickerDialog:填写账单信息时,要通过DatePickerDialog选择账单日期。

记账本的几个页面当中,账单列表页面使用了好几种高级控件,又有翻页视图又有列表视图,以及它们 各自的数据适配器,看起来颇为复杂。为方便读者理清该页面的控件联系,图8-34列出了从活动页面开 始直到账单行的依赖嵌套关系(账单总体页面→每个月份的账单页→每月账单的明细列表→每行的账单 信息)。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9nCRxOQ0-1676725568191)(media/f9b8d2540541c469a5478f9622626788.jpeg)]

关键代码

图8-34 账单列表页面的控件嵌套关系

为了方便读者顺利完成记账本的编码开发,下面罗列几处关键的代码实现逻辑。

如何实现日期下拉框

填写账单时间的时候,输入界面默认展示当天日期,用户若想修改账单时间,就要点击日期文本,此时 界面弹出日期选择对话框,待用户选完具体日期,再回到主界面展示选定日期的文本。这种实现方式类 似于下拉框控件Spinner,可是点击Spinner会弹出文本列表对话框,而非日期选择对话框。尽管

Android未提供现成的日期下拉框,但是结合文本视图与日期选择对话框,也能实现类似Spinner的日期下拉框效果。具体步骤说明如下:

步骤一,在账单填写页面的XML文件中添加名为tv_date的TextView,并给它指定drawableRight属性, 属性值为一个向下三角形的资源图片,也就是让该控件看起来像个下拉框。包含tv_date在内的账单时间布局片段示例如下:

(完整代码见chapter08\src\main\res\layout\activity_bill_add.xml)

步骤二,回到该页面对应的Java代码,给文本视图tv_date注册点击监听器,一旦发现用户点击了该视 图,就弹出日期选择对话框DatePickerDialog。下面是控件tv_date的点击响应代码例子:

(完整代码见chapter08\src\main\java\com\example\chapter08\BillAddActivity.java)

步骤三,注意到第二步构建日期对话框时,将日期监听器设在了当前页面,于是令活动代码实现日期变 更监听接口DatePickerDialog.OnDateSetListener,同时还要重写该接口的onDateSet方法,一旦发现 用户选择了某个日期,就将文本视图tv_date设为该日期文本。重写后的onDateSet方法代码示例如下:

如何编辑与删除账单项

需求描述提到既要支持账单的编辑功能,又要支持账单的删除功能,因为账单明细位于列表视图当中, 且列表视图允许同时设置列表项的点击监听器和长按监听器,所以可考虑将列表项的点击监听器映射到 账单的编辑功能,将列表项的长按监听器映射到账单的删除功能,也就是点击账单项时跳到账单的编辑 页面,长按账单项时弹出删除账单的提醒对话框。为此需要在账单的列表页实现下列两个步骤:

步骤一,给每月账单的列表视图分别注册列表项的点击监听器和长按监听器,注册代码如下:

(完整代码见chapter08\src\main\java\com\example\chapter08\fragment\BillFragment.java)

步骤二,由于第一步将点击监听器和长按监听器设到了列表适配器,因此令BillListAdapter分别实现

AdapterView.OnItemClickListener和AdapterView.OnItemLongClickListener,并且重写对应的点击方 法onItemClick与长按方法onItemLongClick,其中onItemClick内部补充页面的跳转逻辑,而

onItemLongClick内部补充提示窗的处理逻辑。重写之后的点击方法与长按方法代码如下所示:

(完整代码见chapter08\src\main\java\com\example\chapter08\adapter\BillListAdapter.java)

if (position >= mBillList.size()-1) { // 合计行不响应点击事件

return;

}

Log.d(TAG, “onItemClick position=” + position); BillInfo bill = mBillList.get(position);

// 以下跳转到账单填写页面

Intent intent = new Intent(mContext, BillAddActivity.class); intent.putExtra(“xuhao”, bill.xuhao); // 携带账单序号,表示已存在该账单mContext.startActivity(intent); // 因为已存在该账单,所以跳过去实际会编辑账单

}

@Override

public boolean onItemLongClick(AdapterView<?> parent, View view, final int position, long id) {

if (position >= mBillList.size()-1) { // 合计行不响应长按事件

return true;

}

Log.d(TAG, “onItemLongClick position=” + position);

BillInfo bill = mBillList.get(position); // 获得当前位置的账单信息AlertDialog.Builder builder = new AlertDialog.Builder(mContext); String desc = String.format(“是否删除以下账单?\n%s %s%d %s”, bill.date,

bill.type==0?“收入”:“支出”, (int) bill.amount,

bill.desc);

builder.setMessage(desc); // 设置提醒对话框的消息文本

builder.setPositiveButton(“是”, new DialogInterface.OnClickListener() { @Override

public void onClick(DialogInterface dialog, int which) { deleteBill(position); // 删除该账单

}

});

builder.setNegativeButton(“否”, null); builder.create().show(); // 显示提醒对话框return true;

}

合并账单的添加与编辑功能

上述第二点提到账单编辑页面仍然跳到了BillAddActivity,然而该页面原本用作账单填写,若想让它同时 支持账单编辑功能,则需从意图包裹取出名为xuhao的字段,得到上个页面传来的序号数值,通过判断 该字段是否为-1,再分别对应处理,后续的处理过程分成以下两个步骤:

步骤一,若xuhao字段的值为-1,则表示不存在原账单的序号,此时应进入账单添加逻辑;若值不

为-1,则表示已存在该账单序号,此时应进入账单编辑处理,也就是将数据库中查到的原账单信息展示 在各输入框,再由用户酌情修改详细的账单信息。相应的代码逻辑如下所示:

(完整代码见chapter08\src\main\java\com\example\chapter08\BillAddActivity.java)

步骤二,保存账单记录之时,也要先判断数据库中是否已经存在对应账单,如果有找到对应的账单记 录,那么执行记录更新操作,否则执行记录添加操作。对应的数据库的操作代码示例如下:

(完整代码见chapter08\src\main\java\com\example\chapter08\database\BillDBHelper.java)

.6 小结

本章主要介绍了App开发的高级控件相关知识,包括:下拉列表的用法(下拉框Spinner、数组适配器

ArrayAdapter、简单适配器SimpleAdapter)、列表类视图的用法(基本适配器BaseAdapter、列表视图ListView、网格视图GridView)、翻页类视图的基本用法(翻页视图ViewPager、翻页适配器

PagerAdapter、翻页标签栏PagerTabStrip)、碎片的两种用法(静态注册方式、动态注册方式、碎片 适配器FragmentPagerAdapter)。中间穿插了实战模块的运用,如改进后的购物车、改进后的启动引 导页等。最后设计了一个实战项目“记账本”,在该项目的App编码中用到了前面介绍的大部分控件,从而加深了对所学知识的理解。

通过本章的学习,我们应该能够掌握以下4种开发技能:

  1. 学会使用下拉框控件。
  2. 学会使用列表视图和网格视图。
  3. 学会使用翻页视图与翻页标签栏。
  4. 学会通过两种注册方式分别使用碎片。

.7 课后练习题

一、填空题
  1. Spinner是种多选 _ 的下拉框控件。
  2. 若想在页面中部弹出Spinner的列表对话框,要把spinnerMode属性设置为 _ 。
  3. 在XML文件中,如果ListView后面还有其他平级的控件,就要将ListView的高度设为 _ ,同时权重设为1,确保列表视图扩展到剩余的页面区域。
  4. 翻页视图ViewPager设置当前页面的方法是_。
  5. Fragment有两种注册方式,分别是 _ 和 _ 。
二、判断题(正确打√,错误打×)
  1. 简单适配器只能展示纯文本列表。( )
  2. 列表视图只支持列表项的点击事件,不支持列表项的长按事件。( )
  3. 网格视图可以同时指定行数和列数。( )
  4. 引入翻页标签栏PagerTabStrip,它能够在翻页视图上方显示页面标题。( )
  5. 采取动态注册方式的时候,碎片需要配合翻页视图才能正常使用。( )
三、选择题
  1. 下拉框可使用( )。A. 数 组 适 配 器 B. 简 单 适 配 器 C. 基 本 适 配 器 D.翻页适配器
  2. 从BaseAdapter派生的数据适配器,要在( )方法中补充各控件的处理逻辑。

A.getCount B.getItem C.getItemId D.getView

  1. 在列表视图当中,若想不让列表中的控件抢占列表项的焦点,应当将内部视图的焦点抢占方式设置为

( )。A.beforeDescendants B.afterDescendants C.blocksDescendants D.不设置

  1. 在网格视图当中,若想让每行的剩余空间均匀分配给该行的每个网格,应当将拉伸模式设置为(

    )。

A.none B.columnWidth C.spacingWidth D.spacingWidthUniform

  1. 若想让翻页视图在滚动结束后触发某种动作,应当重写翻页适配器的( )方法。

A.onPageScrolled B.onPageSelected

  1. onPageScrollStateChanged
  2. 以上3个都不是
四、简答题

请简要描述App的启动引导页主要采用了哪些控件。

五、动手练习

请上机实验下列3项练习:

  1. 将第6章购物车界面的商品列表改造为列表视图,将商城界面的商品列表改造为网格视图。
    1. 联合运用翻页视图与碎片,实现App启动之时的欢迎引导页面。
    2. 实践本章的记账本项目,要求实现账单的增加、删除、修改、查看功能,并支持账单的列表展示与分 月浏览。

第9章 广播组件Broadcast

本章介绍Android4大组件之一Broadcast的基本概念和常见用法。主要包括如何发送和接收应用自身的 广播、如何监听和处理设备发出来的系统广播、如何监听因为屏幕变更导致App界面改变的状态事件。

.1 收发应用广播

本节介绍应用广播的几种收发形式,包括如何收发标准广播、如何收发有序广播、如何收发静态广播 等。

收发标准广播

App在运行的时候有各种各样的数据流转,有的数据从上一个页面流向下一个页面,此时可通过意图在 活动之间传递包裹;有的数据从应用内存流向存储卡,此时可进行文件读写操作。还有的数据流向千奇 百怪,比如活动页面向碎片传递数据,按照“8.4.2 碎片的动态注册”小节的描述,尚可调用setArguments和getArguments方法存取参数;然而若是由碎片向活动页面传递数据,就没有类似

setResult这样回馈结果的方法了。

随着App工程的代码量日益增长,承载数据流通的管道会越发不够用,好比装修房子的时候,给每个房 间都预留了网线插口,只有插上网线才能上网。可是现在联网设备越来越多,除了电脑之外,电视也要 联网,平板也要联网,乃至空调都要联网,如此一来网口早就不够用了。那怎样解决众多设备的联网问 题呢?原来家家户户都配了无线路由器,路由器向四周发射WiFi信号,各设备只要安装了无线网卡,就 能接收WiFi信号从而连接上网。于是“发射器+接收器”的模式另辟蹊径,比起网线这种固定管道要灵活得多,无须拉线即可随时随地传输数据。

Android的广播机制正是借鉴了WiFi的通信原理,不必搭建专门的通路,就能在发送方与接收方之间建立连接。同时广播(Broadcast)也是Android的四大组件之一,它用于Android各组件之间的灵活通 信,与活动的区别在于:

  1. 活动只能一对一通信;而广播可以一对多,一人发送广播,多人接收处理。

  2. 对于发送方来说,广播不需要考虑接收方有没有在工作,接收方在工作就接收广播,不在工作就丢 弃广播。

  3. 对于接收方来说,因为可能会收到各式各样的广播,所以接收方要自行过滤符合条件的广播,之后 再解包处理。

    与广播有关的方法主要有以下3个。

    sendBroadcast: 发 送 广 播 。 registerReceiver:注册广播的接收器,可在onStart或onResume方法中注册接收器。

    unregisterReceiver:注销广播的接收器,可在onStop或onPause方法中注销接收器。

具体到编码实现上,广播的收发过程可分为3个步骤:发送标准广播、定义广播接收器、开关广播接收 器,分别说明如下。

发送标准广播

广播的发送操作很简单,一共只有两步:先创建意图对象,再调用sendBroadcast方法发送广播即可。不过要注意,意图对象需要指定广播的动作名称,如同每个路由器都得给自己的WiFi起个名称一般,这样接收方才能根据动作名称判断来的是李逵而不是李鬼。下面是通过点击按钮发送广播的活动页面代 码:

(完整代码见chapter09\src\main\java\com\example\chapter09\BroadStandardActivity.java)

public class BroadStandardActivity extends AppCompatActivity implements View.OnClickListener {

private final static String TAG = “BroadStandardActivity”;

// 这是广播的动作名称,发送广播和接收广播都以它作为接头暗号

private final static String STANDARD_ACTION = “com.example.chapter09.standard”;

private TextView tv_standard; // 声明一个文本视图对象

private String mDesc = “这里查看标准广播的收听信息”;

@Override

protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_broad_standard); tv_standard = findViewById(R.id.tv_standard); tv_standard.setText(mDesc);

findViewById(R.id.btn_send_standard).setOnClickListener(this);

}

@Override

public void onClick(View v) {

if (v.getId() == R.id.btn_send_standard) {

Intent intent = new Intent(STANDARD_ACTION); // 创建指定动作的意图

sendBroadcast(intent); // 发送标准广播

}

}

}

定义广播接收器

广播发出来之后,还得有设备去接收广播,也就是需要广播接收器。接收器主要规定两个事情,一个是 接收什么样的广播,另一个是收到广播以后要做什么。由于接收器的处理逻辑大同小异,因此Android 提供了抽象之后的接收器基类BroadcastReceiver,开发者自定义的接收器都从BroadcastReceiver派生而来。新定义的接收器需要重写onReceive方法,方法内部先判断当前广播是否符合待接收的广播名

称,校验通过再开展后续的业务逻辑。下面是广播接收器的一个定义代码例子:

开关广播接收器

为了避免资源浪费,还要求合理使用接收器。就像WiFi上网,需要上网时才打开WiFi,不需要上网时就关闭WiFi。广播接收器也是如此,活动页面启动之后才注册接收器,活动页面停止之际就注销接收器。在注册接收器的时候,允许事先指定只接收某种类型的广播,即通过意图过滤器挑选动作名称一致的广 播。接收器的注册与注销代码示例如下:

完成上述3个步骤后,便构建了广播从发送到接收的完整流程。运行测试App,初始的广播界面如图9-1 所示,点击发送按钮触发广播,界面下方立刻刷新广播日志,如图9-2所示,可见接收器正确收到广播并 成功打印日志。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y8s4emH8-1676725568193)(media/c99421947aa12432705fe5c5fa858ecd.jpeg)]

图9-1 准备接收标准广播

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gcpofmel-1676725568193)(media/a69612a5e3a3b8eda1c143d384d49c42.jpeg)]

收发有序广播

图9-2 收听到了标准广播

由于广播没指定唯一的接收者,因此可能存在多个接收器,每个接收器都拥有自己的处理逻辑。这种机 制固然灵活,却不够严谨,因为不同接收器之间也许有矛盾。

比如只要办了借书证,大家都能借阅图书馆的藏书,不过一本书被读者甲借出去之后,读者乙就不能再 借这本书了,必须等到读者甲归还了该书之后,读者乙方可继续借阅此书。这个借书场景体现了一种有 序性,即图书是轮流借阅着的,且同时刻仅能借给一位读者,只有前面的读者借完归还,才轮到后面的 读者借阅。另外,读者甲一定会归还此书吗?可能读者甲对该书爱不释手,从图书馆高价买断了这本 书;也可能读者甲粗心大意,不小心弄丢了这本书。不管是哪种情况,读者甲都无法还书,导致正在排 队的读者乙无书可借。这种借不到书的场景体现了一种依赖关系,即使读者乙迫不及待地想借到书,也 得看读者甲的心情,要是读者甲因为各种理由没能还书,那么读者乙就白白排队了。上述的借书业务对 应到广播的接收功能,则要求实现下列的处理逻辑:

  1. 一个广播存在多个接收器,这些接收器需要排队收听广播,这意味着该广播是条有序广播。

  2. 先收到广播的接收器A,既可以让其他接收器继续收听广播,也可以中断广播不让其他接收器收 听。

    至于如何实现有序广播的收发,则需完成以下的3个编码步骤:

发送广播时要注明这是个有序广播

之前发送标准广播用到了sendBroadcast方法,可是该方法发出来的广播是无序的。只有调用

sendOrderedBroadcast方法才能发送有序广播,具体的发送代码示例如下:

(完整代码见chapter09\src\main\java\com\example\chapter09\BroadOrderActivity.java)

定义有序广播的接收器

接收器的定义代码基本不变,也要从BroadcastReceiver继承而来,唯一的区别是有序广播的接收器允 许中断广播。倘若在接收器的内部代码调用abortBroadcast方法,就会中断有序广播,使得后面的接收器不能再接收该广播。下面是有序广播的两个接收器代码例子:

注册有序广播的多个接收器

接收器的注册操作同样调用registerReceiver方法,为了给接收器排队,还需调用意图过滤器的

setPriority方法设置优先级,优先级越大的接收器,越先收到有序广播。如果不设置优先级,或者两个接收器的优先级相等,那么越早注册的接收器,会越先收到有序广播。譬如以下的广播注册代码,尽管 接收器A更早注册,但接收器B的优先级更高,结果先收到广播的应当是接收器B。

接下来通过测试页面演示有序广播的收发,如果没要求中断广播,则有序广播的接收界面如图9-3所示, 此时接收器B和接收器A依次收到了广播;如果要求中断广播,则有序广播的接收界面如图9-4所示,此 时只有接收器B收到了广播。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lheVtxyG-1676725568194)(media/8e43aad712651bcefaa1267f3f03b490.jpeg)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MNxlHJpS-1676725568194)(media/daa0dd67842429960acc584c38e8ff7a.jpeg)]图9-3 依次接收有序广播

收发静态广播

图9-4 中途打断有序广播

前面几节使用广播之时,无一例外在代码中注册接收器。可是同为4大组件,活动(activity)、服务

(service)、内容提供器(provider)都能在AndroidManifest.xml注册,为啥广播只能在代码中注册 呢?其实广播接收器也能在AndroidManifest.xml注册,并且注册时候的节点名为receiver,一旦接收器 在AndroidManifest.xml注册,就无须在代码中注册了。

在AndroidManifest.xml中注册接收器,该方式被称作静态注册;而在代码中注册接收器,该方式被称作动态注册。之所以罕见静态注册,是因为静态注册容易导致安全问题,故而Android 8.0之后废弃了大多数静态注册。话虽如此,Android倒也没有彻底禁止静态注册,只要满足特定的编码条件,那么依然 能够通过静态方式注册接收器。具体注册步骤说明如下。

首先右击当前模块的默认包,依次选择右键菜单的New→Package,创建名为receiver的新包,用于存 放静态注册的接收器代码。

其次右击刚创建的receiver包,依次选择右键菜单的New→Other→Broadcast Receiver,弹出如图9-5

所示的组件创建对话框。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0QlLzT96-1676725568195)(media/c634b89085ec79ce2836576ea22acbc9.jpeg)]

图9-5 广播组件的创建对话框

在组件创建对话框的Class Name一栏填写接收器的类名,比如ShockReceiver,再单击对话框右下角的

Finish按钮。之后Android Studio自动在receiver包内创建代码文件ShockReceiver.java,且接收器的默认代码如下所示:

同时AndroidManifest.xml自动添加接收器的节点配置,默认的receiver配置如下所示:

然而自动生成的接收器不仅啥都没干,还丢出一个异常UnsupportedOperationException。明显这个接 收器没法用,为了感知到接收器正在工作,可以考虑在onReceive方法中记录日志,也可在该方法中震 动手机。因为ShockReceiver未依附于任何活动,自然无法直接操作界面控件,所以只能观察程序日

志,或者干脆让手机摇晃起来。实现手机震动之时,要调用getSystemService方法,先从系统服务

VIBRATOR_SERVICE获取震动管理器Vibrator,再调用震动管理器的vibrate方法震动手机。包含手机震动功能的接收器代码示例如下:

(完整代码见chapter09\src\main\java\com\example\chapter09\receiver\ShockReceiver.java)

由于震动手机需要申请对应的权限,因此打开AndroidManifest.xml添加以下的权限申请配置:

此外,接收器代码定义了一个动作名称,其值为“com.example.chapter09.shock”,表示onReceive方 法只处理过滤该动作之后的广播,从而提高接收效率。除了在代码中过滤之外,还能修改

AndroidManifest.xml,在receiver节点内部增加intent-filter标签加以过滤,添加过滤配置后的receiver

节点信息如下所示:

终于到了发送广播这步,由于Android 8.0之后删除了大部分静态注册,防止App退出后仍在收听广播, 因此为了让应用能够继续接收静态广播,需要给静态广播指定包名,也就是调用意图对象的

setComponent方法设置组件路径。详细的静态广播发送代码示例如下:

(完整代码见chapter09\src\main\java\com\example\chapter09\BroadStaticActivity.java)

经过上述的编码以及配置工作,总算完成了静态广播的发送与接收流程。特别注意,经过整改的静态注 册只适用于接收App自身的广播,不能接收系统广播,也不能接收其他应用的广播。

运行测试App,初始的广播发送界面如图9-6所示,点击发送按钮触发静态广播,接着接收器收到广播信息,手机随之震动了若干时间,说明静态注册的接收器奏效了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wYcrTf3U-1676725568196)(media/85ce871fdf99dfd3a8ae41ea4cac60e6.jpeg)]

图9-6 收到静态注册的震动广播

.2 监听系统广播

本节介绍了几种系统广播的监听办法,包括如何接收分钟到达广播、如何接收网络变更广播、如何监听 定时管理器发出的系统闹钟广播等。

接收分钟到达广播

除了应用自身的广播,系统也会发出各式各样的广播,通过监听这些系统广播,App能够得知周围环境 发生了什么变化,从而按照最新环境调整运行逻辑。分钟到达广播便是系统广播之一,每当时钟到达某 分零秒,也就是跳到新的分钟时刻,系统就通过全局大喇叭播报分钟广播。App只要在运行时侦听分钟 广播Intent.ACTION_TIME_TICK,即可在分钟切换之际收到广播信息。

由于分钟广播属于系统广播,发送操作已经交给系统了,因此若要侦听分钟广播,App只需实现该广播 的接收操作。具体到编码上,接收分钟广播可分解为下面3个步骤:

步骤一,定义一个分钟广播的接收器,并重写接收器的onReceive方法,补充收到广播之后的处理逻辑。

步骤二,重写活动页面的onStart方法,添加广播接收器的注册代码,注意要让接收器过滤分钟到达广播

Intent.ACTION_TIME_TICK。

步骤三,重写活动页面的onStop方法,添加广播接收器的注销代码。

根据上述逻辑编写活动代码,使之监听系统发来的分钟广播,下面是演示页面的活动代码例子:

(完整代码见chapter09\src\main\java\com\example\chapter09\SystemMinuteActivity.java)

运行测试App,初始界面如图9-7所示,稍等片刻直到下一分钟到来,界面马上多了广播日志,如图9-8 所示,可见此时准点收到了系统发出的分钟到达广播。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oxr54JGL-1676725568196)(media/e14668c6f732f88284e2ca418893d153.jpeg)]

图9-7 准备接收分钟广播

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a7v1RrBS-1676725568197)(media/7516e7b2afbb4cf5f7393758f9c228cf.jpeg)]

图9-8 收听到了分钟广播

接收网络变更广播

除了分钟广播,网络变更广播也很常见,因为手机可能使用WiFi上网,也可能使用数据连接上网,而后者会产生流量费用,所以手机浏览器都提供了“智能无图”的功能,连上WiFi网络时才显示网页上的图

片,没连上WiFi就不显示图片。这类业务场景就要求侦听网络变更广播,对于当前网络变成WiFi连接、变成数据连接的两种情况,需要分别判断并加以处理。

接收网络变更广播可分解为下面3个步骤:

步骤一,定义一个网络广播的接收器,并重写接收器的onReceive方法,补充收到广播之后的处理逻辑。

步骤二,重写活动页面的onStart方法,添加广播接收器的注册代码,注意要让接收器过滤网络变更广播

android.net.conn.CONNECTIVITY_CHANGE。

步骤三,重写活动页面的onStop方法,添加广播接收器的注销代码。

上述3个步骤中,尤为注意第一步骤,因为onReceive方法只表示收到了网络广播,至于变成哪种网络, 还得把广播消息解包才知道是怎么回事。网络广播携带的包裹中有个名为networkInfo的对象,其数据类型为NetworkInfo,于是调用NetworkInfo对象的相关方法,即可获取详细的网络信息。下面是

NetworkInfo的常用方法说明:

getType:获取网络类型。网络类型的取值说明见表9-1。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RgAmhG6C-1676725568197)(media/a8fe1966d987b3babf566645059ca49f.jpeg)]表9-1 网络类型的取值说明

getTypeName:获取网络类型的名称。

getSubtype:获取网络子类型。当网络类型为数据连接时,子类型为2G/3G/4G的细分类型,如

CDMA、EVDO、HSDPA、LTE等。网络子类型的取值说明见表9-2。

表9-2 网络子类型的取值说明

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cX2wpgXk-1676725568198)(media/8585da465457fc61a10da6a6c603c88e.jpeg)]

getSubtypeName:获取网络子类型的名称。getState:获取网络状态。网络状态的取值说明见表9-3。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1FQQtxT5-1676725568198)(media/32d5807145eddbd0fc6fd9f633d1c4e3.jpeg)]表9-3 网络状态的取值说明

根据梳理后的解包逻辑编写活动代码,使之监听系统发来的网络变更广播,下面是演示页面的代码片 段:

(完整代码见chapter09\src\main\java\com\example\chapter09\SystemNetworkActivity.java)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GjLCpkEI-1676725568199)(media/224c9ec430457bf141cd90fb44163395.jpeg)]运行测试App,初始界面如图9-9所示,说明手机正在使用数据连接。然后关闭数据连接,再开启WLAN,此时界面日志如图9-10所示,可见App果然收到了网络广播,并且正确从广播信息中得知已经切换到了WiFi网络。

图9-9 收到数据连接的网络广播

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0j3oigSv-1676725568199)(media/840812e2fe8f3767a76e5a5416c07c49.jpeg)]

图9-10 切换到WiFi的网络广播

定时管理器AlarmManager

尽管系统的分钟广播能够实现定时功能(每分钟一次),但是这种定时功能太低级了,既不能定制可长 可短的时间间隔,也不能限制定时广播的次数。为此Android提供了专门的定时管理器AlarmManager,它利用系统闹钟定时发送广播,比分钟广播拥有更强大的功能。由于闹钟与震动器同属系统服务,且闹钟的服务名称为ALARM_SERVICE,因此依然调用getSystemService方法获取闹钟管理器的实例,下面是从系统服务中获取闹钟管理器的代码:

得到闹钟实例后,即可调用它的各种方法设置闹钟规则了,AlarmManager的常见方法说明如下:

set:设置一次性定时器。第一个参数为定时器类型,通常填AlarmManager.RTC_WAKEUP;第二个参数为期望的执行时刻(单位为毫秒);第三个参数为待执行的延迟意图(PendingIntent类

型)。

setAndAllowWhileIdle:设置一次性定时器,参数说明同set方法,不同之处在于:即使设备处于 空闲状态,也会保证执行定时器。因为从Android 6.0开始,set方法在暗屏时不保证发送广播,必须 调 用 setAndAllowWhileIdle 方 法 才 能 保 证 发 送 广 播 。 setRepeating:设置重复定时器。第一个参数为定时器类型;第二个参数为首次执行时间(单位为毫秒);第三个参数为下次执行的间隔时间(单位为毫秒);第四个参数为待执行的延迟意图

(PendingIntent类型)。然而从Android 4.4开始,setRepeating方法不保证按时发送广播,只能通过setAndAllowWhileIdle方法间接实现重复定时功能。

cancel:取消指定延迟意图的定时器。

以上的方法说明出现了新名词—延迟意图,它是PendingIntent类型,顾名思义,延迟意图不是马上执行的意图,而是延迟若干时间才执行的意图。像之前的活动页面跳转,调用startActivity方法跳到下个页面,此时跳转动作是立刻发生的,所以要传入Intent对象。由于定时器的广播不是立刻发送的,而是时 刻到达了才发送广播,因此不能传Intent对象只能传PendingIntent对象。当然意图与延迟意图不止一处 区别,它们的差异主要有下列3点:

  1. PendingIntent代表延迟的意图,它指向的组件不会马上激活;而Intent代表实时的意图,一旦被 启动,它指向的组件就会马上激活。
  2. PendingIntent是一类消息的组合,不但包含目标的Intent对象,还包含请求代码、请求方式等信 息。
  3. PendingIntent对象在创建之时便已知晓将要用于活动还是广播,例如调用getActivity方法得到的 是活动跳转的延迟意图,调用getBroadcast方法得到的是广播发送的延迟意图。

就闹钟广播的收发过程而言,需要实现3个编码步骤:定义定时器的广播接收器、开关定时器的广播接收 器、设置定时器的播报规则,分别叙述如下。

定义定时器的广播接收器

闹钟广播的接收器采用动态注册方式,它的实现途径与标准广播类似,都要从BroadcastReceiver派生 新的接收器,并重写onReceive方法。闹钟广播接收器的定义代码示例如下:

(完整代码见chapter09\src\main\java\com\example\chapter09\AlarmActivity.java)

开关定时器的广播接收器

定时接收器的开关流程参照标准广播,可以在活动页面的onStart方法中注册接收器,在活动页面的

onStop方法中注销接收器。相应的接收器开关代码如下所示:

设置定时器的播报规则

首先从系统服务中获取闹钟管理器,然后调用管理器的set***方法,把事先创建的延迟意图填到播报规则当中。下面是发送闹钟广播的代码例子:

完成上述的3个步骤之后,运行测试App,点击“设置闹钟”按钮,界面下方回显闹钟的设置信息,如图9-

11所示。稍等片刻,发现回显文本多了一行日志,如图9-12所示,同时手机也嗡嗡震动了一会,对比日志时间可知,闹钟广播果然在设定的时刻触发且收听了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yhhtmkGc-1676725568200)(media/18b93fdb212ad6710682e6e85004d310.jpeg)]

图9-11 刚刚设置闹钟

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uQzL2537-1676725568200)(media/82825309e5a13bc67bb192159d482b6a.jpeg)]

图9-12 收到闹钟广播

至于闹钟的重复播报问题,因为setRepeating方法不再可靠,所以要修改闹钟的收听逻辑,在

onReceive末尾补充调用sendAlarm方法,确保每次收到广播之后立即准备下一个广播。调整以后的onReceive方法代码示例如下:

.3 捕获屏幕的变更事件

本节介绍几种屏幕变更事件的捕获办法,包括如何监听竖屏与横屏之间的切换事件、如何监听从App界 面回到桌面的事件、如何监听从App界面切换到任务列表的事件等。

竖屏与横屏切换

除了系统广播之外,App所处的环境也会影响运行,比如手机有竖屏与横屏两种模式,竖屏时水平方向 较短而垂直方向较长,横屏时水平方向较长而垂直方向较短。两种屏幕方向不但造成App界面的展示差 异,而且竖屏和横屏切换之际,甚至会打乱App的生命周期。

接下来做个实验观察屏幕方向切换给生命周期带来的影响,现有一个测试页面ActTestActivity.java,参 考第4章的“4.1.2 Activity的生命周期”,它的活动代码重写了主要的生命周期方法,在每个周期方法中都打印状态日志,完整代码见

chapter09\src\main\java\com\example\chapter09\ActTestActivity.java。运行测试App,初始的竖屏界面如图9-13所示;接着旋转手机使之处于横屏,测试App也跟着转过来,此时横屏界面如图9-14所

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ak3oWd0s-1676725568200)(media/4391bf0f5a456e33a8427d61dd10701d.jpeg)]示。

图9-13 初始的竖屏界面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3toiIEvX-1676725568201)(media/f020fb1fdbdb9c69569c1832e3168f61.jpeg)]

图9-14 切换到横屏界面

对比图9-13的竖屏界面和图9-14的横屏界面,发现二者打印的生命周期时间居然是不一样的,而且横屏界面的日志时间全部在竖屏界面的日志时间后面,说明App从竖屏变为横屏的时候,整个活动页面又重 头创建了一遍。可是这个逻辑明显不对劲啊,从竖屏变为横屏,App界面就得重新加载;再从横屏变回 竖屏,App界面又得重新加载,如此反复重启页面,无疑非常浪费系统资源。

为了避免横竖屏切换时重新加载界面的情况,Android设计了一种配置变更机制,在指定的环境配置发生变更之时,无须重启活动页面,只需执行特定的变更行为。该机制的编码过程分为两步:修改

AndroidManifest.xml、修改活动页面的Java代码,详细说明如下。

修改AndroidManifest.xml

首先创建新的活动页面ChangeDirectionActivity,再打开AndroidManifest.xml,看到该活动对应的节 点配置是下面这样的:

给这个activity节点增加android:configChanges属性,并将属性值设为

“orientation|screenLayout|screenSize”,修改后的节点配置如下所示:

新属性configChanges的意思是,在某些情况之下,配置项变更不用重启活动页面,只需调用

onConfigurationChanged方法重新设定显示方式。故而只要给该属性指定若干豁免情况,就能避免无 谓的页面重启操作了,配置变更豁免情况的取值说明见表9-4。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GcwcjxRH-1676725568201)(media/656e3329ed04386a020983fa6b89387c.jpeg)]表9-4 配置变更豁免情况的取值说明

修改活动页面的Java代码

打开ChangeDirectionActivity的Java代码,重写活动的onConfigurationChanged方法,该方法的输入 参数为Configuration类型的配置对象,根据配置对象的orientation属性,即可判断屏幕的当前方向是竖 屏还是横屏,再补充对应的代码处理逻辑。下面是重写了onConfigurationChanged方法的活动代码例子:

(完整代码见chapter09\src\main\java\com\example\chapter09\ChangeDirectionActivity.java)

public class ChangeDirectionActivity extends AppCompatActivity { private TextView tv_monitor; // 声明一个文本视图对象

private String mDesc = “”; // 屏幕变更的描述说明

@Override

protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_change_direction); tv_monitor = findViewById(R.id.tv_monitor);

}

// 在配置项变更时触发。比如屏幕方向发生变更等等

// 有的手机需要在系统的“设置→显示”菜单开启“自动旋转屏幕”,或者从顶部下拉,找到“自动旋转”图标并开启

@Override

public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig);

switch (newConfig.orientation) { // 判断当前的屏幕方向

case Configuration.ORIENTATION_PORTRAIT: // 切换到竖屏

mDesc = String.format(“%s%s %s\n”, mDesc, DateUtil.getNowTime(), “当前屏幕为竖屏方向”);

tv_monitor.setText(mDesc); break;

case Configuration.ORIENTATION_LANDSCAPE: // 切换到横屏

mDesc = String.format(“%s%s %s\n”, mDesc, DateUtil.getNowTime(), “当前屏幕为横屏方向”);

tv_monitor.setText(mDesc); break;

default:

break;

}

}

}

运行测试App,一开始手机处于竖屏界面,旋转手机使之切为横屏状态,此时App界面如图9-15所示, 可见App成功获知了变更后的屏幕方向。反向旋转手机使之切回竖屏状态,此时App界面如图9-16所 示,可见App同样监听到了最新的屏幕方向。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eCkuyoxl-1676725568201)(media/f6e18abf73287709001f918d4c399ce0.jpeg)]

图9-15 切为横屏状态的界面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yH5fPq4u-1676725568202)(media/4839033bd72f20c9c93ee31478503a9d.jpeg)]

图9-16 切回竖屏状态的界面

经过上述两个步骤的改造,每次横竖屏的切换操作都不再重启界面,只会执行

onConfigurationChanged方法的代码逻辑,从而节省了系统的资源开销。

如果希望App始终保持竖屏界面,即使手机旋转为横屏也不改变App的界面方向,可以修改

AndroidManifest.xml,给activity节点添加android:screenOrientation属性,并将该属性设置为

portrait表示垂直方向,也就是保持竖屏界面;若该属性为landscape则表示水平方向,也就是保持横屏 界面。修改后的activity节点示例如下:

回到桌面与切换到任务列表

App不但能监测手机屏幕的方向变更,还能获知回到桌面的事件,连打开任务列表的事件也能实时得 知。回到桌面与打开任务列表都由按键触发,例如按下主页键会回到桌面,按下任务键会打开任务列 表。虽然这两个操作看起来属于按键事件,但系统并未提供相应的按键处理方法,而是通过广播发出事 件信息。

因此,若想知晓是否回到桌面,以及是否打开任务列表,均需收听系统广播

Intent.ACTION_CLOSE_SYSTEM_DIALOGS。至于如何区分当前广播究竟是回到桌面还是打开任务列表,则要从广播意图中获取原因reason字段,该字段值为homekey时表示回到桌面,值为recentapps 时表示打开任务列表。接下来演示一下此类广播的接收过程。

首先定义一个广播接收器,只处理动作为Intent.ACTION_CLOSE_SYSTEM_DIALOGS的系统广播,并判 断它是主页键来源还是任务键来源。该接收器的代码定义示例如下:

(完整代码见chapter09\src\main\java\com\example\chapter09\ReturnDesktopActivity.java)

接着在活动页面的onCreate方法中注册接收器,在onDestroy方法中注销接收器,其中接收器的注册代 码如下所示:

可是监听回到桌面的广播能用来干什么呢?一种用处是开启App的画中画模式,比如原先应用正在播放 视频,回到桌面时势必要暂停播放,有了画中画模式之后,可将播放界面缩小为屏幕上的一个小方块, 这样即使回到桌面也能继续观看视频。注意从Android 8.0开始才提供画中画模式,故而代码需要判断系统版本,下面是进入画中画模式的代码例子:

以上代码用于开启画中画模式,但有时希望在进入画中画之际调整界面,则需重写活动的

onPictureInPictureModeChanged方法,该方法在应用进入画中画模式或退出画中画模式时触发,在此 可补充相应的处理逻辑。重写后的方法代码示例如下:

另外,画中画模式要求在AndroidManifest.xml中开启画中画支持,也就是给activity节点添加

supportsPictureInPicture属性并设为true,添加新属性之后的activity配置示例如下:

运行测试App,正常的竖屏界面如图9-17所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y0FJSOV6-1676725568203)(media/0ecbbc7c16bbfcddd6e0d82b2cac8d84.jpeg)]

图9-17 App正常的竖屏界面

然后按下主页键,在回到桌面的同时,该应用自动开启画中画模式,变成悬浮于桌面的小窗,如图9-18 所示。点击小窗会变成大窗,如图9-19所示,再次点击大窗才退出画中画模式,重新打开应用的完整界 面。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xP1rxsuF-1676725568203)(media/e1ac07a0831dd17bef28b58b4308b1b1.jpeg)]

图9-18 开启了画中画模式的小窗

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4JyIFNLi-1676725568204)(media/4a379331411c60e270faa961c0aaad95.jpeg)]

图9-19 点击小窗变成大窗

.4 小结

本章主要介绍广播组件—Broadcast的常见用法,包括:正确收发应用广播(收发标准广播、收发有序广播、收发静态广播)、正确监听系统广播(接收分钟到达广播、接收网络变更广播、定时管理器AlarmManager)、正确捕获屏幕的变更事件(竖屏与横屏切换、回到桌面与切到任务列表)。

通过本章的学习,我们应该能掌握以下3种开发技能:

  1. 了解广播的应用场景,并学会正确收发应用广播。
  2. 了解常见的系统广播,并学会正确监听系统广播。
  3. 了解屏幕变更的产生条件,并学会捕捉屏幕变更事件。

.5 课后练习题

一、填空题

  1. 活动只能一对一通信,而广播可以_通信。

  2. 通过静态方式注册广播,就要在AndroidManifest.xml中添加名为—的接收器标签。

  3. —代表延迟的意图,它指向的组件不会马上激活。

  4. 手机的屏幕方向默认是—。

  5. 开启—模式之后,可将App界面缩小为屏幕上的一个方块。

    二、判断题(正确打√,错误打×)

  6. 标准广播是无序的,有可能后面注册的接收器反而比前面注册的接收器先收到广播。( )

  7. 通过setPriority方法设置优先级,优先级越小的接收器,越先收到有序广播。( )

  8. 普通应用能够通过静态注册方式来监听系统广播。( )

  9. 闹钟管理器AlarmManager的setRepeating方法保证能够按时发送广播。( )

  10. 旋转手机使得屏幕由竖屏变为横屏,App默认会重新加载整个页面(先销毁原页面再创建新页面)。

    ( )

三、选择题

  1. 在接收器内部调用( )方法,就会中断有序广播。

A.abortBroadcast B.cancelBroadcast C.interrupt D.sendBroadcast

  1. android.permission.VIBRATE表达的是( )权限。

A.呼吸灯B.麦克风C.闹钟D.震动器

  1. 网络类型( )表示手机的数据连接(含2G/3G/4G/5G)。

A.TYPE_WIFI B.TYPE_MOBILE C.TYPE_WIMAX D.TYPE_ETHERNET

  1. 网络状态( )表示已经连接。

A.CONNECTING B.CONNECTED C.SUSPENDED D.DISCONNECTED

5.( )属于configChanges属性配置的显示变更豁免情况。

A.orientation B.screenLayout C.screenSize D.keyboard

四、简答题

请简要描述收发标准广播的主要步骤。

五、动手练习

请上机实验下列3项练习:

  1. 通过设置不同的优先级,实现有序广播的正确收发。
  2. 通过监听网络变更广播,判断当前位于哪种网络。
  3. 通过监听回到桌面广播,实现App的画中画模式。

第10章 自定义控件

本章介绍App开发中的一些自定义控件技术,主要包括:视图是如何从无到有构建出来的、如何改造已 有的控件变出新控件、如何通过持续绘制实现简单动画。然后结合本章所学的知识,演示了一个实战项 目“广告轮播”的设计与实现。

1 0.1 视图的构建过程

本节介绍了一个视图的构建过程,包括:如何编写视图的构造方法,4种构造方法之间有什么区别;如何 测量实体的实际尺寸,包含文本、图像、线性视图的测量办法;如何利用画笔绘制视图的界面,并说明

onDraw方法与dispatchDraw方法的先后执行顺序。

10.1.1 视图的构造方法

Android自带的控件往往外观欠佳,开发者常常需要修改某些属性,比如按钮控件Button就有好几个问题,其一字号太小,其二文字颜色太浅,其三字母默认大写。于是XML文件中的每个Button节点都得添加textSize、textColor、textAllCaps 3个属性,以便定制按钮的字号、文字颜色和大小写开关,就像下面这样:

如果只是一两个按钮控件倒还好办,倘若App的许多页面都有很多Button,为了统一按钮风格,就得给 全部Button节点都加上这些属性。要是哪天产品大姐心血来潮,命令所有按钮统统换成另一种风格,如 此多的Button节点只好逐个修改过去,令人苦不堪言。为此可以考虑把按钮样式提炼出来,将统一的按 钮风格定义在某个地方,每个Button节点引用统一样式便行。为此打开res/values目录下的styles.xml, 在resources节点内部补充如下所示的风格配置定义:

接着回到XML布局文件中,给Button节点添加形如“style=“@style/样式名称””的引用说明,表示当前控 件将覆盖指定的属性样式,添加样式引用后的Button节点如下所示:

(完整代码见chapter10\src\main\res\layout\activity_custom_button.xml)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NcmNkXPh-1676725568204)(media/33c070d423b68dd1f8d07eeccdc6527f.jpeg)]运行测试App,打开按钮界面如图10-1所示,对比默认的按钮控件,可见通过style引用的按钮果然变了个模样。以后若要统一更换所有按钮的样式,只需修改styles.xml中的样式配置即可。

图10-1 通过style属性设置样式的按钮界面

然而样式引用仍有不足之处,因为只有Button节点添加了style属性才奏效,要是忘了添加style属性就不 管用了,而且样式引用只能修改已有的属性,不能添加新属性,也不能添加新方法。若想更灵活地定制 控件外观,就要通过自定义控件实现了。

自定义控件听起来很复杂的样子,其实并不高深,不管控件还是布局,它们本质上都是一个Java类,也拥有自身的构造方法。以视图基类View为例,它有4个构造方法,分别是:

  1. 带一个参数的构造方法public View(Context context),在Java代码中通过new关键字创建视图对象时,会调用这个构造方法。
  2. 带两个参数的构造方法public View(Context context, AttributeSet attrs),在XML文件中添加视图节点时,会调用这个构造方法。
  3. 带3个参数的构造方法public View(Context context, AttributeSet attrs, int defStyleAttr),采取默认的样式属性时,会调用这个构造方法。如果defStyleAttr填0,则表示没有默认的样式。
  4. 带4个参数的构造方法public View(Context context, AttributeSet attrs, int defStyleAttr, int

defStyleRes),采取默认的样式资源时,会调用这个构造方法。如果defStyleRes填0,则表示无样式资 源。

以上的4种构造方法中,前两种必须实现,否则要么不能在代码中创建视图对象,要么不能在XML文件中添加视图节点;至于后两种构造方法,则与styles.xml中的样式配置有关。先看带3个参数的构造方法, 第3个参数defStyleAttr的意思是指定默认的样式属性,这个样式属性在res/values下面的attrs.xml中配 置,如果values目录下没有attrs.xml就创建该文件,并填入以下的样式属性配置:

以上的配置内容表明了属性名称为customButtonStyle,属性格式为引用类型reference,也就是实际样 式在别的地方定义,这个地方便是styles.xml中定义的样式配置。可是customButtonStyle怎样与

styles.xml里的CommonButton样式关联起来呢?每当开发者创建新项目时,AndroidManifest.xml的

application节点都设置了主题属性,通常为android:theme=“@style/AppTheme”,这个默认主题来自于styles.xml的AppTheme,打开styles.xml发现文件开头的AppTheme配置定义如下所示:

原来App的默认主题源自Theme.AppCompat.Light.DarkActionBar,其中的Light表示这是亮色主题,

DarkActionBar表示顶部标题栏是暗色的,内部的3个color项指定了该主题采用的部分颜色。现在给

AppTheme添加一项customButtonStyle,并指定该项的样式为@style/CommonButton,修改后的

AppTheme配置示例如下:

接着到Java代码包中编写自定义的按钮控件,控件代码如下所示,注意在defStyleAttr处填上默认的样式 属性R.attr.customButtonStyle。

(完整代码见chapter10\src\main\java\com\example\chapter10\widget\CustomButton.java)

然后打开测试界面的XML布局文件activity_custom_button.xml,添加如下所示的自定义控件节点

CustomButton:

(完整代码见chapter10\src\main\res\layout\activity_custom_button.xml)
e savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_change_direction); tv_monitor = findViewById(R.id.tv_monitor);

}

// 在配置项变更时触发。比如屏幕方向发生变更等等

// 有的手机需要在系统的“设置→显示”菜单开启“自动旋转屏幕”,或者从顶部下拉,找到“自动旋转”图标并开启

@Override

public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig);

switch (newConfig.orientation) { // 判断当前的屏幕方向

case Configuration.ORIENTATION_PORTRAIT: // 切换到竖屏

mDesc = String.format(“%s%s %s\n”, mDesc, DateUtil.getNowTime(), “当前屏幕为竖屏方向”);

tv_monitor.setText(mDesc); break;

case Configuration.ORIENTATION_LANDSCAPE: // 切换到横屏

mDesc = String.format(“%s%s %s\n”, mDesc, DateUtil.getNowTime(), “当前屏幕为横屏方向”);

tv_monitor.setText(mDesc); break;

default:

break;

}

}

}

运行测试App,一开始手机处于竖屏界面,旋转手机使之切为横屏状态,此时App界面如图9-15所示, 可见App成功获知了变更后的屏幕方向。反向旋转手机使之切回竖屏状态,此时App界面如图9-16所 示,可见App同样监听到了最新的屏幕方向。

[外链图片转存中…(img-eCkuyoxl-1676725568201)]

图9-15 切为横屏状态的界面

[外链图片转存中…(img-yH5fPq4u-1676725568202)]

图9-16 切回竖屏状态的界面

经过上述两个步骤的改造,每次横竖屏的切换操作都不再重启界面,只会执行

onConfigurationChanged方法的代码逻辑,从而节省了系统的资源开销。

如果希望App始终保持竖屏界面,即使手机旋转为横屏也不改变App的界面方向,可以修改

AndroidManifest.xml,给activity节点添加android:screenOrientation属性,并将该属性设置为

portrait表示垂直方向,也就是保持竖屏界面;若该属性为landscape则表示水平方向,也就是保持横屏 界面。修改后的activity节点示例如下:

回到桌面与切换到任务列表

App不但能监测手机屏幕的方向变更,还能获知回到桌面的事件,连打开任务列表的事件也能实时得 知。回到桌面与打开任务列表都由按键触发,例如按下主页键会回到桌面,按下任务键会打开任务列 表。虽然这两个操作看起来属于按键事件,但系统并未提供相应的按键处理方法,而是通过广播发出事 件信息。

因此,若想知晓是否回到桌面,以及是否打开任务列表,均需收听系统广播

Intent.ACTION_CLOSE_SYSTEM_DIALOGS。至于如何区分当前广播究竟是回到桌面还是打开任务列表,则要从广播意图中获取原因reason字段,该字段值为homekey时表示回到桌面,值为recentapps 时表示打开任务列表。接下来演示一下此类广播的接收过程。

首先定义一个广播接收器,只处理动作为Intent.ACTION_CLOSE_SYSTEM_DIALOGS的系统广播,并判 断它是主页键来源还是任务键来源。该接收器的代码定义示例如下:

(完整代码见chapter09\src\main\java\com\example\chapter09\ReturnDesktopActivity.java)

接着在活动页面的onCreate方法中注册接收器,在onDestroy方法中注销接收器,其中接收器的注册代 码如下所示:

可是监听回到桌面的广播能用来干什么呢?一种用处是开启App的画中画模式,比如原先应用正在播放 视频,回到桌面时势必要暂停播放,有了画中画模式之后,可将播放界面缩小为屏幕上的一个小方块, 这样即使回到桌面也能继续观看视频。注意从Android 8.0开始才提供画中画模式,故而代码需要判断系统版本,下面是进入画中画模式的代码例子:

以上代码用于开启画中画模式,但有时希望在进入画中画之际调整界面,则需重写活动的

onPictureInPictureModeChanged方法,该方法在应用进入画中画模式或退出画中画模式时触发,在此 可补充相应的处理逻辑。重写后的方法代码示例如下:

另外,画中画模式要求在AndroidManifest.xml中开启画中画支持,也就是给activity节点添加

supportsPictureInPicture属性并设为true,添加新属性之后的activity配置示例如下:

运行测试App,正常的竖屏界面如图9-17所示。

[外链图片转存中…(img-Y0FJSOV6-1676725568203)]

图9-17 App正常的竖屏界面

然后按下主页键,在回到桌面的同时,该应用自动开启画中画模式,变成悬浮于桌面的小窗,如图9-18 所示。点击小窗会变成大窗,如图9-19所示,再次点击大窗才退出画中画模式,重新打开应用的完整界 面。

[外链图片转存中…(img-xP1rxsuF-1676725568203)]

图9-18 开启了画中画模式的小窗

[外链图片转存中…(img-4JyIFNLi-1676725568204)]

图9-19 点击小窗变成大窗

.4 小结

本章主要介绍广播组件—Broadcast的常见用法,包括:正确收发应用广播(收发标准广播、收发有序广播、收发静态广播)、正确监听系统广播(接收分钟到达广播、接收网络变更广播、定时管理器AlarmManager)、正确捕获屏幕的变更事件(竖屏与横屏切换、回到桌面与切到任务列表)。

通过本章的学习,我们应该能掌握以下3种开发技能:

  1. 了解广播的应用场景,并学会正确收发应用广播。
  2. 了解常见的系统广播,并学会正确监听系统广播。
  3. 了解屏幕变更的产生条件,并学会捕捉屏幕变更事件。

.5 课后练习题

一、填空题

  1. 活动只能一对一通信,而广播可以_通信。

  2. 通过静态方式注册广播,就要在AndroidManifest.xml中添加名为—的接收器标签。

  3. —代表延迟的意图,它指向的组件不会马上激活。

  4. 手机的屏幕方向默认是—。

  5. 开启—模式之后,可将App界面缩小为屏幕上的一个方块。

    二、判断题(正确打√,错误打×)

  6. 标准广播是无序的,有可能后面注册的接收器反而比前面注册的接收器先收到广播。( )

  7. 通过setPriority方法设置优先级,优先级越小的接收器,越先收到有序广播。( )

  8. 普通应用能够通过静态注册方式来监听系统广播。( )

  9. 闹钟管理器AlarmManager的setRepeating方法保证能够按时发送广播。( )

  10. 旋转手机使得屏幕由竖屏变为横屏,App默认会重新加载整个页面(先销毁原页面再创建新页面)。

    ( )

三、选择题

  1. 在接收器内部调用( )方法,就会中断有序广播。

A.abortBroadcast B.cancelBroadcast C.interrupt D.sendBroadcast

  1. android.permission.VIBRATE表达的是( )权限。

A.呼吸灯B.麦克风C.闹钟D.震动器

  1. 网络类型( )表示手机的数据连接(含2G/3G/4G/5G)。

A.TYPE_WIFI B.TYPE_MOBILE C.TYPE_WIMAX D.TYPE_ETHERNET

  1. 网络状态( )表示已经连接。

A.CONNECTING B.CONNECTED C.SUSPENDED D.DISCONNECTED

5.( )属于configChanges属性配置的显示变更豁免情况。

A.orientation B.screenLayout C.screenSize D.keyboard

四、简答题

请简要描述收发标准广播的主要步骤。

五、动手练习

请上机实验下列3项练习:

  1. 通过设置不同的优先级,实现有序广播的正确收发。
  2. 通过监听网络变更广播,判断当前位于哪种网络。
  3. 通过监听回到桌面广播,实现App的画中画模式。

第10章 自定义控件

本章介绍App开发中的一些自定义控件技术,主要包括:视图是如何从无到有构建出来的、如何改造已 有的控件变出新控件、如何通过持续绘制实现简单动画。然后结合本章所学的知识,演示了一个实战项 目“广告轮播”的设计与实现。

1 0.1 视图的构建过程

本节介绍了一个视图的构建过程,包括:如何编写视图的构造方法,4种构造方法之间有什么区别;如何 测量实体的实际尺寸,包含文本、图像、线性视图的测量办法;如何利用画笔绘制视图的界面,并说明

onDraw方法与dispatchDraw方法的先后执行顺序。

10.1.1 视图的构造方法

Android自带的控件往往外观欠佳,开发者常常需要修改某些属性,比如按钮控件Button就有好几个问题,其一字号太小,其二文字颜色太浅,其三字母默认大写。于是XML文件中的每个Button节点都得添加textSize、textColor、textAllCaps 3个属性,以便定制按钮的字号、文字颜色和大小写开关,就像下面这样:

如果只是一两个按钮控件倒还好办,倘若App的许多页面都有很多Button,为了统一按钮风格,就得给 全部Button节点都加上这些属性。要是哪天产品大姐心血来潮,命令所有按钮统统换成另一种风格,如 此多的Button节点只好逐个修改过去,令人苦不堪言。为此可以考虑把按钮样式提炼出来,将统一的按 钮风格定义在某个地方,每个Button节点引用统一样式便行。为此打开res/values目录下的styles.xml, 在resources节点内部补充如下所示的风格配置定义:

接着回到XML布局文件中,给Button节点添加形如“style=“@style/样式名称””的引用说明,表示当前控 件将覆盖指定的属性样式,添加样式引用后的Button节点如下所示:

(完整代码见chapter10\src\main\res\layout\activity_custom_button.xml)

[外链图片转存中…(img-NcmNkXPh-1676725568204)]运行测试App,打开按钮界面如图10-1所示,对比默认的按钮控件,可见通过style引用的按钮果然变了个模样。以后若要统一更换所有按钮的样式,只需修改styles.xml中的样式配置即可。

图10-1 通过style属性设置样式的按钮界面

然而样式引用仍有不足之处,因为只有Button节点添加了style属性才奏效,要是忘了添加style属性就不 管用了,而且样式引用只能修改已有的属性,不能添加新属性,也不能添加新方法。若想更灵活地定制 控件外观,就要通过自定义控件实现了。

自定义控件听起来很复杂的样子,其实并不高深,不管控件还是布局,它们本质上都是一个Java类,也拥有自身的构造方法。以视图基类View为例,它有4个构造方法,分别是:

  1. 带一个参数的构造方法public View(Context context),在Java代码中通过new关键字创建视图对象时,会调用这个构造方法。
  2. 带两个参数的构造方法public View(Context context, AttributeSet attrs),在XML文件中添加视图节点时,会调用这个构造方法。
  3. 带3个参数的构造方法public View(Context context, AttributeSet attrs, int defStyleAttr),采取默认的样式属性时,会调用这个构造方法。如果defStyleAttr填0,则表示没有默认的样式。
  4. 带4个参数的构造方法public View(Context context, AttributeSet attrs, int defStyleAttr, int

defStyleRes),采取默认的样式资源时,会调用这个构造方法。如果defStyleRes填0,则表示无样式资 源。

以上的4种构造方法中,前两种必须实现,否则要么不能在代码中创建视图对象,要么不能在XML文件中添加视图节点;至于后两种构造方法,则与styles.xml中的样式配置有关。先看带3个参数的构造方法, 第3个参数defStyleAttr的意思是指定默认的样式属性,这个样式属性在res/values下面的attrs.xml中配 置,如果values目录下没有attrs.xml就创建该文件,并填入以下的样式属性配置:

以上的配置内容表明了属性名称为customButtonStyle,属性格式为引用类型reference,也就是实际样 式在别的地方定义,这个地方便是styles.xml中定义的样式配置。可是customButtonStyle怎样与

styles.xml里的CommonButton样式关联起来呢?每当开发者创建新项目时,AndroidManifest.xml的

application节点都设置了主题属性,通常为android:theme=“@style/AppTheme”,这个默认主题来自于styles.xml的AppTheme,打开styles.xml发现文件开头的AppTheme配置定义如下所示:

原来App的默认主题源自Theme.AppCompat.Light.DarkActionBar,其中的Light表示这是亮色主题,

DarkActionBar表示顶部标题栏是暗色的,内部的3个color项指定了该主题采用的部分颜色。现在给

AppTheme添加一项customButtonStyle,并指定该项的样式为@style/CommonButton,修改后的

AppTheme配置示例如下:

接着到Java代码包中编写自定义的按钮控件,控件代码如下所示,注意在defStyleAttr处填上默认的样式 属性R.attr.customButtonStyle。

(完整代码见chapter10\src\main\java\com\example\chapter10\widget\CustomButton.java)

然后打开测试界面的XML布局文件activity_custom_button.xml,添加如下所示的自定义控件节点

CustomButton:

(完整代码见chapter10\src\main\res\layout\activity_custom_button.xml)

  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Android开发入门实战源码是指用于学习Android开发的一些示例代码和实际项目代码。在学习Android开发的过程中,源码是非常重要的资源之一。通过阅读和分析源码,能够更好地理解Android框架和API,培养编程思路和设计模式。 对于初学者而言,进入Android开发领域前,需要学习Java编程语言的基础知识,并且掌握Android开发框架以及其相关工具。其中,参考一些较为成熟的开源学习项目及其源码,可以让初学者更好地入门Android开发。 而对于有一定开发经验的开发者而言,获取一些优秀的实战项目源码,可以帮助他们更好地了解Android应用开发中的最佳实践方法和开发技巧。同时,在使用源码时,也可以根据自己的需求和项目特点进行个性化的定制和修改。 总的来说,学习和使用Android开发入门实战源码是一个不断积累、提高自己开发能力的过程。希望广大开发者可以善用这些资源,不断努力,打造更好的Android应用。 ### 回答2: Android开发入门实战源码是一套非常优秀的学习材料,能够为初学者提供全面、系统的学习体验。该源码包含了多个项目和应用程序的完整代码,涉及了Android的基础开发、网络编程、图像处理、多媒体等多个方面,让开发者能够较为顺畅地进入Android开发的领域。 对于初学者来说,这套源码能够提供多个通俗易懂的案例,帮助他们快速掌握Android开发的核心思想和基础知识,熟悉开发环境,熟悉常用类库和API,从而顺利完成初步开发任务。 对于高级开发者来说,该源码提供了许多高级示例和代码实现,让他们能够更加深入了解Android的高级编程思路和技术,如图片缓存、网络请求、自定义视图等问题,加强开发技能和经验,提升开发能力和水平。 总之,Android开发入门实战源码的作用是非常重要的,无论是学习者还是开发者都能够从中获得巨大的益处,扩展知识面,提高技术水平,缩短学习和开发时间。因此,学习者和开发者都应该加以利用,进一步提高自己的实践和应用能力。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值