用于应用开发的安卓-4-新特性-全-

用于应用开发的安卓 4 新特性(全)

原文:zh.annas-archive.org/md5/37F309F5583A3BFE9D4DF14FC6F7D1A9

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

本书是一本实用的、手把手指导开发 Android 应用程序的指南,使用 Android Ice Cream Sandwich(Android 4.0)的新特性,采用逐步讲解和清晰解释的示例代码。你将通过这些示例代码学习 Android 4.0 的新 API。

本书涵盖内容

第一章, 为所有人设计的操作栏, 介绍了操作栏,并展示如何使用和配置操作栏。

第二章, 新布局 - GridLayout, 介绍了 GridLayout,并展示如何使用和配置 GridLayout。GridLayout 是随 Android Ice Cream Sandwich 推出的一种新布局。这个布局是一个优化过的布局,可以替代 LinearLayout 和 RelativeLayout。

第三章, 社交 API, 讲解了随 Android Ice Cream Sandwich 推出的社交 API。这个 API 使得集成社交网络变得简单。此外,在 Ice Cream Sandwich 发布后,现在可以使用高分辨率照片作为联系人的照片。这一章通过示例展示了社交 API 的使用。

第四章, 日历 API, 讲解了与 Android Ice Cream Sandwich 一同推出的日历 API,用于管理日历。事件、参与者、提醒和备忘数据库可以通过这些 API 进行管理。这些 API 使我们能够轻松地将日历与 Android 应用程序集成。这一章展示了如何通过示例使用日历 API。

第五章, 碎片, 介绍了碎片的基础知识以及如何使用它们。

第六章, 支持不同的屏幕尺寸, 介绍了设计支持不同屏幕尺寸的用户界面的方法。

第七章, Android 兼容性包, 介绍了 Android 兼容性包,并展示如何使用它。Android 兼容性包是为了将新 API 移植到 Android 平台的旧版本。

第八章, 新的连接 API - Android Beam 和 Wi-Fi Direct, 介绍了 Android Beam,它使用设备的 NFC 硬件和无需使用无线接入点的 Wi-Fi Direct,允许设备之间相互连接。这一章将教我们如何使用 Android Beam 和 Wi-Fi Direct。

第九章, 多 APK 支持, 介绍了多 APK 支持,这是 Google Play(Android 市场)中的一个新选项,通过它可以为单一应用程序上传多个 APK 版本。

本章可以在以下链接下载:www.packtpub.com/sites/default/files/downloads/Multiple_APK_Support.pdf

第十章Android Jelly Bean 的 API,涵盖了 Android Jelly Bean 及其内部的新 API。

本章可以在 www.packtpub.com/sites/default/files/downloads/Android_JellyBean.pdf 下载。

你需要这本书的内容

要跟随本书中的示例,需要设置并准备好 Android 开发工具。所需的软件列表如下:

  • 带有 ADT 插件的 Eclipse

  • Android SDK 工具

  • Android 平台工具

  • 最新的 Android 平台

可以使用以下操作系统:

  • Windows XP(32 位)、Vista(32 位或 64 位)或 Windows 7(32 位或 64 位)

  • Mac OS X 10.5.8 或更高版本(仅限 x86)

  • Linux(在 Ubuntu Linux,Lucid Lynx 上测试)

    • 需要使用 GNU C 库(glibc)2.7 或更高版本

    • 在 Ubuntu Linux 上,需要版本 8.04 或更高版本

    • 64 位发行版必须能够运行 32 位应用程序

Eclipse IDE 的使用规范如下:

  • Eclipse 3.6.2(Helios)或更高版本(Eclipse 3.5(Galileo)不再支持最新版本的 ADT)

  • Eclipse JDT 插件(包含在大多数 Eclipse IDE 包中)

  • JDK 6(仅 JRE 不够)

  • Android Development Tools 插件(推荐)

本书适合的读者群体

本书适合那些对 Android 平台有经验,但可能不熟悉 Android 4.0 的新特性和 API 的开发者。

想要了解如何支持多种屏幕尺寸和多个 Android 版本的 Android 开发者;这本书也适合你。

约定

在这本书中,你会发现多种文本样式,用以区分不同类型的信息。以下是一些样式示例,以及它们含义的解释。

文中的代码字显示如下:"实现 onCreateOptionsMenuonOptionsItemSelected 方法。"

代码块设置如下:

<?xml version="1.0" encoding="utf-8"?>
<menu  >
    <item android:id="@+id/settings" android:title="Settings">
    </item>
    <item android:id="@+id/about" android:title="About">
    </item>

</menu>

当我们希望引起你注意代码块中的特定部分时,相关的行或项目会以粗体设置:

@Override
public void onPrepareSubMenu(SubMenu subMenu) {
 //In order to add submenus, we should override this method we dynamically created submenus
    subMenu.clear();
    subMenu.add("SubItem1").setOnMenuItemClickListener(this);
    subMenu.add("SubItem2").setOnMenuItemClickListener(this);
  }

新术语重要词汇以粗体显示。你在屏幕上看到的词,例如菜单或对话框中的,文本中会这样出现:"点击 插入 按钮然后点击 列表 按钮"。

注意

警告或重要提示会以这样的框显示。

提示

技巧和窍门会像这样出现。

读者反馈

我们始终欢迎读者的反馈。告诉我们你对这本书的看法——你喜欢或可能不喜欢的地方。读者的反馈对我们开发能让你们充分利用的标题很重要。

要给我们发送一般反馈,只需发送电子邮件至 <feedback@packtpub.com>,并在邮件的主题中提及书名。

如果你在一个主题上有专业知识,并且有兴趣撰写或参与书籍编写,请查看我们在 www.packtpub.com/authors 的作者指南。

客户支持

既然你现在拥有了 Packt 的一本书,我们有一些事情可以帮助你最大限度地利用你的购买。

下载示例代码

你可以从你在 www.PacktPub.com 的账户下载你购买的所有 Packt 图书的示例代码文件。如果你在其他地方购买了这本书,可以访问 www.PacktPub.com/support 注册,我们会直接将文件通过电子邮件发送给你。

源代码也可以在作者的网站 www.ottodroid.net 上获取。

错误更正

尽管我们已经竭尽全力确保内容的准确性,但错误仍然可能发生。如果你在我们的书中发现了一个错误——可能是文本或代码中的错误——我们非常感激你能向我们报告。这样做可以节省其他读者的时间,并帮助我们对本书的后续版本进行改进。如果你发现任何错误,请访问 www.packtpub.com/support 报告,选择你的书籍,点击 错误更正提交表单 链接,并输入错误的详细信息。一旦你的错误报告被验证,你的提交将被接受,并且错误更正将会被上传到我们的网站,或在相应标题的“错误更正”部分添加到现有的错误更正列表中。你可以通过访问 www.packtpub.com/support 选择你的标题来查看任何现有的错误更正。

盗版

互联网上版权材料的盗版问题在所有媒体中持续存在。在 Packt,我们非常重视保护我们的版权和许可。如果你在互联网上以任何形式遇到我们作品的非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。

如果你发现了疑似盗版材料,请通过 <copyright@packtpub.com> 联系我们,并提供链接。

我们感谢你帮助保护我们的作者,以及我们为你提供有价值内容的能力。

问题咨询

如果你在这本书的任何方面遇到问题,可以通过 <questions@packtpub.com> 联系我们,我们将尽力解决。

第一章:适用于所有人的操作栏

操作栏 API 最初是在 Android 3.0 中引入的。随着 Android 冰激凌三明治的发布,操作栏支持小屏幕尺寸。本章展示了如何使用和配置操作栏。

本章涵盖的主题如下:

  • 操作栏类型

  • 添加操作栏

  • 添加 ActionProvider 和 ShareActionProvider

  • 添加操作视图

  • 使用操作栏进行导航

操作栏

操作栏是位于用户设备屏幕顶部的用户界面元素。它为用户提供操作和导航功能。操作栏自 API 级别 11(Android 3.0 Honeycomb)起提供,冰激凌三明治发布后,也支持小屏幕设备。以下截图显示了一个带有标签的操作栏示例:

操作栏

如前图所示,在栏的左侧有一个应用程序图标和标题,然后是导航标签。最后,在标签之后放置操作按钮。那些不适合屏幕显示的操作按钮会以三个点的溢出菜单形式显示在栏的右侧。在前面的截图中,操作栏显示在大屏幕设备上。然而,在小屏幕设备上,操作栏会显示为堆叠的栏,如下截图所示:

操作栏

如前图所示,可以看到没有足够的空间显示所有操作栏项目,操作栏以屏幕顶部的双栏形式显示。

另一种类型的操作栏是分割操作栏。在这种类型的操作栏中,在窄屏幕上,操作按钮显示在屏幕底部的栏中,如下截图所示:

操作栏

添加操作栏

冰激凌三明治之后,Android 不再需要使用菜单按钮来访问选项菜单。最佳实践是使用操作栏而不是菜单按钮。从选项菜单迁移到操作栏非常容易。现在我们将创建一个菜单,然后将该菜单迁移到操作栏。

首先,创建一个 Android 项目,然后添加一个包含设置关于作为菜单项的菜单。生成的菜单 XML 文件应如下代码块所示:

提示

下载示例代码

您可以从您的账户下载您购买的所有 Packt 图书的示例代码文件,网址为www.PacktPub.com。如果您在别处购买了这本书,可以访问www.PacktPub.com/support注册,我们会直接将文件通过电子邮件发送给您。

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

    <item android:id="@+id/settings" android:title="Settings"></item>

    <item android:id="@+id/about" android:title="About"></item>

</menu>

本示例的布局 XML 是一个LinearLayout布局,其中包含一个TextView组件,如下代码块所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <TextView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="@string/hello" />

</LinearLayout>

实现onCreateOptionsMenuonOptionsItemSelected方法,如下代码块所示,以显示菜单项:

package com.chapter1;

import android.app.Activity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.widget.Toast;

public class Chapter1Activity extends Activity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
            //Inflate the menu.xml of the android project
 //in order to create menu
    MenuInflater inflater = getMenuInflater();
    inflater.inflate(R.menu.menu, menu);
    return true;
    }

    @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    // Handle item selection
 //According to selection, show the Toast message
 //of the selected button
    switch (item.getItemId()) {

    case R.id.settings:
      Toast.makeText(this, "Settings options menu button is pressed", Toast.LENGTH_LONG).show();
      return true;
    case R.id.about:
      Toast.makeText(this, "About options menu button is pressed", Toast.LENGTH_LONG).show();
      return true;
      default:
      return super.onOptionsItemSelected(item);
    }
  }
}

为了显示操作栏,Android 应用程序应该在AndroidManifest.xml文件中至少针对 API 级别 11,如下面的代码块所示:

<?xml version="1.0" encoding="utf-8"?>
<!—set targetSDKversion to 11 because Action Bar is
 available since API Level 11-->
<manifest 
    package="com.chapter1"
    android:versionCode="1"
    android:versionName="1.0" >

    < uses-sdk android:minSdkVersion="5" 
 android:targetSdkVersion="11"  />

    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        <activity
            android:name=".Chapter1Activity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

使用这个配置,当应用程序在搭载 Android 3.0 或更高版本的设备上运行时,操作栏将会显示。

当我们在 API 级别 15 的模拟器上运行这个应用程序时,我们将在操作栏的右侧看到溢出菜单,并且当按下溢出菜单时,选项菜单按钮将会显示。为了在操作栏上显示选项菜单按钮(而不是作为溢出菜单),只需在菜单 XML 文件的item标签内添加android:showAsAction="ifRoom|withText"。修改后的菜单 XML 文件应该如下面的代码块所示:

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

    <item android:id="@+id/settings" android:title="Settings" android:showAsAction="ifRoom|withText"></item>

    <item android:id="@+id/about" android:title="About" android:showAsAction="ifRoom|withText"></item>

</menu>

如果没有足够的空间(ifRoom)来显示选项菜单按钮,按钮将会作为溢出菜单显示。为了仅显示带有图标的选项菜单按钮(如果提供了图标),应该移除withText。当你运行应用程序时,它将如下面的截图所示:

添加操作栏

在某些情况下,你可能不希望显示操作栏。为了移除操作栏,你需要在AndroidManifest.xml文件中的activity标签内添加android:theme="@android:style/Theme.Holo.NoActionBar"。修改后的AndroidManifest.xml应该如下面的代码块所示:

<?xml version="1.0" encoding="utf-8"?>
<manifest 
    package="com.chapter1"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk android:minSdkVersion="5" 
              android:targetSdkVersion="11"  />

    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        <activity
            android:name=".Chapter1Activity"
            android:label="@string/app_name" 
            android:theme="@android:style/Theme.Holo.NoActionBar" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

为了将操作栏显示为分割操作栏,在AndroidManifest.xml中的activity标签内添加android:uiOptions="splitActionBarWhenNarrow"属性。修改后的AndroidManifest.xml应该如下面的代码块所示:

<?xml version="1.0" encoding="utf-8"?>
<manifest 
    package="com.chapter1"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk android:minSdkVersion="5"   
        android:targetSdkVersion="11"  />

    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" 
 android:uiOptions="splitActionBarWhenNarrow">
        <activity
            android:name=".Chapter1Activity"
            android:label="@string/app_name" 
            >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

当你在这个模拟器上运行这个应用程序时,屏幕将如下面的截图所示:

添加操作栏

添加 ActionProvider

为了在操作栏中使用自定义视图而不是简单的按钮,ActionProvider类可能是一个解决方案。ActionProvider从 API 级别 14 开始可用。ActionProvider 可以在操作栏中生成自定义视图,可以生成子菜单,并且可以处理它生成的视图的事件。为了创建一个 ActionProvider,我们应该继承ActionProvider类。下面的代码展示了一个扩展了ActionProvider类的示例类,并在操作栏中显示自定义布局而不是简单按钮:

import android.content.Context;
import android.view.ActionProvider;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ImageButton;
import android.widget.Toast;

public class Chapter1ActionProvider extends ActionProvider {

  Context mContext;

  public Chapter1ActionProvider(Context context) {
    super(context);
    mContext = context;
  }

  @Override
  public View onCreateActionView() {
        //This method is the place where we generate a custom layout for the Action Bar menu item
     LayoutInflater layoutInflater = LayoutInflater.from(mContext);
         View view = layoutInflater.inflate(R.layout.action_provider, null);
         ImageButton button = (ImageButton) view.findViewById(R.id.button);

         button.setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View v) {
          Toast.makeText(mContext, "Action Provider click", Toast.LENGTH_LONG).show();
             }
         });
    return view;
  }

  @Override
  public boolean onPerformDefaultAction() {
          //This is the method which is called when the Action Bar menu item is in overflow menu and clicked from there
          Toast.makeText(mContext, "Action Provider click", Toast.LENGTH_LONG).show();
    return true;
  }
}

我们必须添加一个构造函数并重写 onCreateActionView() 方法。在构造函数中,我们将 Context 赋值给一个变量,因为我们在后续实现中需要它。onCreateActionView() 方法是生成操作栏菜单项自定义布局的地方。onPerformDefaultAction() 是当操作栏菜单项在溢出菜单中并被点击时调用的方法。如果 ActionProvider 提供子菜单,则永远不会调用此方法。在 onCreateActionView() 方法中使用的自定义布局的布局 XML 如下代码块所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
    android:layout_width="wrap_content"
    android:layout_height="match_parent"
    android:layout_gravity="center"
    android:focusable="true"
    android:addStatesFromChildren="true"
    android:background="?android:attr/actionBarItemBackground"
    style="?android:attr/actionButtonStyle">

    <ImageButton android:id="@+id/button"
        android:background="@drawable/ic_launcher"
        android:layout_width="32dip"
        android:layout_height="32dip"
        android:layout_gravity="center"
        android:scaleType="fitCenter"
        android:adjustViewBounds="true" />
    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Some Text"
        android:textAppearance="?android:attr/textAppearanceLarge" />

</LinearLayout>

如你在 XML 文件中所见,我们将一个 ImageButton 组件和一个 TextView 组件添加到了 LinearLayout 布局中。ImageButtononClickListener() 事件在 Chapter1ActionProvider 类的 onCreateActionView() 方法中实现。在此事件中,将显示一个 Toast 消息。

显示操作栏的 Activity 类如下代码块所示:

public class Chapter1ActionProviderActivity extends Activity{

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    // TODO Auto-generated method stub
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
  }

  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
    MenuInflater inflater = getMenuInflater();
    inflater.inflate(R.menu.menu, menu);
    return true;
  }

   @Override
    public boolean onOptionsItemSelected(MenuItem item) {
    // Handle item selection
    switch (item.getItemId()) {

    case R.id.about:
        Toast.makeText(this, "About options menu button is pressed", Toast.LENGTH_LONG).show();
        return true;
      default:
        return super.onOptionsItemSelected(item);
      }
    }
}

为了为操作栏菜单项显示自定义布局,我们必须在 menu XML 文件中分配一个 ActionProvider 类。我们分配了之前作为 ActionProvider 实现的 Chapter1ActionProvider。我们示例中的 menu XML 文件如下所示:

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

    <item android:id="@+id/settings" android:title="Settings" android:showAsAction="ifRoom|withText"

 android:actionProviderClass="com.chapter1.Chapter1ActionProvider"></item>

    <item android:id="@+id/about" android:title="About" android:showAsAction="ifRoom|withText"></item>

</menu>

