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


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


  在之前的一篇博文(Android 之窗口小部件詳解--App Widget)中,已經介紹了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.javaAppWidgetProvider的繼承類,而且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.javaRemoteViewsService的繼承類


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博客索引

實例效果圖:

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM