安卓RemoteViews学习、Notification通知栏开发、PendingIntent概述

最近项目用到widget,与之相关的一个较为重要的东西“remoteViews”,借此空闲时间学习学习,记录记录。

1、remoteView是什么?

简单来说就是一种特殊的、可跨进程来更新显示的view。

其实它和远程 Service 是一样的,RemoteViews 表示的是一个 View 结构,可以在其他进程中显示,由于它在其他进程中显示,为了能够更新它的界面,Remote Views 提供了一组基础的操作用于跨进程更新它的界面。

Remote Views 在 Android 中的使用场景有两种:通知栏widget桌面小部件,为了更好地分析Remote Views的内部机制,先简单介绍Remote Views 在通知栏和桌面小部件上的应用,接着分析 Remote Views 的内部机制,最后分析 RemotcVicws 的意义并给出一个采用 RemoteViews 来跨进程更新界面的示例。

2、remoteView在桌面小部件及通知栏的使用、及pendingIntent概述

在widget中的使用上一篇文章已经讲过,此处不再赘述。

下面讲讲在通知栏中的使用

安卓通知栏借助于notification实现。直接上代码:

package com.example.viewpage2activity;

import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.RemoteViews;

import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.NotificationCompat;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    private static final String TAG = "WW_WW";

    private static final int NOTIFICATION_ID_ORIGINAL = 1;

    private static final int NOTIFICATION_ID_CUSTOM = 1;

    private Button btnOpen;

    private Button btnClose;

    private Button btnOpenCus;

    private Button btnCloseCus;

    private NotificationManager mManager;

    private Notification notificationOriginal;

    private Notification notificationCustom;

    @RequiresApi(api = Build.VERSION_CODES.O)
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
        initNotification();
        initView();
    }

    private void initView() {
        btnOpen = findViewById(R.id.btn_open);
        btnClose = findViewById(R.id.btn_close);

        btnOpenCus = findViewById(R.id.btn_open_cus);
        btnCloseCus = findViewById(R.id.btn_close_cus);
        btnOpen.setOnClickListener(this);
        btnClose.setOnClickListener(this);
        btnOpenCus.setOnClickListener(this);
        btnCloseCus.setOnClickListener(this);

    }

    private void initNotification() {
        /**
         * 使用安卓自带的notification
         */
        NotificationCompat.Builder builder = getNotificationBuilder();
        Intent intent = new Intent(this, SecondActivity.class);
        builder.setContentTitle("您收到一条新消息");
        builder.setContentText("请打开app查看消息");

        builder.setSmallIcon(R.drawable.img_1); // 至少必须有一个smallIcon,不然app崩溃
        builder.setAutoCancel(true);
        builder.setContentIntent(PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT));
        notificationOriginal = builder.build();

        /**
         * 自定义notification
         */
        NotificationCompat.Builder builder2 = getNotificationBuilder();
        RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.custom_notification_layout); // 自定义view,实际是remoteView
        remoteViews.setTextViewText(R.id.text_1_notification, "自定义通知");
        remoteViews.setImageViewResource(R.id.img_notification, R.drawable.img_4);
        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
        builder2.setSmallIcon(R.drawable.img_1);
        remoteViews.setOnClickPendingIntent(R.id.click_area, pendingIntent); // 设置点击事件

        notificationCustom = builder2.build();
        notificationCustom.contentIntent = pendingIntent; // 赋值
        notificationCustom.contentView = remoteViews; // 赋值可见是将我们声明的remoteView赋给了notification中的contentView
        notificationCustom.when = System.currentTimeMillis();
    }

    private NotificationCompat.Builder getNotificationBuilder() {

        /**
         * Android 8.0 系统,Google引入通知渠道,提高用户体验,方便用户管理通知信息,同时也提高了通知到达率
         *
         * 什么是通知渠道呢?顾名思义,就是每条通知都要属于一个对应的渠道。每个App都可以自由地创建当前App拥有哪些通知渠道,但是这些通知渠道的控制权都是掌握在用户手上的。用户可以自由地选择这些通知渠道的重要程度,是否响铃、是否振动、或者是否要关闭这个渠道的通知。
         *
         * 当build.gradle中targetSdkVersion设置大于等于26。这时如果不对通知渠道适配,通知就无法显示。
         */
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            NotificationChannel channel = new NotificationChannel("channel_id", "channel_name",
                    NotificationManager.IMPORTANCE_DEFAULT);
            //是否绕过请勿打扰模式
            channel.canBypassDnd();
            //闪光灯
            channel.enableLights(true);
            //锁屏显示通知
//            channel.setLockscreenVisibility(VISIBILITY_SECRET);
            //闪关灯的灯光颜色
            channel.setLightColor(Color.RED);
            //桌面launcher的消息角标
            channel.canShowBadge();
            //是否允许震动
            channel.enableVibration(true);
            //获取系统通知响铃声音的配置
            channel.getAudioAttributes();
            //获取通知取到组
            channel.getGroup();
            //设置可绕过  请勿打扰模式
            channel.setBypassDnd(true);
            //设置震动模式
            channel.setVibrationPattern(new long[]{100, 100, 200});
            //是否会有灯光
            channel.shouldShowLights();
            mManager.createNotificationChannel(channel);
        }

        return new NotificationCompat.Builder(this, "channel_id");
    }

    @Override
    public void onClick(View v) {
        int id = v.getId();

        if (id == R.id.btn_open) {
            Log.i(TAG, "onClick:   open");
            mManager.notify(NOTIFICATION_ID_ORIGINAL, notificationOriginal);
        }

        if (id == R.id.btn_close) {
            Log.i(TAG, "onClick:   close");
            mManager.cancel(NOTIFICATION_ID_ORIGINAL);
        }

        if (id == R.id.btn_open_cus) {
            mManager.notify(NOTIFICATION_ID_CUSTOM, notificationCustom);
        }

        if (id == R.id.btn_close_cus) {
            mManager.cancel(NOTIFICATION_ID_CUSTOM);
        }

    }
}

