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 呢,我来教你搜一下:

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.

  1. 记着在这个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类型。下面看一个实例:

/*自定义的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 如何调用*/

...

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使用差别不大,先看布局文件:











ViewFlipper里有2个CalendarView。如何实现翻页呢?如果只需要一个方向的翻页,则在配置文件写好就好了,但是如果需要左右翻,或者上下翻,则需要在ViewFlipper里添加2个如showNext,showPrevious的方法(因为在AppWidget无法用RemoteView向ViewFlipper传动画对象),并将动画文件放到framework里,示例如下:

    /@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做刷新。






你可能感兴趣的:(Android)