安卓专家级编程-二-

70 阅读1小时+

安卓专家级编程(二)

原文:Expert Android

协议:CC BY-NC-SA 4.0

三、自定义布局的原则和实践

Abstract

在前两章中,我们介绍了如何创建和使用自定义视图和复合控件。我们现在处于视图定制之旅的第三站:创建和使用定制布局。前三章的内容显然是相关的。该材料依赖于与第一章中所述相同的视图基础架构。然而,每一章都表达了视图架构的不同方面。

在前两章中,我们介绍了如何创建和使用自定义视图和复合控件。我们现在处于视图定制之旅的第三站:创建和使用定制布局。前三章的内容显然是相关的。该材料依赖于与第一章中所述相同的视图基础架构。然而,每一章都表达了视图架构的不同方面。

对比自定义布局

让我们花点时间来比较和对比 Android UI 框架中定制控件的三种方法:定制视图、复合控件和定制布局。这种比较有助于我们关注定制布局的特殊材料:从ViewGroup开始扩展,测量子视图,布置子视图。

自定义视图

当我们设计定制视图时,主要关注的是onMeasure()和onDraw()。自定义视图没有子视图。所以像onLayout()这样的方法并不适用,因为onLayout()是给孩子用的。视图的基础layout()方法负责这一点。另外,onMeasure()非常简单,因为您只处理一个视图。你的注意力主要转移到onDraw()以及如何使用画布上。

当然,在这三种方法中,定义和读取定制属性的方式非常相似。因为自定义视图没有子视图,所以布局参数在自定义视图中不起作用,但在自定义布局中起重要作用。在定制视图中,我们还担心使用BaseSavedState模式保存视图的状态。总之,在定制视图时,您必须考虑以下事项:

  • 扩展View
  • 覆盖onMeasure()
  • 覆盖onDraw()
  • 使用BaseSavedState模式保存状态
  • 使用自定义属性
  • 理解和应用 requestLayout 和 invalidate

相反,在创建自定义视图时,您可以忽略以下细节:

  • 覆盖onLayout()
  • 实施和使用LayoutParams

复合控件

对于复合控件,主要关注的是如何使用现有的布局,如线性布局和相对布局,来组成具有特定行为的合并组件。

因为我们使用的是现有的布局,所以不需要测量、布局或绘图。我们在复合控件中的主要焦点是使用BaseSavedState模式为复合控件本身及其子控件保存视图状态。当然,我们也可以使用自定义属性。所以对于复合控件,你需要担心:

  • 扩展现有布局
  • 使用BaseSavedState模式保存状态
  • 控制其子视图的保存状态
  • 使用自定义属性

虽然你可以忽略:

  • 覆盖onMeasure()
  • 覆盖onDraw()
  • 覆盖onLayout()
  • 担心requestLayout()invalidate()
  • 实施和使用LayoutParams

自定义布局

现在,您可以将上述两种方法与实现自定义布局所需的以下步骤进行对比(第三种方法):

Inherit from ViewGroup   Override onMeasure()   Override onLayout()   Implement custom LayoutParams with any additional layout attributes   Override layout parameters construction methods in the custom layout class  

我们解释了每一个步骤,并提供了一个带注释的示例代码。首先,我们展示了我们计划编写的自定义布局,作为一个示例来说明所有指示的步骤。

规划简单的流程布局

为了记录像线性布局这样的自定义布局的创建,我们使用流布局作为示例。为了演示流布局,我们将一组按钮水平放置,当水平空间用完时,将它们绕到下一行。图 3-1 显示了我们将要设计的流程布局,布置在一个活动中。

A978-1-4302-4951-1_3_Fig1_HTML.jpg

图 3-1。

A set of buttons encapsulated in a custom flow layout

在图 3-1 中,整个活动呈线性布局。流布局位于中间,在显示“欢迎使用流布局”的文本视图和底部显示“调试文本暂存”的文本视图之间您可以看到流布局采用了许多大小不同的按钮,并将它们包裹起来。

到本章结束时,你会对如何做好这一点有一个清晰的想法,也将有基础来编写你自己的自定义布局。自定义布局有点棘手,但是对于简化 UI 设计来说很实用。我们在这里的目的是让你拥有自信地使用定制布局所需的一切。

现在你已经有了一个FlowLayout的心理图像,我们开始探索实现它所需的每个步骤。

从 ViewGroup 类派生

正如在内置的LinearLayout, FlowLayout中扩展了ViewGroup。清单 3-1 显示了FlowLayout如何扩展一个ViewGroup。该清单还显示了 a)?? 如何使用构造函数读取其特定的定制属性,以及 b)适当地初始化超类 ??。

清单 3-1。从视图组扩展的 FlowLayout

public class FlowLayout

extends ViewGroup

{

private int hspace=10;

private int vspace=10;

public FlowLayout(Context context) {

super(context);

initialize(context);

}

public FlowLayout(Context context, AttributeSet attrs, int defStyle) {

super(context, attrs, defStyle);

TypedArray t = context.obtainStyledAttributes(attrs

R.styleable.FlowLayout, 0, 0);

hspace = t.getDimensionPixelSize(R.styleable.FlowLayout_hspace, hspace);

vspace = t.getDimensionPixelSize(R.styleable.FlowLayout_vspace, vspace);

t.recycle();

initialize(context);

}

public FlowLayout(Context context, AttributeSet attrs) {

this(context, attrs, 0);

}

private void initialize(Context context) {

//Do any common initializations you may have

//It is empty in our implementation

}

清单 3-1 中的代码很像第一章和第二章中讨论的自定义视图的任何其他扩展:扩展一个基类(View,或者一个现有的布局,比如LinearLayout,或者这里的ViewGroup)。在所有情况下,您都可以拥有自定义属性;我们在attrs.xml中定义自定义属性,并在构造函数中读取它们。在FlowLayout的情况下,自定义属性是“hspace”(水平间距)和“vspace”(垂直间距),用于子视图和换行时的新行。现在让我们看看清单 3-1 中构造函数代码的attrs.xml

在 Attrs.xml 中为 FlowLayout 定义自定义属性

清单 3-2 显示了我们自定义的FlowLayoutattrs.xml。这个文件需要在“/res/values”子目录中。

清单 3-2。为 FlowLayout 定义自定义属性

<resources>

<declare-styleable name="``FlowLayout

<attr name="hspace" format="dimension"/>

<attr name="vspace" format="dimension" />

</declare-styleable>

<declare-styleable name="``FlowLayout_Layout

<attr name="``layout_space

</declare-styleable>

</resources>

清单 3-2 中的 styleable FlowLayout定义了两个定制属性:hspacevspace。可样式化的FlowLayout_Layout自定义属性是为需要由FlowLayout定义为内部类的LayoutParams对象定义的。(我们很快会谈到这一点。)这些后来的定制布局参数代表父布局类存储在每个子视图中,如FlowLayout,以允许子视图在必要时覆盖任何父属性。

按照惯例,我们通过布局的名称来调用属于主布局的样式——在本例中是“FlowLayout.”同样按照惯例,我们用布局名称后跟“layout.来调用属于LayoutParams的样式。在清单 3-2 中,这个名称是“FlowLayout_layout.

在本章的后面你会看到这两种类型的属性是如何在布局文件中被赋值的(未来的清单 3-8)。你已经在清单 3-1 中看到了如何读取样式。稍后您将看到如何读取FlowLayout_layout styleables(未来的清单 3-5)。

使用 onMeasure()

以下是当你使用像FlowLayout这样的自定义布局时onMeasure()的关键方面。

当您扩展一个ViewGroup时,您需要首先度量子元素,并添加这些度量以作为ViewGroup的度量返回。在第一章的中,我们说过一个视图有一个名为“measure()”的方法,它将被视图的父视图调用,作为父视图的onMeasure()的一部分。在FlowLayout的情况下,FlowLayout是父代。图 3-1 中的按钮是子视图。所以FlowLayout需要在它的onMeasure()中调用每个子Button控件的measure()方法。

然而,有一个问题。你会渴望使用child.measure()。但是不要。相反,您需要使用ViewGroup.measureChild(),然后将这些度量值相加,得到您的总尺寸。这是因为measureChild()是一种聪明的方法,它考虑到了ViewGroup ( AT_MOSTUNSPECIFIEDEXACT)的测量规格,并在需要时用不同的测量规格适当地询问孩子。如果你调用child.measure(),那么你就要自己做这个逻辑。我们在本章末尾包含了源代码ViewGroup.measureChild()来帮助你理解这一点。(我们认为在这里包含冗长的代码会分散您对本节主要内容的注意力。)

一旦有了FlowLayout的总大小,也就是它所有子元素的总和,就调用预制方法resolveSize()来计算出流布局的最终测量大小。让我们进一步解释一下resolveSize()吧。假设你有很多孩子。子级的总测量大小可能超过流布局的父级为流布局建议的大小。当流布局的测量规格为UNSPECIFIED时,可以返回较大的尺寸。但是如果测量规格是AT_MOST,那么您需要返回建议的尺寸,并且不能超过它。测量规格也可以说EXACT。在这种情况下,也不能超过建议的精确大小。所有这些逻辑都由内置方法resolveSize()处理。该resolveSize()考虑了测量规格的变化,并将可能较大的测量尺寸裁剪为与onMeasure()的测量规格中指定的尺寸一致的适当尺寸。

一旦这个度量过程完成,您需要在需要定位子视图的地方覆盖onLayout()。此练习与测量相同,因为您需要知道每个视图的大小,以便可以一个接一个地放置视图,并在每个视图之间留出足够的垂直和水平空间。那么为什么要测量两次呢?如果您还能在测量过程中记住每个子视图的原点,那么您可以在onLayout()中使用该原点和每个视图的维度。要存储每个视图的原点,可以使用 layout parameters 对象,它代表父视图FlowLayout.与每个视图一起持有

这就结束了应该如何为FLowLayout实现onMeasure()的理论。在我们进入onMeasure()的实现之前,我们想要呈现一个建议尺寸、流布局的测量尺寸、子尺寸的测量尺寸以及它们如何相互关联的图形表示(图 3-2 )。我们还使用图 3-2 解释测量算法。

A978-1-4302-4951-1_3_Fig2_HTML.jpg

图 3-2。

Measuring a compound control: DurationControl

让我们快速浏览一下图 3-2 。值RWRH代表“实际宽度”和“实际高度”这是从FlowLayout的父节点建议的宽度和高度。我们从传递给onMeasure()方法的measurespec中接收这些值。当然,RHRW只对AT_MOSTEXACT有效。在UNSPECIFIED的情况下,像FlowLayout这样的子节点可以返回它的最大值,而不用考虑传入的大小。

在下一级,whFlowLayout的测量宽度和高度,通过结合子视图的测量宽度(vw =视图宽度)和测量高度(vh =视图高度)来确定。当我们浏览FlowLayout的每个子视图时,我们将相应地增加hw

在逻辑上,我们依赖于子视图的来源。在图 3-2 中,我们使用点(h1,w1)作为当前视图的原点。然后我们通过计算(h2,w2)来计算下一个视图的原点。当我们计算这个(h2,w2))时,我们考虑了水平和垂直方向上有多少可用空间。一旦我们测量了手头的当前视图并记录了它的原点,我们就移动到下一个视图并将(h2,w2)设置回(h1,w1)作为新的原点,并且重复这个过程直到我们穷尽所有的视图。(清单 3-3 给出了执行这个逻辑的实际源代码。)

