Android App Widget桌面小部件开发实践

App Widget桌面小部件已经被很多APP所使用,在日常的生活中也有很多人在使用,最常见的就是时钟、天气、日历、记事本这样的小工具,但是不少主流APP也提供了一些快捷功能的小部件,例如音乐快捷播放、代办事项、课程表、股票、购物快捷搜索、邮箱邮件等等,桌面小部件是APP部分业务功能的快捷入口,可以有效提升用户的使用体验缩短触达链路。

一、小部件基本组成

1.1 小部件UI布局

和Activity类似,在 XML 资源中为定义初始布局并将其保存在项目的 res/layout/目录中,App Widget 布局基于RemoteViews,它不支持所有类型的容器布局和视图控件。

RemoteViews 可支持的容器和控件如下:

容器

  • FrameLayout (帧布局)
  • LinearLayout (线性布局)
  • RelativeLayout (相对布局)

控件

  • AnalogClock (时钟)
  • Button (按钮)
  • Chronometer (计时器)
  • ImageButton (图片按钮)
  • ImageView (图片)
  • ProgressBar (进度条)
  • TextView (文本框)
  • ListView (纵向列表)
  • GridView (宫格)
  • StackView (堆叠)
  • ViewFlipper (翻转控件)
  • AdapterViewFlipper (翻转控件,但需要Adapter)

并且,不支持继承了上述容器和控件的View,也就是不支持自定义View。

例如:

<FrameLayout
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:background="@drawable/my_widget_background">

  <TextView
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

FrameLayout>

1.2 AppWidgetProviderInfo 元数据

AppWidgetProviderInfo 用于描述一个 App Widget 的基本属性,如布局文件、布局尺寸、布局可变方向、最小可变布局尺寸、预览图、更新频率、配置Activity。

使用单个元素在 XML 资源文件中定义 AppWidgetProviderInfo 对象,并将其保存在项目的 res/xml 文件夹中。

例如:

<appwidget-provider 
    xmlns:android = "http://schemas.android.com/apk/res/android" 
    android:minWidth = "250dp" 
    android:minHeight = "110dp" 
    android:updatePeriodMillis = "86400000" 
    android:previewImage = "@drawable/preview" 
    android:initialLayout = "@layout/example_appwidget" 
    android:configure = "com.example.android.ExampleAppWidgetConfigure" 
    android:resizeMode = "horizo​​ntal|vertical" /> 

以下是属性的摘要:

  • minWidthminHeight 属性的值指定默认情况下App Widget 消耗的最小空间量 。默认主屏幕根据具有定义高度和宽度的单元格网格在其窗口中定位应用小部件,如果一个App Widget的最小宽度或高度的值不匹配的单元的尺寸,则会调整到最接近的小区大小。

【注意】为适配不同的屏幕尺寸,小部件的最小尺寸不应大于 4 x 4 单元格。

  • minResizeWidthminResizeHeight属性指定 App Widget 的绝对最小尺寸。这些值应指定应用小部件即将无法辨认或无法使用的大小,使用这些属性允许用户将窗口小部件的大小调整为可能小于由minWidth和minHeight属性定义的默认窗口小部件大小的大小 。
  • updatePeriodMillis属性定义了系统更新 App Widget 的频率。不能保证使用此值准确按时进行更新,官方建议尽可能不频繁地进行更新,建议每小时不超过一次以节省电池电量,并且最小值为1800000——即30分钟。

