Android开发艺术探索——理解RemoteViews

这是我在学习过程中总结的知识
目的是希望日后回来看或者需要用的时候可以 一目了然 # 的回顾、巩固、查缺补漏
不追求详细相当于书本的精简版或者说是导读(想看详细的直接对应翻书),但会尽力保证读者都能快速理解和快速使用(随理解加深会总结的更加精简),但必要时会附上一些较详细解释的链接
脚注是空白的:表示还没弄懂的知识,了解后会添加

  • 是一种远程View,类似远程Service
  • 这种View可以在其他进程中显示,可跨进程更新View
  • 一般用于:通知栏和桌面小部件

5.1 RemoteViews的应用

  • 通知栏主要通过NotificationManager的nofity来实现的(8.0后要创建通知渠道)
  • 桌面小部件是用AppWidgetProvider,本质是一个广播

5.1.1 RemoteViews在通知栏上的应用

            Notification notification = new Notification();
            notification.icon = R.drawable.ic_launcher;
            notification.tickerText = "hello world";
            notification.when = System.currentTimeMillis();
            notification.flags = Notification.FLAG_AUTO_CANCEL;
            Intent intent = new Intent(this, DemoActivity_2.class);
            PendingIntent pendingIntent = PendingIntent.getActivity(this,
                    0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
            notification.setLatestEventInfo(this, "chapter_5", "this is notification.", pendingIntent);
            NotificationManager manager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
            manager.notify(sId, notification);

上述代码弹出默认样式的通知,下面自定义通知
首先提供一个自定义布局,然后通过RemoteViews来加载

            Notification notification = new Notification();
            notification.icon = R.drawable.ic_launcher;
            notification.tickerText = "hello world";
            notification.when = System.currentTimeMillis();
            notification.flags = Notification.FLAG_AUTO_CANCEL;
            Intent intent = new Intent(this, DemoActivity_1.class);
            intent.putExtra("sid", "" + sId);
            PendingIntent pendingIntent = PendingIntent.getActivity(this,
                    0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
            System.out.println(pendingIntent);
			//创建对象只需要当前应用的包名和布局文件资源id,其中的layout就是通知栏的标题、图片、描述的布局
            RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.layout_notification);
			//标题,这就是一种更新远程View的方法,TextView的id和要设置的文本
            remoteViews.setTextViewText(R.id.msg, "chapter_5: " + sId);
			//图片
            remoteViews.setImageViewResource(R.id.icon, R.drawable.icon1);
            PendingIntent openActivity2PendingIntent = PendingIntent.getActivity(this,
                    0, new Intent(this, DemoActivity_2.class), PendingIntent.FLAG_UPDATE_CURRENT);
    remoteViews.setOnClickPendingIntent(R.id.open_activity2, openActivity2PendingIntent);
            notification.contentView = remoteViews;
            notification.contentIntent = pendingIntent;
            NotificationManager manager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
            manager.notify(sId, notification);

为什么更新RemoteViews如此复杂呢?因为没有和View类似的findViewById方法,当然还有其他原因见5.2

5.1.2 RemoteViews 在桌面小部件上的应用

AppWidgetProvider继承于广播类BroadcastReceiver,下面简单介绍桌面小部件的开发步骤

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="match_parent"
    android:orientation="vertical" >

    <ImageView
        android:id="@+id/imageView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/icon1" />

</LinearLayout>

2. 定义小部件配置信息
在res/xml/appwidget_provider_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>

3. 定义小部件的类
这个类有点复杂,我们跟着注释走

public class MyAppWidgetProvider extends AppWidgetProvider {
    public static final String TAG = "MyAppWidgetProvider";
	//判断小部件动作的
    public static final String CLICK_ACTION = "com.ryg.chapter_5.action.CLICK";

    public MyAppWidgetProvider() {
        super();
    }

/**
这个方法是处理接收到的动作的方法
*/
    @Override
    public void onReceive(final Context context, Intent intent) {
        super.onReceive(context, intent);
        Log.i(TAG, "onReceive : action = " + intent.getAction());

        // 这里判断是自己的action,做自己的事情,比如小工具被点击了要干啥,这里是做一个动画效果
        if (intent.getAction().equals(CLICK_ACTION)) {
            Toast.makeText(context, "clicked it", Toast.LENGTH_SHORT).show();

            new Thread(new Runnable() {
                @Override
                public void run() {
                    Bitmap srcbBitmap = BitmapFactory.decodeResource(
                            context.getResources(), R.drawable.icon1);
                    AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
                    for (int i = 0; i < 37; i++) {
					//旋转360度
                        float degree = (i * 10) % 360;
                        RemoteViews remoteViews = new RemoteViews(context
                                .getPackageName(), R.layout.widget);
                       
					//旋转一次
						remoteViews.setImageViewBitmap(R.id.imageView1,
                                rotateBitmap(context, srcbBitmap, degree));
                        Intent intentClick = new Intent();
                        intentClick.setAction(CLICK_ACTION);
                        PendingIntent pendingIntent = PendingIntent
                                .getBroadcast(context, 0, intentClick, 0);
                        remoteViews.setOnClickPendingIntent(R.id.imageView1, pendingIntent);
                        appWidgetManager.updateAppWidget(new ComponentName(
                                context, MyAppWidgetProvider.class),remoteViews);
                        SystemClock.sleep(30);
                    }

                }
            }).start();
        }
    }

    /**
     * 每次窗口小部件被点击更新都调用一次该方法
     */
    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager,
            int[] appWidgetIds) {
        super.onUpdate(context, appWidgetManager, appWidgetIds);
        Log.i(TAG, "onUpdate");

        final int counter = appWidgetIds.length;
        Log.i(TAG, "counter = " + counter);
        for (int i = 0; i < counter; i++) {
            int appWidgetId = appWidgetIds[i];
            onWidgetUpdate(context, appWidgetManager, appWidgetId);
        }

    }

    /**
     * 窗口小部件更新
     * 
     * @param context
     * @param appWidgeManger
     * @param appWidgetId
     */
    private void onWidgetUpdate(Context context,
            AppWidgetManager appWidgeManger, int appWidgetId) {

        Log.i(TAG, "appWidgetId = " + appWidgetId);
        RemoteViews remoteViews = new RemoteViews(context.getPackageName(),
                R.layout.widget);

        // "窗口小部件"点击事件发送的Intent广播
        Intent intentClick = new Intent();
        intentClick.setAction(CLICK_ACTION);
        PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0,
                intentClick, 0);
        remoteViews.setOnClickPendingIntent(R.id.imageView1, pendingIntent);
        appWidgeManger.updateAppWidget(appWidgetId, remoteViews);
    }

    private Bitmap rotateBitmap(Context context, Bitmap srcbBitmap, float degree) {
        Matrix matrix = new Matrix();
        matrix.reset();
        matrix.setRotate(degree);
        Bitmap tmpBitmap = Bitmap.createBitmap(srcbBitmap, 0, 0,
                srcbBitmap.getWidth(), srcbBitmap.getHeight(), matrix, true);
        return tmpBitmap;
    }
}

