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

199 阅读39分钟

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

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

译者:飞龙

协议:CC BY-NC-SA 4.0

第五章:碎片

尽管碎片是在 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。