Android AppWidget


AppWidget不知道大家使用這個多不多,這個在手機上也叫做掛件,掛件也就是放在桌面方便用戶進行使用的,從android1.6開始掛件支持一些簡單的lauout和view,到了android4.0之后谷歌在掛件上也是加上了更為豐富的view支持,下面我們就從頭開始來介紹一下這些掛件吧。

 

如何添加一個簡單的AppWidget掛件

添加一個掛件很簡單,分為四部,只要按照這四部來弄就很容易添加上一個掛件:

(1)添加AppWidgetProviderInfo信息,這個信息是一個以xml文件形式出現的,這個文件是描述的是這個掛件的屬性信息,比如大小和布局文件等等。那么這個xml文件定義在哪里呢?它就定義在res目錄在xml目錄中,看下圖

 

看上面的圖片中在xml目錄下創建一個itchq_info.xml文件,這個文件里面就是描述掛件的屬性,來看看這個里面的代碼:

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider 
    xmlns:android="http://schemas.android.com/apk/res/android" 
    android:minWidth="70dp"
    android:minHeight="120dp"
    android:initialLayout="@layout/activity_main"
    >

</appwidget-provider>

上面的都是基本的屬性,android:minWidth和android:minHeight這個兩個分別是表示掛件的寬和高,android:initialLayout這個就是設置這個掛件的布局文件,這個布局文件就是在layout下的。

(2)添加布局信息,從上面中已經看到掛件的布局文件是Activity_main,看一下這個布局文件,這一步估計就很簡單的,

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity" >

    <TextView
        android:id="@+id/txt"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/hello_world"
        android:layout_centerHorizontal="true"
         />
    <Button 
        android:layout_below="@id/txt"
        android:id="@+id/btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="button"
        android:layout_centerHorizontal="true"
        />

</RelativeLayout>

上面的布局很簡單,就是顯示一個簡單的TextView和Button,這個就不多做介紹了。

(3)創建一個類繼承於AppWidgetProvider,這個類類似於我們的Activty類,定義Appwidget的生命周期,我們來看看系統中AppWidgetProvider是一個啥樣的類

從上面的圖片我們就可以看出這個AppWidgetProvider是繼承於廣播的類,所以這個AppwidgetProvider就是一個廣播,當是它又有自己的生命周期函數,來看看這個我們定義這個類的代碼:

package com.itchq.appwidgetactivity;

import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.Context;
import android.content.Intent;

public class itchqAppWidget extends AppWidgetProvider{

    @Override
    public void onReceive(Context context, Intent intent) {
        // TODO Auto-generated method stub
        super.onReceive(context, intent);
    }

    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager,
            int[] appWidgetIds) {
        // TODO Auto-generated method stub
        super.onUpdate(context, appWidgetManager, appWidgetIds);
    }

    @Override
    public void onDeleted(Context context, int[] appWidgetIds) {
        // TODO Auto-generated method stub
        super.onDeleted(context, appWidgetIds);
    }

    @Override
    public void onEnabled(Context context) {
        // TODO Auto-generated method stub
        super.onEnabled(context);
    }

    @Override
    public void onDisabled(Context context) {
        // TODO Auto-generated method stub
        super.onDisabled(context);
    }

    
}

 

上面這五個方法就是AppWidgetProvider經常使用到的一些函數,它們分別的意義是:

onReceive(Context context, Intent intent):

這個大家都知道是接受廣播的方法,因為我們的AppWidgetProvider就是一個廣播類,每一次對掛件的添加和刪除這個方法都會接受到一個廣播,都會跑一次這個方法

onEnabled(Context context)

這個是剛開始添加的掛件的時候跑的方法,注意在android機制中同一個掛件可以添加多次,這個只有第一次添加該掛件時才會跑的方法,

onUpdate(Context context, AppWidgetManager appWidgetManager,int[] appWidgetIds)

根據AppWidgetProviderInfo 中updatePeriodMillis 屬性描述的時間間隔來調用此方法對 Widget 進行更新和每一次添加掛件時都會調用這個方法,注意這個方法和onEnabled()區別是每一次添加掛件都會調用,而onEnabled()只有第一次添加掛件的時候才會調用,還有一個是關於android:updatePeriodMillis這個屬性好像沒有作用,就算你設置了1分鍾更新一次沒過一分鍾也不會取調用這個方法也就是沒有更新(這個好像一直都是這樣的,不知道是為什么)。

onDeleted(Context context, int[] appWidgetIds)

這個方法是每刪除一次掛件就會調用,注意前面有說過同一個掛件可以添加多次,所以當有多個掛件在桌面上時每刪除一個掛件就就會調用上面的方法。

onDisabled(Context context)

