Android App Widgets总结

简介

App Widgets 是一个可以嵌入到其他app(比如 HomeScreen)的能接收到周期性更新的一个微型的App 界面。这个view对用户来说相当于一个小插件或者小部件。如果我们把这个小部件放到桌面的话,可以方便用户不用打开app而能查看到重要的消息。

重要的类

AppWidgetProviderInfo

一个描述appWIdget的类,此类中的信息表示我们的app widget layout、最小高宽度或者更新频率等等信息。

AppWidgetProvider

这个类非常重要,我们在开发的时候也会继承它。这个类集成自broadcastreceiver,而且我们可以跳进它源码中看到,在它的onReceive()里面分发了很多事件,比如更新app widget会分发到onUpdate(),第一次添加app widget会分发到onEnable(),最后一个app widget删除后,会调用onDisable()等等。

RemoteViews

一个用于远程传输的view,其实它不是一个view,它会把我们的view和对该view的操作用action封装起来,然后序列化传输到另外的进程,然后反序列化,然后再在另外的进程对view进行我们要进行的操作。

AppWidgetManager

连接app进程和我们widget所处的进程的一个桥梁。后面详细讲。

用法

我们知道,一个app widget是放在homeScreen或者锁屏界面的,所以它实际上是运行在其他进程的,所以这里得用到进程间通信。这也是为什么开发一个app widget很麻烦的原因。

AndroidManifest.xml

首先在我们的AndroidManifest.xml中声明一个AppWidgetProvider用于接收app widget的更新事件。AppWidgetProvider实际上是一个广播,所以注册方式和广播类似。

<receiver android:name="ExampleAppWidgetProvider" >
    <intent-filter>
        <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
    </intent-filter>
    <meta-data android:name="android.appwidget.provider"
               android:resource="@xml/example_appwidget_info" />
</receiver>

上面代码中有一个meta-data,后面的name必须是android.appwidget.provider。因为系统正是根据这个来找到我们widget的配置文件的,下面resource就是它的配置文件,它对应着上面的AppWidgetProviderInfo。我们在resource中配置的所有配置项都可以在这个类中找到。
xml/example_appwidget_info.xml:

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:minWidth="40dp"
    android:minHeight="40dp"
    android:updatePeriodMillis="86400000"
    android:previewImage="@drawable/preview"
    android:initialLayout="@layout/example_appwidget"
    android:configure="com.example.android.ExampleAppWidgetConfigure"
    android:resizeMode="horizontal|vertical"
    android:widgetCategory="home_screen">
</appwidget-provider>
  • android:updatePeriodMillis:更新时间(默认30min),每过一段时间,系统会给我们的AppWidgetProvider发送一个update消息,我们可以在AppWidgetProvider中的onUpdate()中来处理更新widget界面。
  • android:previewImage:预览图标,用户在桌面选择widget的时候可以看到这个图标。和ic_launcher有点类似的感觉。
  • android:initialLayout:layout,不用说都知道
  • android:configure:配置。要是有需要的话,用户在添加widget的时候会打开这个Activity。来自定义配置相关的参数。
  • android:resizeMode:widget可以被拉伸的方向。horizontal表示可以水平拉伸,vertical表示可以竖直拉伸
  • android:widgetCategory: widget可以显示的地方,可以是桌面,也可以是锁屏。

在添加configure的时候,我们也要在AndroidMainfest.xml中为要打开的Activity设置intent-filter:

<activity android:name=".ExampleAppWidgetConfigure">
    <intent-filter>
        <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
    </intent-filter>
</activity>

layout

由于widget不是跑在我们自己的进程,所以对其界面有限制,因为系统会用到binder通信,把我们的传到另外一个进程去。

widget现在支持的布局有:

FrameLayout
LinearLayout
RelativeLayout
GridLayout

支持的控件有:

AnalogClock
Button
Chronometer
ImageButton
ImageView
ProgressBar
TextView
ViewFlipper
ListView
GridView
StackView
AdapterViewFlipper

其实RemoteViews也支持ViewStub

margin

在android4.0以前,我们得为widget的边缘添加margin,因为它不能触碰到屏幕边界。
在android 4.0以后,系统默认为我们添加了padding,所以我们在android4.0以后不用管。

AppWidgetProvider

我们的widget对应着AppWidgetProvider,我们在AppWidgetProvider中接收系统发给我们的更新、添加、删除widget消息。

onUpdate()
我们可以在这个函数里面做一些更新界面的事情。当updatePeriodMillis所设置的时候到达后,就会运行这个函数。

onEnable()
用户添加第一个widget的时候会调用

onDisable()
和onEnable相反,删除最后一个widget的时候调用。

onAppWidgetOptionsChanged()
widget尺寸改变的时候调用(用户可以调整大小)。

onDeleted()
用户每次删除widget的时候调用

onReceive(Context, Intent)
上述函数都在这个函数里面分发。也就是说,实际上系统是给app发的一个个广播事件。在这个函数中根据不同的事件,调用上述不同的函数。

