第五章-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會被替換成新的

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章