這個是刪除桌面上最后一個掛件的時候才會調用,注意不要和onDeleted()方法搞混淆了,onDisabled()表示的是一個桌面上添加了多個相同的掛件當我們刪除完最后一個時就會調用這個方法。

 

 

看上面的log,第一次添加掛件時先是跑了onEnabled()方法之后就接受到android.appwidget.action.APPWIDGET_ENABLED的廣播,之后又調用了opUpdate()方法以及接受到android.appwidget.action.APPWIDGET_UPDATE的廣播,這些廣播都是系統發的,我們再看下面的log,在添加第二次相同的掛件的時候我們發現直接就調用onUpdate()以及接受到android.appwidget.action.APPWIDGET_UPDATE的廣播,沒有調用了onEnabled()方法了,這個就是因為該掛件不是第一次添加所以就不會再調用onEnabled()方法了。

接下來我們看看刪除的時候,在第一次刪除時調用了onDeleted()方法以及接受到android.appwidget.action.APPWIDGET_DELETED的廣播,但是沒有調用onDisabled()這個方法,因為該掛件添加了兩次,我們在刪除第一個的時候還有一個在桌面上,當我們再把最后一個給刪除的時候就發現先調用onDeleted()方法之后就調用onDisabled()方法,

上面每一次調用方法之后都會接受相應的廣播,這個就是android系統發來的。

 

(4)最后一步是在AndroidManifest.xml中聲明,AppWidgetProvider本身是一個廣播,那么既然是廣播我們就需要在AndroidManifest.xm進行注冊,來看看這個是如何注冊的:

 

        <receiver 
            android:name="com.itchq.appwidgetactivity.itchqAppWidget"
            >
            <intent-filter>
                <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
            </intent-filter>
            <meta-data android:name="android.appwidget.provider" android:resource="@xml/itchq_info"/>
        </receiver>

看上面的代碼,這個廣播要記得需要靜態的注冊android.appwidget.action.APPWIDGET_UPDATE廣播,如果你不添加這個就無法在掛件欄里面看到這個掛件,也就無法添加新的掛件,在meta-data標簽里面android:name是固定的,來看看android:resource這個就是我們在第一步添加AppWidgetProviderInfo信息

我們來看看界面的顯示效果

上面就是顯示效果,一個textView和一個button

 

掛件的交互事件

我們在上面定義一個按鈕和一個文本,那邊在掛件中如何設置按鈕的點擊事件以及給文本框設置文本呢?這個跟我們在Activity的處理是完全不一樣的,這個涉及到一個RemoteViews類的使用,RemoteViews類描述了一個View對象能夠顯示在其他進程中,可以融合從一個 layout資源文件實現布局。雖然該類在android.widget.RemoteViews而不是appWidget下面但在Android Widgets開發中會經常用到它,我們先來看看按鈕的點擊事件是如何設置的,

 

 @Override public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { // TODO Auto-generated method stub
        super.onUpdate(context, appWidgetManager, appWidgetIds); RemoteViews remoteViews=new RemoteViews(context.getPackageName(), R.layout.activity_main); Intent intent=new Intent(); intent.setAction("btn.itchq.com"); PendingIntent pendingIntent=PendingIntent.getBroadcast(context, 0, intent, 0); remoteViews.setOnClickPendingIntent(R.id.btn, pendingIntent); appWidgetManager.updateAppWidget(appWidgetIds, remoteViews); Log.i("cheng", "onUpdate"); }

 


我們看到這個RemoteViews對象的構造函數中第二個參數就是我們的初始化布局文件,即就是在第一步中添加AppWidgetProviderInfo信息的中的 android:initialLayout這個布局,當然如何我們在這里指定其它布局也可以,如果在這里指定其它布局那么這個布局就會調換掉默認的初始化布局文件,上面的代碼可以看到按鈕的點擊事件通過設置想要的intent來實現的,在設置好的intent中通過PendingIntent包裝之后,remoteViews.setOnClickPendingIntent()就是設置的按鈕的點擊事件,這個方法中第一個參數指定的是這個button的id,第二個方法指定的就是已經設置好的PengingIntent,我們可以看到這個是一個廣播事件,當然我們也可以設置按鈕的來開啟一個Activity和一個服務,如何修改看下面的代碼:

        RemoteViews remoteViews=new RemoteViews(context.getPackageName(), R.layout.activity_main);
        Intent intent=new Intent(context,MainActivity.class);
        //intent.setAction("btn.itchq.com");
        PendingIntent pendingIntent=PendingIntent.getActivity(context, 0, intent, 0);
        
        remoteViews.setOnClickPendingIntent(R.id.btn, pendingIntent);
        appWidgetManager.updateAppWidget(appWidgetIds, remoteViews);

