Xamarin.Android 日历

日历 API

Android 4 中引入的新日历 API 集支持旨在读取或写入日历提供程序的数据的应用程序。 这些 API 支持大量与日历数据的交互选项,包括读取和写入事件、与会者和提醒的功能。 通过使用应用程序中的日历提供程序,通过 API 添加的数据将显示在 Android 4 附带的内置日历应用中。

添加权限

在应用程序中使用新的日历 API 时,首先需要向 Android 清单添加适当的权限。 添加的权限为 android.permisson.READ_CALENDARandroid.permission.WRITE_CALENDAR,具体取决于是读取和/还是写入日历数据。

使用日历协定

设置权限后,可以使用 CalendarContract 类与日历数据进行交互。 此类提供一个数据模型,应用程序在与日历提供程序交互时可以使用这些数据模型。 CalendarContract 允许应用程序将 URI 解析为日历实体,例如日历和事件。 它还提供了一种方法来与每个实体中的各种字段进行交互,例如日历的名称和 ID,或事件的开始日期和结束日期。

让我们看一个使用日历 API 的示例。 在此示例中,我们将检查如何枚举日历及其事件,以及如何向日历添加新事件。

列出日历

首先,让我们来看看如何枚举已在日历应用中注册的日历。 为此,我们可以实例化 CursorLoader。 在 Android 3.0 (API 11) 中引入,CursorLoader 是使用 ContentProvider 的首选方法。 至少需要为日历和要返回的列指定内容 URI;此列规范称为投影

通过调用 CursorLoader.LoadInBackground 方法,我们可以查询内容提供程序以获取数据,例如日历提供程序。 LoadInBackground 执行实际加载操作,并返回包含查询结果的 Cursor

CalendarContract 帮助我们指定内容 Uri 和投影。 若要获取用于查询日历的内容 Uri,我们只需使用以下 CalendarContract.Calendars.ContentUri 属性:

var calendarsUri = CalendarContract.Calendars.ContentUri;

使用 CalendarContract 指定所需的日历列同样简单。 只需将 CalendarContract.Calendars.InterfaceConsts 类中的字段添加到数组中。 例如,以下代码包括日历的 ID、显示名称和帐户名称:

string[] calendarsProjection = {
    CalendarContract.Calendars.InterfaceConsts.Id,
    CalendarContract.Calendars.InterfaceConsts.CalendarDisplayName,
    CalendarContract.Calendars.InterfaceConsts.AccountName
};

如果使用 SimpleCursorAdapter 将数据绑定到 UI,则 Id 很重要,我们很快就会看到。 在内容 URI 和投影到位后,我们实例化 CursorLoader 并调用 CursorLoader.LoadInBackground 方法以返回带有日历数据的游标,如下所示:

var loader = new CursorLoader(this, calendarsUri, calendarsProjection, null, null, null);
var cursor = (ICursor)loader.LoadInBackground();

此示例的 UI 包含一个 ListView,列表中每个项目都表示单个日历。 以下 XML 显示包含 ListView 的标记:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
  <ListView
    android:id="@android:id/android:list"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content" />
</LinearLayout>

此外,我们需要为每个列表项指定 UI,我们将其放置在单独的 XML 文件中,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
  <TextView android:id="@+id/calDisplayName"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:textSize="16dip" />
  <TextView android:id="@+id/calAccountName"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:textSize="12dip" />
</LinearLayout>

从这一点开始,将光标中的数据绑定到 UI 只是正常的 Android 代码。 我们将按如下所示使用 SimpleCursorAdapter

string[] sourceColumns = {
    CalendarContract.Calendars.InterfaceConsts.CalendarDisplayName,
    CalendarContract.Calendars.InterfaceConsts.AccountName };

int[] targetResources = {
    Resource.Id.calDisplayName, Resource.Id.calAccountName };      

SimpleCursorAdapter adapter = new SimpleCursorAdapter (this,
    Resource.Layout.CalListItem, cursor, sourceColumns, targetResources);

ListAdapter = adapter;

在上面的代码中,适配器接受 sourceColumns 数组中指定的列,并将其写入游标中每个日历项的 targetResources 数组中的用户界面元素。 此处使用的活动是 ListActivity 的子类;它包括我们为其设置适配器的 ListAdapter 属性。

下面是显示最终结果的屏幕截图,其中日历信息显示在 ListView 中:

CalendarDemo running in emulator, displaying two calendar entries

列出日历事件

接下来,让我们看看如何枚举给定日历的事件。 基于上面的示例,当用户选择其中一个日历时,我们将提供事件列表。 因此,我们需要在前面的代码中处理项选择:

ListView.ItemClick += (sender, e) => {
    int i = (e as ItemEventArgs).Position;

    cursor.MoveToPosition(i);
    int calId =
        cursor.GetInt (cursor.GetColumnIndex (calendarsProjection [0]));

    var showEvents = new Intent(this, typeof(EventListActivity));
    showEvents.PutExtra("calId", calId);
    StartActivity(showEvents);
};

在此代码中,我们将创建一个意向以打开 EventListActivity 类型的活动,并在意向中传递日历的 ID。 我们需要 ID 才能知道要查询事件的日历。 在 EventListActivityOnCreate 方法中,我们可以从 Intent 检索 ID,如下所示:

_calId = Intent.GetIntExtra ("calId", -1);

