Android — AppWidget 高級篇

3.1 AppWidget到底支持哪些view
在Android 2.2 SDK中我們首次啟動模擬器可以看到和以前不一樣的是多出瞭一個綠色的小機器人提示信息,Google給我們瞭演示瞭Android中如何通過 RemoteView和簡單的圖片輪換方式實現動畫效果在桌面小工具中,appWidget的基類時AppWidgetProvider類,不過 Widget本身的生命周期管理並非Activity,相對於的而是BroadcastReceiver廣播方式處理的。一直想知道如何在AppWidget裡面添加 ListView,EditText 這些復雜的View.我們知道要在AppWidget裡添加 View都是通過RemoteView來做到瞭,然而RemoteView本身功能很弱,支持的操作很少,而且支持RemoteView的Widget很 少:
A RemoteViews object (and, consequently, an App Widget) can support the following layout classes:
 FrameLayout
 LinearLayout
 RelativeLayout
And the following widget classes:
 AnalogClock
 Button
 Chronometer
 ImageButton
 ImageView
 ProgressBar
 TextView
Descendants of these classes are not supported.
從這裡可以知道,為什麼在AppWidget裡添加 EditText會顯示LoadError瞭,因為本身它就不支持這些復雜的 Widget.
但我們又會有疑問瞭, 為什麼Google Search會有EditText呢?其實這些都是假象,並不是AppWidget支持EditText。細心的你應該會發現, AnalogClock也不是如Button,TextView的簡單Widget ,其實 AnalogClock也是Google自定義的RemoteViews。
在網上可以看到,AppWidget很多特效,它確實支持瞭復雜Widget,比如:ListView/GridView,EditText. 這些確實是我們可以看到的,但它是怎麼做到的呢?我也很想知道,AppWidget支持到那麼強大,甚至超過瞭本身AP的功能,很搶眼。但不管是怎麼實現 的,我想人傢肯定是花瞭大力氣去做到瞭,我猜想可能是將Google 提供的AppWidget進行瞭比較大的改動。我們查看一下framework下的appwidget:
:ls frameworks/base/core/java/android/appwidget/ -lh
total 60K
-rw-r–r– 1 pjq users 7.9K 2009-09-29 21:49 AppWidgetHost.java
-rw-r–r– 1 pjq users  12K 2009-09-29 21:49 AppWidgetHostView.java
-rw-r–r– 1 pjq users  14K 2009-09-29 21:49 AppWidgetManager.java
-rw-r–r– 1 pjq users  691 2009-09-29 21:49 AppWidgetProviderInfo.aidl
-rw-r–r– 1 pjq users 5.6K 2009-09-29 21:49 AppWidgetProviderInfo.java
-rwxr-xr-x 1 pjq users 6.3K 2009-09-29 21:49 AppWidgetProvider.java
-rw-r–r– 1 pjq users 1.5K 2009-09-29 21:49 package.html
可以看 到,appwidget的文件很少,雖然不能說明什麼,但按照正常的推理,文件少功能一般也強大不到哪裡去,這種想法雖然有些牽強,但暫且就這樣認為吧。

