欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Android RemoteViews的基本使用(下)之窗口小部件

程序员文章站 2022-05-30 16:47:27
...

一,写在前面            

        在文章RemoteViews的基本使用(上)之通知栏 中讲述了的RemoteViews使用场景之通知栏,这篇文章主要讲述RemoteViews在窗口小部件中的使用。在写好了一个窗口小部件之后,如果需要对小部件的界面进行更新,由于在本应用中无法调用findViewbyid(id)方法获取控件引用(需要跨进程访问界面),这个时候RemoteViews就派上用场了。在上篇文章中讲到,RemoteViews可以实现跨进程更新界面,内部实现原理是Binder机制,后面会单独更新一篇从源码角度分析RemoteViews。这篇文章主要介绍RemoteViews在窗口小部件的使用,并介绍窗口小部件的简单实现。

二,窗口小部件

       如何实现一个窗口小部件呢,大致需要这样几个步骤:
       1,创建一个类MyAppWidgetProvider,并继承AppWidgetProvider,并重写方法:onReceiver,onEnabled,onDisabled,onUpdate,onDeleted等;
       2,在AndroidManifest.xml文件中对MyAppWidgetProvider进行配置,需要引入一个xml文件,见步骤3;
       3,在res目录下,创建xml文件夹,并提编辑一个AppWidgetProviderInfo对应的资源文件:my_appwidget_info.xml,;需要引用一个xml布局,见步骤4;
       4,创建一个layout的布局文件:my_appwidget.xml;

 

AppWidgetProvider的注册     

       分析:AppWidgetProvider继承了BroadcastReceiver,可以看出窗口小部件就是一个广播接受者,因此在步骤2中需要对广播进行注册,查看官方文档有这样一个例子:
Android RemoteViews的基本使用(下)之窗口小部件
       可以看到我们配置了一个这样的action:android.appwidget.action.APPWIDGET_UPDATE,官方文档有描述这个action是必须配置的。在实际测试之后发现,如果不添加该action,那么在窗口小部件列表中找不到这个app widget。AppWidgetProvider可以接受其他的广播,可以在Intent-filter中配置其他广播action。 <mata-data>是指定AppWidgetProviderInfo对应的资源文件,也就是步骤3中的my_appwidget_info.xml。

AndroidManifest.xml添加代码如下:
        <receiver android:name="com.example.appwidgetdemo.MyAppWidgetProvider" >    
            <intent-filter>        
                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
                <!-- 注册接受点击Button发送的广播 --> 
                <action android:name="com.widget.wang"/>
            </intent-filter>    
            
            <meta-data 
                android:name="android.appwidget.provider"     
                android:resource="@xml/my_appwidget_info" />
        </receiver>

AppWidgetProviderInfo对应资源文件

附上my_appwidget_info.xml文件的代码,如下:
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" 
    android:minWidth="180dp"  
    android:minHeight="110dp"  
    android:previewImage="@drawable/ic_launcher"
    android:initialLayout="@layout/my_appwidget">

</appwidget-provider>
分析:在创建该资源文件时,选择Resource Type为AppWidget Provider,这样会自动生成一个带有appwidget-provider标签的文件,手写亦可。

下面分析一些常用的属性:
minWidth,minHeight:小部件的最小宽度,最小高度。它是一个约束值,小部件的具体宽高会根据设备的网格单元来设置,它们一定会>=minWidth/minHeight。也就是说小部件的宽高大小的单位是网格单元,有些手机会提供4*4网格,平板提供8*7网格,网格与dp值的关系,见如下表格:
Android RemoteViews的基本使用(下)之窗口小部件
例如:minWidth设置为30dp,那么系统会设置小部件宽度为一个单元格,大小为40dp;

previewImage:窗口小部件列表中的预览图片;
initialLayout:初始化布局,显示在桌面上的布局文件;
当然,还有其他一些属性,这里就不一一介绍了。


layout布局文件

代码如下:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    
    <TextView 
        android:id="@+id/tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="18sp"
        android:textColor="#f00"
        android:layout_centerHorizontal="true"
        android:text="TextView"/>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/tv"
        android:orientation="horizontal">
        <ImageView 
            android:id="@+id/iv"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:src="@drawable/ttnkh"/>
        <Button 
            android:id="@+id/btn"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:layout_gravity="center"
            android:text="Button"/>
    </LinearLayout>

</RelativeLayout>


AppWidgetProvider的生命周期

先描述窗口小部件生命周期各个方法的调用时机:
onReceive:接受系统发送的广播,用于调度onEnabled,onDisabled,onUpdate,onDeleted方法的调用;
onEnabled:只在app widget第一次出现在桌面时调用;
onDisabled:只在最后一个小部件被删除时调用;
onUpdate:小部件每次添加到桌面,或小部件更新时调用;
onDeleted:部件从桌面删除时调用,删除一次,调用一次;