【注意】如果设备在更新时处于睡眠状态,则设备将唤醒以执行更新。如果每小时更新不超过一次,这可能不会对电池寿命造成重大问题。但是,如果需要更频繁地更新或不需要在设备休眠时更新,那么可以根据不会唤醒设备的闹钟执行更新,使用 AppWidgetProvider 接收的 Intent 设置 AlarmManager,将闹钟类型设置为ELAPSED_REALTIME或 RTC,然后设置 updatePeriodMillis为0。

  • initialLayout属性指向定义 App Widget 布局的布局资源。
  • configure属性定义了在用户添加 App Widget 时启动的配置Activity,以便用户配置 App Widget 属性。
  • previewImage属性指定应用小部件在的外观预览图,用户在选择应用小部件时会看到该预览图。如果未提供,用户将看到APP的启动图标。
  • resizeMode属性指定可以调整小部件大小的规则。此属性使主屏幕小部件可调整大小——水平、垂直或在两个轴上。用户按住小部件以显示其调整大小手柄,然后拖动水平和/或垂直手柄以更改布局网格上的大小。该resizeMode属性的值 包括“horizo​​ntal”、“vertical”和“none”。要将小部件声明为水平和垂直均可调整大小,则使用“horizo​​ntal|vertical”。

1.3 AppWidgetProvider 类实现

AppWidgetProvider类是小部件的核心类,继承了BroadcastReceiver,可以处理广播消息。

AppWidgetProvider 仅接收与 App Widget 相关的事件广播,例如 App Widget 何时更新、删除、启用和禁用。

当这些广播事件发生时,AppWidgetProvider 的主要生命周期方法会被调用:

  • onUpdate()
    此方法会根据配置的 updatePeriodMillis 属性定义的时间间隔进行调用,作用为更新小部件,当用户添加小部件时也会调用此方法,因此它应该执行一些基本的设置。但是,如果你已经声明了一个ConfigureActivity,这个方法在用户添加小部件时不会调用,而是在后续更新时调用。ConfigureActivity负责在配置完成后执行第一次更新。
  • onDeleted(Context, int[])
    每次从桌面删除小部件时都会调用此方法。
  • onEnabled(Context)
    只有在桌面上添加对应小部件的第一个实例时会调用此方法。例如,如果用户连续添加了一个小部件两次,只有第一次添加时会调用此方法。
  • onDisabled(Context)
    当对应小部件的最后一个实例从 App Widget 主机中删除时,会调用此方法,在此方法中应执行资源释放等工作。
  • onReceive(Context, Intent)
    每次接收到广播和上述每个回调方法之前会调用此方法,主要用于处理点击事件、手动更新等事项。

例如:

public class ExampleAppWidgetProvider extends AppWidgetProvider {

    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        final int N = appWidgetIds.length;
        for (int i=0; i<N; i++) {
            int appWidgetId = appWidgetIds[i];

            // 创建一个打开Activity的Intent
            Intent intent = new Intent(context, ExampleActivity.class);
            PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);

            // 点击事件的处理通过setOnClickPendingIntent而不是setOnclickListener
            RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.appwidget_provider_layout);
            views.setOnClickPendingIntent(R.id.button, pendingIntent);

            appWidgetManager.updateAppWidget(appWidgetId, views);
        }
    }
}

1.4 在清单中声明应用小部件

小部件和广播的注册方式类似,AppWidgetProvider在AndroidManifest.xml文件中声明后即可生效:

<receiver android:name="ExampleAppWidgetProvider" >
    <intent-filter>
        <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
    intent-filter>
    <meta-data android:name="android.appwidget.provider"
               android:resource="@xml/example_appwidget_info" />
receiver>

二、高级功能开发

2.1 提升更新频率

【1.2 AppWidgetProviderInfo 元数据】中提到:updatePeriodMillis属性定义了系统更新 App Widget 的频率,且最小值为1800000——即30分钟。

但是大部分情况下,30分钟更新一次数据会产生很大的误差,在用户使用时必然是希望实时刷新的,这时候自动刷新已经无法满足用户的需求了,必须要进行手动触发刷新,提升更新的频率。

App Widget 是一个广播接收器,它的生命周期里的onUpdate()是一个特定的广播触发的,可以通过Service定时发送刷新的广播,达到提升更新频率的目的。

/**
 * 控制 桌面小部件 更新
 */
public class WidgetService extends Service {

    /** 
     * 周期性更新 widget 的周期
     */
    private static final int UPDATE_TIME = 1000;
    private Timer mTimer;
    private TimerTask mTimerTask;
    
