App Widgets 详解四 RemoteViews、RemoteViewsService和RemoteViewsFactory

来源:互联网 发布:记忆枕 知乎 编辑:程序博客网 时间:2024/06/16 20:37

导读

本篇文章将介绍”集合视图”,App Widget 复杂布局的实现

  • App Widget 小部件系列其他文章链接

App Widgets 详解一 简单使用

App Widgets 详解二 Configuration Activity

App Widgets 详解三 Activity中添加App Widgets

App Widgets 详解四 RemoteViews、RemoteViewsService和RemoteViewsFactory

RemoteViews、RemoteViewsService和RemoteViewsFactory 简介

RemoteViews 构造函数

远程视图,App Widget中的视图,都是通过RemoteViews实现.

在RemoteViews的构造函数中,通过传入R.layout.XX(AppWidgets 的XML布局文件),拿到该Layout中的所有View视图;

再通过RemoteViews.setTextView()、RemoteViews.setOnClickPendingIntent()等方法设置对应组件的响应事件

因此,我们可以将 “RemoteViews 看作是 App Widgets layout文件中所包含的全部视图的集合”.

RemoteViews 官方文档

==注意==

由于Widget的布局需要RemoteViews支持,因此不能随便定义或自定义view(可尝试是重写remoteViews)**

支持的布局:

  • FrameLayout
  • LinearLayout
  • RelativeLayout
  • GridLayout

支持的控件:

  • AnalogClock
  • Button
  • Chronometer
  • ImageButton
  • ImageView
  • ProgressBar
  • TextView
  • ViewFlipper
  • ListView
  • GridView
  • StackView
  • AdapterViewFlipper

其中 ViewFlipper,ListView,GridView,StackView,AdapterViewFlipper 等包含子元素的视图,属于”集合视图”

RemoteViewsService 类

在AppWidgetProvider类中,RemoteViewsService作为一个接口适配器Service,用于实现RemoteViews对象中的集合视图

RemoteViewsService更新”集合视图”的一般步骤是:

  1. 通过RemoteViews.setRemoteAdapter(R.id.ListView_ID,Service 的 intent)来设置 “RemoteViews对应RemoteViewsService”

  2. 在RemoteViewsService中,实现RemoteViews对应RemoteViewsService.RemoteViewsFactory接口.

  3. 在RemoteViewsFactory接口中对”集合视图”的各个需要实现的方法进行设置

因此,我们可以将 RemoteViewsService 看作是 “管理layout中集合视图的服务”.

RemoteViewsService 官方文档

RemoteViewsFactory 接口

RemoteViewsService.RemoteViewsFactory是RemoteViewsService的子类,用于管理RemoteViews远程集合视图(GridView、ListView、StackView、AdapterViewFlipper等)

该接口类似ListView 的 BaseAdapter 用于将View与数据绑定并显示,其中比较重要的两个方法是onCreate()和getViewAt(int position)

  • onCreat() : 用于初始化数据,首次创建Factory时被调用
  • getViewAt(int position) : 获取”集合视图”中的第position项的视图,返回RemoteViews()

因此,我们可以将 “RemoteViewsFactory 看作是 layout中集合视图管理的具体实施者”.

RemoteViewsFactory 官方文档

注意:我们不能在Service 或单例中持久化数据.因此,我们不应该在RemoteViewsService中存储任何数据(除非它是静态的).如果希望AppWidget的数据持续存在,最好的方法是使用ContentProvider

“集合视图” 开发说明(ListView为例):

一、在清单文件配置service节点和receiver节点

<!--MyRemoteService-->        <service            android:name=".remote.MyRemoteService"            android:exported="false"            android:permission="android.permission.BIND_REMOTEVIEWS">        </service>        <!--MyRemoteAppWidget-->        <receiver android:name=".remote.MyRemoteAppWidget">            <intent-filter>                <!--指定AppWidgetProvider接受系统的APPWIDGET_UPDATE广播-->                <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>            </intent-filter>            <!--指定Meta_data名称,使用android.appwidgetb必须确定AppWidgetProviderInfo描述符的数据-->            <!--指定AppWidgetProviderInfo资源XML文件-->            <meta-data                android:name="android.appwidget.provider"                android:resource="@xml/my_remote_widget_info"/>        </receiver>

二、创建AppWidgetProviderInfo XML文件

该XML文件定义 App Widget 的基本属性,在res/xml/目录下创建appwidger-provider 标签的XML文件

