说说Android桌面(Launcher应用)背后的故事(九)——让我的桌面多姿多彩

时间:2022-11-24 17:53:07

博客搬家啦——为了更好地经营博客,本人已经将博客迁移至www.ijavaboy.com。这里已经不再更新,给您带来的不便,深感抱歉!这篇文章的新地址:点击我说说Android桌面(Launcher应用)背后的故事(九)——让我的桌面多姿多彩
 

       到这里我们的Launcher已经可以跑起来了,而且效果也如系统Launcher一般,但是,遗憾的是,我们的桌面上似乎都是一个摸样的Shortcut,而再看看系统桌面上,Search框,天气控件啊,各种大小参差不齐,界面上的控件丰富多彩。桌面上除了一个个Shortcut之外,还应该有各种大小不一的控件——Widget。

        要想让我们的桌面也支持Widget,我们就要对Widget这个东西稍加研究一翻。Widget是一种特殊的独立体,可以嵌入在另一个应用中,只要这个应用实现为一个WidgetHost,那么它就可以容纳各式各样的Widget。Android已经提供了一套Widget开发接口,包括两个方面:一个是创建Widget,一个是创建WidgetHost。为了避免直接将复杂的代码加入到已经实现的功能当中,我们先通过两个小Demo来分别看看如何开发一个Widget,以及如何实现一个WidgetHost,这样,通过这两个Demo,再将实现支持Widget的代码添加到我们的Launcher中,就比较容易理解了。

       在此申明:以下两个Demo,从本人的个人代码库中取得,由于时间较久,不知道是收集网上的,还是纯粹自己写的。如果您发现是您的功劳,本人深表抱歉,但是您的Demo写的很有参考价值,借用一下,在此说声:Thank you!

一、开发一个Widget——桌面小部件

开发一个Widget,做如下准备

1、appwidget-provider:在res\xml\目录下创建一个.xml文件,里面内容如下:

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:minWidth="294dip"
  android:minHeight="150dip"
  android:updatePeriodMillis="0"
  android:initialLayout="@layout/widget_demo">
    
</appwidget-provider>

解释一下:这个文件主要定义了小部件需要在桌面上占据的大小以及指定小部件所用的布局文件。其中android:updatePeriodMillis指的是每隔多长时间更新一下小部件。这个值在1.5之后的版本中好像至少要30分钟以上才会生效,但是,本人没有试过。只知道,将其设置成几秒,几分钟是肯定无效的。

2、继承AppWidgetProvider,实现自己需要的逻辑:不要被其名字给蒙骗了,它是一个BroadcastReceiver,它实现了onReceive方法,同时,提供了几个自己的方法:

        * onUpdate(Context,AppWidgetManager,appWidgetIds)

        * onDeleted(Context context, int[] appWidgetIds)

        * onEnabled(Context context)

        * onDisabled(Context context)

具体每个方法的含义,自己查下文档。我们经常开发的时候需要的就是实现onUpdate方法,这个方法就是更新添加到桌面上的Widget

3、在manifest中配置:

        <receiver android:name="DemoAppWidgetProvider">
        	<meta-data android:name="android.appwidget.provider"
        			android:resource="@xml/widget_demo" />	
        	<intent-filter>
        		<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
        	</intent-filter>

        </receiver>

其中指定了一个meta-data,用来指定Widget说明文件。同时,intent-filter中使用android.appwidget.action.APPWIDGET_UPDATE这个Action。

4、Widget的布局文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:orientation="vertical"
  android:layout_width="match_parent"
  android:layout_height="match_parent">
  	<LinearLayout 
  		android:orientation="vertical"
  		android:layout_width="fill_parent"
  		android:layout_height="100dip"
  		android:background="#F3F3F3">
  		
  		<TextView android:id="@+id/demo"
	    	android:layout_width="wrap_content"
	    	android:layout_height="wrap_content"
	    	android:layout_marginLeft="20dip"
	    	android:layout_marginTop="20dip"
	    	android:text="this is a demo"
	    	android:textSize="20sp"
	    	android:textStyle="bold"/>	

  	</LinearLayout>
  	
  	<LinearLayout 
  		android:orientation="horizontal"
  		android:layout_width="fill_parent"
  		android:layout_height="50dip"
  		android:background="@drawable/widget_bottom">
  		
  		<Button android:id="@+id/pre"
  			android:layout_width="wrap_content"
  			android:layout_height="wrap_content"
  			android:background="@drawable/pre_bg"
  			android:layout_marginLeft="20dip"
  			android:layout_marginTop="2dip"/>
  			
  		<Button android:id="@+id/next"
  			android:layout_width="wrap_content"
  			android:layout_height="wrap_content"
  			android:background="@drawable/next_bg"
  			android:layout_marginLeft="20dip"
  			android:layout_marginTop="2dip"/>  			
  			
  	</LinearLayout>
    
    
