時間小部件在很多android智能機上都很常見,本篇主要通過介紹時間部件的編寫流程,介紹在待機上顯示小部件的方法,先來張圖。
在不瞭解Launcher上如何顯示窗口小部件之前,我們暫時分步驟完成小部件的編寫,完成後再找機會做深入研究。
第一步、配置AndroidManifest.xml。
我們需要爲小部件編寫一個TimeWidgetProvider繼承自AppWidgetProvider,從AppWidgetProvider的父類BroadcastReceiver看出,該類實際上是一個用來接受widget廣播消息的類。那麼在AndroidManifest.xml中需要一個receiver標籤,如下:
<receiver android:name="com.blackhill.mytimewidget.TimeWidgetProvider">
<meta-data android:name="android.appwidget.provider"
android:resource="@xml/time_widget_provider"/>
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="android.appwidget.action.APPWIDGET_DISABLED"/>
</intent-filter>
</receiver>
其中meta-data標籤中android:resource="@xml/time_widget_provider",指定了我們要完成的小部件的屬性,我們需要在工程的res下創建一個xml文件夾,並在其中創建一個time_widget_provider.xml文件,類似如下:
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="300dp"
android:minHeight="200dp"
android:updatePeriodMillis="0"
android:initialLayout="@layout/activity_time_widget"
>
</appwidget-provider>
以上各項屬性逐一介紹如下:
minWidth:widget所佔的屏幕寬度
minHeight:widget所佔的屏幕高度
updatePeriodMillis:更新時間頻率。據說從android 1.6以後,默認更新時間爲30分鐘。所以我們需要另外增加一個service來定時刷新。
initialLayout:UI佈局
第二步、創建widget的UI佈局
在layout中增加布局文件activity_time_widget.xml,類似如下:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".TimeWidgetActivity" >
<ImageView android:id="@+id/id_bg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/bg"
android:contentDescription="@string/descrip_bg"/>
<RelativeLayout android:id="@+id/id_time_and_date"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<RelativeLayout
android:id="@+id/id_time_area"
android:layout_width="fill_parent"
android:layout_height="116dp"
android:layout_marginLeft="0dp"
android:layout_marginTop="0dp" >
<RelativeLayout android:id="@+id/id_hour_area"
android:layout_width="118dp"
android:layout_height="92dp"
android:layout_marginTop="18dp"
android:layout_marginLeft="26dp">
<ImageView android:id="@+id/id_hour_num_one"
android:src="@drawable/num_0"
android:contentDescription="@string/descrip_digit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginLeft="20dp"
/>
<ImageView android:id="@+id/id_hour_num_two"
android:src="@drawable/num_0"
android:contentDescription="@string/descrip_digit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/id_hour_num_one"
android:layout_centerVertical="true"
/>
</RelativeLayout>
<RelativeLayout android:id="@+id/id_minute_area"
android:layout_width="118dp"
android:layout_height="92dp"
android:layout_marginTop="18dp"
android:layout_toRightOf="@id/id_hour_area">
<ImageView android:id="@+id/id_minute_num_1"
android:src="@drawable/num_0"
android:contentDescription="@string/descrip_digit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginLeft="20dp"
/>
<ImageView android:id="@+id/id_minute_num_2"
android:src="@drawable/num_0"
android:contentDescription="@string/descrip_digit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/id_minute_num_1"
android:layout_centerVertical="true"
/>
</RelativeLayout>
</RelativeLayout>
<RelativeLayout android:id="@+id/id_date_area"
android:layout_below="@id/id_time_area"
android:layout_width="fill_parent"
android:layout_height="44dp">
<RelativeLayout android:id="@+id/id_date"
android:layout_width="150dp"
android:layout_height="fill_parent">
<TextView android:id="@+id/id_date_text"
android:text="@string/str_date_text"
android:layout_centerVertical="true"
android:layout_centerHorizontal="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</RelativeLayout>
<RelativeLayout android:id="@+id/id_lunar"
android:layout_width="150dp"
android:layout_height="fill_parent"
android:layout_toRightOf="@id/id_date">
<TextView android:id="@+id/id_lunar_text"
android:text="@string/str_lunar_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true" />
</RelativeLayout>
</RelativeLayout>
</RelativeLayout>
</FrameLayout>
以上佈局內容看起來比較複雜,建議將部分內容分開寫,用include標籤加進來會好看很多。那到這步,就能看到初步效果了。接下來,我們需要編寫AndroidManifest.xml中的TimeWidgetProvider了。
第三步、編寫XXXWidgetProvider
package com.blackhill.mytimewidget.ui;
import java.util.ArrayList;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.text.format.DateFormat;
import android.util.Log;
import android.widget.RemoteViews;
import com.blackhill.mytimewidget.R;
import com.blackhill.mytimewidget.lunar.DateAndTimeModel;
public class TimeWidgetProvider extends AppWidgetProvider {
private final String TAG = "TimeWidgetProvider";
private final String ACTION_UPDATE_WIDGET = "com.blackhill.mytimewidget.UPDATE_WIDGET";
private static final int[] timePic = {R.drawable.num_0, R.drawable.num_1, R.drawable.num_2, R.drawable.num_3, R.drawable.num_4,
R.drawable.num_5, R.drawable.num_6, R.drawable.num_7, R.drawable.num_8, R.drawable.num_9};
private static final int[] weekdayString = {R.string.Sunday, R.string.Monday, R.string.Tuesday, R.string.Wednesday,
R.string.Thursday, R.string.Friday, R.string.Saturday};
private static ArrayList<Integer> widgetIds = new ArrayList<Integer>();
private Intent mUpdateService = null;
@Override
public void onDeleted(Context context, int[] appWidgetIds) {
// TODO Auto-generated method stub
Log.d(TAG, "onDeleted");
releaseAppWidgetIds(appWidgetIds);
super.onDeleted(context, appWidgetIds);
}
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager,
int[] appWidgetIds) {
// TODO Auto-generated method stub
Log.d(TAG, "onUpdate");
addAppWidgetIds(appWidgetIds);
updateDateAndTimeWidget(context, appWidgetManager, getAppWidgetIds(widgetIds));
// start service for updating widget per minute.
startService(context);
super.onUpdate(context, appWidgetManager, appWidgetIds);
}
@Override
public void onDisabled(Context context) {
// TODO Auto-generated method stub
stopService(context);
super.onDisabled(context);
}
@Override
public void onEnabled(Context context) {
// TODO Auto-generated method stub
startService(context);
super.onEnabled(context);
}
static ComponentName getComponentName(Context context) {
return new ComponentName(context, TimeWidgetProvider.class);
}
public void startService(Context context) {
Log.d(TAG, "startService");
mUpdateService = new Intent(context, TimeWidgetService.class);
context.startService(mUpdateService);
}
public void stopService(Context context) {
Log.d(TAG, "stop service");
if (mUpdateService != null) {
context.stopService(mUpdateService);
}
}
@Override
public void onReceive(Context context, Intent intent) {
// TODO Auto-generated method stub
Log.d(TAG, "onReceive");
final String action = intent.getAction();
Log.d(TAG, "onReceive: action = " + action);
if (action.equals("com.blackhill.mytimewidget.UPDATE_WIDGET")) {
Log.d(TAG, "onReceive: updateDateAndTimeWidget!!!");
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
updateDateAndTimeWidget(context, appWidgetManager, appWidgetManager.getAppWidgetIds(getComponentName(context)));
}else {
Log.d(TAG, "onReceive: super.onReceive");
super.onReceive(context, intent);
}
}
void updateDateAndTimeWidget(Context context, AppWidgetManager appWidgetManager,
int[] appWidgetIds) {
boolean b24HourFormat = DateFormat.is24HourFormat(context);
DateAndTimeModel mDateTime = new DateAndTimeModel();
int hour = mDateTime.getCurrentHour(b24HourFormat);
int minute = mDateTime.getCurrentMin();
// String format = context.getString(R.string.lunar_date_formatter);
// SimpleDateFormat timeFormat = new SimpleDateFormat(format);
// String date = timeFormat.format(mDateTime.getCurrentDate());
String date = mDateTime.getCurrentDate(context);
int weekdayId = getWeekdayStringId(mDateTime.getCurrentWeekDay());
String weekday = context.getString(weekdayId);
Log.d(TAG, "updateDateAndTimeWidget:length = " + appWidgetIds.length);
Log.d(TAG, "updateDateAndTimeWidget:appWidgetIds = " + appWidgetIds.toString());
Log.d(TAG, "updateDateAndTimeWidget:date = " + date);
Log.d(TAG, "updateDateAndTimeWidget:weekday = " + weekday);
for (int appWidgetId: appWidgetIds) {
Log.d(TAG, "updateDateAndTimeWidget:appWidgetId = " + appWidgetId);
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.activity_time_widget);
views.setImageViewResource(R.id.id_hour_num_one, getTimeStringId(hour/10));
views.setImageViewResource(R.id.id_hour_num_two, getTimeStringId(hour%10));
views.setImageViewResource(R.id.id_minute_num_1, getTimeStringId(minute/10));
views.setImageViewResource(R.id.id_minute_num_2, getTimeStringId(minute%10));
views.setTextViewText(R.id.id_date_text, date);
views.setTextViewText(R.id.id_lunar_text, weekday);
appWidgetManager.updateAppWidget(appWidgetId, views);
}
}
int getTimeStringId(int num) {
if (num < timePic.length) {
return timePic[num];
}else {
return timePic[0];
}
}
int getWeekdayStringId(int weekday) {
if (weekday < weekdayString.length) {
return weekdayString[weekday - 1];
}else {
return weekdayString[0];
}
}
public void addAppWidgetIds(int[] appWidgetIds) {
for (int widgetId: appWidgetIds) {
if (!widgetIds.contains(widgetId)) {
widgetIds.add(widgetId);
}
}
}
public void releaseAppWidgetIds(int[] appWidgetIds) {
for (int widgetId:appWidgetIds) {
if (!widgetIds.contains(widgetId)) {
widgetIds.remove(widgetId);
}
}
}
public int[] getAppWidgetIds(ArrayList<Integer> widgetIds) {
int[] appWidgets = new int[widgetIds.size()];
try {
for (int i = 0; i < widgetIds.size(); i++) {
appWidgets[i] = widgetIds.get(i);
}
}catch (Exception excep) {
}
return appWidgets;
}
}
其中必須要重載onReceive函數以及onUpdate函數,如果有資源需要釋放,需要再重載onDeleted, onDisable等。TimeWidgetProvider繼承自AppWidgetProvider,從AppWidgetProvider的源碼可以看出,onEnable, onDisable, onDeleted, onUpdate都是在AppWidgetProvider的onReceive中調用的,如:
public void onReceive(Context context, Intent intent) {
// Protect against rogue update broadcasts (not really a security issue,
// just filter bad broacasts out so subclasses are less likely to crash).
String action = intent.getAction();
if (AppWidgetManager.ACTION_APPWIDGET_UPDATE.equals(action)) {
Bundle extras = intent.getExtras();
if (extras != null) {
int[] appWidgetIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_IDS);
if (appWidgetIds != null && appWidgetIds.length > 0) {
this.onUpdate(context, AppWidgetManager.getInstance(context), appWidgetIds);
}
}
}
else if (AppWidgetManager.ACTION_APPWIDGET_DELETED.equals(action)) {
Bundle extras = intent.getExtras();
if (extras != null && extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_ID)) {
final int appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID);
this.onDeleted(context, new int[] { appWidgetId });
}
}
else if (AppWidgetManager.ACTION_APPWIDGET_OPTIONS_CHANGED.equals(action)) {
Bundle extras = intent.getExtras();
if (extras != null && extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_ID)
&& extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS)) {
int appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID);
Bundle widgetExtras = extras.getBundle(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS);
this.onAppWidgetOptionsChanged(context, AppWidgetManager.getInstance(context),
appWidgetId, widgetExtras);
}
}
else if (AppWidgetManager.ACTION_APPWIDGET_ENABLED.equals(action)) {
this.onEnabled(context);
}
else if (AppWidgetManager.ACTION_APPWIDGET_DISABLED.equals(action)) {
this.onDisabled(context);
}
}
所以,可以選擇在onReceive或者onUpdate中加入service,以便更新時間。這裏,我們加入了一個TimeWidgetService.java來實現時間的更新。
第四步、後臺Service
package com.blackhill.mytimewidget.ui;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.IBinder;
import android.util.Log;
public class TimeWidgetService extends Service {
private final static String TAG = "TimeWidgetService";
@Override
public IBinder onBind(Intent arg0) {
// TODO Auto-generated method stub
return null;
}
private BroadcastReceiver mTimeWidgetReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
// TODO Auto-generated method stub
String action = intent.getAction();
Log.d(TAG, "onReceive:action name = " + action);
if (action.equals(Intent.ACTION_TIMEZONE_CHANGED)
|| action.equals(Intent.ACTION_TIME_CHANGED)
|| action.equals(Intent.ACTION_TIME_TICK)
|| action.equals(Intent.ACTION_DATE_CHANGED)) {
Log.d(TAG, "onReceive: update the widget");
updateWidget();
}
}
};
@Override
public void onCreate() {
// TODO Auto-generated method stub
super.onCreate();
Log.d(TAG, "onCreate");
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_TIME_CHANGED);
filter.addAction(Intent.ACTION_TIME_TICK);
filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
filter.addAction(Intent.ACTION_DATE_CHANGED);
registerReceiver(mTimeWidgetReceiver, filter);
}
@Override
public void onDestroy() {
// TODO Auto-generated method stub
super.onDestroy();
Log.d(TAG, "onDestroy");
unregisterReceiver(mTimeWidgetReceiver);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// TODO Auto-generated method stub
Log.d(TAG, "onStartCommand");
return super.onStartCommand(intent, flags, startId);
}
public void updateWidget() {
Intent broadcastIntent = new Intent("com.blackhill.mytimewidget.UPDATE_WIDGET");
broadcastIntent.addCategory("com.blackhill.mytimewidget.widgetcategory");
sendBroadcast(broadcastIntent);
}
}
在Service的生命週期中,onCreate()註冊系統廣播,時間更新我們需要增加ACTION_TIME_CHANGED, ACTION_TIME_TICK, ACTION_TIME_DATE_CHANGED等。其中系統時間每到整一分鐘就會發送ACTION_TIME_TICK出來,收到系統廣播後,我們就可以updateWidget()。這裏我們採用發廣播的方式,發送自定義的廣播com.blackhill.mytimewidget.UPDATE_WIDGET
,當然,這個需要在AndroidManifest.xml中聲明。
<receiver android:name="com.blackhill.mytimewidget.ui.TimeWidgetProvider">
<meta-data android:name="android.appwidget.provider"
android:resource="@xml/time_widget_provider"/>
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="android.appwidget.action.APPWIDGET_DISABLED"/>
<action android:name="android.intent.action.DATE_CHANGED"/>
<action android:name="android.intent.action.TIME_CHANGED"/>
<action android:name="android.intent.action.TIME_SET"/>
<action android:name="android.intent.action.TIME_TICK"/>
<action android:name="android.intent.action.TIMEZONE_CHANGED"/>
<action android:name="android.intent.action.CONFIGURATION_CHANGED"/>
<action android:name="com.blackhill.mytimewidget.UPDATE_WIDGET" />
<category android:name="com.blackhill.mytimewidget.widgetcategory" />
</intent-filter>
</receiver>
在TimeWidgetProvider收到UPDATE_WIDGET後,調用updateDateAndTimeWidget並刷新UI組件。
另外,再介紹獲取時間日期的文件DateAndTimeModel.java,以供參考。
package com.blackhill.mytimewidget.lunar;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.TimeZone;
import android.content.Context;
import android.util.Log;
import com.blackhill.mytimewidget.R;
public class DateAndTimeModel {
private static final String TAG = "DateAndTimeModel";
private static final String TIMEZONE_PROPERTY = "persist.sys.timezone";
private static final int HOUR_FORMAT = 12;
private static final int MONTH_ONE_YEAR = 12;
private static final int DAYS_ONE_WEEK = 7;
private GregorianCalendar mCalendar;
private TimeZone timeZone;
public DateAndTimeModel() {
Log.d(TAG, "DateAndTimeModle()");
timeZone = TimeZone.getDefault();
mCalendar = new GregorianCalendar(timeZone);
mCalendar.setTimeInMillis(getSystemTimeMillis());
}
public long getSystemTimeMillis() {
return System.currentTimeMillis();
}
public Date getCurrentDate() {
return new Date();
}
public String getCurrentDate(Context context) {
StringBuffer dateStrBuffer = new StringBuffer();
dateStrBuffer.append(getCurrentYear());
dateStrBuffer.append(context.getString(R.string.lunar_year));
dateStrBuffer.append(getCurrentMonth());
dateStrBuffer.append(context.getString(R.string.lunar_month));
dateStrBuffer.append(getCurrentDay());
dateStrBuffer.append(context.getString(R.string.lunar_day));
return dateStrBuffer.toString();
}
public int getCurrentYear() {
Log.d(TAG, "getCurrentHour: YEAR = " + mCalendar.get(Calendar.YEAR));
return mCalendar.get(Calendar.YEAR);
}
public int getCurrentMonth() {
Log.d(TAG, "getCurrentHour:month = " + mCalendar.get(Calendar.MONTH));
// calculate from 0, as January equals to 0, February equals to 1.
int indexOfMonth = mCalendar.get(Calendar.MONTH) + 1;
return indexOfMonth % MONTH_ONE_YEAR;
}
public int getCurrentDay() {
Log.d(TAG, "getCurrentHour:day of month = " + mCalendar.get(Calendar.DAY_OF_MONTH));
return mCalendar.get(Calendar.DAY_OF_MONTH);
}
public int getCurrentWeekDay() {
Log.d(TAG, "getCurrentHour:day of week = " + mCalendar.get(Calendar.DAY_OF_WEEK));
return mCalendar.get(Calendar.DAY_OF_WEEK);
}
public int getCurrentHour(boolean b24HourFormat) {
Log.d(TAG, "getCurrentHour:Hour = " + mCalendar.get(Calendar.HOUR_OF_DAY));
Log.d(TAG, "getCurrentHour:b24HourFormat = " + b24HourFormat);
if (b24HourFormat) {
return mCalendar.get(Calendar.HOUR_OF_DAY);
}else {
return mCalendar.get(Calendar.HOUR_OF_DAY) % HOUR_FORMAT;
}
}
public int getCurrentMin() {
Log.d(TAG, "getCurrentHour:Minute = " + mCalendar.get(Calendar.MINUTE));
return mCalendar.get(Calendar.MINUTE);
}
}
時間日期的獲取採用GregorianCalendar來取得,GregorianCalendar 是java.util包中 Calendar 的一個具體子類,有興趣可以度娘哦~