1. 程式人生 > >Android之AppWidget 開發淺析

Android之AppWidget 開發淺析

什麼是AppWidget

  AppWidget 即桌面小部件,也叫桌面控制元件,就是能直接顯示在Android系統桌面上的小程式,先看圖:

                

  圖中我用黃色箭頭指示的即為AppWidget,一些使用者使用比較頻繁的程式,可以做成AppWidget,這樣能方便地使用。典型的程式有時鐘、天氣、音樂播放器等。AppWidget 是Android 系統應用開發層面的一部分,有著特殊用途,使用得當的化,的確會為app 增色不少,它的工作原理是把一個程序的控制元件嵌入到別外一個程序的窗口裡的一種方法。長按桌面空白處,會出現一個 AppWidget 的資料夾,在裡面找到相應的 AppWidget ,長按拖出,即可將 AppWidget 新增到桌面,

如何開發AppWidget

  AppWidget 是通過 BroadCastReceiver 的形式進行控制的,開發 AppWidget 的主要類為 AppWidgetProvider, 該類繼承自 BroadCastReceiver。為了實現桌面小部件,開發者只要開發一個繼承自 AppWidgetProvider 的子類,並重寫它的 onUpdate() 方法即可。重寫該方法,一般來說可按如下幾個步驟進行:

  1、建立一個 RemoteViews 物件,這個物件載入時指定了桌面小部件的介面佈局檔案。

  2、設定 RemoteViews 建立時載入的佈局檔案中各個元素的屬性。

  3、建立一個 ComponentName 物件

  4、呼叫 AppWidgetManager 更新桌面小部件。

下面來看一個實際的例子,用 Android Studio 自動生成的例子來說。(注:我用的是最新版的 AS 2.2.3,下面簡稱 AS。)

  新建了一個 HelloWorld 專案,然後新建一個 AppWidget ,命名為 MyAppWidgetProvider,按預設下一步,就完成了一個最簡單的AppWidget的開發。執行程式之後,將小部件新增到桌面。操作步驟和預設效果如下:

    

  我們看看 AS 為我們自動生成了哪些程式碼呢?對照著上面說的的步驟我們來看看。

  首先,有一個 MyAppWidgetProvider 的類。

複製程式碼

package com.example.joy.remoteviewstest;

import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.Context;
import android.widget.RemoteViews;

/**
 * Implementation of App Widget functionality.
 */
public class MyAppWidgetProvider extends AppWidgetProvider {

    static void updateAppWidget(Context context, AppWidgetManager appWidgetManager,
                                int appWidgetId) {

        CharSequence widgetText = context.getString(R.string.appwidget_text);  
        // Construct the RemoteViews object
        RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.my_app_widget_provider);
        views.setTextViewText(R.id.appwidget_text, widgetText);

        // Instruct the widget manager to update the widget
        appWidgetManager.updateAppWidget(appWidgetId, views);
    }

    @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
    }

    @Override
    public void onDisabled(Context context) {
        // Enter relevant functionality for when the last widget is disabled
    }
}

複製程式碼

   該類繼承自 AppWidgetProvider ,AS預設幫我們重寫 onUpdate() 方法,遍歷 appWidgetIds, 呼叫了 updateAppWidget() 方法。再看 updateAppWidget() 方法,很簡單,只有四行:

  第一行, CharSequence widgetText = context.getString(R.string.appwidget_text);

     聲明瞭一個字串;

  第二行, RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.my_app_widget_provider);

    建立了一個 RemoteViews 物件,第一個引數傳應用程式包名,第二個引數指定了,RemoteViews 載入的佈局檔案。這一行對應上面步驟中說的第一點。可以看到在 res/layout/ 目錄下面 AS 自動生成了一個 my_app_widget_provider.xml 檔案,內容如下:

複製程式碼

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#09C"
    android:padding="@dimen/widget_margin">

    <TextView
        android:id="@+id/appwidget_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true"
        android:layout_margin="8dp"
        android:background="#09C"
        android:contentDescription="@string/appwidget_text"
        android:text="@string/appwidget_text"
        android:textColor="#ffffff"
        android:textStyle="bold|italic" />

</RelativeLayout>

