1.继承AppWidgetProvider,并在清单文件中声明此组件。其虽名为Provider,其实是一个广播接收者,负责接收系统定期发送的Widget广播。
2.在res/xml目录定义Widget初始化信息文件(该信息最终会被读取到载体AppWidgetProviderInfo中),并将该定义文件指定到AndroidManifest.xml中1所声明的AppWidgetProvider继承者中,以便为AppWidgetProvider提供窗口小部件的布局等信息。
3.实现指定Widget所加载的布局文件,该布局文件在步骤2所定义文件中有所声明。该布局文件正是作为Widget展示在其它应用程序(如Launcher)的具体视图文件。用户可利用RemoteViews远程操作展示在其它程序中Widget布局内的内容、点击事件等。
4.实现AppWidgetProvider周期方法如onEnabled、onUpdate、onDeleted、onDisabled。在onUpdate中我们可以更新Widget,但由于onUpdate调用时长的原因,我们常在onUpdate中启动专用Service负责更新Widget操作。
5.如果还想再创建Widget时进行一些配置操作,我们可以在步骤2定义的文件中配置configure指定创建Widget时弹出的Activity进行Widget基础性配置。
我们自定义广播组件继承自AppWidgetProvider,然后在清单文件中声明。此广播组件需要通过意图过滤选择想要接收的广播,此处我们定义的过滤action为android.appwidget.action.APPWIDGET_UPDATE,此action为过滤系统Widget相关的广播。同时该组件作为Widget提供者,还需要指定Widget的元信息(该元信息中指定Widget的初始化信息文件,在该文件中指出了Widget的最小宽高、刷新频率、视图布局、预览图、Widget类型等信息)。
在res/xml目录下定义Widget初始化信息文件widget_info(此文件名需要与步骤1中清单文件中所定义元信息的resource一致)。
属性解释:
1)minWidth & minHeight:定义了 Widget 的最小宽高,当 minWidth 和 minHeight 不是桌面 cell 的整数倍时,Widget 的宽高会被阔至与其最接近的 cells 大小。Google 官方给出了一个大致估算 minWidth & minHeight 的公式,根据 Widget 所占的 cell 数量来计算宽高:70 * n − 30,n 是所占的 cell 数量。
2)previewImage:指的是Launcher中所示Widget list列表中的预览效果图。
3)resizeMode:当Widget添加到应用程序窗口中后,长按Widget时可选择的拉伸模式。horizontal表示可横向伸缩,vertical表示纵向伸缩,但不论横向纵向拉伸最小值都不能小于minWidth、minHeight。
4)updatePeriodMillis:用于指定Widget更新频率,系统根据此值定时回调AppWidgetProvider中的onUpdate方法。Android系统默认的最小Widget更新周期为30分钟,也就是说除了新增Widget会回调onUpdate之外,onUpdate回调周期至少30分钟,即使updatePeriodMillis指定更小也无用。
5)widget_category:指定当前Widget的显示位置类型。home_screen类型表示Widget位于桌面,keyguard类型表示Widget位于锁屏页。
6)initialLayout:表示当显示类型为home_screen即桌面时Widget视图布局。
7)initialKeyguardLayout:表示当显示类型为keyguard即锁屏页时Widget的视图布局。
8)configure:值为用户添加Widget时弹出的Widget基础性配置Activity。用户可在此Activity中对Widget内容等进行定制。
在步骤1中我们定义了继承自AppWidgetProvider的一个广播接收者,此接收者会根据不同的广播事件回调不同的生命周期方法。我们可以去AppWidgetProvider的源码中看看:
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);
} else if (AppWidgetManager.ACTION_APPWIDGET_RESTORED.equals(action)) {
Bundle extras = intent.getExtras();
if (extras != null) {
int[] oldIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_OLD_IDS);
int[] newIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_IDS);
if (oldIds != null && oldIds.length > 0) {
this.onRestored(context, oldIds, newIds);
this.onUpdate(context, AppWidgetManager.getInstance(context), newIds);
}
}
}
}
作为继承类,我们没必要重写广播接收者的onReceive方法(此方法父类AppWidgetProvider已经为我们实现),只需要重写onReceive中扩展出来的周期方法即可。
以下是周期方法的解释:
1)onEnabled:当 Widget 第一次被添加时调用,例如用户添加了两个你的 Widget,那么只有在添加第一个 Widget 时该方法会被调用。
2)onUpdate:每次添加Widget都会被调用,且会根据用户在步骤2定义文件中的updatePeriodMillis值周期性被调用。一般我们在此回调中启动一个Service,在Service内部启动周期性定时Timer负责实时更新Widget。
3)onDeleted:每次Widget被删除时调用此方法。我们如果在onUpdate中启动有Service,那么需要在此处根据appWidgetId来停止掉Service中的更新。
4)onDisabled:当最后一个Widget被删除时调用。
5)onAppWidgetOptionsChanged:当 Widget 第一次被添加或者大小发生变化时调用该方法,可以在此控制 Widget 元素的显示和隐藏。
如果我们需要在创建Widget时进行一些基础性配置,那么我们可通过在步骤2中定义的文件中添加configure属性,值指定一个Activity,此Activity会在创建Widget时弹出方便用户配置Widget信息。如下所示,我们在清单文件中声明此Activity同时还需要指定action为android.intent.action.APPWIDGET_CONFIGURE的意图过滤器。系统判断此intentFilter进行创建Widget时弹出配置Activity操作。
在WidgetConfigureActivity中我们可以通过intent获取到当前需要配置的appWidgetId,并可以对此appWidgetId对应的视图内容进行不同的初始化操作。当然,如果想使得此配置生效,别忘了在finish此Activity时setResult(RESULT_OK)。
首先我们定义WidgetProvider继承于AppWidgetProvider。重写其主要周期方法onUpdate、onDeleted以在Service控制各个Widget内部实时时钟线程。
public class WidgetProvider extends AppWidgetProvider {
private static final String TAG = "WidgetProvider";
/**
*更新操作,每当用户添加一个当前widget到桌面就会回调此方法一次,
* 默认情况下根据widget_info中的updatePeriodMillis时间更新,如果updatePeriodMillis最小值不可小于30分钟
* 小于30系统也只会按30min/次更新
*
* @param context 当前应用所在的上下文环境
* @param appWidgetManager manager,如果不是需要实时更新widget,则可以直接在方法中使用manager进行更新
* @param appWidgetIds 一般此处都为1个,当应用更新版本时,当前数组中可能为多个
*/
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
super.onUpdate(context, appWidgetManager, appWidgetIds);
Log.i(TAG, "onUpdate: "+Arrays.toString(appWidgetIds)+", context:"+context.getPackageName());
Intent intent = new Intent(context, UpdateWidgetService.class);
//把更改的appWidgetIds
intent.putExtra("appWidgetIds",appWidgetIds);
intent.setAction(UpdateWidgetService.ACTION_UPDATE);
context.startService(intent);
}
@Override
public void onDeleted(Context context, int[] appWidgetIds) {
super.onDeleted(context, appWidgetIds);
Log.i(TAG, "onDeleted: "+ Arrays.toString(appWidgetIds));
Intent intent = new Intent(context, UpdateWidgetService.class);
//把更改的appWidgetIds
intent.putExtra("appWidgetIds",appWidgetIds);
intent.setAction(UpdateWidgetService.ACTION_DELETE);
context.startService(intent);
}
@Override
public void onEnabled(Context context) {
super.onEnabled(context);
Log.i(TAG, "onEnabled: ");
}
@Override
public void onDisabled(Context context) {
super.onDisabled(context);
Log.i(TAG, "onDisabled: ");
}
}
可以看见我们在onUpdate和onDeleted中使用startService启动服务(多次调用只会执行Service的onStartCommand周期方法)。同时把appWidgetIds作为intent携带数据传给Service,这样方便我们在Service中对不同appWidgetId进行更新。当然,我们也应该在清单文件中声明此组件,代码详情正如步骤1所示。
在清单文件中我们制定了元数据resource的文件为widget_info.xml,即步骤2所示文件。在其中我们声明widget视图布局为widget_layout.xml,此布局如下所示:
可以看见,widget视图布局中我们指定了一个TextView居中显示在红色背景的容器中。而对此视图的内容操作则被放入了WidgetProvider启动的UpdateWidgetService中了。
细心的读者可能已经发现,我们在步骤2所示的widget_info文件中指定了在创建Widget时需要打开的配置Activity页面WidgetConfigureActivity。而在WidgetConfigureActivity中则指定了视图布局中TextView初始化创建时显示时钟的时间。
public class WidgetConfigureActivity extends AppCompatActivity implements View.OnClickListener {
private static final String TAG = "WidgetConfigureActivity";
private int appWidgetId;
private Button btn_choose_date;
private Button btn_choose_time;
private Button btn_confirm;
private Calendar calendar;
private TextView txt_time;
private SimpleDateFormat format;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_widget_configure);
checkWidgetId();
bindViews();
}
private void checkWidgetId() {
Bundle extras = getIntent().getExtras();
if (extras!=null){
appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID,AppWidgetManager.INVALID_APPWIDGET_ID);
}
if (appWidgetId==AppWidgetManager.INVALID_APPWIDGET_ID){
//当前没有传递appWidgetId
Log.i(TAG, "onCreate: appWidgetId is invalid");
finish();
}
}
private void bindViews() {
btn_choose_date = findViewById(R.id.btn_choose_date);
btn_choose_time = findViewById(R.id.btn_choose_time);
btn_confirm = findViewById(R.id.btn_confirm);
txt_time = findViewById(R.id.txt_time);
btn_choose_date.setOnClickListener(this);
btn_choose_time.setOnClickListener(this);
btn_confirm.setOnClickListener(this);
calendar = Calendar.getInstance();
format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
txt_time.setText(format.format(calendar.getTime()));
}
@Override
public void onClick(View v) {
switch (v.getId()){
case R.id.btn_choose_date:
DatePickerDialog datePickerDialog = new DatePickerDialog(this, new DatePickerDialog.OnDateSetListener() {
@Override
public void onDateSet(DatePicker view, int year, int month, int dayOfMonth) {
Log.i(TAG,"year:"+year+",month:"+month+",day:"+dayOfMonth);
calendar.set(year,month,dayOfMonth);
txt_time.setText(format.format(calendar.getTime()));
}
}, calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH), calendar.get(Calendar.DAY_OF_MONTH));
datePickerDialog.show();
break;
case R.id.btn_choose_time:
TimePickerDialog timePickerDialog = new TimePickerDialog(this, new TimePickerDialog.OnTimeSetListener() {
@Override
public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
Log.i(TAG,"hour:"+hourOfDay+",minute:"+minute);
calendar.set(Calendar.HOUR_OF_DAY,hourOfDay);
calendar.set(Calendar.MINUTE,minute);
txt_time.setText(format.format(calendar.getTime()));
}
}, calendar.get(Calendar.HOUR_OF_DAY), calendar.get(Calendar.MINUTE), true);
timePickerDialog.show();
break;
case R.id.btn_confirm:
Intent intent = new Intent(getApplicationContext(), UpdateWidgetService.class);
intent.setAction(UpdateWidgetService.ACTION_WIDGET_CONFIGURE);
intent.putExtra("calendar",calendar);
intent.putExtra("appWidgetId",appWidgetId);
startService(intent);
setResult(RESULT_OK);
finish();
break;
}
}
}
在WidgetConfigureActivity中我们定义了修改日期、修改时间、确定三个按钮。
通过点击'选择日期',可弹出弹框给用户自行选择时钟的开始日期,通过点击'选择时间',可弹出弹框给用户自行选择时钟的开始时间。同时,从onClick方法中我们可以看到,点击‘确认’按钮时我们会把选择的时钟时间通过UpdateWidgetService设置给新创建的Widget中(别忘了在finish时调用setResult方法使此初始化配置生效哦)。
OK!!!说了那么多,重头戏Widget更新操作其实是在接下来的UpdateWidgetService中。
在UpdateWidgetService中我们通过在onStartCommand中判断intent不同的action得知当前需要进行update还是delete操作。
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
String action = intent.getAction();
if (!TextUtils.isEmpty(action)){
switch (action){
case ACTION_UPDATE:
//更新某个widget
int[] updates = intent.getIntArrayExtra("appWidgetIds");
if (updates!=null&&updates.length>0){
for (int appWidgetId :
updates) {
updateWidget(appWidgetId, getCalendarFromMap(generateName(appWidgetId)));
}
}
break;
case ACTION_DELETE:
//删除某个widget
int[] deletes = intent.getIntArrayExtra("appWidgetIds");
if (deletes!=null&&deletes.length>0){
for (int appWidgetId :
deletes) {
deleteWidget(appWidgetId);
}
}
break;
case ACTION_WIDGET_CONFIGURE:
int appWidgetId = intent.getIntExtra("appWidgetId",AppWidgetManager.INVALID_APPWIDGET_ID);
if (appWidgetId!=AppWidgetManager.INVALID_APPWIDGET_ID){
Calendar calendar = (Calendar) intent.getSerializableExtra("calendar");
if (calendar!=null){
putCalendarToMap(generateName(appWidgetId),calendar);
updateWidget(appWidgetId,calendar);
}
}
break;
}
}
return START_NOT_STICKY;
}
如果是ACTION_UPDATE,我们则需要对不同的appWidgetId进行实时时钟更新操作。可以看见在updateWidget中我们根据appWidgetId的不同从map集合中获取到不同的Calendar作为参数传入。此时我们就需要思考,如果用户创建了多个同一视图的Widget,都需要进行实时更新操作,那么我们该怎么把控各个Widget的更新?
显然,我们应该针对各个appWdigetId开启不同的线程,既保证各个Widget中数据不相互干扰,也能保证对各Widget进行不同的实时更新操作。
private void updateWidget(final int appWidgetId, final Calendar calendar) {
final Handler handler = ThreadManager
.getInstance(getApplicationContext())
.getHandlerByName(generateName(appWidgetId));
handler.removeCallbacksAndMessages(null);
handler
.post(new Runnable() {
@Override
public void run() {
handler.removeCallbacks(this);
/**
* 两种获取Manager方式
* AppWidgetManager manager = (AppWidgetManager) getSystemService(Context.APPWIDGET_SERVICE);
*/
AppWidgetManager manager = AppWidgetManager.getInstance(getApplicationContext());
AppWidgetProviderInfo appWidgetInfo = manager.getAppWidgetInfo(appWidgetId);
RemoteViews remoteViews = new RemoteViews(getPackageName(),R.layout.widget_layout);
calendar.setTimeInMillis(calendar.getTimeInMillis()+100);
remoteViews.setTextViewText(R.id.time,generateName(appWidgetId)+"\n"+mFormat.format(calendar.getTime()));
manager.updateAppWidget(appWidgetId,remoteViews);
handler.postDelayed(this,100);
Log.i(TAG,"updateWidget-"+appWidgetId);
}
});
}
可以看见我们每100ms就对各个Widget进行了一次时钟更新操作。
此处我们为了保证线程不冗余创建,使用了map集合进行了管理。而各个线程我们为了效率和CPU考虑,使用的是封装后的WidgetThread(继承自HandlerThread)。以上getHandlerByName方法首先会获取我们的Widget线程,再从Widget线程中获取到线程专属Handler。
public Handler getHandlerByName(String name){
return getWidgetThread(name).getHandler();
}
private synchronized WidgetThread getWidgetThread(String name) {
WidgetThread thread;HashMap map = getThreadMap();
if (map.containsKey(name)){
//取出
thread = (WidgetThread) map.get(name);
}else {
//创建HandlerThread
thread = new WidgetThread(name);
thread.start();
putWidgetThread(name,thread);
}
return thread;
}
以下是WidgetThread封装,关于HandlerThread用法读者可移步这篇文章,这里不做详细介绍。
public class WidgetThread extends HandlerThread{
private Handler mHandler;
public WidgetThread(String name) {
super(name);
}
@Override
protected void onLooperPrepared() {
getHandler();
}
public Handler getHandler() {
if (mHandler==null){
synchronized (WidgetThread.class){
if (mHandler==null)
mHandler = new Handler(getLooper());
}
}
return mHandler;
}
@Override
public boolean quitSafely() {
getHandler().removeCallbacksAndMessages(null);
return super.quitSafely();
}
@Override
public boolean quit() {
getHandler().removeCallbacksAndMessages(null);
return super.quit();
}
}
在WidgetProvider的onDeleted方法回调时,我们使用ACTION_DELETE启动UpdateWidgetService,并在Service中针对appWidgetId的实时工作线程需要退出处理。
private void deleteWidget(int appWidgetId) {
ThreadManager
.getInstance(getApplicationContext())
.stopWidgetThread(generateName(appWidgetId));
}
最后,附上效果图。图1展示了创建Widget过程。图2展示了多Widget实时更新过程。