如你在 menu XML 文件中所见,我们为 settings 菜单项提供了一个 ActionProvider 类。最后重要的一步是在 AndroidManifest.xml 文件中将最低 SDK 版本设置为 API 级别 14,因为 ActionProvider 是在 API 级别 14 中发布的新功能。AndroidManifest.xml 文件应如下代码块所示:

<?xml version="1.0" encoding="utf-8"?>
<manifest 
    package="com.chapter1"
    android:versionCode="1"
    android:versionName="1.0" >
<!—set minSDKversion to 11 because ActionProvider is
 available since API Level 11-->

    <uses-sdk android:minSdkVersion="14" 
        android:targetSdkVersion="14"  />

    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        <activity
            android:name=".Chapter1ActionProviderActivity"
            android:label="@string/app_name" 
            >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

当你在模拟器中运行此应用程序时,操作栏中将显示一个带有图像按钮和文本视图的用户界面组件。如果你按下图像按钮,将显示一个吐司消息。屏幕将如下所示:

添加 ActionProvider

向 ActionProvider 添加子菜单

可以通过 ActionProvider 显示子菜单。为了添加子菜单,我们应该在 Chapter1ActionProvider 类中重写 onPrepareSubMenu(SubMenu subMenu)hasSubMenu() 方法。Chapter1ActionProvider 类的结果代码应如下代码块所示:

package com.chapter1;

import android.app.Activity;
import android.content.Context;
import android.view.ActionProvider;
import android.view.LayoutInflater;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MenuItem.OnMenuItemClickListener;
import android.view.SubMenu;
import android.view.View;
import android.widget.ImageButton;
import android.widget.Toast;

public class Chapter1ActionProvider extends ActionProvider implements 
OnMenuItemClickListener {

  Context mContext;

  public Chapter1ActionProvider(Context context) {
    super(context);
    mContext = context;
  }

  @Override
  public View onCreateActionView() {
         return null;
  }

  @Override
  public boolean onPerformDefaultAction() {

    Toast.makeText(mContext, "Action Provider click", Toast.LENGTH_LONG).show();
    return true;
  }

  @Override
  public void onPrepareSubMenu(SubMenu subMenu) {
 //In order to add submenus, we should override this method
 // we dynamically created submenus

    subMenu.clear();
    subMenu.add("SubItem1").setOnMenuItemClickListener(this);
    subMenu.add("SubItem2").setOnMenuItemClickListener(this);
  }

  @Override
  public boolean onMenuItemClick(MenuItem item) {

    Toast.makeText(mContext, "Sub Item click", Toast.LENGTH_LONG).show();
    return true;
  }

  @Override
  public boolean hasSubMenu() {
 // we implemented it as returning true because we have menu
    return true;
  }

}

onPrepareSubMenu(SubMenu subMenu) 方法中,我们动态创建了子菜单并设置了它们的 onMenuItemClickListener 事件。如果 hasSubMenu() 方法返回 true,则会调用 onPrepareSubMenu(SubMenu subMenu) 方法,因此我们将其实现为返回 true。

也可以通过 menu XML 文件创建子菜单。如果你想从 menu XML 文件创建子菜单,onPrepareSubMenu(SubMenu subMenu) 应该如下代码块所示:

  @Override
  public void onPrepareSubMenu(SubMenu subMenu) {

    MenuInflater inflater = ((Activity)mContext).getMenuInflater();
    inflater.inflate(R.menu.menu2, subMenu);
  }

此代码展示了如何使用 menu XML 文件 menu2 将 XML 文件展开以创建子菜单。

ShareActionProvider

ShareActionProvider提供了一种一致的分享方式。它在操作栏上放置一个带有分享图标的动作按钮。当你点击该按钮时,它会列出可用于分享的应用程序。你需要在menu项中声明ShareActionProvider,如下面的代码块所示:

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

    <item android:id="@+id/share" android:title="Share" android:showAsAction="ifRoom"
    android:actionProviderClass="android.widget.ShareActionProvider"></item>
    <item android:id="@+id/about" android:title="About" android:showAsAction="ifRoom"></item>
    <item android:id="@+id/settings" android:title="Settings" android:showAsAction="ifRoom"></item>

</menu>

使用ShareActionProviderActivity类应该如下代码块所示:

package com.chapter1;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.widget.ShareActionProvider;

public class Chapter1ShareActionProviderActivity extends Activity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {

      ShareActionProvider myShareActionProvider;
    MenuInflater inflater = getMenuInflater();
    inflater.inflate(R.menu.menu, menu);
    MenuItem item = menu.findItem(R.id.share);
    myShareActionProvider = (ShareActionProvider)item.getActionProvider();
    myShareActionProvider.setShareHistoryFileName(ShareActionProvider.DEFAULT_SHARE_HISTORY_FILE_NAME);
 myShareActionProvider.setShareIntent(getShareIntent());

    return true;
    }

    private Intent getShareIntent() {
      Intent shareIntent = new Intent(Intent.ACTION_SEND);
      shareIntent.setType("text/plain");
      shareIntent.putExtra(Intent.EXTRA_TEXT, "www.somesite.com");
      return shareIntent;
      }

}

如代码所示,我们在onCreateOptionsMenu(Menu menu)方法中获取了menu项的ShareActionProvider属性。然后我们使用ShareActionProvidersetShareIntent方法定义分享的意图。getShareIntent()方法创建了一个用于分享文本的意图。我们使用此方法为ShareActionProvider实例定义意图。

ShareActionProvider会在一个文件中保存用于分享的应用程序历史记录。ShareActionProvider默认使用的文件是ShareActionProvider.DEFAULT_SHARE_HISTORY_FILE_NAME。可以通过setShareHistoryFileName方法更改此文件。你需要向此方法传递一个带有.xml 扩展名的 XML 文件名。ShareActionProvider使用这个文件来查找最常用于分享的应用程序。然后它将最常使用的应用程序显示在分享动作按钮附近,作为默认分享目标。

使用ShareActionProvider的应用程序界面如下所示:

ShareActionProvider

由于ShareActionProvider是在 API 级别 14 引入的,因此我们必须在AndroidManifest.xml文件中将最小 SDK 设置为 14,如下代码块所示:

<?xml version="1.0" encoding="utf-8"?>
<manifest 
    package="com.chapter1"
    android:versionCode="1"
    android:versionName="1.0" >
<!—set minSdkVersion to 14 because ShareActionProvider is available
 since API Level 14-->

    <uses-sdk android:minSdkVersion="14" />

    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        <activity
            android:name=".Chapter1ShareActionProviderActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

添加一个动作视图

动作视图是出现在操作栏中的用户界面组件,而不是操作按钮。这个视图是可以折叠的,即如果它被配置为可折叠,意味着在按下动作按钮时会展开。如果没有配置为可折叠,默认会展开显示。在以下示例中,我们添加了一个动作视图,并展示了其事件以及如何处理这些事件。

首先,添加一个动作视图的布局,其中包含三个带有文本LargeMediumSmall的按钮,如下代码块所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal" >
    <Button
        android:id="@+id/buttonLarge"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:text="Large"
        android:textSize="15dp" />

    <Button
        android:id="@+id/buttonMedium"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:text="Medium"
        android:textSize="12dp" />

    <Button
        android:id="@+id/buttonSmall"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:text="Small"
        android:textSize="9dp" />

</LinearLayout>

然后,我们需要将此动作视图绑定到操作栏的menu项。menu的 XML 代码如下所示:

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

 <item android:id="@+id/size" android:title="Size" android:showAsAction="ifRoom|collapseActionView"
 android:actionLayout="@layout/actionview"></item>

    <item android:id="@+id/about" android:title="About" android:showAsAction="ifRoom"></item>
    <item android:id="@+id/settings" android:title="Settings" android:showAsAction="ifRoom|withText"></item>

</menu>

如你在menu的 XML 代码中所见,我们通过设置actionLayout属性将动作视图绑定到size菜单项。我们还设置了showAsAction属性为collapseActionView。这样,动作视图就是可折叠的,当按下动作按钮项时展开。这个选项可以帮助我们在操作栏中节省空间。如果此属性未设置为collapseActionView,则动作视图默认会展开显示。

处理动作视图事件的Activity类如下代码块所示:

package com.chapter1;

import android.app.Activity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MenuItem.OnActionExpandListener;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.Toast;

public class Chapter1ActionViewActivity extends Activity implements 
OnClickListener {
  Button buttonLarge;
  Button buttonMedium;
  Button buttonSmall;
  Menu menu;

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
      //you can set on click listeners of the items in Action View in this method

      this.menu = menu;
    MenuInflater inflater = getMenuInflater();
    inflater.inflate(R.menu.menu, menu);
    MenuItem item = menu.findItem(R.id.size);
    item.setOnActionExpandListener(new Chapter1ActionListener(this));
    buttonLarge = (Button)item.getActionView().findViewById(R.id.buttonLarge);
    buttonLarge.setOnClickListener(this);

    buttonMedium = (Button)item.getActionView().findViewById(R.id.buttonMedium);
    buttonMedium.setOnClickListener(this);

    buttonSmall = (Button)item.getActionView().findViewById(R.id.buttonSmall);
    buttonSmall.setOnClickListener(this);

    return true;
    }

    @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    // Handle item selection
    switch (item.getItemId()) {

    case R.id.size:
      Toast.makeText(this, "Size options menu button is pressed", Toast.LENGTH_LONG).show();
      return true;
    case R.id.about:
      Toast.makeText(this, "About options menu button is pressed", Toast.LENGTH_LONG).show();
      return true;
      case R.id.settings:
      Toast.makeText(this, "Settings options menu button is pressed", Toast.LENGTH_LONG).show();
      return true;
    default:
      return super.onOptionsItemSelected(item);
    }
  }

  @Override
  public void onClick(View v) {

    if(v == buttonLarge )
    {
      Toast.makeText(this, "Large button is pressed", Toast.LENGTH_LONG).show();
 //Collapse the action view
      menu.findItem(R.id.size).collapseActionView();
    }
    else if(v == buttonMedium )
    {
      Toast.makeText(this, "Medium button is pressed", Toast.LENGTH_LONG).show();
                    //Collapse the action view
      menu.findItem(R.id.size).collapseActionView();
    }
    else if(v == buttonSmall)
    {
      Toast.makeText(this, "Small button is pressed", Toast.LENGTH_LONG).show();
                    //Collapse the action view
      menu.findItem(R.id.size).collapseActionView();
    }

  }

      // This class returns a callback when Action View is expanded or collapsed
  public static class Chapter1ActionListener implements OnActionExpandListener
  {
    Activity activity;

    public Chapter1ActionListener(Activity activity)
    {
      this.activity = activity;
    }

    @Override
    public boolean onMenuItemActionCollapse(MenuItem item) {

      Toast.makeText(activity, item.getTitle()+" button is collapsed", Toast.LENGTH_LONG).show();
      return true;
  }

   @Override
    public boolean onMenuItemActionExpand(MenuItem item) {
      Toast.makeText(activity, item.getTitle()+" button is expanded", Toast.LENGTH_LONG).show();
      return true;
    }

  }
}

正如在Chapter1ActionViewActivity中看到的,你可以在onCreateOptionsMenu(Menu menu)方法中设置动作视图内各项目的事件监听器。我们在onCreateOptionsMenu(Menu menu)方法中设置了动作视图内按钮的onClickListener事件。

可以通过expandActionView()collapseActionView()方法以编程方式展开和折叠动作视图。正如在Chapter1ActionViewActivityonClick(View v)方法中看到的那样,我们使用collapseActionView()方法手动折叠了动作视图。

当动作视图展开或折叠时,你可以使用OnActionExpandListener类执行一个动作。正如代码中看到的,我们定义了实现OnActionExpandListenerChapter1ActionListener类。我们重写了这个类的onMenuItemActionCollapse(MenuItem item)onMenuItemActionExpand(MenuItem item)方法,以便显示一个Toast消息。我们将Activity作为参数传递给Chapter1ActionListener的构造函数,因为显示Toast消息时需要Activity。为了处理展开和折叠事件,我们必须注册setOnActionExpandListener()方法与OnActionExpandListener类。正如代码中看到的,我们在onCreateOptionsMenu(Menu menu)方法中注册了这个事件。当动作视图折叠和展开时,我们会显示一个Toast消息。

由于动作视图是在 API 级别 14 中引入的,因此我们必须在AndroidManifest.xml文件中将最小 SDK 属性设置为 14 或更高,如下代码块所示:

<?xml version="1.0" encoding="utf-8"?>
<manifest 
    package="com.chapter1"
    android:versionCode="1"
    android:versionName="1.0" >
<!—set minSdkVersion to 14 because Action View is
 available since API Level 14-->

    <uses-sdk android:minSdkVersion="14" />

    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        <activity
            android:name=".Chapter1ActionViewActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

当你在模拟器上运行这个应用时,它看起来会像下面的截图一样:

添加动作视图

使用操作栏进行导航

标签导航也可以通过TabWidget类实现。然而,操作栏有一些优点。操作栏会根据设备屏幕大小自动调整自身。例如,如果没有足够的空间显示标签,它会以堆叠条的方式显示标签。因此,在实现标签导航时,最好使用操作栏。

现在,我们将要了解如何使用操作栏进行标签导航。首先,创建一个 Android 项目并添加两个片段:一个显示Fragment A,另一个显示Fragment B。片段的布局 XML 应该如下代码块所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <TextView
        android:id="@+id/textView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Fragment A"
        android:textAppearance="?android:attr/textAppearanceLarge" android:layout_gravity="center_horizontal"/>

</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <TextView
        android:id="@+id/textView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Fragment B"
        android:textAppearance="?android:attr/textAppearanceLarge" 
        android:layout_gravity="center_horizontal"/>

</LinearLayout>

为这两个片段扩展Fragment类的类应该如下代码块所示:

package com.chapter1;

import android.app.Fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

public class FragmentA extends Fragment {

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container,
      Bundle savedInstanceState) {
      View view = inflater.inflate(R.layout.fragment_a, container,false);
    return view;
  }
}
package com.chapter1;

import android.app.Fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

public class FragmentB extends Fragment {

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container,
      Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.fragment_b, container, false);
    return view;
  }
}

为了使用操作栏进行标签导航,我们首先应该实现ActionBar.TabListener类。实现TabListener的类将在Activity类中添加标签时使用。带有TabListener实现的Activity类应该如下代码块所示:

package com.chapter1;

import android.app.ActionBar;
import android.app.ActionBar.Tab;
import android.app.ActionBar.TabListener;
import android.app.Activity;
import android.app.Fragment;
import android.app.FragmentTransaction;
import android.os.Bundle;

public class Chapter1ActionBarTabActivity extends Activity {

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    ActionBar actionBar = getActionBar();
    actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);

    Tab tab = actionBar
        .newTab()
        .setText("First tab")
        .setTabListener(
          new Chapter1TabListener<FragmentA>(this, "fragmentA",FragmentA.class));
    actionBar.addTab(tab);

    tab = actionBar
        .newTab()
        .setText("Second Tab")
        .setTabListener(
            new Chapter1TabListener<FragmentB>(this, "fragmentB",FragmentB.class));
    actionBar.addTab(tab);
  }

  public static class Chapter1TabListener<T extends Fragment> implements
 TabListener {
    private Fragment mFragment;
    private final Activity mActivity;
    private final String mTag;
    private final Class<T> mClass;

    public Chapter1TabListener(Activity activity, String tag, Class<T> clz) {
      mActivity = activity;
      mTag = tag;
      mClass = clz;
    }

    @Override
    public void onTabSelected(Tab tab, FragmentTransaction ft) {
 // we initialize and add the fragment to our Activity if it doesn't exist

      if (mFragment == null) {

        mFragment = Fragment.instantiate(mActivity, mClass.getName());			
        ft.add(android.R.id.content, mFragment, mTag);

        } else {
 // If it exists, we simply attach it
        ft.attach(mFragment);
      }
    }

    @Override
    public void onTabUnselected(Tab tab, FragmentTransaction ft) {
 // in this method we detach the fragment because // it shouldn't be displayed
    if (mFragment != null) {
        ft.detach(mFragment);
        }
    }
    @Override
    public void onTabReselected(Tab tab, FragmentTransaction ft) {
 // This method is called when the tab is reselected
    }
  }
}

Chapter1TabListener 类中有三个需要重写的方法:onTabReselected(Tab tab, FragmentTransaction ft)onTabUnselected(Tab tab, FragmentTransaction ft)onTabSelected(Tab tab, FragmentTransaction ft)。在 onTabSelected(Tab tab, FragmentTransaction ft) 方法中,如果片段不存在,我们会初始化并将片段添加到我们的活动中。如果它已存在,我们只需连接到它。当标签被取消选择时,会调用 onTabUnselected(Tab tab, FragmentTransaction ft) 方法。在这个方法中,我们分离片段,因为它不应该被显示。当标签被重新选择时,会调用 onTabReselected(Tab tab, FragmentTransaction ft) 方法。在这个方法中我们不执行任何操作。在 Chapter1ActionBarTabActivity 类中,我们创建并设置操作栏。我们活动的布局只有一个 LinearLayout 布局,我们使用片段作为用户界面。首先,我们将操作栏的导航模式设置为 ActionBar.NAVIGATION_MODE_TABS,因为我们需要标签导航。然后我们创建两个标签,设置它们的 TabListener 事件,并将它们添加到 操作栏 实例中。当你运行应用程序时,你会看到两个标签,名为 FIRST TABSECOND TAB。第一个标签将显示 Fragment A,第二个标签将显示 Fragment B。屏幕将如下所示:

