App widget编程原理与技巧

App widget又叫应用程序小插件,是安卓1.5引入的新功能,把应用程序与android操作系统之间的集成提升到了一个新的高度。其历史比动态墙纸还要悠久,动态墙纸live wallpaer才不过是2.1引入的功能。但程序是否具有WIDGET功能,一是取绝于程序的需要,二是取绝于作者的意愿。虽然很多程序,没有WIDGET也可以运行的很好,而且即使你这样做了,也不一定会有多少客户愿意使用,这让WIDGET显得多少有些鸡肋。但学习WIDGET,并在合适的机会和合适的程序中展示他,无疑会给客户留下很深刻的印象。如果能让愿意客户把他天天放在桌面最显眼的位置,那就太酷太爽太兴奋太激动了,这在大拇指时代无疑是一张很好的名片加广告。

要创建应用程序小插件,首先就要了解AppWidgetProvider,我们的小插件类需要从AppWidgetProvider派生,并重载他的一些主要方法。这个类不是很复杂,从android文档中我们可以看到他的主要方法介绍:

// 编译自AppWidgetProvider.java (版本 1.5:49.0,超级位)

public class android.appwidget.AppWidgetProvider extendsandroid.content.BroadcastReceiver {

  public AppWidgetProvider();

  public voidonReceive(android.content.Context context, android.content.Intent intent);

  public voidonUpdate(android.content.Context context, android.appwidget.AppWidgetManagerappWidgetManager, int[] appWidgetIds);

  public voidonDeleted(android.content.Context context, int[] appWidgetIds);

  public voidonEnabled(android.content.Context context);

  public voidonDisabled(android.content.Context context);

}

这个类扩展了BroadcastReceiver 类,是为了方便类来处理App Widget广播。因为WIDGET不是运行在自己进程里,而是宿主进程,所以交互需要处理App Widget广播。AppWidgetProvider只接收和这个App Widget相关的事件广播,比如这个App Widget被更新,删除,启用,以及禁用。当这些广播事件发生时,AppWidgetProvider 将通过自己的方法来处理,这些方法包括:

onUpdate(Context, AppWidgetManager, int[])

这个方法调用来间隔性的更新App Widget,间隔时间用AppWidgetProviderInfo 里的updatePeriodMillis属性定义。这个方法也会在用户添加App Widget时被调用,因此它应该执行基础的设置,比如为视图定义事件处理器并启动一个临时的服务Service

onDeleted(Context, int[])

App Widget从宿主中删除时被调用。

onEnabled(Context)

当一个App Widget实例第一次创建时被调用。比如,如果用户添加两个你的App Widget实例,只在第一次被调用。如果你需要打开一个新的数据库或者执行其他对于所有的App Widget实例只需要发生一次的设置,那么这里是完成这个工作的好地方。

onDisabled(Context)

当你的App Widget的最后一个实例被从宿主中删除时被调用。你应该在onDisabled(Context)中做一些清理工作,比如关掉你的后台服务。

onReceive(Context, Intent)

这个接收到每个广播时都会被调用,而且在上面的回调函数之前。你通常不需要实现这个方法,因为缺省的AppWidgetProvider 实现过滤所有App Widget 广播并恰当的调用上述方法。但在Android 1.5中,据一些外国的大牛们认为,onDeleted()方法在该调用时有时会不被调用。为了规避这个问题,他们建议像Group post中描述的那样实现onReceive() 来接收这个onDeleted()回调。至于现在4.03还有没有这个问题,至少目前我在调试中是没有发现,但是出于安全考虑,你也许需要实现这个方法以避免出现一些用户抱怨投诉之类的麻烦。

不过这四个方法的调用一般来说取绝于你的软件情况。很多时候我们在别人的代码里只能看到onUpdate,onDeleted,一个用于提供刷新内容,一个用于释放必要的资源。一般来说,如果你需要做一个有意义的WIDGET,那么你至少需要重载onUpdate,他会在widget被创建时调用,还会在存在过程中被回调来更新内容。但是如果你的WIDGET如果足够简单,没过复杂的服务和更新,你也一个也不使用,只是从AppWidgetProvider派生一个新类,简单修改一下系统配置的XML文件,添加一些必要的WIDGET XML定义文件,也能生成一个简单的WIGGET。这样的WIDGET虽然没什么用,但对于学习来说,却是最容易理解的好对象。应用面向对象的方便的扩展机制能让你很轻松对这个简单的WIDGET进一步开发和完善,也就能做出功能丰富而界面友好的软件。

随便创建一个工程。随便创建一个工程。基于AppWidgetProvider派生一个新类,名字自定,生成后代码如下:

package com.mpw;

import android.appwidget.AppWidgetProvider;