4. 在AndroidManifest.xml中声明小部件
注册广播

        <receiver android:name=".MyAppWidgetProvider" >
            <meta-data
                android:name="android.appwidget.provider"
                android:resource="@xml/appwidget_provider_info" >
            </meta-data>

            <intent-filter>
                <action android:name="com.ryg.chapter_5.action.CLICK" />
                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
            </intent-filter>
        </receiver>

其中有两个Action,其中第一个用于识别小部件的单击行为、第二个作为小部件的标识必须存在,否则无法添加这个小部件

AppWidgetProvider还有其他的几个方法,当广播来到后会自动根据广播的Action通过onReceive方法来自动分发广播也就是调用如下方法:
· onEnable:小部件第一次添加到桌面时调用该方法
· onUpdate:小部件被添加或者更新时调用,周期自动更新时机由updatePeriodMillis来指定
· onDelete:删除一次调用一次
· onDisable:最后一个该类型的桌面小部件被删除后调用

具体onReceive方法可以查看源码,以上步骤是通用的开发小部件的步骤,可以发现,小部件初始化还是更新都需要用到RemoteViews

5.1.3 PendingIntent概述

· PendingIntent是在某个不确定的时间发生了,不同于Intent立即发生
· 因为RemoteViews运行在远程进程中,不能像View通过setOnClickListener来设置单击事件,所以必须使用PendingIntent
· PendingIntent通过send和cancel方法来发送和取消特定Intent

PendingIntent支持三种意图:启动Activity、启动Service、发送广播,对应如下三种接口方法

  • getActivity(Context c,int requestCode,Intent i,int flags):返回一个PendingIntent,相当于Context.startActivity(Intent)
  • getService:类似上
  • getBroadcast:类似上,相当于Context.sendBroadcast(Intent)

下面讲解4个参数:
· requestCode表示发送方的请求码,多数为0即可,另外会影响flags的效果
· 当两个PendingIntent内部的Intent和requestCode相同时,两个PendingIntent也就相同
· 当两个Intent的ComponentName和intent-filter相同时,两个Intent也就相同

