Android中RemoteViews应用与原理:通知栏和桌面小部件

关键字:RemoteView、通知栏、桌面小部件

转载请注明链接:http://blog.csdn.net/feather_wch/article/details/79175518

本文包含RemoteView在通知栏和桌面小部件上的使用、RemoteViews的内部原理、RemoteViews的意义。
所有知识点都以面试提问的形式总结。

RemoteViews应用与原理:通知栏和桌面小部件

版本:2018/2/13-1


  • RemoteViews应用与原理通知栏和桌面小部件
    • RemoteViews在通知栏上的应用
    • RemoteViews在桌面小部件上的应用
    • PendingIntent
    • RemoteViews的内部机制
    • RemoteViews源码分析

1、RemoteViews是什么?

  1. 是一种远程View,表示一个View结构
  2. 可以在其他进程中显示,也提供了相应的基础操作
  3. Android中有两种使用场景: 通知栏、桌面小部件

2、RemoteViews在应用中的要点

  1. 通知栏开发中主要是通过NotificationManagernotify方法来实现,也支持自定义布局
  2. 桌面小部件主要是通过AppWidgetProvider来实现的,AppWidgetProvider本质上是一个广播
  3. 两个使用场景都是运行在系统的SystemServer进程中,为了跨进程更新界面,RemoteViews提供一些列set方法
  4. RemoteViews中支持的View类型是有限的

RemoteViews在通知栏上的应用

3、系统默认样式通知栏实现(Android6.0版):

