Android编程权威指南 笔记(一)

Android基础知识

Activity,负责管理用户与屏幕的交互。应用的功能是通过编写一个个Activity子类来实现的。

布局(Layout)定义了一系列用户界面对象以及它们显示在屏幕上的位置。组成布局的定义保存在XML文件中。

com.bignerdranch.android.geoquiz包名
包名遵循了“DNS反转”约定,亦即将企业组织或公司的域名反转后,在尾部附加上应用名称。遵循此约定可以保证包名的唯一性,这样,同一设备和Google Play商店的各类应用就可以区分开来。

QuizActivity类
注意类名的Activity后缀。尽管不是必需的,但我们建议遵循这一好的命名约定。

activity_quiz.xml布局文件名
为体现布局与activity间的对应关系,布局名称(Layout Name)会自动更新为activity_quiz。布局的命名规则是:将activity名称的单词顺序颠倒过来并全部转换为小写字母,然后在单词间添加下划线。

应用activity的布局默认定义了两个组件(widget): RelativeLayout和TextView。组件是组成用户界面的构造模块。组件可以显示文字或图像、与用户交互,甚至是布置屏幕上的其他组件。按钮、文本输入控件和选择框等都是组件。

Android SDK内置了多种组件,通过配置各种组件可获得所需的用户界面及行为。每一个组件是View类或其子类(如TextView或Button)的一个具体实例。

需要特别注意的是,开发工具无法校验布局XML内容,请避免输入或拼写错误。

XML文件根元素,必须指定Android XML资源文件的命名空间属性为http://schemas.android.com/apk/res/android。

ViewGroup组件是一个包含并配置其他组件的特殊组件。

ViewGroup子类还包括LinearLayout、FrameLayout、TableLayout和RelativeLayout。

几乎每类组件都需要android:layout_width和android:layout_height属性。

match_parent:视图与其父视图大小相同。
wrap_content:视图将根据其内容自动调整大小。

以前还有一个fill_parent属性值,等同于match_parent,目前已废弃不用。

android:padding=”24dp”属性告诉组件在决定大小时,除内容本身外,还需增加额外指定量的空间。

dp即density-independent pixel,指与设备无关的像素。

android:orientation属性是两个LinearLayout组件都具有的属性,决定了二者的子组件是水平放置的还是垂直放置的。

TextView与Button组件具有android:text属性。该属性指定组件显示的文字内容。

android:text属性值不是字符串字面值,而是对字符串资源(string resources)的引用。

字符串资源包含在一个独立的名为strings的XML文件中,虽然可以硬编码设置组件的文本属性, 如android:text=”True”, 但这通常不是个好方法。

每个项目都包含一个名为strings.xml的默认字符串文件。

在GeoQuiz项目的任何XML文件中,只要引用到@string/false_button,应用运行时,就会得到文本“ False”。

字符串文件默认被命名为strings.xml,当然也可以按个人喜好任意取名。一个项目也可以有多个字符串文件。只要这些文件都放置在res/values/目录下,并且含有一个resources根元素,以及多个string子元素,字符串定义即可被应用找到并得到正确使用。

将XML定义的布局转换成视图对象:

1
2
3
4
5
6
7
8
public class QuizActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_quiz);
}
}

activity子类的实例创建后, onCreate(Bundle)方法将会被调用。

activity创建后,它需要获取并管理属于自己的用户界面。获取activity的用户界面,可调用以下Activity方法:

1
public void setContentView(int layoutResID);

通过传入布局的资源ID参数,该方法生成指定布局的视图并将其放置在屏幕上。布局视图生成后,布局文件包含的组件也随之以各自的属性定义完成实例化。

布局是一种资源。 资源是应用非代码形式的内容,比如图像文件、音频文件以及XML文件等。项目的所有资源文件都存放在目录res的子目录下。

可使用资源ID在代码中获取相应的资源。 activity_quiz.xml文件定义的布局资源ID为 R.layout.activity_quiz。

在包浏览器展开目录gen,找到并打开R.java文件,即可看到GeoQuiz应用当前所有的资源ID。

R.java文件在Android项目编译过程中自动生成,遵照该文件头部的警示,请不要尝试修改该文件的内容。

要为组件生成资源ID,请在定义组件时为其添加上android:id属性。

请注意android:id属性值前面有一个+标志,而android:text属性值则没有,这是因为我们将要创建资源ID,而对字符串资源只是做了引用。

1
2
private Button mTrueButton;
private Button mFalseButton;

请注意新增的两个成员(实例)变量名称的m前缀。该前缀是Android编程所遵循的命名约定。

自动完成类包导入:

  • Command+Shift+O(Mac系统)
  • Ctrl+Shift+O(Windows和Linux系统)

引用组件:

1
mTrueButton = (Button)findViewById(R.id.true_button);

Android应用属于典型的事件驱动类型。

应用等待某个特定事件的发生,也可以说该应用正在“监听”特定事件。为响应某个事件而创建的对象叫做监听器(listener)。

对按钮的单击事件设置监听器:

1
2
3
4
5
6
7
8
9
10
mTrueButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 注意此处应输入的参数是QuizActivit.this,不要想当然地直接输入this作为参数。
// 因为匿名类的使用,这里的this指的是监听器View.OnClickListener。
Toast.makeText(QuizActivity.this,
R.string.incorrect_toast,
Toast.LENGTH_SHORT).show();
}
});