<?xml version="1.0" encoding="utf-8"?><appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"                    android:initialKeyguardLayout="@layout/my_remote_widget"                    android:initialLayout="@layout/my_remote_widget"                    android:minHeight="50dp"                    android:minWidth="50dp"                    android:previewImage="@mipmap/ic_launcher"                    android:resizeMode="horizontal|vertical"                    android:updatePeriodMillis="86400000"                    android:widgetCategory="home_screen|keyguard"></appwidget-provider>

三、定义 AppWidgetProvider 类

public class MyRemoteAppWidget extends AppWidgetProvider {    static void updateAppWidget(Context context, AppWidgetManager appWidgetManager,                                int appWidgetId) {        // 获取Widget的组件名        ComponentName thisWidget = new ComponentName(context,                MyRemoteAppWidget.class);        // 创建一个RemoteView        RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.my_remote_widget);        // 把这个Widget绑定到RemoteViewsService        Intent intent = new Intent(context, MyRemoteService.class);        // 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.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);        intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));        //设置适配器        remoteViews.setRemoteAdapter(R.id.widget_list, intent);        //TODO 设置当显示的widget_list为空显示的View remoteViews.setEmptyView();        // 设置点击列表触发事件        Intent clickIntent = new Intent(context, MyRemoteAppWidget.class);        // Set the action for the intent.        // When the user touches a particular view, it will have the effect of        // broadcasting TOAST_ACTION.        // 设置Action,方便在onReceive中区别点击事件        clickIntent.setAction("clickAction");        clickIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);        clickIntent.setData(Uri.parse(clickIntent.toUri(Intent.URI_INTENT_SCHEME)));        PendingIntent pendingIntentTemplate = PendingIntent.getBroadcast(                context, 0, clickIntent, PendingIntent.FLAG_UPDATE_CURRENT);        //使用"集合视图",如果直接setOnClickPendingIntent是不可行的,        //建议setPendingIntentTemplate和FillInIntent结合使用        //FillInIntent用于区分单个点击事件        remoteViews.setPendingIntentTemplate(R.id.widget_list,                pendingIntentTemplate);        // 刷新按钮        final Intent refreshIntent = new Intent(context,                MyRemoteAppWidget.class);        refreshIntent.setAction("refresh");        final PendingIntent refreshPendingIntent = PendingIntent.getBroadcast(                context, 0, refreshIntent, PendingIntent.FLAG_UPDATE_CURRENT);        remoteViews.setOnClickPendingIntent(R.id.button_refresh,                refreshPendingIntent);        // 更新Widget        appWidgetManager.updateAppWidget(appWidgetId, remoteViews);    }    @Override    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {        // There may be multiple widgets active, so update all of them        for (int appWidgetId : appWidgetIds) {            updateAppWidget(context, appWidgetManager, appWidgetId);        }    }    @Override    public void onEnabled(Context context) {        // Enter relevant functionality for when the first widget is created        Toast.makeText(context, "用户将widget添加桌面了",                Toast.LENGTH_SHORT).show();    }    @Override    public void onDisabled(Context context) {        // Enter relevant functionality for when the last widget is disabled    }    @Override    public void onDeleted(Context context, int[] appWidgetIds) {        Toast.makeText(context, "用户将widget从桌面移除了",                Toast.LENGTH_SHORT).show();        super.onDeleted(context, appWidgetIds);    }    /**     * 接受Intent     *     * @param context     * @param intent     */    @Override    public void onReceive(Context context, Intent intent) {        super.onReceive(context, intent);        String action = intent.getAction();        if (action.equals("refresh")) {            int i = 0;            // 刷新Widget            final AppWidgetManager mgr = AppWidgetManager.getInstance(context);            final ComponentName cn = new ComponentName(context,                    MyRemoteAppWidget.class);            MyRemoteViewsFactory.mList.add("音乐" + i);            i=i+1;            // 这句话会调用RemoteViewSerivce中RemoteViewsFactory的onDataSetChanged()方法。            mgr.notifyAppWidgetViewDataChanged(mgr.getAppWidgetIds(cn),                    R.id.widget_list);        } else if (action.equals("clickAction")) {            // 单击Wdiget中ListView的某一项会显示一个Toast提示。            Toast.makeText(context, intent.getStringExtra("content"),                    Toast.LENGTH_SHORT).show();        }    }}

==注意==