//1. 创建PendingIntent,用于点击通知栏跳转
val intent = Intent(this@MainActivity ,MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(this@MainActivity, 0,
        intent, PendingIntent.FLAG_UPDATE_CURRENT)
//2. 实例化Notification (By 建造者模式)
val notification = Notification.Builder(this)
        .setSmallIcon(R.mipmap.ic_launcher) //小图标
        .setContentTitle("通知标题")
        .setContentText("通知内容")
        .setWhen(System.currentTimeMillis()) //通知时间:默认为系统发出通知的当前时间,可以不设置
        .setContentIntent(pendingIntent)
        .build()
//3. 获取notificationManager实例
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
//4. 通过notificationManager发送通知,id=1
notificationManager.notify(1, notification)

4、RemoteViews实现自定义通知:

        //1. 通过自定义布局创建RemoteViews
        val remoteViews = RemoteViews(packageName, R.layout.layout_remoteview)
        //2. 设置远程的ImageView
        remoteViews.setImageViewResource(R.id.remote_imageview, //控件ID
                R.mipmap.ic_launcher)  //ImageView资源
        //3. 设置远程TextView
        remoteViews.setTextViewText(R.id.remote_textview, "RemoteView中自定义文本")
        //4. 设置远程Button的点击事件
        val remoteButtonPendingIntent = PendingIntent.getActivity(this@MainActivity,
                0,
                Intent(this@MainActivity ,Main2Activity::class.java),
                PendingIntent.FLAG_UPDATE_CURRENT)
        remoteViews.setOnClickPendingIntent(R.id.remote_button, remoteButtonPendingIntent)

        //5. 给自定义Notification设置RemoteViews和PendingIntent(点击效果)
        val intent = Intent(this@MainActivity ,MainActivity::class.java)
        val pendingIntent = PendingIntent.getActivity(this@MainActivity, 0,
                intent, PendingIntent.FLAG_UPDATE_CURRENT)
        val notification = Notification.Builder(this)
                .setContent(remoteViews)   //设置RemoteViews和PendingIntent
                .setContentIntent(pendingIntent)
                .setSmallIcon(R.mipmap.ic_launcher) //小图标
                .setWhen(System.currentTimeMillis()) //通知时间:默认为系统发出通知的当前时间,可以不设置
                .build()
        //6. 获取notificationManager实例
        val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        //7. 通过notificationManager发送通知,id=1
        notificationManager.notify(1, notification)

布局:

<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <ImageView
        android:id="@+id/remote_imageview"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_alignParentRight="true"/>
    <TextView
        android:id="@+id/remote_textview"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:gravity="clip_vertical"/>
    <Button
        android:id="@+id/remote_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="按钮"
        android:layout_toRightOf="@id/remote_textview"/>
RelativeLayout>

RemoteViews在桌面小部件上的应用

5、AppWidgetProvider是什么?

  1. 实现桌面小部件的类
  2. 直接继承自BroadcastReceiver
  3. 实际开发中直接当成BroadcastReceiver

6、AppWidgetProvider实现步骤

  1. 自定义Widget的布局(样式)
  2. 定义Widget的规格大小
  3. 实现Widget继承自AppWidgetProvider(本质是广播)
  4. AndroidManifest中进行注册
  5. res/layout 中自定义小部件的布局app_widget.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/appwidget"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#09C"
    android:padding="@dimen/widget_margin"
    android:orientation="horizontal">

    <TextView
        android:id="@+id/appwidget_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="8dp"
        android:background="#09C"
        android:contentDescription="@string/appwidget_text"
        android:text="@string/appwidget_text"
        android:textColor="#ffffff"
        android:textSize="12dp"
        android:textStyle="bold|italic" />

    <Button
        android:id="@+id/appwidget_button"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:text="Button"
        android:gravity="center"/>
LinearLayout>
  1. res/xml中定义小部件的规格大小等app_widget_info.xml

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:initialKeyguardLayout="@layout/app_widget"
    android:initialLayout="@layout/app_widget"
    android:minHeight="40dp"
    android:minWidth="110dp"
    android:previewImage="@drawable/example_appwidget_preview"
    android:resizeMode="horizontal|vertical"
    android:updatePeriodMillis="86400000"
    android:widgetCategory="home_screen">
appwidget-provider>
  1. 实现小部件AppWidget继承自AppWidgetProvider
public class AppWidget extends AppWidgetProvider {
    public static final String CLICK_ACTION = "com.example.action.CLICK";
    /**
     * 每次小部件更新时,都会调用一次该方法
     */
    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        // 可能有多个widget被激活,因此全部更新
        for (int appWidgetId : appWidgetIds) {
            updateAppWidget(context, appWidgetManager, appWidgetId);
        }
    }
    /**
     * 更新Widget,被onUpdate调用
     */
    static void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) {
        //1. 构造RemoteViews(通过布局app_widget),并给远程TextView设置值
        RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.app_widget);
        remoteViews.setTextViewText(R.id.appwidget_text, "Hello");
        //2. 设置远程View的点击事件-getBroadcast获取广播
        Intent intent = new Intent();
        intent.setAction(CLICK_ACTION); //设置自定义ACTION
        PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
        remoteViews.setOnClickPendingIntent(R.id.appwidget_button, pendingIntent);
        //3. 构造WidgetManager来更新部件
        appWidgetManager.updateAppWidget(appWidgetId, remoteViews);
    }
    /**
     * 接受广播并完成事件的分发: 分发给onEnable\onUpdate\onDeleted\onDisabled
     */
    @Override
    public void onReceive(Context context, Intent intent) {
        //1. 必须有, 完成事件的分发
        super.onReceive(context, intent);
        //2. 接收到自定义的ACTION
        if(intent.getAction().equals(CLICK_ACTION)){
            //3. 通过RemoteViews更新UI
            RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.app_widget);
            remoteViews.setTextViewText(R.id.appwidget_text, "onReceive");
            AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
            ComponentName componentName = new ComponentName(context, AppWidget.class);
            appWidgetManager.updateAppWidget(componentName, remoteViews);
        }
    }
}
  1. AndroidManifest.xml定义该小部件
<receiver android:name=".AppWidget">
            <intent-filter>
                //自定义的ACTION-点击事件
                <action android:name="com.example.action.CLICK"/>
                //AppWidget必须要有该Action
                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
            intent-filter>
            <meta-data
                android:name="android.appwidget.provider"
                android:resource="@xml/app_widget_info" />
receiver>

7、Wdiget小部件的minWidth/minHeight的尺寸

单元格个数(行或列) 对应设置大小(dp)(minWidth或minHeight)
1 40dp
2 110dp
3 180dp
4 250dp
n 70 * n - 30

PendingIntent