本书所有的监听器都作为匿名内部类来实现。这样做的好处有二。其一,在大量代码块中,监听器方法的实现一目了然;其二,匿名内部类的使用只出现在一个地方,因此可以减少一些命名类的使用。

Android的toast指用来通知用户的简短弹出消息,但无需用户输入或做出任何操作。

如果使用的是Windows系统,需要将内存选项值从1024改为512,这样虚拟设备才能正常运行。

右击GeoQuiz项目文件夹。在弹出的右键菜单中,选择Run As → Android Application菜单项。 Eclipse会自动找到新建的虚拟设备,安装应用包(APK),然后启动并运行应用。在此过程中,如果Eclipse询问是否使用LogCat自动监控,选择“ Yes”。

我们已经知道在项目文件发生变化时,无需使用命令行工具, Eclipse便会自动进行编译。

为了让.apk应用能够在模拟器上运行, .apk文件必须以debug key签名。(分发.apk应用给用户时,应用必须以release key签名。

如需了解更多有关编译过程的信息,可参考Android开发文档http://developer.android.com/tools/publishing/preparing.html。)

当QuizActivity类的onCreate(…)方法调用setContentView(…)方法时,QuizActivity使用LayoutInflater类实例化定义在布局文件中的每一个View对象。

除了在XML文件中定义视图的方式外,也可以在activity里使用代码的方式创建视图类。但应用展现层与逻辑层分离有很多好处,其中最主要的优点是可以利用SDK内置的设备配置改变。

编译功能已整合到正在使用的ADT开发插件中,插件调用了aapt等Android的标准编译工具,但编译过程本身是由Eclipse来管理的。

有时,出于种种原因,可能需要脱离Eclipse进行代码编译。最简单的方法是使用命令行编译工具。当前最流行的两大工具是maven和ant。

Android与MVC设计模式

命名规范:

  • m作为成员变量的前缀
  • s作为静态成员变量的前缀

自动生成getter和setter函数:

  • 打开Eclipse首选项对话框(Mac用户选择Eclipse菜单, Windows用户选择Windows → Preferences菜单)。在Java选项下选择Code Style设置前缀等。
  • 右击构造方法后方区域,选择Source → Generate Getters And Setters…菜单项。点击Select All按钮,为每个变量都生成getter与setter方法。

Android应用是基于模型-视图-控制器(Model-View-Controller,简称MVC)的架构模式进行设计的。

MVC设计模式表明,应用的任何对象,归根结底都属于模型对象、视图对象以及控制对象中的一种。

  • 模型对象存储着应用的数据和业务逻辑。模型对象不关心用户界面,它存在的唯一目的就是存储和管理应用数据。
  • 视图对象知道如何在屏幕上绘制自己以及如何响应用户的输入,如用户的触摸等。凡是能够在屏幕上看见的对象,就是视图对象。GeoQuiz应用的视图层是由activity_quiz.xml文件中定义的各类组件构成的。
  • 控制对象包含了应用的逻辑单元,是视图与模型对象的联系纽带。响应由视图对象触发的各类事件,此外还用来管理模型对象与视图层间的数据流动。在Android的世界里,控制器通常是Activity、 Fragment或Service的一个子类。

模型对象与视图对象不直接交互。控制器作为它们间的联系纽带,接收来自对象的消息,然后向其他对象发送操作指令。

以Java类的方式组织代码有助于我们从整体视角设计和理解应用。这样,我们就可以按类而不是一个个的变量和方法去思考设计开发问题。

同样,把Java类以模型、视图和控制层进行分类组织,也有助于我们设计和理解应用。这样,我们就可以按层而非一个个类来考虑设计开发了。

如果遇到设备无法识别的问题,首先尝试重置adb。

还有一个low-density–ldpi目录。不过,目前大多数低像素密度的设备基本已停止使用,可以不用理会。)

在正式发布的应用里,为不同dpi的设备提供定制化的图片非常重要。

项目中的所有图片资源都会随应用安装在设备里, Android操作系统知道如何为不同设备提供最佳匹配。

应用运行时,操作系统知道如何在特定的设备上显示匹配的图片。

以@string/开头的定义是引用字符串资源。以@drawable/开头的定义是引用drawable资源。

右键单击包浏览器中的项目,选择Copy选项,然后再右键单击选择Paste选项。 Eclipse会提示为新项目命名。输入新项目名称后确认完成项目复制。

TextView也是View的子类,因此就如同Button一样,可为TextView设置View.OnClickListener监听器。

将按钮组件替换成ImageButton后, Eclipse会警告说找不到android:contentDescription属性定义。该属性为视力障碍用户提供方便,在为其设置文字属性值后,如果用户设备的可访问性选项作了相应设置,那么当用户点击图形按钮时,设备便会读出属性值的内容。

Activity的生命周期

每个Activity实例都有其生命周期。在其生命周期内, activity在运行、暂停和停止三种可能的状态间进行转换。

生命周期函数:

  • onCreate()
  • onDestroy()
  • onStart()
  • onStop()
  • onResume()
  • onPause()

通常, activity通过覆盖onCreate(…)方法来准备以下用户界面的相关工作:

  • 实例化组件并将组件放置在屏幕上(调用方法setContentView(int));
  • 引用已实例化的组件;
  • 为组件设置监听器以处理用户交互;
  • 访问外部模型数据。

Android内部的android.util.log类能够发送日志信息到系统级别的共享日志中心。

1
Log.d(TAG, "onCreate(Bundle) called");

