App Widgets 详解四 RemoteViews、RemoteViewsService和RemoteViewsFactory

导读

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

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

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值