8、PendingIntent是什么?

  1. 表示一种pending状态的意图Intent-也就是指待定、等待、即将发生的意图
  2. PendingIntent就在将来的某个不确定的时刻发生
  3. Intent是立即发生
  4. PengdingIntent典型场景就是给RemoteViews添加单击事件(无法通过setOnClickListener的方式添加)
  5. PengdingIntent有三种待定意图:启动Activity、启动Service和发送广播

9、PengdingIntent的三种待定意图

意图 方法 备注
Activity getActivity(Context context, int requestCode, Intent intent, int flags) 该意图发生时,等效于Context.startActivity(Intent)
Service getService(Context context, int requestCode, Intent intent, int flags) 该意图发生时,等效于Context.startService(Intent)
Braodcast getBroadcast(Context context, int requestCode, Intent intent, int flags) 该意图发生时,等效于Context.startBroadcast(Intent)

10、PendingIntent的匹配规则(何时两个PendingIntent相同)

  1. 匹配规则:当两个PendingIntent内部的Intent相同,并且requestCode(发送方请求码)相同,则两者为同一个PendingIntent.
  2. requestCode相同就是单纯的数值相同
  3. Intent的相同必须满足:两个Intent的ComponentNameintent-filter相同,则两者相同。因为extras不参与匹配过程,因此extras可以不同

11、Intent匹配规则中ComponentName是什么?

  1. ComponentName是指组件名,该类可以用于定义组件-如四大组件
  2. 通过给Intent设置ComponentName,可以进行如启动一个Activity等操作
// 通过ComponentName启动Activity
ComponentName componentName=new ComponentName("com.hao.myapplication", "com.hao.myapplication.Main2Activity");
Intent intent = new Intent();
intent.setComponent(componentName);
startActivity(intent);

12、PendingIntent的flags参数的具体含义

Flags 含义 备注
FLAG_ONE_SHOT 当前的PengdingIntent只能被使用一次,然后被自动cancel,如果有后续相同PendingIntent,它们的send都会调用失败 若通知栏消息被这样标记,则同类通知中只要有某一条消息被单击打开,其他以及后续的通知都将无法再单击打开
FLAG_NO_CREATE 当前PendingIntent不会主动创建:若当前PendingIntent之前不存在,则三大方法会直接返回null 极少见,一般没什么用
FLAG_CANCEL_CURRENT 当前PendingIntent如果已经存在,则都cancel掉,然后会创建新的PendingIntent 通知栏中只有最新的通知可以打开,之前弹出的通知均无法打开
FLAG_UPDATE_CURRENT 当前PendingIntent如果已经存在,则更新它们Intent中的extras 之前弹出的通知的PendingIntent都被更新,且都跟最新一条完全一致(包括extras),所有消息都可以打开

补充:通知栏消息的manager.notify(1, notification)中,如果ID一致,后续的通知都会直接替换前面的通知;如果ID不一致,且PendingIntent不匹配,则所有消息互不干扰;若PendingIntent匹配,则会根据上面的4种情况产生不同的结果。

RemoteViews的内部机制

13、RemoteViews支持的所有布局和View有哪些?

类型 详情
View(禁止任何自定义View) Button/ImageButton/ImageView/TextView/ListView/GridView/ProgressBar/AnalogClock/StackView/AdapterViewFlipper/ViewFlipper/ViewStub/Chronometer
Layout FrameLayout/LinearLayout/RelativeLayout/GridLayout

14、RemoteViews内部原理分析

  1. RemoteViews会通过Binder传递到SystemServer进程(RemoteViews实现了Parcelable接口)
  2. 系统会根据RemoteViews中的包名等信息,去获取该应用的资源
  3. 会通过LayoutInflater去加载RemoteViews中的布局文件
  4. 对于SystemServer进程,加载好布局后,就是一个普通的View。然而对于我们的进程是一个RemoteViews
  5. 接着系统会对View进行UI刷新(提交的一系列set方法)
  6. set方法的操作不会立即执行,RemoteViews内部会记录所有的UI更新操作,在RemoteViews被加载之后才会执行
  7. 最终RemoteViews完成了在SystemServer进程中的显示。后续的UI更新,就是通过NotificationManagerAppWidgetManager提交到SystemServer进程中完成相应操作