效果呢,是四个按钮,分别是安卓默认的notification样式。和自定义的。
此demo的效果
在这里插入图片描述
在这里插入图片描述

上面代码

        RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.custom_notification_layout); // 自定义view,实际是remoteView
        remoteViews.setTextViewText(R.id.text_1_notification, "自定义通知");
        remoteViews.setImageViewResource(R.id.img_notification, R.drawable.img_4);
        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
        builder2.setSmallIcon(R.drawable.img_1);
        remoteViews.setOnClickPendingIntent(R.id.click_area, pendingIntent); // 设置点击事件

可以看到,只需要包名和布局资源id即可创建remoteViews。如何更新?倒是和直接的view不同。如textView的setText。
但remoteView提供了一些set方法可以让我们实现更新。

比如:setTextViewText(id,text)、setImageViewResources(id,resId)、setOnClickPendingIntent(id,pendingIntent)。。。
有这些set方法帮我们实现更新view。

为什么更新remoteView如此复杂呢?直观原因是因为没有提供和view类似的findViewById,因此无法直接获取到remoteView的具体实例,也就无法通过类似setText等方法更新。具体原因后续会提到。

PendingIntent概述

之前我们用到了多次的pendingIntent,到底是什么?

intent意图,而pendingIntent表示一种处于pending状态的意图,即为一种待定的、等待、即将发生的意思。就是说接下来有一个intent在将来的某个确定的时刻发生。

pendingIntent典型的使用场景是给RemoteViews添加点击事件,因为remoteView运行在远程进程(System进程)中,所以不同于普通的view的setOnclickListener,不能直接的设置点击事件。想要给remoteViews设置点击事件,就必须使用pendingIntent,通过send和cancle方法来发送和取消特定的待定intent。
在这里插入图片描述

pendingIntent支持三种待定意图:启动activity、启动Servic和发送广播

上面三个方法的一、三参数比较好理解,特别说明下第二、四个。

requestCode发送方的请求码,一般为0即可,但其值又影响到的四个参数flag的作用。
FLAG常见类型有四:
FLAG_ONE_SHOT、
FLAG_NO_CREATE、
FLAG_CANCLE_CURRENT、
FLAG_UPDATE_CURRENT。

理解这四个标记位之前,必须先明白PendingIntent的匹配规则,即在什么情况下俩个pendingIntent是相同的。

规则为:如果两个pendingIntent内部的intent相同并requestCode相同,则俩pendingIntent相同。requestCode的好理解。什么时候俩intent相同呢?规则是:俩intent的ComponentName和intent-filter相同,那么这俩intent就相同。而intent的Extras不参与匹配。

总结上面规则也就是说:俩pendingIntent的requestCode相同、内部的ientent的ComponentName相同、intent-filter相同,则俩pendingIntent相同。

下面来看四个标志位:

FLAG_ONE_SHOT
当前描述的PendingIntent只能被使用一次,之后自动被cancle。后续如果有相同的pendingIntent,那么send方法就会调用失败。对于通知栏消息,如果选用此标记位,那么同类的通知只能使用一次。后续单击将无法打开。