複製程式碼

    這個檔案就是我們最後看到的桌面小部件的樣子,佈局檔案中只有一個TextView。這是你可能會問,想要加圖片可以嗎?可以,就像正常的Activity佈局一樣新增 ImageView 就行了,聰明的你可能開始想自定義小部件的樣式了,新增功能強大外觀漂亮逼格高的自定義控制元件了,很遺憾,不可以。小部件佈局檔案可以新增的元件是有限制的,詳細內容在下文介紹RemoteViews 時再說。

  第三行, views.setTextViewText(R.id.appwidget_text, widgetText);

    將第一行宣告的字串賦值給上面佈局檔案中的 TextView,注意這裡賦值時,指定TextView的 id,要對應起來。這一行對於了上面步驟中的第二點。

  第四行,  appWidgetManager.updateAppWidget(appWidgetId, views);

    這裡呼叫了 appWidgetManager.updateAppWidget() 方法,更新小部件。這一行對應了上面步驟中的第四點。

  這時,你可能有疑問了,上面明明說了四個步驟,其中第三步,建立一個 ComponentName 物件,明明就不需要。的確,這個例子中也沒有用到。如果我們手敲第四步程式碼,AS的智慧提示會告訴你,appWidgetManager.updateAppWidget() 有三個過載的方法。原始碼中三個方法沒有寫在一起,為了方便,這裡我複製貼出官方 API 中的介紹

 void 

          Set the RemoteViews to use for all AppWidget instances for the supplied AppWidget provider.

   void

          Set the RemoteViews to use for the specified appWidgetIds.

   void

          Set the RemoteViews to use for the specified appWidgetId.

   這個三個方法都接收兩個引數,第二個引數都是 RemoteViews 物件。其中第一個方法的第一個引數就是 ComponentName 物件,更新所有的 AppWidgetProvider 提供的所有的 AppWidget 例項,第二個方法時更新明確指定 Id 的 AppWidget 的物件集,第三個方法,更新明確指定 Id 的某個 AppWidget 物件。所以一般我們使用第一個方法,針對所有的 AppWidget 物件,我們也可以根據需要選擇性地去更新。

  到這裡,所有步驟都結束了,就完了?還沒。前面說了,自定義的 MyAppWidgetProvider 繼承自 AppWidgetProvider,而 AppWidgetProvider 又是繼承自 BroadCastReceiver,

所以說 MyAppWidgetProvider 本質上是一個廣播接受者,屬於四大元件之一,需要我們的清單檔案中註冊。開啟AndroidManifest.xml檔案可以看到,的確是註冊了小部件的,內容如下:

複製程式碼

 <receiver android:name=".MyAppWidgetProvider">
     <intent-filter>
         <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
      </intent-filter>

      <meta-data
            android:name="android.appwidget.provider"
            android:resource="@xml/my_app_widget_provider_info" />
  </receiver>

複製程式碼

  上面程式碼中有一個 Action,這個 Action 必須要加,且不能更改,屬於系統規範,是作為小部件的標識而存在的。如果不加,這個 Receiver 就不會出現在小部件列表裡面。然後看到小部件指定了 @xml/my_app_widget_provider_info 作為meta-data,細心的你發現了,在 res/ 目錄下面建立了一個 xml 資料夾,下面新建了一個 my_app_widget_provider_info.xml 檔案,內容如下:

複製程式碼

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

複製程式碼

  這裡配置了一些小部件的基本資訊,常用的屬性有 initialLayout 就是小部件的初始化佈局, minHeight 定義了小部件的最小高度,previewImage 指定了小部件在小部件列表裡的預覽圖,updatePeriodMillis 指定了小部件更新週期,單位為毫秒。更多屬性,可以檢視API文件。

  到這裡,上面這個極簡單的小部件開發過程就真的結束了。為了開發出更強大一點小部件,我們還需要進一步瞭解 RemoteViews 和 AppWidgetProvider。