要想打开LogCat,可选择Window → Show View → Other…菜单项。在随后弹出的对话框中,展开Android文件夹找到并选择LogCat,然后单击OK按钮。

单击设备的后退键,相当于通知Android系统“我已完成acitivity的使用,现在不需要它了。” 接到指令后,系统立即销毁了acitivity。这实际是Android系统节约使用设备有限资源的一种方式。

单击主屏幕键,相当于通知Android“我去别处看看,稍后可能回来。” 此时,为快速响应随时返回应用, Android只是暂停当前activity而并不销毁它。需要注意的是,停止的activity能够存在多久,谁也无法保证。如果系统需要回收内存,它将首先销毁那些停止的activity。

旋转设备会改变设备配置(device configuration)。设备配置是用来描述设备当前状态的一系列特征。这些特征包括:屏幕的方向、屏幕的密度、屏幕的尺寸、键盘类型、底座模式以及语言,等等。

在运行时配置变更(runtime configuration change)发生时,可能会有更合适的资源来匹配新的设备配置。

注意,两个布局文件必须具有相同的文件名,这样它们才能以同一个资源ID被引用。

访问Android开发网页http://developer.android.com/guide/topics/resources/providing-resources.html,可查看Android的配置修饰符列表以及配置修饰符代表的设备配置信息。

设备处于水平方向时, Android会找到并使用res/layout-land目录下的布局资源。其他情况下,会默认使用res/layout目录下的布局资源。

设备一经旋转, Android需要销毁当前的QuizActivity,然后再新建一个QuizActivity来完成QuizActivity.onCreate(…)方法的调用,从而实现使用最佳资源匹配新的设备配置。

请记住,只要在应用运行中设备配置发生了改变, Android就会销毁当前activity,然后再新建一个activity。

在设备运行中发生配置变更时,如设备旋转,需采用某种方式保存以前的数据。覆盖以下Activity方法就是一种实现方式:

1
protected void onSaveInstanceState(Bundle outState)

该方法通常在onPause()、 onStop()以及onDestroy()方法之前由系统调用。

可通过覆盖onSaveInstanceState(…)方法,将一些数据保存在Bundle中,然后在onCreate(…)方法中取回这些数据。

注意,我们在Bundle中存储和恢复的数据类型只能是基本数据类型(primitive type)以及可以实现Serializable接口的对象。创建自己的定制类时,如需在onSaveInstanceState(…)方法中保存类对象,记得实现Serializable接口。

Android从不会为了回收内存,而去销毁正在运行的activity。 activity只有在暂停或停止状态下才可能会被销毁。

常见的做法是,覆盖onSaveInstanceState(…)方法,将数据暂存到Bundle对象中,覆盖onPause()方法处理其他需要处理的事情。

用户按了后退键后,系统会彻底销毁当前的activity。此时,暂存的activity记录同时被清除。此外,系统重启或长时间不使用activity时,暂存的activity记录通常也会被清除。

使用String.format对输出日志信息进行格式化操作,以满足个性化的使用要求。

Android应用的调试

两种不同的代码跟踪调试方法:

  • 记录栈跟踪诊断性日志;
  • 利用调试器设置断点调试。

没有哪种方法更好些,它们各有所长。通过实际应用中的比较,也许我们会有自己的偏爱。

栈跟踪记录的优点是,在同一日志记录中可以看到多处的栈跟踪信息;缺点是,必须学习如何添加日志记录方法,重新编译、运行并跟踪排查应用问题。相对而言,代码调试的方法更为方便。以调试模式运行应用后(选择Debug As → Android Application菜单项),可在应用运行的同时,在不同的地方设置断点,寻找解决问题的线索。

Android Lint是Android应用代码的静态分析器。实际上,它是无需代码运行,就能够进行代码错误检查的特殊程序。Android Lint深入检查代码,找出编译器无法发现的问题。 Android Lint检查出的问题通常值得关注。

右键单击GeoQuiz项目,选择Android Tools → Run Lint: Check for Common Errors菜单项打开 Lint Warnings视图。

如果Eclipse无法生成新的R.java文件,我们可以删除整个gen目录。 Eclipse会重新编译项目并创建一个新的gen目录,内含功能完备的R类。

第二个activity

启动activity意味着请求操作系统创建新的activity实例并调用它的onCreate(Bundle)方法。

如不习惯GUI的开发方式,可不使用布局向导。例如,要创建新布局文件,可直接在res/layout目录新建activity_cheat.xml文件,然后刷新res/layout目录让Eclipse识别它。记住,唯一必须使用的开发向导是新建Android应用向导。

应用的所有activity都必须在manifest配置文件中声明,这样操作系统才能够使用它们。

1
2
3
<activity
android:name=".CheatActivity"
android:label="@string/app_name" />

这里的android:name属性是必需的。属性值前面的“.”可告知OS:在manifest配置文件头部包属性值指定的包路径下,可以找到activity的类文件。

activity调用startActivity(…)方法时,调用请求实际发给了操作系统。准确地说,该方法调用请求是发送给操作系统的ActivityManager。 ActivityManager负责创建Activity实例并调用其onCreate(…)方法。

1
2
Intent i = new Intent(QuizActivity.this, CheatActivity.class);
startActivity(i);

intent对象是component用来与操作系统通信的一种媒介工具。除了Activity,还有其他一些component: service、 broadcast receiver以及content provider。

Context对象告知ActivityManager在哪一个包里可以找到Class对象。