很容易了,說白了就是這個PendingIntent的使用方法了,好了我們再來看看還有最好一句話appWidgetManager.updateAppWidget(appWidgetIds, remoteViews);

 

 這個是掛件的更新,這個很重要,我們要記住每一個使用RemoteViews設置完相應的動作之后一定要更新一下掛件,如果不更新那么我們所有設置都是無效的,比如上面沒有更新的話那么我們點擊按鈕是沒有任何效果的,由於這個是在onUpdate()方法里面設置的,所以更新這個直接那appWidgetManager這個對象來更新就行,那么如果不是在onUpdate()里面更新的話在外面又是如何來更新的呢?,我們直接來看下面的代碼

 

            ComponentName thisName=new ComponentName(context, itchqAppWidget.class);
            AppWidgetManager manager=AppWidgetManager.getInstance(context);
            manager.updateAppWidget(thisName, remoteViews);

 

這個就是直接獲取的AppWidgetManager對象,通過這個就可以更新掛件,itchqAppWidget.class是繼承AppWidgetProvider的類名,這里還有另外一個方法:

            AppWidgetManager manager=AppWidgetManager.getInstance(context);
            int[] appids=manager.getAppWidgetIds(new ComponentName(context, itchqAppWidget.class));
            manager.updateAppWidget(appids, remoteViews);

這兩個更新的方法都是一樣的,很簡單吧,好了回到按鈕的點擊事件,上面中我們設置一個按鈕的點擊事件,其實是一個發送廣播的方式,這個廣播是btn.itchq.com,那要讓一個掛件接受這個廣播我們就必須進行注冊,這個和廣播的機制是一樣的

            <intent-filter>
                <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
                <action android:name="btn.itchq.com"/>
            </intent-filter>

看上面的代碼就很容易理解了,那么我們再看看接受廣播之后做哪些事件呢

    public void onReceive(Context context, Intent intent) {
        // TODO Auto-generated method stub
        super.onReceive(context, intent);
        Log.i("cheng", "onReceive="+intent.getAction());
        if(intent.getAction().equals("btn.itchq.com")){
            RemoteViews remoteViews=new RemoteViews(context.getPackageName(), R.layout.activity_main);
            remoteViews.setTextViewText(R.id.txt, "Success");
            ComponentName thisName=new ComponentName(context, itchqAppWidget.class);
            AppWidgetManager manager=AppWidgetManager.getInstance(context);
            manager.updateAppWidget(thisName, remoteViews);

        }

remoteViews.setTextViewText()就是更新文本,第一個參數就是文本的id,第二個參數就是要更新的文件內容,很容易理解吧,最后不要忘了更新一些掛件

其實在RomteViews類里面有一系列的setXXX()方法,我們可以 這些set設置相應的功能,比如可以切換ImageView的圖片

 android:configure

 

<appwidget-provider 
    xmlns:android="http://schemas.android.com/apk/res/android" 
    android:minWidth="70dp"
    android:minHeight="120dp"
    android:initialLayout="@layout/activity_main"
    android:configure="com.itchq.appwidgetactivity.MainActivity"
    >

</appwidget-provider>

 

這個是在appwidgetproviderInfo中定義的(就是我們的res目錄下xml文件中),如果用戶在添加一個App Widget時需要配置設置,那么可以創建一個app widget configuration Activity。這個Activity由App Widget host自動啟動,並且允許用戶在創建的時候配置可能的設置,不如掛件的大小呀,一些需要的參數呀等等,這個Activity和我們普通的Avtivity是一樣需要在Android manifest文件中進行聲明,但是要注意這個activity必須能接收這樣的Intent “android.appwidget.action.APPWIDGET_CONFIGURE“,因為只有接受這個才會被它會被App Widget host中的ACTION_APPWIDGET_CONFIGUREaction啟動

 

        <activity android:name="com.itchq.appwidgetactivity.MainActivity" >
            <intent-filter>
                <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
            </intent-filter>
        </activity>

 

我們再看看這個Avtivity中的代碼

package com.itchq.appwidgetactivity;


import android.os.Bundle;
import android.app.Activity;
import android.appwidget.AppWidgetManager;
import android.content.Intent;
import android.util.Log;
import android.view.Menu;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.RemoteViews;
import android.widget.TextView;

public class MainActivity extends Activity {

    private int mAppWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID;
    private EditText text;
    private Button btn;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setResult(RESULT_CANCELED);
        setContentView(R.layout.main);
        Intent intent = getIntent();
        Bundle extras = intent.getExtras();
        if (extras != null) {
            mAppWidgetId = extras.getInt(
                    AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
        }
        if (mAppWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
            Log.i("cheng", "aaaaa");
            finish();
        }
        text=(EditText) findViewById(R.id.test);
        btn=(Button) findViewById(R.id.ok);
        btn.setOnClickListener(new OnClickListener() {
            
            @Override
            public void onClick(View v) {
                // TODO Auto-generated method stub
                click();
            }
        });
    }

    public void click(){
        RemoteViews remoteViews=new RemoteViews(getPackageName(), R.layout.activity_main);
        remoteViews.setTextViewText(R.id.txt, text.getText());
        AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(this);
        appWidgetManager.updateAppWidget(mAppWidgetId, remoteViews);  
        
        Intent resultValue = new Intent();
        resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId);
        setResult(RESULT_OK, resultValue);
        finish();
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }
    
}