AppWidget的妝容——RemoteViews

  下面簡單說說 RemoteViews 相關的幾個類。

  1.1 RemoteViews

  RemoteViews,從字面意思理解為它是一個遠端檢視。是一種遠端的 View,它在其它程序中顯示,卻可以在另一個程序中更新。RemoteViews 在Android中的使用場景主要有:自定義通知欄和桌面小部件。

  在RemoteViews 的建構函式中,第二個引數接收一個 layout 檔案來確定 RemoteViews 的檢視;然後,我們呼叫RemoteViews 中的 set 方法對 layout 中的各個元件進行設定,例如,可以呼叫 setTextViewText() 來設定 TextView 元件的文字。

  前面提到,小部件佈局檔案可以新增的元件是有限制的,它可以支援的 View 型別包括四種佈局:FrameLayout、LinearLayout、RelativeLayout、GridLayout 和 13 種View: AnalogClock、Button、Chronometer、ImageButton、ImageView、ProgressBar、TextView、ViewFlipper、ListView、GridView、StackView、AdapterViewFlipper、ViewSub。注意:RemoteViews 也並不支援上述 View 的子類。

  RemoteViews 提供了一系列 setXXX() 方法來為小部件的子檢視設定屬性。具體可以參考 API 文件。

  1.2 RemoteViewsService

  RemoteViewsService,是管理RemoteViews的服務。一般,當AppWidget 中包含 GridView、ListView、StackView 等集合檢視時,才需要使用RemoteViewsService來進行更新、管理。RemoteViewsService 更新集合檢視的一般步驟是:

  (01) 通過 setRemoteAdapter() 方法來設定 RemoteViews 對應 RemoteViewsService 。

  (02) 之後在 RemoteViewsService 中,實現 RemoteViewsFactory 介面。然後,在 RemoteViewsFactory 介面中對集合檢視的各個子項進行設定,例如 ListView 中的每一Item。

  1.3 RemoteViewsFactory

  通過RemoteViewsService中的介紹,我們知道RemoteViewsService是通過 RemoteViewsFactory來具體管理layout中集合檢視的,RemoteViewsFactory是RemoteViewsService中的一個內部介面。RemoteViewsFactory提供了一系列的方法管理集合檢視中的每一項。例如:

  RemoteViews getViewAt(int position)

  通過getViewAt()來獲取“集合檢視”中的第position項的檢視,檢視是以RemoteViews的物件返回的。

  int getCount()

  通過getCount()來獲取“集合檢視”中所有子項的總數。

AppWidget的美貌——AppWidgetProvider

  我們說一位女同事漂亮,除了因為她穿的衣服、化的妝漂亮以外,我想最主要的原因還是她本人長的漂亮吧。同樣,小部件之所以有附著在桌面,跨程序更新 View 的能力,主要是因為AppWidgetProvider 是一個廣播接收者。我們發現,上面的例子中,AS 幫我們自動生成的程式碼中,除了 onUpdate() 方法被我們重寫了,還有重寫 onEnable() 和 onDisable() 兩個方法,但都是空實現,這兩個方法什麼時候會被呼叫?還有,我們說自定義的 MyAppWidgetProvider,繼承自 AppWidgetProvider,而 MyAppWidgetProvider 又是BroadCastReceiver 的子類,而我們卻沒有向寫常規廣播接收者一樣重寫 onReceiver() 方法?下面跟進去 AppWidgetProvider 原始碼,一探究竟。

  這個類程式碼並不多,其實,AppWidgetProvider 出去構造方法外,總共只有下面這些方法:

  onEnable() :當小部件第一次被新增到桌面時回撥該方法,可新增多次,但只在第一次呼叫。對用廣播的 Action 為 ACTION_APPWIDGET_ENABLE。

  onUpdate():  當小部件被新增時或者每次小部件更新時都會呼叫一次該方法,配置檔案中配置小部件的更新週期 updatePeriodMillis,每次更新都會呼叫。對應廣播 Action 為:ACTION_APPWIDGET_UPDATE 和 ACTION_APPWIDGET_RESTORED 。

  onDisabled(): 當最後一個該型別的小部件從桌面移除時呼叫,對應的廣播的 Action 為 ACTION_APPWIDGET_DISABLED。

  onDeleted(): 每刪除一個小部件就呼叫一次。對應的廣播的 Action 為: ACTION_APPWIDGET_DELETED 。

  onRestored(): 當小部件從備份中還原,或者恢復設定的時候,會呼叫,實際用的比較少。對應廣播的 Action 為 ACTION_APPWIDGET_RESTORED。

  onAppWidgetOptionsChanged(): 當小部件佈局發生更改的時候呼叫。對應廣播的 Action 為 ACTION_APPWIDGET_OPTIONS_CHANGED。

  最後就是 onReceive() 方法了,AppWidgetProvider 重寫了該方法,用於分發具體的時間給上述的方法。看看原始碼:

複製程式碼

public void onReceive(Context context, Intent intent) {
        // Protect against rogue update broadcasts (not really a security issue,
        // just filter bad broacasts out so subclasses are less likely to crash).
        String action = intent.getAction();
        if (AppWidgetManager.ACTION_APPWIDGET_UPDATE.equals(action)) {
            Bundle extras = intent.getExtras();
            if (extras != null) {
                int[] appWidgetIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_IDS);
                if (appWidgetIds != null && appWidgetIds.length > 0) {
                    this.onUpdate(context, AppWidgetManager.getInstance(context), appWidgetIds);
                }
            }
        } else if (AppWidgetManager.ACTION_APPWIDGET_DELETED.equals(action)) {
            Bundle extras = intent.getExtras();
            if (extras != null && extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_ID)) {
                final int appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID);
                this.onDeleted(context, new int[] { appWidgetId });
            }
        } else if (AppWidgetManager.ACTION_APPWIDGET_OPTIONS_CHANGED.equals(action)) {
            Bundle extras = intent.getExtras();
            if (extras != null && extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_ID)
                    && extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS)) {
                int appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID);
                Bundle widgetExtras = extras.getBundle(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS);
                this.onAppWidgetOptionsChanged(context, AppWidgetManager.getInstance(context),
                        appWidgetId, widgetExtras);
            }
        } else if (AppWidgetManager.ACTION_APPWIDGET_ENABLED.equals(action)) {
            this.onEnabled(context);
        } else if (AppWidgetManager.ACTION_APPWIDGET_DISABLED.equals(action)) {
            this.onDisabled(context);
        } else if (AppWidgetManager.ACTION_APPWIDGET_RESTORED.equals(action)) {
            Bundle extras = intent.getExtras();
            if (extras != null) {
                int[] oldIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_OLD_IDS);
                int[] newIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_IDS);
                if (oldIds != null && oldIds.length > 0) {
                    this.onRestored(context, oldIds, newIds);
                    this.onUpdate(context, AppWidgetManager.getInstance(context), newIds);
                }
            }
        }
    }

複製程式碼

AppWidget 練習

  下面再自己寫個例子,學習 RemoteViews 中的其它知識點,這個例子中小部件佈局中用到 button 和 listview。上程式碼:

  小部件的佈局檔案 mul_app_widget_provider.xml 如下:

複製程式碼

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <LinearLayout
        android:layout_width="100dp"
        android:layout_height="200dp"
        android:orientation="vertical">
        <ImageView
            android:id="@+id/iv_test"
            android:layout_width="match_parent"
            android:layout_height="100dp"
            android:src="@mipmap/ic_launcher"/>
        <Button
            android:id="@+id/btn_test"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="點選跳轉"/>
    </LinearLayout>

    <TextView
        android:layout_width="1dp"
        android:layout_height="200dp"
        android:layout_marginLeft="5dp"
        android:layout_marginRight="5dp"
        android:background="#f00"/>

    <ListView
        android:id="@+id/lv_test"
        android:layout_width="100dp"
        android:layout_height="200dp">
    </ListView>

</LinearLayout>

複製程式碼

  小部件的配置資訊 mul_app_widget_provider_info.xml 如下:

複製程式碼

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:initialLayout="@layout/mul_app_widget_provider"
    android:minHeight="200dp"
    android:minWidth="200dp"
    android:previewImage="@mipmap/a1"
    android:updatePeriodMillis="86400000">
</appwidget-provider>

複製程式碼

  MulAppWidgetProvider.java:

複製程式碼

package com.example.joy.remoteviewstest;

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.os.Bundle;
import android.text.TextUtils;
import android.widget.RemoteViews;
import android.widget.Toast;