3.2 如何自定義RemoteViews
要知道RemoteView的功能很少,特別是對事件處理的能力,都需要通過PendingIntent,傳到BroadcastReceiver去處理。所以這裡對一些事件處理也僅限於比較簡單事 件:比如說:Button Clicked,其它的我好像還沒怎麼用過,對復雜的View:比如!ListView(當然這裡還不支持,打個比方),!ListView裡面那麼多Item,要設置Listener,要傳值,這些 RemoteView都不能像一個單純的Activity那樣處理,如果要實現,則需要更加復雜的手段,通過廣播實現。
由於日歷小部件需要實現onClick事件,顯示日歷,動畫效果等復雜的操作和效果,AppWidget支持的操作遠遠不能滿足,這就需要修改framework裡的代碼,目前我已經在AppWidget裡顯示CalendarView(日歷)、Viewflipper復雜的Widget,同時實現瞭如何讓這些自定義的RemoteViews與AppWidget進行交互。現在讓我詳細介紹如何在AppWidget裡自定義CalendarView(當然是繼承自view咯 ^_^)和Viewflipper(隻是在原基礎上做瞭修改)這些復雜的 Widget.
我們知道AppWidget隻支持RemoteView,哪些Widget是RemoteView 呢,我來教你搜一下:
[python]
frameworks/base/core/java/android/widget $ grep -i -n -A 1  @remoteview *.java 
AbsoluteLayout.java:40:@RemoteView 
AbsoluteLayout.java-41-public class AbsoluteLayout extends ViewGroup { 
— 
AnalogClock.java:39:@RemoteView 
AnalogClock.java-40-public class AnalogClock extends View { 
— 
Button.java:58:@RemoteView 
Button.java-59-public class Button extends TextView { 
— 
Chronometer.java:45:@RemoteView 
Chronometer.java-46-public class Chronometer extends TextView { 
— 
FrameLayout.java:47:@RemoteView 
FrameLayout.java-48-public class FrameLayout extends ViewGroup { 
— 
ImageButton.java:66:@RemoteView 
ImageButton.java-67-public class ImageButton extends ImageView { 
— 
ImageView.java:55:@RemoteView 
ImageView.java-56-public class ImageView extends View { 
— 
LinearLayout.java:44:@RemoteView 
LinearLayout.java-45-public class LinearLayout extends ViewGroup { 
— 
ProgressBar.java:122:@RemoteView 
ProgressBar.java-123-public class ProgressBar extends View { 
— 
RelativeLayout.java:66:@RemoteView 
RelativeLayout.java-67-public class RelativeLayout extends ViewGroup { 
— 
TextView.java:186:@RemoteView 
TextView.java-187-public class TextView extends View implements ViewTreeObserver.OnPreDrawListener { 

就是這些瞭,類名前面加瞭"@RemoteView",和我前面列出的那些是不是一樣的呢?所以,如果你需要自定義一個(或者是一個已定義的復雜View如listview)作為RemoteViews使用,你就必須在其類名前加"@RemoteView"標識。
關於如何自定義一個Widget你完全可以參照frameworks/base/core/java/android/widget已有的這些Widget,照樣寫一個。
其實如果你需要自定義一個Widget,比如說支持ListView,你可以先在一個activity裡實現它,然後將它移到framework下面去。
這 裡說一下可能需要註意的地方:
1.如果有多個文件,需要Package的時候,名字最好按照這樣的形 式:android.widget.CalendarView
其中CalendarView就是你要添加一個Widget存放的地方,這樣的話你就可以在 frameworks/base/core/java/android/widget 目錄下新增CalendarView文件夾,將java文件放在這個目錄下。
如果你新增的Widget隻有一個java文件就可以不用這樣瞭,可以 完全按照已經存在的Widget的樣子,直接將java文件放到frameworks/base/core/java/android/widget目錄 下。
2.資源文件存放:
frameworks/base/core/res/res
資 源文件都放到這個目錄下。
3.資源的引用:
要用這樣的方式引用:com.android.internal.R.drawable.
記著在這個Customer Widget類名前加上"@RemoteView"標記.
這 些都做完瞭,你就已經將一個自定義的Widget添加到framework瞭。之後要做的工作就是編譯整個工程瞭(在這裡教一個比較懶的方法,直接編譯frameworks 就OK,命令:
:mmm frameworks/base/
:adb push out/target/product/msm7627_ffa/system/framework/
framework.jar /system/framework/)。
最後你就可以在AppWidget引用你自定義的這個Widget瞭:
com.widget.CalendarView。

3.3  AppWidget訪問RemoteViews的方法
至 此,你已經用上瞭你自定義的這個Widget,並且可以加到AppWidget。如果你想在自定義的RemoteViews上顯示像日歷的內容,你隻需要在自定義的RemoteViews的onDraw()方法裡實現就OK。但是並不是所有內容都是在RemoteViews預先設置好的,很多內容是由用戶自己設置的。如,在AppWdiget顯示”HelloWord”,我們是這樣實現的
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.appwidget_provider);
views.setTextViewText(R.id.appwidget_text, "HelloWord"); 
RemoteViews為外部提供的方法非常有限,查看API文檔:
Public Methods View apply(Context context, ViewGroup parent)
Inflates the view hierarchy represented by this object and applies all of the actions.
int describeContents()
Describe the kinds of special objects contained in this Parcelable's marshalled representation.
int getLayoutId() String getPackage() boolean onLoadClass(Class clazz)
Hook to allow clients of the LayoutInflater to restrict the set of Views that are allowed to be inflated.
void reapply(Context context, View v)
Applies all of the actions to the provided view.
void setBitmap(int viewId, String methodName, Bitmap value)
Call a method taking one Bitmap on a view in the layout for this RemoteViews.
void setBoolean(int viewId, String methodName, boolean value)
Call a method taking one boolean on a view in the layout for this RemoteViews.
void setByte(int viewId, String methodName, byte value)
Call a method taking one byte on a view in the layout for this RemoteViews.
void setChar(int viewId, String methodName, char value)
Call a method taking one char on a view in the layout for this RemoteViews.
void setCharSequence(int viewId, String methodName, CharSequence value)
Call a method taking one CharSequence on a view in the layout for this RemoteViews.
void setChronometer(int viewId, long base, String format, boolean started)
Equivalent to calling Chronometer.setBase, Chronometer.setFormat, and Chronometer.start() or Chronometer.stop().
void setDouble(int viewId, String methodName, double value)
Call a method taking one double on a view in the layout for this RemoteViews.
void setFloat(int viewId, String methodName, float value)
Call a method taking one float on a view in the layout for this RemoteViews.
void setImageViewBitmap(int viewId, Bitmap bitmap)
等同於調用ImageView.setImageBitmap方法,從Bitmap對象中設置一個圖片
void setImageViewResource(int viewId, int srcId)
等同於調用ImageView.setImageResource,從一個資源中設置圖片
void setImageViewUri(int viewId, Uri uri)
等同於調用ImageView.setImageURI,從URI中設置圖像
void setInt(int viewId, String methodName, int value)
Call a method taking one int on a view in the layout for this RemoteViews.
void setLong(int viewId, String methodName, long value)
Call a method taking one long on a view in the layout for this RemoteViews.
void setOnClickPendingIntent(int viewId, PendingIntent pendingIntent)
Equivalent to calling setOnClickListener(android.view.View.OnClickListener) to launch the provided PendingIntent.
void setProgressBar(int viewId, int max, int progress, boolean indeterminate)
等同於調用ProgressBar.setMax, ProgressBar.setProgress, and ProgressBar.如果indeterminate為true則進度條的最大和最小進度將會忽略
void setShort(int viewId, String methodName, short value)
Call a method taking one short on a view in the layout for this RemoteViews.
void setString(int viewId, String methodName, String value)
Call a method taking one String on a view in the layout for this RemoteViews.
void setTextColor(int viewId, int color)
等同於setTextColor(int).,設置文本的顏色
void setTextViewText(int viewId, CharSequence text)
等同於TextView.setText,設置文本內容
void setUri(int viewId, String methodName, Uri value)
Call a method taking one Uri on a view in the layout for this RemoteViews.
void setViewVisibility(int viewId, int visibility)
等同於調用View.setVisibility,設置該ID控件的可見性
void writeToParcel(Parcel dest, int flags)
Flatten this object in to a Parcel.
無論你自定義的RemoteView有多少方法,但它終究是一個RemoteViews,所以在AppWidget中使用RemoteViews(無論是否是自定義的)隻能調用以上提供的方法。現在你一定很疑惑,那麼是不是AppWidget不能調用自定義RemoteViews裡的方法呢?能,當然能,並且還可以將RemoteViews裡的結果反饋到AppWidget,實現AppWidget和RemoteViews的交互,隻不過這種方法實現起來比較復雜,容我慢慢道來。
以上方法中,有幾個特殊的方法
void  java.lang.String, android.graphics.Bitmap) setBitmap(int viewId,  String methodName,  Bitmap value)
void  java.lang.String, boolean) setBoolean(int viewId,  String methodName, boolean value)
void  java.lang.String, byte) setByte(int viewId,  String methodName, byte value)
void  java.lang.String, char) setChar(int viewId,  String methodName, char value)
void  java.lang.String, java.lang.CharSequence) setCharSequence(int viewId,  String methodName,  CharSequence value)
void  java.lang.String, double) setDouble(int viewId,  String methodName, double value)
void  java.lang.String, float) setFloat(int viewId,  String methodName, float value)
void  java.lang.String, android.os.Bundle) setBundle(int viewId,  String methodName,  Bundle value)
這些方法是通往自定義RemoteViews的接口。它們都包含3個參數,第一個參數 viewId是你所要調用RemoteViews在佈局文件裡定義的Id,第二個參數 methodName是你要調用的RemoteViews裡的方法名,如你要調用RemoteViews裡的一個setName(String name),第二個參數你就可以設置為“setName”;第三個參數是你所要調用的方法的參數,以setName(String name)方法為例,你必須傳入一個String的參數,則在這裡調用void  java.lang.String, java.lang.CharSequence) setCharSequence(int viewId,  String methodName,  CharSequence value) ,因為隻有這個方法的第三個參數是String類型。下面看一個實例:
[java]
/*自定義的RemoteView*/ 
 
@RemoteView 
 
public class CalendarView extends View { 
 
… 
 
    /@hide*/ 
 
    @android.view.RemotableViewMethod 
    public void setTextSize(Bundle bundle){ 
    mLineSize = bundle.getInt("LineSize", 1); 
    mDigitalSize = bundle.getInt("DigitalSize", 2); 
    } 
… 
 

 
/* AppWidget 如何調用*/  www.aiwalls.com
 
… 
 
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.agenda_appwidget); 
 
Bundle bundle = new Bundle(); 
 
… 
 
… 
 
views. java.lang.String, android.os.Bundle) setBundle(R.id.preview,"setTextSize", bundle);  