如通过指定Context与Class对象,然后调用intent的构造方法来创建Intent,则创建的是显式intent。同一应用中,我们使用显式intent来启动activity。

一个应用的activity如需启动另一个应用的activity,可通过创建隐式intent来处理。

extra信息可以是任意数据,它包含在Intent中,由启动方activity发送出去。接受方activity接收到操作系统转发的intent后,访问并获取包含在其中的extra数据信息。

Intent.putExtra(…)方法。

使用包名来修饰extra数据信息,这样可以避免来自不同应用的extra间发生命名冲突。

getBooleanExtra(…)方法。

若需要从子activity获取返回信息时,可调用以下Activity方法:

1
public void startActivityForResult(Intent intent, int requestCode)
  • Activity.RESULT_OK
  • Activity.RESULT_CANCELED

在没有调用setResult(…)方法的情况下,如果用户单击了后退按钮,父activity则会收到Activity.RESULT_CANCELED的结果代码。

为什么不在接收信息的父activity中定义extra常量呢?这是因为,传入及传出extra针对CheatActivity定义了统一的接口。这样,如果在应用的其他地方使用CheatActivity,我们只需要关注使用定义在CheatActivity中的那些常量。

在用户单击后退键回到QuizActivity时, ActivityManager调用父activity的以下方法:

1
protected void onActivityResult(int requestCode, int resultCode, Intent data)

在桌面启动器中点击GeoQuiz应用时,操作系统并没有启动应用,而只是启动了应用中的一个activity。确切地说,它启动了应用的launcher activity。在GeoQuiz应用中, QuizActivity就是它的launcher activity。

在CheatActivity中调用Activity.finish()方法同样可以将CheatActivity从栈里弹出。

ActivityManager维护着一个非特定应用独享的回退栈。所有应用的activity都共享该回退栈。这也是将ActivityManager设计成操作系统级的activity管理器来负责启动应用activity的原因之一。不局限于单个应用,回退栈作为一个整体共享给操作系统及设备使用。

Android SDK 版本与兼容

生产商往往更愿意投入资源推出新设备,而不是保持旧设备的更新升级。

老设备的硬件有时无法满足运行Android新版本。

应用开发时,不同尺寸设备的处理要比想象中的简单。布局系统为编程适配做了很好的工作。不过,对于同样运行着Android系统的Google TV,由于UI差异太大,因此通常需要针对它开发单独的应用。

Android的“ SDK版本”和“ API级别”代表同一意思,可以交替使用。

manifest是操作系统用来与应用交互的元数据。以最低版本设置值为标准,操作系统会拒绝将应用安装在系统版本低于标准的设备上。

目标版本的设定值可告知Android:应用是设计给哪个API级别去运行的。大多数情况下,目标版本即最新发布的Android版本。

降低SDK目标版本可以保证的是,即便在高于目标版本的设备上,应用仍然可以正常运行,且运行行为仍和目标版本保持一致。

Android的特色功能是通过SDK中的类和方法展现的。

编译目标的最佳选择为最新的API 级别。

Google API包括Android API以及Google附加API(即支持使用Google地图服务的重要API)。

受益于Android Lint的不断改进,最终,当新版本API代码在老版本系统上运行时,可能存在的问题在运行时就被捕获了。如果使用了高版本系统API中的代码, Android Lint会提示编译错误。

Android开发者文档是优秀而丰富的信息来源。文档分为三大部分,即设计、 开发和发布。 设计部分的文档包括应用UI设计的模式和原则。 开发部分包括SDK文档和培训资料。 发布部分告知我们如何在Google Play商店上或通过开放发布模式准备并发布应用。有机会的话,一定要仔细研读这些资料。

只有在应用运行时才能知道设备的编译版本。

UI fragment与fragment管理器

UI设计具有灵活性是以上假设情景的共同点。即根据用户或设备的需要, activity界面可以在运行时组装,甚至重新组装。

采用fragment而不是activity进行应用的UI管理,可绕开Android系统activity规则的限制。

管理用户界面的fragment又称为UI fragment。

activity视图含有可供fragment视图插入的位置。如果有多个fragment要插入, activity视图也可提供多个位置。

利用一个个构建块,很容易做到构建分页界面、动画侧边栏界面等更多其他定制界面。

CrimeFragment的实例将通过一个名为CrimeActivity的activity来托管。

我们可以暂时把托管理解成activity在其视图层级里提供一处位置用来放置fragment的视图。Fragment本身不具有在屏幕上显示视图的能力。因此,只有将它的视图放置在activity的视图层级结构中, fragment视图才能显示在屏幕上。

FrameLayout组件为CrimeFragment要显示的视图安排了存放位置。

为满足平板设备的UI灵活性设计要求, Fragment被引入到API11级中。

对于fragment来说,保证向后兼容相对比较容易,仅需使用Android支持库中的fragment相关类即可。支持库位于libs/android-support-v4.jar内。

UUID

1
mId = UUID.randomUUID();

为托管UI fragment, activity必须做到:

  • 在布局中为fragment的视图安排位置;
  • 管理fragment实例的生命周期。

fragment的生命周期方法是由托管activity而不是操作系统调用的。操作系统无从知晓activity用来管理视图的fragment。 fragment的使用是activity自己内部的事情。

在activity中托管一个UI fragment,有如下两种方式:

  • 添加fragment到activity布局中
  • 在activity代码中添加fragment

添加fragment到activity布局中,就等同于将fragment及其视图与activity的视图绑定在一起,且在activity的生命周期过程中,无法切换fragment视图。