public class MulAppWidgetProvider extends AppWidgetProvider {

    public static final String CHANGE_IMAGE = "com.example.joy.action.CHANGE_IMAGE";

    private RemoteViews mRemoteViews;
    private ComponentName mComponentName;

    private int[] imgs = new int[]{
            R.mipmap.a1,
            R.mipmap.b2,
            R.mipmap.c3,
            R.mipmap.d4,
            R.mipmap.e5,
            R.mipmap.f6
    };


    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        mRemoteViews = new RemoteViews(context.getPackageName(), R.layout.mul_app_widget_provider);
        mRemoteViews.setImageViewResource(R.id.iv_test, R.mipmap.ic_launcher);
        mRemoteViews.setTextViewText(R.id.btn_test, "點選跳轉到Activity");
        Intent skipIntent = new Intent(context, MainActivity.class);
        PendingIntent pi = PendingIntent.getActivity(context, 200, skipIntent, PendingIntent.FLAG_CANCEL_CURRENT);
        mRemoteViews.setOnClickPendingIntent(R.id.btn_test, pi);

        // 設定 ListView 的adapter。
        // (01) intent: 對應啟動 ListViewService(RemoteViewsService) 的intent
        // (02) setRemoteAdapter: 設定 ListView 的介面卡
        // 通過setRemoteAdapter將 ListView 和ListViewService關聯起來,
        // 以達到通過 GridWidgetService 更新 gridview 的目的
        Intent lvIntent = new Intent(context, ListViewService.class);
        mRemoteViews.setRemoteAdapter(R.id.lv_test, lvIntent);
        mRemoteViews.setEmptyView(R.id.lv_test,android.R.id.empty);

        // 設定響應 ListView 的intent模板
        // 說明:“集合控制元件(如GridView、ListView、StackView等)”中包含很多子元素,如GridView包含很多格子。
        // 它們不能像普通的按鈕一樣通過 setOnClickPendingIntent 設定點選事件,必須先通過兩步。
        // (01) 通過 setPendingIntentTemplate 設定 “intent模板”,這是比不可少的!
        // (02) 然後在處理該“集合控制元件”的RemoteViewsFactory類的getViewAt()介面中 通過 setOnClickFillInIntent 設定“集合控制元件的某一項的資料”
        
        /*
         * setPendingIntentTemplate 設定pendingIntent 模板
         * setOnClickFillInIntent   可以將fillInIntent 新增到pendingIntent中
         */
        Intent toIntent = new Intent(CHANGE_IMAGE);
        PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 200, toIntent, PendingIntent.FLAG_UPDATE_CURRENT);
        mRemoteViews.setPendingIntentTemplate(R.id.lv_test, pendingIntent);


        mComponentName = new ComponentName(context, MulAppWidgetProvider.class);
        appWidgetManager.updateAppWidget(mComponentName, mRemoteViews);
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        super.onReceive(context, intent);
        if(TextUtils.equals(CHANGE_IMAGE,intent.getAction())){
            Bundle extras = intent.getExtras();
            int position = extras.getInt(ListViewService.INITENT_DATA);
            mRemoteViews = new RemoteViews(context.getPackageName(), R.layout.mul_app_widget_provider);
            mRemoteViews.setImageViewResource(R.id.iv_test, imgs[position]);
            mComponentName = new ComponentName(context, MulAppWidgetProvider.class);
            AppWidgetManager.getInstance(context).updateAppWidget(mComponentName, mRemoteViews);
        }
    }
}

複製程式碼

  MainActivity.java:

複製程式碼

package com.example.joy.remoteviewstest;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

複製程式碼

  下面重點是 ListView 在小部件中的用法:

複製程式碼

package com.example.joy.remoteviewstest;

import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.widget.RemoteViews;
import android.widget.RemoteViewsService;

import java.util.ArrayList;
import java.util.List;

public class ListViewService extends RemoteViewsService {
    public static final String INITENT_DATA = "extra_data";

    @Override
    public RemoteViewsFactory onGetViewFactory(Intent intent) {
        return new ListRemoteViewsFactory(this.getApplicationContext(), intent);
    }

    private class ListRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory {

        private Context mContext;

