第五章-RemoteViews应用

一、RemoteViews的应用

RemoteViews在实际的开发中,主要用在通知栏和桌面小部件的开发过程中。通知栏每个人都不陌生,主要是通过NotificationManager的notify方法去实现的,它除了默认效果外,还可以另外自定义布局。桌面小部件则是通过AppWidgetProvider来实现的,AppWidgetProvider本质上就是一个广播。通知栏和桌面小部件的开发过程中都会用到RemoteViews,它们在更新界面时无法像在Activity里面那样直接更新View,这是因为两者的界面都运行在其它进程中,确切来说是系统的SystemService进程。为了跨进程更新界面,RemoteViews提供了一系列的set方法,并且这些方法只是View全部方法的子集,另外RemoteViews中所支持的View类型也是有限的。
下面简单介绍下RemoteViews在通知栏和桌面小部件中的使用方法。

1.RemoteViews在通知栏上的应用
首先来看下通知栏,我们先了解一下系统默认的样式

String title = "通知标题";
String content = "通知内容";

String id = "channel_id_01";
String name="channel_id_01_name";
Context context = getApplication();
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
Notification notification = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {//解决android9.0上通知不显示的问题。
	NotificationChannel mChannel = new NotificationChannel(id, name, NotificationManager.IMPORTANCE_LOW);
	notificationManager.createNotificationChannel(mChannel);

	Intent intent = new Intent(this, testCustomViewActivity.class);
	PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
	//新的sdk中找不到这个方法了。会报错
	//notification.setLatestEventInfo(this, "Test", "This is Notification", pendingIntent);

	notification = new Notification.Builder(context)
			.setChannelId(id)
			.setContentTitle(title)
			.setContentText(content)
			.setContentIntent(pendingIntent)//设置跳转到指定的activity
			.setAutoCancel(true)//设置点击跳转后自动清除消息
			.setSmallIcon(R.mipmap.ic_launcher).build();
} else {
	NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(context)
			.setContentTitle(title)
			.setContentText(content)
			.setSmallIcon(R.mipmap.ic_launcher)
			.setOngoing(true);
	notification = notificationBuilder.build();
}

notificationManager.notify(1008, notification);//注意第一个参数如果是一个常量,那么每次通知都覆盖。如果每次都不同,在通知栏就会出现多个消息

上面会弹出一个系统的默认的通知(兼容了android9.0)。点击通知的时候会跳转到指定的activity,并且清除本身。
效果如下:

为了满足个性化需求,我们还可能会用到自定义通知。自定义通知也很简单,首先我们要提供一个布局文件,然后通过RemoteViews来加载这个布局文件改变通知的样式,代码如下所示:

		String id = "channel_id_01";
        String name="channel_id_01_name";
        Context context = getApplication();
        NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
        Notification notification = null;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {//解决android9.0上通知不显示的问题。
            NotificationChannel mChannel = new NotificationChannel(id, name, NotificationManager.IMPORTANCE_LOW);
            notificationManager.createNotificationChannel(mChannel);

            /** 生成跳转的intent */
            Intent intent = new Intent(this, testCustomViewActivity.class);
            PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);

            /** 构造RemoteView */
            RemoteViews remoteViews = new RemoteViews(getPackageName(),R.layout.layout_notification);
            remoteViews.setTextViewText(R.id.tv_title,"自定义标题_Hello");
            remoteViews.setTextViewText(R.id.tv_content,"自定义内容_welcome to android world");
            remoteViews.setImageViewResource(R.id.iv_img,R.mipmap.ic_launcher);
            remoteViews.setOnClickPendingIntent(R.id.bt_confirm,pendingIntent);//给自定义view的按钮设置一个跳转监听,如果不设置,点击按钮就跳转不了

            notification = new Notification.Builder(context)
                    .setChannelId(id)
                    .setCustomContentView(remoteViews)//设置自定义的布局
                    .setContentIntent(pendingIntent)//设置跳转到指定的activity。这个和上面那个按钮的跳转都是生效的。
                    .setAutoCancel(true)//设置点击跳转后自动清除消息
                    .setSmallIcon(R.mipmap.ic_launcher).build();
        } else {

            /** 生成跳转的intent */
            Intent intent = new Intent(this, testCustomViewActivity.class);
            PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);

            /** 构造RemoteView */
            RemoteViews remoteViews = new RemoteViews(getPackageName(),R.layout.layout_notification);
            remoteViews.setTextViewText(R.id.tv_title,"自定义标题_Hello");
            remoteViews.setTextViewText(R.id.tv_content,"自定义内容_welcome to android world");
            remoteViews.setImageViewResource(R.id.iv_img,R.mipmap.ic_launcher);
            remoteViews.setOnClickPendingIntent(R.id.bt_confirm,pendingIntent);//给自定义view的按钮设置一个跳转监听,如果不设置,点击按钮就跳转不了

            NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(context)
                    .setSmallIcon(R.mipmap.ic_launcher)
                    .setAutoCancel(true)//设置点击跳转后自动清除消息
                    .setCustomContentView(remoteViews)//设置自定义的布局
                    .setOngoing(true);
            notification = notificationBuilder.build();
        }

        /** 发送通知 */
        notificationManager.notify(1008, notification);