這個MainActivity中布局中顯示一個EditText和Button,setResult(RESULT_CANCELED);這句話說明的就是配置Activity第一次打開時,設置Activity result為RESULT_CANCLED。這樣,用戶在未完成配置設置而退出時,App Widget被告知配置取消,將不會添加App Widget。

        Intent intent = getIntent();
        Bundle extras = intent.getExtras();
        if (extras != null) {
            mAppWidgetId = extras.getInt(
                    AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
        }

這段代碼從啟動Activity的Intent中獲取App Widget ID。我們后面會通過這個ID來更新掛件

        if (mAppWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
            finish();
        }

這句話表示的就是這個給定的app widget ID無效的時候就直接結束這個Activity,最后我們來看看這個按鈕的點擊事件中的代碼:

        RemoteViews remoteViews=new RemoteViews(getPackageName(), R.layout.activity_main);
        remoteViews.setTextViewText(R.id.txt, text.getText());
        AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(this);
        appWidgetManager.updateAppWidget(mAppWidgetId, remoteViews);  

這段代碼表示當我們點擊了按鈕之后,掛件外面的TextView就顯示成我們在這里設置的Text,之后還不要忘了要更新一下掛件

       Intent resultValue = new Intent();
        resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId);
        setResult(RESULT_OK, resultValue);
        finish();

最后,創建返回Intent,設置給Activity返回結果,並且結束Activity。

這個整體的效果就是在添加掛件之后就會先彈出一個Activity界面,設置好Text之后點擊按鈕就返回掛件外面,掛件的TextView控件顯示的就是我們剛剛開始設置的Text。

 

 

 

 

 

android 4.0之后掛件的變化

 

谷歌在android 4.0對掛件進行了大修改,添加了不少的新功能,在這里我說一下我們在res/layout所定義的widget布局使用的組件必須是RemoteViews所支持的,我們先來看看在android4.0之前掛件能支持的控件是哪些?

上圖就是在android4.0之后掛件布局里面所支持的控件,也就是說我們只能使用上面的控件,如果在android4.0之前使用除上面之外的控件添加掛件就會報錯,而且這個還不能說繼承來使用,比如 不能像下面這樣使用

 

public class widgetImage extends ImageView

這個也是不行的,所以這樣直接導致android4.0之前掛件的功能都是很簡單,沒有listview等負責的控件,很多掛件都是顯示圖片,如果想自定widget的view也行,但是這個只能是在framework里面進行修改,這個只有廠家能使用,第三方就不好修改了。

我們再來看看android4.0所帶來的appWidget的變化,首先我們來對比在控件上android4.0又添加哪些直接看下圖:

 

 從這個圖中可以看出谷歌在4.0之后對widget的支持添加了很多控件,這里有listView還有gridview等等,這樣也使我們的掛件顯示更加豐富。

除了上面的變化之外,android4.0之后還加入了一個可以變大小的功能,在我們長按掛件之后如果沒有刪除時就會出現這個效果,先來看看圖片

 

看上面的掛件左右上面都有一個點,這個表示可以改變改掛件的大小,我們手動放到對應的點上進行拉動就可以改變掛件的大小,那這個是什么實現的呢?

要實現這個效果就需要在AppWidgetProviderInfo(就是在res/xml目錄下那個xml文件)中加上android:resizeMode屬性,該屬性一共有三個值分別設置,分別是:

android:resizeMode="horizontal"這個表示在水平方向可以進行大小的改變,也就是掛件的寬度

android:resizeMode="vertical"這個表示在垂直方向可以進行大小的改變,這個也就是可以改變掛件的高度

android:resizeMode="none"這個就是指定該掛件不可改變大小,不管是寬度還是高度都不能改變大小

如果我們想同時改變的寬度和高度的大小的話可以這樣設置android:resizeMode="horizontal|vertical"

 

android4.0中還添加了一個掛件的預覽圖,同樣也是在AppWidgetProviderInfo里面android:previewImage來指定掛件的預覽圖,

android:previewImage="@drawable/preview"這個就是我指定一個預覽圖片,那么這個有什么作用呢?

 

看圖片的右邊就是添加了一個預覽圖片,這個就是在添加掛件顯示界面看到的圖片,這個也是很容易了。

 

