文章内容
1. 实例代码分析
经过《应用小部件(App Widget)---- 基础篇(1)》的介绍后,并对Android 4.0.3上的一个实例代码进行
了部分修改,然后将其作为例子进行具体地讲解。
实例代码来源可以参考:http://developer.android.com/resources/samples/WiktionarySimple/index.html
1. 实例代码分析
正如《应用小部件(App Widget)---- 基础篇(1)》中的第二部分(App Widgets的基本要素 )所述,在该
实例程序中可以很明显区分出构成部件的基本组成要素。
1.1 应用小部件提供器信息对象(AppWidgetProviderInfo object)
在工程目录res/xml/下的widget_word.xml文件中设置部件提供器信息对象的基本信息,内容如下:
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="146dip"
android:minHeight="72dip"
android:updatePeriodMillis="100000"
android:initialLayout="@layout/widget_message"
android:resizeMode="horizontal"
/>
在此,我们设置了部件的大小尺寸,更新周期,初始布局,及调整大小的模式。
1.2 小部件视图布局(View layout)
在工程目录res/layout/下的widget_word.xml文件中定义了部件,内容如下:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/widget"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusable="true"
style="@style/WidgetBackground">
<TextView
android:id="@+id/time_update"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dip"
style="@style/Text.Time"/>
<ImageView
android:id="@+id/icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:src="@drawable/star_logo" />
<TextView
android:id="@+id/word_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/time_update"
android:layout_marginBottom="1dip"
android:includeFontPadding="false"
android:singleLine="true"
android:ellipsize="end"
style="@style/Text.WordTitle" />
<TextView
android:id="@+id/word_type"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/word_title"
android:layout_toLeftOf="@id/icon"
android:layout_alignBaseline="@id/word_title"
android:paddingLeft="4dip"
android:includeFontPadding="false"
android:singleLine="true"
android:ellipsize="end"
style="@style/Text.WordType" />
<TextView
android:id="@+id/show_id"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/word_title"
android:paddingRight="4dip"
android:includeFontPadding="false"
android:singleLine="true"
style="@style/BulletPoint" />
<TextView
android:id="@+id/definition"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/word_title"
android:layout_toRightOf="@id/show_id"
android:paddingRight="5dip"
android:paddingBottom="4dip"
android:includeFontPadding="false"
android:lineSpacingMultiplier="0.9"
android:maxLines="4"
android:fadingEdge="vertical"
style="@style/Text.Definition" />
</RelativeLayout>
这个小部件里主要包括了显示更新时间的视图元素,标题视图元素,标题的定义视图元素,显示部件实例ID的视图元素及一个小图标视图元素。
1.3 小部件提供器(AppWidgetProvider)类
该类的定义如下:
package com.example.android.simplewiktionary;
import java.util.Calendar;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.net.Uri;
import android.os.IBinder;
import android.os.SystemClock;
import android.text.format.DateFormat;
import android.text.format.Time;
import android.util.Log;
import android.widget.RemoteViews;
import android.widget.Toast;
import com.example.android.simplewiktionary.SimpleWikiHelper.ApiException;
import com.example.android.simplewiktionary.SimpleWikiHelper.ParseException;
/**
* Define a simple widget that shows the Wiktionary "Word of the day." To build
* an update we spawn a background {@link Service} to perform the API queries.
*/
public class WordWidget extends AppWidgetProvider
{
public final static String MARK_WIDGET_ID = "appWidgetId";
public final static String ACTION_UPDATE = "android.test.appwidget.action_updated";
public Intent intent;
public void onUpdateWidgets(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds)
{
final int N = appWidgetIds.length;
Log.w("wanjf", "Id count is :" + String.valueOf(N));
for (int i = 0; i < N; i++)
{
int appWidgetId = appWidgetIds[i];
String strId = String.format("This widget's id = %d ", appWidgetId);
Intent intent = new Intent(context, UpdateService.class);
appWidgetManager.getAppWidgetInfo(appWidgetId).updatePeriodMillis = 0;
intent.putExtra(MARK_WIDGET_ID, appWidgetId);
context.startService(intent);
Log.w("wanjf", strId);
}
}
@Override
public void onReceive(Context context, Intent intent)
{
Log.w("wanjf","onReceive is called!");
this.intent = intent;
if(ACTION_UPDATE.equals(intent.getAction()))
{
AppWidgetManager gm = AppWidgetManager.getInstance(context);
ComponentName thisWidget = new ComponentName(context, WordWidget.class);
int[] appWidgetIds = gm.getAppWidgetIds(thisWidget);
Log.w("wanjf", "Recieve the broadcast !!");
Log.w("wanjf", "Widgets' count is:" + String.valueOf(appWidgetIds.length));
onUpdateWidgets(context, gm, appWidgetIds);
}
else
{
//
super.onReceive(context, intent);
//
}
}
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds)
{
Log.w("wanjf","onUpdate is called!");
onUpdateWidgets(context, appWidgetManager, appWidgetIds);
super.onUpdate( context, appWidgetManager, appWidgetIds);
}
@Override
public void onDeleted(Context context,int[] widgetIds)
{
Log.w("wanjf","onDeleted is called!");
//
CharSequence prompt = "Oh! Widget id : " + String.valueOf(widgetIds[0]) + " id deleted.";
Toast.makeText(context, prompt ,Toast.LENGTH_SHORT).show();
//
context.stopService(new Intent(context, UpdateService.class));
// TODO Auto-generated method stub
super.onDisabled(context);
}
@Override
public void onDisabled(Context context)
{
Log.w("wanjf","onDisabled is called!");
//
AlarmManager am = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
PendingIntent sender = PendingIntent.getBroadcast (context, 0, new Intent(ACTION_UPDATE), 0);
am.cancel(sender);
//
CharSequence prompt = "Oh! I am the last Widget";
Toast.makeText(context, prompt ,Toast.LENGTH_SHORT).show();
// TODO Auto-generated method stub
super.onDisabled(context);
//
}
@Override
public void onEnabled(Context context)
{
Log.w("wanjf","onEnabled is called!");
AppWidgetManager gm = AppWidgetManager.getInstance(context);
ComponentName thisWidget = new ComponentName(context, WordWidget.class);
int[] appWidgetIds = gm.getAppWidgetIds(thisWidget);
//
PendingIntent sender = PendingIntent.getBroadcast (context, 0, new Intent(ACTION_UPDATE), 0);
// We want the alarm to go off 30 seconds from now.
long firstTime = SystemClock.elapsedRealtime();
firstTime += 20*1000;
// Schedule the alarm!
AlarmManager am = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
am.setRepeating(AlarmManager.ELAPSED_REALTIME,firstTime, 15*1000, sender);
//
CharSequence prompt = "Oh! I am the first Widget id : " + String.valueOf(appWidgetIds[0]);
Toast.makeText(context, prompt ,Toast.LENGTH_SHORT).show();
// TODO Auto-generated method stub
super.onEnabled(context);
//
}
public static class UpdateService extends Service
{
@Override
public void onStart(Intent intent, int startId)
{
Log.w("wanjf","Service's onStart() is called!");
// Build the widget update for today
int appWidgetId = intent.getIntExtra(MARK_WIDGET_ID, -1);
RemoteViews updateViews = buildUpdate(this, appWidgetId);
if(appWidgetId == -1)
stopSelf();
// Push update for this widget to the home screen
//ComponentName thisWidget = new ComponentName(this, WordWidget.class);
AppWidgetManager manager = AppWidgetManager.getInstance(this);
//manager.updateAppWidget(thisWidget, updateViews);
manager.updateAppWidget(appWidgetId, updateViews);
}
/**
* Build a widget update to show the current Wiktionary
* "Word of the day." Will block until the online API returns.
*/
public RemoteViews buildUpdate(Context context, int widgetId)
{
// Pick out month names from resources
Resources res = context.getResources();
String[] monthNames = res.getStringArray(R.array.month_names);
// Find current month and day
Time today = new Time();
today.setToNow();
// Build today's page title, like "Wiktionary:Word of the day/March 21"
String pageName = res.getString(R.string.template_wotd_title, monthNames[today.month], today.monthDay);
//Log.w("wanjf",pageName);
RemoteViews updateViews = null;
String pageContent = "";
try
{
// Try querying the Wiktionary API for today's word
SimpleWikiHelper.prepareUserAgent(context);
pageContent = SimpleWikiHelper.getPageContent(pageName, false);
Log.w("wanjf",pageContent);
}
catch (ApiException e)
{
Log.e("WordWidget", "Couldn't contact API", e);
}
catch (ParseException e)
{
Log.e("WordWidget", "Couldn't parse API response", e);
}
// Use a regular expression to parse out the word and its definition
Pattern pattern = Pattern.compile(SimpleWikiHelper.WORD_OF_DAY_REGEX);
Matcher matcher = pattern.matcher(pageContent);
if (matcher.find())
{
CharSequence time = DateFormat.format("hh:mm:ss", Calendar.getInstance()) ;
// Build an update that holds the updated widget contents
updateViews = new RemoteViews(context.getPackageName(), R.layout.widget_word);
String wordTitle = matcher.group(1);
Log.w("wanjf",wordTitle);
updateViews.setTextViewText(R.id.word_title, wordTitle);
updateViews.setTextViewText(R.id.word_type, matcher.group(2));
updateViews.setTextViewText(R.id.definition, matcher.group(3).trim());
updateViews.setTextViewText(R.id.show_id, "[" + String.valueOf(widgetId) + "]");
updateViews.setTextViewText(R.id.time_update, time);
// When user clicks on widget, launch to Wiktionary definition page
String definePage = res.getString(R.string.template_define_url,Uri.encode(wordTitle));
Intent defineIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(definePage));
PendingIntent pendingIntent = PendingIntent.getActivity(context,
0 /* no requestCode */, defineIntent, 0 /* no flags */);
updateViews.setOnClickPendingIntent(R.id.widget, pendingIntent);
}
else
{
// Didn't find word of day, so show error message
updateViews = new RemoteViews(context.getPackageName(), R.layout.widget_message);
CharSequence errorMessage = context.getText(R.string.widget_error);
updateViews.setTextViewText(R.id.message, errorMessage);
}
return updateViews;
}
@Override
public IBinder onBind(Intent intent)
{
// We don't need to bind to this service
return null;
}
}
}
在原始代码的基础上,我们做了如下的修改:
1. 重写了基类appWidgetProvider的所有方法;
2. 设置警报来更新小部件;
3. 增加对部件进行操作的提示;
4. 对某些操作(警报,服务)的设置和清理做了控制;
接下来,我们重点对上述做了修改的部分进行分析。
修改后的代码重写了基类的onReceive,onUpdate,onDeleted,onDisabled,onEnabled,onUpdate等
方法。有了这些方法,我们便很容易发现当他们被调用时在部件上究竟发生了什么。
首先,当我们把第一个部件实例添加到宿主视图内时,onReceive方法首先被调用,然后是onEnabled方法,接
着还是onReceive方法,最后是onUpdate方法。由于我们在onEnabled内设置了一个重复触发的警报,因此当警报
在随后被触发时,还将调用onReceive方法。
提醒:由于onEnabled方法只在部件实例的首次被创建时才会被调用,因此在此方法内执行一些对所有部件实例都适用的操作是最佳场所。
接着,当继续向宿主视图内添加一个(或多个)该部件的实例时,首先调用的是onReceive方法,然后是
onUpdate方法。
然后,从宿主内删除一个该部件的实例时,首先调用的是onReceive方法,然后是onDeleted方法。
最后,当该部件的所有实例从宿主内被移除时,首先调用的是onReceive方法,然后是onDeleted方法,接着由
是onReceive方法,最后是onDisabled方法。
提醒:onDisabled方法也只被调用一次,因此也是执行清理操作的最佳场所。此例中,在该方法内取消了警报,停止服务。
由于我们重写了基类的诸方法,因此别忘记调用该方法的基类版本。尤其是在重写onReceive方法后,如果忘记
调用基类版本的onReceive,我们重写其他方法也不会被调用。除非在重写的onReceive方法对收到的广播进行过滤
后再手动调用其他方法。
除了以上方法外,我们需要留意自定义方法onUpdateWidgets。在该方法内我们启动了一个“服务”来执行部
件的更新(主要是更细其上的显示内容)。之所以这么做主要是因为部件上的内容中有些是自于网络上的数据,而对
于网络数据的请求可能耗时几秒钟,甚至更多。在服务内执行网络数据请求可以保证WordWidget的onReceive及时
返回。否则,可能会因为ANR错误,活动管理器在后台将其结束掉(Kill)。
此例中,“服务”内除了获取网络数据外,还对部件与用户接口上的交互进行了设置。于是当用户点击部件时,
将会打开相应的网页。
现在我们来看一下运行后的效果:
Home Screen(宿主)内的这一大一小两个部件都是我们刚刚创建的,由于在部件提供器信息对象对应的xml文
件内设置了 android:resizeMode="horizontal",因此我们可以在水平方向上调整部件的大小。除此,我们需要留意
在部件上的蓝色数字和红色数字。蓝色数字是每个部件实例最后一次更新的时间,而红色数字是该部件的实例ID。
部件的实例id是由部件管理器维护的一组数字,表示每个实例对应一个不同的id,因此在更新时可以获取他们并
对其代表的部件实例进行更新。
通过观察部件上时间变化可知,每个实例上的时间变化并不是一致进行的。这是因为在onUpdateWidgets方法内通过迭代id数组后,再使用指定的id对其表示的实例进行更新。如果要是所有实例上的时间同时更新,可以将onUpdateWidgets方法修改成:
public void onUpdateWidgets(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds)
{
Intent intent = new Intent(context, UpdateService.class);
context.startService(intent);
}
然后再将内部类UpdateService的onStart方法修改成:
public void onStart(Intent intent, int startId)
{
Log.w("wanjf","Service's onStart() is called!");
// Build the widget update for today
RemoteViews updateViews = buildUpdate(this, appWidgetId);
updateViews.setViewVisibility(R.id.show_id, View.INVISIBLE);
// Push update for this widget to the home screen
ComponentName thisWidget = new ComponentName(this, WordWidget.class);
manager.updateAppWidget(thisWidget, updateViews);
}
至此,创建一个简单的小部件程序就分析到这里。当然,在此实例代码里还有涉及到网络编程及正则表达式方面的内容。关于网络的那部分代码可以参考Android SDK上的介绍来理解具体接口的用法和作用。而关于正则表达的理解可以参考:http://www.java3z.com/cwbwebhome/article/article8/Regex/Java.Regex.Tutorial.html#note01
2012年4月27日,毕