在图 3-2 中,当我们浏览每个视图时,不难看出何时增加w以及何时增加h(当前需要的总宽度和高度)。宽度随着我们向右移动而增加;当我们检测到一个新的行时,高度增加。当前原点宽度(w1)与当前子宽度(VW)相加,并且加在一起超过了可用的实际宽度(RW,这就是一个新行。

给定这个逻辑的图示(图 3-2 ,清单 3-3 展示了我们是如何为FlowLayout实现onMeasure()的。

清单 3-3。实施 onMeasure()

//This is very basic

//doesn't take into account padding

//You can easily modify it to account for padding

@Override

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)

{

//********************

//Initialize

//********************

int rw = MeasureSpec.getSize(widthMeasureSpec);

int rh = MeasureSpec.getSize(heightMeasureSpec);

int h = 0; //current height

int w = 0; //current width

int h1 = 0, w1=0; //Current point to hook the child to

//********************

//Loop through children

//********************

int numOfChildren = this.getChildCount();

for (int i=0; i < numOfChildren; i++ )

{

//********************

//Front of the loop

//********************

View child = this.getChildAt(i);

this.measureChild(child,widthMeasureSpec, heightMeasureSpec);

int vw = child.getMeasuredWidth();

int vh = child.getMeasuredHeight();

if (w1 + vw > rw)

{

//new line: max of current width and current width position

//when multiple lines are in play w could be maxed out

//or in uneven sizes is the max of the right side lines

//all lines don't have to have the same width

//some may be larger than others

w = Math.max(w,w1);

//reposition the point on the next line

w1 = 0; //start of the line

h1 = h1 + vh; //add view height to the current heigh

}

//********************

//Middle of the loop

//********************

int w2 = 0, h2 = 0; //new point for the next view

w2 = w1 + vw;

h2 = h1;

//latest height: current point + height of the view

//however if the previous height is larger use that one

h = Math.max(h,h1 + vh);

//********************

//Save the current origin coords for the view

//in its layout

//********************

LayoutParams lp = (LayoutParams)child.getLayoutParams();

lp.x = w1;

lp.y = h1;

//********************

//Restart the loop

//********************

w1=w2;

h1=h2;

}

//********************

//End of for

//********************

w = Math.max(w1,w);

//h = h;

setMeasuredDimension(

resolveSize(w, widthMeasureSpec)

resolveSize(h,heightMeasureSpec));

};

正如我们在onMeasure()的解释中指出的,点(h1,w1)代表每个子视图的原点。为了帮助布局阶段,清单 3-3 中的onMeasure()代码将这个原点存放在子视图持有的布局参数对象中。一个ViewGroup的每个子视图保证有一个LayoutParams对象。在这种情况下,像FlowLayout这样的自定义布局可以专门化这个基础LayoutParms对象,使其具有像原点这样的附加参数。在清单 3-3 中,我们检索这个布局参数对象LayoutParams并为该视图设置原点。我们突出显示了清单 3-3 中的代码,以显示视图原点的设置。

同样,为了让清单 3-3 中的逻辑清晰明了,我们没有使用我们在清单 3-2 的attrs.xml中定义的间距布局参数。但是您可以使用下面的代码读取这个间距参数,方法是将它放在清单 3-2 的onMeasure()方法中的任何地方。参见清单 3-5 中的FlowLayout.LayoutParams内部类定义。

LayoutParams lp = (LayoutParams)child.getLayoutParams();

int spacing = lp.spacing;

//Adjust your widths based on this spacing.

在我们继续描述onLayout()之前,让我们总结一下关于onMeasure()的几个事实:

Each child is measured independently of its siblings in a view group.   You have to alter the measurespec passed to the FlowLayout before measuring the children. This is evident when you consider EXACT coming into FlowLayout becomes AT_MOST when applied to children; otherwise, each child will take all the space given to the FlowLayout. This alteration is done by the ViewGroup.measureChild(). So unless you have a reason not to, you should call this method to measure children.   It is perfectly okay to add all the children’s measurements. No need to worry about the total size being too large (exceeding the suggested height or width) because the resolveSize(  ) will clip the final reported size for the FlowLayout.   FlowLayout is not playing favorites and distributing the available space by itself. It relies on well-behaved children to claim space on a first come, first served basis. It is possible that a misbehaving view could take most of the space!   A layout like FlowLayout first forms a full picture of all its children in a best-case scenario, and could be larger than what is suggested. It is the resolveSize(  ) that cuts its size down as appropriate. So when you are envisioning your layout, imagine its full picture and not a constrained one by the input size.  

现在来说说onLayout

实现 onLayout()

在第一章的中,我们介绍了测量通道和布局通道。测量通道用于测量儿童。它不关心每个孩子的安置。相反,这是在布局过程中完成的。布局路径在onLayout()方法中定义。

在我们的FlowLayout's onMeasure()中,我们已经存储了每个视图的布局原点,onLayout()的实现变得非常简单。遍历每个子对象并检索其LayoutParams对象。从该对象中,获得xy位置。利用这些xy的位置和测量出的孩子的宽度和高度,在孩子身上调用layout()。这就是你在清单 3-4 中看到的。

清单 3-4。实现 onLayout()

@Override

protected void onLayout(boolean arg0, int arg1, int arg2, int arg3, int arg4)

{

//Call layout() on children

int numOfChildren = this.getChildCount();

for (int i=0; i < numOfChildren; i++ )

{

View child = this.getChildAt(i);

LayoutParams lp = (LayoutParams)child.getLayoutParams();

child.layout(lp.x

lp.y

lp.x + child.getMeasuredWidth()

lp.y + child.getMeasuredHeight());

}

}

定义自定义 LayoutParams

正如我们已经指出的,LayoutParams是一个对象,它保存了一些特定于像FlowLayout这样的布局的参数。它们与FlowLayout的其他局部变量分开的原因是这些LayoutParams是由子视图创建和保存的。然而,是FlowLayout定义了这些是什么,如何阅读它们,以及如何解释它们。

清单 3-5 显示了我们如何为类FlowLayout定义布局参数。LayoutParams类定义了一个名为spacing的变量来表示一个视图相对于它的兄弟视图需要多少间距。这类似于清单 3-1 中的“hspace”。不同之处在于:hspace定义了所有视图的间距,而LayoutParams中的间距特定于每个视图。所以清单 3-5 中的spacing有机会覆盖hspace的布局级别设置。

清单 3-5。定义自定义 FlowLayout LayoutParams 类

//*********************************************************

//Custom Layout Definition

//*********************************************************

public static class LayoutParams extends ViewGroup.MarginLayoutParams {

public int spacing = -1;

public int x =0;

public int y =0;

public LayoutParams(Context c, AttributeSet attrs) {

super(c, attrs);

TypedArray a =

c.obtainStyledAttributes(attrs, R.styleable.FlowLayout_Layout);

spacing = a.getDimensionPixelSize(R.styleable.FlowLayout_Layout_layout_space, 0);

a.recycle();

}

public LayoutParams(int width, int height) {

super(width, height);

spacing = 0;

}

public LayoutParams(ViewGroup.LayoutParams p) {

super(p);

}

public LayoutParams(MarginLayoutParams source) {

super(source);

}

}//eof-layout-param

清单 3-5 展示了如何从 XML 布局文件中读取间距变量。这种方法与我们在第一章和第二章中使用定制属性的方法非常相似:在attrs.xml中定义变量名(见清单 3-2),然后通过构造函数中的类型化属性读取它。

除了布局参数,我们还为FlowLayoutLayoutParams类分配了一个额外的职责。参见布局参数类上的xy公共成员。我们使用这些公共变量在测量过程中存储子视图的原点,并在布局过程中重用它。

重写自定义 LayoutParams 构造

子视图,如TextviewButtonview,可以嵌入到任何父布局中,如LinearLayoutFlowLayout。子视图如何知道如何构造和持有与父布局相关的LayoutParams对象?这不能硬编码到视图中,因为视图在编译时不知道它的父视图是什么。

为了解决这一困境,Android SDK 在ViewGroup(所有布局的父级)上提供了许多需要被覆盖的标准方法。当一个视图被放入一个视图组时,这些标准方法被调用来构造派生的LayoutParams并将其关联到视图。清单 3-6 显示了这些构建派生的FlowLayout.LayoutParamsViewGroup标准方法。

清单 3-6。覆盖 LayoutParams 创建

//*********************************************************

//Layout Param Support

//*********************************************************

@Override

public LayoutParams generateLayoutParams(AttributeSet attrs) {

return new FlowLayout.LayoutParams(getContext(), attrs);

}

@Override

protected LayoutParams generateDefaultLayoutParams() {

return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);

}

@Override

protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {

return new LayoutParams(p);

}

// Override to allow type-checking of LayoutParams.

@Override

protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {

return p instanceof FlowLayout.LayoutParams;

}

调用第一个方法来实例化派生布局的布局参数类。如果我们不覆盖它,那么基本视图组将实例化它的实现MarginLayoutParams。这个人不会有 x 和 y,也不会读取自定义的"spacing"变量。

其余的方法在不同的时间被适当地调用。清单 3-6 是一组标准的方法,可以复制并粘贴到您的自定义 latouts 中,就像我们在这里对FlowLayout所做的一样。

FlowLayout 的源代码

清单 3-7 显示了FlowLayout;的全部源代码,在这里你可以看到我们到目前为止在一个地方为定制布局覆盖的所有步骤:(a)扩展视图组,(b)读取定制属性,(c) onMeasure(),(d) onLayout,(e)实现FlowLayout.LayoutParams,以及(f)实现定制布局参数的支持方法。为了能够编译和运行这个文件,你还需要清单 3-2 所示的attrs.xml文件,放在/res/values子目录中。

清单 3-7。FlowLayout 的完整源代码

public class FlowLayout

extends ViewGroup

{

private int hspace=10;

private int vspace=10;

public FlowLayout(Context context) {

super(context);

initialize(context);

}

public FlowLayout(Context context, AttributeSet attrs, int defStyle) {

super(context, attrs, defStyle);

TypedArray t = context.obtainStyledAttributes(attrs

R.styleable.FlowLayout, 0, 0);

hspace = t.getDimensionPixelSize(R.styleable.FlowLayout_hspace

hspace);

vspace = t.getDimensionPixelSize(R.styleable.FlowLayout_vspace

vspace);

t.recycle();

initialize(context);

}

public FlowLayout(Context context, AttributeSet attrs) {

this(context, attrs, 0);

}

private void initialize(Context context) {

}

//This is very basic

//doesn't take into account padding

//You can easily modify it to account for padding

@Override

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)

{

//********************

//Initialize

//********************

int rw = MeasureSpec.getSize(widthMeasureSpec);

int rh = MeasureSpec.getSize(heightMeasureSpec);

int h = 0; //current height

int w = 0; //current width

int h1 = 0, w1=0; //Current point to hook the child to

//********************

//Loop through children

//********************

int numOfChildren = this.getChildCount();

for (int i=0; i < numOfChildren; i++ )

{

//********************

//Front of the loop

//********************

View child = this.getChildAt(i);

this.measureChild(child,widthMeasureSpec, heightMeasureSpec);

int vw = child.getMeasuredWidth();

int vh = child.getMeasuredHeight();

if (w1 + vw > rw)

{

//new line: max of current width and current width position

//when multiple lines are in play w could be maxed out

//or in uneven sizes is the max of the right side lines

//all lines don't have to have the same width

//some may be larger than others

w = Math.max(w,w1);

//reposition the point on the next line

w1 = 0; //start of the line

h1 = h1 + vh; //add view height to the current height

}

//********************

//Middle of the loop

//********************

int w2 = 0, h2 = 0; //new point for the next view

w2 = w1 + vw;

h2 = h1;

//latest height: current point + height of the view

//however if the previous height is larger use that one

h = Math.max(h,h1 + vh);

//********************

//Save the current coords for the view

//in its layout

//********************

LayoutParams lp = (LayoutParams)child.getLayoutParams();

lp.x = w1;

lp.y = h1;

//********************

//Restart the loop

//********************

w1=w2;

h1=h2;

}

//********************

//End of for

//********************

w = Math.max(w1,w);

//h = h;

setMeasuredDimension(

resolveSize(w, widthMeasureSpec)

resolveSize(h,heightMeasureSpec));

};

@Override

protected void onLayout(boolean arg0, int arg1, int arg2, int arg3, int arg4)

{

//Call layout() on children

int numOfChildren = this.getChildCount();

for (int i=0; i < numOfChildren; i++ )

{

View child = this.getChildAt(i);

LayoutParams lp = (LayoutParams)child.getLayoutParams();

child.layout(lp.x

lp.y

lp.x + child.getMeasuredWidth()

lp.y + child.getMeasuredHeight());

}

}

//*********************************************************

//Layout Param Support

//*********************************************************

@Override

public LayoutParams generateLayoutParams(AttributeSet attrs) {

return new FlowLayout.LayoutParams(getContext(), attrs);

}

@Override

protected LayoutParams generateDefaultLayoutParams() {

return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);

}

@Override

protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {

return new LayoutParams(p);

}

// Override to allow type-checking of LayoutParams.

@Override

protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {

return p instanceof FlowLayout.LayoutParams;

}

//*********************************************************

//Custom Layout Definition

//*********************************************************

public static class LayoutParams extends ViewGroup.MarginLayoutParams {

public int spacing = -1;

public int x =0;

public int y =0;

public LayoutParams(Context c, AttributeSet attrs) {

super(c, attrs);

TypedArray a =

c.obtainStyledAttributes(attrs, R.styleable.FlowLayout_Layout);

spacing = a.getDimensionPixelSize(R.styleable.FlowLayout_Layout_layout_space, 0);

a.recycle();

}

public LayoutParams(int width, int height) {

super(width, height);

spacing = 0;

}

public LayoutParams(ViewGroup.LayoutParams p) {

super(p);

}

public LayoutParams(MarginLayoutParams source) {

super(source);

}

}//eof-layout-params

}// eof-class