這個是我直接從谷歌上面截取過來的,從這段文字中我們也可以看出在android4.2中谷歌對掛件進行一個小變動,LockSreen鎖機掛件,意思就是說這個掛件可以添加到我們的桌面同時也可以設置這個掛件添加到鎖機界面上去,這個設置就是通過android:widgetCategory來設置的,這個一共有兩個值:

android:widgetCategory="home_screen"表示添加到桌面中

android:widgetCategory="keyguard"表示添加到鎖機界面中

我來看下面的代碼

<?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="keyguard |home_screen"
    android:initialKeyguardLayout="@layout/widget_layout"> 
    

</appwidget-provider>

這段是appWidgetProviderInfo信息,我們在段信息中發現android:initialLayout和android:initialKeyguardLayout都是設置布局文件的,那么這兩個的區別主要是在於android:initialLayout中布局是設置掛件在桌面中顯示的布局,android:initialKeyguardLayout這個就是指定掛件在鎖機界面顯示的布局文件。當時在谷歌說明中也指定了可以在代碼中動態來判斷創建不同的layout文件。就是在運行時檢測widget category進行響應。調用getAppWidgetOptions()獲取widget放置位置的Bundle數據。返回的Bundle將會包括 key值OPTION_APPWIDGET_HOST_CATEGORY,它的值為WIDGET_CATEGORY_HOME_SCREEN或者 WIDGET_CATEGORY_KEYGUARD。然后在AppWidgetProvider中檢測widget category。例如:

AppWidgetManager appWidgetManager;
int widgetId;
Bundle myOptions = appWidgetManager.getAppWidgetOptions (widgetId);

// Get the value of OPTION_APPWIDGET_HOST_CATEGORY
int category = myOptions.getInt(AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY, -1);

// If the value is WIDGET_CATEGORY_KEYGUARD, it's a lockscreen widget
boolean isKeyguard = category == AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD;

一旦知道了widget的category,就可以有選擇的裝載baselayout,設置不同的屬性等。例如

int baseLayout = isKeyguard ? R.layout.keyguard_widget_layout :R.layout_widget_layout;

好的,除了上面的這些之外,我們就重點來介紹有關於android4.0所添加的stackView ,gridview,listview等一些用法,這個stackview也是android4.0之后才添加到手機上的,之前一直是在android3.0中使用也就是平板電腦,我們可以先來看看StackView顯示的效果

看上面這個掛件的效果是不是很酷呀,這個就是stackview的效果,這個是使用谷歌的列子,想這中stackView ,gridview,listview控件,我們平時使用的時候都需要一個adpater適配器來設置相應的數據,那么在掛件當中如何給這些來設置數據呢?這個就引入RemoteViewsService這個類和RemoteViewsService.RemoteViewsFactory這個接口,

從上圖中可以看出這個RemoteViewsService是繼承於Service類,所以這個RemoteViewsService就是一個服務,它管理RemoteViews的服務,我們在RemoteViewsService這個類中主要是實現onGetViewFactory這個方法,返回一個RemoteViewsFactory的實現類,看下面的代碼:

public class StackWidgetService extends RemoteViewsService {
    @Override
    public RemoteViewsFactory onGetViewFactory(Intent intent) {
        return new StackRemoteViewsFactory(this.getApplicationContext(), intent);
    }
}

StackRemoteViewsFactory就是實現了RemoteViewsService.RemoteViewsFactory接口。

RemoteViewsFactory是RemoteViewsService中的一個接口。RemoteViewsFactory提供了一系列的方法管理“集合視圖”中的每一項,也就是相當於我們adpater適配器作用,

 

class StackRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory {
    private static final int mCount = 10;
    private List<WidgetItem> mWidgetItems = new ArrayList<WidgetItem>();
    private Context mContext;
    private int mAppWidgetId;