FrameLayout是服务于CrimeFragment的容器视图。注意容器视图是通用性视图,不局限于CrimeFragment类。

但创建和配置fragment视图是通过另一个fragment生命周期方法来完成的:

1
2
public View onCreateView(LayoutInflater inflater, ViewGroup parent,
Bundle savedInstanceState)
1
2
3
4
5
6
7
8
9
public class CrimeFragment extends Fragment {
...
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup parent,
Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_crime, parent, false);
return v;
}
}

以上代码中第二个参数是视图的父视图,通常我们需要父视图来正确配置组件。第三个参数告知布局生成器是否将生成的视图添加给父视图。这里,我们传入了false参数,因为我们将通过activity代码的方式添加视图。

Fragment类引入到Honeycomb时,为协同工作,Activity类被更改为含有FragmentManager类。 FragmentManager类负责管理fragment并将它们的视图添加到activity的视图层级结构中。

fragment事务被用来添加、移除、附加、分离或替换fragment队列中的fragment。

使用容器视图资源ID去识别UI fragment已被内置在FragmentManager的使用机制中。

为什么队列中已经有fragment存在了呢?activity被销毁时,它的FragmentManager会将fragment队列保存下来。这样,activity重建时,新的FragmentManager会首先获取保存的队列,然后重建fragment队列,从而恢复到原来的状态。

添加fragment供FragmentManager管理时, onAttach(Activity)、 onCreate(Bundle)以及 onCreateView(…)方法会被调用。

向处于运行状态的activity中添加fragment时,以下fragment生命周期方法会被依次调用:onAttach(Activity)、 onCreate(Bundle)、 onCreateView(…)、 onActivityCreated(Bundle)、onStart(),以及onResume()方法。

但fragment方法究竟是在activity方法之前还是之后调用的这一点是无法保证的。

不值得为使用fragment还是activity而伤脑筋,相信我们,总是使用fragment!

使用布局与组件创建用户界面

禁用按钮可以保证它不响应用户的单击事件。

CheckBox是CompoundButton的子类。

样式(style)是XML资源文件,含有用来描述组件行为和外观的属性定义。

将属性定义添加并保存在res/values/目录下的样式文件中,然后在布局文件中以@style/my_own_style(样式文件名)的形式引用它们。

主题是各种样式的集合。从结构上来说,主题本身也是一种样式资源,只不过它的属性指向了其他样式资源。

使用主题属性引用,可将预定义的应用主题样式添加给指定组件。例如,在fragment_crime.xml文件中,样式属性值?android:listSeparatorTextViewStyle的使用就是一个很好的例子。

使用主题属性引用,相当于告知Android运行资源管理器:“在应用主题里找到名为listSeparatorTextViewStyle的属性。该属性指向其他样式资源,请将其资源的值放在这里”。使用主题属性引用,可以确保组件在应用中拥有正确一致的界面观感。

Android提供了密度无关的尺寸单位(density-independent dimension units)。使用这种单位,可在不同屏幕密度的设备上获得同样大小的尺寸。无需麻烦的转换计算,应用运行时, Android会自动将这种单位转换成像素单位。

dp - density-independent pixel
密度无关像素。在设置边距、内边距或任何不打算按像素值指定尺寸的情况下,通常都使用dp这种单位。 1dp单位在设备屏幕上总是等于1/160英寸。使用dp的好处是,无论屏幕密度如何,总能获得同样的尺寸。

sp - scale-independent pixel
缩放无关像素。这种像素会受用户字体偏好设置的影响。我们通常会使用sp来设置屏幕上的字体大小。

pt、 mm、 in
类似于dp的缩放单位。允许以点(1/72英寸)、毫米或英寸为单位指定用户界面尺寸。但在实际开发中不建议使用这些单位,因为并非所有设备都能按照这些单位进行正确的尺寸缩放配置。

“48dp调和”设计原则。访问网址http://developer.android.com/design/index.html,可查看Android所有的开发设计原则。

名称以layout_开头的属性则作用于组件的父组件。我们将这些属性统称为布局参数。它们会告知父布局如何在内部安排自己的子元素。

属性android:padding告诉组件:在绘制组件自身时,要比所含内容大多少。

android:layout_weight属性告知LinearLayout如何进行子组件的布置安排。

在决定子组件视图的宽度时, LinearLayout使用的是layout_width与layout_weight参数的混合值。

LinearLayout依据layout_weight属性值进行额外的空间分配。

如想让LinearLayout分配完全相同的宽度给各自的视图,该如何处理呢?很简单,只需设置各组件的layout_width属性值为0dp以避开第一步的空间分配就可以了,这样LinearLayout就会只考虑使用layout_weight属性值来完成所需的空间分配了。

如果一个组件只存在于一个布局上,则需先在代码中进行空值检查,确认当前方向的组件存在后,再调用相关方法。

定义在水平或竖直布局文件里的同一组件必须具有同样的android:id属性,这样代码才能引用到它。

调用Date对象的toString()方法,可获得一个时间戳。

android.text.format.DateFormat类。

使用ListFragment显示列表

ListFragment是Fragment的子类。

应用能够在内存里存在多久,单例就能存在多久,因此将对象列表保存在单例里可保持crime数据的一直存在,不管activity、 fragment及它们的生命周期发生什么变化。

CrimeLab类的构造方法需要一个Context参数。这在Android开发里很常见,使用Context参数,单例可完成启动activity、获取项目资源,查找应用的私有存储空间等任务。