public class ExampleAppWidgetProvider extends AppWidgetProvider {

    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        final int N = appWidgetIds.length;//该widget个数
        for (int i=0; i<N; i++) {
            int appWidgetId = appWidgetIds[i];
      //pendingIntent是一种还没有发生的intent
      //我们可以用pendingIntent来为view设置一个onClick后调用的Intent事件。
      //这里是当用户点击了R.id.button按钮后,用intent打开ExampleActivity。 
           Intent intent = new Intent(context, ExampleActivity.class);
            PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
            RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.appwidget_provider_layout);
            views.setOnClickPendingIntent(R.id.button, pendingIntent);

            //用appwidgetManager来通知系统更新widget
            appWidgetManager.updateAppWidget(appWidgetId, views);
        }
    }
}

上面代码只是一点击widget是某个button打开activity的例子。当然我们也可以在这里用PendingIntent来设置一个事件,让用户点击某个控件后可以向我们发送一个广播,然后在接收到广播后做我们想做的事情。

Intent intent = new Intent();
intent.addAction(xxx);
PendingIntent ppendingIntent = PendingIntent.getBroadcast(context, 0, intent, 0);

当然,我们得在ExampleAppWidgetProvider加入相应的action。

到这里,widget基本的操作已经完成。

Widget配合ListView等Collections使用

在我们日常开发中,ListView等Collections的使用是必不可少的,这一节就讲一下关于如何在widget中使用collections,我这里用listview为代表,其他的都类似。

前面没有Collections的时候,直接用RemoteViews就可以直接填充视图,然后加上Listview后,你会发现,没法来填充ListView里面的Item。

这里就要介绍下在用Collections的时候,其他几个重要的类了:

  • RemoteViewsService :一个连接widget和app进程的service,我们可以继承这个类,来提供一个adapter给ListView/GridView
  • RemoteViewsService.RemoteViewsFactory:一个类似BaseAdapter一样的东西,我们可以直接把它看成ListView的Adapter。

有了这两个类就好说了。首先RemoteViewsService 是一个Service,所以我们得在manifest.xml里面注册,而且得申明绑定widget的权限。

<service android:name="MyWidgetService"
...
android:permission="android.permission.BIND_REMOTEVIEWS" />

申明后,我们的ListView得这么用(官方栗子):

public void onUpdate(Context context, AppWidgetManager appWidgetManager,
int[] appWidgetIds) {
    //初始化listView并指定adapter
    for (int i = 0; i < appWidgetIds.length; ++i) {

        //先new一个intent来启动service
        //这个service会提供给我们listview的item的界面。
        Intent intent = new Intent(context, MyWidgetService.class);
        //把app widget id 放到intent里面
        intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]);
        intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
        //widget_layout.xml里面包含一个listview
        RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.widget_layout);
    
        //为listview指定一个 remote adapter,这个adapter会用intent连接到一个特定的service(MyWidgetService)
        //我们后面就可以在service里面来提供数据。
        rv.setRemoteAdapter(appWidgetIds[i], R.id.list_view, intent);
        //为我们的listview指定一个empty view,这个empty view得也在上面的widget_layout里面
        rv.setEmptyView(R.id. , R.id.empty_view);

            //这里为了让我们的item可以设置点击事件,我们得为listview设置一个pending intent template(一个pendingIntent模板)
            //然后后面我们会在adapter里面为每个item设置一个fillInIntent
            Intent toastIntent = new Intent(context, ExampleAppWidgetProvider.class);
            //这里设置了一个action。
            //ExampleAppWidgetProvider.TOAST_ACTION是我们自定义的一个action。这里得把这个action添加到广播接收者里面。以便我们能接收到按下item后的广播事件。
            toastIntent.setAction(ExampleAppWidgetProvider.TOAST_ACTION);
            toastIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]);
            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.list_view, toastPendingIntent);

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

下面我们来看看MyWidgetService

public class MyWidgetServiceextends RemoteViewsService {
    @Override
    public RemoteViewsFactory onGetViewFactory(Intent intent) {
    //为widget提供一个adapter
        return new MyWidgetService(this.getApplicationContext(), intent);
    }
}

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

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

    // 初始化data
        public void onCreate() {
           
            //这里不建议下载或者做一些很重的操作,因为这里停留20s,你就会收到一个ANR的奖励。
            for (int i = 0; i < mCount; i++) {
                mWidgetItems.add(new WidgetItem(i + "!"));
            }
           ...
        }
        ...

      //相当于baseAdapter的getView()方法。
        public RemoteViews getViewAt(int position) {
           //这里用layout创建一个RemoteVies。
            RemoteViews rv = new RemoteViews(mContext.getPackageName(), R.layout.widget_item);
            rv.setTextViewText(R.id.widget_item, mWidgetItems.get(position).text);
            //这里用fill-intent来填充前面设置的Intent模板
            Bundle extras = new Bundle();
            extras.putInt(ExampleAppWidgetProvider.EXTRA_ITEM, position);
            Intent fillInIntent = new Intent();
            fillInIntent.putExtras(extras);
            //为item设置intent onClick。当点击后,会发出一个特定intent的广播。我们在ExampleAppWidgetProvider里面接收广播就可以做我们想做的事了。
            //但是这里还是要记得在AndroidManifest.xml里面ExampleAppWidgetProvider申明接收这个action。
            rv.setOnClickFillInIntent(R.id.widget_item, fillInIntent);

            ...

            // 返回view
            return rv;
        }
    ...
    }