    public StackRemoteViewsFactory(Context context, Intent intent) {
        mContext = context;
        mAppWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
                AppWidgetManager.INVALID_APPWIDGET_ID);
    }

    public void onCreate() {
        // In onCreate() you setup any connections / cursors to your data source. Heavy lifting,
        // for example downloading or creating content etc, should be deferred to onDataSetChanged()
        // or getViewAt(). Taking more than 20 seconds in this call will result in an ANR.
        for (int i = 0; i < mCount; i++) {
            mWidgetItems.add(new WidgetItem(i + "!"));
        }

        // We sleep for 3 seconds here to show how the empty view appears in the interim.
        // The empty view is set in the StackWidgetProvider and should be a sibling of the
        // collection view.
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void onDestroy() {
        // In onDestroy() you should tear down anything that was setup for your data source,
        // eg. cursors, connections, etc.
        mWidgetItems.clear();
    }

    public int getCount() {
        return mCount;
    }

    public RemoteViews getViewAt(int position) {
        // position will always range from 0 to getCount() - 1.

        // We construct a remote views item based on our widget item xml file, and set the
        // text based on the position.
        RemoteViews rv = new RemoteViews(mContext.getPackageName(), R.layout.widget_item);
        rv.setTextViewText(R.id.widget_item, mWidgetItems.get(position).text);

        // Next, we set a fill-intent which will be used to fill-in the pending intent template
        // which is set on the collection view in StackWidgetProvider.
        Bundle extras = new Bundle();
        extras.putInt(StackWidgetProvider.EXTRA_ITEM, position);
        Intent fillInIntent = new Intent();
        fillInIntent.putExtras(extras);
        rv.setOnClickFillInIntent(R.id.widget_item, fillInIntent);

        // You can do heaving lifting in here, synchronously. For example, if you need to
        // process an image, fetch something from the network, etc., it is ok to do it here,
        // synchronously. A loading view will show up in lieu of the actual contents in the
        // interim.
        try {
            System.out.println("Loading view " + position);
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // Return the remote views object.
        return rv;
    }

    public RemoteViews getLoadingView() {
        // You can create a custom loading view (for instance when getViewAt() is slow.) If you
        // return null here, you will get the default loading view.
        return null;
    }

    public int getViewTypeCount() {
        return 1;
    }

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

    public boolean hasStableIds() {
        return true;
    }

    public void onDataSetChanged() {
        // This is triggered when you call AppWidgetManager notifyAppWidgetViewDataChanged
        // on the collection view corresponding to this factory. You can do heaving lifting in
        // here, synchronously. For example, if you need to process an image, fetch something
        // from the network, etc., it is ok to do it here, synchronously. The widget will remain
        // in its current state while work is being done here, so you don't need to worry about
        // locking up the widget.
    }
}

這個就是實現RemoteViewsFactory這個接口的類,從這個類中所實現的方法和BaseAdapter差不多是一樣的,

 public int getCount()

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

RemoteViews getViewAt(int position)

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

public RemoteViews getLoadingView()

This allows for the use of a custom loading view which appears between the time that getViewAt(int) is called and returns.

public void onDataSetChanged()

Called when notifyDataSetChanged() is triggered on the remote adapter.

下面兩個都是使用谷歌原話不難理解,實在不行就在線翻譯一下咯,哈哈。

下面介紹一下如何使用RemoteViewsService的過程,先來看下面的代碼:

 

    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        // update each of the widgets with the remote adapter
        for (int i = 0; i < appWidgetIds.length; ++i) {

            // Here we setup the intent which points to the StackViewService which will
            // provide the views for this collection.
            Intent intent = new Intent(context, StackWidgetService.class);
            intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]);
            // 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.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
            RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.widget_layout);
            rv.setRemoteAdapter(appWidgetIds[i], R.id.stack_view, intent);

            // The empty view is displayed when the collection has no items. It should be a sibling
            // of the collection view.
            rv.setEmptyView(R.id.stack_view, R.id.empty_view);//@1

            // Here we setup the a pending intent template. Individuals items of a collection
            // cannot setup their own pending intents, instead, the collection as a whole can
            // setup a pending intent template, and the individual items can set a fillInIntent
            // to create unique before on an item to item basis.
            Intent toastIntent = new Intent(context, StackWidgetProvider.class);
            toastIntent.setAction(StackWidgetProvider.TOAST_ACTION);
            toastIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]);//@2
            intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
            PendingIntent toastPendingIntent = PendingIntent.getBroadcast(context, 0, toastIntent,
                    PendingIntent.FLAG_UPDATE_CURRENT);
            rv.setPendingIntentTemplate(R.id.stack_view, toastPendingIntent);

            appWidgetManager.updateAppWidget(appWidgetIds[i], rv);
        }
        super.onUpdate(context, appWidgetManager, appWidgetIds);
    }

 

從上面的代碼中我們發現是通過  rv.setRemoteAdapter來設置對應的RemoteViewsService,這個Intent定義 Intent intent = new Intent(context, StackWidgetService.class)中StackWidgetService就是我們繼承與RemoteViewsService的類,上面我們也可以這樣寫,不需要第一個參數的

            Intent serviceIntent = new Intent(context, StackWidgetService.class);        
            rv.setRemoteAdapter(R.id.stack_view, serviceIntent);    

很容易理解的,是不是很像我們Activity之間的跳轉呀,rv.setPendingIntentTemplate(R.id.stack_view, toastPendingIntent);設置的就是這個Item的相應事件,這里要說明一下:“集合控件(如GridView、ListView、StackView等)”中包含很多子元素,它們不能像掛件上普通的按鈕一樣通過 setOnClickPendingIntent 設置點擊事件,必須先通過兩步。