    @Override
    public void onCreate() {
        super.onCreate();
        // 每经过指定时间,发送一次广播
        mTimer = new Timer();
        mTimerTask = new TimerTask() {
            @Override
            public void run() {
                Intent updateIntent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
                sendBroadcast(updateIntent);
            }
        };
        mTimer.schedule(mTimerTask, UPDATE_TIME, UPDATE_TIME);
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override

    public void onDestroy() {
        super.onDestroy();
        mTimerTask.cancel();
        mTimer.cancel();
    }
}

2.2 列表、宫格、轮播实现

这一类型样式的实现有一个共同点,需要依赖Adapter完成item的UI以及逻辑的编写。

平时开发中,需要通过目标控件的setAdapter()方法将对应的Adapter绑定到控件上,但是RemoteViews不能通过findViewById()等方法拿到目标控件的实例,也就无法使用Adapter。

这时候就需要使用RemoteViews.setRemoteAdapter(int viewId, Intent intent)方法设置,但这里用的是Intent,这个Intent指向一个RemoteViewsService(实现RemoteViewsFactory onGetViewFactory(Intent intent)方法)。

真实的Adapter就是RemoteViewsFactory实例了,通过其RemoteViews getViewAt(int position)方法完成item的UI以及逻辑的编写。

public class StackWidgetProvider extends AppWidgetProvider {
    public static final String TOAST_ACTION = "com.example.android.stackwidget.TOAST_ACTION";
    public static final String EXTRA_ITEM = "com.example.android.stackwidget.EXTRA_ITEM";

    ...

    // Called when the BroadcastReceiver receives an Intent broadcast.
    // Checks to see whether the intent's action is TOAST_ACTION. If it is, the app widget 
    // displays a Toast message for the current item.
    @Override
    public void onReceive(Context context, Intent intent) {
        AppWidgetManager mgr = AppWidgetManager.getInstance(context);
        if (intent.getAction().equals(TOAST_ACTION)) {
            int appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
                AppWidgetManager.INVALID_APPWIDGET_ID);
            int viewIndex = intent.getIntExtra(EXTRA_ITEM, 0);
            Toast.makeText(context, "Touched view " + viewIndex, Toast.LENGTH_SHORT).show();
        }
        super.onReceive(context, intent);
    }
    
    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        // update each of the app widgets with the remote adapter
        for (int i = 0; i < appWidgetIds.length; ++i) {
    
            // Sets up the intent that points to the StackViewService that will
            // provide the views for this collection.
            Intent intent = new Intent(context, StackWidgetService.class);
            intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]);
            // When intents are compared, the extras are ignored, so we need to embed the extras
            // into the data so that the extras will not be ignored.
            intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
            RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.widget_layout);
            rv.setRemoteAdapter(appWidgetIds[i], R.id.stack_view, intent);
    
            // The empty view is displayed when the collection has no items. It should be a sibling
            // of the collection view.
            rv.setEmptyView(R.id.stack_view, R.id.empty_view);

            // This section makes it possible for items to have individualized behavior.
            // It does this by setting up a pending intent template. Individuals items of a collection
            // cannot set up their own pending intents. Instead, the collection as a whole sets
            // up a pending intent template, and the individual items set a fillInIntent
            // to create unique behavior on an item-by-item basis.
            Intent toastIntent = new Intent(context, StackWidgetProvider.class);
            // Set the action for the intent.
            // When the user touches a particular view, it will have the effect of
            // broadcasting TOAST_ACTION.
            toastIntent.setAction(StackWidgetProvider.TOAST_ACTION);
            toastIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]);
            intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
            PendingIntent toastPendingIntent = PendingIntent.getBroadcast(context, 0, toastIntent,
                PendingIntent.FLAG_UPDATE_CURRENT);
            rv.setPendingIntentTemplate(R.id.stack_view, toastPendingIntent);
            
            appWidgetManager.updateAppWidget(appWidgetIds[i], rv);
        }
    super.onUpdate(context, appWidgetManager, appWidgetIds);
    }
}