运行中的流程布局

你现在可能急于学习如何在活动布局文件中使用FlowLayout来查看如图 1-1 所示的视图。下面是清单 3-8 中产生图 1-1 的布局文件:

清单 3-8。为活动使用流程布局

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android

xmlns:cl="http://schemas.android.com/apk/res/com.androidbook.customLayouts

android:orientation="vertical"

android:layout_width="fill_parent"

android:layout_height="match_parent"

>

<TextView

android:id="@+id/text2"

android:layout_width="fill_parent"

android:layout_height="wrap_content"

android:text="Welcome to the Compound Controls"

/>

<com.androidbook.customLayouts.FlowLayout

android:id="@+id/durationControlId"

android:layout_width="fill_parent"

android:layout_height="wrap_content"

cl:hspace="10dp"

cl:vspace="10dp">

<Button android:text="Button1"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

cl:layout_space="20dp"

/>

<Button android:text="Button2"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

/>

<Button android:text="Button3"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

/>

<Button android:text="Button4"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

/>

<Button android:text="Button5"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

/>

<Button android:text="B1"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

/>

<Button android:text="B2"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

/>

<Button android:text="B3"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

/>

<Button android:text="B4"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

/>

<Button android:text="B5"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

/>

</com.androidbook.customLayouts.FlowLayout>

<TextView

android:id="@+id/text1"

android:layout_width="fill_parent"

android:layout_height="wrap_content"

android:text="Scratch for debug text"

/>

</LinearLayout>

请特别注意我们是如何使用我们自己的定制名称空间“cl”(定制布局)来指示定制属性的。

了解 ViewGroup.getChildMeasureSpec

在覆盖onMeasure()时,我们已经表示使用ViewGroup.measureChild(),不直接使用child.measure()。当您看到清单 3-9 中的ViewGroup.getChildMeasureSpec()(由ViewGroup.measureChild()触发)的实现时,这变得非常清楚。清单 3-9 中的代码直接取自 Android 源代码中的ViewGroup类。

清单 3-9 中需要注意的关键是如何更仔细地查看FlowLayout' s(或ViewGroup' s)度量规格,并且如果必要的话,将不同的度量规格传递给孩子以测量孩子的尺寸。

清单 3-9。找出正确的子度量规格

public static int getChildMeasureSpec(int spec, int padding, int childDimension)

{

int specMode = MeasureSpec.getMode(spec);

int specSize = MeasureSpec.getSize(spec);

int size = Math.max(0, specSize - padding);

int resultSize = 0;

int resultMode = 0;

switch (specMode) {

// Parent has imposed an exact size on us

case MeasureSpec.EXACTLY:

if (childDimension >= 0) {

resultSize = childDimension;

resultMode = MeasureSpec.EXACTLY;

} else if (childDimension == LayoutParams.MATCH_PARENT) {

// Child wants to be our size. So be it.

resultSize = size;

resultMode = MeasureSpec.EXACTLY;

} else if (childDimension == LayoutParams.WRAP_CONTENT) {

// Child wants to determine its own size. It can't be

// bigger than us.

resultSize = size;

resultMode = MeasureSpec.AT_MOST;

}

break;

// Parent has imposed a maximum size on us

case MeasureSpec.AT_MOST:

if (childDimension >= 0) {

// Child wants a specific size... so be it

resultSize = childDimension;

resultMode = MeasureSpec.EXACTLY;

} else if (childDimension == LayoutParams.MATCH_PARENT) {

// Child wants to be our size, but our size is not fixed.

// Constrain child to not be bigger than us.

resultSize = size;

resultMode = MeasureSpec.AT_MOST;

} else if (childDimension == LayoutParams.WRAP_CONTENT) {

// Child wants to determine its own size. It can't be

// bigger than us.

resultSize = size;

resultMode = MeasureSpec.AT_MOST;

}

break;

// Parent asked to see how big we want to be

case MeasureSpec.UNSPECIFIED:

if (childDimension >= 0) {

// Child wants a specific size... let him have it

resultSize = childDimension;

resultMode = MeasureSpec.EXACTLY;

} else if (childDimension == LayoutParams.MATCH_PARENT) {

// Child wants to be our size... find out how big it should

// be

resultSize = 0;

resultMode = MeasureSpec.UNSPECIFIED;

} else if (childDimension == LayoutParams.WRAP_CONTENT) {

// Child wants to determine its own size.... find out how

// big it should be

resultSize = 0;

resultMode = MeasureSpec.UNSPECIFIED;

}

break;

}

return MeasureSpec.makeMeasureSpec(resultSize, resultMode);

}

参考

第一章和第二章中列出的大部分参考资料对本章也是有效的。因此,我们在这里只添加了几个本章特有的参考文献。

摘要

在第一章中,我们展示了视图的基本架构,以及如何定制一个开放式视图对象和如何直接使用画布。在《??》第二章中,我们谈到了通过扩展现有布局来构建用户级控件。在本章中,我们介绍了如何通过直接扩展视图组来创建自定义布局。在这三种方法中,我们认为您最有机会使用自定义布局。例如,流布局的需求是必不可少的,但它不在原生 SDK 中。举个例子,我们正在做一个文字游戏,我们需要把单词的字母排列成一个流动的布局。在同一个游戏中,我们希望将一组菜单图标呈现为一个旋转的表格。你可以在 Google Play 中看到这个名为解读专家的游戏。或者,您可能想要创建一个图像滚轴平台,如相册。这些都是自定义布局的例子。考虑到这一点,我们在这里详细介绍了onMeasure()和onLayout()以便帮助您进行自定义布局。

复习问题

以下一组详细的问题应该可以巩固您在本章中学到的内容:

What classes do you extend to create custom layouts?   Can you use custom attributes for the custom layout?   What are LayoutParams?   Who implements the LayoutParams class?   Who has methods to construct a LayoutParams class?   Why do views hold LayoutParams on behalf of a parent layout?   Why should you use resolveSize(  )?   Why should you use ViewGroup.measureChild() and not child.measure()?   What would be wrong if a layout passes its measure spec as is to each of its children?   What happens if the total measured size of the children exceeds the suggested size of the parent layout?   How do you define and read layout parameters from attrs.xml?

四、用于设备上持久性的 JSON

Abstract

移动应用或游戏的一个关键方面是需要设备上的持久性。Android 为持久性提供了许多非常好的选项,包括关系存储。然而,所有提供的使用关系存储的机制都需要相当长的开发时间。本章为许多简单到中等程度的持久性需求提供了一条捷径。该方法使用 GSON/JSON 技术将对象树直接存储在非 SQL 存储中,例如设备上的共享首选项和内部文件存储方式。

移动应用或游戏的一个关键方面是需要设备上的持久性。Android 为持久性提供了许多非常好的选项,包括关系存储。然而,所有提供的使用关系存储的机制都需要相当长的开发时间。本章为许多简单到中等程度的持久性需求提供了一条捷径。该方法使用 GSON/JSON 技术将对象树直接存储在非 SQL 存储中,比如共享首选项和内部文件存储。

JSON 代表 JavaScript 对象符号。GSON 是 Google 项目中的一个 Java 库,它使用 JSON 结构将 Java 对象序列化和反序列化为字符串。

你会惊讶地发现 GSON 是如何提高你的移动应用的工作效率的。尤其是那些需要中等持久性的应用可以从 GSON 中受益。在这种方法中,您将应用的持久状态表示为一个 JSON 字符串。然后,您将使用神奇的 GSON Java 库将您的应用对象转换为 JSON,并将生成的 JSON 字符串持久化,或者保存在共享的首选项中,或者保存在设备的内部文件中。

这种方法适合快速编写中等大小的应用。使用这种方法,每隔几周发布一个新的应用是合理的,而开发这样的应用可能需要一两个月。事实上,在某些应用上,你可能会获得两到三倍的优势。即使当您考虑复杂的应用时,您也可以从这种方法中获得显著的优势,因为您可以将想法原型化,并在有限的版本中进行测试。

为了帮助您理解这种基于 JSON 的应用存储方法,我们在本章中介绍了以下内容:

Using GSON to convert Java objects to JSON strings and back   Storing and retrieving JSON strings using shared preferences   Storing and retrieving JSON strings from Android internal file storage  