下面讲flags的四个参数
FLAG_ONE_SHOT
当前PendingIntent只使用一次,然后自动被cancel.后续相同的PendingIntent的send方法就会调用失败
对于通知栏消息,同类的通知只能使用一次,后续通知单击后无法再打开

FLAG_NO_CREATE
不主动创建,不常用

FLAG_CANCEL_CURRENT
如果当前PendingIntent已存在,那么它会被cancel,然后系统再创建一个新的PendingIntent
被cancel的消息单击后无法再打开

FLAG_UPDATE_CURRENT
已经存在,那么会被更新,即内部Intent的Extras会被换成最新的

现在让我们继续分析,最后发出通知的代码是manager.notify(1,notification).
第一个参数1是一个id,是常量.那么多次调用notify都只会弹出最新的一个通知,因为id没变.
如果id不同就会弹出多个通知

如果id不同时
· 不同的PendingIntent无论哪个标记位,都不会互相干扰
· 同PendingIntent+FLAG_ONT_SHOT:多条通知,只能点其中一个,然后其他失效.清除所有通知后重新生效
· 同PendingIntent+FLAG_CANCEL_CURRENT:只有最新的通知可打开,之前的都不行
·同PendingIntent+FLAG_UPDATE_CURRENT:之前弹出的被更新到和最新的PendingIntent一致,并且都可以打开

5.2 RemoteViews 的内部机制

我们还记得RemoteViews的构造方法有两个参数:当前应用的包名和布局文件id,下面来看支持哪种类型的View(不支持自定义View)

Layout
FrameLayout、LinearLayout、RelativeLayout、GridLayout
View
AnalogClock、Button、Chronometer、ImageButton、ImageView、ProgressBar、TextView、ViewFlipper、ListView、GridView、StackView、AdapterViewFlipper、ViewStub

使用RemoteViews的时候提供了一系列的set方法(ctrl+鼠标左键或者.set自动弹出提示,然后看方法名就能大致知道用法,不了解的再去百度)
其中RemoteViews还可以通过反射调用View的方法:setInt()

关于内部机制理论知识来啦

通知栏和小部件由NotificationManager和AppWidgetManager管理
以上两个Manager通过Binder分别和SystemServer进程中的NotificationManagerService以及AppWidgetService进行通信
所以他们的布局文件实际是在以上各自的Service中被加载的,运行在SystemServer中,是一个跨进程通信

RemoteViews是怎么做的呢?

1.RemoteViews(已经实现了Parcelable)先通过Binder传递到SystemServer进程.系统根据包名和布局id,进行常见的View加载
2.系统执行一系列View的更新任务(RemoteViews的set方法过来的)注意set方法不是立即执行,而是等待RemoteViews被加载以后才执行

系统并没有通过Binder去支持所有的View操作

· 代价太大,所以就有了Action(Parcelable接口)代表一个View操作
· 每次调用set方法,都会RemoteViews都会添加一个对应的Action,通过Manager提交我们的更新,然后在远程进程中依次进行

下面分析源码(自己打开as一步一步看··)

拿setTextViewText方法举例
源码路径:setTextViewText-setCharSequence-addAction
意思就是:我要用setText方法-我去通知兄弟(反射)加个Action-我把Action加进队列

RemoteViews的apply方法

源码路径:RemoteViews-apply
源码大致内容是,先加载布局文件,加载完后通过performApply方法去执行更新操作
performApply就遍历mActions这个列表去执行每一个Aciont的apply方法(真正操作View的方法)

Action的子类ReflectionAction(反射)的具体实现

在此之前先了解一下apply和reapply作用
· apply:首次加载布局和更新界面使用
· reapply:后续更新界面,无加载布局功能
现在查看ReflectionAction源码,其中的getMethod就是根据方法名来得到反射所需的Method对象
除了ReflectionAction,还有其他Action:TextViewSizeAction、SetOnClickPendingIntent等

关于单击事件

setOnClickPendingIntent用于给普通View设置单击事件,不能给ListView、StackView中的(集合型)View设置单击事件.应该将setPendingIntentTemplate和setOnclickFillIntent组合使用才行

5.3 RemoteViews的意义

这个小节给出的是一个跨进程更新界面的demo,书P239页
简单介绍一下这个demo,感兴趣再翻书看看

  • 为了简单起见,用广播代替Binder(都是跨进程通信手段)
  • 该例子比较实用,也类似于两个应用间的跨应用更新界面
  • 使用RemoteViews代替了AIDL,比较简单高效,但是不能用于自定义View
  • 跨应用传布局id可能会失效,所以改用直接传约定好的布局文件名
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值