接下来,查看AppWidgetProvider$onReceive源码,查看其工作机制:
    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);
        }
    }

        当系统发送广播时,AppWidgetProvider可以接受广播,并对广播中的action进行判断,分别调用onEnabled,onDisabled,onUpdate,onDeleted等方法。查看源码可知,这些方法的方法体都是空的,具体的实现需要子类重写咯。

MyAppWidgetProvider代码如下:
public class MyAppWidgetProvider extends AppWidgetProvider {

	@Override
	public void onReceive(Context context, Intent intent) {
		super.onReceive(context, intent);
		if ("com.widget.wang".equals(intent.getAction())) {
			//接受点击按钮后发送的广播,将RemoteView的图片设置为R.drawable.ic_launcher
			RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.my_appwidget);
			rv.setImageViewResource(R.id.iv, R.drawable.ic_launcher);
			
			AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
			ComponentName provider = new ComponentName(context, MyAppWidgetProvider.class);
			appWidgetManager.updateAppWidget(provider, rv);
		}
	}
	
	@Override
	public void onEnabled(Context context) {
		super.onEnabled(context);
		Log.e("MyAppWidgetProvider", "call onEnabled");
	}
	
	@Override
	public void onUpdate(Context context, AppWidgetManager appWidgetManager,
			int[] appWidgetIds) {
		super.onUpdate(context, appWidgetManager, appWidgetIds);
		Log.e("MyAppWidgetProvider", "call onUpdate");
		
		RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.my_appwidget);
		rv.setTextViewText(R.id.tv, "更新窗口小部件界面");
		rv.setTextColor(R.id.tv, Color.WHITE);
		
		Intent intent = new Intent();
		intent.setAction("com.widget.wang");
		PendingIntent pi = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
		// 给Button设置点击事件,触发一个Intent,这里是发送广播
		rv.setOnClickPendingIntent(R.id.btn, pi);
		appWidgetManager.updateAppWidget(appWidgetIds, rv);
		
	}
	
	@Override
	public void onDeleted(Context context, int[] appWidgetIds) {
		super.onDeleted(context, appWidgetIds);
		Log.e("MyAppWidgetProvider", "call onDeleted");
	}
	
	@Override
	public void onDisabled(Context context) {
		super.onDisabled(context);
		Log.e("MyAppWidgetProvider", "call onDisabled");
	}
	
}

验证窗口小部件的生命周期方法的调用,执行这样一些操作:添加小部件到桌面 -> 添加小部件到桌面->删除小部件->删除小部件。查看日志如下:
Android RemoteViews的基本使用(下)之窗口小部件
        现在,介绍一下该Demo具体实现的一个简单需求:小部件添加到桌面时,设置TextView控件的文字内容,文字颜色,并给Button设置点击事件。
        
通过上面简单分析可知,在onUpdate方法中实现上面需求。若想跨进程更新窗口小部件的界面,有这样一些步骤:
1,需创建一个RemoteViews对象,构造方法参数传入包名,以及布局资源id;
2,对RemoteViews中控件进行更新,以及设置控件的点击事件;
3,获取一个AppWidgetManager对象,调用updateAppWidget(...)方法更新窗口小部件,该方法有一个参数需要传入步骤1中的RemoteViews的实例;

发送广播       
点击按钮后,发送一个action为"com.widget.wang"的广播,具体实现见上面代码。
接受广播:设置窗口小部件中的ImageView控件的图片资源
MyAppWidgetProvider接受广播,需要在AndroidManifest.xml对广播进行注册,并在onReceive方法中接受广播并处理。接受action为"com.widget.wang"的广播,更新窗口小部件的界面需要使用RemoteViews,步骤同上。
窗口小部件展示如下:
点击按钮前
Android RemoteViews的基本使用(下)之窗口小部件

点击按钮后,如下:
Android RemoteViews的基本使用(下)之窗口小部件

三,另外

         在重写的onReceive方法中,有这样一行代码:super.onReceive(context, intent),通过上面分析知道,它完成对窗口小部件生命周期的调度。不断接受系统发送的广播,所以onEnabled,onDisabled,onUpdate,onDeleted被调用前都会调用onReceive方法。
         验证时,若在onReceive方法中添加Log,日志信息如下:
Android RemoteViews的基本使用(下)之窗口小部件

四,最后

本篇文章介绍了如何实现一个简单的窗口小部件,而,RemoteViews用于更新远程进程中窗口小部件的界面。

这篇文章就分享到这里啦,有疑问可以留言,亦可纠错,亦可补充,互相学习...^_^