public class StackWidgetService extends RemoteViewsService {
    @Override
    public RemoteViewsFactory onGetViewFactory(Intent intent) {
        return new StackRemoteViewsFactory(this.getApplicationContext(), intent);
    }
}

class StackRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory {
    private static final int mCount = 10;
    private List<WidgetItem> mWidgetItems = new ArrayList<WidgetItem>();
    private Context mContext;
    private int mAppWidgetId;

    public StackRemoteViewsFactory(Context context, Intent intent) {
        mContext = context;
        mAppWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
                AppWidgetManager.INVALID_APPWIDGET_ID);
    }

      // Initialize the data set.
     public void onCreate() {
         // In onCreate() you set up any connections / cursors to your data source. Heavy lifting,
         // for example downloading or creating content etc, should be deferred to onDataSetChanged()
         // or getViewAt(). Taking more than 20 seconds in this call will result in an ANR.
         for (int i = 0; i < mCount; i++) {
             mWidgetItems.add(new WidgetItem(i + "!"));
         }
         ...
     }
     ...
    
      // Given the position (index) of a WidgetItem in the array, use the item's text value in 
      // combination with the app widget item XML file to construct a RemoteViews object.
     public RemoteViews getViewAt(int position) {
           // position will always range from 0 to getCount() - 1.
    
         // Construct a RemoteViews item based on the app widget item XML file, and set the
         // text based on the position.
         RemoteViews rv = new RemoteViews(mContext.getPackageName(), R.layout.widget_item);
          rv.setTextViewText(R.id.widget_item, mWidgetItems.get(position).text);
    
            // Next, set a fill-intent, which will be used to fill in the pending intent template
         // that is set on the collection view in StackWidgetProvider.
         Bundle extras = new Bundle();
         extras.putInt(StackWidgetProvider.EXTRA_ITEM, position);
         Intent fillInIntent = new Intent();
         fillInIntent.putExtras(extras);
            // Make it possible to distinguish the individual on-click
            // action of a given item
           rv.setOnClickFillInIntent(R.id.widget_item, fillInIntent);
        
           ...
        
          // Return the RemoteViews object.
          return rv;
      }
      ...
}

2.3 小部件配置Activity

如果希望用户在添加新的 App Widget 时配置一些自定义的设置,可以创建一个 App Widget 配置Activity,这时创建App Widget时不会调用 onUpdate() 方法。这个Activity 将自动启动,并允许用户在创建时配置 App Widget 的一些自定义设置,例如 App Widget 颜色、大小、更新周期或其他功能设置。

清单文件中添加:

<activity android:name=".ExampleAppWidgetConfigure">
    <intent-filter>
        <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
    intent-filter>
activity>

此外,必须在 AppWidgetProviderInfo XML 文件中声明 Activity,并带有 android:configure属性:

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    ...
    android:configure="com.example.android.ExampleAppWidgetConfigure" 
    ... >
appwidget-provider>

当 App Widget 使用配置 Activity 时,Activity 负责在配置完成后更新 App Widget:

Intent intent = getIntent();

Bundle extras = intent.getExtras();

if (extras != null) {

    mAppWidgetId = extras.getInt(

            AppWidgetManager.EXTRA_APPWIDGET_ID,

            AppWidgetManager.INVALID_APPWIDGET_ID);

}

AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);

RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.example_appwidget);

appWidgetManager.updateAppWidget(mAppWidgetId, views);

Intent resultValue = new Intent();

resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId);

setResult(RESULT_OK, resultValue);

finish();

【注意】当你的配置Activity打开时,将Activity结果设置为RESULT_CANCELED。这样,如果用户在到达结束之前退出 Activity,则通知 App Widget 配置已取消,并且不会添加 App Widget。

完毕

今天的分享就到这里,文章多有不足,各位小伙伴有什么想法可以直接评论或是私信,要是对你有所帮助就给我一个赞吧,喜欢我的小伙伴可以关注我哦~

支持我的小伙伴们可以微信搜索“Android思维库”,或者微信扫描下方二维码,关注我的公众号,每天都会推送新知识~

你可能感兴趣的:(Android学习历程,android)