使用操作栏进行导航

重要的是不要忘记将最低 SDK 级别设置为 API 级别 11 或更高,因为操作栏是在 API 级别 11 中引入的。

总结

在本章中,你学习了如何使用操作栏,因为这种方法比使用选项菜单更为一致。你还了解了如何使用 ActionProvider 在操作栏中创建自定义布局。你学习了如何使用 ShareActionProvider,以及它如何在你的应用中实现分享的有效方式。你学习了如何使用操作视图以及如何使其可折叠。最后,你学习了如何使用操作栏进行标签导航。它具有适应设备屏幕尺寸的优点,因此使用操作栏比使用较旧的 API 更好。在下一章中,我们将学习一个名为 GridLayout 的 Android 布局,并了解如何添加和配置它。

第二章:新布局——GridLayout

随着 Android Ice Cream Sandwich 的推出,引入了一种新的布局,称为GridLayout。这个布局是一个优化的布局,可以代替LinearLayoutRelativeLayout。本章展示了如何使用和配置 GridLayout。

本章涵盖的主题包括:

  • 为什么使用 GridLayout

  • 添加一个 GridLayout

  • 配置 GridLayout

GridLayout

GridLayout是一种将其视图空间划分为行、列和单元格的布局。GridLayout 自动在其中放置视图,但也可以定义列和行索引来在 GridLayout 中放置视图。通过单元格的跨度属性,可以使一个视图跨越多行或多列。下面的代码块展示了一个使用GridLayout布局的示例布局文件:

<?xml version="1.0" encoding="utf-8"?>
<GridLayout 
android:id="@+id/GridLayout1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:columnCount="2"
android:orientation="horizontal" android:rowCount="2">

<TextView
android:id="@+id/textView1"
android:text="Cell 1,1"
android:textAppearance="?android:attr/textAppearanceLarge" />

<TextView
android:id="@+id/textView2"
android:text="Cell 1,2"
android:textAppearance="?android:attr/textAppearanceLarge" />

<TextView
android:id="@+id/textView3"
android:text="Cell 2,1"
android:textAppearance="?android:attr/textAppearanceLarge" />

<TextView
android:id="@+id/textView4"
android:text="Cell 2,2"
android:textAppearance="?android:attr/textAppearanceLarge" />

</GridLayout>

当这个布局 XML 在模拟器中查看时,它将如下截图所示:

GridLayout

在这个布局 XML 文件中,我们放置了四个TextView组件,文本分别为Cell 1,1Cell 1,2Cell 2,1Cell 2,2。将 GridLayout 的orientation设置为horizontal,并且columnCountrowCount属性设置为2,GridLayout 首先自动将项目放置在第一行,当项目数量达到columnCount时,它开始将项目放置在第二行。

你在这个布局中首先会注意到的就是TextView组件没有layout_widthlayout_height属性。这些属性没有被使用,因为GridLayout使用layout_gravity属性来确定单元格的大小,而不是layout_widthlayout_height属性。通常gravity属性用于对齐视图的内容,但在GridLayout中它被用于不同的目的。可用的重力常数包括lefttoprightbottomcenter_horizontalcenter_verticalcenterfill_horizontalfill_verticalfill

GridLayout中,你可以通过指定列和行的索引明确地定义一个视图将被放置的单元格。如果没有指定索引,GridLayout会根据GridLayout布局的取向自动将视图放置在第一个可用的位置。

为什么使用 GridLayout

LinearLayoutRelativeLayout是 Android 用户界面设计中使用最普遍的布局。对于简单的用户界面,它们是一个不错的选择,但是当用户界面变得复杂时,嵌套 LinearLayout 的使用往往会增加。嵌套布局(任何类型)可能会影响性能,而且嵌套超过 10 层的 LinearLayout 可能会导致应用程序崩溃。因此,你应该避免使用过多的嵌套 LinearLayout 块,或者使用 RelativeLayout 以减少嵌套 LinearLayout 块。这些布局对于复杂用户界面的另一个缺点是可读性差。维护具有许多视图的嵌套 LinearLayout 或 RelativeLayout 布局很困难。在这些情况下,使用GridLayout是一个不错的选择。通过使用 GridLayout,可以避免使用过多的嵌套 LinearLayout。此外,维护 GridLayout 要容易得多。许多使用 LinearLayout、RelativeLayout 或TableLayout的用户界面可以转换为 GridLayout,其中 GridLayout 将提供性能提升。与其他布局相比,GridLayout 的主要优势之一是你可以控制视图在水平和垂直轴上的对齐方式。

添加一个 GridLayout

在本节中,我们将把一个 Android 应用程序从LinearLayout迁移到GridLayout。使用LinearLayout的应用程序的布局 XML 代码如下代码块所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" android:background="#ffffff">
<!-- we used 3 nested LinearLayout-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" >
<!—LinearLayout that contains labels-->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical" >

<TextView
android:id="@+id/textView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Username:"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="#000000" android:layout_gravity="right"/>

<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Password:"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textColor="#000000" />
</LinearLayout>
<!—Linearlayout that contains fields-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" >

<EditText
android:id="@+id/editText1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/borders_bottom_right"
android:ems="10" >

</EditText>

<EditText
android:id="@+id/editText2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@drawable/borders_bottom_right"
android:ems="10" />
</LinearLayout>
</LinearLayout>

<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="OK" android:layout_gravity="center_horizontal"/>

</LinearLayout>

在前一个布局文件中使用的 drawable borders_bottom_right背景如下代码块所示:

<?xml version="1.0" encoding="utf-8"?>
  <layer-list  >
  <item>
    <shape android:shape="rectangle">
    <stroke android:width="1dp" android:color="#FFFFFF" />
    <solid android:color="#000000" />
    </shape>
  </item>
  </layer-list>

屏幕将如下所示:

添加 GridLayout

正如你在布局 XML 代码中看到的,我们使用了三个嵌套的LinearLayout实例来实现一个简单的登录屏幕。如果这个屏幕是用GridLayout设计的,布局的 XML 代码将如下代码块所示:

<?xml version="1.0" encoding="utf-8"?>
<GridLayout 
  android:layout_width="fill_parent"
  android:layout_height="fill_parent"
  android:background="#ffffff"
  android:columnCount="2"
  android:orientation="horizontal" >

  <TextView
    android:id="@+id/textView1"
    android:layout_gravity="right"
    android:text="Username:" android:textColor="#000000"/>

  <EditText
  android:id="@+id/editText1"
  android:ems="10" android:background="@drawable/borders"/>

  <TextView
    android:id="@+id/textView2"
    android:text="Password:"
    android:textAppearance="?android:attr/textAppearanceLarge" android:textColor="#000000"/>

  <EditText
    android:id="@+id/editText2"
    android:ems="10" android:background="@drawable/borders">

  </EditText>

  <Button
    android:id="@+id/button1"
    android:layout_columnSpan="2"
    android:text="Button" android:layout_gravity="center_horizontal"/>

</GridLayout>

我们将columnCount属性设置为2,因为我们在一行中有一个TextView组件和一个EditText组件。之后我们放置了视图,并没有指定行或列的索引。GridLayout会根据orientationcolumnCount自动放置它们。我们将layout_columnSpan属性设置为2,以使按钮跨越两列。通过layout_gravity属性,我们使按钮在行的中心显示。正如你在布局的 XML 代码中看到的,使用 GridLayout 设计相同的屏幕非常简单和容易。此外,GridLayout 的对其方式更加简单,并且代码的可读性更好。

GridLayout从 API 级别 14 开始可用,因此在AndroidManifest.xml文件中,最低 SDK 属性应设置为14或更高,如下代码行所示:

<uses-sdkandroid:minSdkVersion="14" />

配置 GridLayout

首先,我们将编写一个示例GridLayout XML 代码,然后我们将使用此代码作为其他示例的基础。示例布局的 XML 代码将如下代码块所示:

<?xml version="1.0" encoding="utf-8"?>
<GridLayout 
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:columnCount="3"
android:rowCount="3" >

<TextView
android:text="[1,1]" 
android:textAppearance="?android:attr/textAppearanceLarge"/>

<TextView
android:text="[1,2]" 
android:textAppearance="?android:attr/textAppearanceLarge"/>

<TextView
android:text="[1,3]" 
android:textAppearance="?android:attr/textAppearanceLarge"/>

<TextView
android:text="[2,1]" 
android:textAppearance="?android:attr/textAppearanceLarge"/>

<TextView
android:text="[2,2]" 
android:textAppearance="?android:attr/textAppearanceLarge"/>

<TextView
android:text="[2,3]" 
android:textAppearance="?android:attr/textAppearanceLarge"/>

<TextView
android:text="[3,1]" 
android:textAppearance="?android:attr/textAppearanceLarge"/>

<TextView
android:text="[3,2]" 
android:textAppearance="?android:attr/textAppearanceLarge"/>

<TextView
android:text="[3,3]" 
android:textAppearance="?android:attr/textAppearanceLarge"/>

</GridLayout>

使用前面的代码块,屏幕将如下所示:

配置 GridLayout

如你在布局 XML 代码中所见,TextView组件没有索引地放置,并根据orientationcolumnCountrowCountGridLayout中自动定位。现在我们将[1, 3]的索引编号设置为[2, 1]。布局 XML 代码应如下所示:

<?xml version="1.0" encoding="utf-8"?>
<GridLayout 
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:columnCount="3"
android:rowCount="3" >

<TextView
android:text="[1,1]" 
android:textAppearance="?android:attr/textAppearanceLarge"/>

<TextView
android:text="[1,2]" 
android:textAppearance="?android:attr/textAppearanceLarge"/>
<!-- set the row and column index with layout_row and layout_column
properties-->
<TextView
android:text="[1,3]"
android:textAppearance="?android:attr/textAppearanceLarge"
android:layout_row="1" android:layout_column="1"/>

<TextView
android:text="[2,1]" 
android:textAppearance="?android:attr/textAppearanceLarge"/>

<TextView
android:text="[2,2]" 
android:textAppearance="?android:attr/textAppearanceLarge"/>

<TextView
android:text="[2,3]" 
android:textAppearance="?android:attr/textAppearanceLarge"/>

<TextView
android:text="[3,1]" 
android:textAppearance="?android:attr/textAppearanceLarge"/>

<TextView
android:text="[3,2]" 
android:textAppearance="?android:attr/textAppearanceLarge"/>

<TextView
android:text="[3,3]" 
android:textAppearance="?android:attr/textAppearanceLarge"/>

</GridLayout>

屏幕应如下所示:

配置 GridLayout

如你在布局 XML 代码(高亮部分)中所见,我们使用layout_rowlayout_column属性设置行和列索引。索引是基于零的,因此带有文本[1, 3]TextView组件被放置在第二行和第二列。这里有趣的部分是,带有文本[2, 1]TextView组件被放置在[1, 3]之后。这是因为[2, 1]没有索引,GridLayout[1, 3]之后继续定位。这就是GridLayout在最后一个放置视图之后寻找第一个可用位置的方式。另一个值得注意的是,在移动索引后,行数增加到了4,尽管我们设置了行数为3GridLayout在这种情况下不会抛出异常。

在以下示例中,我们将交换[1, 2][2, 2]。布局 XML 代码应如下所示:

<?xml version="1.0" encoding="utf-8"?>
<GridLayout 
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:columnCount="3"
android:rowCount="3" >

<TextView
android:text="[1,1]" 
android:textAppearance="?android:attr/textAppearanceLarge"/>
<!-- set layout_row of [1, 2] to 1-->
<TextView
android:text="[1,2]"
android:textAppearance="?android:attr/textAppearanceLarge"
android:layout_row="1"/>
<!-- set layout_row of [1, 2] to 1-->
<TextView
android:text="[1,3]"
android:textAppearance="?android:attr/textAppearanceLarge"
android:layout_row="0"/>

<TextView
android:text="[2,1]" 
android:textAppearance="?android:attr/textAppearanceLarge"/>
<!-- set the layout_row of [2, 2] to 0 and layout_column to 1-->
<TextView
android:text="[2,2]"
android:textAppearance="?android:attr/textAppearanceLarge"
android:layout_row="0" android:layout_column="1"/>
<!-- set layout_row of [2, 3] to 1 in order to make it appear after [1,2]'s
new position-->
<TextView
android:text="[2,3]"
android:textAppearance="?android:attr/textAppearanceLarge"
android:layout_row="1"/>

<TextView
android:text="[3,1]" 
android:textAppearance="?android:attr/textAppearanceLarge"/>

<TextView
android:text="[3,2]" 
android:textAppearance="?android:attr/textAppearanceLarge"/>

<TextView
android:text="[3,3]" 
android:textAppearance="?android:attr/textAppearanceLarge"/>

</GridLayout>

屏幕应如下所示:

配置 GridLayout

如你在布局 XML 代码中所见,我们首先将[1, 2]layout_row设置为1。这样,它将出现在[2, 2]的位置。然后,我们必须将[1, 3]layout_row设置为0,将layout_column设置为2,因为GridLayout的游标位置通过设置[1, 2]的索引而改变了。如果我们不改变[1, 3]的索引,它将被放置在[1, 2]索引新位置的后面。之后,为了使[2, 2]出现在[1, 2]的位置,我们将[2, 2]layout_row设置为0,将layout_column设置为1。最后,我们必须将[2, 3]layout_row设置为1,以使其出现在[1, 2]索引新位置的后面。在GridLayout中配置视图看起来有点复杂,但如果你在模拟器中尝试,你会发现这并不那么困难。

在以下示例代码中,我们将删除[2, 2]并将[1, 2]设置为跨越两行。布局 XML 代码应如下所示:

<?xml version="1.0" encoding="utf-8"?>
<GridLayout 
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:columnCount="3"
android:rowCount="3" >

<TextView
android:text="[1,1]" 
android:textAppearance="?android:attr/textAppearanceLarge"/>
<!-- set layout_rowSpan property of [1,2] to 2\. By this way [1,2] will
cover 2 rows.-->
<TextView
android:text="[1,2]"
android:textAppearance="?android:attr/textAppearanceLarge"
android:layout_rowSpan="2" android:layout_gravity="fill"
android:gravity="center"/>

<TextView
android:text="[1,3]" 
android:textAppearance="?android:attr/textAppearanceLarge"/>

<TextView
android:text="[2,1]" 
android:textAppearance="?android:attr/textAppearanceLarge"/>

<TextView
android:text="[2,3]" 
android:textAppearance="?android:attr/textAppearanceLarge"/>

<TextView
android:text="[3,1]" 
android:textAppearance="?android:attr/textAppearanceLarge"/>

<TextView
android:text="[3,2]" 
android:textAppearance="?android:attr/textAppearanceLarge"/>

<TextView
android:text="[3,3]" 
android:textAppearance="?android:attr/textAppearanceLarge"/>

</GridLayout>

使用前面的代码块,我们得到以下屏幕:

配置 GridLayout

如你在布局 XML 代码中所见,我们删除了单元格[2,2]并将[1,2]layout_rowSpan属性设置为2。这样,[1,2]将覆盖两行。我们将layout_gravity属性设置为fill,以使其填充两行的空间。然后我们将gravity属性设置为center,以使TextView组件的内容对其覆盖的空间中心对齐。`

`# 一个新的视图 - 空间

Space是 Android Ice Cream Sandwich 中引入的一个新视图。它用于在视图之间放置空间。在GridLayout中它非常有用。在以下示例布局 XML 代码中,我们移除了带有文本[2, 2][2, 3]TextView组件,然后用Space替换它们,如下面的代码块所示:

<?xml version="1.0" encoding="utf-8"?>
<GridLayout 
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:columnCount="3"
android:rowCount="3" >

<TextView
android:text="[1,1]" 
android:textAppearance="?android:attr/textAppearanceLarge"/>

<TextView
android:text="[1,2]" 
android:textAppearance="?android:attr/textAppearanceLarge"/>

<TextView
android:text="[1,3]" 
android:textAppearance="?android:attr/textAppearanceLarge"/>

<TextView
android:text="[2,1]" 
android:textAppearance="?android:attr/textAppearanceLarge"/>
<Space
android:layout_row="1"
android:layout_column="1"
android:layout_columnSpan="2"
android:layout_gravity="fill"/>

<TextView
android:text="[3,1]" 
android:textAppearance="?android:attr/textAppearanceLarge"/>

<TextView
android:text="[3,2]" 
android:textAppearance="?android:attr/textAppearanceLarge"/>

<TextView
android:text="[3,3]" 
android:textAppearance="?android:attr/textAppearanceLarge"/>

</GridLayout>

如你在布局 XML 代码中所见,我们移除了带有文本[2,2][2,3]TextView组件。我们在第1行和第1列放置了一个Space视图。我们将layout_columnSpan设置为2,以使其跨越两列。屏幕将看起来像下面这样:

新视图 - Space

总结