public class mypicwidprivider extends AppWidgetProvider {

}
这是一个空类,所有方法都调用其父类的实现。然后给我们的WIDGET创建一个布局文件,在RES文件夹里添加一个mypicwid.xml文件,放在layout文件夹下,我们的WIDGET只是显示一张图片,所以添加一个img view 控件,代码如下:

  
  
mypic你可以随便找一张或者网上下一张,名字改一下就行,这是我们的widget布局文件,显示一张图片。然后我们要把这个文件添加给我们的widget使用。并且我们要定义widget的参数设置,这时在res下新建一个XML文件夹,在文件夹中添加一个widget.xml的文件,其内容如下:


  

这个文件定义了我们WIDGET最小高宽都是72DP,需要注意的是这是最小宽高,一般要求其大小等于74乘以占用格数减去2,这里是只占用一格,所以是72。更新间隔是86400000,updatePeriodMillis系统是有限制的,最短周期不能低于30分钟,就是1800000毫秒。设置widget布局文件为我们刚才定义的那个布局XML。。

最后要让系统了解我们的应用小插件,我们要在程序的androidmainifest.xml的文件里添加标记,完整代码如下:




    

    
          
              
                  
              
              
          
        
        
            
                

                
            
        
    


上面包含了我们创建的那个空类mypicwidprivider和widget声明文件xml/widget.xml,到这里,我们的一个小WIDGET就成功了,可以运行看效果。可以在添加小部件中添加,那么,WIDGET要更新怎么办?要提供多种风格给用户选择怎么办?这就需要继续深入开发了。

为widget添加配置窗口,这里简单的把我们创建工程时默认的Activity作为配置窗口调用.打开xml/widget.xml文件,添加如下语句:

android:configure="com.mpw.MypicwidActivity"

最终xml/widget.xml显示如下:

  
  
  


这时运行程序,添加WIDGET就会发现,会自动打开配置窗口 MypicwidActivity ,但由于我们没有在配置窗口中添加任何代码,使得从窗口退出后,什么事也没有发生。这时需要修改 MypicwidActivity 代码,使我们配置生效后,能根据配置结果显示 WIDGET 。在 MypicwidActivity 类的方法 onCreate中创建如下代码

Intent intent = getIntent();
		Bundle extras = intent.getExtras();
		if (extras != null) {
			mAppWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID,
					AppWidgetManager.INVALID_APPWIDGET_ID);
			
		Intent resultValue = new Intent();
		resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
					mAppWidgetId);

		setResult(RESULT_OK, resultValue);
		}
		else
		{
			setResult(RESULT_CANCELED);
		}

这里没有对WIDGET做特别处理,只是把选中的WIDGET传递给添加到desktop中,在以后的代码中可以完善这个配置,使用户能够通过一些设置来自定义WIDGET风格。但由于这关系到一些其他控件的知识,放在后面了解。这里上面的代码用到了Activity的参数传递,主要应用了intent方法和setResult方法。

上面的方法使得用户在使用 WIDGET 时可以先配置,再运行配置后的 WIDGET 。但如果我们在 WIDGET 运行期间,让用户可以随时点击 WIDGET 图标,打开 WIDGET 设置,修改 WIDGET 应该怎么做呢?很多 WIDGET 都是这样做的,其实也不难。为widget添加一个单击打的设置窗口需要使用 PendingIntent 类和 RemoteViews 类,当然这个窗口也不一定是设置窗口,也可以是另一个程序,或者是 WIDGET 控件功能的扩展。只要通过 RemoteViews.setOnClickPendingIntent(int viewId,PendingIntent pendingIntent)方法为WIDGET指定响应的行为。需要注意的是 PendingIntent Intent 不是派生关系,虽然两者命名上看起来很像有种某种或近或远的亲戚关系,并且都可以用来打一个 activity ,但 Intent 直接打开, PendingIntent 不直接打开,可以理解为暂缓打开,通过RemoteViews的setOnClickPendingIntent方法,在用户点击WIDGET图标时打开设置窗口。Widget的显示使用 RemoteViews ,因为他不是在应用程序的进程中运行,而是运行在宿主进程中,修改他要使用 RemoteViews 。在 mypicwidprivider 类中添加一个自定义的方法,具体代码如下,我们只需要使用到PendingIntent.getActivity(Contextcontext, int requestCode, Intent intent, int flags)来获取一个PendingIntent实例;
private static void updateWidgetView(Context context, int[] appWidgetIds) {
		Log.i("AAAAAAAAAAAAAAAAAA", "DDDDDDDDDDDDD");
		// ComponentName simpleWidget = new ComponentName(context,
		// mypicwidprivider.class);
		AppWidgetManager appWidgetManager = AppWidgetManager
				.getInstance(context);
		int mAppWidgetId;
		final int N = appWidgetIds.length;
		for (int i = 0; i < N; i++) {
			mAppWidgetId = appWidgetIds[i];
			RemoteViews remoteView = new RemoteViews(context.getPackageName(),
					R.layout.mypicwid);
			Intent launchAppIntent = new Intent(context, MypicwidActivity.class);
			launchAppIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
					mAppWidgetId);
			PendingIntent launchAppPendingIntent = PendingIntent.getActivity(
					context, 
                                        mAppWidgetId 
, launchAppIntent,PendingIntent.FLAG_UPDATE_CURRENT);remoteView.setOnClickPendingIntent(R.id.my_widget_img,launchAppPendingIntent);appWidgetManager.updateAppWidget(mAppWidgetId, remoteView);}}