现在,让我们查询此日历 ID 的事件。 查询事件的过程类似于我们之前查询日历列表的方式,但这次我们将使用 CalendarContract.Events 类。 以下代码创建用于检索事件的查询:

var eventsUri = CalendarContract.Events.ContentUri;

string[] eventsProjection = {
    CalendarContract.Events.InterfaceConsts.Id,
    CalendarContract.Events.InterfaceConsts.Title,
    CalendarContract.Events.InterfaceConsts.Dtstart
};

var loader = new CursorLoader(this, eventsUri, eventsProjection,
                   String.Format ("calendar_id={0}", _calId), null, "dtstart ASC");
var cursor = (ICursor)loader.LoadInBackground();

在此代码中,我们首先从 CalendarContract.Events.ContentUri 属性获取事件的内容 Uri。 然后,指定要在 eventsProjection 数组中检索的事件列。 最后,我们使用此信息实例化 CursorLoader,并调用加载程序 LoadInBackground 方法以返回事件数据的 Cursor

若要在 UI 中显示事件数据,可以使用标记和代码,就像之前一样显示日历列表。 同样,我们使用 SimpleCursorAdapter 将数据绑定到 ListView,如以下代码所示:

string[] sourceColumns = {
    CalendarContract.Events.InterfaceConsts.Title,
    CalendarContract.Events.InterfaceConsts.Dtstart };

int[] targetResources = {
    Resource.Id.eventTitle,
    Resource.Id.eventStartDate };

var adapter = new SimpleCursorAdapter (this, Resource.Layout.EventListItem,
    cursor, sourceColumns, targetResources);

adapter.ViewBinder = new ViewBinder ();       
ListAdapter = adapter;

此代码和前面用于显示日历列表的代码之间的主要区别是使用 ViewBinder,该代码在行中设置:

adapter.ViewBinder = new ViewBinder ();

ViewBinder 类允许我们进一步控制如何将值绑定到视图。 在这种情况下,我们使用它将事件开始时间从毫秒转换为日期字符串,如以下实现中所示:

class ViewBinder : Java.Lang.Object, SimpleCursorAdapter.IViewBinder
{    
    public bool SetViewValue (View view, Android.Database.ICursor cursor,
        int columnIndex)
    {
        if (columnIndex == 2) {
            long ms = cursor.GetLong (columnIndex);

            DateTime date = new DateTime (1970, 1, 1, 0, 0, 0,
                DateTimeKind.Utc).AddMilliseconds (ms).ToLocalTime ();

            TextView textView = (TextView)view;
            textView.Text = date.ToLongDateString ();

            return true;
        }
        return false;
    }    
}

这会显示事件列表,如下所示:

Screenshot of example app displaying three calendar events

添加日历事件

我们已了解如何读取日历数据。 现在,让我们了解如何向日历添加事件。 为此,请务必包括前面提到的 android.permission.WRITE_CALENDAR 权限。 若要向日历添加事件,我们将:

  1. 创建一个 ContentValues 实例。
  2. 使用 CalendarContract.Events.InterfaceConsts 类中的键填充 ContentValues 实例。
  3. 设置事件开始和结束时间的时区。
  4. 使用 ContentResolver 将事件数据插入日历中。

下面的代码演示了以下步骤:

ContentValues eventValues = new ContentValues ();

eventValues.Put (CalendarContract.Events.InterfaceConsts.CalendarId,
    _calId);
eventValues.Put (CalendarContract.Events.InterfaceConsts.Title,
    "Test Event from M4A");
eventValues.Put (CalendarContract.Events.InterfaceConsts.Description,
    "This is an event created from Xamarin.Android");
eventValues.Put (CalendarContract.Events.InterfaceConsts.Dtstart,
    GetDateTimeMS (2011, 12, 15, 10, 0));
eventValues.Put (CalendarContract.Events.InterfaceConsts.Dtend,
    GetDateTimeMS (2011, 12, 15, 11, 0));

eventValues.Put(CalendarContract.Events.InterfaceConsts.EventTimezone,
    "UTC");
eventValues.Put(CalendarContract.Events.InterfaceConsts.EventEndTimezone,
    "UTC");

var uri = ContentResolver.Insert (CalendarContract.Events.ContentUri,
    eventValues);

请注意,如果未设置时区,则会引发 Java.Lang.IllegalArgumentException 类型的异常。 由于事件时间值必须以毫秒为单位表示,因为自纪元以来,我们将创建一个 GetDateTimeMS 方法 (EventListActivity),以将日期规范转换为毫秒格式:

long GetDateTimeMS (int yr, int month, int day, int hr, int min)
{
    Calendar c = Calendar.GetInstance (Java.Util.TimeZone.Default);

    c.Set (Java.Util.CalendarField.DayOfMonth, 15);
    c.Set (Java.Util.CalendarField.HourOfDay, hr);
    c.Set (Java.Util.CalendarField.Minute, min);
    c.Set (Java.Util.CalendarField.Month, Calendar.December);
    c.Set (Java.Util.CalendarField.Year, 2011);

    return c.TimeInMillis;
}

如果将按钮添加到事件列表 UI 并在按钮的单击事件处理程序中运行上述代码,该事件将添加到日历并更新在我们的列表中,如下所示:

Screenshot of example app with calendar events followed by Add Sample Event button

如果打开日历应用,我们会看到该事件也写入其中:

Screenshot of calendar app displaying the selected calendar event

正如你所看到的,Android 允许强大且易于访问来检索和保存日历数据,从而允许应用程序无缝集成日历功能。