</LinearLayout>

说明:这个就是正常的布局文件。但是关于布局文件的规格系统文档中有个指导,需要自己按照自导文档去看看关于横竖屏中定义Widget需要注意的问题。主要是大小问题。

除了上面这些,你可能还需要一个Service。假如,要完成的操作比较耗时,则需要创建一个Service来完成主要的功能。这个Demo中就创建了一个Service。

下面直接上代码了:


public class DemoAppWidgetProvider extends AppWidgetProvider {
	
	public static final ComponentName APPWIDGET_COMPONENT = new ComponentName("demo.widget", "demo.widget.DemoAppWidgetProvider");
	
    public void onUpdate(Context context, AppWidgetManager appWidgetManager
    		, int[] appWidgetIds) {
    	
    	/**
    	 * 当Widget被添加到桌面的时候执行
    	 */
    	
    	final RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_demo);
    	
    	linkButtons(context, views);
    	
    	final AppWidgetManager gm = AppWidgetManager.getInstance(context);
    	
    	if(appWidgetIds != null){
    		gm.updateAppWidget(appWidgetIds, views);
    	}else{
    		gm.updateAppWidget(APPWIDGET_COMPONENT, views);
    	}
    	
    	//启动DemoService
    	context.startService(new Intent(ActionDefinition.ACTION_APP_WIDGET_SERVICE));
    }

    /**
     * 为按钮绑定事件
     * @param context
     * @param views
     */
	private void linkButtons(Context context, RemoteViews views) {
		
		final ComponentName serviceName = new ComponentName(context, DemoService.class);
		Intent intent = null;
		PendingIntent pIntent = null;
		
		//为pre按钮绑定onClick事件
		intent = new Intent(ActionDefinition.ACTION_APP_WIDGET_PREV);
		intent.setComponent(serviceName);
		pIntent = PendingIntent.getService(context, 0, intent, 0);

		views.setOnClickPendingIntent(R.id.pre, pIntent);
		
		//为next按钮绑定onClick事件
		intent = new Intent(ActionDefinition.ACTION_APP_WIDGET_NEXT);
		intent.setComponent(serviceName);
		pIntent = PendingIntent.getService(context, 0, intent, 0);
		views.setOnClickPendingIntent(R.id.next, pIntent);
		
	}

上面是继承AppWidgetProvider 实现了一个AppWidgetProvider ,在onUpdate方法中绑定了两个按钮的事件行为。所有的行为似乎都是由RemoteView来完成的,但是RemoteView并不是真正显示在桌面上的你看到的View,它只是一个封装了一些操作和描述,在Widget和WidgetHost之间传递信息。

上面程序中,使用了一个Service,如下:

/**
 * 该Service负责更新App Widget;同时处理pre按钮和next按钮的onClick事件
 * @author liner
 *
 */
public class DemoService extends Service {

	private String[] contentDemos = new String[]{
			"Demo1",
			"Demo2",
			"Demo3",
			"Demo4"
	};
	
	private int currentDisplayItem = 0;
	
	public void onCreate(){
		super.onCreate();
		Log.v("DemoService", "onCreate execute");

	}
	
	public void onStart(Intent intent, int startId){
		super.onStart(intent, startId);
		Log.v("DemoService", "onStart execute");
		
		//这里获取Action信息,判断其是哪个行为
		String action = intent.getAction();
		
		if(action.equals(ActionDefinition.ACTION_APP_WIDGET_PREV)){
			
			doPrev();
			
		}else if(action.equals(ActionDefinition.ACTION_APP_WIDGET_NEXT)){
			
			doNext();
			
		}else{ //if(action.equals(ActionDefinition.ACTION_APP_WIDGET_SERVICE))
			notifyWidget();
		}
	}
	

	//通知更新
	private void notifyWidget() {
		ComponentName widget = new ComponentName(this, DemoAppWidgetProvider.class);
		AppWidgetManager manager = AppWidgetManager.getInstance(this);
		
		RemoteViews views = buildUpdateViews();
		
		manager.updateAppWidget(widget, views);
	}