上面的代码通过aunchAppIntent.putExtra把当前widgetID传给了配置窗口,主要用于当前WIDGET具有多个实例,且每个实例各不相同的情况,如果你的widget只能运行一个实例或者多个实例在显示上功能上都没区分,可以不需要调用aunchAppIntent.putExtra方法,直接在appWidgetManager.updateAppWidget方法的第一个参数设置为数组appWidgetIds就行了。一定不能忽略appWidgetManager.updateAppWidget,他会把我们的setOnClickPendingIntent更新给当前按键,不更新,设置不生效。RemoteViews视图创建调用的是我们的在前一节说的widget的布局文件,而侦听的对象是我们布局文件里的img控件。重载mypicwidprivider方法onUpdate把方法添加进入重载函数中,如下:

updateWidgetView(context, appWidgetIds);

最终,mypicwidprivider类如下:

package com.mpw;

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.util.Log;
import android.widget.RemoteViews;

public class mypicwidprivider extends AppWidgetProvider {
	@Override
    public void onDisabled(Context context) {
		Log.i("AAAAAAAAAAAAAAAAAA", "BBBBBBBBBBBBBBBBBBB");
        super.onDisabled(context);
    }
    @Override
    public void onEnabled(Context context) {
        // update the remove view right away
    	Log.i("AAAAAAAAAAAAAAAAAA", "CCCCCCCCCCCCCC");
		super.onEnabled(context);
    }
    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager,
        int[] appWidgetIds) {
        // all instantiated widgets will read from the same data, so we don't
        // need to track by widget id
        // Just need to request the data be updated, the internal Service will
        // handle all the rest
        // NOTE: No matter what updatePeriodMillis is set, this will not come any
        // more frequently
        // than once every 30 minutes.
    	Log.i("AAAAAAAAAAAAAAAAAA", "EEEEEEEEEEEEEEEEE");
    	updateWidgetView(context, appWidgetIds);
    }
    @Override
    public void onDeleted(android.content.Context context, int[] appWidgetIds)
    {
    	super.onDeleted(context, appWidgetIds);
    }

	private static void updateWidgetView(Context context, int[] appWidgetIds) {
		Log.i("AAAAAAAAAAAAAAAAAA", "DDDDDDDDDDDDD");
		// ComponentName simpleWidget = new ComponentName(context,
		// mypicwidprivider.class);
		AppWidgetManager appWidgetManager = AppWidgetManager
				.getInstance(context);
		int mAppWidgetId;
		final int N = appWidgetIds.length;
		for (int i = 0; i < N; i++) {
			mAppWidgetId = appWidgetIds[i];
			RemoteViews remoteView = new RemoteViews(context.getPackageName(),
					R.layout.mypicwid);
			Intent launchAppIntent = new Intent(context, MypicwidActivity.class);
			launchAppIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
					mAppWidgetId);
			PendingIntent launchAppPendingIntent = PendingIntent.getActivity(
					context, 
                                        mAppWidgetId 
, launchAppIntent,PendingIntent.FLAG_UPDATE_CURRENT);remoteView.setOnClickPendingIntent(R.id.my_widget_img,launchAppPendingIntent); //remoteView.setImageViewResource(             //        R.id.my_widget_img, R.drawable.mypic1);  appWidgetManager.updateAppWidget(mAppWidgetId, remoteView);}}}

重新运行代码,发现widget可以单击了。但由于我们的配置窗口到现在为止,并不能为用户提供有用的交互功能,要与用户交互,就要添加交互代码,我们这里设置一个简单的交互,在MypicwidActivity窗口中添加三个单选按钮,用来把WIDGET旋转90180270度和放缩。首先修改main.xml,在其中加入单选按钮,如下:

 

        

        

        
    
完整代码如下:


    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

            android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="@string/hello" />

            android:id="@+id/RadioGroup01"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" >

                    android:id="@+id/radioButton1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="90" />

                    android:id="@+id/radioButton2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="180" />

                    android:id="@+id/radioButton3"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="270" />
   


然后修改代码,在MypicwidActivity中添加代码实现让用户配置生效,在MypicwidActivity类的onCreate方法函数添加代码:

//add setting
		final RadioGroup group = (RadioGroup)findViewById(R.id.RadioGroup01);
		//appWidgetManager.updateAppWidget(mAppWidgetId, views);
		//views.setImageViewResource(
		//		R.id.my_widget_img, R.drawable.mypic1);
		//views.setImageViewBitmap(viewId, bitmap);
		group.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
			RemoteViews views = new RemoteViews(MypicwidActivity.this
					.getPackageName(), R.layout.mypicwid);
			
			AppWidgetManager appWidgetManager = AppWidgetManager
					.getInstance(MypicwidActivity.this);
			@Override
			public void onCheckedChanged(RadioGroup group, int checkedId) {
				// TODO Auto-generated method stub
				if (checkedId != -1)
				{
					RadioButton rb = (RadioButton)findViewById(checkedId);
					Bitmap rbm = null;
					if (rb != null)
					{

						rbm = BitmapFactory.decodeResource(getResources(), R.drawable.mypic);
						if (rb.getId() == R.id.radioButton1)
						{
							Matrix mat = new Matrix();
							mat.preRotate(90);
							mat.preScale(2, 2);
							Bitmap rbm1 = Bitmap.createBitmap(rbm, 0,0,rbm.getWidth(), rbm.getHeight(), mat,false);
							views.setImageViewBitmap(R.id.my_widget_img, rbm1);
						}
						else if (rb.getId() == R.id.radioButton2)
						{
							Matrix mat = new Matrix();
							mat.preRotate(180);
							mat.preScale(2, 2);
							Bitmap rbm1 = Bitmap.createBitmap(rbm, 0,0,rbm.getWidth(), rbm.getHeight(), mat,false);
							views.setImageViewBitmap(R.id.my_widget_img, rbm1);
						}
						else
						{
							Matrix mat = new Matrix();
							mat.preRotate(270);
							mat.preScale(2, 2);
							Bitmap rbm1 = Bitmap.createBitmap(rbm, 0,0,rbm.getWidth(), rbm.getHeight(), mat,false);
							views.setImageViewBitmap(R.id.my_widget_img, rbm1);
						}
						
						Intent intent = new Intent(MypicwidActivity.this, MypicwidActivity.class);
						intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,mAppWidgetId);
						PendingIntent pendingIntent = PendingIntent.getActivity(MypicwidActivity.this, mAppWidgetId,
								intent, 0);
						views.setOnClickPendingIntent(R.id.my_widget_img, pendingIntent);
			            
						appWidgetManager.updateAppWidget(mAppWidgetId, views);
						Intent resultValue = new Intent();
						resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
									mAppWidgetId);

						setResult(RESULT_OK, resultValue);
						finish();
					}
					else
					{
						
					}
				}
				
			}
		});

