欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

App Widgets 详解四 RemoteViews、RemoteViewsService和RemoteViewsFactory

程序员文章站 2022-05-30 20:34:11
...

导读

本篇文章将介绍”集合视图”,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;
    }
}

效果图

App Widgets 详解四 RemoteViews、RemoteViewsService和RemoteViewsFactory

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

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

App Widgets 详解四 RemoteViews、RemoteViewsService和RemoteViewsFactory

总结

本系列Demo源码

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