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日,毕