  1. RemoteViews.setEmptyView() 设置空视图必须是集合视图的兄弟节点,空视图表示空状态 (没数据时设置空视图??)
  2. 当我们使用集合视图,如LIstView,除了创建AppWidgets的XML布局,还需要创建list item 的XML布局

四、配置 AppWidgets 和 List_ltem 的XML布局文件

my_remote_widget.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"              android:layout_width="match_parent"              android:layout_height="wrap_content"              android:background="@android:color/white"              android:orientation="vertical" >    <Button        android:id="@+id/button_refresh"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_gravity="center_horizontal"        android:layout_marginTop="2dp"        android:text="添加" />    <ListView        android:divider="#000"        android:id="@+id/widget_list"        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:cacheColorHint="#00000000"        android:scrollbars="none" />    <!-- 此处的ListView 可以换成StackView或者GridView --></LinearLayout>

list_itlem.xml

<?xml    version="1.0" encoding="utf-8"?><RelativeLayout    xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="match_parent">    <TextView        android:id="@+id/item"        android:layout_width="fill_parent"        android:layout_height="wrap_content"        android:layout_marginBottom="5px"        android:layout_marginTop="5px"        android:gravity="center"        android:paddingBottom="25px"        android:paddingTop="5px"        android:textColor="#ff0000"        android:textSize="60px"        />    <ImageView        android:id="@+id/imageItem"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_alignParentRight="true"        android:layout_alignRight="@id/item"        android:src="@mipmap/ic_launcher_round"        /></RelativeLayout>

五、定义 RemoteViewsService 类

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

六、定义 RemoteViewsService.RemoteViewsFactory 实现类

public class MyRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory {    private final Context mContext;    public static List<String> mList = new ArrayList<>();    /*     * 构造函数     */    public MyRemoteViewsFactory(Context context, Intent intent) {        mContext = context;    }    /*     * MyRemoteViewsFactory调用时执行,这个方法执行时间超过20秒回报错。     * 如果耗时长的任务应该在onDataSetChanged或者getViewAt中处理     */    @Override    public void onCreate() {        for (int i = 0; i < 5; i++) {            mList.add("item" + i);        }    }    /*     * 当调用notifyAppWidgetViewDataChanged方法时,触发这个方法     * 例如:MyRemoteViewsFactory.notifyAppWidgetViewDataChanged();     */    @Override    public void onDataSetChanged() {    }    /*     * 这个方法不用多说了把,这里写清理资源,释放内存的操作     */    @Override    public void onDestroy() {        mList.clear();    }    /*     * 返回集合视图数量     */    @Override    public int getCount() {        return mList.size();    }    /*     * 创建并且填充,在指定索引位置显示的View,这个和BaseAdapter的getView类似     */    @Override    public RemoteViews getViewAt(int position) {        if (position < 0 || position >= mList.size())            return null;        String content = mList.get(position);        // 创建在当前索引位置要显示的View        final RemoteViews rv = new RemoteViews(mContext.getPackageName(),                R.layout.list_item);        // 设置要显示的内容        rv.setTextViewText(R.id.item, content);        // 填充Intent,填充在AppWdigetProvider中创建的PendingIntent        Intent intent = new Intent();        // 传入点击行的数据        intent.putExtra("content", content);        rv.setOnClickFillInIntent(R.id.item, intent);        return rv;    }    /*     * 显示一个"加载"View。返回null的时候将使用默认的View     */    @Override    public RemoteViews getLoadingView() {        return null;    }    /*     * 不同View定义的数量。默认为1(本人一直在使用默认值)     */    @Override    public int getViewTypeCount() {        return 1;    }    /*     * 返回当前索引的。     */    @Override    public long getItemId(int position) {        return position;    }    /*     * 如果每个项提供的ID是稳定的,即她们不会在运行时改变,就返回true(没用过。。。)     */    @Override    public boolean hasStableIds() {        return true;    }}

效果图

这里写图片描述

AppWidget “集合视图” 数据更新流程图

当widget指定其具体的AppWidgetProvider,AppWidgetProvider通过创建RemoteViews来加载视图,其RemoteViews将会调用setRemoteViewsAdapter来设置内部适配器,此适配器也将会继续获取widget管理器调用updateAppWidget()方法,此方法有会用远程视图工厂(RemoteViewsFactroy)来初始化数据并调用其onDataSetChanged()来通知适配器更新数据,具体更新那个widget的界面,是通过其GetViewAt将界面更新后并返回,其详细流程图如下:

这里写图片描述

总结

本系列Demo源码

本篇文章到此结束,欢迎关注,后续有补充的会即使更新,有问题也欢迎评论,共同成长