布局文件layout_notification.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
	android:id="@+id/ll_nf"
	android:layout_width="match_parent"
	android:layout_height="match_parent"
	android:gravity="center_vertical"
	android:orientation="horizontal">

	<ImageView
		android:id="@+id/iv_img"
		android:layout_width="20dp"
		android:layout_height="20dp"
		android:background="@mipmap/ic_launcher"
		/>

	<LinearLayout
		android:layout_width="0dp"
		android:layout_height="wrap_content"
		android:layout_weight="1"
		android:gravity="center_vertical"
		android:orientation="vertical"
		android:paddingLeft="10dp">

		<TextView
			android:id="@+id/tv_title"
			android:layout_width="wrap_content"
			android:layout_height="wrap_content"
			android:textColor="@android:color/black" />

		<TextView
			android:id="@+id/tv_content"
			android:layout_width="wrap_content"
			android:layout_height="wrap_content"
			android:textColor="@android:color/black" />


	</LinearLayout>
	<Button
		android:id="@+id/bt_confirm"
		android:layout_width="wrap_content"
		android:layout_height="wrap_content"
		android:text="OK"
		/>

</LinearLayout>

以上代码在Android9.0上测试通过。
注意:点击按钮跳转的时候,通知并不会自动清除。
效果如下:
在这里插入图片描述

关于PendingIntent,它表示的是一种待定的Intent,这个Intent中所包含的意图必须由用户来触发。为什么更新RemoteViews如此复杂呢?
直观原因是因为RemoteViews没有提供和View类似的findViewById这个方法,因此我们无法获取到RemoteViews中的子View,当然实际原因绝非如此,具体会在下面分析。

2.RemoteViews 在桌面小部件的应用

AppWidgetProvider是Android提供给的用于实现桌面小部件的类,其本质也就是一个广播,即BroadcastReceived。所以实际使用中把他看成一个广播即可,我们来看下怎么去具体的实现一个小部件。

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="wrap_content"
    android:orientation="horizontal"
    android:background="#ffffff"
    >

    <ImageView
        android:id="@+id/iv1"
        android:layout_width="wrap_content"
        android:layout_height="30dp"
        android:src="@mipmap/ic_launcher"
        android:layout_marginTop="4dp"
        />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:layout_marginLeft="8dp"
        android:layout_marginTop="2dp"
        android:layout_marginBottom="2dp"
        >
        <TextView
            android:id="@+id/tv_singer"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="邓紫棋"
            android:textSize="14dp" />

        <TextView
            android:id="@+id/tv_lyric"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="因为成长,我们逼不得已要习惯,因为成长。"
            android:textSize="12dp"
            />
    </LinearLayout>

</LinearLayout>

2.定义小部件配置信息

在res/xml/下中新建一个appwidget_provider_info.xml文件(名称是随意的,只要和后面的AndroidManifest中配置对应好就行)

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:initialLayout="@layout/widget"
    android:minWidth="250dp"
    android:minHeight="40dp"
    android:updatePeriodMillis="86400000">

</appwidget-provider>

上面的几个参数的含义很明确,android:initialLayout就是加载布局,其它两个就是最小的高宽,而updatePeriodMillis就是更新小组件的时间周期。
特别注意:这里的宽高比就是在桌面添加的比例(例如:4 x 1)

3.定义小部件的实现类
这个类需要继承AppWidgetProvider,代码如下(已经在android9.0上测试通过):


package com.example.appWidgetProvider;

import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.os.SystemClock;
import android.util.Log;
import android.widget.RemoteViews;
import android.widget.Toast;

import com.example.test.R;

public class MyAppWidgetProvider extends AppWidgetProvider {
    public static final String TAG = "MWP";
    public static final String CLICK_ACTION = "com.example.test.action.CLICK";

    private AppWidgetManager appWidgetManage;
    private float degree;
    private Bitmap bitmap;

    public MyAppWidgetProvider() {
        super();
    }