這樣就完成瞭一次調用,當在AppWidget調用views. java.lang.String, android.os.Bundle) setBundle(…)時,就相當與調用
Calendar 裡的 setTextSize方法。如此就實現瞭AppWidget對RemoteViews的訪問。
註意:設為能在AppWidget被調用的方法setTextSize前必須加@…

3.4  AppWidget接收來自RemoteViews的信息
接下來說說你對自定義的RemoteViews的操作,如何讓AppWidget響應反饋?
似乎是沒有其它更好的方法,幸好Android提供瞭一個非常有效的讓不同進程傳遞數據和事件的媒介,即廣播機制。當對自定義RemoteViews的操作需要告知AppWidget時,就需要用廣播機制,發送一個廣播,並在AppWidget註冊這個廣播的監聽,AppWidget就能響應瞭。廣播機制大傢都很清楚,這裡就不班門弄斧瞭。

3.5 讓AppWidget實現翻頁動畫效果
那麼如何實現viewflipper翻頁動畫呢?
如果對上面的介紹都瞭解的話,翻頁動畫就簡單瞭。同樣,將在viewflipper類前加RemoteView標識@RemoteView,讓AppWidget支持viewflipper。因為上面我們自定義的 CalendarView是被支持的,所以可以在AppWidget佈局文件加上viewflipper和CalendarView(不被AppWidget支持的widget是不能被加到AppWidget佈局文件的,否則運行出錯)。那麼如何實現翻頁動畫效果呢,其實和一般的viewflipper使用差別不大,先看佈局文件:
[java] 
<ViewFlipper   
    android:id="@+id/flipper" 
    android:layout_width="fill_parent"  
    android:layout_height="fill_parent"  
    android:gravity="center"  
    android:persistentDrawingCache="animation"   
    android:flipInterval="1000"   
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
    android:orientation="vertical"  
    android:layout_width="match_parent" 
    android:layout_height="match_parent"> 
