Android 开发艺术探索读书笔记 5 -- 理解 Remot

作者: 开心wonderful | 来源:发表于2017-08-16 19:39 被阅读74次

    本篇文章主要介绍以下几个知识点:

    • RemoteViews 的应用
    • RemoteViews 的内部机制
    • RemoteViews 的意义
    hello,夏天 (图片来源于网络)

      RemoteViews 表示的是一种 View 的结构,它可以在其他的进程中显示,其使用场景有两种:通知栏和桌面小部件。

    5.1 RemoteViews 的应用

    5.1.1 RemoteViews 在通知栏上的应用

      通知栏除了默认效果还支持自定义布局,使用系统默认的样式弹出一个通知如下:

       /**
         * 系统默认样式
         */
        private void defaultNotice() {
            Intent intent = new Intent(this, NoticeActivity.class);
            PendingIntent pi = PendingIntent.getActivity(this,0,intent,0);
    
            // 1. 获取 NotificationManager 实例来对通知进行管理
            NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
            // 2. 使用 Builder 构造器来创建 Notification 对象
            Notification notification = new NotificationCompat.Builder(this)
                    .setContentTitle("This is content title")
                    .setContentText("This is content text")
                    .setWhen(System.currentTimeMillis())
                    .setSmallIcon(R.mipmap.ic_launcher)
                    .setLargeIcon(BitmapFactory.decodeResource(getResources(),R.mipmap.ic_launcher))
                    .setContentIntent(pi) // 传入pi
                    .build();
            // 3. 显示通知
            manager.notify(1,notification);
        }
    

      运行效果如下:

    系统默认通知效果

      为了满足个性化需求,需要用到自定义通知,首先提供一个布局文件,然后通过 RemoteViews 加载这个布局文件即可,如下:

       /**
         * 自定义通知栏
         */
        private void customizeNotice() {
            Intent intent = new Intent(this, NoticeActivity.class);
            PendingIntent pi = PendingIntent.getActivity(this,0,intent,0);
    
            RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.chapter05_notice_item_layout);
            remoteViews.setTextViewText(R.id.tv_title, "This is content title"); // 文字
            remoteViews.setTextViewText(R.id.tv_content, "This is content text");// 文字
            remoteViews.setImageViewResource(R.id.iv_notice, R.mipmap.ic_notice);// 图片
            remoteViews.setOnClickPendingIntent(R.id.ll_open_notice, pi);        // 点击
    
            // 1. 获取 NotificationManager 实例来对通知进行管理
            NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
            // 2. 使用 Builder 构造器来创建 Notification 对象
            Notification notification = new NotificationCompat.Builder(this)
                    .setContent(remoteViews)  // 传入 remoteViews
                    .setSmallIcon(R.mipmap.ic_notice)
                    .setWhen(System.currentTimeMillis())
                    .build();
            // 3. 显示通知
            manager.notify(2, notification);
        }
    

      其中布局文件代码如下:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/ll_open_notice"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal">
    
        <ImageView
            android:id="@+id/iv_notice"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
    
        <LinearLayout
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center_vertical"
            android:orientation="vertical"
            android:padding="5dp">
    
            <TextView
                android:id="@+id/tv_title"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textColor="@color/colorAccent" />
    
            <TextView
                android:id="@+id/tv_content"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textColor="@color/colorPrimary" />
    
        </LinearLayout>
    
    </LinearLayout>
    

      运行效果如下:

    自定义通知效果

    5.1.2 RemoteViews 在桌面小部件的应用

      AppWidgetProvider 是 Android 中提供的用于实现桌面小部件的类,其本质是一个广播,即 BroadcastReceiver,其继承关系如下:

    AppWidgetProvider 的类继承关系

      桌面小部件的开发步骤如下:

      1. 定义小部件的界面

      在 res/layout 下新建个 widget.xml (名称和内容可自定义)如下:

    <?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="match_parent"
        android:gravity="center_horizontal"
        android:orientation="vertical">
        
        <ImageView
            android:id="@+id/iv_widget"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@mipmap/ic_header"/>
    
        <TextView
            android:id="@+id/tv_widget"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:padding="5dp"
            android:text="wonderful"/>
    
    </LinearLayout>
    

      2. 定义小部件配置信息

      在 res/xml 下新建 widget_info.xml (名称可自定义)如下:

    <?xml version="1.0" encoding="utf-8"?>
    <!-- initialLayout 小工具使用的初始化布局
         minHeight、minWidth 小工具的最小尺寸
         updatePeriodMillis 小工具的自动更新周期(毫秒) -->
    <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
        android:initialLayout="@layout/widget"
        android:minHeight="66dp"
        android:minWidth="66dp"
        android:updatePeriodMillis="86400000">
    
    </appwidget-provider>
    

      3. 定义小部件的实现类

      这个类需继承 AppWidgetProvider 如下:

    
    /**
     * Function:小部件的实现类
     * Author:Wonderful on 2017/8/16 10:50
     * Email:KXwonder@163.com
     */
    
    public class MyWidgetProvider extends AppWidgetProvider{
    
        public static final String CLICK_ACTION = "com.wonderful.androidartexplore.chapter05.action.CLICK";
    
        public MyWidgetProvider() {
            super();
        }
    
        @Override
        public void onReceive(final Context context, final Intent intent) {
            super.onReceive(context, intent);
            // 判断是自己的 action,做自己的事情,如小部件被单击了要干什么,
            // 这里是做一个动画效果
            if (intent.getAction().equals(CLICK_ACTION)) {
                ToastUtils.show("clicked it");
    
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), R.mipmap.ic_header);
                        AppWidgetManager appWidgetManage = AppWidgetManager.getInstance(context);
                        for (int i = 0; i < 37; i++) {
                            float degree = (i * 10) % 360;
                            RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget);
                            remoteViews.setImageViewBitmap(R.id.iv_widget, rotateBitmap(bitmap, degree));
                            Intent intentClick = new Intent(CLICK_ACTION);
                            PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intentClick, 0);
                            remoteViews.setOnClickPendingIntent(R.id.iv_widget, pendingIntent);
                            appWidgetManage.updateAppWidget(new ComponentName(context, MyWidgetProvider.class), remoteViews);
                            SystemClock.sleep(30);
                        }
                    }
                }).start();
            }
        }
    
        /**
         * 每次桌面小部件更新时都会调用一次该方法
         */
        @Override
        public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
            super.onUpdate(context, appWidgetManager, appWidgetIds);
    
            for (int appWidgetId : appWidgetIds) {
                RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget);
                // 点击桌面小部件发送广播
                Intent intentClick = new Intent(CLICK_ACTION);
                PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intentClick, 0);
                remoteViews.setOnClickPendingIntent(R.id.iv_widget, pendingIntent);
                appWidgetManager.updateAppWidget(appWidgetId, remoteViews);
            }
        }
    
        /**
         * 动画
         */
        private Bitmap rotateBitmap(Bitmap bitmap, float degree) {
            Matrix matrix = new Matrix();
            matrix.reset();
            matrix.setRotate(degree);
            return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
        }
    }
    

      上述代码实现了一个简单的桌面小部件,小部件上显示一张图片,点击后旋转一周。

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

      桌面小部件本质是一个广播组件,必须要注册,如下:

    <receiver android:name=".chapter05.MyWidgetProvider">
         <intent-filter >
              <!-- 用于识别小部件的点击行为 -->
              <action android:name="com.wonderful.androidartexplore.chapter05.action.CLICK"/>
              <!-- 小部件的标识,必须存在 -->
              <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
         </intent-filter>
    
         <meta-data
              android:name="android.appwidget.provider"
              android:resource="@xml/widget_info" />
     </receiver>
    

      运行效果如下:

    桌面小部件运行效果

      上面描述了一个开发桌面小部件的完整过程,实际开发流程都是一样的。 不管小部件的界面初始化还是界面的更新,在界面上的操作都是通过 RemoteViews。

      AppWidgetProvider 除了最常用的 onUpdate 方法,还有 onEnableonDisabledonDeleted以及onReceiver 方法,这些方法会自动的被 onReceiver 在合适的时间调用,其含义如下:

    • onEnable 当该窗口小部件第一次添加到桌面的时候调用该方法,可添加多次但是只在第一次调用

    • onUpdate 小部件被添加或每次更新时都会调用一次该方法,更新机制由 updatePeriodMillis 来指定

    • onDeleted 每删除一次小部件就调用一次

    • onDisabled最后一个该类型的小部件被删除时调用

    • onReceiver 广播内置的方法

    5.1.3 PendingIntent 概述

      PendingIntent 表示一种处于待定、等待、即将发生的意思,它与 Intent 的区别在于,PendingIntent 是在将来某个不确定的时刻发生,Intent 是立刻发生。(给 RemoteViews 设置点击事件必须使用 PendingIntent)

      PendingIntent 支持三种待定意图:启动Activity、启动Service、发送广播,具体如下:

    PendingIntent 的主要方法

      上图中三个方法的参数都是一样的,其中第二个参数 requstCode 表示 PendingIntent 发送方的请求码,多数情况设为 0,它也会影响到参数 flags 的效果。


      PendingIntent 的匹配规则:若两个 PendingIntent 内部的 Intent 相同并且 requstCode 也相同,那么这两个 PendingIntent 就是相同的。

      Intent 的匹配规则:若两个 Intent 的 ComponentNameintent-filter 都相同,那么这两个 intent 就是相同的。(注:Extras 不参与匹配过程)


      下面介绍参数 flags 的含义:

    • FLAG_ONE_SHOT
      当前 PendingIntent 只能被使用一次,然后被 cancel,若后续还有相同的 PendingIntent,则无效。通知栏,同类的通知只能使用一次,后续的无法打开。

    • FLAG_NO_CREATE
      当前 PendingIntent 不会主动去创建,若之前不存在,则获取 PendingIntent 失败(实际中无意义)。

    • FLAG_CANCEL_CURRENT
      当前 PendingIntent 若已存在,则会被 cancel,然后系统会创建一个新的 PendingIntent。通知栏,那些被 cancel 的消息将无法被打开。

    • FLAG_UPDATE_CURRENT
      当前 PendingIntent 若已存在,则会被更新,即它们的 intent 中的 Extras 会被替换成最新的。

      下面结合通知栏信息描述这四个标记位,如下代码:

    // 若 notify 的第一个参数 id 是常量,多次调用 notify 只弹出一个通知,后续的会把前面的替换掉
    // 若每次 id 都不同,多次调用 notify 会弹出多个通知
    manager.notify(1, notification);
    

      若 notify 的 id 是常量,不管 PendingIntent 是否匹配,后面的通知会替换前面的通知。

      若 notify 的 id 每次都不同,当 PendingIntent不匹配时,通知之间不互相干扰。PendingIntent 处于匹配状态时,分如下情况:

    • FLAG_ONE_SHOT 后续通知中的 PendingIntent 会和第一条通知保持一致,包括其中的 Extras,单击任何一条通知后,剩余的都无法打开,当所有通知被清除后,会重复此过程
    • FLAG_CANCEL_CURRENT 只有最新的通知可以打开,之前弹出均无法打开
    • FLAG_UPDATE_CURRENT 之前弹出的通知中的 PendingIntent 会被更新,最终它们和最新的一条通知保持一致,包括其中的Extras,这些通知都可以被打开

    5.2 RemoteViews 的内部机制

      RemoteViews 的作用在其他进程中显示并且更新 View 的界面,其最常用的构造方法:

    // 两个参数:第一个表示当前的包名,第二个是待加载的布局文件
    public RemoteViews(String packageName, int layoutId) {
        this(getApplicationInfo(packageName, UserHandle.myUserId()), layoutId);
    }
    

      RemoteViews 支持的所有 View 类型如下:

    RemoteViews 所支持的 View 类型

      RemoteViews 中不能使用除了上述列表中以外的 View,也无法使用自定义 View。

      RemoteViews 没有提供 findViewById 方法,无法直接访问里面的 View 元素,必须通过它所提供的一系列 set 方法来完成。其部分 set 方法如下:

    RemoteViews 的部分 set 方法

      关于 RemoteViews 的内部机制,有兴趣的可以去看看书或源码,这里提供一张图,不多做介绍了:

    RemoteViews 的内部机制

    5.3 RemoteViews 的意义

      下面打造一个模拟的通知栏效果并且实现跨进程的 UI 更新。

      有两个 Activity 分别运行在两个不同的进程,一个是A,一个是B,其中A扮演着通知栏的角色,而B则可以不停地发送通知栏消息。为了模拟通知栏的效果,修改A的 process 属性使其运行在单独的进程中,这样A和B就构成了多进程通信的情形。在B中创建 Remoteviews 对象,然后通知A显示这个 RemoteViews 对象。

      B每发送一次模拟通知,就会发送一个特定的广播,然后A接收到广播后就开始显示B中定义的 RemoteViews 对象,此过程和系统的通知栏消息的显示过程几乎一致。

      首先看B的实现,B只要构造 RemoteViews 对象并将其传输给A即可,代码如下:

    /**
     * Function:模拟通知效果 B activity
     * Author:Wonderful on 2017/8/9 10:30
     * Email:KXwonder@163.com
     */
    
    public class B_Activity extends BaseActivity {
        @Override
        protected int initLayoutId() {
            return R.layout.activity_chapter05_b;
        }
    
        @Override
        protected void initView() {
    
        }
    
        @OnClick(R.id.btn_send)
        public void onViewClicked() {
            ToastUtils.show("发送广播");
            RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.chapter05_notice_item_layout);
            remoteViews.setTextViewText(R.id.tv_title, "发送给 A 的通知");
            remoteViews.setTextViewText(R.id.tv_content, "mag from process:" + Process.myPid());
            //remoteViews.setImageViewResource(R.id.iv_notice, R.mipmap.ic_notice);
            PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, new Intent(this, A_Activity.class), PendingIntent.FLAG_UPDATE_CURRENT);
            PendingIntent pendingIntent2 = PendingIntent.getActivity(this, 0, new Intent(this, NoticeActivity.class), PendingIntent.FLAG_UPDATE_CURRENT);
            remoteViews.setOnClickPendingIntent(R.id.iv_notice, pendingIntent);
            remoteViews.setOnClickPendingIntent(R.id.ll_open_notice, pendingIntent2);
            Intent intent = new Intent(Constants.REMOTE_ACTION);
            // 将RemoteViews 对象通过Intent传输到A中
            intent.putExtra(Constants.EXTRA_REMOTE_VIEWS, remoteViews);
            sendBroadcast(intent);
        }
    }
    

      A中只需接收B中的广播并显示 RemoteViews 即可,代码如下:

    /**
     * Function:模拟通知效果 A activity
     * Author:Wonderful on 2017/8/9 10:30
     * Email:KXwonder@163.com
     */
    
    public class A_Activity extends BaseActivity {
    
        @BindView(R.id.ll_remoteViews)
        LinearLayout llRemoteViews;
    
        @Override
        protected int initLayoutId() {
            return R.layout.activity_chapter05_a;
        }
    
        @Override
        protected void initView() {
            // 注册广播
            IntentFilter intent = new IntentFilter(Constants.REMOTE_ACTION);
            registerReceiver(mBroadcastReceiver, intent);
        }
    
        private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                LogUtils.e("A_activity", "接收广播成功");
                // 1. 当收到广播后,从Intent中取出RemoteViews对象
                RemoteViews remoteViews = intent.getParcelableExtra(Constants.EXTRA_REMOTE_VIEWS);
                if (remoteViews != null) {
                    // 2. 通过apply方法加载布局并且执行更新操作,
                    View view = remoteViews.apply(context, llRemoteViews);
                    // 3. 将得到的 View 添加到A的布局中
                    llRemoteViews.addView(view);
                }
            }
        };
    
        @Override
        protected void onDestroy() {
            super.onDestroy();
            // 解除广播
            unregisterReceiver(mBroadcastReceiver);
        }
    
        @OnClick(R.id.btn_to_b)
        public void onViewClicked() {
            // 跳转到 B activity
            IntentUtils.to(this, B_Activity.class);
        }
    }
    

      运行效果如下:

    模拟通知效果

      现有两应用,一个应用能更新另一个应用中的某个界面,这时可选择以下两种方式实现:

    • AIDL 缺点:当对界面的更新频繁时,会有效率问题,同时 AIDL 接口会变得复杂。
    • RemoteViews 缺点:只支持一些常见的View,不支持自定义 View。

      面对这种问题,若界面中的 View 是一些简单的且被 RemoteViews 支持的 View,可考虑采用 RemoteViews,否则就不适合用 RemoteViews。


      采用 RemoteViews 来实现两应用间的界面更新,还有一个布局文件的加载问题。在上面的代码中,直接通过 RemoteViews 的 apply 方法来加载并更新界面:

    // 2. 通过apply方法加载布局并且执行更新操作,
    View view = remoteViews.apply(context, llRemoteViews);
    // 3. 将得到的 View 添加到A的布局中
    llRemoteViews.addView(view);
    

      这种写法在同一个应用的多进程情形下是适用的,但若A和B属于不同应用,由于资源id不可能刚好一样,B中的布局文件的资源id传输到A中后可能无效。

      面对这种情况,可通过资源名称来加载布局文件。

      首先两个应用要提前约定好 RemoteViews 中的布局的文件名称,然后在A中根据名称找到并加载,接着再调用 Remoteviews 的 reapply 方法即可将B中对View所做的一系列更新操作全作用到A中加载的View上。修改后的代码如下:

    int layoutId = getResources().getIdentifier("layout_simulated_notification","layout",getPackageName());
    View view = getLayoutInflater().inflate(layoutId,llRemoteViews,false);
    remoteViews.reapply(this,view);
    llRemoteViews.addView(view);
    

      本篇文章就介绍到这。

    相关文章

      网友评论

        本文标题:Android 开发艺术探索读书笔记 5 -- 理解 Remot

        本文链接:https://www.haomeiwen.com/subject/ezibrxtx.html