getApplicationContext()方法。

application context是针对应用的全局性Context。任何时候,只要是应用层面的单例,就应该一直使用application context。

ListFragment类默认实现方法已生成了一个全屏ListView布局。

当ListView没有内容可以显示时, ListFragment会通过内置的ListView显示一个圆形进度条。

ListView是ViewGroup的子类,每一个列表项都是作为ListView的一个View子对象显示的。

比较聪明的做法是在需要显示的时候才创建视图对象。即当ListView需要显示某个列表项时,它才会去申请一个可用的视图对象。

setListAdapter(ListAdapter)是一个ListFragment类的便利方法,使用它可为CrimeListFragment管理的内置ListView设置adapter。

我们在adapter的构造方法中指定的布局(android.R.layout.simple_list_item_1)是Android SDK提供的预定义布局资源。该布局拥有一个TextView根元素。

无论用户是单击硬按键还是软按键,抑或是手指的触摸,都会触发onListItemClick(…)方法。

总而言之,在布局文件里,一个组件必须首先被定义,这样,其他组件才能在定义时使用它的资源ID。

#使用fragment argument

从fragment中启动activity的实现方式,基本等同于从activity中启动另一activity的实现方式。我们调用Fragment.startActivity(Intent)方法,该方法在后台会调用对应的Activity方法。

由于UUID是Serializable对象,我们调用了可接受Serializable对象的putExtra(…)方法,即putExtra(String, Serializable)方法。

每个fragment实例都可附带一个Bundle对象。该bundle包含有key-value对,我们可以如同附加extra到Activity的intent中那样使用它们。

1
2
3
4
Bundle args = new Bundle();
args.putSerializable(EXTRA_MY_OBJECT, myObject);
args.putInt(EXTRA_MY_INT, myInt);
args.putCharSequence(EXTRA_MY_STRING, myString);

附加argument bundle给fragment,需调用Fragment.setArguments(Bundle)方法。注意,该任务必须在fragment创建后、添加给activity前完成。

Android开发者遵循的习惯做法是:添加名为newInstance()的静态方法给Fragment类。使用该方法,完成fragment实例及bundle对象的创建,然后将argument放入bundle中,最后再附加给fragment。托管activity需要fragment实例时,需调用newInstance()方法,而非直接调用其构造方法。

使用ViewPager

为UI添加ViewPager后,用户可滑动屏幕,切换查看不同列表项的明细页面。

ViewPager类来自于支持库。与Fragment类不同, ViewPager只存在于支持库中。

AdapterView需借助于Adapter才能提供视图。同样地,ViewPager也需要PagerAdapter的支持。

ViewPager默认加载当前屏幕上的列表项,以及左右相邻页面的数据,从而实现页面滑动的快速切换。可通过调用setOffscreenPageLimit(int)方法,定制预加载相邻页面的数目。

操作栏(旧版本设备上叫标题栏)。

使用OnPageChangeListener监听ViewPager当前显示页面的状态变化。页面状态发生变化时,可将Crime实例的标题设置给CrimePagerActivity的标题。

onPageScrolled(…)方法可告知我们页面将会滑向哪里。

通常来说,使用FragmentStatePagerAdapter更节省内存。另一方面,如果用户界面只需要少量固定的fragment,则FragmentPagerAdapter是个安全且合适的选择。最常见的例子为分页显示用户界面。

对话框

对话框既能引起用户的注意也可接收用户的输入。在提示重要信息或提供用户选项方面,它都非常有用。

实际开发中, AlertDialog类是一个经常会用到的多用途Dialog子类。

不使用DialogFragment,也可显示AlertDialog视图,但Android开发原则不推荐这种做法。使用FragmentManager管理对话框,可使用更多配置选项来显示对话框。

Android有3种可用于对话框的按钮:positive按钮、negative按钮以及neutral按钮。用户点击positive按钮接受对话框展现信息。

在Froyo以及Gingerbread版本的设备上, positive按钮出现在对话框的最左端。而在较新版本设备上, positive按钮则出现在对话框的最右端。

调用AlertDialog.Builder.create()方法,返回已配置完成的AlertDialog实例。

DatePicker对象的初始化需整数形式的月、日、年。Date就是个时间戳,它无法直接提供整数形式的月、日、年。要想获得所需的整数数值,必须首先创建一个Calendar对象,然后用Date对象对其进行配置,即可从Calendar对象中取回所需信息。

父activity接收到Activity.onActivityResult(…)方法的调用后,其FragmentManager会调用对应fragment的Fragment.onActivityResult(…)方法。

手机的屏幕空间非常有限。因此,通常需要使用一个activity托管全屏的fragment界面,以显示用户输入要求。

使用MediaPlayer播放音频

MediaPlayer是一个支持音频及视频文件播放的Android类,可播放不同来源(本地或网络流媒体)、多种格式(如WAV、 MP3、 Ogg Vorbis、 MPEG-4以及3GPP)的多媒体文件。

音频文件将会放置在res/raw目录下。目录raw负责存放那些不需要Android编译系统特别处理的各类文件。项目中的res/raw目录并非默认存在,因此必须手工添加它。

AudioPlayer是我们编写的类,用于封装MediaPlayer类。也可选择不封装MediaPlayer类,而让HelloMoonFragment直接与MediaPlayer进行交互。不过,为保持代码的整洁与独立,我们推荐封装MediaPlayer类的设计。