(01) 通過 setPendingIntentTemplate 設置 “intent模板”,這是比不可少的!

在上面的代碼中

            Intent toastIntent = new Intent(context, StackWidgetProvider.class);
            toastIntent.setAction(StackWidgetProvider.TOAST_ACTION);

這個原理和setOnClickPendingIntent一樣發送一個廣播,然后通過rv.setPendingIntentTemplate(R.id.stack_view, toastPendingIntent)來設置intent模板


(02) 最后在處理該“集合控件”的RemoteViewsFactory類的getViewAt()接口中 通過 setOnClickFillInIntent 設置“集合控件的某一項的數據”

這個在下面會講解到。

 

代碼中還有 rv.setEmptyView(R.id.stack_view, R.id.empty_view)這么一句話,這句話的意思是當這個沒有Item的時候就是顯示這個控件,R.id.empty_view布局如下

    <TextView 
        android:id="@+id/empty_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:background="@drawable/widget_item_background"
        android:textColor="#ffffff"
        android:textStyle="bold"
        android:text="@string/empty_view_text"
        android:textSize="20sp" />

那么我們從前面中我們知道這個RemoteViewsService是一個繼承於Service的,既然是一個服務就必須要在AndroidManifast,xml文件中進行聲明。下面這段代碼就是聲明一個RemoteViewsService

        <service android:name="StackWidgetService"
            android:permission="android.permission.BIND_REMOTEVIEWS"
            android:exported="false" />

 

 上面的RemoteViewsService設置好之后我們返回來看看這個Item,也就是實現RemoteViewsService.RemoteViewsFactory 接口中 public RemoteViews getViewAt(int position)方法里面的設置。

    public RemoteViews getViewAt(int position) {
        // position will always range from 0 to getCount() - 1.

        // We construct a remote views item based on our widget item xml file, and set the
        // text based on the position.
        RemoteViews rv = new RemoteViews(mContext.getPackageName(), R.layout.widget_item);
        rv.setTextViewText(R.id.widget_item, mWidgetItems.get(position).text);

        // Next, we set a fill-intent which will be used to fill-in the pending intent template
        // which is set on the collection view in StackWidgetProvider.
        Bundle extras = new Bundle();
        extras.putInt(StackWidgetProvider.EXTRA_ITEM, position);
        Intent fillInIntent = new Intent();
        fillInIntent.putExtras(extras);
        rv.setOnClickFillInIntent(R.id.widget_item, fillInIntent);

        // You can do heaving lifting in here, synchronously. For example, if you need to
        // process an image, fetch something from the network, etc., it is ok to do it here,
        // synchronously. A loading view will show up in lieu of the actual contents in the
        // interim.
        try {
            System.out.println("Loading view " + position);
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // Return the remote views object.
        return rv;
    }

看上面的代碼我們看到RemoteViews rv = new RemoteViews(mContext.getPackageName(), R.layout.widget_item);這個里面的第二個參數就是Item中的Layout,我們再來看看這個布局文件是什么寫的

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" 
    android:background="@drawable/widget_item_background" >"
    <TextView 
        android:id="@+id/widget_item"
        android:layout_width="120dp"
        android:layout_height="120dp"
        android:gravity="center"
        android:textColor="#ffffff"
        android:textStyle="bold"
        android:textSize="44sp" />
</LinearLayout>

這個就是只有這個簡單的TextView,那么我們是如何來設置這個TextView相應的文本呢,rv.setTextViewText(R.id.widget_item, mWidgetItems.get(position).text);這句話就是設置TextView相應的文本,mWidgetItems.get(position).text是一個String字符串,第一個參數就是TextView的Id,這個理解起來也是很容易吧

接下來要處理的就是這個Item的點擊事件了,

        Bundle extras = new Bundle();
        extras.putInt(StackWidgetProvider.EXTRA_ITEM, position);
        Intent fillInIntent = new Intent();
        fillInIntent.putExtras(extras);
        rv.setOnClickFillInIntent(R.id.widget_item, fillInIntent);

這段代碼中就是設置Item的點擊事件,這個extras.putInt(StackWidgetProvider.EXTRA_ITEM, position);是傳送一個參數,rv.setOnClickFillInIntent(R.id.widget_item, fillInIntent);中第一個參數就是響應點擊事件的ID,這個就是我們上面所說的第(2)點內容。設置好之后我們來看看這個廣播是如何來接受我們的信息的:

    @Override
    public void onReceive(Context context, Intent intent) {
        AppWidgetManager mgr = AppWidgetManager.getInstance(context);
        if (intent.getAction().equals(TOAST_ACTION)) {
            int appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
                    AppWidgetManager.INVALID_APPWIDGET_ID);
            int viewIndex = intent.getIntExtra(EXTRA_ITEM, 0);
            Toast.makeText(context, "Touched view " + viewIndex, Toast.LENGTH_SHORT).show();
        }
        super.onReceive(context, intent);
    }