运行后可以设置了,重新开机,发现widget图标又恢复原状了,这是因为我们的数据没有保存,这里对数据进行保存。使用SharedPreferences类可以很方便的对所有应用的参数保存和取出。在MypicwidActivity中添加代码实现保存:

SharedPreferences sp = MypicwidActivity.this.getSharedPreferences("mypicwid", Context.MODE_PRIVATE); 
							 Editor edit = sp.edit(); 
							 edit.putInt("rotion"+mAppWidgetId, 90);
							 edit.commit(); 

把角度保存到 /data/data/com.mpw/shared_prefs/mypicwid.xml文件中,使用rotion+id区分每个widget的不同配置。然后在mypicwidprivider类的onUpdate方法中读取这些设置,更新相关WIDGET。代码如下:

int dv = 0;
			dv = sp.getInt("rotion"+mAppWidgetId, dv);
			Bitmap rbm = BitmapFactory.decodeResource(context.getResources(), R.drawable.mypic);
			Matrix mat = new Matrix();
			mat.preRotate(dv);
			mat.preScale(2, 2);
			Bitmap rbm1 = Bitmap.createBitmap(rbm, 0,0,rbm.getWidth(), rbm.getHeight(), mat,false);
			remoteView.setImageViewBitmap(R.id.my_widget_img, rbm1);
			
			appWidgetManager.updateAppWidget(mAppWidgetId, remoteView);

这样就实现了保存和读取功能。

最后有关widget的一个重要内容是更新,如果要动态更新WIDGET怎么办,可以使用服务。其实上面的代码只是随便写写,最好的方法是使用SharedPreferences 把配置保存,在mypicwidprivider创建一个服务监听onSharedPreferenceChanged修改,通过update刷新。



参考文档:http://blog.csdn.net/silenceburn/article/details/6093074
http://blog.csdn.net/iefreer/article/details/4626274

致谢

你可能感兴趣的:(MTK专栏)