    @Override
    public void onReceive(final Context context, Intent intent) {
        super.onReceive(context, intent);
        String action = intent.getAction();
        Log.i(TAG, "onReceive , action:" + action);

        if (CLICK_ACTION.equals(action)) {
            Toast.makeText(context, "小部件接收到了自定义的点击事件,onReceive调用", Toast.LENGTH_SHORT).show();

            new Thread(new Runnable() {
                @Override
                public void run() {
                    bitmap = BitmapFactory.decodeResource(context.getResources(), R.mipmap.ic_launcher);

                    appWidgetManage = AppWidgetManager.getInstance(context);
                    for (int i = 0; i < 37; i++) {
                        degree = (i * 10) % 360;
                        RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget);
                        remoteViews.setImageViewBitmap(R.id.iv1, rotateBitmap(bitmap,degree));
                        Intent intentClick = new Intent();
                        intentClick.setClass(context,MyAppWidgetProvider.class);
                        intentClick.setAction(CLICK_ACTION);
                        PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intentClick, 0);
                        remoteViews.setOnClickPendingIntent(R.id.iv1, pendingIntent);
                        appWidgetManage.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");

        RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget);
        Intent intentClick = new Intent();
        intentClick.setClass(context,MyAppWidgetProvider.class);//必须要添加这个,否则点击发送不了广播
        intentClick.setAction(CLICK_ACTION);
        PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intentClick, 0);//发送广播
//        Intent intent = new Intent(context, testCustomViewActivity.class);//跳转到指定的activity
//        PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
        Log.d(TAG,"pendingIntent = " + pendingIntent);
        remoteViews.setOnClickPendingIntent(R.id.iv1, pendingIntent);

        appWidgetManage = AppWidgetManager.getInstance(context);
        appWidgetManage.updateAppWidget(new ComponentName(context, MyAppWidgetProvider.class), remoteViews);

    }

    //动画
    private Bitmap rotateBitmap(Bitmap srcBitmap ,float degree) {
        Bitmap temBitmap = null;
        try {
            Matrix matrix = new Matrix();
            matrix.reset();
            matrix.setRotate(degree);
            temBitmap = Bitmap.createBitmap(srcBitmap, 0, 0, srcBitmap.getWidth(), srcBitmap.getHeight(), matrix, true);
        } catch (Exception e) {
            Log.e(TAG,"error = " + e.getMessage());
        }
        return temBitmap;
    }

}

上面的代码实现一个类似歌词的页面,点击图标可以触发对应的点击事件。

4.在清单文件中声明小部件

最后一步,因为桌面小部件本质是一个广播组件,因此必须要在AndroidManifest中注册,如下:

<!--小部件 AppWidgetProvider-->
<receiver android:name="com.example.appWidgetProvider.MyAppWidgetProvider">
	<meta-data
		android:name="android.appwidget.provider"
		android:resource="@xml/appwidget_provider_info" />

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

上面的代码有两个Action,其中第一个是识别小部件的动作,第二个就是他的标识,必须存在,这是系统的规范
运行的效果如下:
在这里插入图片描述

我们还可以在activity中发送广播来更新小部件的View,例如我们在主页点击下按钮发送一个广播:


Intent intent = new Intent();
intent.setClass(this, MyAppWidgetProvider.class);
intent.setAction("com.example.test.action.CLICK");
sendBroadcast(intent);

实际测试,小部件中的onReceive可以正常回调

在实现小部件的过程中遇到过几个小地方需要注意下(在android9.0上,可能低版本不存在)
注意点:

1、在AndroidStudio中,默认创建的工程项目中有/res/mipmap-anydpi-v26这个文件夹 这个会导致 bitmap
= BitmapFactory.decodeResource(context.getResources(), R.mipmap.ic_launcher); 返回一个null,导致app挂掉。不能继续点击了。 解决方法:暂时把这个文件夹删除就OK了。

2、点击不能正常发送广播(onReceived不能回调) Intent intentClick = new Intent();
intentClick.setClass(context,MyAppWidgetProvider.class);//需要设置这个,书中的写法实际测试无效。
intentClick.setAction(CLICK_ACTION);

3、部件占用的宽高设置 在appwidget_provider_info.xml的宽高比例,决定了小部件在桌面上的
android:minWidth=“250dp”
android:minHeight=“40dp” 例如设置成上面比例,就是4 x 1

AppWidgetProvider 除了最常用的onUpdate方法,还有其他几个方法,onEnabled,onDisabled,onDeleted以及onReceive。这些方法都会被onReceive在适当的时候调用,所以含义如下:

  • onEnabled:当该窗口小部件第一次添加到桌面的时候调用该方法,可添加多次但是只有第一次调用。
  • onUpdate:小部件被添加或者第一次更新的时候都会调用一次该方法,小部件的更新机制由
  • updatePeriodMillis来指定,每个周期小部件都会自动更新一次。
  • onDeleted:每删除一次小部件就会调用一次。
  • onDisabled:当最后一个该类型的桌面小部件被删除时调用该方法,注意是最后一个。
  • onReceiver:这是广播的内置方法,用于分发具体的事件给其它方法。

关于AppWidgetProvider 的onReceiver方法的具体分发过程,可以参看源码中的实现,如下所示:


public void onReceive(Context context, Intent intent) {
	// Protect against rogue update broadcasts (not really a security issue,
	// just filter bad broacasts out so subclasses are less likely to crash).
	String action = intent.getAction();
	if (AppWidgetManager.ACTION_APPWIDGET_UPDATE.equals(action)) {
		Bundle extras = intent.getExtras();
		if (extras != null) {
			int[] appWidgetIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_IDS);
			if (appWidgetIds != null && appWidgetIds.length > 0) {
				this.onUpdate(context, AppWidgetManager.getInstance(context), appWidgetIds);
			}
		}
	} else if (AppWidgetManager.ACTION_APPWIDGET_DELETED.equals(action)) {
		Bundle extras = intent.getExtras();
		if (extras != null && extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_ID)) {
			final int appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID);
			this.onDeleted(context, new int[] { appWidgetId });
		}
	} else if (AppWidgetManager.ACTION_APPWIDGET_OPTIONS_CHANGED.equals(action)) {
		Bundle extras = intent.getExtras();
		if (extras != null && extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_ID)
				&& extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS)) {
			int appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID);
			Bundle widgetExtras = extras.getBundle(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS);
			this.onAppWidgetOptionsChanged(context, AppWidgetManager.getInstance(context),
					appWidgetId, widgetExtras);
		}
	} else if (AppWidgetManager.ACTION_APPWIDGET_ENABLED.equals(action)) {
		this.onEnabled(context);
	} else if (AppWidgetManager.ACTION_APPWIDGET_DISABLED.equals(action)) {
		this.onDisabled(context);
	} else if (AppWidgetManager.ACTION_APPWIDGET_RESTORED.equals(action)) {
		Bundle extras = intent.getExtras();
		if (extras != null) {
			int[] oldIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_OLD_IDS);
			int[] newIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_IDS);
			if (oldIds != null && oldIds.length > 0) {
				this.onRestored(context, oldIds, newIds);
				this.onUpdate(context, AppWidgetManager.getInstance(context), newIds);
			}
		}
	}
}

上面描述了开发一个桌面小部件的典型过程 ,例子比较简单,实际开发过程中会稍微复杂一些,但是开发流程都是一样的。
可以发现,桌面小部件在界面上的操作都要通过RemoteViews,不管是小部件的界面初始化还是界面更新都必须依赖它。

3.PendingIntent概述

在这里插入图片描述
在这里插入图片描述

如图中所示,这三个方法的参数都是一样的,主要理解的是第二个参数requstCode和第四个参数flags,code代表的是发送码,多数情况下为0,而且code会影响到flag,flag常见的有几种我们下面会说,其实最主要是理解匹配规则,

PendingIntent的匹配规则为:如果两个PendingIntent他们内部的Intent相同并且requstCode也相同的话,那么PendingIntent就是相同的,code比较好理解,那什么情况下Intent相同呢,Intent的匹配规则是:如果两个Intent的ComponentName的匹配过程,只要Intent之间的ComponentName和intent-filter相同,那么这两个intent就相同,需要注意的是Extras不参与匹配过程,只要intent之间的name和intent-filter相同就行,我们再来说下flags的参数含义

  • FLAG_ONE_SHOT

当前描述的PendingIntent只能被使用一次,然后他就会被cancel,如果后续还有相同的PendingIntent,那么他的send方法就会失败,对于通知栏的消息来说,如果采用此标记位,那么同类的通知只能使用一次,后续将无法打开

  • FLAG_NO_CREATE

当前描述的PendingIntent不会主动去创建,如果当前PendingIntent之前不存在,那么getActivity等方法都会直接返回null,即获取PendingIntent失败,这个标记位很少见,他无法单独使用,因此在日常开发当中,并没有太多的意义,这里就不过多的介绍了

  • FLAG_CANCEL_CURRENT

当前描述的PendingIntent如果已经存在,那么就会被cancel,然后系统创建一个新的PendingIntent,对于通知栏来说,那些被cancel的消息将无法被打开

  • FLAG_UPDATE_CURRENT

当前描述的PendingIntent如果已经存在的话,那么他们就会被更新,他们的intent中的extras会被替换成新的

在这里插入图片描述

发布了126 篇原创文章 · 获赞 42 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/gaopinqiang/article/details/105230868