Android桌面小部件与RemoteViews

Android桌面小部件与RemoteViews

标签(空格分隔): Android


一、简介

  App Widget是应用程序窗口小部件(Widget)是微型的应用程序视图,它可以被嵌入到其它应用程序中(比如桌面)并接收周期性的更新。你可以通过一个App Widget Provider来发布一个Widget。
  小部件通过AppWidgetProvide来实现,AppWidgetProvide本质上是一个广播,在小部件的开发过程中会用到RemoteViews,因为小部件在更新界面时无法像在Activity中直接更新View,这是因为它的界面运行在其他进程中,确切来说是在系统的SystemServer进程中。为了跨进程更新界面,RemoteViews中提供一系列的set方法,并且这些方法只是View全部方法的子集,而且它支持的View类型也是有限的。

二、使用小部件

1.定义小部件界面


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    >
 <TextView
    android:id="@+id/tv_widget_content"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="点击更换内容"
    android:textSize="16sp"
   >
  <Button
    android:id="@+id/btn_widget_openactivityt"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="点击打开MainActivity"
    android:textSize="16sp"
   >
LinearLayout>

2.定义小部件配置信息

在res/xml/下新建一个appwidget_provide_info.xml


<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:initialLayout="@layout/widget"
    android:minHeight="84dp"
    android:minWidth="84dp"
    android:updatePeriodMillis="86400000"
   >
appwidget-provider>

其中initialLayout-初始化布局,updatePeriodMillis-自动更新周期。

3.定义小部件的实现类

public class MyWidgetProvider extends AppWidgetProvider {

    public static final String TAG = "MyWidgetProvider";

    public static final String CLICK_ACTION = "com.gaop.HutHelper.action_click";

   @Override
    public void onReceive(Context context, Intent intent) {
        super.onReceive(context, intent);//不可去掉
        if(intent.getAction().equals(CLICK_ACTION)){
        RemoteViews remoteviews = new RemoteViews(context.getPackageName(), R.layout.widget);
        remoteviews.setTextViewText(R.id.tv_widget_content, "已点击");

        //获得appwidget管理实例,用于管理appwidget以便进行更新操作
        AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
        //相当于获得所有本程序创建的appwidget
        ComponentName componentName = new ComponentName(context, MyWidgetProvider.class);
        //更新appwidget
        appWidgetManager.updateAppWidget(componentName, remoteviews);
        }
    }


    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        final int counter = appWidgetIds.length;
        for (int i = 0; i < counter; i++) {
            int appWidgetId = appWidgetIds[i];
            onWidgetUpdate(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
    }

    private void onWidgetUpdate(Context context, AppWidgetManager appWidgetManager, int appWidgetId) {

        RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget);

        Intent intent = new Intent(context, MyWidgetProvider.class);
        intent.setAction(CLICK_ACTION);
        PendingIntent pendingIntentpre = PendingIntent.getBroadcast(context, 0, intent, 0);
        //打开MainActivity
        Intent intent2 = new Intent(context,MainActivity.class);
        PendingIntent pendingIntent2 = PendingIntent.getActivity(context, 0, intent2, 0);
        remoteViews.setOnClickPendingIntent(R.id.btn_widget_openactivity, pendingIntent2);

        appWidgetManager.updateAppWidget(appWidgetId, remoteViews);
    }
}

上面实现了一个特别简单的小部件,点击TextView更换内容,点击Button打开MainActiviy。当小部件添加到桌面时,会通过RemoteViews来加载布局文件,点击更换效果则是通过不断更新RemoteViews来实现的。

4.在AndroidManifest.xml中声明小部件

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

当然这是一个特简单的例子,具体开发会复杂很多。

三、小部件常用方法

AppWidgetProvider除了最常用的onUpdate方法,还有onEnable、onDisabled、onDeleted、onReceive。它们都自动的在合适的时间调用(其实就是onReceive方法中的super()所做的:AppWidgetProvider会自动的根据广播的Action通过onReceive方法分发广播)。以下为调用时机:

  • onEnable:小部件第一次添加到桌面时调用,添加多次也只会调用一次。
  • onUpdate:小部件被添加到桌面或者更新时就会调用一次该方法。
  • onDelete:每删除一个小部件,调用一次。
  • onDisabled:当最后一个小部件被删除时调用。
  • onReceive:广播的内置方法,用于分发具体事件。

以下为onReceive方法的分发过程:

public void More ...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);
                 }
             }
         }
     }

四、PendingIntent

上面的例子中使用到PendingIntent,这其实是一种等待(pending)状态的intent,就是intent将在接下来的某个待定时间发生,而Intent是立即发生。
给RemoteViews添加点击事件就是一个典型的使用场景,因为RemoteViews无法直接向View那个通过setOnClickListener来设置点击事件,PendingIntent通过send和cancel来发送、取消待定的Intent。
它支持三种特定的意图:
1. getActivity(Context context,int requestCode,Intent intent,int flags) 获取一个PendingIntent,发生时效果相当于startActivity(Intent intent).
2. getService(Context context,int requestCode,Intent intent,int flags) 获取一个PendingIntent,发生时效果相当于startService(Intent intent).
3. getBroadcast(Context context,int requestCode,Intent intent,int flags) 获取一个PendingIntent,发生时效果相当于sendBroadcast(Intent intent).