除非调用MediaPlayer.release()方法,否则MediaPlayer将一直占用着音频解码硬件及其他系统资源。而这些资源是由所有应用共享的。 MediaPlayer类有一个stop()方法。 该方法可使MediaPlayer实例进入停止状态,等需要时再重新启动。不过,对于简单的音频播放应用,建议使用release()方法销毁实例,并在需要时进行重建。

只保留一个MediaPlayer实例,保留的时长即音频文件播放的时长。

在play(Context)方法的开头就调用stop()方法,可避免用户多次单击Play按钮创建多个MediaPlayer实例的情况发生。

HelloMoonFragment被销毁后, MediaPlayer仍可不停地播放,这是因为MediaPlayer运行在一个不同的线程上。

关于视频的播放, Android提供了多种实现方式。其一便是使用刚才讲到的MediaPlayer,而我们唯一要做的就是设置在哪里播放视频。

通常来说,使用VideoView实例播放视频更容易些。

fragment的保留

fragment的retainInstance属性值默认为false。这表明其不会被保留。因此,设备旋转时fragment会随托管activity一起销毁并重建。调用setRetainInstance(true)方法可保留fragment。已保留的fragment不会随activity一起被销毁。相反,它会被一直保留并在需要时原封不动的传递给新的activity。

对于已保留的fragment实例,其全部实例变量(如mPlayButton、 MPlayer和mStopButton)值也将保持不变,因此可放心继续使用。

保留的fragment利用了这样一个事实: 可销毁和重建fragment的视图,但无需销毁fragment自身。

为什么不保留每个fragment,或者默认设置fragment的retainInstance属性值为true? 这是因为Android似乎并不鼓励保留fragment。

如果activity是因操作系统需要回收内存而被销毁,则所有被保留的fragment也会被随之销毁。

如只需短暂保留数据,能应对设备配置改变就可以了,则保留fragment可以很轻松地解决问题。如果是保存对象,则更能体会使用保留fragment的便利。因为我们再也无需操心要保存的对象是否已实现Serializable接口了。

如需持久地保存数据,保留fragment的方式就行不通了。用户暂时离开应用后,如系统因回收内存需要销毁activity,则保留的fragment也会被随之销毁。

应用本地化

首先创建带有目标语言配置修饰符的资源子目录,然后将可选资源放入其中。Android资源系统会为我们处理其他后续工作。

中文的修饰符为-zh。res/values-zh/。

没有配置修饰符的资源是Android的默认资源。默认资源的提供非常重要。如果Android无法找到匹配设备配置的资源,而又没有默认资源可用时,应用将会崩溃。

尽管Android的drawable资源使用稍显复杂,但请记住一点:无需在res/drawable/目录下放置默认的drawable资源。

可以在同一资源目录上使用多个配置修饰符。创建一个名为values-zh-land的资源目录,为HelloMoon应用准备水平模式的中文字符串资源。

在同一资源目录上使用多个配置修饰符,各配置修饰符必须按照优先级别顺序排列。因此,values-zh-land是一个有效的资源目录名,而values-land-zh目录名则无效。

资源的名字只能由小写字母组成并且不能包含空格,一些正确命名的例子有:one_small_step.wav, app_name, armstrong_on_moon.jpg。

所有资源都必须保存在res/目录的子目录下。尝试在res/目录的根目录下保存资源将会导致编译错误。

res子目录的名字直接与Android编译过程绑定,因此无法随意进行更改。我们已看到过的子目录有drawable/、 layout/、 menu/、 raw/以及values/等。

Android会无视res/目录下的其他子目录。创建res/my_stuff可能不会导致错误发生,但Android不会使用放置在其中的任何资源。

存储与加载本地文件

Android设备上的所有应用都有一个放置在沙盒中的文件目录。将文件保存在沙盒中可阻止其他应用的访问、甚至是其他用户的私自窥探(当然,要是设备被root了的话,则用户可以随意获取任何数据)。

每个应用的沙盒目录都是设备/data/data目录的子目录,且默认以应用包命名。例如,CriminalIntent应用的沙盒目录全路径为: /data/data/com.bignerdranch.android.criminalintent。

除沙盒目录外,应用也可将文件保存在外部存储介质上,如常用的SD存储卡等。虽然文件甚至整个应用都可以存储到SD卡上。但出于安全考虑,通常不推荐这么做。这其中最重要的一个因素就是,外部存储上的数据存取并不仅仅局限于应用本身,也就是说,任何人都可以读取、写入以及删除这些数据。如果需要,也可以使用同样的API存取外部存储上的文件。

Android SDK内置了标准的org.json类包,我们可以利用其中的类和方法来创建和解析JSON文件。