TOAST_ACTION就是我們點擊的時候發送的廣播,intent.getIntExtra(EXTRA_ITEM, 0);這個EXTRA_ITEM就是上面extras.putInt(StackWidgetProvider.EXTRA_ITEM, position);傳進來的,哈哈這里看到就明白很多了吧。

 

讓掛件支持復雜的View

這個地方一般來說很少有人去東這里的東西,這個可能只有手機制作廠家在要弄酷炫的掛件的時候可以取改這些東西,在這里我只是簡單的介紹一些就可以了。

我們知道對於一個掛件來說,它所支持的View不管是在android2.3或者是在Android4.0中支持都是很少的,而且我們要注意到這些功能要是弄個簡單的動畫都是很麻煩的,你會發現有寫東西根本就沒法用,因為我們修改一切都必須是RemoteViews這個有的方法才行,不如修改修改文本呀或者設置點擊事件呀都是通過RemoteViews類中的方法來實現的。這個時候如果想自定義掛件支持的View或者是ViewGroup,在這些里面做一些RemoteViews所不支持的的話,就可以使用自定義View的方法,如何自定義View呢?首先我們來在源碼目錄下添加我們自定義的View

Frameworks\base\core\java\android\widget這個目錄就是我們存放view類的目錄

在這個目錄下我們隨便打開一個掛件所支持的View看一下都會發現在掛件所支持的View當中類前面都會有@RemoteView這么一個注釋,所以第一步就是

(1)在自定義的View或者ViewGroup類前面加上@RemoteView,並且將該類放到Frameworks\base\core\java\android\widget目錄下面

第一步定義好之后在掛件外面添加該類是可以支持的,但是我們如何來進行傳參數呢,就是從掛件外面把參數傳到這個自定義的view中呢?注意這個傳參數不能是簡單的一個函數調用,這個我們可以看一下RemoteViews類,因為我們必須通過這個類進行傳參數,那么如果查看源碼就會發現這個類中的方法前面都是加上了@android.view.RemotableViewMethod聲明,而且還有這些方法都是public公共方法。所以我們要記住第二點

(2)要想通過RemoteViews對象調用framework自定的方法,就要在自定義的類中聲明一個public方法,該方法前面要加上@android.view.RemotableViewMethod注釋

貼一段代碼上來看看吧

 

@RemoteView
public class widgetTest extends RelativeLayout{

    public widgetTest(Context context) {
        super(context);
        // TODO Auto-generated constructor stub
    }

    public widgetTest(Context context, AttributeSet attrs) {
        super(context, attrs);
        // TODO Auto-generated constructor stub
    }

    @android.view.RemotableViewMethod
    public void init(Bundle bundle){
        
    }
}

 

上面我定義了一個簡單的ViewGroup類,這個類前面我添加了@RemoteView方法,說明這個類可以被掛件所支持,注意這里不能放在自己的應用程序中,必須放在framework路徑下,public void init(Bundle bundle)就是我定義要在掛件中傳入參數的方法,這個前面加上了@android.view.RemotableViewMethod注釋,而且是public類的,我們再來看看onUpdate中是如何傳參數的

            RemoteViews remoteViews=new RemoteViews(context.getPackageName(), R.layout.activity_main);
            
            Bundle bundle=new Bundle();
            bundle.putInt("image", R.drawable.ic_launcher);
            remoteViews.setBundle(R.id.widgettest, "init", bundle);

注意看最后一句話remoteViews.setBundle(R.id.widgettest, "init", bundle);,這個里面第一個參數就是我們上面自定義ViewGroup在掛件布局中的ID,第二個就是方法名,這個方法名也是我們上面所定義的public void init(Bundle bundle),第三個參數就是傳進來的bundle,上面的代碼就是傳一張圖片進去。

 

在這里還要說一下我們自定義的方法中的參數必須是remoteViews這個類可以設置的參數,打個比方吧,我們上面傳進來的參數是Bundle對象可以,因為remoteViews對象中有setBundle()這個方法可以傳Bundle對象,如何我們修改自定義的方法參數為public void init(int a)參數是int類型的也是可以的,因為這個remoteViews中有setInt()方法,但是如果我們參數也是一個自定義的View類,那這個就不行,因為remoteViews沒有相應的setView方法,無法給View傳參數,這個問題就是說我們自定義的方法中參數還要受到remoteViews的影響。

 

 代碼地址:

http://files.cnblogs.com/itchq/AppWidgetActivity.zip

StackViewDemo.rar

 


免責聲明!

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



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