但是如果我们要更新listview中的数据的话,下面官方文档里面给出一一幅图,很好地描述了工作流程,我们只要更新数据后,调用AppWidgetManager.notifyAppWidgetViewDataChanged()就可以了。

appwidget_collections.png

大坑提醒:要是在开发中发现程序跑出来的效果不是我们所想象的, 最好把app下载了,然后重新添加桌面小插件可能就解决问题了,我在实际开发的时候,被这个问题坑了好几次==。

RemoteViews的工作原理

看了上面的开发流程,我们来大概分析一下它的工作原理。

RemoteViews主要用于通知栏和桌面小部件,然而通知栏和桌面小部件又是分别由NotificationManagerAppWidgetManager管理的。而我们在开发的时候,经常还很把它们和NotificationManagerService或者AppWidgetService一起使用。而我们在其他Android开发的时候,经常会遇到各种Manager。还有很多的Service,比如WindowManagerServiceWindowManager。用多了我们就会发现,它们的用法有很多相似的地方。

要是稍微观察一下它们的工作机制的话,就会发现它们都是差不多的。

其实AppWidgetManager是通过BinderSystemServer进程中的AppWidgetService通信的。

由此可见,桌面小插件实际上是在AppWidgetService被加载的。而它们又是运行的SystemServer中,这就满足了跨进程通信的条件。

我们发现RemoteViews实际上是实现了Parcelable接口的,所以它也是便于在Binder中进行跨进程传输的。

还记得我们在new RemoteViews的时候,是传递了一个包名进去的么?
其实系统会根据RemoteViews中指定的包名去得到相应的资源,然后用LayoutInflater来加载布局文件。这就在SystemServer中加载了一个View了。然后还记得我们对View的操作都是用set来完成的么?比如:

rv.setTextViewText(R.id.text_view,"hello");

然后来看看源码:

 public void setTextViewText(int viewId, CharSequence text) {
        setCharSequence(viewId, "setText", text);
    }
    
 public void setCharSequence(int viewId, String methodName, CharSequence value) {
        addAction(new ReflectionAction(viewId, methodName, ReflectionAction.CHAR_SEQUENCE, value));
    }

我们发现为viewsetText的时候,本来平常view是通过view.setText(xxx)来完成的,刚好这里把我们的id和方法setText还有text记录下来,然后new了一个action.其实这里的ReflectionAction就是继承Action的。然后再addAction();

其实在RemoteViews里面有一个ArrayList<Action>。addAction()会把我们的Action放到list里面。当把这个RemoteViews传输到SystemServer的时候 ,系统会调用RemoteViewsapply()方法:

 public View apply(Context context, ViewGroup parent, OnClickHandler handler) {
        RemoteViews rvToApply = getRemoteViewsToApply(context);

        View result;
    LayoutInflater inflater = (LayoutInflater)
                context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

        // Clone inflater so we load resources from correct context and
        // we don't add a filter to the static version returned by getSystemService.
        inflater = inflater.cloneInContext(inflationContext);
        inflater.setFilter(this);
        result = inflater.inflate(rvToApply.getLayoutId(), parent, false);

        rvToApply.performApply(result, parent, handler);
        return result;
        }

印证了刚刚说了用inflater加载布局,然后再调用perforApply()方法:

    private void performApply(View v, ViewGroup parent, OnClickHandler handler) {
        if (mActions != null) {
            handler = handler == null ? DEFAULT_ON_CLICK_HANDLER : handler;
            final int count = mActions.size();
            for (int i = 0; i < count; i++) {
                Action a = mActions.get(i);
                a.apply(v, parent, handler);
            }
        }
    }

遍历Action ,然后调用其apply()方法。我们来看看刚刚的ReflectionActionapply()方法:

  public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
            final View view = root.findViewById(viewId);
            if (view == null) return;

            Class<?> param = getParameterType();
            if (param == null) {
                throw new ActionException("bad type: " + this.type);
            }

            try {
                getMethod(view, this.methodName, param).invoke(view, wrapArg(this.value));
            } catch (ActionException e) {
                throw e;
            } catch (Exception ex) {
                throw new ActionException(ex);
            }
        }

豁然开朗,用反射调用我们实际操作的view.setText()

首先序列化,再通过Binder传递对象,然后再在SystemServer中来apply我们的操作。这就完成了我们想要的操作,方法非常巧妙。

总结

跨进程操作很巧妙,我们可以通过学习google的做法,在我们实际开发当中,也有不少的跨进程操作,我们也可以用这种方式来实现操作其他对象。特别是在开放SDK中,这种方式非常有帮助,异步调用要执行的代码。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章