	//每次更新的时候,都是创建一个RemoteView来完成更新的
	private RemoteViews buildUpdateViews() {
		RemoteViews views = new RemoteViews(this.getPackageName(), R.layout.widget_demo);
		
		views.setTextViewText(R.id.demo, contentDemos[currentDisplayItem]);
		
		return views;
	}

	private void doNext() {
		if(currentDisplayItem >= contentDemos.length -1){
			currentDisplayItem = 0;
		}else{
			currentDisplayItem++;
		}
		
		notifyWidget();
	}

	private void doPrev() {
		
		if(currentDisplayItem > 0){
			currentDisplayItem--;
		}else{
			currentDisplayItem = contentDemos.length-1;
		}
		
		notifyWidget();
	}

	@Override
	public IBinder onBind(Intent intent) {
		return null;
	}

}

当Widget被添加到桌面上的时候,onUpdate方法执行了过后,我们点击Widget上面的pre和next按钮,实际上都触发了Service,通过Service来完成更新操作的。

到这里,我们知道如何开发一个Widget了。下面我们再来看看如何开发一个可以容纳Widget的应用。效果如下:

说说Android桌面(Launcher应用)背后的故事(九)——让我的桌面多姿多彩

 

二、开发一个WidgetHost——让我的应用也可以海纳百川

为了开发一个WidgetHost,我们首先需要一个可以容纳各种Widget的布局控件,系统Launcher中使用的是CellLayout,那么我们也继承ViewGroup简单实现一个Layout。

 

public class WidgetLayout extends ViewGroup {
	
	private int[] cellInfo = new int[2];
	
	private OnLongClickListener mLongClickListener;
	
	public WidgetLayout(Context context){
		this(context, null);
	}
	
	public WidgetLayout(Context context, AttributeSet attrs){
		this(context, attrs, 0);
	}

	public WidgetLayout(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);

	}
	
	//@1:当长按的时候触发该动作,记录长按的位置
	public boolean dispatchTouchEvent(MotionEvent event){
		cellInfo[0] = (int)event.getX();
		cellInfo[1] = (int)event.getY();
		
		Log.e("event:", cellInfo[0]+","+cellInfo[1]);
		return super.dispatchTouchEvent(event);
	}
	
	//@2:当用户选择了某个widget时,触发这个动作,将其所选的widget(child)添加到桌面上
	public void addInScreen(View child, int width, int height){
		LayoutParams params = new LayoutParams(width, height);
		params.x = cellInfo[0];
		params.y = cellInfo[1];
		//params.width = width
		child.setOnLongClickListener(mLongClickListener);
		
		Log.e("size", "x,y,width,height"+params.x+","+params.y+","+params.width+","+params.height);
		
		addView(child, params);
	}
	
	//@3:测量每个孩子的宽度和高度
	public void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
		//super.onMeasure(widthMeasureSpec, heightMeasureSpec);
		
		final int count = getChildCount();
		LayoutParams lp = null;
		
		for(int i=0; i<count;i++){
			View child = getChildAt(i);
			lp = (LayoutParams)child.getLayoutParams();
			Log.e("onMeasure:w,h", lp.width+","+lp.height);
			child.measure(MeasureSpec.makeMeasureSpec(lp.width,MeasureSpec.EXACTLY), 
					MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY));
		}
		
		setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec));
	}

	//@4:将每个孩子按照其layoutparams中定义的进行布局
	@Override
	protected void onLayout(boolean changed, int l, int t, int r, int b) {
		final int count = getChildCount();
		LayoutParams lp = null;
		
		for(int i=0; i<count;i++){
			View child = getChildAt(i);
			lp = (LayoutParams)child.getLayoutParams();
			child.layout(lp.x, lp.y, lp.x+lp.width, lp.y+lp.height);
		}		
	}
	
	public static class LayoutParams extends ViewGroup.LayoutParams{
		
		int x;
		
		int y;
		
		public LayoutParams(int width, int height) {
			super(width, height);
			this.width = width;
			this.height = height;
		}
		
	}

}



有了可以容纳的地方,那么我们怎么实现呢?系统Launcher是,长按桌面的时候弹出一个Dialog,然后,里面有一项是:Add Widget。我们直接从这个开始,调用系统自带的Widget选择程序来选择。完整代码如下:

/**
 * 这里示例如何选择已安装的Widget,并且将选择的Widget添加到指定的位置
 * @author liner
 *
 */
public class AppWidgetHostDemoActivity extends Activity {
	private static final int APPWIDGET_HOST_ID = 0x200;
	
	private static final int REQUEST_ADD_WIDGET = 1;
	private static final int REQUEST_CREATE_WIDGET = 2;
	
	private AppWidgetHost mWidgetHost;
	
	private AppWidgetManager mWidgetMananger;
	
	private WidgetLayout mLayout;
	
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        mWidgetHost = new AppWidgetHost(getApplicationContext(), APPWIDGET_HOST_ID);
        mWidgetMananger = AppWidgetManager.getInstance(getApplicationContext());
        
        mLayout = new WidgetLayout(this);
        mLayout.setOnLongClickListener(new View.OnLongClickListener() {
			
			@Override
			public boolean onLongClick(View v) {
				
				selectWidgets();
				
				return false;
			}
		});
        
        setContentView(mLayout);
        
        //开始监听Widget的变化
        
        mWidgetHost.startListening();
    }
    
    public void onActivityResult(int requestCode, int resultCode, Intent data){
    	if(resultCode == RESULT_OK){
    		
    		switch (requestCode) {
			case REQUEST_ADD_WIDGET:
				addWidget(data);
				break;
			case REQUEST_CREATE_WIDGET:
				createWidget(data);
				break;
			default:
				break;
			}
    		
    	}else if(requestCode == REQUEST_CREATE_WIDGET && resultCode == RESULT_CANCELED && data != null){
    		int appWidgetId = data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1);
    		if(appWidgetId != -1){
    			mWidgetHost.deleteAppWidgetId(appWidgetId);
    		}
    	}
    }   

    private void createWidget(Intent data) {
    	//获取选择的widget的id
    	int appWidgetId = data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1);
    	
    	//获取所选的Widget的AppWidgetProviderInfo信息
    	AppWidgetProviderInfo appWidget = mWidgetMananger.getAppWidgetInfo(appWidgetId);
    	
    	//根据AppWidgetProviderInfo信息,创建HostView
    	View hostView = mWidgetHost.createView(this, appWidgetId, appWidget);
    	
    	//将HostView添加到桌面
    	mLayout.addInScreen(hostView, appWidget.minWidth, appWidget.minHeight);
	}

	/**
     * 添加选择的widget。需要判断其是否含有配置,如果有,需要首先进入配置
     * @param data
     */
    private void addWidget(Intent data) {
    	int appWidgetId = data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1);
    	AppWidgetProviderInfo appWidget = mWidgetMananger.getAppWidgetInfo(appWidgetId);
    	
    	Log.d("AppWidget", "configure:"+appWidget.configure);
    	
    	if(appWidget.configure != null){
    		//有配置,弹出配置
    		Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_CONFIGURE);
    		intent.setComponent(appWidget.configure);
    		intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
    		
    		startActivityForResult(intent, REQUEST_CREATE_WIDGET);
    		
    	}else{
    		//没有配置,直接添加
    		onActivityResult(REQUEST_CREATE_WIDGET, RESULT_OK, data);
    	}
		
    			
	}

	/**
     * 显示系统中已经存在的Widget信息,供用户选择
     */
	protected void selectWidgets() {
		int widgetId = mWidgetHost.allocateAppWidgetId();
		
		Intent pickIntent = new Intent(AppWidgetManager.ACTION_APPWIDGET_PICK);
		pickIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId);
		
		startActivityForResult(pickIntent, REQUEST_ADD_WIDGET);
	}
    
    
}

效果:

说说Android桌面(Launcher应用)背后的故事(九)——让我的桌面多姿多彩


到这里,应该可以理解了,要让我们的Launcher也可以容纳各式各样的Widget,我们至少需要加上上面类似的代码。但是,还有什么问题亟待解决呢?

1、CellLayout如何计算当前添加的Widget需要占据的单元格数目

2、CellLayout如何分配当前添加的Widget占据的单元格数目

3、CellLayout中已经没有足够的区域来容纳这么大小的Widget怎么办?

有了前面的代码,下面我们的任务就可以聚集在上面三个问题上了,解决了这三个问题,再加入之前的代码,就可以让我们的桌面也如系统桌面一样,可以容纳各式各样的小部件了。

下一篇就将解决这些问题...