<CalendarView 
    android:id="@+id/preview" 
    android:layout_width="match_parent" 
    android:layout_height="match_parent"/> 
</LinearLayout> 
 
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
android:orientation="vertical"  
android:layout_width="match_parent" 
android:layout_height="match_parent"> 
<CalendarView 
    android:id="@+id/nextview" 
    android:layout_width="match_parent" 
    android:layout_height="match_parent"/> 
 
</LinearLayout> 
</ViewFlipper> 
ViewFlipper裡有2個CalendarView。如何實現翻頁呢?如果隻需要一個方向的翻頁,則在配置文件寫好就好瞭,但是如果需要左右翻,或者上下翻,則需要在ViewFlipper裡添加2個如showNext,showPrevious的方法(因為在AppWidget無法用RemoteView向ViewFlipper傳動畫對象),並將動畫文件放到framework裡,示例如下:
[java] 
/@hide*/ 
 
@android.view.RemotableViewMethod 
public void showPreviousAppWidget(String str) { 
setInAnimation(AnimationUtils.loadAnimation(this.getContext(), com.android.internal.R.anim.slide_in_right)); 
setOutAnimation(AnimationUtils.loadAnimation(this.getContext(), com.android.internal.R.anim.slide_out_left)); 
setDisplayedChild(mWhichChild – 1); 

 
/@hide*/ 
@android.view.RemotableViewMethod 
public void showNextAppWidget(String str) { 
setInAnimation(AnimationUtils.loadAnimation(this.getContext(), com.android.internal.R.anim.slide_in_left)); 
setOutAnimation(AnimationUtils.loadAnimation(this.getContext(), com.android.internal.R.anim.slide_out_right)); 
setDisplayedChild(mWhichChild + 1); 

同樣,這個兩個方法將在AppWidget裡被調用,所以在方面前加 @android.view.RemotableViewMethod標識。參數str其實隻是為瞭匹配
void  java.lang.String, java.lang.CharSequence) setCharSequence(int viewId,  String methodName,  CharSequence value)的第三個參數。在AppWidget裡調用  java.lang.String, java.lang.CharSequence) setCharSequence方法時第三個參數傳入“”。
那麼,又是如何在Appwidget實現onTouch事件呢?其實同樣簡單,就是在自定義的RemoteView裡加上GestureDetector,用來識別手勢,並發送相應的廣播給AppWidget,讓Appwidget作相應的處理,如上滑下滑的手勢。

3.6 總結
然後我說說遇到的一些問題和使用AppWidget的建議。
1、查看AppWidget源碼知道,AppWidget是繼承BroadcastReceiver,並且AppWidget的onUpdate,onDeleted,onEnabled,onDisabled四個方法都是從onReceive分化出來的,如何讓AppWidget接收廣播呢?重寫onReceive即可,隻是最後一定要調用AppWidgetManager.updateAppWidget(appWidgetIds, views)這個方法。
2、BroadcastReceiver的生命周期是短暫的,接收到消息很快就銷毀瞭,如何在接收到消息後需要做大量計算的,最好在接收到消息後啟動一個service,並將數據傳給service,讓service幫我們去計算。當然,這裡還有一個關鍵,就是要把int[] appWidgetIds通過一個share類傳到service,當service執行玩任務以後,調用AppWidgetManager.updateAppWidget(appWidgetIds, views)刷新AppWidget.
3、AppWidget是定期或定期刷新的,所以我們有必要將靜態變量放在一個share類裡,也可利用share類傳appWidgetIds值,這樣方便在service對AppWidget做刷新。

作者:fangchongbory

發佈留言