应用读取文件的最便捷方式是使用Context类的I/O方法。这些方法可以返回标准的Java类实例,如java.io.File和java.io.FileInputStream。(Context类几乎是所有关键应用组件的超类,常见的几个应用组件有: Application、 Activity和Service。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class CriminalIntentJSONSerializer {
...
public void saveCrimes(ArrayList<Crime> crimes) throws JSONException, IOException {
// build an array in JSON
JSONArray array = new JSONArray();
for (Crime c : crimes)
array.put(c.toJSON());
// write the file to disk
Writer writer = null;
try {
OutputStream out = mContext.openFileOutput(mFilename, Context.MODE_PRIVATE);
writer = new OutputStreamWriter(out);
writer.write(array.toString());
} finally {
if (writer != null)
writer.close();
}
}
}

要打开文件并写入数据,需使用Context.openFileOutput(…)方法。该方法接受文件名以及文件操作模式参数,会自动将传入的文件名附加到应用沙盒文件目录路径之后,形成一个新路径,然后在新路径下创建并打开文件,等待数据写入。如选择手动获取私有文件目录并在其下创建和打开文件,记得总是使用Context.getFilesDir()替代方法。不过,如需创建不同使用权限的文件,还是少不了要使用openFileOutput(…)方法。

1
2
3
4
5
JSONObject json = new JSONObject();
json.put("id", mId.toString());
json.put("title", mTitle);
json.put("solved", mSolved);
json.put("date", mDate.getTime());

什么时点保存数据合适呢?适用于移动应用的一个普遍规则是:尽可能频繁地保存数据,尤其是用户数据修改行为发生时。既然修改crime记录后的数据更新都需CrimeLab类处理,那么最靠谱的就是在该类中将数据保存到文件中。

对于超频繁数据保存的应用来说,应考虑采用某种方式只保存修改过的数据,而不是每次都保存全部数据,比如说使用SQLite数据库等。

实际开发时,如文件保存失败,最好考虑采用某种方式直接提醒用户,例如,使用Toast或对话框。

Context的openFileInput(…)方法,我们从文件中读取数据并转换为JSONObjects类型的string,然后再解析为JSONArray,接着再解析为ArrayList,最后返回获得的ArrayList。

注意,在finally代码块中,应调用reader.close()方法。这样,即使发生错误,也可以保证完成底层文件句柄的释放。

要将数据写入外部存储,需预先完成两件事。首先,检查外部存储是否可用,可借助android.os.Environment类的一些方法和常量进行判断。其次,获得外部文件目录的句柄(可在Context类中找到获取方法)。接下来的数据写入实现可参照CriminalIntentJSONSerializer类的处理。

相机

我们可以通过隐式intent与照相机进行交互。大多数Android设备都会内置相机应用。相机应用会自动侦听由MediaStore.ACTION_IMAGE_CAPTURE创建的intent。

截止本书写作时,在大多数设备上,隐式intent的相机接口有一个bug, 会导致用户无法保存全尺寸的照片。因此,对于那些只需要缩略图的应用来说,隐式intent完全可以满足要求。然而, CriminalIntent应用需要的是全尺寸的作案现场图片,别无选择,我们只能去学习使用Camera API了。

相机是一种独占性资源:一次只能有一个activity能够调用相机。

SurfaceView实例是相机的取景器。 SurfaceView是一种特殊的视图,可直接将要显示的内容渲染输出到设备的屏幕上。

需要在配置文件中增加uses-permission元素节点以获得使用相机的权限。

相机是一种系统级别的重要资源,因此,很关键的一点就是,需要时使用,用完及时释放。如果忘记释放,除非重启设备,否则其他应用将无法使用相机。

在CrimeCameraFragment生命周期中,我们应该在onResume()和onPause()回调方法中打开和释放相机资源。这两个方法可确定用户能够同fragment视图交互的时间边界,只有在用户能够同fragment视图交互时,相机才可以使用。

在CrimeCameraFragment.onResume()方法中,使用Camera.open(int)静态方法来初始化相机。然后传入参数0打开设备可用的第一相机(通常指的是后置相机)。如果设备没有后置相机(如Nexus 7机型) ,那么前置相机将会打开。

Fragment被销毁时,应该及时释放相机资源,以便于其他应用需要时可以使用。

SurfaceHolder是我们与Surface对象联系的纽带。 Surface对象代表着原始像素数据的缓冲区。

Surface对象也有生命周期: SurfaceView出现在屏幕上时,会创建Surface; SurfaceView从屏幕上消失时, Surface随即被销毁。 Surface不存在时,必须保证没有任何内容要在它上面绘制。

SurfaceView及其协同工作对象都不会自我绘制内容。对于任何想将内容绘制到Surface缓冲区的对象,我们称其为Surface的客户端。在CrimeCameraFragment类中,Camera实例是Surface的客户端。

Surface创建完成后,需要将Camera连接到SurfaceHolder上;Surface销毁后,再将Camera从SurfaceHolder上断开。

SurfaceHolder提供了另一个接口: SurfaceHolder.Callback。该接口监听Surface生命周期中的事件,这样就可以控制Surface与其客户端协同工作。

任何时候,打开相机并完成任务后,必须记得及时释放它,即使是在发生异常时。

在surfaceChanged(…)实现方法中,我们设置相机预览大小为空。在确定可接受的预览大小前,这只是一个临时赋值。相机的预览大小不能随意设置,如果设置了不可接受的值,应用将会抛出异常。

可以查询PackageManager确认设备是否带有相机。

FEATURE_CAMERA常量代表后置相机,而FEATURE_CAMERA_FRONT常量代表前置相机。

为什么必须在activity中实现隐藏呢?在调用Activity.setContentView(…)方法(该方法是在CrimeCameraActivity类的onCreate(Bundle)超类版本方法中被调用的。)创建activity视图之前,就必须调用requestWindowFeature(…)方法及addFlags(…)方法。而fragment无法在其托管activity视图创建之前添加,因此,必须在activity里调用隐藏操作栏和状态栏的相关方法。

默认情况下,某个应用的activity只能从自己的应用里启动。将android:exported属性值设为true相当于告诉Android,其他应用也可以启动指定应用的activity。(如果将intent过滤器添加到activity的声明中,该activity的android:exported属性值会被自动设为true。)