FLAG_NO_CREATE
顾名思义,当前描述的pendingIntent不会主动创建,
那么getActivity、getService、getBrocast都会直接返回null。
这个标记位很少见,无法单独使用,日常也见的少。就不多讲了。

FLAG_CANCLE_CURRENT
当前描述的pendingIntent如果已经存在,那么会被cancle。,系统会创建个新的。对于通知栏来说,被cancle的消息单击后将无法打开

FLAG_UPDATE_CURRENT
当前描述的pendingIntent如果已经存在,那么它们都会被更新,即intent中的extra会被更新。

下面结合通知栏消息再描述一编:
manager.notify(1,notifi)。如果第一个参数是常量,那么多次调用notify只能弹出一个通知,后续的通知会把之前的完全替代掉,每次id不同,那么调用notify则会弹出多个。

如果相同,很好理解。如果不同,那么当pindingIntent不匹配时,则这些通知完全互不干扰。如果pendingIntent处于匹配状态,要分情况讨论:
如果采取FLAG_ONE_SHOT,那么后续通知中的PendingIntent会和第一条通知保持完全一致,包括extras,单击任何一条后,剩下的都无法再打开,当所有都被清除时,会重复这个过程。

如果采用FLAG_CANCLE_CURRENT,那么只有最新的通知可以打开,之前弹出的均无法打开。

如果采用FLAG_UPDATE_CURRENT那么之前弹出的PendingIntent会被更新,最终它们和最新的一条通知保持完全一致,包括extras,并且都可打开。

3、remoteViews的内部机制?

3.1理论基础

remoteView的作用是在其他进程中显示并更新view界面,为了更好理解内部机制,我们从构造方法看起。介绍最常用的:public RemoteViews(String packageName,int layout)。俩参数,一是当前应用包名,另是待加载的布局文件。

而remoteViews目前并不支持所有的view及layout,支持的如下:

在这里插入图片描述

remoteViews不支持他们的子类及其他,也就是只能用上面固定的view,不支持继承view、自定义view。比如我们在通知栏的remoteViews使用Edittext,那么无法弹出,且抛出异常。
在这里插入图片描述

RemoteViews没提供findViewById,所以无法直接的访问里面的具体view元素,必须通过它所提供的一系列set方法来实现。是因为remoteViews在远程进程中显示,没办法直接findViewById。

下表列举了部分常用的set方法
在这里插入图片描述
从图知,原本的view.setXXX方法必须通过remoteView的方法才能完成。而且从方法声明来看,像是反射实现的。实际的确是。

内部机制:
我们从通知栏和桌面小部件来分析它的工作过程。

通知栏和小部件分别由NotificationManager和AppWidgetManager管理,而这俩通过binder分别和SystemServer进程中的NotificationManagerService以及AppWidgetService进行通信。可见通知栏和小部件的布局文件实际是在两个service中被加载,而运行在系统的SystemServer,这就形成了跨进程通信。

1、首先remoteView会通过binder传递到systemServer进程,是因为remoteViews实现了Parcelable接口,因此可以跨进程。系统根据remoteView的包名信息找到该应用资源。2、然后通过layoutInflater去加载remoteView中的布局文件,在systemServer进程加载后的布局文件是一个普通的view,但相对于我们的进程它是remoteViews。3、接着系统会对view执行一系列更新任务,这些就是我们之前通过set方法提交的。**set方法对view所做更新不是立即执行,在remoteView内部会记录所有的更新操作,具体执行时机要等到remoteViews被加载以后才能执行。**这样remoteView就可在systemServer进程中显示。当需要更新时候,我们通过remoteView的set方法提交任务,并通过NotificationManager和AppWidgetManager来提交更新任务,具体操作实现。

理论上说,系统完全可通过binder支持所有view及view操作,但这样做代价太大,view方法太多了。太多IPC操作也会影响效率。为解决这问题,系统没通过binder直接去支持view的跨进程访问,而是提供了个action概念,action代表view一个操作,也实现了Parcel接口。

1、首先系统将view操作封装成action对象并将对象跨进程传递到远端进程,接着在远程进程中执行action对象中的具体操作。
2、在我们应用每一次调用set方法,remoteView中就会添加一个对应的action对象,当我们通过NotificationManager和AppWidgetManager来提交更新时,这些action对象就会传输到远程进程并在远程进程中依次执行。远程进程通过RemoteViews的apply方法来进行view的更新操作,RemoteViews的apply方法内部则会去遍历所有action对象并调用它们的apply方法,具体view更新操作上action的apply方法完成的。

