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

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

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

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

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

版本:2018/2/13-1


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
<?xml version="1.0" encoding="utf-8"?>
<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)
140dp
2110dp
3180dp
4250dp
n70 * n - 30

PendingIntent

8、PendingIntent是什么?

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

9、PengdingIntent的三种待定意图

意图方法备注
ActivitygetActivity(Context context, int requestCode, Intent intent, int flags)该意图发生时,等效于Context.startActivity(Intent)
ServicegetService(Context context, int requestCode, Intent intent, int flags)该意图发生时,等效于Context.startService(Intent)
BraodcastgetBroadcast(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
LayoutFrameLayout/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<Action>();
        }
        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添加点击事件
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猎羽

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值