15、RemoteViews的set等操作内部细节

  1. 系统并没有通过Binder去支持View的跨进程访问
  2. RemoteViews提供了一种Action的概念,Action实现了Parcelable接口
  3. 系统将RemoteViews的一系列操作封装到Action对象中,并将Action跨进程传输到SystemServer进程,最后在远程进程中执行Action对象中的所有操作。
  4. 每调用一次set方法,RemoteViews中就会添加对应的Action对象,最终会传到远程进程中。
  5. 远程进程通过RemoteViews的apply方法进行View的更新操作(遍历所有Action对象,并调用其apply方法)

16、RemoteViews内部机制的优点

  1. 不需要定义大量的Binder接口
  2. 通过在远程进程中的批量操作,避免了大量的IPC操作,提高性能

RemoteViews源码分析

17、RemoteViews的set方法源码要点

    public void setTextViewText(int viewId, CharSequence text) {
        setCharSequence(viewId, // view的id
                     "setText", // 方法名
                         text);
    }
    public void setCharSequence(int viewId, String methodName, CharSequence value) {
        //1. 添加‘反射类型Action’对象
        addAction(new ReflectionAction(viewId, methodName, ReflectionAction.CHAR_SEQUENCE, value));
    }
    private void addAction(Action a) {
        //...
        //1. 添加Action到Action链表中
        if (mActions == null) {
            mActions = new ArrayList();
        }
        mActions.add(a); //添加action
        a.updateMemoryUsageEstimate(mMemoryUsageCounter); //更新内存使用状态
    }

18、RemoteViews的apply方法源码要点

    public View apply(Context context, ViewGroup parent, OnClickHandler handler) {
        RemoteViews rvToApply = getRemoteViewsToApply(context);
        //1. 去加载RemoteViews中的布局文件
        View result = inflateView(context, rvToApply, parent);
        loadTransitionOverride(context, handler);
        //2. 通过performApply执行一些更新操作
        rvToApply.performApply(result, parent, handler);
        return result;
    }
    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();
            //1. 遍历Actions链表,并执行Action的apply方法(真正操作View)
            for (int i = 0; i < count; i++) {
                Action a = mActions.get(i);
                a.apply(v, parent, handler);
            }
        }
    }
  1. 例如AppWidgetManagerupdateAppWidget的内部实现中,是通过RemoteViewsapply和reApply加载或更新界面
  2. apply: 加载布局并且更新界面
  3. reApply:只会更新界面
  4. 通知栏和桌面小部件初始化时会调用apply,后续更新都调用reapply

19、ReflectionAction源码解析:

private final class ReflectionAction extends Action {
        //省略成员变量和parcelable相关方法
        //1. 构造方法
        ReflectionAction(int viewId, String methodName, int type, Object value) {
            this.viewId = viewId;
            this.methodName = methodName;
            this.type = type;
            this.value = value;
        }
        @Override
        //1. 反射动作,对View的操作,会以反射的方式调用
        public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
            final View view = root.findViewById(viewId);
            if (view == null) return;
            //2. 获取参数的类型(如String.class等等)
            Class param = getParameterType();
            if (param == null) {
                throw new RemoteViews.ActionException("bad type: " + this.type);
            }
            //3. getMethod根据方法名来得到反射所需的Method对象
            try {
                //4. invoke给View设置上值value
                getMethod(view, this.methodName, param).invoke(view, wrapArg(this.value));
            } catch (RemoteViews.ActionException e) {
                throw e;
            } catch (Exception ex) {
                throw new RemoteViews.ActionException(ex);
            }
        }
        //...省略...
}
  1. ReflectionAction使用反射实现
  2. TextViewSizeAction就比较简单,没有使用反射来实现

20、RemoteViews中的PendingIntent的点击事件

分类 作用
setOnClickPendingIntent 给普通View设置点击事件,禁止给(ListView、StackView)等集合中的View设置点击事件(开销太大)
setPendingIntentTemplate/setOnClickFillIntent 组合使用-用于给ListView等View中的item添加点击事件

你可能感兴趣的:(Android)