        private List<String> mList = new ArrayList<>();

        public ListRemoteViewsFactory(Context context, Intent intent) {
            mContext = context;
        }

        @Override
        public void onCreate() {
            mList.add("一");
            mList.add("二");
            mList.add("三");
            mList.add("四");
            mList.add("五");
            mList.add("六");
        }

        @Override
        public void onDataSetChanged() {

        }

        @Override
        public void onDestroy() {
            mList.clear();
        }

        @Override
        public int getCount() {
            return mList.size();
        }

        @Override
        public RemoteViews getViewAt(int position) {
            RemoteViews views = new RemoteViews(mContext.getPackageName(), android.R.layout.simple_list_item_1);
            views.setTextViewText(android.R.id.text1, "item:" + mList.get(position));

            Bundle extras = new Bundle();
            extras.putInt(ListViewService.INITENT_DATA, position);
            Intent changeIntent = new Intent();
            changeIntent.setAction(MulAppWidgetProvider.CHANGE_IMAGE);
            changeIntent.putExtras(extras);

            /* android.R.layout.simple_list_item_1 --- id --- text1
             * listview的item click:將 changeIntent 傳送,
             * changeIntent 它預設的就有action 是provider中使用 setPendingIntentTemplate 設定的action*/
            views.setOnClickFillInIntent(android.R.id.text1, changeIntent);
            return views;
        }

        /* 在更新介面的時候如果耗時就會顯示 正在載入... 的預設字樣,但是你可以更改這個介面
         * 如果返回null 顯示預設介面
         * 否則 載入自定義的,返回RemoteViews
         */
        @Override
        public RemoteViews getLoadingView() {
            return null;
        }

        @Override
        public int getViewTypeCount() {
            return 1;
        }

        @Override
        public long getItemId(int position) {
            return position;
        }

        @Override
        public boolean hasStableIds() {
            return false;
        }
    }
}

複製程式碼

  最後看看清單檔案:

複製程式碼

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.joy.remoteviewstest">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <receiver android:name=".MulAppWidgetProvider"
            android:label="@string/app_name">
            <intent-filter>
                <action android:name="com.example.joy.action.CHANGE_IMAGE"/>
                <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
            </intent-filter>
            <meta-data
                android:name="android.appwidget.provider"
                android:resource="@xml/mul_app_widget_provider_info">
            </meta-data>
        </receiver>

        <service android:name=".ListViewService"
            android:permission="android.permission.BIND_REMOTEVIEWS"
            android:exported="false"
            android:enabled="true"/>

    </application>

</manifest>

複製程式碼

  這個小部件新增到桌面後有一個 ImageView 顯示小機器人,下面有一個 Button ,右邊有一個ListView。

  這裡主要看看,Button 和 ListView 在 RemoteViews中如何使用。、

  Button 設定 Text 和 TextView 一樣,因為 Button 本身繼承自 TextView,Button 設定點選事件如下:

     Intent skipIntent = new Intent(context, MainActivity.class);
        PendingIntent pi = PendingIntent.getActivity(context, 200, skipIntent, PendingIntent.FLAG_CANCEL_CURRENT);
        mRemoteViews.setOnClickPendingIntent(R.id.btn_test, pi);

  用到方法 setOnClickPendingIntent,PendingIntent 表示延遲的 Intent , 與通知中的用法一樣。這裡點選之後跳轉到了 MainActivity。

  關於 ListView 的用法就複雜一些了。首先需要自定義一個類繼承自 RemoteViewsServices ,並重寫 onGetViewFactory 方法,返回 RemoteViewsService.RemoteViewsFactory 介面的物件。這裡定義了一個內部類實現該介面,需要重寫多個方法,與 ListView 的多佈局適配很類似。重點方法是

public RemoteViews getViewAt(int position){} 

這個方法中指定了 ListView 的每一個 item 的佈局以及內容,同時通過  setOnClickFillInIntent() 或者 setOnClickPendingIntent() 給 item 設定點選事件。這裡我實現的點選 item,替換左邊的 ImageView 的圖片。重寫了 MulAppWidgetProvider 類的 onReceiver 方法,處理替換圖片的邏輯。

  程式執行效果如下圖: