• Android 之窗口小部件高级篇--App Widget 之 RemoteViews


      在之前的一篇博文( Android 之窗口小部件详解--App Widge t)中,已经介绍了App Widget的基本用法和简单实例。这篇主要讲解 App Widget 的高级内容,即通过 RemoteViews 去管理Widget的中GridView、ListView、StackView等内容。在学习本篇之前,建议读者先掌握 App Widget 的基本知识。

    1 RemoteViews等相关类的介绍

    下面先简单介绍RemoteViews、RemoteViewsService、RemoteViewsFactory。

    1.1 RemoteViews

        顾名思义,它是一个远程视图。App Widget中的视图,都是通过RemoteViews表现的。 
        在RemoteViews的构造函数中,通过传入layout文件的id来获取 “layout文件对应的视图(RemoteViews)”;然后,调用RemoteViews中的方法能对layout中的组件进行设置(例如,可以调用setTextViewText()来设置TextView组件的文本,可以调用setOnClickPendingIntent() 来设置Button的点击响应事件)。

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

    1.2 RemoteViewsService

        RemoteViewsService,是管理RemoteViews的服务。 
        一般,当App Widget 中包含“GridView、ListView、StackView等”集合视图时,才需要使用RemoteViewsService来进行更新、管理。(集合视图是指GridView、ListView、StackView等包含子元素的视图) 
        RemoteViewsService更新“集合视图”的一般步骤是: 
    (01) 通过setRemoteAdapter来设置 “RemoteViews对应RemoteViewsService”。 
    (02) 之后在RemoteViewsService中,实现RemoteViewsFactory接口。然后,在RemoteViewsFactory接口中对“集合视图”的各个子项进行设置(“集合视图”的各个子项:例如,GridView的每一个格子都是一个子项;ListView中的每一列也是一个子项)。

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

    1.3 RemoteViewsFactory

        通过RemoteViewsService中的介绍,我们可以了解“RemoteViewsService是通过RemoteViewsFactory来具体管理layout中集合视图的”,即“RemoteViewsFactory管理集合视图的实施者”。 
        RemoteViewsFactory是RemoteViewsService中的一个接口。RemoteViewsFactory提供了一系列的方法管理“集合视图”中的每一项。例如: 
    (01)RemoteViews getViewAt(int position) 
          通过getViewAt()来获取“集合视图”中的第position项的视图,视图是以RemoteViews的对象返回的。 
    (02)int getCount() 
          通过getCount()来获取“集合视图”中所有子项的总数。

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

    2 实例介绍

        实现一个App Widget,App Widget可缩放,且包含“3个组成部分”。 
    第1部分:是一个TextView文本,标题内容是“Sky Wang”。 
    第2部分:是一个Button按钮。点击按钮,会弹出一个Toast提示框,提示响应了Button点击事件。 
    第3部分:是一个GridView视图。GridView的每一个格子包含“图片”和“文本”两部分数据。点击GridView中的每一个格子,会弹出响应的提示语。

    manifest代码如下 :

    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.skywang.test"
        android:versionCode="1"
        android:versionName="1.0" >
    
        <uses-sdk
            android:minSdkVersion="14"
            android:targetSdkVersion="17" />
    
        <application
            android:allowBackup="true"
            android:icon="@drawable/ic_launcher"
            android:label="@string/app_name"
            android:theme="@style/AppTheme" >
            
            <receiver android:name=".GridWidgetProvider">
                <intent-filter>                
                    <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
                    
                    <!-- GridWidgetProvider接收点击gridview的响应事件 -->
                    <action android:name="com.skywang.test.COLLECTION_VIEW_ACTION" />
                    <!-- GridWidgetProvider接收点击bt_refresh的响应事件 -->
                    <action android:name="com.skywang.test.BT_REFRESH_ACTION" />
                </intent-filter>
                <meta-data android:name="android.appwidget.provider"
                    android:resource="@xml/widget_provider"/>
            </receiver>        
            
            <service   
                android:name=".GridWidgetService"  
                android:permission="android.permission.BIND_REMOTEVIEWS" />  
                
        </application>
    
    </manifest>

    说明: 
    (01) GridWidgetProvider.java 是 AppWidgetProvider的继承类 ,而且AppWidgetProvider的配置文件是 widget_provider.xml 。 
    (02) GridWidgetProvider.java 除了 响应 App Widget 的更新事件 (android.appwidget.action.APPWIDGET_UPDATE)之外; 
           也会“ 响应App Widget包含的GridView的点击事件 (com.skywang.test.COLLECTION_VIEW_ACTION) ”  和“ App Widget包含的按钮的点击事件 (com.skywang.test.BT_REFRESH_ACTION) ”。 
    (03) GridWidgetService.java 是 RemoteViewsService的继承类 。

    widget_provider.xml代码如下 : 
    widget_provider.xml是AppWidgetProvider对应配置文件

    <?xml version="1.0" encoding="utf-8"?>
    <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
        android:minWidth="180dp"
        android:minHeight="180dp"
        android:previewImage="@drawable/preview"
        android:initialLayout="@layout/widget_layout"
        android:resizeMode="horizontal|vertical"
        android:widgetCategory="home_screen"> 
        
    </appwidget-provider>

    说明: 
    (01) android:minWidth="180dp"                                     表明 " Widget支持的最小宽度是3格 " 
    (02) android:minHeight="180dp"                                   表明 " Widget支持的最小高度是3格 " 
    (03) android:initialLayout="@layout/widget_layout" 表明 " Widget对应的布局文件是widget_layout.xml " 
    (04) android:previewImage="@drawable/preview"     表明 " Widget对应的预览图片是preview.png " 
    (05) android:resizeMode="horizontal|vertical"           表明 " Widget支持水平和竖直伸缩 " 
    (06) android:widgetCategory="home_screen"            表明 " Widget只能添加到桌面上,而不能添加到锁屏界面上 "

    widget_layout.xml代码如下 : 
    widget_layout.xml是App Widget的布局文件

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical" >
    
        
        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="8dip"
            android:layout_marginRight="8dip"
            >
            
            <TextView
                android:id="@+id/tv_head"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentLeft="true"
                android:layout_alignParentTop="true"
                android:gravity="left|center_vertical"
                android:textSize="24sp"
                android:text="SkyWang" />
            
            <Button
                android:id="@+id/bt_refresh"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentTop="true"
                android:layout_alignParentRight="true"
                android:gravity="right|center_vertical"
                android:textSize="18sp"
                android:text="Refresh" />
            
        </RelativeLayout>            
            
        <GridView
            android:id="@+id/gridview"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:numColumns="auto_fit"
            android:verticalSpacing="4dip"
            android:horizontalSpacing="4dip"
            android:columnWidth="80dip"
            android:gravity="center" />        
    
    </LinearLayout> 

    GridWidgetProvider.java代码如下 : 
    GridWidgetProvider.java是AppWidgetProvider的继承类

    package com.skywang.test;
    
    import android.app.PendingIntent;
    import android.appwidget.AppWidgetManager;
    import android.appwidget.AppWidgetProvider;
    import android.content.ComponentName;
    import android.content.Context;
    import android.content.Intent;
    import android.util.Log;
    import android.widget.RemoteViews;
    import android.widget.Toast;
    
    import com.skywang.test.R;
    
    /**
     * @desc App Widget高级功能测试程序
     * @author skywang *
     */
    public class GridWidgetProvider extends AppWidgetProvider {
        
        private static final String TAG = "SKYWANG";
    
        public static final String BT_REFRESH_ACTION = "com.skywang.test.BT_REFRESH_ACTION";
        public static final String COLLECTION_VIEW_ACTION = "com.skywang.test.COLLECTION_VIEW_ACTION";
        public static final String COLLECTION_VIEW_EXTRA = "com.skywang.test.COLLECTION_VIEW_EXTRA";
        
        @Override  
        public void onUpdate(Context context, AppWidgetManager appWidgetManager,  
                int[] appWidgetIds) {  
    
            Log.d(TAG, "GridWidgetProvider onUpdate");
            for (int appWidgetId:appWidgetIds) {
                // 获取AppWidget对应的视图
                RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.widget_layout);
                
                // 设置响应 “按钮(bt_refresh)” 的intent
                Intent btIntent = new Intent().setAction(BT_REFRESH_ACTION);
                PendingIntent btPendingIntent = PendingIntent.getBroadcast(context, 0, btIntent, PendingIntent.FLAG_UPDATE_CURRENT);
                rv.setOnClickPendingIntent(R.id.bt_refresh, btPendingIntent);            
                
                // 设置 “GridView(gridview)” 的adapter。
                // (01) intent: 对应启动 GridWidgetService(RemoteViewsService) 的intent  
                // (02) setRemoteAdapter: 设置 gridview的适配器
                //    通过setRemoteAdapter将gridview和GridWidgetService关联起来,
                //    以达到通过 GridWidgetService 更新 gridview 的目的
                Intent serviceIntent = new Intent(context, GridWidgetService.class);        
                rv.setRemoteAdapter(R.id.gridview, serviceIntent);            
                
                
                // 设置响应 “GridView(gridview)” 的intent模板            
                // 说明:“集合控件(如GridView、ListView、StackView等)”中包含很多子元素,如GridView包含很多格子。
                //     它们不能像普通的按钮一样通过 setOnClickPendingIntent 设置点击事件,必须先通过两步。
                //        (01) 通过 setPendingIntentTemplate 设置 “intent模板”,这是比不可少的!
                //        (02) 然后在处理该“集合控件”的RemoteViewsFactory类的getViewAt()接口中 通过 setOnClickFillInIntent 设置“集合控件的某一项的数据”
                Intent gridIntent = new Intent();
                gridIntent.setAction(COLLECTION_VIEW_ACTION);
                gridIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
                PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, gridIntent, PendingIntent.FLAG_UPDATE_CURRENT);
                // 设置intent模板
                rv.setPendingIntentTemplate(R.id.gridview, pendingIntent);
                // 调用集合管理器对集合进行更新
                appWidgetManager.updateAppWidget(appWidgetId, rv);
            }
            super.onUpdate(context, appWidgetManager, appWidgetIds);
        }
        
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();        
            AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
    
            Log.d(TAG, "GridWidgetProvider onReceive : "+intent.getAction());
            if (action.equals(COLLECTION_VIEW_ACTION)) {
                // 接受“gridview”的点击事件的广播
                int appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
                    AppWidgetManager.INVALID_APPWIDGET_ID);
                int viewIndex = intent.getIntExtra(COLLECTION_VIEW_EXTRA, 0);
                Toast.makeText(context, "Touched view " + viewIndex, Toast.LENGTH_SHORT).show();
            } else if (action.equals(BT_REFRESH_ACTION)) {
                // 接受“bt_refresh”的点击事件的广播
                Toast.makeText(context, "Click Button", Toast.LENGTH_SHORT).show();
            }
            super.onReceive(context, intent);
        }
    }

    说明: 
    (01) RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.widget_layout); 
         通过上面的语句, 获取widget_layout.xml对应的 RemoteViews ;进而通过RemoteViews对 widget_layout.xml中的各个元素进行管理。 
    (02) rv.setOnClickPendingIntent(R.id.bt_refresh, btPendingIntent); 
         通过上面的语句,设置 点击“按钮(bt_refresh)”时会触发的Intent 。从而对按钮点击事件进行处理。 
    (03) rv.setRemoteAdapter(R.id.gridview, serviceIntent); 
        通过上面的语句, 设置 “GridView(gridview)” 的远程适配器 ,serviceIntent是 GridWidgetService 的Intent。 
        从而在通过GridWidgetService对gridview进行管理。 
    (04) PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, gridIntent, PendingIntent.FLAG_UPDATE_CURRENT); 
        通过上面的语句,设置响应 “GridView(gridview)” 点击事件的 intent模板 。关于“Intent模板”,后面在" 关于'GridView'的点击事件 "会详细说明。 
    (05) if (action.equals(COLLECTION_VIEW_ACTION)) ... 
        通过上面的语句,响应“GridView”的点击事件。 
    (06) if (action.equals(BT_REFRESH_ACTION)) ... 
        通过上面的语句,响应“按钮(bt_refresh)”的点击事件。

    关于 “GridView”的点击事件。这里详细说明以下!

        像GridView这样的集合控件,不能单单像按钮一样,通过setOnClickPendingIntent() 来设置它的点击事件的Intent;而要通过以下两步来进行。 
        第一,设置GridView的点击响应事件的“Intent模板”。 
                  这是通过 setPendingIntentTemplate() 来进行设置的。 
                  这样做的目的有两个:首先, 设置Intent模板。 因为GridView有许多子项,它们这些子项都 统一 的要响应父亲的 Intent模板 。其次, 传递附件参数(例如,App Widget的ID) ,因为App Widget可以设置许多widget,每一个Widget的ID都不同,而且它们显示的内容可能不同(例如,不同大小的Widget显示不同大小的文字)。 
        第二, 设置GridView子项的点击响应事件的Intent。 
                  设置通过 setOnClickFillInIntent() 来进行设置的。 
                  这样做的首要目的,是 设置 GridView的子项所包含的信息 (例如,点击的 GridView子项的索引值 )。 
                  通过这两步的设置之后,点击GridView中具体每一项的所产生的事件就是 “第一”和“第二”步中Intent的合集(组合) 。也就是说, 点击“GridView中某一个子项”所产生的Intent,同时包含了“通过 setPendingIntentTemplate 传递的Intent数据”和“通过 setOnClickFillInIntent 传递的Intent数据” 。 理解这一点对理解这个RemoteView的原理至关重要!!!

    GridWidgetService.java的代码如下:

    package com.skywang.test;
    
    import android.app.PendingIntent;
    import android.appwidget.AppWidgetManager;
    import android.content.Context;
    import android.content.Intent;
    import android.widget.RemoteViews;
    import android.widget.RemoteViewsService;
    import android.util.Log;
    
    import java.util.List;
    import java.util.ArrayList;
    import java.util.HashMap;
    
    public class GridWidgetService extends RemoteViewsService{
    
        private static final String TAG = "SKYWANG";
        @Override
        public RemoteViewsService.RemoteViewsFactory onGetViewFactory(Intent intent) {
            Log.d(TAG, "GridWidgetService");
            return new GridRemoteViewsFactory(this, intent);
        }
        
        private class GridRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory {
    
            private Context mContext;
            private int mAppWidgetId;        
    
            private String IMAGE_ITEM = "imgage_item";
            private String TEXT_ITEM = "text_item";
            private ArrayList<HashMap<String, Object>> data ;
            
            private String[] arrText = new String[]{ 
                    "Picture 1", "Picture 2", "Picture 3", 
                    "Picture 4", "Picture 5", "Picture 6",
                    "Picture 7", "Picture 8", "Picture 9"
                    };
            private int[] arrImages=new int[]{
                    R.drawable.p1, R.drawable.p2, R.drawable.p3, 
                    R.drawable.p4, R.drawable.p5, R.drawable.p6, 
                    R.drawable.p7, R.drawable.p8, R.drawable.p9
                    };
            
            /**
             * 构造GridRemoteViewsFactory
             * @author skywang
             */
            public GridRemoteViewsFactory(Context context, Intent intent) {
                mContext = context;
                mAppWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
                        AppWidgetManager.INVALID_APPWIDGET_ID);
                Log.d(TAG, "GridRemoteViewsFactory mAppWidgetId:"+mAppWidgetId);
            }
            
            @Override
            public RemoteViews getViewAt(int position) {
                HashMap<String, Object> map; 
    
                Log.d(TAG, "GridRemoteViewsFactory getViewAt:"+position);
                // 获取 grid_view_item.xml 对应的RemoteViews
                RemoteViews rv = new RemoteViews(mContext.getPackageName(), R.layout.grid_view_item);
                
                // 设置 第position位的“视图”的数据
                map = (HashMap<String, Object>) data.get(position);
                rv.setImageViewResource(R.id.itemImage, ((Integer)map.get(IMAGE_ITEM)).intValue());
                rv.setTextViewText(R.id.itemText, (String)map.get(TEXT_ITEM));
    
                // 设置 第position位的“视图”对应的响应事件
                Intent fillInIntent = new Intent();
                fillInIntent.putExtra(GridWidgetProvider.COLLECTION_VIEW_EXTRA, position);
                rv.setOnClickFillInIntent(R.id.itemLayout, fillInIntent);
                
                return rv;
            }        
    
            /**
             * 初始化GridView的数据
             * @author skywang
             */
            private void initGridViewData() {
                data = new ArrayList<HashMap<String, Object>>();
                
                for (int i=0; i<9; i++) {
                    HashMap<String, Object> map = new HashMap<String, Object>(); 
                    map.put(IMAGE_ITEM, arrImages[i]);
                    map.put(TEXT_ITEM, arrText[i]);
                    data.add(map);
                }
            }
            
            @Override
            public void onCreate() {
                Log.d(TAG, "onCreate");
                // 初始化“集合视图”中的数据
                initGridViewData();
            }
            
            @Override
            public int getCount() {
                // 返回“集合视图”中的数据的总数
                return data.size();
            }
            
            @Override
            public long getItemId(int position) {
                // 返回当前项在“集合视图”中的位置
                return position;
            }
    
            @Override
            public RemoteViews getLoadingView() {
                return null;
            }
            
            @Override
            public int getViewTypeCount() {
                // 只有一类 GridView
                return 1;
            }
    
            @Override
            public boolean hasStableIds() {
                return true;
            }        
    
            @Override
            public void onDataSetChanged() {            
            }
            
            @Override
            public void onDestroy() {
                data.clear();
            }
        }
    }

    说明: 
    (01) public RemoteViewsService.RemoteViewsFactory onGetViewFactory(Intent intent) ... 
           通过上面的语句,返回一个RemoteViewsFactory对象。 
           RemoteViewsService是一个服务,通过之前对RemoteViewsService的介绍。我们知道,它 管理layout中集合视图的服务。 
           RemoteViewsService管理layout中集合视图的服务,是通过 RemoteViewsFactory 实现的;那么它是如何实现的呢? 
           RemoteViewsService是“通过 onGetViewFactory() 接口返回一个 RemoteViewsFactory 对象” 来实现的。 
    (02) public void onCreate() ... 
           在onCreate()中进行 RemoteViewsFactory 的初始化工作 。 
    (03) public int getCount() ... 
           通过getCount()返回 “集合视图”中的数据项的总数。 
    (04) public RemoteViews getViewAt(int position) ... 
           通过上面的语句,返回 一个“集合视图”中具体每一项的视图。 
           getViewAt()是非常重要的函数! 我们对"集合视图"中每一项的初始化都是在getViewAt()中进行设置的 。 

    grid_view_item.xml的代码如下:

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/itemLayout"
        android:layout_height="match_parent" 
        android:layout_width="match_parent">
        
        <ImageView
            android:id="@+id/itemImage"
            android:layout_width="80dip"
            android:layout_height="60dip"
            android:layout_centerHorizontal="true"
            android:scaleType="fitXY" />
        
        <TextView 
            android:id="@+id/itemText" 
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" 
            android:layout_below="@+id/itemImage"
            android:layout_centerHorizontal="true" 
            android:text="TextView01"/>
        
    </RelativeLayout>

    点击下载: 源代码

    点击查看更多内容: 
    1. Android 之窗口小部件详解--App Widget 
    2. sky wang博客索引

    实例效果图:

  • 相关阅读:
    iOS启动项目(二)引入第三方库
    Swift技巧(九)CGImage To CVPixelBuffer
    Swift技巧(四)设置照片尺寸和格式
    Swift技巧(十) Protocol 的灵活使用
    Alamofire5.0.0 以上报错
    Swift技巧(八)CVPixelBuffer To CGImage
    Swift技巧(十一)重写运算符
    Swift技巧(五)设置圆角的代码
    Swift技巧(七)重识 Array
    Swift技巧(六)设置按钮状态并更改
  • 原文地址:https://www.cnblogs.com/Free-Thinker/p/4422415.html
Copyright © 2020-2023  润新知