其中requestCode为发送方的请求码,一般设为0,同时它也会影响flags的效果。flags常见的类型有:
1. FLAG_ONE_SHOT:当前PendingIntent只能被使用一次,然后就被cancel,如果后续有相同的PendingInent,它们的send方法都会调用失败。
2. FLAG_NO_CREATE:当前PendingIntent不会主动创建,如果当前PendingIntent之前不存在,那么get××()方法直接返回null。
3. FLAG_CANCEL_CURRENT:当前PendingIntent如果不存在,那么它们都会被cancel,然后系统会构建一个新的PendingIntent。
4. FLAG_UPDATE_CURRENT:如果当前PendingIntent存在,那么它会被更新。

然后再说下PendingIntent的匹配规则:如果两个PendingIntent的Intent相同而且requestCode也相同,那么两个PendingIntent就是相同的。其中Intent的匹配规则为:如果两个Intent的ComponentName和intent-filter都相同,则它们相同。

五、RemoteViews的内部机制

首先看一下RemoteViews的构造方法: public(String packagename,int layoutid);
其中Remoteviews并不支持全部Layout和View,只支持以下:
layout:
FrameLayout LinearLayout RelativeLayout GridLayout
View:
AnalogClock button Chronometer ImageButton ImageView ProgressBar TextView ViewFlipper ListView GridView StackView AdapterViewFilpper ViewStub

由于RemoteViews没有findViewById的方法 因为它是远程的View即使findViewById我们也不知道远程app的资源文件,它提供了一系列的set方法(例如 setTextViewText,setImageViewResource)来设置内容。

接下来要说的是RemoteViews的工作过程,在小部件中AppWidgetManager是通过Binder与在SystemServer进程中的AppWidgetService通信。可见,小部件的布局文件实际是在SystemServer中加载的。
首先RemoteViews(实现了Parcelable接口)通过Binder传递到SystemServer进程,系统根据RemoteViews中的包名等信息获取该应用的资源。然后通过LayoutInflater加载RemoteViews中的布局文件。接着系统会根据通过我们的set方法提交的内容来更新View,这样小部件就可以使用了。
下面再说一下更新View的具体实现:Android这里提供了一个Action的概念,Action代表一个View操作,同样,它也实现了Parcealable接口,在RemoteViews中有一个mActions的ArrayList对象,每调用一次set方法,RemoteViews就会创建相应的Action添加到mActions中,然后通过Binder将包含mActions的RemoteViews传递到SystemServer进程中。接着调用RemoteViews的apply方法来更新View,这个方法将遍历每个Action并调用它们的apply方法,根据Action的不同来更新不同View的内容。下面根据setTextViewText方法的源码分析下整个流程:

public void setTextViewText(int viewId, CharSequence text) {
        setCharSequence(viewId, "setText", text);
    }
public void setCharSequence(int viewId, String methodName, CharSequence value) {
        //ReflectionAction为一个通用Acton,通过反射调用得到具体的Method对象
        addAction(new ReflectionAction(viewId, methodName, ReflectionAction.CHAR_SEQUENCE, value));

    }
private void addAction(Action a) {
        if (hasLandscapeAndPortraitLayouts()) {
            throw new RuntimeException("RemoteViews specifying separate landscape and portrait" +
                    " layouts cannot be modified. Instead, fully configure the landscape and" +
                    " portrait layouts individually before constructing the combined layout.");
        }
        if (mActions == null) {
            mActions = new ArrayList();
        }
        mActions.add(a);

        // update the memory usage stats
        a.updateMemoryUsageEstimate(mMemoryUsageCounter);
    }
public View apply(Context context, ViewGroup parent, OnClickHandler handler) {
        RemoteViews rvToApply = getRemoteViewsToApply(context);

        View result = inflateView(context, rvToApply, parent);
        loadTransitionOverride(context, handler);

        rvToApply.performApply(result, parent, handler);

        return result;
    }
private View inflateView(Context context, RemoteViews rv, ViewGroup parent) {
        // RemoteViews may be built by an application installed in another
        // user. So build a context that loads resources from that user but
        // still returns the current users userId so settings like data / time formats
        // are loaded without requiring cross user persmissions.
        final Context contextForResources = getContextForResources(context);
        Context inflationContext = new ContextWrapper(context) {
            @Override
            public Resources getResources() {
                return contextForResources.getResources();
            }
            @Override
            public Resources.Theme getTheme() {
                return contextForResources.getTheme();
            }
            @Override
            public String getPackageName() {
                return contextForResources.getPackageName();
            }
        };

        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);
        return inflater.inflate(rv.getLayoutId(), parent, false);
    }
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);
            }
        }
    }

你可能感兴趣的:(Android)