LinearLayoutRelativeLayout是 Android 应用开发中最常见的布局。然而,在设计复杂的用户界面时,你可能需要使用嵌套的 LinearLayout 或 RelativeLayout。这对你的代码性能和可读性是一个缺点,因为这些布局增加了视图层次结构,导致在视图刷新时进行不必要的迭代。GridLayout是 Android Ice Cream Sandwich 中引入的一种新布局,它克服了这些问题。你可以设计用户界面,而无需嵌套布局。如果你正在为 API 级别 14 及以上的版本开发应用,使用 GridLayout 会更好。在下一章中,我们将学习到 Android Ice Cream Sandwich 引入的新社交 APIs。`

第三章:社交 API

随着 Android Ice Cream Sandwich 的推出,引入了新的社交 API,这个 API 使得集成社交网络变得简单。此外,在 Android Ice Cream Sandwich 发布后,可以使用高分辨率照片作为联系人照片。本章通过示例展示了社交 API 的使用。

本章涵盖的主题如下:

  • Android 中的联系人基础

  • 使用社交 API

Android 中的联系人基础

一个人可能有多个联系人信息详情。在 Android 中,这些多个联系人信息详情被合并并显示为一个联系人详情。例如;一个人可能有一个 Google+联系人,一个 Skype 联系人,以及一个电话联系人,Android 将这些联系人全部合并为一个联系人。这些联系人的每个来源都是RawContact。每个 RawContact 有一个或多个数据行,它们保存有关联系人的某些数据,如电话号码、电子邮件等。参考以下框图可以更好地理解它们之间的关系:

Android 中的联系人基础

每个 RawContact 在 Android Ice Cream Sandwich 中都支持存储社交网络流——文本和照片。每个 RawContact 都与StreamItems相关联,其中包含来自社交媒体更新的文本、时间戳和评论,如 Google+,每个 StreamItem 都与包含照片的StreamItemPhotos相关联(如 Google+帖子中的照片)。然而,存储在 RawContact 中的 StreamItems 数量是有限制的。这个数字可以通过查询StreamItems.CONTENT_LIMIT_URI URI获取。当数量超过限制时,将移除时间戳最旧的流项目。以下框图描述了这些块之间的关系:

Android 中的联系人基础

使用社交 API

在以下示例中,我们将展示如何添加StreamItem,以及如何显示已添加的 StreamItems。首先,我们在用户界面中插入了两个按钮,一个用于触发插入操作,另一个用于列出 StreamItems。为了显示 StreamItems,我们在布局中放置了三个TextView组件。布局 XML 应如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >
    <!-- we put two buttons to the user interface, one for triggering insert and one for listing stream items-->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content" >
        <Button
            android:id="@+id/buttonInsert"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Insert" />

        <Button
            android:id="@+id/buttonList"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="List" />
    </LinearLayout>
 <!-- In order to display stream items, we put three TextViews to the layout-->
    <TextView
        android:id="@+id/txt1"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:textAppearance="?android:attr/textAppearanceLarge"/>
    <TextView
        android:id="@+id/txt2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="?android:attr/textAppearanceLarge" />
    <TextView
        android:id="@+id/txt3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="?android:attr/textAppearanceLarge" />
</LinearLayout>

我们将逐步实现Activity类,首先添加一个联系人,然后添加StreamItems并显示它们。带有onCreate()方法的Activity类如下代码块所示:

package com.chapter3;

import java.io.IOException;
import java.io.InputStream;
import java.util.Calendar;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.provider.ContactsContract.CommonDataKinds.Email;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.CommonDataKinds.StructuredName;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.RawContacts;
import android.provider.ContactsContract.StreamItemPhotos;
import android.provider.ContactsContract.StreamItems;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;

public class Chapter3_1Activity extends Activity implements OnClickListener {

  Button insertButton;
  Button listButton;
  Button chooseButton;
  TextView txt1;
  TextView txt2;
  TextView txt3;
  long rawContactId;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
 //initialize UI components
    insertButton = (Button) this.findViewById(R.id.buttonInsert);
    insertButton.setOnClickListener(this);
    listButton = (Button) this.findViewById(R.id.buttonList);
    listButton.setOnClickListener(this);
    txt1 = (TextView) this.findViewById(R.id.txt1);
    txt2 = (TextView) this.findViewById(R.id.txt2);
    txt3 = (TextView) this.findViewById(R.id.txt3);

  }
       @Override
  public void onClick(View v) {
 // when the insert button is clicked, addContact method // is called
    if (v == insertButton)
      this.rawContactId = addContact("Murat Aydın", "9999999",
          "maydin@gmail.com", "Murat", "com.google");
    else if (v == listButton) {
      getStreams(this.rawContactId);
    } 
  }

}

如您在此代码中看到的,我们首先在onCreate(Bundle savedInstanceState)方法中获取布局中的ButtonTextView实例。Chapter3_1Activity类为按钮实现了OnClickListener。如您在onClick(View v)方法中看到的,当点击Insert按钮时,会调用addContact()方法。addContact()方法定义如下:

public long addContact(String name, String phone, String email,
      String accountName, String accountType) {
 // firstly a raw contact is created with the // addRawContact method
    Uri rawContactUri = addRawContact(accountName, accountType);

    if (rawContactUri != null) {
      long rawContactId = ContentUris.parseId(rawContactUri);
 // we use the ID of the created raw contact in // creating name, email, phone number and stream // items
      addName(name, rawContactId);

      addPhoneNumber(phone, rawContactId);

      addEmail(email, rawContactId);

 addContactStreamItem(rawContactId, accountName,
 accountType, "Social Media Update 1");
 addContactStreamItem(rawContactId, accountName,
 accountType, "Social Media Update 2");
 addContactStreamItem(rawContactId, accountName,
 accountType, "Social Media Update 3");

      return rawContactId;
    }
    return 0;
  }

addContact()方法中,首先使用addRawContact()方法创建一个 RawContact。在addRawContact()方法中,我们使用accountNameaccountType来创建原始联系人。addRawContact()方法的定义如下:

public Uri addRawContact(String accountName, String accountType) {
             // we use account name and type to create a raw contact
    ContentResolver cr = getContentResolver();
    ContentValues values = new ContentValues();
 values.put(RawContacts.ACCOUNT_TYPE, accountType);
 values.put(RawContacts.ACCOUNT_NAME, accountName);
    Uri rawContactUri = cr.insert(RawContacts.CONTENT_URI, values);
    return rawContactUri;
  }

创建原始联系人后,我们使用创建的原始联系人 ID 来创建姓名、电子邮件、电话号码和 StreamItems。addName()addEmail()addPhoneNumber()方法使用ContentValues类来创建姓名、电子邮件和电话号码数据,如下代码块所示:

 // This method is for creating email data
  private void addEmail(String email, long rawContactId) {
    ContentResolver cr = getContentResolver();
    ContentValues values = new ContentValues();
    values.put(Email.ADDRESS, email);
    values.put(Email.TYPE, Email.TYPE_OTHER);
    values.put(Email.MIMETYPE, Email.CONTENT_ITEM_TYPE);
    values.put(Data.RAW_CONTACT_ID, rawContactId);
    cr.insert(Data.CONTENT_URI, values);
  }
 //This method is for creating phone number data
  private void addPhoneNumber(String phone, long rawContactId) {

    ContentResolver cr = getContentResolver();
    ContentValues values = new ContentValues();
    values.put(Phone.NUMBER, phone);
    values.put(Phone.TYPE, Phone.TYPE_OTHER);
    values.put(Phone.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
    values.put(Data.RAW_CONTACT_ID, rawContactId);
    cr.insert(Data.CONTENT_URI, values);
  }
       //This method is for adding name data
  private void addName(String name, long rawContactId) {
    ContentValues values = new ContentValues();
    values.put(Data.RAW_CONTACT_ID, rawContactId);
    values.put(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
    values.put(StructuredName.DISPLAY_NAME, name);
    getContentResolver().insert(Data.CONTENT_URI, values);
  }

addContactStreamItem()方法中,我们创建 StreamItems。我们提供了原始联系人 ID、StreamItem 的文本、StreamItem 创建时的时间戳(毫秒)、账户名称和类型来创建 StreamItems。原始联系人 ID、账户名称和类型是创建 StreamItem 所需的必填字段。addContactStreamItem()方法的定义如下:

 //StreamItems are created in this method
  private long addContactStreamItem(long rawContactId, String accountName,
 String accountType, String text) {
 // Raw contact ID, account name and type are required // fields for creating a stream item.

 ContentResolver cr = getContentResolver();
 ContentValues values = new ContentValues();
 values.put(StreamItems.RAW_CONTACT_ID, rawContactId);
 values.put(StreamItems.TEXT, text);
 values.put(StreamItems.TIMESTAMP, Calendar.getInstance().getTime()
 .getTime());
 Uri.Builder builder = StreamItems.CONTENT_URI.buildUpon();
 builder.appendQueryParameter(RawContacts.ACCOUNT_NAME,
 accountName);
 builder.appendQueryParameter(RawContacts.ACCOUNT_TYPE,
 accountType);
 Uri streamItemUri = cr.insert(builder.build(), values);
 long streamItemId = ContentUris.parseId(streamItemUri);

 addContactStreamPhoto(streamItemId, accountName, accountType);

 return streamItemId;
 }

addContactStreamPhoto()方法用于为 StreamItem 创建 StreamItemPhotos。我们必须提供二进制格式的照片,或者PHOTO_FILE_ID,或者PHOTO_URI。正如以下代码块所示,我们使用了loadPhotoFromResourcereadInputStream方法从资源中加载并创建二进制照片。我们还提供了 StreamItem ID、排序索引、账户名称和类型以创建流照片。如果我们不提供排序索引,将使用 ID 列进行排序。addContactStreamPhoto()方法的定义如下:

 //This method is used for creating a stream photo for a stream item
 private long addContactStreamPhoto(long streamItemId,String accountName,
 String accountType) {
 // provide stream item ID, sort index, account name and type for creating a stream photo
 ContentValues values = new ContentValues();
 values.put(StreamItemPhotos.STREAM_ITEM_ID, streamItemId);
 values.put(StreamItemPhotos.SORT_INDEX, 1);
 values.put(StreamItemPhotos.PHOTO, loadPhotoFromResource(R.drawable.ic_launcher));
 Uri.Builder builder = StreamItems.CONTENT_PHOTO_URI.buildUpon();
 builder.appendQueryParameter(RawContacts.ACCOUNT_NAME, accountName);
 builder.appendQueryParameter(RawContacts.ACCOUNT_TYPE, accountType);
 Uri photoUri = getContentResolver().insert(builder.build(), values);
 long photoId = ContentUris.parseId(photoUri);
 return photoId;
 }
 //This method is used for creating a photo in binary
  private byte[] loadPhotoFromResource(int resourceId) {
        InputStream is = getResources().openRawResource(resourceId);
        return readInputStream(is);
    }
  private byte[] readInputStream(InputStream is) {
        try {
            byte[] buffer = new byte[is.available()];
            is.read(buffer);
            is.close();
            return buffer;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

当点击列表按钮时,会调用getStreams()方法。正如以下代码所示,在getStream()方法中,我们首先使用getContactId()方法获取原始联系人的contactId详情。然后我们使用这个联系人 ID 作为查询参数来查询 StreamItems。由于我们查询的是 StreamItems,因此使用ContactsContract.StreamItems.CONTENT_URI作为 URI。最后,使用游标检索 StreamItems,并在 TextViews 中显示 StreamItems 的文本。getStreams()方法和getContactId()方法的定义如下:

 public void getStreams(long rawContactId) {
 long contactId = getContactId(rawContactId);
 ContentResolver cr = getContentResolver();
 Cursor pCur = cr.query(ContactsContract.StreamItems.CONTENT_URI,
 null,
 ContactsContract.StreamItems.CONTACT_ID + " = ?",
 new String[] { String.valueOf(contactId) }, null);
 int i = 0;
 if (pCur.getCount() > 0) {
 while (pCur.moveToNext()) {
 String text = pCur.getString(pCur
 .getColumnIndex(ContactsContract.StreamItems.TEXT));
 if (i == 0)
 this.txt1.setText(text);
 else if (i == 1)
 this.txt2.setText(text);
 else if (i == 2)
 this.txt3.setText(text);
 i++;

 }
 }
 pCur.close();
 }
  public long getContactId(long rawContactId) {
    Cursor cur = null;
    try {
      cur = this.getContentResolver().query(
          ContactsContract.RawContacts.CONTENT_URI,
          new String[] { 
          ContactsContract.RawContacts.CONTACT_ID },
          ContactsContract.RawContacts._ID + "=" + rawContactId, null, null);
      if (cur.moveToFirst()) {
        return cur
            .getLong(cur
                .getColumnIndex(ContactsContract.RawContacts.CONTACT_ID));
      }
    } catch (Exception e) {
      e.printStackTrace();
    } finally {
      if (cur != null) {
        cur.close();
      }
    }
    return -1l;
  }

最后,我们需要一些权限来读取和写入社交流和联系人:READ_SOCI AL_STREAMWRITE_SOCIAL_STREAM, READ_CONTACTSWRITE_CONTACTS。此外,为了使用社交 API,我们必须将最小 SDK 设置为 API Level 15。AndroidManifest.xml文件应如下代码块所示:

<?xml version="1.0" encoding="utf-8"?>
<manifest 
    package="com.chapter3"
    android:versionCode="1"
    android:versionName="1.0" >

 <uses-sdk android:minSdkVersion="15" />
 <uses-permission android:name="android.permission.READ_CONTACTS" />
 <uses-permission android:name="android.permission.READ_SOCIAL_STREAM" />
 <uses-permission android:name="android.permission.WRITE_SOCIAL_STREAM" />
 <uses-permission android:name="android.permission.WRITE_CONTACTS" />

    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        <activity
            android:name=".Chapter3_1Activity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

当我们在模拟器中执行应用程序时,点击插入按钮,然后点击列表按钮;屏幕将如下所示:

使用社交 API

当您在模拟器中执行人员应用时,您会看到名为Murat Aydın的联系人已创建,如下截图所示:

使用社交 API

您还将看到我们以编程方式创建的包含照片的最新社交网络更新:

使用社交 API

设备用户资料

从 API 级别 14 开始,Android 将在联系人列表顶部显示设备用户资料,标记为,如下屏幕所示:

设备用户资料

可以使用 ContactsContract.Profile.CONTENT_URIContactsContract.Profile.CONTENT_RAW_CONTACTS_URI URI 来读取和写入设备用户资料。操作与读取和写入联系人相似,但需要在 AndroidManifest.xml 文件中拥有 READ_PROFILEWRITE_PROFILE 权限。

总结

新的社交 API 随 Android Ice Cream Sandwich 一同引入,使得联系人和社会网络的整合变得更加简单。StreamItemsStreamItemPhotos 类用于存储要保存在联系人中的社交网络更新。在本章中,我们学习了如何使用这些类。此外,我们还学习了设备用户资料,它显示设备用户联系信息。

Android Ice Cream Sandwich 引入了新的 API 来管理日历。在下一章中,我们将学习新的日历 API 及其使用方法。

第四章:日历 API

Android Ice Cream Sandwich 引入了新的日历 API,用于管理日历。这些 API 可以管理事件、参与者、提醒和重复事件的数据库。这些 API 允许我们轻松地将日历与我们的 Android 应用程序集成。本章展示了如何使用示例使用日历 API。

本章涵盖的主题如下:

  • 使用日历 API

  • 创建一个事件

  • 添加参与者

  • 添加一个提醒

使用日历 API

管理日历数据的主要类是CalendarContract类。值得注意的存储日历信息的表格如下:

  • CalendarContract.Calendar:这个表格为每个日历存储特定的日历数据

  • CalendarContract.Event:这个表格为每个事件存储特定的事件数据

  • CalendarContract.Attendee:这个表格存储了事件参与者的数据

  • CalendarContract.Reminder:这个表格存储了事件提醒的数据

备注

在以下示例中,我们将在 Android 设备上执行应用程序,因为在模拟器中测试日历 API 需要一个账户。如果你想在模拟器中测试示例,确保在创建AVD时选择Google API的 API 级别 14 或更高。Google API 允许你向模拟器添加一个 Google 账户,这对于日历 API 是必需的。你还需要设置日历与 Gmail 同步。添加账户时,可以使用[m.google.com](http://m.google.com)作为服务器和<your_email@gmail.com>作为域/用户名。创建并同步账户后,你可以在模拟器中运行以下示例。

创建一个事件

为了创建一个日历事件,我们需要创建一个ContentValues实例并将事件信息放入这个实例中。然后,使用ContentResolver类,我们可以将事件信息插入到日历中。插入日历中的事件需要一些必填字段。这些字段如下:

  • 事件的开始时间

  • 如果事件不是重复的,其结束时间

  • 如果事件是重复的,其重复规则或重复日期

  • 如果事件是重复的,其持续时间

  • 事件时区和日历 ID

插入事件的Activity类定义如下:

package com.chapter4;

import java.util.Calendar;
import java.util.TimeZone;

import android.app.Activity;
import android.content.ContentValues;
import android.database.Cursor;
import android.os.Bundle;
import android.content.ContentUris;
import android.net.Uri;
import android.provider.CalendarContract;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;

public class Chapter4_1Activity extends Activity implements OnClickListener {

  Button insertButton;
  long calendarID;
  long eventID;

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        insertButton = (Button)this.findViewById(R.id.buttonInsertEvent);
        insertButton.setOnClickListener(this);
    }

  @Override
  public void onClick(View v) {

    addEvent();

  }

  private void addEvent() {
    calendarID = getCalendarID();
    ContentValues eventValues = new ContentValues ();
 // provide the required fields for creating an event to
 // ContentValues instance and insert event using // ContentResolver
    eventValues.put (CalendarContract.Events.CALENDAR_ID,calendarID);
    eventValues.put (CalendarContract.Events.TITLE,"Event 1");
    eventValues.put (CalendarContract.Events.DESCRIPTION,
    "Testing Calendar API");
    eventValues.put 
(CalendarContract.Events.DTSTART,Calendar.getInstance().getTimeInMillis());
    eventValues.put 
(CalendarContract.Events.DTEND,Calendar.getInstance().getTimeInMillis());

  eventValues.put(CalendarContract.Events.EVENT_TIMEZONE,
TimeZone.getDefault().toString());

    Uri eventUri = this.getContentResolver().insert 
(CalendarContract.Events.CONTENT_URI, eventValues);
             eventID = ContentUris.parseId(eventUri);
  }
 // we use this method in order to get the ID of the calendar because
 // calendar ID is a required field in creating an event
  public long getCalendarID() {
    Cursor cur = null;
    try {
 // provide CalendarContract.Calendars.CONTENT_URI to
 // ContentResolver to query calendars
      cur = this.getContentResolver().query(
          CalendarContract.Calendars.CONTENT_URI,
          null,null,null, null);
      if (cur.moveToFirst()) {
        return cur
            .getLong(cur

  .getColumnIndex(CalendarContract.Calendars._ID));
      }
      } catch (Exception e) {
      e.printStackTrace();
    } finally {
      if (cur != null) {
      cur.close();
      }
    }
    return -1L;
  }
}

如你所见,在这段代码中,我们使用getCalendarID()方法来获取日历的 ID,因为calendarID在创建事件时是一个必填字段。我们向ContentResolver提供了CalendarContract.Calendars.CONTENT_URI以查询日历。

我们使用了按钮点击事件来添加一个事件。点击这个按钮时,我们调用addEvent()方法。在addEvent()方法中,我们为创建事件的必要字段提供给ContentValues实例,并使用ContentResolver插入事件。我们向ContentResolver提供CalendarContract.Events.CONTENT_URI以添加一个事件。

本应用程序布局的 XML 代码是LinearLayout,包含一个Button组件,如下代码块所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <Button
        android:id="@+id/buttonInsertEvent"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="insert event" />

</LinearLayout>

执行此代码时,屏幕将如下所示:

创建事件

为了使用新的日历 API,AndroidManifest.xml文件中的最低 SDK 版本应设置为 API 级别 14 或更高。此外,还需要WRITE_CALENDARREAD_CALENDAR权限才能读写日历。AndroidManifest.xml文件应如下所示:

<?xml version="1.0" encoding="utf-8"?>
<manifest 
    package="com.chapter4"
    android:versionCode="1"
    android:versionName="1.0" >

 <uses-sdk android:minSdkVersion="14" />
<uses-permission android:name="android.permission.WRITE_CALENDAR" />
<uses-permission android:name="android.permission.READ_CALENDAR" />
    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        <activity
            android:name=".Chapter4_1Activity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

创建事件后,日历将如下所示:

创建事件

使用意图创建事件

同样可以使用Intent对象创建事件。以下方法展示了如何使用Intent对象添加事件:

private void addEventUsingIntent() {
    calendarID = getCalendarID();
    Intent intent = new Intent(Intent.ACTION_INSERT)
        .setData(CalendarContract.Events.CONTENT_URI)
        .putExtra(CalendarContract.Events.DTSTART, 
Calendar.getInstance().getTimeInMillis())
        .putExtra(CalendarContract.Events.DTEND, 
Calendar.getInstance().getTimeInMillis())
        .putExtra(CalendarContract.Events.TITLE,"Event 1")
        .putExtra(CalendarContract.Events.DESCRIPTION,"Testing Calendar API");
    startActivity(intent);     
  }

我们可以调用这个方法,而不是addEvent()方法,以使用Intent对象创建事件。通过使用Intent对象,我们无需创建视图即可创建事件。使用Intent对象是修改和显示日历的最佳实践。

添加参与者

添加参与者的过程与创建事件类似。我们使用CalendarContract.Attendees.CONTENT_URI作为插入参与者的 URI。插入参与者所需的字段包括事件 ID、参与者电子邮件、参与者关系、参与者状态和参与者类型。我们在应用程序的 XML 布局中放置了一个Button组件。结果布局如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <Button
        android:id="@+id/buttonInsertEvent"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="insert event" />

    <Button
        android:id="@+id/buttonInsertAttendee"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="insert attendee" />
</LinearLayout>

然后我们在点击添加参与者按钮时调用以下方法:

  private void addAttendee()
  {
    ContentValues cv = new ContentValues();
    cv.put(Attendees.ATTENDEE_NAME, "Murat AYDIN");
    cv.put(Attendees.ATTENDEE_EMAIL, "maydin@gmail.com");
    cv.put(Attendees.EVENT_ID, eventID);
    cv.put(Attendees.ATTENDEE_RELATIONSHIP, 
Attendees.RELATIONSHIP_ATTENDEE);
    cv.put(Attendees.ATTENDEE_STATUS, 
Attendees.ATTENDEE_STATUS_INVITED);
    cv.put(Attendees.ATTENDEE_TYPE,Attendees.TYPE_OPTIONAL);

  this.getContentResolver().insert(CalendarContract.Attendees.CONTENT_URI, 
cv);
  }

在点击添加参与者按钮之前,应该先创建一个事件,因为我们在插入参与者时需要使用事件 ID。

添加提醒

我们使用CalendarContract.Reminder.CONTENT_URI作为插入事件提醒的 URI。插入提醒所需的字段包括事件 ID、提醒需要在事件前触发的分钟数以及方法。我们在应用程序的 XML 布局中放置了一个Button组件。结果布局如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <Button
        android:id="@+id/buttonInsertEvent"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="insert event" />

    <Button
        android:id="@+id/buttonInsertAttendee"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="insert attendee" />

    <Button
        android:id="@+id/buttonInsertReminder"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="insert reminder" />

</LinearLayout>

然后我们在点击添加提醒按钮时调用以下方法:

private void addReminder()
  {
    ContentValues values = new ContentValues();
    values.put(Reminders.MINUTES, 15);
    values.put(Reminders.EVENT_ID, eventID);
    values.put(Reminders.METHOD, Reminders.METHOD_ALERT);

  this.getContentResolver().insert(CalendarContract.Reminders.CONTENT_URI, 
  values);
  }

如你所见,在这段代码中,这个提醒会在事件发生前 15 分钟触发。在按下添加提醒按钮之前,应该先创建一个事件,因为我们在插入提醒时需要使用事件 ID。

添加提醒后,日历将如下所示:

添加提醒

概要

使用新的日历 API,将日历集成到 Android 应用程序变得更加容易。在本章中,我们学习了如何创建一个事件以及创建事件所需的字段。然后我们了解了如何向事件中添加参与者和提醒。我们需要设置修改日历所需的权限。

尽管片段(Fragments)是在 Android 3.0 中引入的,但现在它们也适用于配备 Android Ice Cream Sandwich 的小屏幕设备。在下一章中,我们将介绍片段的基础知识以及如何使用它们。

第五章:碎片

尽管碎片是在 Android 3.0 中引入的,但现在它们也适用于小屏幕设备,支持 Android Ice Cream Sandwich。本章将介绍碎片的基础知识以及如何使用它们。

本章涵盖的主题如下:

  • 碎片基础

  • 创建和管理碎片

  • 碎片类型

碎片基础

碎片是活动中的一个模块化组件,它有自己的生命周期和事件处理,与活动非常相似。尽管碎片有自己的生命周期,但它们直接受到所属活动生命周期的直接影响。例如,如果一个活动被销毁,它的碎片也会被销毁。每个碎片都应该有一个所属活动。碎片可以动态地添加到活动中或从活动中移除。

碎片提高了软件的可复用性,并在用户界面设计中提供了灵活性。一个碎片可以被多个活动使用。这样,你只需实现一次,就可以多次使用。此外,碎片可以用于不同的布局配置和不同的屏幕模式,从而在用户界面设计中提供灵活性。

注意

设计碎片时,重要的是要使它们能够独立工作,即它们不应该依赖于其他碎片和活动。这样,碎片可以独立于其他碎片被复用。

碎片生命周期

碎片拥有自己的生命周期;然而,它们仍然直接受到所属活动生命周期的直接影响。下图展示了碎片生命周期的创建流程:

碎片生命周期

图中的各个块执行以下任务:

  • onAttach(): 当碎片被添加到活动中时,会调用onAttach()方法。

  • onCreate(): 当创建碎片时,会调用此方法。

  • onCreateView(): 此方法返回一个视图。这个视图是碎片的用户界面。如果碎片只进行后台工作并且没有用户界面,那么这个方法应该返回 null。

  • onActivityCreated(): 在所属活动创建后,会调用此方法。

  • onStart(): 在此方法被调用后,碎片的视图对用户可见。

  • onResume(): 在此方法被调用后,碎片变为活动状态,用户可以与碎片交互。这个方法可能会被多次调用,因为每次应用重新启动或暂停后都会调用此方法。

下图展示了碎片生命周期的销毁流程:

碎片生命周期

图中的各个块执行以下任务:

  • onPause(): 当碎片暂停并且不再与用户交互时,会调用此方法。

  • onStop(): 当碎片停止时,会调用此方法。在此方法被调用后,碎片对用户不再可见。

  • onDestroyView(): 当碎片视图被销毁时,会调用此方法。

  • onDestroy(): 当片段不再使用时,会调用这个方法。

  • onDetach(): 当片段从活动中移除时,会调用这个方法。

创建和管理片段

我们将要学习如何通过一个示例安卓应用来创建和管理片段。这个应用将列出书籍名称。当点击书籍名称时,会显示书籍的作者。这个应用将针对小屏幕和大屏幕设备进行设计,这样我们就能看到如何在不同屏幕尺寸下使用片段。以下是为小屏幕设备的应用截图。如您在这张截图中所见,屏幕左侧有书籍列表,当点击书籍时,屏幕右侧会显示点击书籍的作者信息:

创建和管理片段

我们将首先实现这些屏幕,然后针对大屏幕设计这个应用。

在这个应用中,我们有两个活动(Activity),一个用于第一屏,另一个用于第二屏。每个活动包含一个片段(Fragment)。以下图表展示了该应用的结构:

创建和管理片段

Fragment B 的布局 XML 代码如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center_horizontal"
    android:orientation="vertical" >
    <TextView
        android:id="@+id/textViewAuthor"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="?android:attr/textAppearanceLarge" />

</LinearLayout>

如您在这段代码中看到的,它有一个包含 TextView 组件的 LinearLayout 布局。TextView 用于显示书籍的作者。我们没有为 Fragment A 设计布局,因为它是包含 ListView 组件的 ListFragment 属性。

现在我们需要为每个片段创建两个扩展 Fragment 类的类。以下是 Fragment A 的类定义:

package com.chapter5;

import android.app.ListFragment;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;

public class Chapter5_1FragmentA extends ListFragment implements  
OnItemClickListener {

  @Override
  public void onActivityCreated(Bundle savedInstanceState) {
             super.onActivityCreated(savedInstanceState);
             //initialize the adapter and set on click events of items
    ArrayAdapter<String> adapter = new ArrayAdapter<String>(getActivity(),
        android.R.layout.simple_list_item_1, Book.BOOK_NAMES);
    this.setListAdapter(adapter);
    getListView().setOnItemClickListener(this);
  }
  @Override
  public void onItemClick(AdapterView<?> parent, View view, int position, long id) 
  {
    //Start a new Activity in order to display author name
    String author = Book.AUTHOR_NAMES[position];
    Intent intent = new Intent(getActivity().getApplicationContext(),
    Chapter5_1Activity_B.class);
    intent.putExtra("author", author);
    startActivity(intent);
  }
}

如您所见,Chapter5_1FragmentA 类扩展了 ListFragment,因为我们在这一屏中列出书籍。它类似于 ListActivity,并且这个类有一个 ListView 视图。在 onActivityCreated 方法中,我们设置了 ListFragmentListAdapter 属性。适配器的数据源是一个包含书籍名称和作者字符串数组的类,如下代码块所示:

package com.chapter5;

public class Book {
  public static final String[] BOOK_NAMES = { "Book Name 1", "Book Name 2", "Book Name 3", "Book Name 4", "Book Name 5", "Book Name 6", "Book Name 7", "Book Name 8" };
  public static final String[] AUTHOR_NAMES = { "Author of Book 1", "Author of Book 2", "Author of Book 3", "Author of Book 4", "Author of Book 5", "Author of Book 6", "Author of Book 7", "Author of Book 8" };
}

在初始化ListAdapter之后,我们设置了ListView视图的OnItemClickListener事件。当点击ListView的项时,会调用此事件。项被点击时,会调用onItemClick方法。在这个方法中,会启动一个新的活动,显示书籍作者的信息。如代码所示,我们通过getActivity()方法获取到片段的所有者活动。我们也可以通过getActivity()方法获取ApplicationContext。请记住,OnCreateView方法是在OnActivityCreated之前调用的,因此我们在OnActivityCreated方法中初始化ListAdapterListView,因为我们需要在初始化它们之前创建用户界面组件,而这些组件是在OnCreateView中创建的。我们不需要重写ListFragmentOnCreateView方法,因为它已经返回了一个ListView。如果你想要使用自定义的ListView,可以重写OnCreateView方法。

以下是Fragment B的类:

package com.chapter5;

import android.app.Fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

public class Chapter5_1FragmentB extends Fragment {

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container,
      Bundle savedInstanceState) {

    View view = inflater.inflate(R.layout.fragment_b, container, false);
    return view;
  }
}

从这段代码中可以看出,如果一个片段有用户界面,那么应该重写这个方法,并且返回一个视图。在我们的示例应用程序中,我们返回了一个使用我们之前实现的 XML 布局填充的视图。

现在我们需要两个托管这些片段的活动类。以下是托管Fragment AActivity AActivity类:

package com.chapter5;

import android.app.Activity;
import android.os.Bundle;

public class Chapter5_1Activity_A extends Activity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_a);
    }
}

这是一个简单的Activity类,只是用布局设置了内容视图。Activity A类的 XML 布局代码如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical">

    <fragment
        android:id="@+id/fragment_a"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        class="com.chapter5.Chapter5_1FragmentA" />

</LinearLayout>

从这段代码中可以看出,我们使用类属性com.chapter5.Chapter5_1FragmentA指定了Fragment A,并且我们还指定了id属性。片段应该有一个idtag属性作为标识符,因为当活动重新启动时,Android 需要它来恢复片段。

以下是托管Fragment BActivity BActivity类:

package com.chapter5;

import android.app.Activity;
import android.os.Bundle;
import android.widget.TextView;

public class Chapter5_1Activity_B extends Activity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_b);

        Bundle extras = getIntent().getExtras();
    if (extras != null) {
      String s = extras.getString("author");
      TextView view = (TextView) findViewById(R.id.textViewAuthor);
      view.setText(s);
    }
    }
}

这是一个简单的Activity类,只是用布局设置了内容视图。Activity B的 XML 布局代码如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <fragment
        android:id="@+id/fragment_b"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        class="com.chapter5.Chapter5_1FragmentB" />

</LinearLayout>

从这段代码中可以看出,我们使用类属性com.chapter5.Chapter5_1FragmentB指定了Fragment B

以编程方式添加一个片段

在我们之前的示例应用程序中,我们曾在 XML 布局代码中将一个片段添加到活动布局中。你也可以以编程方式将片段添加到活动中。以下是我们之前示例应用程序的以编程方式添加片段的版本以及活动的 XML 布局代码:

package com.chapter5;

import android.app.Activity;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.os.Bundle;

public class Chapter5_1Activity_A extends Activity {
  /** Called when the activity is first created. */
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_a);
    addFragment();
  }

 public void addFragment() {
 FragmentManager fragmentManager = getFragmentManager();
 FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();

 Chapter5_1FragmentA fragment = new Chapter5_1FragmentA();
 fragmentTransaction.add(R.id.layout_activity_a, fragment);
 fragmentTransaction.commit();
 }
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
    android:id="@+id/layout_activity_a"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

</LinearLayout>

从这个 XML 代码中可以看出,我们移除了Fragment标签,因为我们是程序化地添加Fragment A。在Chapter5_1Activity_A类中,我们添加了一个名为addFragment()的方法。我们使用了FragmentTransaction类来添加Fragment AFragmentTransaction类用于添加片段、移除片段、将片段附加到 UI 等操作。在addMethod()方法中可以看出,你可以通过FragmentManager使用beginTransaction()方法获取FragmentTransaction的实例。最后,我们必须调用commit()方法,以便应用更改。

FragmentManager用于管理片段。从代码中可以看出,你可以通过getFragmentManager()方法获取FragmentManager的实例。FragmentManager允许你通过beginTransaction()方法开始一个事务,通过findFragmentById()findFragmentbyTag()方法在活动中获取一个片段,以及通过popBackStack()方法将片段从返回栈中弹出。

与活动共享事件

在我们的示例中,我们在ListFragment类的onItemClick方法中启动了一个活动。我们可以通过在ListFragment中创建一个回调接口,并让Activity类实现该回调,来建立相同的操作。这样,Fragment类就会通知所有者Activity类。当所有者Activity类得到通知时,可以通过其他片段共享该通知。这种方式,片段可以共享事件并进行通信。我们可以按照以下步骤进行此操作:

  1. 我们在Chapter5_1FragmentA类中创建回调接口:

    public interface OnBookSelectedListener 
      {
        public void onBookSelected(int bookIndex);
      }
    
  2. 我们在Chapter5_1FragmentA类中创建一个OnBookSelectedListener实例,并将所有者活动分配给该实例:

    OnBookSelectedListener mListener;
    @Override
      public void onAttach(Activity activity) {
        super.onAttach(activity);
        mListener = (OnBookSelectedListener) activity;
      }
    

    从这段代码中可以看出,Chapter5_1FragmentA的所有者活动类应该实现onBookSelectedListener实例,否则会出现类转换异常。

  3. 我们让Chapter5_1Activity_A类实现onBookSelectedListener接口:

    public class Chapter5_1Activity_A extends Activity implements 
    OnBookSelectedListener {
    //some code here
            @Override
            public void onBookSelected(int bookIndex) {
    
              String author = Book.AUTHOR_NAMES[bookIndex];
              Intent intent = new Intent(this,Chapter5_1Activity_B.class);
              intent.putExtra("author", author);
              startActivity(intent);
           } 
    //some more code here
    }
    

    从这段代码中可以看出,Chapter5_1Activity_A在事件回调中接收选定的书籍索引,并使用作者数据启动活动。

  4. 我们在Chapter5_1FragmentA类的onItemClick方法中调用了onBookSelected方法:

    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
    
      mListener.onBookSelected(position);
    }
    

这样,我们就让活动和片段共享了一个事件回调。

在活动中使用多个片段

我们的示例书籍列表应用程序是为小屏幕设计的。当你在更大的屏幕上执行此应用程序时,它看起来会很糟糕。我们必须在大屏幕尺寸中有效地利用空间。为了实现这一点,我们必须为大屏幕创建一个新的布局。新布局如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
    android:id="@+id/layout_small_a"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="horizontal" >

 <fragment
        android:id="@+id/fragment_a"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        class="com.chapter5.Chapter5_1FragmentA" 
        android:layout_weight="1"/>
 <fragment
        android:id="@+id/fragment_b"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        class="com.chapter5.Chapter5_1FragmentB" 
        android:layout_weight="1"/>

</LinearLayout>

从此代码中可以看出,我们将两个片段放入了水平LinearLayout布局中。在之前的示例应用程序中,每个活动中只有一个片段,但为了有效地利用空间,这个活动中有两个片段。通过将layout_weight属性设置为1,我们使片段在屏幕上占据相等的空间。

我们必须将这个新的布局 XML 文件放在res文件夹下的一个名为layout-xlarge-land的文件夹中。这样,当设备屏幕大且处于横屏模式时,Android 会使用这个布局文件。Android 根据布局文件夹名称在运行时决定使用哪个布局文件。layout是 Android 的默认文件夹名称。如果 Android 找不到适合设备屏幕尺寸和模式的布局文件夹,它会使用layout文件夹中的布局。一些常见的布局限定符如下:

  • small用于小屏幕尺寸

  • normal用于正常屏幕尺寸

  • large用于大屏幕尺寸

  • xlarge用于超大屏幕尺寸

  • land用于横屏方向

  • port用于竖屏方向

然而,这种布局还不足以使我们的示例在大屏幕上正确运行。为了使新布局正确运行,我们必须改变管理片段的方式。更新Chapter5_1Activity_A中的onBookSelected属性如下:

  @Override
  public void onBookSelected(int bookIndex) {

    FragmentManager fragmentManager = getFragmentManager();
    Fragment fragment_b = fragmentManager.findFragmentById(R.id.fragment_b);
    String author = Book.AUTHOR_NAMES[bookIndex];
    if(fragment_b == null)
    {
      Intent intent = new Intent(this,
          Chapter5_1Activity_B.class);
      intent.putExtra("author", author);
      startActivity(intent);
    }
    else
    {
      TextView textViewAuthor = (TextView)fragment_b.getView().findViewById(R.id.textViewAuthor);
      textViewAuthor.setText(author);
    }
  }

从这段代码中可以看出,我们通过使用FragmentManager获取了Fragment B类。如果fragment_b不为空,我们理解此活动包含Fragment B,并且设备具有大屏幕,因为只有在屏幕大且处于横屏模式时,Activity A才会使用Fragment B。然后使用fragment_b,我们获取了textViewAuthor TextView 组件,并用所选书籍的作者姓名更新其文本。在屏幕右侧,我们可以看到所选书籍的作者姓名。

如果fragment_b为空,我们理解设备屏幕较小,并通过Intent启动新的活动。

AndroidManifest.xml文件中,我们必须将最低 SDK 版本设置为 API 级别 14,因为自 API 级别 14 起,小屏幕上就已经可以使用片段了。AndroidManifest.xml文件应如下面的代码块所示:

<?xml version="1.0" encoding="utf-8"?>
<manifest 
    package="com.chapter5"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk android:minSdkVersion="14" />

    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        <activity
            android:name=".Chapter5_1Activity_A"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name=".Chapter5_1Activity_B"/>
    </application>

</manifest>

在大屏幕上,我们的示例应用程序将如下所示:

在活动中使用多个片段

片段的类型

有四种类型的片段:

  • ListFragment

  • DialogFragment

  • PreferenceFragment

  • WebViewFragment

在本节中,我们将开发一个使用这些片段的示例应用程序。在本节结束时,应用程序将开发完成。

ListFragment

此片段与ListActivity相似,默认包含一个ListView视图,用于显示项目列表。在我们之前的示例代码中,我们使用了ListFragment;有关ListFragment的创建和管理,请参阅创建和管理片段部分。

DialogFragment

此片段在其所属活动的顶部显示一个对话框。在以下示例应用程序中,我们将创建一个带有删除按钮的片段。点击该按钮时,将显示一个DialogFragment对话框。DialogFragment对话框将包含一条确认信息以及两个按钮——确定取消按钮。如果点击确定按钮,将显示一条消息,并关闭DialogFragment。示例应用程序的屏幕将如下截图所示:

DialogFragment

带有删除按钮的片段的布局 XML 代码如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center_horizontal"
    android:orientation="vertical" >

    <Button
        android:id="@+id/buttonFragment"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Delete" />

</LinearLayout>

这个布局是一个简单的布局,其中包含一个LinearLayout布局和一个Button组件。此布局的Fragment类如下:

package com.chapter5;

import android.app.Fragment;
import android.app.FragmentTransaction;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.Button;

public class Chapter5_2Fragment extends Fragment implements OnClickListener{

  Button fragmentButton;
  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container,
      Bundle savedInstanceState) {

    View view = inflater.inflate(R.layout.fragment, container, false);
    fragmentButton = (Button)view.findViewById(R.id.buttonFragment);
    fragmentButton.setOnClickListener(this);
    return view;
  }

  @Override
 public void onClick(View v) {
 //we need a FragmentTransaction in order to display a dialog
 FragmentTransaction transaction = getFragmentManager().beginTransaction();

 Chapter5_2DialogFragment dialogFragment = new Chapter5_2DialogFragment();

 dialogFragment.show(transaction, "dialog_fragment");

 }
}

如您从这段代码中看到的,在Chapter5_2Fragment类的onClick方法中,创建了Chapter5_2DialogFragment类的一个实例,并使用此实例显示对话框,通过其show()方法。

DialogFragment对话框的布局代码如下:

<?xml version="1.0" encoding="utf-8"?>
<GridLayout android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center_horizontal"
    android:columnCount="2"
    android:orientation="horizontal" >

    <TextView
        android:id="@+id/textViewMessage"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_columnSpan="2"
        android:layout_gravity="fill"
        android:text="This item will be deleted. Do you want to continue?"
        android:textAppearance="?android:attr/textAppearanceLarge" />

     <!—we used a linear layout here because we need it in order to evenly distribute the buttons -->
    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_gravity="fill_horizontal"
        android:layout_columnSpan="2" >

        <Button
            android:id="@+id/buttonOk"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="OK" />

    <Button
        android:id="@+id/buttonCancel"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:text="CANCEL" />

    </LinearLayout>

</GridLayout>

如您从之前的代码中看到的,我们使用了GridLayout作为根布局。然后我们输入了一个显示确认信息的TextView组件。最后,在布局中添加了两个按钮——确定取消按钮。以下是此布局的DialogFragment类:

package com.chapter5;

import android.app.DialogFragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.Toast;

public class Chapter5_2DialogFragment extends DialogFragment implements
OnClickListener{

  Button okButton;
  Button cancelButton;
  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container,
      Bundle savedInstanceState) {

    View view = inflater.inflate(R.layout.dialog_fragment, container, false);
    //initialize the buttons and set click events
    okButton = (Button)view.findViewById(R.id.buttonOk)
    okButton.setOnClickListener(this);

    cancelButton = (Button)view.findViewById(R.id.buttonCancel);
    cancelButton.setOnClickListener(this);

    return view;
  }

  @Override
  public void onClick(View v) {

    if(v == cancelButton)
      dismiss();
    else if( v == okButton)
    {
      Toast.makeText(this.getActivity(), "Item is deleted.", Toast.LENGTH_LONG).show();
      dismiss();
    }

  }
}

如您从这段代码中看到的,这个类扩展了DialogFragment类。在Chapter5_2DialogFragment类的onCreateView方法中,我们初始化按钮并为它们设置onClick事件。在Chapter5_2DialogFragment类的onClick方法中,我们处理按钮点击事件。如果点击的按钮是取消,我们关闭对话框窗口。如果点击的按钮是确定,我们显示一条信息消息并关闭对话框。如您从前面的代码中看到的,dismiss()方法用于关闭对话框。

PreferenceFragment

这个片段与PreferenceActivity类似。它显示偏好设置,并将其保存到SharedPreferences中。在本节中,我们将扩展之前的示例代码。我们将添加一个关于在删除期间显示确认信息的偏好设置。用户可以选择是否查看确认信息。以下是使用PreferenceFragment的步骤:

  1. 创建一个偏好屏幕的源 XML,并将其放在res/xml文件夹下:

    <?xml version="1.0" encoding="utf-8"?>
    <PreferenceScreen  >
    
        <CheckBoxPreference android:summary="check this in order to show confirmation message when deleting"
            android:title="show confirmation message" 
            android:key="checkbox_preference"/>
    
    </PreferenceScreen>
    

    如您从之前的代码中看到的,我们的偏好屏幕包含一个用于确认信息的复选框偏好设置。

  2. 创建一个扩展PreferenceFragment的类:

    package com.chapter5;
    
    import android.os.Bundle;
    import android.preference.PreferenceFragment;
    
    public class Chapter5_2PereferenceFragment extends PreferenceFragment {
    
      @Override
      public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        addPreferencesFromResource(R.xml.pref);
      }
    
    }
    

从这段代码中可以看出,创建一个偏好设置屏幕非常简单;你只需调用addPreferencesFromResource方法,并传入你为偏好设置创建的 XML 文件即可。现在,我们将添加一个设置选项菜单项,并通过点击这个菜单项来打开偏好设置屏幕。为了实现这一点,我们将按照以下步骤修改Chapter5_2Fragment类:

  1. 我们将在Chapter5_2Fragment类的onCreateView方法中添加setHasOptionsMenu(true)

      @Override
      public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) 
    {
    
        View view = inflater.inflate(R.layout.fragment, container, false);
        fragmentButton = (Button)view.findViewById(R.id.buttonFragment);	
        fragmentButton.setOnClickListener(this);
    
        setHasOptionsMenu(true);
        return view;
      }
    
  2. 我们将在Chapter5_2Fragment类中添加以下方法:

      @Override
      public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
        inflater.inflate(R.menu.fragment_menu, menu);
    
      }
    
      @Override
      public boolean onOptionsItemSelected(MenuItem item) {
    
        Intent intent = new Intent(getActivity(),Chapter5_2PreferenceActivity.class);
        startActivity(intent);
        return true;
      }
    

从这段代码中可以看出,onCreateOptionsMenu为选项菜单做出了贡献。这就是一个片段如何为所属活动的菜单做出贡献的。当点击选项菜单项时,将使用onOptionsItemSelected方法启动一个新的活动。

fragment_menu菜单 XML 如下所示:

<menu >
    <item android:id="@+id/itemSettings" android:title="Settings"></item>
</menu>

Chapter5_2PreferenceActivity是托管Chapter5_2PereferenceFragment的类:

package com.chapter5;

import android.app.Activity;
import android.os.Bundle;

public class Chapter5_2PreferenceActivity extends Activity {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    getFragmentManager().beginTransaction()
 .replace(android.R.id.content, new Chapter5_2PereferenceFragment())
 .commit();
  }
}

从这段代码中可以看出,我们以编程方式将Chapter5_2PereferenceFragment添加到Chapter5_2PreferenceActivity类中。

偏好设置屏幕应如下所示截图:

偏好设置截图

通过添加此偏好设置选项,用户可以选择是否接收确认消息。(要读取设置,请使用标准的SharedPreference API。)

WebViewFragment

WebViewFragment是一个预先封装在片段中的WebView。当片段暂停或恢复时,此片段中的WebView会自动暂停或恢复。在本节中,我们将扩展之前的示例代码,以展示WebViewFragment的使用方法。

  1. 我们在Chapter5_2Fragment类的布局 XML 代码中添加了一个打开网页按钮。生成的布局如下所示:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout 
        android:id="@+id/layout_fragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center_horizontal"
        android:orientation="vertical" >
    
        <Button
            android:id="@+id/buttonFragment"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Delete" />
    
     <Button
     android:id="@+id/buttonOpenWeb"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:text="Open Web" />
    
    </LinearLayout>
    
  2. 我们创建了一个扩展WebViewFragment的类,以及一个使用以下代码块托管此片段的活动:

    package com.chapter5;
    
    import android.os.Bundle;
    import android.webkit.WebViewFragment;
    
    public class Chapter5_2WebViewFragment extends WebViewFragment {
    
      @Override
      public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
    
        getWebView().loadUrl("http://www.google.com");
      }
    }
    

从这段代码中可以看出,我们在onActivityCreated方法中获取了WebView实例,并加载了一个打开谷歌网站的 URL。

托管此片段的活动如下:

package com.chapter5;

import android.app.Activity;
import android.os.Bundle;

public class Chapter5_2WebActivity extends Activity {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

 getFragmentManager().beginTransaction()
 .replace(android.R.id.content, new Chapter5_2WebViewFragment())
 .commit();
  }
}

从这段代码中可以看出,我们以编程方式将Chapter5_2WebViewFragment添加到Chapter5_2WebViewActivity中。当点击打开网页按钮时,这个示例应用程序将打开www.google.com网站。

Chapter5_2Fragment类的最终版本如下:

package com.chapter5;

import android.app.Fragment;
import android.app.FragmentTransaction;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.Button;

public class Chapter5_2Fragment extends Fragment implements OnClickListener{

  Button fragmentButton;
  Button openWebButton;

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

    View view = inflater.inflate(R.layout.fragment, container, false);
    fragmentButton = (Button)view.findViewById(R.id.buttonFragment);	
    fragmentButton.setOnClickListener(this);

    openWebButton = (Button)view.findViewById(R.id.buttonOpenWeb);
    openWebButton.setOnClickListener(this);

    setHasOptionsMenu(true);
    return view;
  }

  @Override
  public void onClick(View v) {
 if(v == fragmentButton)
 {
 FragmentTransaction transaction = getFragmentManager().beginTransaction();
 Chapter5_2DialogFragment dialogFragment = new Chapter5_2DialogFragment();
 dialogFragment.show(transaction, "dialog_fragment");
 }
 else if( v == openWebButton)
 {
 Intent intent = new Intent(getActivity(),Chapter5_2WebActivity.class);
 startActivity(intent);
 }
  }

  @Override
  public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    inflater.inflate(R.menu.fragment_menu, menu);

  }

  @Override
  public boolean onOptionsItemSelected(MenuItem item) {

Intent intent = new Intent(getActivity(),Chapter5_2PreferenceActivity.class);
    startActivity(intent);
    return true;
  }
}

该应用程序的主Activity类如下所示:

package com.chapter5;

import android.app.Activity;
import android.os.Bundle;

public class Chapter5_2Activity extends Activity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

}

这个Activity类是Chapter5_2Fragment的所有者活动。前面Activity的布局如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
    android:id="@+id/layout_small_a"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

 <fragment
 android:id="@+id/fragment"
 android:layout_width="match_parent"
 android:layout_height="match_parent"
 class="com.chapter5.Chapter5_2Fragment" />

</LinearLayout>

该示例应用程序的AndroidManifest.xml文件应如下所示:

<manifest 
    package="com.chapter5"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
 android:minSdkVersion="14"
 android:targetSdkVersion="15" />
    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name=".Chapter5_2Activity"
            android:label="@string/title_activity_main" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
 <activity android:name=".Chapter5_2PreferenceActivity"></activity>
 <activity android:name=".Chapter5_2WebActivity"></activity>
    </application>

</manifest>

从这段代码中可以看出,我们需要互联网权限来打开一个网站。此外,为了在小屏幕上使用片段,我们还需要将最低 SDK 设置为 API Level 14。

总结

片段(Fragments)在引入 Android Ice Cream Sandwich 的小屏幕设备上可用。在本章中,我们首先学习了片段的基础知识,以及片段的构建与销毁生命周期。接着,我们通过一个示例应用程序学习了创建和管理片段的方法。最后,我们了解了片段的特殊类型——ListFragmentDialogFragmentPreferenceFragmentWebViewFragment。在下一章中,我们将看到一些实践方法,以开发支持不同屏幕尺寸的应用程序。

第六章:支持不同的屏幕尺寸

安卓 3.0 仅适用于大屏幕设备。然而,安卓冰淇淋三明治系统适用于所有小屏幕和大屏幕设备。开发者应该创建支持大屏幕和小屏幕尺寸的应用程序。本章将展示支持不同屏幕尺寸的设计用户界面的方法。

本章涵盖的主题包括:

  • 使用match_parentwrap_content

  • 使用九宫格图(nine-patch)

  • 使用dip而不是px

安卓 4.0 支持不同的屏幕尺寸

安卓设备种类繁多,因此也有许多不同的屏幕尺寸。

下面的图表(来源opensignalmaps.com)展示了安卓设备碎片化情况:

安卓 4.0 支持不同的屏幕尺寸

如同图中所示,有各种各样的设备(近 4000 种独特的设备)。这意味着有许多不同的屏幕尺寸和密度。安卓会缩放和调整你的应用程序的用户界面。然而,这并不总是足够。例如,为小屏幕设计的用户界面在大型屏幕上会被放大。这在大型屏幕上看起来并不美观。大型屏幕上的空间应该被有效利用,大型屏幕的用户界面应该与小屏幕的用户界面不同。安卓提供了一些 API,用于设计适合不同屏幕尺寸和密度的用户界面。你应该使用这些 API,使你的应用程序在不同的屏幕尺寸和密度上看起来都很好。这样,可以提高安卓应用程序的用户体验。

设计安卓应用程序用户界面时需要考虑的事项如下:

  • 屏幕尺寸:这是设备的物理屏幕尺寸。屏幕尺寸可能从 2.5"到 10.1"不等,适用于智能手机和平板电脑。

  • 分辨率:这是设备在每个维度上的像素数量。它通常以宽度和高度的乘积来定义,例如 640 x 480。

  • 屏幕密度:这是物理区域内的最大像素数。高密度屏幕在相同区域内比低密度屏幕拥有更多的像素。

  • 屏幕方向:设备可以是横向或纵向模式。在横向模式下,其宽度会增加。

使用match_parentwrap_content

match_parentwrap_content可用于设置视图的layout_heightlayout_width属性。当使用match_parent时,Android 会扩展视图以使其大小与其父级相同。使用wrap_content时,Android 会根据内容大小扩展视图。也可以使用像素值设置宽度和高度。然而,使用像素值不是一个好的实践,因为像素数量会根据屏幕属性而变化,且给定的像素值在每个屏幕上不是相同的大小。在以下示例中,我们使用像素值来设置视图的宽度和高度。布局 XML 代码如下代码块所示:

<RelativeLayout 

    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <TextView
 android:layout_width="240px"
 android:layout_height="30px"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true"
        android:padding="@dimen/padding_medium"
        android:text="hello world1 hello world 2 hello world 3 hello"
        tools:context=".Chapter6_1Activity" />

</RelativeLayout>

注意

在前面的布局 XML 文件中使用的一些定义,如@dimen,将在本书的源代码文件中提供。

如您在代码中所见,我们将layout_width设置为240px,将layout_height设置为30px。我们将在三个具有不同屏幕属性的模拟器中执行此应用程序。模拟器属性如下:

  • 小屏幕属性:此配置适用于小屏幕。这些属性可以像以下截图所示进行配置:使用 match_parent 和 wrap_content

  • 正常屏幕属性:此配置适用于正常屏幕。这些属性可以像以下截图所示进行配置:使用 match_parent 和 wrap_content

  • 大屏幕属性:此配置适用于大屏幕。这些属性可以像以下截图所示进行配置:使用 match_parent 和 wrap_content

当此应用程序在前面的模拟器配置中执行时,屏幕将如下所示:

使用 match_parent 和 wrap_content

如截图中所示,在小屏幕上看起来是好的。然而,在正常屏幕上,文本被裁剪,不是TextView组件的所有内容都可见。在大屏幕上,则什么也看不见。这个示例说明使用像素作为宽度和高度值不是一个好的做法。

现在,我们将使用wrap_contentmatch_parent来设置高度和宽度长度。布局 XML 代码将如下所示:

<RelativeLayout 

    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <TextView
 android:layout_width="match_parent"
 android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true"
        android:padding="@dimen/padding_medium"
        android:text="hello world1 hello world 2 hello world 3 hello"
        tools:context=".Chapter6_1Activity" />

</RelativeLayout>

当使用相同的模拟器配置执行应用程序时,屏幕将如下所示:

使用 match_parent 和 wrap_content

如您在截图中所见,应用程序在每个模拟器和屏幕配置中看起来都相同,且TextView组件的所有内容都得到显示。因此,在设计用户界面时使用wrap_contentmatch_parent是最佳实践。

使用 dip 代替 px

对于前面的示例,另一个选项是使用 dip(与密度无关的像素)值替代 px 值。这样,TextView 组件在不同屏幕尺寸下看起来几乎相同。代码如下所示:

<RelativeLayout 

    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <TextView
 android:layout_width="350dip"
 android:layout_height="40dip"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true"
        android:padding="@dimen/padding_medium"
        android:text="hello world1 hello world 2 hello world 3 hello"
        tools:context=".Chapter6_1Activity" />

</RelativeLayout>

如你所见,在这个代码中,我们为宽度和高度使用了 dip 值。如果你在前一节定义的模拟器中运行这个应用,它将看起来如下所示:

使用 dip 而不是 px

提示

对于字体大小,可以使用 sp(与缩放无关的像素)单位替代 px。

避免使用 AbsoluteLayout

AbsoluteLayout 是一个已弃用的布局,它为其中的视图使用固定位置。AbsoluteLayout 在设计用户界面中不是一个好的实践,因为它在不同屏幕尺寸下看起来不会相同。我们将通过以下示例布局看到这一点:

<?xml version="1.0" encoding="utf-8"?>
<AbsoluteLayout 
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <TextView
        android:id="@+id/textView5"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
 android:layout_x="96dp"
 android:layout_y="8dp"
        android:text="Text Top"
        android:textAppearance="?android:attr/textAppearanceLarge" />

    <TextView
        android:id="@+id/textView4"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
 android:layout_x="89dp"
 android:layout_y="376dp"
        android:text="Text Bottom"
        android:textAppearance="?android:attr/textAppearanceLarge" />

</AbsoluteLayout>

如你所见,在这个 XML 代码中,AbsoluteLayout 中的视图使用了固定位置。当这个应用在前一节定义的模拟器中运行时,它将看起来如下所示:

避免使用 AbsoluteLayout

如你所见,在截图上,小屏幕底部文本是不可见的,但在其他屏幕上可见。AbsoluteLayout 在用户界面设计中不是一个好的实践。

为不同的屏幕密度提供不同的位图资源

安卓根据屏幕密度缩放位图。然而,如果只提供一个位图,图像看起来不会很好。图像可能会模糊或损坏。为不同的屏幕密度提供不同的位图资源是一个好习惯。在下面的截图中,使用了两个图像按钮。为第一个图像按钮提供了不同的位图资源,为第二个图像按钮提供了一个低密度的位图资源。如你所见,在截图中,第二个图像按钮的位图看起来模糊;然而,第一个图像按钮的位图看起来很好。

为不同的屏幕密度提供不同的位图资源

提示

如果将图像放在 drawable-nodpi 文件夹中,它们不会被缩放。

为不同的屏幕尺寸提供不同的布局

安卓会缩放布局以适应设备屏幕。然而,在某些情况下这还不够。在第五章,片段中,我们开发了一个列出书籍的应用,当点击书籍时,会显示书籍的作者。

以下是显示在小屏幕上的截图:

为不同的屏幕尺寸提供不同的布局

对于大屏幕来说,这个设计并不是一个好的选择。用户界面看起来会很糟糕。我们应该在大屏幕上高效地利用空间。我们可以为更大的屏幕使用不同的布局,将两个在小屏幕上显示的屏幕结合起来。用户界面应该看起来如下所示:

为不同的屏幕尺寸提供不同的布局

布局文件应放置在适当的文件夹中。例如,大屏幕的布局文件应放在res/layout-large文件夹中,小屏幕的应放在res/layout-small文件夹中。

除了现有的限定符外,Android 3.2 引入了新的屏幕尺寸限定符。新的限定符如下:

  • sw<N>dp:此限定符定义了最小的宽度。例如,res/layout-sw600dp/

    • 当屏幕宽度至少为N dp 时,无论屏幕方向如何,都将使用此文件夹中的布局文件。
  • w<N>dp:此限定符定义了确切可用的宽度。例如,res/layout-w600dp/

  • h<N>dp:此限定符定义了确切可用的高度。例如,res/layout-h600dp/

九宫格

九宫格特性允许使用可拉伸的位图作为资源。位图根据定义的可拉伸区域进行拉伸。可拉伸区域由 1 像素宽的黑线定义。以下是一个示例九宫格可绘制资源:

Nine-patch

图像文件应放置在带有扩展名. 9.png . 的 drawable 文件夹中。顶边和左边黑线定义了可拉伸区域,底边和右边黑线定义了适应内容的可拉伸区域。

有一个名为Draw 9-patch的工具与 Android SDK 捆绑在一起。你可以使用这个编辑器轻松创建九宫格图片。

总结

在本章中,我们学习了一些设计规范,以便支持不同的屏幕尺寸和密度。我们不应该使用硬编码的像素值来定义布局的宽度和高度。相反,我们应该使用wrap_contentmatch_parent属性,或者使用dip值代替px值。我们应该为不同的屏幕尺寸使用不同的布局,以使应用程序在所有屏幕尺寸上看起来都很好。我们还了解了使用九宫格规则创建可拉伸位图的方法。开发应用程序后,我们应在 Android 模拟器中测试各种屏幕尺寸和密度下的应用程序,以查看其外观。这样我们可以检测用户界面问题和错误。

在下一章中,我们将学习关于 Android 兼容性包的内容,并了解如何使用它。

第七章:Android 兼容性包

新的 Android API 在之前版本的 Android 中无法工作,因此引入了 Android 兼容性包,以便将新的 API 移植到旧版本的 Android 平台。本章展示了我们如何使用 Android 兼容性包。

本章涵盖的主题包括:

  • Android 兼容性包是什么以及为什么我们要使用它

  • 如何使用 Android 兼容性包

什么是 Android 兼容性包

Android 在 3.0 及其后续版本中发布了一些伟大的新 API。然而,许多用户并没有将他们的设备升级到最新的 Android 平台。Google 发布了包含对一些随 Android 3.0 及其后续版本发布的新 API 支持的 Android 兼容性包。这样,开发者就可以开发使用新 API 且能在旧版本 Android 中运行的应用程序。以下是一些包含在 Android 兼容性包中的类:

  • Fragment

  • FragmentManager

  • FragmentTransaction

  • ListFragment

  • DialogFragment

  • LoaderManager

  • Loader

  • AsyncTaskLoader

  • CursorLoader

Android 兼容性包中并不包括一些有用的 API,如动画类、操作栏和 FragmentMapActivity。

如何使用 Android 兼容性包

  1. 我们需要下载并安装 Android 兼容性包。为了下载 Android 兼容性包,请按照以下截图所示在 Eclipse 菜单中点击Android SDK 管理器按钮:如何使用 Android 兼容性包

    或者,我们可以通过 Eclipse 菜单使用窗口 | Android SDK 管理器来访问 Android SDK 管理器。在打开Android SDK 管理器窗口后,勾选Android 支持库选项,如下截图所示:

    如何使用 Android 兼容性包

  2. 然后,点击安装按钮并安装该包。现在我们准备开发一个可以使用 Android 兼容性包的 Android 项目。首先,在 Eclipse 中创建一个 Android 项目。然后,我们需要将支持库添加到我们的 Android 项目中。如果不存在,请在 Android 项目的根目录下创建一个名为libs的文件夹,如下截图所示:如何使用 Android 兼容性包

  3. 现在,找到并复制<your android sdk folder>/extras/android/support/v4/android-support-v4.jar文件到libs文件夹中。文件夹结构应该如下截图所示:如何使用 Android 兼容性包

  4. 最后,如果.jar文件不在项目的构建路径中,请按照以下截图所示将.jar文件添加到项目构建路径中:如何使用 Android 兼容性包

现在你知道了如何手动添加支持库。Eclipse 通过添加支持库的菜单选项使这个过程变得简单。使用以下步骤:

  1. 在资源管理器中右键点击项目。

  2. 转到Android Tools | 添加支持库…选项。

  3. 按照步骤完成向导。

现在我们可以使用兼容性包。我们将创建一个使用Fragment类的应用程序,但是使用的是兼容包中的Fragment类,通过以下步骤显示文本:

  1. 首先,为 Fragment 创建一个布局 XML,并将 XML 文件命名为fragment.xml

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout 
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center_horizontal"
        android:orientation="vertical" >
    
        <TextView
            android:id="@+id/textView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Hello Android Compatibility Package"
            android:textAppearance="?android:attr/textAppearanceLarge" />
    
    </LinearLayout>
    
  2. 然后,使用以下代码块为活动创建一个布局:

    <RelativeLayout 
    
        android:id="@+id/main_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
    
    </RelativeLayout>
    
  3. 现在,我们将为fragment.xml布局创建一个Fragment类:

    package com.chapter7;
    
    import android.os.Bundle;
    import android.support.v4.app.Fragment;
    import android.view.LayoutInflater;
    import android.view.View;
    import android.view.ViewGroup;
    
    public class Chapter7Fragment extends Fragment {
    
      @Override
      public View onCreateView(LayoutInflater inflater, ViewGroup 
    container,
          Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment, container, 
    false);
        return view;
      }
    }
    

如你所见,在上述代码中,Fragment类来自android.support.v4.app.Fragment包。这意味着我们正在使用 Android 兼容性包。如果我们不想使用兼容包,那么我们应该使用android.app.Fragment包中的Fragment类。

我们应用程序的Activity类如下:

package com.chapter7;

import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.view.Menu;

public class Chapter7Activity extends FragmentActivity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        addFragment();
    }

    public void addFragment() {
    FragmentManager fragmentManager = 
 this.getSupportFragmentManager();
    FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();

    Chapter7Fragment fragment = new Chapter7Fragment();
    fragmentTransaction.add(R.id.main_layout,fragment);
    fragmentTransaction.commit();
  }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }
}

如你所见,在上述代码块中,支持库 API 与标准 API 的命名相同。我们只需要使用正确的导入并调用正确的管理器。为了使用兼容包中的类,我们需要将android.support.v4.app添加到我们的导入列表中。

为了获取FragmentManager的实例,我们调用了我们Activity类的getSupportFragmentManager()方法。你可能已经注意到,Activity类扩展了FragmentActivity类。我们需要这样做,因为这是使用 Fragments 的唯一方式。

AndroidManifest.xml文件应该如下所示:

<manifest 
    package="com.chapter7"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
 android:minSdkVersion="8"
        android:targetSdkVersion="15" />

    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name=".Chapter7Activity"
            android:label="@string/title_activity_chapter7" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

如你所见,在这段代码中,最低 SDK 级别被设置为 API 级别 8。我们可以将最低 API 级别设置为 4 或更高。这样,我们就可以在旧版本的 Android 中使用新的 API。

概要

在本章中,我们了解了 Android 兼容性包是什么以及如何使用它。我们还学习到了如何借助这个库,在旧版本的 Android 中使用新的 API。

在下一章中,我们将学习使用新的连接 API——Android Beam 和 Wi-Fi Direct。

第八章:新的连接性 API - Android Beam 和 Wi-Fi Direct

随着 Android Ice Cream Sandwich 的推出,引入了新的连接性 API - Android Beam,它使用设备的 NFC 硬件,以及Wi-Fi Direct,允许设备在不使用无线接入点的情况下相互连接。本章将教我们如何使用 Android Beam 和 Wi-Fi Direct API。

本章节涵盖的主题包括:

  • Android Beam

  • 发送 NdefMessages

  • 使用 Wi-Fi Direct 共享数据

Android Beam

拥有 NFC 硬件的设备可以通过轻触在一起来共享数据。这可以通过 Android Beam 功能实现。它类似于蓝牙,因为我们能无缝地发现和配对,就像蓝牙连接一样。设备在彼此靠近时(不超过几厘米)连接。用户可以使用 Android Beam 功能共享图片、视频、联系人等。

发送 NdefMessages

在本节中,我们将实现一个简单的 Android Beam 应用程序。此应用程序将在两个设备轻触在一起时将图片发送到另一设备。随着 Android Ice Cream Sandwich 的推出,引入了三种用于发送NdefMessages的方法。这些方法如下:

  • setNdefPushMessage(): 此方法接受一个 NdefMessage 作为参数,并在设备轻触在一起时自动将其发送到另一设备。当消息是静态的且不变化时,通常使用这种方法。

  • setNdefPushMessageCallback(): 此方法用于创建动态 NdefMessages。当两个设备轻触在一起时,会调用createNdefMessage()方法。

  • setOnNdefPushCompleteCallback(): 此方法设置一个回调,当 Android Beam 成功时调用。

我们将在示例应用程序中使用第二种方法。

我们的示例应用程序的用户界面将包含一个TextView组件用于显示文本消息,以及一个ImageView组件用于显示从另一设备接收的图片。布局 XML 代码如下所示:

<RelativeLayout 
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true"
        android:text=""
         />

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/textView"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="14dp"
         />

</RelativeLayout>

现在,我们将逐步实现示例应用程序的Activity类。带有onCreate()方法的Activity类的代码如下:

public class Chapter9Activity extends Activity implements
 CreateNdefMessageCallback
  {

  NfcAdapter mNfcAdapter;
  TextView mInfoText;
  ImageView imageView;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    imageView = (ImageView) findViewById(R.id.imageView);
    mInfoText = (TextView) findViewById(R.id.textView);
    // Check for available NFC Adapter
       mNfcAdapter = NfcAdapter.getDefaultAdapter(getApplicationContext());

    if (mNfcAdapter == null) 
    {
      mInfoText = (TextView) findViewById(R.id.textView);
      mInfoText.setText("NFC is not available on this device.");
      finish();
      return;
    }
    // Register callback to set NDEF message
    mNfcAdapter.setNdefPushMessageCallback(this, this);
  }

  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
    getMenuInflater().inflate(R.menu.main, menu);
    return true;
  }
}

如您在此代码中看到的,我们可以检查设备是否提供NfcAdapter。如果提供,我们将获取NfcAdapter的实例。然后,我们使用NfcAdapter实例调用setNdefPushMessageCallback()方法来设置回调。我们将Activity类作为回调参数发送,因为Activity类实现了CreateNdefMessageCallback

为了实现CreateNdefMessageCallback,我们应该重写createNdefMessage()方法,如下面的代码块所示:

  @Override
  public NdefMessage createNdefMessage(NfcEvent arg0) {

    Bitmap icon =  BitmapFactory.decodeResource(this.getResources(),
        R.drawable.ic_launcher);
    ByteArrayOutputStream stream = new ByteArrayOutputStream();
    icon.compress(Bitmap.CompressFormat.PNG, 100, stream);
    byte[] byteArray = stream.toByteArray();

    NdefMessage msg = new NdefMessage(new NdefRecord[] {
 createMimeRecord("application/com.chapter9", byteArray)
 , NdefRecord.createApplicationRecord("com.chapter9")
});
    return msg;
  }
  public NdefRecord createMimeRecord(String mimeType, byte[] payload) {

    byte[] mimeBytes = mimeType.getBytes(Charset.forName("US-ASCII"));
    NdefRecord mimeRecord = new NdefRecord(NdefRecord.TNF_MIME_MEDIA,
        mimeBytes, new byte[0], payload);
    return mimeRecord;
  }

如您在代码中所见,我们获取了一个可绘制对象,将其转换为位图,然后再转换为一个字节数组。然后我们使用两个NdefRecords创建了一个NdefMessage。第一个记录包含 mime 类型和字节数组。第一个记录是通过createMimeRecord()方法创建的。第二个记录包含Android 应用记录AAR)。Android 应用记录是在 Android Ice Cream Sandwich 中引入的。这个记录包含了应用的包名,增加了当扫描NFC 标签时应用启动的确定性。也就是说,系统首先尝试将意图过滤器和 AAR 一起匹配以启动活动。如果它们不匹配,则启动与 AAR 匹配的活动。

当活动由 Android Beam 事件启动时,我们需要处理由 Android Beam 发送的消息。我们在Activity类的onResume()方法中处理此消息,如下面的代码块所示:

  @Override
  public void onResume() {
    super.onResume();
    // Check to see that the Activity started due to an Android Beam
    if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(getIntent().getAction())) {
      processIntent(getIntent());
    }
  }

  @Override
  public void onNewIntent(Intent intent) {
    // onResume gets called after this to handle the intent
    setIntent(intent);
  }

  void processIntent(Intent intent) {

    Parcelable[] rawMsgs = intent

  .getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES);
    // only one message sent during the beam
 NdefMessage msg = (NdefMessage) rawMsgs[0];
 // record 0 contains the MIME type, record 1 is the AAR
 byte[] bytes = msg.getRecords()[0].getPayload();
    Bitmap bmp = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);

    imageView.setImageBitmap(bmp);
  }

如您在代码中所见,我们首先检查意图是否为ACTION_NDEF_DISCOVERED。这意味着Activity类是由于 Android Beam 而启动的。如果是因为 Android Beam 而启动,我们使用processIntent()方法处理意图。我们首先从意图中获取NdefMessage。然后我们从第一个记录中获取字节数组并将其转换为位图,使用BitmapFactory。记住,第二个记录是 AAR,我们不对其进行任何操作。最后,我们设置了ImageView组件的位图。

应用程序的AndroidManifest.xml文件应如下所示:

<manifest 
    package="com.chapter9"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-permission android:name="android.permission.NFC"/>
 <uses-feature android:name="android.hardware.nfc" android:required="false" />

    <uses-sdk
        android:minSdkVersion="14"
        android:targetSdkVersion="15" />

    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name=".Chapter9Activity"
            android:label="@string/title_activity_chapter9" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
 <intent-filter>
 <action android:name="android.nfc.action.NDEF_DISCOVERED" />
 <category android:name="android.intent.category.DEFAULT" />
 <data android:mimeType="application/com.chapter9" />
 </intent-filter>
        </activity>
    </application>

</manifest>

如您在代码中所见,我们需要在AndroidManifest.xml文件中将最低 SDK 设置为 API 级别 14 或更高,因为这些 API 在 API 级别 14 或更高版本中可用。此外,我们需要设置使用 NFC 的权限。我们还设置了AndroidManifest.xml中的uses特性。该特性设置为非必需。这意味着我们的应用可以在没有 NFC 支持的设备上使用。最后,我们为android.nfc.action.NDEF_DISCOVERED创建了一个意图过滤器,其mimeTypeapplication/com.chapter9

当一个设备使用我们的示例应用发送图像时,屏幕将如下所示:

发送 NdefMessages

Wi-Fi Direct

在传统的无线网络中,设备通过无线接入点相互连接。借助Wi-Fi Direct,设备无需无线接入点即可相互连接。它类似于蓝牙,但速度更快,Wi-Fi Direct 的范围也更广。随着 Android Ice Cream Sandwich 引入了新的 Wi-Fi Direct API,我们可以使用 Android 设备的 Wi-Fi Direct 属性。

主要帮助我们查找和连接对等设备的类是WifiP2pManager类。在查找和连接对等设备的过程中,我们将使用以下Listener类:

  • WifiP2pManager.ActionListener

  • WifiP2pManager.ChannelListener

  • WifiP2pManager.ConnectionInfoListener

  • WifiP2pManager.PeerListListener

最后,以下意图将有助于我们在 Wi-Fi Direct 连接中:

  • WIFI_P2P_CONNECTION_CHANGED_ACTION

  • WIFI_P2P_PEERS_CHANGED_ACTION

  • WIFI_P2P_STATE_CHANGED_ACTION

  • WIFI_P2P_THIS_DEVICE_CHANGED_ACTION

在这一节中,我们将学习如何通过一个示例应用程序来使用这些新的 Wi-Fi Direct API。

Wi-Fi Direct 应用示例

为了使用 Wi-Fi Direct API,我们需要在AndroidManifest.xml中将最小 SDK 版本设置为 API Level 14 或更高。此外,我们需要一些权限来使用 Wi-Fi Direct API。AndroidManifest.xml文件应如下所示:

<manifest 
    package="com.chapter9"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="14"
        android:targetSdkVersion="15" />
 <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
 <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
 <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
 <uses-permission android:name="android.permission.INTERNET" />
 <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name=".Chapter9Activity"
            android:label="@string/title_activity_chapter9" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

我们需要的第一种类是扩展了BroadcastReceiver并处理我们之前在onReceive()方法中列出的意图的类。这个类的构造函数如下所示:

package com.chapter9;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.net.NetworkInfo;
import android.net.wifi.p2p.WifiP2pManager;
import android.net.wifi.p2p.WifiP2pManager.Channel;
import android.net.wifi.p2p.WifiP2pManager.PeerListListener;
import android.widget.Toast;

public class Chapter9WiFiDirectBroadcastReceiver extends BroadcastReceiver {

 private WifiP2pManager manager;
 private Channel channel;
 private Chapter9Activity activity;

    public Chapter9WiFiDirectBroadcastReceiver(WifiP2pManager manager, Channel 
channel,
        Chapter9Activity activity) {
        super();
        this.manager = manager;
        this.channel = channel;
        this.activity = activity;
    }
}

如您在此代码中看到的,我们将ChannelWifiP2pManagerActivity类作为参数传递给构造函数,因为我们在后面的onReceive()方法中需要它们。我们需要重写BroadcastReceiveronReceive()方法,如下代码块所示:

@Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        if (WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION.equals(action)) {

            int state = intent.getIntExtra(WifiP2pManager.EXTRA_WIFI_STATE, -1);

            if (state == WifiP2pManager.WIFI_P2P_STATE_ENABLED) {
                // Wifi Direct mode is enabled
              Toast.makeText(activity, "wifi direct is enabled",Toast.LENGTH_LONG).show();
            } else {
              // Wifi Direct mode is disabled
              Toast.makeText(activity, "wifi direct is disabled",Toast.LENGTH_LONG).show();
            }

        } else if (WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION.equals(action)) 
        {

            // request peers from the wifi p2p manager
            if (manager != null) {
                manager.requestPeers(channel, (PeerListListener) activity);
            }

        } else if (WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION.equals(action)) {

            if (manager == null) {
                return;
            }

            NetworkInfo networkInfo = (NetworkInfo) intent
                    .getParcelableExtra(WifiP2pManager.EXTRA_NETWORK_INFO);

            if (networkInfo.isConnected()) {

                // request connection info
                manager.requestConnectionInfo(channel, activity);
            } else {
                // It's a disconnect

            }
        } else if (WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION.equals(action)) {

        }
    }

在这个方法中,我们处理收到的意图。首先,我们检查意图是否为WIFI_P2P_STATE_CHANGED_ACTION。当 Wi-Fi Direct 被启用或禁用时,我们会收到这个意图。我们从意图中获取 Wi-Fi Direct 的状态,并根据 Wi-Fi Direct 的状态采取行动。

其次,我们检查意图是否为WIFI_P2P_PEERS_CHANGED_ACTION。当调用WifiP2pManager类的discoverPeers()方法时,我们会收到这个意图。在收到WIFI_P2P_PEERS_CHANGED_ACTION意图时,我们从Wifi2P2pManager类的requestPeers()方法获取对等体列表。

接下来,我们检查收到的意图是否为WIFI_P2P_CONNECTION_CHANGED_ACTION。当 Wi-Fi 连接发生变化时,我们会收到这个意图。在收到WIFI_P2P_CONNECTION_CHANGED_ACTION意图时,我们处理连接或断开的情况。首先,我们从意图中获取NetworkInfo来判断是否存在连接或断开。如果是连接,我们会调用WifiP2pManagerrequestConnectionInfo()方法进行连接。

最后,我们检查意图是否为WIFI_P2P_THIS_DEVICE_CHANGED_ACTION。当设备详情发生变化时,我们会收到这个意图。对于这个意图,我们不执行任何操作。

我们为这个应用程序提供了一个简单的用户界面;一个带有两个按钮的布局。第一个按钮用于查找,第二个按钮用于连接对等体。布局的 XML 代码如下:

<LinearLayout 

    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <Button
        android:id="@+id/buttonFind"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="find" />

    <Button
        android:id="@+id/buttonConnect"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="connect" />

</LinearLayout>

用户界面将如下截图所示:

Wi-Fi Direct 应用示例

最后,我们需要实现这个应用程序的Activity类。Activity类的代码应如下所示:

package com.chapter9;

import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.IntentFilter;
import android.net.wifi.p2p.WifiP2pConfig;
import android.net.wifi.p2p.WifiP2pDevice;
import android.net.wifi.p2p.WifiP2pDeviceList;
import android.net.wifi.p2p.WifiP2pInfo;
import android.net.wifi.p2p.WifiP2pManager;
import android.net.wifi.p2p.WifiP2pManager.ActionListener;
import android.net.wifi.p2p.WifiP2pManager.Channel;
import android.net.wifi.p2p.WifiP2pManager.ChannelListener;
import android.net.wifi.p2p.WifiP2pManager.ConnectionInfoListener;
import android.net.wifi.p2p.WifiP2pManager.PeerListListener;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.Toast;
public class Chapter9Activity extends Activity implements 
ChannelListener,OnClickListener,PeerListListener,ConnectionInfoListener {

    private WifiP2pManager manager;
    private final IntentFilter intentFilter = new IntentFilter();
    private Channel channel;
    private BroadcastReceiver receiver = null;
    private Button buttonFind;
    private Button buttonConnect;
    private WifiP2pDevice device;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        manager = (WifiP2pManager) getSystemService(Context.WIFI_P2P_SERVICE);
 channel = manager.initialize(this, getMainLooper(), null);

 intentFilter.addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION);
 intentFilter.addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION);
 intentFilter.addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION);
 intentFilter.addAction(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION);

        receiver = new Chapter9WiFiDirectBroadcastReceiver(manager, channel, this);
        registerReceiver(receiver, intentFilter);

        this.buttonConnect = (Button) this.findViewById(R.id.buttonConnect);
        this.buttonConnect.setOnClickListener(this);

        this.buttonFind = (Button)this.findViewById(R.id.buttonFind);
        this.buttonFind.setOnClickListener(this);
    }
}

目前的实现还不完整。我们将逐步添加必要的方法。

如您在此代码中所见,我们的Activity类实现了各种Listeners来处理 Wi-Fi Direct 事件。ConnectionInfoListener用于当连接信息可用时的回调。PeerListListener用于当对等设备列表可用时的回调。ChannelListener用于当通道丢失时的回调。

我们创建一个意图过滤器,并在继承BroadcastReceiver的类的onReceive()方法中添加我们将检查的意图。

我们通过调用initialize()方法来初始化WifiP2pManager类。这将使我们的应用程序与 Wi-Fi 网络注册。

  1. 因为我们实现了ChannelListener,所以我们需要重写onChannelDisconnected()方法,如下代码块所示:

    @Override
      public void onChannelDisconnected() {
        //handle the channel lost event
      }
    
  2. 因为我们实现了PeerListListener,所以我们需要实现onPeersAvailable()方法,如下代码块所示:

    @Override
      public void onPeersAvailable(WifiP2pDeviceList peerList) {
    
        for (WifiP2pDevice device : peerList.getDeviceList()) {
          this.device = device;
          break;
        }
      }
    

    在此方法中,我们获取可用的peerList。我们获取第一个设备并跳出for循环。我们需要这个设备来进行连接。

  3. 因为我们实现了ConnectionInfoListener,所以我们需要实现onConnectionInfoAvailable()方法,如下代码块所示:

      @Override
      public void onConnectionInfoAvailable(WifiP2pInfo info) {
        String infoname = info.groupOwnerAddress.toString();
    
      }
    

    在这里,我们获取连接信息,并与对等设备连接并发送数据。例如,可以在这里执行一个传输文件的AsyncTask

  4. 我们需要为按钮实现onClick()方法:

      @Override
      public void onClick(View v) {
        if(v == buttonConnect)
        {
          connect(this.device);
        }
        else if(v == buttonFind)
        {
          find();
        }
    
      }
    

find()connect()方法如下所示:

public void connect(WifiP2pDevice device)
  {
    WifiP2pConfig config = new WifiP2pConfig();
    if(device != null)
    {
      config.deviceAddress = device.deviceAddress;
      manager.connect(channel, config, new ActionListener() {

          @Override
          public void onSuccess() {

            //success
          }

          @Override
          public void onFailure(int reason) {
            //fail
          }
      });
  }
    else
    {
      Toast.makeText(Chapter9Activity.this, "Couldn't connect, device is not found",

                Toast.LENGTH_SHORT).show();
    }
  }  
       public void find()
  {
    manager.discoverPeers(channel, new WifiP2pManager.ActionListener() 
       {

            @Override
            public void onSuccess() {
                Toast.makeText(Chapter9Activity.this, "Finding Peers",
                        Toast.LENGTH_SHORT).show();
       }

            @Override
            public void onFailure(int reasonCode) 
           {
                Toast.makeText(Chapter9Activity.this, "Couldnt find peers ",
                        Toast.LENGTH_SHORT).show();
            }
        });
  }

当点击查找按钮时,我们会调用WifiP2pManagerdiscoverPeers()方法来发现可用的对等设备。如您所记得,调用discoverPeers()方法将导致BroadcastReceiver接收到WIFI_P2P_PEERS_CHANGED_ACTION意图。然后在BroadcastReceiver中我们会请求对等设备列表。

当点击连接按钮时,我们使用设备信息调用WifiP2pManagerconnect()方法。这将开始与指定设备的点对点连接。

通过这些方法,介绍 Wi-Fi Direct API 的示例应用程序就完成了。

总结

在本章中,我们首先学习了 Android 的 Android Beam 功能。通过此功能,设备可以使用 NFC 硬件发送数据。我们实现了一个示例 Android Beam 应用程序,并学习了如何使用 Android Beam API。其次,我们了解了 Wi-Fi Direct 是什么以及如何使用 Wi-Fi Direct API。

posted @ 2024-05-23 11:09  绝不原创的飞龙  阅读(4)  评论(0编辑  收藏  举报