在涵盖这些主题的过程中,我们回答了以下关键问题:(1)使用共享偏好作为简单应用和游戏的数据存储有哪些限制?(2)除了简单的键/值对之外,有没有一个官方的词不使用共享首选项?(3)使用此方案可以存储的数据量是否有最大限制?(4)我是否应该探索其他数据存储选项?(Android 内部存储和外部存储是什么意思?(6)我应该使用共享首选项还是内部文件?(7)我应该使用外部存储卡上的文件吗?(8)我何时需要使用 SQL 存储?以及(8)我能否以这样一种方式编写我的应用,以便为以后的版本迁移到 SQL 存储?

我们首先快速回顾一下 Android 中用于管理持久状态的数据存储选项。

Android 中的数据存储选项

Android 中存储数据的方式有五种:(1)共享首选项,(2)内部文件,(3)外部文件,(4) SQLite,(5)云端网络存储。让我们逐一回顾一下。

共享偏好设置是应用和设备内部的。该数据不可用于其他应用。用户不能通过安装到 USB 端口上来直接操作这些数据。删除应用时,数据会自动删除。共享首选项是结构化的键/值对数据,并遵循 Android 为使用存储的数据作为首选项而强加的一些其他语义。共享首选项以 XML 文件的形式维护。

内部文件是非结构化的私有文件,应用可以在其中创建和存储数据。像共享首选项一样,内部文件遵循应用的生命周期。卸载应用时,由应用创建的内部文件也会被删除。

外部文件存储在 SD 卡上。这些成为公共文件,其他应用包括用户可以在你的应用环境之外看到。外部文件通常用于主要由用户创建并且在创建它的应用之外有意义的数据,例如音频文件、视频文件、word 文档等。当数据量很大(比如 10MB 或更大)时,外部文件也是合适的。

SQLite 是一个关系数据库。对于结构化数据,比如您计划使用 JSON 的数据,SQLite 是首选。然而,在 Java 对象和关系数据库之间要做大量的工作。即使在使用精心制作的 o/r 映射工具或库的最简单的情况下,仍然有大量的工作要做。然而,SQLite 是为后续版本调整和重构应用的绝佳选择。这种重构将使您的应用响应更快,使用更少的能量。数据库也是应用私有的,外部应用无法使用,除非您将数据库包装在内容提供者框架中。(有关内容供应器的详细报道,请参考我们的配套书籍 Pro Android 4。)

网络存储允许您的应用选择将持久数据保存在云中。然而,网络存储并不是很多应用的完整选项,因为你可能需要应用在断开互联网连接时工作。可能有更多机会使用 parse.com 或类似的 BaaS(后端即服务)平台来实现混合方法,从而将一些数据存储在云中,并在需要时与设备同步。

我们的研究使我们建议在使用 GSON 将对象转换成 JSON 后,使用共享参数或内部文件作为对象的存储机制。这些存储选项有两个优点。第一,它们是私有的;第二,当应用被删除时,它们也被删除。它们的缺点是尺寸有限;如果文件变得太大(几十兆字节或者有图像或视频),一旦你的应用启动,你可以在后续版本中迁移到 SQLite。

使用 JSON 实现持久性的一般方法

JSON 是一种字符串格式,将 JavaScript 对象表示为字符串。您可以将一组 JavaScript 对象转换成 JSON 字符串。然后,这些字符串可以通过网络传输,并作为 JavaScript 对象读回。由于 Java 对象类似于 JavaScript 对象,所以可以使用相同的 JSON 字符串格式将 Java 对象转换为字符串。产生的 JSON 字符串可以转换回 Java 对象。

因为 JSON 已经成为跨网络传输对象的主要格式,所以围绕 JSON 出现了许多工具来简化这个过程。在我们的例子中,我们使用 JSON 字符串,不是为了传输而是为了将它们持久化到磁盘上并读回它们。在这种方法中,我们使用 Google 工具 GSON 将 Java 对象转换成 JSON 字符串,然后再转换回来。如上所述,JSON 字符串可以存储在内部文件或共享的首选项中。

与 GSON 合作

GSON 是一个 Java 库(一个单独的 jar 文件),可以用来将 Java 对象转换成 JSON 或者相反。(您可以在本章末尾的参考资料中看到主页、用户指南和 API 的链接。)让我们在将 Java 对象序列化和反序列化为 JSON 的同时,快速看一下 GSON 的特性和局限性。

GSON 的特点

大多数类型的 Java 对象都可以使用 GSON 转换成 JSON 字符串。使用 GSON,您可以在嵌套结构中的对象中包含对象。如果您镜像 Java 对象并关注它们的存储结构,那么您可以将大多数(如果不是全部)对象序列化为 JSON 字符串。因此,您希望您的 Java 对象主要表示数据,而不是可能干扰用 GSON 序列化它们的成熟行为。

只要您的成员集合使用 Java 泛型,GSON 就可以成功地序列化您的集合。您将很快看到一个这样的例子。使用 Java 泛型时,需要指定集合的类型。这有助于 GSON 在反序列化时实例化正确类型的对象。(您可以参考 GSON 用户指南了解更多功能和限制。)

GSON 还将转义引号字符和任何其他对 JSON 特殊的字符。默认情况下,GSON 对 HTML 和 XML 字符进行转义。在我们的研究中,我们发现这种行为非常令人满意,因为它允许您序列化任何 Java 对象——即使它的成员包含具有任何类型字符的任意字符串。

将 GSON Jar 添加到应用中

要开始在 Android 代码中使用 GSON,您需要将 GSON jar 文件添加到您的 Eclipse/ ADT 项目中。下面是添加 GSON jar 文件所需的步骤:

Go to GSON home page: http://code.google.com/p/google-gson/ .   download the GSON jar file.   Create a subdirectory under the root of your Eclipse project called "libs” (a sibling of src).   Copy the GSON jar to that lib directory.   Go go “project properties.”   Go to the “Java Build Path” option in project properties.   Go to “Library” tab.   Add the GSON jar as an external jar.   Go to the “order/export” tab.   Choose the GSON jar to be exported as well.  

这些步骤将使 GSON jar 可用于编译您的代码,也可用于将它与 GSON jar 一起部署到仿真器或设备上。添加外部 jar 所需的步骤可能会随着您所使用的 Eclipse/ADT 版本的不同而略有不同。

您也许能够链接外部 GSON jar,而不需要复制到您的 APK 项目,如这里所示。在这种情况下,您将能够成功地编译和链接。但是,当您在设备或模拟器上运行应用时,将会出现“未找到运行时类”异常。因此,按照上述步骤将 GSON jar 添加到您的 Eclipse/ADT 项目中。

为 GSON 规划 Java 对象

您可以将您的持久性结构建模为一组相互连接的 Java 对象。然后,您可以使用 GSON 将根 Java 对象转换成 JSON。我们向您展示了几个具有代表性的 Java 对象;使用这些例子,您可以用类似的方式对您的存储对象建模。

我们从根对象开始,我们称它为MainObject。清单 4-1 是这个根MainObject.的源代码

清单 4-1。用于存储应用状态的根对象的示例结构

public class MainObject

{

public int intValue = 5;

public String stringValue = "st<ri>\"ng\"Value<node1>test</node2>";

public String[] stringArray;

public ArrayList<ChildObject> childList = new ArrayList<ChildObject>();

...

}

注意,我们已经在清单 4-1 的MainObject中模拟了许多存储类型。我们有普通的整数、字符串、数组和嵌入的子对象集合。在字符串值中,我们甚至存储了嵌套的 XML 节点。这些嵌套的 XML 节点将允许我们测试 GSON 和 Android 共享偏好的转义特性。

在清单 4-1 中,还要注意我们如何使用一个通用的ArrayList集合来保存子对象的集合。每个子对象都有自己的内部结构。子对象的类定义如清单 4-2 所示。

清单 4-2。存储为 JSON 的子对象的示例结构

public class ChildObject {

public String name;

public int age;

public boolean likesVeggies = false;

public ChildObject(String inName, int inAge)   {

name = inName;

age = inAge;

}

}

尽管我们已经将ChildObject的成员变量表示为公共变量,但 GSON 确实允许它们作为私有成员,只要您提供与它们的名称匹配的 get/set 方法。

尽管清单 4-1 和 4-2 中的这些对象主要是为了建模存储需求,但是您可以向这些类添加基本行为,并提供以结构化方式初始化和存储数据的好方法。你可以在清单 4-3 中看到这一点,我们扩展了MainObject来添加一些行为。

清单 4-3。显示行为的对象

public class MainObject

{

public int intValue = 5;

public String strinValue = "st<ri>\"ng\"Value<node1>test</node2>";

public String[] stringArray;

public ArrayList<ChildObject> childList = new ArrayList<ChildObject>();

public void addChild(ChildObject co)   {

childList.add(co);

}

public void populateStringArray()   {

stringArray = new String[2];

stringArray[0] = "first";

stringArray[1] = "second";

}

//This method is used to create a sample MainObject

public static MainObject createTestMainObject()   {

MainObject mo = new MainObject();

mo.populateStringArray();

mo.addChild(new ChildObject("Eve",30));

mo.addChild(new ChildObject("Adam",28));

return mo;

}

//this method is used to verify two MainObject

//instances are the same.

public static String checkTestMainObject(MainObject mo)   {

MainObject moCopy = createTestMainObject();

if (!(mo.strinValue.equals(moCopy.strinValue)))

{

return "String values don't match:" + mo.strinValue;

}

if (mo.childList.size() != moCopy.childList.size())

{

return "array list size doesn't match";

}

//get first child

ChildObject firstChild = mo.childList.get(0);

ChildObject firstChildCopy = moCopy.childList.get(0);

if (!firstChild.name.equals(firstChildCopy.name))

{

return "first child name doesnt match";

}

return "everything matches";

}

}

将 Java 对象转换成 JSON

既然我们已经为我们的持久性需求定义了对象结构,让我们看看如何获取一个MainObject的实例并将其转换成 JSON。我们还将获取生成的 JSON 字符串,并将其转换回实例MainObject。然后我们将比较这个生成的MainObject实例,看它是否与原型MainObject匹配。清单 4-4 显示了这样做的代码。

清单 4-4。使用 GSON 序列化和反序列化 Java 对象

public void testJSON()

{

MainObject mo = MainObject.createTestMainObject();

Gson gson = new Gson();

//Convert to string

String jsonString = gson.toJson(mo);

//Convert it back to object

MainObject mo1 = gson.fromJson(jsonString, MainObject.class);

String compareResult = MainObject.checkTestMainObject(mo1);

Log.i(“sometag”,compareResult);

}

非常简单。

现在剩下的就是如何在一个持久化的地方存储和检索这个结果 JSON 字符串。我们有两个选项:共享首选项和内部文件存储。我们首先讨论共享偏好。

使用 JSON 持久性的共享首选项

在 Android 中,共享偏好用于满足两个主要需求。Android SDK 使用共享首选项机制来创建为应用自动创建首选项屏幕所必需的 UI。Android SDK 还直接公开了共享的首选项,没有 UI 组件。在后一种模式中,共享首选项只是键/值对的存储/检索机制。Android SDK 提供了许多类和方法来直接处理这些键/值对。(你可以在我们的同伴 Pro Android 系列书籍中阅读更多关于共享偏好的内容。)

共享首选项的核心是一个 XML 文件,它以持久的方式保存键/值对。XML 文件是实现细节;Android 将来可能会选择不同的表现形式,如果它决定这样做的话。您可以拥有任意数量的共享首选项(XML 文件)。每个共享首选项可以有任意多的键/值对。

这些共享的首选项 XML 文件是您的应用专用的。卸载应用时,它们会被移除。要获取对共享首选项的引用,您需要一个上下文对象。Activity是 Android 中上下文对象的一个例子。一旦有了上下文对象,就可以用文件名调用方法getSharedPreferences()来访问共享的首选项对象。然后,您可以使用该对象来保存和检索键/值对。

如果您可以访问一个Activity,您可以只调用getPreferences()来代替。这个方法只是调用前面的getShaerdPreferences(),用活动名作为 XML 文件的名称。

获取对应用上下文的访问

应用中的每个活动都可以有自己的共享首选项文件。但是我们感兴趣的是整个应用的持久性需求,而不仅仅是特定的活动。所以我们不能在Activity类上使用getPreferences()方法。据您所知,您甚至可能在您的应用中没有活动。

因此,我们需要一个适用于整个应用级别的上下文。为此,您需要覆盖Application类并创建应用的一个实例。清单 4-5 显示了这一点。

清单 4-5。收集应用上下文

public class MyApplication extends Application

{

public final static String tag="MyApplication";

public static Context s_applicationContext = null;

@Override

public void onConfigurationChanged(Configuration newConfig) {

super.onConfigurationChanged(newConfig);

Log.d(tag,"configuration changed");

}

@Override

public void onCreate() {

super.onCreate();

s_applicationContext = getApplicationContext();

Log.d(tag,"oncreate");

}

@Override

public void onLowMemory() {

super.onLowMemory();

Log.d(tag,"onLowMemory");

}

@Override

public void onTerminate() {

super.onTerminate();

Log.d(tag,"onTerminate");

}

}

清单 4-5 中的关键行用粗体突出显示。在这个清单中,我们获取应用上下文并将其存储在一个公共静态变量中,以便应用上下文在应用中全局可用。

一旦你有了你的应用MyApplication,如清单 4-5 所示,你需要调整你的清单文件来注册MyApplication,如清单 4-6 所示。

清单 4-6。在清单文件中注册应用对象

<application android:name="com.androidbook.testjson.MyApplication"

...

</application>

清单 4-6 中的声明将导致调用MyApplicationonCreate()方法,如清单 4-5 所示

使用共享偏好设置存储和恢复字符串

有了MyApplication中的静态全局应用上下文,您可以使用清单 4-7 中的代码来保存和恢复共享首选项中的字符串。

清单 4-7。使用共享偏好设置存储和恢复字符串

//Use an XML file called myprefs.xml to represent shared preferences

//for this example.

private SharedPreferences getSharedPreferences()

{

SharedPreferences sp

= MyApplication

.s_applicationContext

.getSharedPreferences("myprefs", Context.MODE_PRIVATE);

return sp;

}

public void testEscapeCharactersInPreferences()

{

//Use a string that is a bit more complicated

//to see how escape characters work

String testString = "<node1>blabhhh</node1>";

//get shared preferences

SharedPreferences sp = getSharedPreferences();

//Prepare the shared preferences for save

SharedPreferences.Editor spe = sp.edit();

//add a key/value pair

spe.putString("test", testString);

//Commit the changes to persistence

spe.commit();

//Retrieve what is stored

String savedString = sp.getString("test", null);

if (savedString == null)

{

Log.d(tag,"no saved string");

return;

}

//Compare the two strings

Log.d(tag,savedString);

if (testString.equals(savedString))

{

Log.d(tag,"Saved the string properly. Match");

return;

}

//they dont match

Log.d(tag,"They don't match");

return;

}

让我们回顾一下清单 4-7 中的代码。我们首先使用静态全局上下文来使用myprefs.xml作为共享首选项文件来测试字符串的存储。我们还指出了在私有模式下创建和维护myprefs.xml。其他可能的模式如清单 4-8 所示。

清单 4-8。共享偏好设置的模式位

MODE_PRIVATE (value of 0 and default)

MODE_WORLD_READABLE

MODE_WORLD_WRITEABLE

MODE_MULTI_PROCESS

在清单 4-7 所示的测试方法中,我们使用了一个包含 XML 字符的字符串,知道共享的首选项存储在 XML 节点中。我们想看看共享偏好机制是否足够聪明来避开这些字符。

此时,您可能想知道共享首选项文件myprefs.xml是何时创建的,因为我们没有明确要求创建任何文件。文档表明,当我们试图使用清单 4-9 中所示的代码段保存第一个键/值对时,创建了共享首选项的底层 XML 文件。(这个清单摘自前面的清单 4-7。)

清单 4-9。使用编辑器保存和提交首选项

SharedPreferences.Editor spe = esp.edit();

spe.put...();

spe.commit();

从清单 4-9 中的代码,您可以看到SharedPreferences有点奇怪。我们不直接使用SharedPreferences对象来保存值。相反,我们使用它的内部类SharedPreferences.Editor来完成保存。这只是一种你必须习惯的模式。在项目的生命周期中,您可以选择向一个共享的首选项添加多个键/值对。最后,一个commit()将把它写到持久性存储中,这将保存对文件的多次写入。

此时一个奇怪的问题是,这个myprefs.xml存储在设备的什么地方?这个文件存储在设备的路径中,如清单 4-10 所示。

清单 4-10。共享首选项文件路径

/data/data/YOUR_PACKAGE_NAME/shared_prefs/myprefs.xml

在 eclipse/ADT 中,您使用文件管理器工具来查看模拟器或设备上的文件,并将myprefs.xml文件拖到本地驱动器并查看它。清单 4-11 显示了清单 4-7 中的代码创建的myprefs.xml文件。看看 Android 首选项如何对字符串值中的 XML 字符进行转义。

清单 4-11。共享首选项 XML 文件的内容

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>

<map>

<string name="test">&lt;node1&gt;blabhhh&lt;/ndoe1&gt;</string>

</map>

在共享偏好设置中使用 GSON 保存/恢复对象

现在,您已经拥有了从共享首选项中保存和恢复 Java 对象所需的所有信息。清单 4-12 显示了这是如何做到的。在这个例子中,我们创建了MainObject并使用 GSON 将其转换成 JSON。然后,我们使用共享首选项来存储它。我们像前面一样使用文件名myprefs.xml,我们使用MainObject JSON 字符串的键作为json。一旦我们成功存储了对象,我们就检索 JSON 字符串并将其转换回MainObject。然后,我们将它与原始对象进行比较,看它们是否匹配。

清单 4-12 .从共享首选项中存储/检索对象

public void storeJSON()

{

MainObject mo = MainObject.createTestMainObject();

//

Gson gson = new Gson();

String jsonString = gson.toJson(mo);

Log.i(tag, jsonString);

MainObject mo1 = gson.fromJson(jsonString, MainObject.class);

Log.i(tag, jsonString);

SharedPreferences sp = getSharedPreferences();

SharedPreferences.Editor spe = sp.edit();

spe.putString("json", jsonString);

spe.commit();

}

public void retrieveJSON()

{

SharedPreferences sp = getSharedPreferences();

String jsonString = sp.getString("json", null);

if (jsonString == null)

{

Log.i(tag,"Not able to read the preference");

return;

}

Gson gson = new Gson();

MainObject mo = gson.fromJson(jsonString, MainObject.class);

Log.i(tag,"Object successfully retrieved");

String compareResult = MainObject.checkTestMainObject(mo);

if (compareResult != null)

{

//there is an error

Log.i(tag,compareResult);

return;

}

//compareReesult is null

Log.i(tag,"Retrieved object matches");

return;

}

您可能想看看在执行清单 4-12 中的代码后,myprefs.xml是什么样子,因为这将让您有机会看到 GSON 是如何对字符串进行转义的。下面是清单 4-13 中的文件。

清单 4-13。演示共享首选项中的转义字符

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>

<map>

<string name="test">&lt;node1&gt;blabhhh&lt;/ndoe1&gt;</string>

<string name="json">{&quot;childList&quot;:[{&quot;name&quot;:&quot;Adam&quot;,&quot;likesVeggies&quot;:false

&quot;age&quot;:30},{&quot;name&quot;:&quot;Eve&quot;,&quot;likesVeggies&quot;:false

&quot;age&quot;:28}],&quot;stringArray&quot;:[&quot;first&quot;,&quot;second&quot;]

&quot;strinValue&quot;:&quot;st\u003cri\u003e\&quot;ng\&quot;Value\u003cnode1\u003etest\u003c/node2\u003e

&quot;,&quot;intValue&quot;:5}</string>

</map>

请注意清单 4-13 中 GSON 和 Android 首选项之间的转义是如何工作的。两者都有转义序列。所以不用担心包含特殊字符的对象中的字符串值。它们被 GSON 和 Android 偏好自动处理得很好。

作为讨论共享首选项的最后一点,我们提供了一个问题的答案,共享首选项中可以存储的数据量有限制吗?答案是它相当大,至少理论上是如此。实质上,设备上可用的内部存储量提供了上限。重要的是,Android 没有对共享偏好文件的大小施加任何任意限制。但是,由于所有应用共享同一个存储设备,所以您需要谨慎使用多少存储设备。可能任何小于 10MB 的都是合理的。

为 JSON 使用内部存储

最终,共享的首选项作为键/值对存储在 XML 文件中。Android 以一种特殊的方式处理共享偏好,正如前面共享偏好一节所展示的。例如,共享首选项是文件这一事实不会直接暴露给程序员。此外,键/值对作为节点/值对存储在 XML 文档中。相反,您可能希望完全控制存储 JSON 的文件。在这种情况下,您可以使用 Android 提供的内部存储选项。这些内部文件与共享首选项 XML 文件非常相似,也是专门为您的应用存储在设备上的,默认情况下是私有的。

从内部存储器存储和检索

在内部文件中存储和检索 JSON 非常简单。Android SDK 有一个打开和关闭内部文件的 API。Android SDK 还决定这些文件驻留在哪里。作为一名程序员,你可以控制文件名。您还可以控制何时读取/写入它们。您可以使用标准的 Java I/O 库从文件流中读取和写入文件流。

清单 4-14 和 4-15 中的代码段演示了如何完成以下工作:

Open an internal file for writing   Convert objects to JSON and write to the file   Open the same internal file for reading   Read the JSON string from the file   Convert the JSON string to objects   Compare the source and target objects to ensure they are same  

清单 4-14 .从内部文件存储中存储/检索对象

public void saveJSONToPrivateStorage()

{

String json = createJSON();

saveToInternalFile(json);

String retrievedString = this.readFromInternalFile();

//Create the object from retrievedString

Gson gson = new Gson();

MainObject mo = gson.fromJson(retrievedString, MainObject.class);

//makesure it is the same object

MainObject srcObject = MainObject.createTestMainObject();

String compareResult = mo.checkTestMainObject(srcObject);

Log.i(tag,compareResult);

}

private String createJSON()

{

MainObject mo = MainObject.createTestMainObject();

Gson gson = new Gson();

String jsonString = gson.toJson(mo);

return jsonString;

}

private String readFromInternalFile()

{

FileInputStream fis = null;

try {

Context appContext = MyApplication.s_applicationContext;

fis = appContext.openFileInput("``datastore-json.txt

String jsonString = readStreamAsString(fis);

return jsonString;

}

catch(IOException x)

{

Log.d(tag,"Cannot create or write to file");

return null;

}

finally

{

closeStreamSilently(fis);

}

}

private void saveToInternalFile(String ins)

{

FileOutputStream fos = null;

try {

Context appContext = MyApplication.s_applicationContext;

fos = appContext.openFileOutput("datastore-json.txt"

,Context.MODE_PRIVATE);

fos.write(ins.getBytes());

}

catch(IOException x)

{

Log.d(tag,"Cannot create or write to file");

}

finally

{

closeStreamSilently(fos);

}

}

一点都不复杂。清单 4-14 中的代码使用了几个文件工具方法。你可以根据自己的喜好来设计这些方法。但是如果你想看看它们的快速实现,清单 4-15 中有这些方法。

清单 4-15。支持文件实用程序方法

private void copy(InputStream reader, OutputStream writer)

throws IOException

{

byte byteArray[] = new byte[4092];

while(true)   {

int numOfBytesRead = reader.read(byteArray,0,4092);

if (numOfBytesRead == -1)      {

break;

}

// else

writer.write(byteArray,0,numOfBytesRead);

}

return;

}

private String readStreamAsString(InputStream is)

throws FileNotFoundException, IOException

{

ByteArrayOutputStream baos = null;

try    {

baos = new ByteArrayOutputStream();

copy(is,baos);

return baos.toString();

}

finally    {

if (baos != null)

closeStreamSilently(baos);

}

}

private void closeStreamSilently(OutputStream os)

{

if (os == null) return;

//os is not null

try {os.close();} catch(IOException x)    {

throw new RuntimeException(

"This shouldn't happen. exception closing a file",x);

}

}

private void closeStreamSilently(InputStream os)

{

if (os == null) return;

//os is not null

try {os.close();} catch(IOException x)    {

throw new RuntimeException(

"This shouldn't happen. exception closing a file",x);

}

}

在清单 4-14 中,我们使用了文件名datastore-json.txt。您可以在您的设备上找到这个文件,位置如清单 4-16 所示。

清单 4-16。Android 内部文件的文件位置

data/data/<your-pkg-name>/files/datastore-json.txt

一旦执行了清单 4-14 中的代码,就可以使用 Eclipse/ ADT 中的文件管理器来查看这个文件的内容。这个文件的内容将类似于清单 4-17 所示的文本。

清单 4-17。内部存储文件中的 JSON 字符串

{"childList":[{"name":"Adam","likesVeggies":false,"age":30}

{"name":"Eve","likesVeggies":false,"age":28}],"stringArray":["first","second"]

"strinValue":"st\u003cri\u003e\"ng\"Value\u003cnode1\u003etest\u003c/node2\u003e"

"intValue":5}

您可以使用多个文件来存储多个 Java 根对象,以细分存储的粒度。这将有助于某种程度的优化。

在外部存储器上存储 JSON

Android SDK 提供 API 来控制外部 SD 卡上的目录和文件。正如我们在前面的“数据存储选项”一节中指出的,这些外部文件是公共的。如果您选择将 JSON 字符串存储在这些外部文件中,那么您可以遵循与上一节中描述的内部文件相似的模式。然而,在大多数情况下,外部存储选项有很好的理由。

因为通常您表示为 JSON 的数据是特定于您的应用的,所以将其作为外部存储是没有意义的,外部存储通常用于音乐文件、视频文件或其他应用可以理解的通用格式的文件。因为诸如 SD 卡的外部存储器可以处于各种状态(可用、未安装、已满等。),当数据足够小,可以通过更简单的内部文件管理时,很难为简单的应用编写这种卡。

因此,我们不认为应用状态是在外部存储上维护的。如果应用需要音乐和照片,那么混合方法可能是有意义的,这些可以放在外部存储上,而您将核心状态数据保存在 JSON 和内部文件中。如果您选择使用外部存储,请参考 Android SDK 文档,了解一些正确管理外部存储的 API。

将 SQLite 用于结构化存储

没有什么像 GSON 一样简单和甜蜜的事情可以不存在。如果每次发生变化时都要编写文件,那么这种 JSON 方法的成本会很高。当然,存在一个平衡点,在这个平衡点上,您希望将代码迁移到 SQLite 并使用粒度更新。

如果是这样的话,您可以构建您的代码,这样您就可以用 SQLite 之类的替代品来交换持久层,而不会对您的代码进行重大修改。要做到这一点,把你的应用分成一个持久性服务层和其余部分。您将需要足够的训练来使用服务层作为一组无状态的服务,您可以在以后使用不同的实现。记住接口和服务接口防火墙。在参考资料中,我们包含了一篇文章的链接,这篇文章是作者之一在 2006 年写的,内容是关于极限原型的。您可以借用相同的原则来构建一个可以交换的服务层。

参考

我们发现以下链接对本章的研究很有帮助。

下载本章专用的测试项目:www.androidbook.com/expertandroid/projects.ZIP 文件的名称是ExpertAndroid_ch04_TestGSON.zip

摘要

GSON 有助于将您的应用快速部署到市场。这是测试市场实力的好方法。它也有助于快速发布大量简单的应用,只需做最少的持久性工作。为了实现这个目标,我们在本章中介绍了如何使用 GSON 将对象存储在共享的首选项或内部文件中。在这个过程中,我们还讨论了 Android 的其他数据存储选项,看它们是否适合存储 JSON。

复习问题

以下一组问题应该可以巩固您在本章中学到的内容:

What are the five different storage options for Android applications?   How would you use JSON for persisting Android application state?   What are the pros and cons of using JSON for persistence?   What is GSON?   Can you save nested objects using GSON?   Can you save nested collections of objects using GSON?   How are characters in strings escaped in GSON?   How do you add external jar files like the GSON jar to the Android APK file?   What are shared preferences?   What is the difference between getSharedPreferences and getPreferences?   How do you get a context independent of an activity?   Where are shared preference files stored?   How do you save JSON to a shared preference file?   What does the saved preference file look like?   How do you read JSON from shared preferences back as Java objects?   How do escape characters work in Android preferences?   What is internal storage?   How do you save to internal storage?   How do you restore objects from internal storage?   Should you use external storage like the SD card?   What are the useful references while working with this approach?   How do you code in such a way that you can migrate in the future to SQLite?

五、多设备编程

Abstract

Android 的一个奇妙之处是有如此多的设备运行它。从手机到平板电脑,从电视到相机到跑步机,Android 作为各种电子设备的软件平台非常受欢迎。这对应用开发人员来说也是一个巨大的挑战。设备之间的差异可能很多,从不同尺寸的屏幕到不同的屏幕密度,从不同的硬件功能到不同版本的 Android。优雅地处理所有这些差异是可能的,而且可能比你想象的更容易。

Android 的一个奇妙之处是有如此多的设备运行它。从手机到平板电脑,从电视到相机到跑步机,Android 作为各种电子设备的软件平台非常受欢迎。这对应用开发人员来说也是一个巨大的挑战。设备之间的差异可能很多,从不同尺寸的屏幕到不同的屏幕密度,从不同的硬件功能到不同版本的 Android。优雅地处理所有这些差异是可能的,而且可能比你想象的更容易。

随着 Android 的发展,开发人员也有了更多的选择。本章将探讨设备差异以及如何处理它们,这样一个应用就可以在你想要的任意多的设备上运行。

不同的屏幕密度

你需要完全理解的第一个概念是屏幕密度。在这个领域有很多术语,也有一些常见的误解。首先,表 5-1 列出了一些术语及其真实含义。

表 5-1。

Density Terms and Their Meanings

| 学期 | 定义 | 意义 | | --- | --- | --- | | 像素 | 图像中的一个颜色点 | 像素可以是屏幕上的实体,也可以是构成数字图像的点(在文件中或内存中) | | 像素 | Android 屏幕像素 | 视频屏幕上显示颜色的微小物理点。你也可以称之为绝对像素。术语 px 在 Android 中与维度一起使用。 | | 像素 | 每英寸像素 | 每英寸显示器的实际物理显示像素数 | | (灰)点/英寸 (扫描仪的清晰度参数) | 每英寸点数 | DPI 最初是一个打印概念,但有时也用于描述视频显示。然而,视频像素通常由多种颜色的点组成,因此 DPI 在视频显示方面会令人困惑。避免使用这个术语。 | | 数据处理 | 密度无关像素 | 最好称之为虚拟像素,因为它不存在于现实世界中。但 Android 使用这些作为应用中图形对象的测量单位。一个 dp 大约相当于一个 160 PPI 的像素。 | | Sp | 与比例无关的像素 | 非常类似于 dp,但仅用于字体。 |

关于像素的一切

屏幕是物理对象,其表面由物理像素组成。像素的产生方式会因屏幕技术的不同而不同;例如,一些使用不同颜色的多个点来产生一个像素。当一个屏幕的 1 英寸表面上有很多像素时,我们说像素密度高。确切的数值是屏幕的 PPI。例如,谷歌 Nexus 10 平板电脑的 PPI 为 300。这意味着 1 英寸是 300 像素宽。换句话说,谷歌 Nexus 10 像素是 1/300 英寸宽。最初的摩托罗拉 Xoom 平板电脑的 PPI 为 149,即每英寸 149 个物理像素。然而,这两款平板电脑的整体物理尺寸大致相同。Nexus 10 的像素密度(PPI)高于 Xoom。这意味着 Nexus 10 可以在 1 英寸见方的屏幕上显示比 Xoom 更多的细节。

Note

一个常见的误解是大屏幕具有更高的屏幕密度(PPI)。这是完全不正确的,谷歌 Nexus 10 和摩托罗拉 Xoom 平板电脑的比较就说明了这一点。

位图、图标、字形等图形图像也是由像素组成的,但是是按颜色的行和列排列的。行数代表图像的高度,列数代表宽度。但是图像的尺寸不是用英寸来表示的;它们的尺寸只是宽度和高度。

如果您决定将 96x96 位图图形 1 图像像素对 1 屏幕像素映射到 Nexus 10 平板电脑的屏幕上,它将占用 96 屏幕像素的宽度和 96 屏幕像素的高度,或者大约 0.32 英寸乘 0.32 英寸的屏幕。同样的位图以同样的方式显示在 Xoom 平板电脑上,每个维度也将占用 96 个像素,但那将是 0.64 英寸乘 0.64 英寸。这似乎不是一个很大的区别(每边只多 0.32 英寸),但它是。Xoom 上的位图看起来要大四倍!这是一个问题,因为您希望在总体大小相同的设备上保持应用外观的一致性。

将视图映射到密度

无论平板电脑的制造商是谁,平板电脑应用中的按钮看起来都应该差不多。如果您为 Xoom 创建的图形文件版本的大小是 96x96 图像的四分之一(即 48x48),那么它在 Xoom 上显示时的大小将与在 Nexus 10 上显示的 96x96 相同。或者,如果有人动态地将图形缩放到 Xoom 的四分之一大小,那也是可行的。坚持这样的想法,因为你稍后会回到这个问题上。

然而事情变得更加复杂,因为真实的屏幕有许多不同的 PPI 值。Android 团队不希望开发人员不得不处理将图形转换成每一种密度大小的数学问题,以便它在不同的屏幕密度上看起来都正确。为所有可用设备上的每个 PPI 创建唯一的图形文件也没有意义。因此,他们决定了四个密度大小的桶,称为 ldpi、mdpi、hdpi 和 xhdpi(代表低、中、高和超高),它们分别对应于 120、160、240 和 320 的 PPI。设备制造商选择他们的设备属于哪个类别,然后为该设备配置 Android,告诉应用它是这些 dpi 值之一。

设备的真实 PPI 可能与规定的密度大小(即桶大小)不同,但这没关系。在上面提到的两个片剂中,没有一个与这些值中的一个完全相同。Xoom 被归类为 mdpi 设备,因为 149 最接近 160。谷歌 Nexus 10 平板电脑被归类为 xhdpi 设备,因为 300 最接近 320。作为开发者,你不需要知道或者关心设备的实际 PPI 您将只处理设备的屏幕密度-大小桶 PPI 值。

上面提到的可能解决方案——缩放图形以适应设备的实际 PPI,并为不同的屏幕密度创建多个版本的图形——都适用于 Android。制造商处理规定的屏幕密度和设备的真实 PPI 之间的调整,开发人员负责创建每个图形文件的版本,每个版本对应应用支持的每个密度大小的存储桶。这意味着在为应用创建图形文件时,您只需担心四个 PPI 值。事实上,如果你愿意,Android 甚至可以处理桶之间的缩放。

密度比例因子

事实证明,桶大小之间的比例因子是很好的简单值。如果我们说 mdpi 是 1.0,那么 ldpi 是那个的 0.75,hdpi 是那个的 1.5,xhdpi 是那个的 2.0。如果您希望 200x200 的图形在不同的设备上以相同的大小呈现给用户,那么您可以为 ldpi 设备制作 75x75 的版本,为 mdpi 制作 100x100 的版本,为 hdpi 制作 150x150 的版本,为 xhdpi 制作 200x200 的版本。最佳实践是以尽可能高的密度创建您的原创作品,或者更好的是,使用矢量图形包,然后从那里为 Android 密度桶生成图形。这样做的原因是,你想要尽可能多的细节。从较小的尺寸开始并将图形放大到较大的尺寸会导致像素化或块化,即在图像中看到块而不是平滑曲线和渐变的效果。

另一种方法是只为 mdpi 提供一个 100x100 的文件,Android 会相应地为其他密度进行缩放。但是,如果您选择这种方法,从 mdpi 自动缩放到 xhdpi 的图形可能看起来不像您希望的那样清晰。

从技术上讲,还有另一个非常高端的密度:xxhdpi。这对应于大约 480 PPI 的密度和 3.0 的比例因子。谷歌表示,不要担心提供这种密度的图形文件,但随着市场上出现这种密度的设备,如果这会影响到你,不要感到惊讶。您可能需要提供额外的图形文件,这将增加应用的整体大小,并可能对性能和应用限制产生其他影响。

Android 团队定义了另一个屏幕密度-尺寸桶,那就是 tvdpi。它的可比 PPI 约为 213,位于 mdpi 和 hdpi 之间,旨在用于谷歌电视应用。建议不要担心智能手机或平板电脑应用的密度。但是,如果您确实想创建这种密度的图形,您可以为 mdpi 创建 100x100 的图形,并为 tvdpi 创建 133x133 的版本。

资源目录

在 Android 应用的应用文件中,ldpi、mdpi、hdpi、xhdpi 和 xxhdpi 的图形文件(也称为 drawables)有单独的目录。例如,ldpi 可绘制文件位于 res 目录下的 drawable-ldpi 目录中。基于运行应用的设备的屏幕密度设置(即 ldpi、mdpi、hdpi、xhdpi 或 xxhdpi),Android 将从适当的目录中选择适当的图形文件。如果 Android 无法在设备的屏幕密度大小桶的相应资源目录中找到命名的 drawable,它将在默认的 drawable 目录(/res/drawable)中查找 drawable 文件,或者从另一个 drawable 目录中选择一个并适当地缩放它。Android 通过选择在缩放时产生最佳结果的目录来决定其他可绘制目录的最佳选择。

如果你创建一个新的 Android 项目并查看初始的 drawable 目录,你应该会看到名为 ic_launcher.png 的默认图标文件(或者类似的文件)。请注意,每个目录中的文件名都是相同的。这很重要;您将使用文件名在布局文件中指定图像,因此不同密度的文件名必须相同——否则 Android 将无法找到它。如前所述,您实际上不需要为每个目录提供每个图形文件。Android 将选择一个,并根据运行该应用的设备的需要进行缩放。

使用 dp 指定尺寸

您已经了解到,不同的屏幕密度被简化为五个密度大小的桶,开发人员应该为属于这些桶的设备提供不同版本的图形文件。但是,Android 应用的 UI 中的图形元素的规范是怎样的呢?Android 如何知道使用哪个图形?你如何简单地设计一个用户界面?

这就是密度无关像素发挥作用的地方。使用缩写“dp”,您应该使用与密度无关的像素大小来指定图形元素的大小。与密度无关的像素大约相当于 160 PPI 屏幕上物理像素的大小。通过使用“dp”指定可绘制的大小,Android 将确定用于在运行应用的设备上绘制可绘制内容的实际物理像素数量。如果该设备被分类为 mdpi 设备,所使用的物理像素的数量将与 dps 的数量大致相同。如果设备是 xhdpi,那么使用的物理像素数将是 dps 的两倍。这意味着图形在任一设备上的尺寸将大致相同,并且布局指令将使用相同数量的 DP。例如,如果您在 UI 中用以下命令指定一个ImageView:

android:layout_width="96dp"   android:layout_height="96dp"

那么当在 mdpi 屏幕上并使用 mdpi 可绘制文件时,图像将是大约 96×96 像素。它在 ldpi 屏幕上将是 72x72 像素并使用 ldpi 可绘制文件,在 hdpi 屏幕上将是 144x144 像素并使用 hdpi 可绘制文件,在 xhdpi 屏幕上将是 192x192 像素并使用 xhdpi 可绘制文件。即 72 是 96 的 0.75 倍;144 是 96 的 1.5 倍;192 是 96 的 2 倍。

一个真正是 216 PPI 的设备,如谷歌 Nexus 7 平板电脑,很可能在 hdpi (240 PPI)桶中,Nexus 7 也是如此。在幕后,调整正在发生。如果你的应用为一个正方形图形指定了 480x480dp,那将计算出每边大约 3 英寸的距离。(请记住,dp 大约相当于 160 PPI 像素,每英寸 480 除以 160 像素等于 3。)在谷歌 Nexus 7 的情况下,图形不会是每边 2 英寸(480 除以 240),也不会是每边 2.222 英寸(480 除以 216)。Android 正在进行调整,以便图形每边 3 英寸,或每边约 648 个谷歌 Nexus 7 像素。然而,由于这里正在进行调整,图形也可能不是每边 3 英寸。如果你认为一个图形会以一个绝对尺寸出现在屏幕上,实际上可能会有一点不同。

关于 Android 的另一个误解是,你可以确定屏幕的实际 PPI。不幸的是,(到目前为止)还没有可靠的 API 调用返回这些信息。文档中说要使用DisplayMetrics(详见本章末尾的参考文献)和属性xdpiydpi。然而,在某些情况下,检索到的数据甚至与事实不符。不要试图在 Android 屏幕上绘制东西,因为你需要绘制的图像具有精确的尺寸——除非你将你的应用绑定到一个设备上,在这个设备上你确切地知道你在屏幕上处理什么,以及如何进行缩放和调整。但这严重限制了你的应用。

不同的屏幕尺寸

您的应用很可能会根据显示它的屏幕的整体大小进行不同的布局,而不考虑屏幕的实际物理像素分辨率。如果你的目标是 10 英寸的平板电脑,你几乎肯定会有相同的按钮、标签、输入栏、图像等排列。,平板电脑是低密度(ldpi)还是超高密度(xhdpi)并不重要。密度将影响哪个图形图像文件用于屏幕上的ImageView,但不影响它对用户显示的大小或它在屏幕上的位置。

筛网尺寸桶

值得重申的是,设备的屏幕尺寸与其屏幕密度并不对应。屏幕密度与要在屏幕上显示的图形文件的外观以及显示细节的能力密切相关,但是很有可能大屏幕密度低,小屏幕密度高。表 5-2 显示了截至 2013 年 5 月 1 日的已知设备组合,包括其尺寸和密度。

表 5-2。

Known Device Sizes and Densities (as of May 1, 2013)

|   | ldpi(消歧义) | mdpi(mdpi) | tvdpi | hdpi | xhdpi | xxhdpi | | --- | --- | --- | --- | --- | --- | --- | | 小的 | 9.8% |   |   |   |   |   | | 标准 | 0.1% | 16.1% |   | 37.3% | 24.9% | 1.3% | | 大的 | 0.6% | 2.9% | 1.0% | 0.4% | 0.7% |   | | 品牌介绍 | 0.2% | 4.5% |   | 0.1% | 0.1% |   |

正如你在表 5-2 中看到的,一些设备似乎确实证明了尺寸和密度是相辅相成的,一些小屏幕是 ldpi,一些超大屏幕是 xhdpi。但是绝大多数平板(大屏和超大屏)是 mdpi 密度,绝大多数手机(普通屏)是 hdpi 或 xhdpi 密度。这再次证明了屏幕尺寸与屏幕密度无关。

屏幕尺寸与你一次能向用户显示多少内容有关。屏幕越大,可以显示的内容就越多。这里,制造商又制造了许多屏幕尺寸可供选择,对于一个应用来说,单独处理每一个可能的精确屏幕尺寸几乎是不可能的。因此,Android 团队选择了四个屏幕大小的桶进行选择:小、普通、大和 xlarge。前两种一般是手机,后两种一般是平板。大尺寸最初是为 7 英寸平板电脑设计的,而 xlarge 是为 10 英寸平板电脑设计的。

事实证明,电视属于大型类别,尽管它们明显大于 10 英寸。其原因是电视是从远处观看的,因此屏幕上的对象必须占据更多的物理像素才能被正确地看到。如果电视被视为超大,则项目可能太小,观众无法阅读。当然,有些电视很大,所以这些桶不一定是找出如何布置屏幕的最佳方式。你很快就会发现还有另一种方法。

重访布局

布局通常是你在应用的用户界面中指定一切的方式。它们表示为 XML 文件,包含按钮、标签、输入字段、图像、片段和其他用户界面对象的标签。这些视图对象排列在布局标记内。

如果显示器是平板电脑,常见的布局模式是主/细节,其中屏幕一侧的列表允许用户进行选择,并且关于该选择的详细信息可以同时出现在屏幕另一侧。许多电子邮件应用在平板电脑上就是这样工作的。但是在较小的屏幕上,比如手机,没有足够的空间来同时显示列表和细节。因此,列表是单独显示的,如果用户单击列表中的某个项目,就会在列表活动的顶部显示一个详细活动。然后,用户按 Back 键返回到项目列表。见图 5-1 。

A978-1-4302-4951-1_5_Fig1_HTML.jpg

图 5-1。

Tablet vs. phone screen layout

在图 5-1 中,左边和中间的布局代表了上面有碎片的平板电脑。右边的布局代表智能手机屏幕,从本质上来说,它更小。智能手机使用片段,但一次只能看到一个片段。想象一下,如果你愿意,这是所有三个设备上的相同应用。平板电脑横向模式的布局不同于平板电脑纵向模式的布局,智能手机纵向模式的布局也不同。从功能的角度来看,Frag 中的列表会发生什么。1 对三者都是一样的。也就是说,适配器会将数据拉进一个列表,并在右边显示一个滚动条。当用户点击列表中的一个条目时,列表条目的详细信息将显示在 Frag 中。2(可能与片段中的列表同时可见,也可能不可见。1).你想尽可能多地重复使用碎片。1,以及背后的代码,同时因为设备不同而容纳不同的布局。现在让我们来看看如何管理不同设备的不同布局。

使用布局处理屏幕尺寸

应用活动的布局将根据屏幕大小而有所不同。根据设备是处于横向模式还是纵向模式,它们也会有所不同。当你的应用显示用户界面时,Android 会根据设备的屏幕大小和创建活动时设备的方向找到合适的布局。布局文件中包含布局和控件定义。

和以前/res 下不同的目录用于不同密度的图形文件一样,Android 在/res 下有不同的目录用于不同的布局。在这种情况下,可以有很多不同的目录。不仅可以有不同的大小和方向布局(最常见的),还可以根据国家、语言、夜间模式和许多其他条件有不同的布局。当 Android 需要定位一个命名的布局资源文件时,它会根据可用的同名布局资源文件选择最佳的一个,然后使用布局目录名称上的其他限定符来与设备的属性和当前配置进行比较。

指定布局目录的最简单方法如下:

/res/layout-normal-land

这代表正常尺寸屏幕上的横向布局。您可以为小、大、超大屏幕以及端口(纵向)模式创建替代布局目录。您将在您的应用项目中找到一个默认的/res/layout目录;如果 Android 找不到更好的东西可以使用,它就会去那里寻找。您也可以将布局文件存储在默认目录中,以包含在其他目录中的其他布局文件中。这允许您创建部分布局,并在应用中的任何地方使用它们,而只需维护它们一次。

你不应该根据设备的屏幕密度(PPI)创建布局目录。对于布局,你真的不关心密度是多少。你确实关心你要处理多少屏幕空间。在这里,Android 更喜欢使用密度独立像素(dp)规格的屏幕尺寸。屏幕尺寸(通常以英寸表示)不如 dps 中的屏幕尺寸重要。表 5-3 显示了不同筛网尺寸的铲斗与 dp 尺寸的关系。

表 5-3。

Screen-size bucket specs in dps

| 屏幕大小的存储桶名称 | 至少应该是这样(以 dp 表示) | | --- | --- | | 小的 | 426 x 320 | | 标准 | 470 x 320 | | 大的 | 640 x 480 | | 品牌介绍 | 960 x 720 |

屏幕尺寸用高度和宽度表示,其中高度大于宽度。这与设备的正常使用方式无关(即,横向模式还是纵向模式)。现在我们可以介绍指定布局目录的另一种方法。

Android 团队发现屏幕大小的桶是不够的。从 Android 3.2 开始,他们根据 dp 宽度和 dp 高度为布局资源目录添加了一些额外的限定符。由于大多数用户界面会垂直滚动,对用户来说很自然,所以很少看到布局资源目录使用高度限定符。所以,宽度是你会最感兴趣的限定词。这里,Android 团队有两个选项:最小宽度和当前宽度。最小宽度是一个系统值,表示屏幕尺寸的两个维度中较短的一个,与设备的方向无关。当前宽度是器件在当前方向上的宽度。布局资源目录名中的限定符将分别是–sw<N>dp–w<N>dp,其中<N>是您想要指定的宽度的最小尺寸。例如,使用/res/layout-sw600dp/res/layout–w720dp指定 600 个或更多密度无关像素的最短宽度,或 720 dp 或更多的当前宽度。

您的应用中可能有多个带有–w<N>dp限定符的布局目录,根据设备的方向选择最大的一个。例如,如果你的应用运行在一个 10 英寸的平板电脑上,并且你有一个–w600dp和一个–w1000dp布局资源目录,当 10 英寸的平板电脑处于纵向模式时,将选择–w600dp布局;当平板电脑处于横向模式时,将选择–w1000dp布局。

如果使用–sw<N>dp限定符,情况就不一样了。该器件的最小宽度为或更大,或者没有。布局文件选择不会因方向而改变。因此,如果您正在使用 –sw<N>dp限定符,并且您想要不同的纵向和横向布局,您需要添加–land和/或–port作为另一个限定符,以便为您的应用选择预期的布局文件。

根据你运行的 ADT 版本的不同,当你创建一个新的 Android 项目时,Google 可能会默认提供/res/values–sw600dp/res/values–sw720dp-land资源目录。但是,对于布局,默认只是默认的布局资源目录。查看默认的布局 XML 文件,您会发现一些边距规格,这些规格引用存储在这些默认值资源目录下的维度。尺寸文件名为 dimens.xml。在这些文件中,你会发现<dimen>标签覆盖了布局 xml 文件中的尺寸。这是另一种针对不同设备和方向定制布局的技术,无需创建大量不同的布局 XML 文件。在运行时,布局的值将基于设备的当前配置来自不同的值资源文件。

屏幕之外的不同限定符

我们已经讨论了与可绘制性和布局有关的限定符,但是 Android 还有许多其他限定符,您可以利用它们在您的应用中提供最佳的用户体验。虽然有许多限定词与键和键盘、夜晚与白天等等有关,但让我们简要地讨论一下语言。您可能不认为语言会对您的布局产生影响,但是您可能还没有见过按钮和标签被翻译成另一种语言的用户界面。填满按钮、占据标签空间的翻译单词可以极大地改变用户界面的外观。按钮可以更宽或更窄;标签也是如此。这可能会迫使您为其他语言创建替代布局。

不同的硬件特性

Android 设备可以有许多不同的硬件功能。一些设备具有陀螺仪、GPS、光传感器、磁力计、相机、蓝牙等,而其他设备几乎没有附加的硬件功能。因为把你的应用安装到一个没有你的应用正常运行所需硬件的设备上是没有意义的,所以 Android 提供了一种机制,让你可以声明你的应用需要什么硬件。通过这种方式,Google Play 将从没有该硬件的设备上的用户那里过滤掉您的应用。例如,如果您的应用需要前置摄像头,则使用没有前置摄像头的设备的用户将无法在 Google Play 中看到您的应用。

使用标签

应用的AndroidManifest.xml文件应该包含应用使用的每个硬件特性的<uses-feature>标签。标签的语法如下:

<uses-feature android:name="name.of.feature" android:required="true" android:glEsVersion="#" />

其中name.of.feature将被替换为特性的适当字符串名称,例如android.hardware.camera.front。这个标签应该在<manifest>里面,在<application>之前。必需的属性可以是 true 或 false,因此可以指定您的应用可以使用一个硬件特性(如果它存在的话),但这不是必需的。如果android:required被设置为false,你的应用实际上将在 Google Play 中对不支持该硬件功能的用户可见。这也意味着当硬件特性不存在时,您的应用需要是优雅的。在您的应用中,您可以执行以下操作:

boolean hasFrontCamera =

getPackageManager()

.hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT);

然后,当hasFrontCamerafalse时,采取适当的步骤以正常运行。

当您的应用需要高级版本的 OpenGL 库时,可以选择使用最后一个属性(android:glEsVersion)。所有的机器人都支持 1.1 版,所以如果你只需要这个属性或者你的应用不使用 OpenGL,你就不需要指定这个属性。如果需要 2.0 版本,那么将值设置为“0x 00020000”;对于 2.1 版,将该值设置为“0x00020001”高 16 位代表主版本号,低 16 位代表次版本号。

我们不要忘记,访问设备上的摄像头也需要权限(android.permission.CAMERA),并且用户必须在安装应用时授予权限。那么<uses-permission>如何与<uses-feature>合作呢?清单文件中必须有<uses-permission>标签,用于您的应用使用的任何需要权限的内容。<uses-permission>标记没有必需的属性;没有办法使许可成为可选的。对于与硬件特性相关的<uses-permission>标签,Android 将隐含地假设一个相应的<uses-feature>标签,required 设置为 true。这个规则的例外是蓝牙;更多详情请参见<uses-feature>的在线参考文档。

最佳实践是始终指定应用需要的所有<uses-feature>标签。但是如果您的应用不需要这个特性,您必须指定一个<uses-feature>标签(将android:required设置为false)。否则,<uses-permission>将导致您的应用对没有该硬件特性的设备隐藏。

如果您的应用需要某个硬件功能的权限,但该功能不是必需的,则在安装时,即使该功能在该设备上不存在,用户仍会看到正在请求权限。你可能想在 Google Play 上的应用描述中加入一些东西,向用户解释这一点,这样他们就不会混淆。

使用和标签

值得指出的是,屏幕尺寸是一种硬件特性,但是使用的标签不是<compatible-screens>就是<supports-screens>。使用这些标记类型之一,您可以指定应用可以在哪些类型的设备屏幕上运行。在第一种情况下,在中,您将提供单独的标签,列出适用于您的应用的筛桶尺寸和筛桶密度的组合。Google Play 将使用这些组合对不具备所列组合之一的设备隐藏您的应用。如果你不列出一个具体的组合,Google Play 就不会用那样的设备向用户展示你的应用。例如:

<compatible-screens>

<screen android:screenSize="normal" android:screenDensity="mdpi" />

<screen android:screenSize="normal" android:screenDensity="hdpi" />

</compatible-screens>

将导致您的应用仅对具有正常屏幕尺寸和中高像素密度的用户可见。这也意味着当新的 xxhdpi 屏幕出现时,它们将看不到您的应用。这可能是一件好事,也可能是一件坏事,取决于你的观点。

Note

<compatible-screens>仅由 Google Play 和相关服务使用。它从不在设备上使用或被设备使用。

相比之下,下面的例子:

<supports-screens android:smallScreens="false"

android:normalScreens="false"

android:largeScreens="true"

android:xlargeScreens="true"

android:requiresSmallestWidthDp="600" />

说明您的应用可以在最小边长至少为 600dp 的大型或超大屏幕设备上运行。然而,这个标签实际上是告诉 Google Play 支持的最小屏幕尺寸。任何大于第一个且属性值为 true 的屏幕尺寸也将在 Google Play 中看到该应用。如果您将 normalScreens 属性值设置为 true,并将 largeScreens 和 xlarge screens 属性值设置为 false,您的应用在 Google Play 中对于大屏幕和 xlargeScreens 设备仍然可见。Android 认为可以在更大的屏幕上调整应用的大小;如果不希望调整大小,请将android:resizeable属性指定为 false。如果你真的想精确地控制哪些设备可以接收你的应用,那就坚持使用<compatible-screens>

不同版本的 Android

在这最后一节,我们将介绍如何处理“在野外”存在的所有不同版本的 Android。成为这么多不同设备上的流行操作系统是福也是祸。当谷歌发布新的操作系统时,大多数制造商和运营商不会迅速为他们的所有设备升级 Android 操作系统。有些从未升级。其他人在几个月后获得一次升级,然后再也没有获得另一次升级。其结果是许多设备在野生运行弗罗育(2.2),姜饼(2.3),冰淇淋三明治(4.0),和果冻豆(4.1,4.2 和 4.3)。还有其他设备运行 Donut (1.6)、éclair(2.1)和 Honeycomb (3.x),但它们现在很少。

Android 的每一个新版本都有新的特性,一些类和/或方法被弃用,所以它们不应该再被使用。一些特性甚至在不同版本之间改变它们的含义(例如,Froyo 之后的 MotionEvent 中的指针 id 和指针索引)。在某些情况下,Android 编程的主要概念会被移植回兼容性库中的旧版本。开发者该怎么做?

标签

首先,清单文件中的标签可以而且应该用来指定与 Android 版本的兼容性。它的语法是:

<uses-sdk android:minSdkVersion="integer"

android:targetSdkVersion="integer"

android:maxSdkVersion="integer" />

其中 minSdkVersion 告诉 Google Play 将运行该应用的最老版本是什么,maxSdkVersion 告诉 Google Play 将运行该应用的最新版本是什么。整数值是对应于更常见的 Android OS 版本的 API 级别。例如,Android Froyo 版的 SdkVersion 编号为 8。maxSdkVersion 值与设备本身没有关系,因此,如果应用安装在设备上,并且设备随后接收到超过 maxSdkVersion 的操作系统更新,则应用仍将存在于设备上,并且用户仍将被允许运行它。Android 团队表示,Android 的未来版本是向后兼容的,只要开发者遵循最佳实践,新的操作系统将负责让应用工作。

如果操作系统的版本比 targetSdkVersion 新,Android 的内置兼容性行为就会发挥作用。如果设备的操作系统版本与 targetSdkVersion 匹配,则认为应用已经过全面测试,不需要操作系统进行特殊处理。

因此,理论上,您可以为您的应用创建单独的 apk,每个版本的 Android 一个,并相应地设置<uses-sdk>标签。你甚至可以为 Google Play 中的单个应用列表上传多个 apk,这样用户会觉得这是一个应用,而实际上可能有很多。但是,不要这样。您已经看到,设备上的操作系统更新会导致应用 APK 与操作系统不匹配。如果你开始针对下一个更高版本的操作系统测试你的应用 apk,你会发疯的。你还会发现,无论如何,你的大部分应用在不同版本的 Android 之间是相同的。所以你不妨换一种方式。

您仍然需要使用标签来指定 SDK 的最低版本和目标版本。设置最大值并不重要,事实上谷歌也不鼓励这样做。基于您的应用所需的特性和 Android APIs,您希望选择适合您和您的用户的最低版本的 Android。这就是您将用于 minSdkVersion 属性值的内容。然后,您希望为您认为最受欢迎的版本指定 targetSdkVersion,或者使您的生活最轻松的版本。这同样可以基于该版本 Android 中可用的 API。选择一个缺少您的应用可以使用的东西的 minSdkVersion 是很好的,只要您的应用在没有该东西的情况下仍然可以运行。您很快就会看到,您将在代码中容纳缺失的项目。

在代码中处理 Android 版本的一个最简单的方法是询问设备它运行的是什么版本,并据此采取行动。Build.VERSION.SDK_INT static int 以整数值的形式保存设备的 API 级别。所以使用起来非常简单:

if (Build.VERSION.SDK_INT >= 14) { ...

然后你就可以做以前只能在 Android 4.0 及以上版本上做的事情了。要查看最终的值列表,请查看Build.VERSION_CODES上的文档。

您还可以使用 Java 反射来查看某个类或类方法是否存在,如果存在,您可以使用它,如果不存在,则不使用它。如果您的应用依赖于某些特定于制造商的 API,而这些 API 不会存在于某个版本的 Android 的所有设备中,那么您也可以使用这种方法。关于以这种方式使用反射的例子,请参见第八章(高级调试)中的“旧 Android 版本的严格模式”一节。

兼容性库

当 Android 在 Honeycomb 中引入片段时,它代表了如何构建 Android 应用的一个重要新概念。Android 团队没有创建以前版本的 Android 操作系统的新版本,而是创建了一个兼容性库,其中包含用于旧操作系统的片段、加载器和其他几个代码。现在有几个版本的库,并在此过程中添加了额外的功能。通过在您的应用中包含 Android 支持库,并以 11 或更高的 Android API 级别为目标,您可以相当容易地使用片段和其他现代功能,同时支持 Android 的旧版本。

Note

如果您在使用高 targetSDK 构建应用时遇到问题,Google 建议您尝试将 targetSDK 设置为与 minSDK 相同的版本。

库 jar 文件位于 Android SDK 中的extras/support/android下。然而,包含库的最简单方法是在 Eclipse 中右键单击您的项目名称,选择 Android Tools 菜单,然后选择菜单项 Add Support Library。这为你设置好了一切。现在是警告。。。

为了让兼容性库既能支持不支持新功能的旧版本 Android,也能支持新版本 Android,你必须在代码中做一些改动。第一个主要的是用一个FragmentActivity代替一个Activity;,用FragmentActivity.getSupportFragmentManager()代替FragmentActivity.getFragmentManager();,用FragmentActivity.getSupportLoaderManager()代替FragmentActivity.getLoaderManager()。与以前不同的是,您检查 Android 的版本,为一个或另一个版本的 Android 执行不同的代码,您只需为所有版本的 Android 使用兼容性库类,在幕后,一切都适用于您的应用运行的任何版本的 Android。

为 Honeycomb 或更高版本编写应用和用兼容性库编写应用的另一个很大的区别是兼容性库中不支持ActionBar。关于蜂巢的想法发生了转变,从在应用上使用菜单到使用ActionBar。当你的应用运行在一个有ActionBar的 Android 版本上时,你希望它能被正确使用。当您的应用运行在旧版本上时,您会看到一个选项菜单。您需要做的是为 Options 菜单编写代码,但是为您在ActionBar上想要的每个菜单项调用一个特殊的兼容性库实用程序方法。在运行时,会出现正确的行为。清单 5-1 显示了菜单 XML 文件的样子,后面是正确实例化ActionBar或选项菜单的 Java 代码。

清单 5-1。兼容菜单和动作栏的 XML 和 Java

<!-- This file is /res/menu/options.xml -->

<menu xmlns:android="http://schemas.android.com/apk/res/android

<item android:id="@+id/menu_item1"

android:title="Item1"

android:icon="@android:drawable/ic_media_previous"

android:orderInCategory="0" />

<item android:id="@+id/menu_item2"

android:title="Item2"

android:icon="@android:drawable/ic_media_next"

android:orderInCategory="1" />

<item android:id="@+id/menu_item3"

android:title="Item3"

android:icon="@android:drawable/ic_menu_compass"

android:orderInCategory="1" />

</menu>

@Override

public boolean onCreateOptionsMenu(Menu menu) {

MenuInflater inflater = getMenuInflater();

inflater.inflate(R.menu.options, menu);

MenuItemCompat.setShowAsAction(menu.findItem(R.id.menu_item1)

MenuItemCompat.SHOW_AS_ACTION_ALWAYS);

MenuItemCompat.setShowAsAction(menu.findItem(R.id.menu_item2)

MenuItemCompat.SHOW_AS_ACTION_ALWAYS);

MenuItemCompat.setShowAsAction(menu.findItem(R.id.menu_item3)

MenuItemCompat.SHOW_AS_ACTION_NEVER);

return true;

}

这个清单采用了一些快捷方式,比如在 XML 文件中硬编码菜单项的文本,这是您通常不会做的。我们借用安卓图标,而不是创造自己的图标。但是这样更容易演示。菜单看起来很标准。变化出现在Activity中的onCreateOptionsMenu()代码中。一旦创建了菜单,不管你是在蜂窝系统之前的 Android 还是更新的 Android 上,你都可以使用MenuItemCompat.setShowAsAction()方法并指定该项目应该如何出现在ActionBar上。兼容性库将这种方法视为没有ActionBar的旧版本 Android 的禁用方法,因此没有必要做任何特殊处理。对于支持ActionBar的 Android 版本,这个方法调用将设置ActionBar而不是选项菜单。

Note

清单 5-1 中的代码包含在我们网站的可下载 zip 文件中。有关更多信息,请参阅本章末尾的参考资料。

兼容性库中有很多类和方法,我们无法在本章中介绍。要查看所有可用的内容,请访问在线 Android 参考文档,并开始搜索 android.support。当您在搜索字段中键入该内容时,您将看到兼容性库中的软件包选择。如果你很好奇,你也可以通过 Android SDK 管理器把源代码下载到兼容库,它会把它和 jar 文件一起安装在 Android SDK 目录的extras/android/support下。大部分来源在extras/android/support/v4/src/java/android/support/v4/app下。

参考

以下是一些对您可能希望进一步探索的主题有帮助的参考:

摘要

本章介绍了以下内容:

  • 屏幕密度以及物理像素和与密度无关的像素之间的差异
  • 屏幕密度与图像在尺寸和质量方面的关系
  • 屏幕尺寸和四个桶的描述
  • 解释屏幕尺寸与布局的关系
  • Android 3.2 新增布局资源限定符(-sw<N>dp–w<N>dp)
  • 语言限定符及其对布局的影响
  • 如何指定应用可能需要或使用的硬件特性,包括屏幕
  • 处理不同版本 Android 的几种方法,包括兼容性库

复习问题

以下是一些你可以问自己的问题,以巩固你对这个话题的理解:

True or false? The more pixels on a device’s display, the bigger the display is.   True or false? An image file that is 160x160 pixels will always appear 1-inch square on a display.   What density bucket would a device with a PPI of 206 be in?   To make an image that is 120x240 on an hdpi display look the same size on an ldpi display, what should its dimensions be?   What kinds of files would you find in a drawables resource directory?   What are the differences between physical pixels and density-independent pixels?   Why are there four screen sizes: small, normal, large, and xlarge? Why use these and not actual screen dimensions?   What are some of the qualifiers that can be used for a layout resource directory? Describe their relevance to layouts.   When should you use the tag in the AndroidManifest.xml file? Why?   How would you ensure that your application could be seen in Google Play for devices that use a normal screen, but not on devices that use a large or xlarge screen?   What are three attributes of the tag?   True or false? All Android applications for Gingerbread devices must now be built using the compatibility library.   Under what circumstances would you not need to use the compatibility library?   Does the compatibility library provide an ActionBar for pre-Honeycomb releases of Android?