Android桌面小部件与RemoteViews

标签(空格分隔): Android


一、简介

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

二、使用小部件

1.定义小部件界面

<?xml version="1.0" encoding="utf-8"?><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

<?xml version="1.0" encoding="utf-8"?><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);            }        }    }

更多相关文章

  1. [译]提升Android应用性能的小贴士
  2. Android(安卓)APK应用安装原理(1)-解析AndroidManifest原理-Pack
  3. [置顶] android软键盘弹出,会把原来的界面挤上去的问题 处理方法
  4. Android(安卓)面试精华题目总结
  5. android 启动默认的邮件客户端,多附件的问题
  6. Android设置背景色为透明的两种方法
  7. Android多module初始化application
  8. 浅谈Java中Collections.sort对List排序的两种方法
  9. Python list sort方法的具体使用

随机推荐

  1. MYSQL必知必会-SQL语句查询
  2. 《高性能MySQL》学习笔记一
  3. mysql数据版本控制系统的最佳实践
  4. Log4j2记录日志到数据库(MySQL&MongoDB)
  5. php mysql有条件地将行插入到不同的表中
  6. MySQL架构:空列与联接
  7. CentOS 6.4 安装 JAVA + MYSQL + APACHE
  8. 为什么准备好的语句由每个会话管理?
  9. 程序猿(媛)Shell脚本必备技能之一: 在Lin
  10. ThinkPHP2.1 读取SqlServer数据(并解决Sql