好处很明显,不需要定义大量binder接口,其次通过封装成remoteViews并通过在远程进程中批量执行RemotViews的修改操作,避免了大量的IPC。提高了性能。

3.2代码分析

我们从提供的一系列方法来看下:比如setTextViewText方法。
在这里插入图片描述
在这里插入图片描述
上面的核心代码,viewId是被操作view的id,“setText”是方法名,text为将要设置的字串内容。接着看下setCharSequence方法

在这里插入图片描述
从setCharSequence方法可看出,内部并没对view直接操作,而是add了个ReflectionAction对象,见名知意,该是一个反射操作。再看看addAction方法

在这里插入图片描述
上面可知RemoteViews内部有个mAction成员变量,是ArrayList,外界每调用一次set,就会为其创建一个Action对象并被add到这个mAction。这里只是保存,还并未对view更新。到这,我们已经分析完setTextViewText方法的源码,但仍不明不白,ReflectionAction是什么?我们接着看。

在看ReflectionAction之前,我们先看下RemoteViews的apply方法,如下:

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

        View result = inflateView(context, rvToApply, parent);
        rvToApply.performApply(result, parent, handler);
        return result;
    }

    private View inflateView(Context context, RemoteViews rv, ViewGroup parent) {
        return inflateView(context, rv, parent, 0);
    }


    private View inflateView(Context context, RemoteViews rv, ViewGroup parent,
            @StyleRes int applyThemeResId) {
        // 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 RemoteViewsContextWrapper(context, contextForResources);

        // If mApplyThemeResId is not given, Theme.DeviceDefault will be used.
        if (applyThemeResId != 0) {
            inflationContext = new ContextThemeWrapper(inflationContext, applyThemeResId);
        }
        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);
        View v = inflater.inflate(rv.getLayoutId(), parent, false);
        v.setTagInternal(R.id.widget_frame, rv.getLayoutId());
        return v;
    }

在apply方法中,我们看到通过
View result = inflateView(context, rvToApply, parent);获取到view,再通过
rvToApply.performApply(result, parent, handler);去apply。

而inflateView方法则是 View v = inflater.inflate(rv.getLayoutId(), parent, false);去获取到view并返回。

总结就是首先通过LayoutInflater去加载RemoteView中的布局文件,通过getLatoutId获得,加载完后通过performApply去执行更新。
performApply代码如下:

  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);
            }
        }
    }

performAppy实现很好理解,拿出mAction的元素遍历,依次执行每个action的apply。这也就对应了,remoteViews每调用一次set方法,就给mAction加了个Action对象。因此能断定,action的apply方法是真正操作view的地方。

remoteViews在通知栏和小部件的工作过程和上面描述的过程一致,当调用remoteViews的set方法,不会立即执行,而必须通过Notification的notify方法及updateAppWidget才能更新界面。实际中这俩方法的实现中,的确是通remoteViews的apply和reApply方法来加载或更新界面。apply和reApply区别在于:apply会加重并更新界面,而reApply则只会更新。

通知栏和桌面小部件在初始化界面时会调用apply方法,后续更新界面则会调用reApply方法。这里看下BaseStatusBar的updateNotificationViews方法,如下:
在这里插入图片描述
在这里插入图片描述
显然,上述代码表示当通知栏界面需要更新时候,会通过RemoteViews的reApply方法来更新界面。

再看下AppWidgetHostView的updateAppWidget方法,在内部有一段代码如下:
在这里插入图片描述
可发现,桌面小部件更新界面也是通过remoteViews的reApply方法来实现的。

现在看一些继承Action子类的具体实现:
在这里插入图片描述

通过代码发现,ReflectionAction表示的是一个反射动作,通过它对view的操作会以反射的方式来调用,其中getMethod就是根据方法名来得到所需的对象。使用ReflectionAction的set方法有:setTextViewText、setBoolean、setLong、setdouble等。其他的Action也类似。

关于单击事件,RemoteViews中只支持pendingIntent,不支持onClickListener等模式。另外,我们需要注意注意setOnClickPendingIntent、setPendingIntentTemplate以及setOnClickFillInIntent它们之间的区别和联系。首先setOnClickPendingIntent用于给普通View设置单击事件,但是不能给集合(listView、StackView)中的view设置。其次,如果要给ListView和StackView中的item添加单击事件,则必须将setPendingIntent和setOnClickFillINIntent组合使用才可以。

以上就是关于通知栏Notification开发、RemoteViews原理及PendingIntent的内容。

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

约翰兰博之西安分博

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

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

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

打赏作者

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

抵扣说明:

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

余额充值