Android widget development

1. Introduction to App widgets

App Widgets are application widgets (Widgets) are miniature application views that can be embedded in other applications (such as the desktop) and receive periodic updates. You can publish a Widget through an App Widget Provider.

 

2、AppWidgetProvider

AppWidgetProvider  inherits from BroadcastReceiver, which can receive widget-related broadcasts, such as widget update, delete, enable and disable. 

The broadcast handler function in AppWidgetProvider is as follows:

onUpdate()
  is executed when the widget is updated.
  Also, when the user first adds a widget, onUpdate() is called so that the widget can do the necessary setup work (if needed). However, if the configure attribute of the widget is defined (ie android:config, which will be described later), then when the user adds the widget for the first time, onUpdate() will not be called; then when the widget is updated, onUpdate will be called.

onAppWidgetOptionsChanged() executes onAppWidgetOptionsChanged()
  when the widget is first added or when the size of the widget is changed. In this function, you can show/hide something according to the size of the widget. You can use getAppWidgetOptions() to return the Bundle object to read the widget size information. The Bundle includes the following information: OPTION_APPWIDGET_MIN_WIDTH  -- Contains the lower limit of the current width of the widget, in dp. OPTION_APPWIDGET_MIN_HEIGHT  -- Contains the lower bound of the widget's current height, in dp. OPTION_APPWIDGET_MAX_WIDTH  -- Contains the upper limit of the widget's current width, in dp. OPTION_APPWIDGET_MAX_HEIGHT  -- Contains the upper limit of the widget's current height, in dp.
  
  
  
  

onAppWidgetOptionsChanged() was introduced in Android 4.1.

onDeleted(Context, int[])
  is fired when the widget is deleted.

onEnabled(Context) fires
  when the first widget instance is created. That is, if the user adds the same widget twice (two instances), then onEnabled() will only fire the first time the widget is added.

onDisabled(Context) fires
  when the last widget instance is removed.

onReceive(Context, Intent)
  接收到任意广播时触发,并且会在上述的方法之前被调用。

  总结,AppWidgetProvider 继承于 BroadcastReceiver。实际上,App Widge中的onUpdate()、onEnabled()、onDisabled()等方法都是在 onReceive()中调用的;是onReceive()对特定事情的响应函数

 

3、AppWidgetProvider 

AppWidgetProviderInfo描述一个App Widget元数据,比如App Widget的布局,更新频率,以及AppWidgetProvider 类。这个应该在XML里定义。下面以XML示例来对AppWidgetProviderInfo中常用的类型进行说明。

 

示例XML

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"

    android:minWidth="250dp"

    android:minHeight="150dp"

    android:minResizeWidth="250dp"

    android:minResizeHeight="150dp"

    android:updatePeriodMillis="0"

    android:previewImage="@drawable/appwidget_digital_clock_preview"

    android:initialLayout="@layout/new_digital_appwidget"

    android:resizeMode="vertical|horizontal"

    android:widgetCategory="keyguard|home_screen"

    >

 

</appwidget-provider>

示例说明
minWidth 和minHeight 
  它们指定了App Widget布局需要的最小区域。
  缺省的App Widgets所在窗口的桌面位置基于有确定高度和宽度的单元网格中。如果App Widget的最小长度或宽度和这些网格单元的尺寸不匹配,那么这个App Widget将上舍入(上舍入即取比该值大的最接近的整数——译者注)到最接近的单元尺寸。
  注意:app widget的最小尺寸,不建议比 “4x4” 个单元格要大。关于app widget的尺寸,后面在详细说明。

minResizeWidth 和 minResizeHeight 
  它们属性指定了 widget 的最小绝对尺寸。也就是说,如果 widget 小于该尺寸,便会因为变得模糊、看不清或不可用。 使用这两个属性,可以允许用户重新调整 widget 的大小,使 widget 的大小可以小于 minWidth 和 minHeight。
  注意:(01) 当 minResizeWidth 的值比 minWidth 大时,minResizeWidth 无效;当 resizeMode 的取值不包括 horizontal 时,minResizeWidth 无效。
           (02) 当 minResizeHeight 的值比 minHeight 大时,minResizeHeight 无效;当 resizeMode 的取值不包括 vertical 时,minResizeHeight 无效。

updatePeriodMillis 
  它定义了 widget 的更新频率。实际的更新时机不一定是精确的按照这个时间发生的。建议更新尽量不要太频繁,最好是低于1小时一次。 或者可以在配置 Activity 里面供用户对更新频率进行配置。 实际上,当updatePeriodMillis的值小于30分钟时,系统会自动将更新频率设为30分钟!关于这部分,后面会详细介绍。
  注意: 当更新时机到达时,如果设备正在休眠,那么设备将会被唤醒以执行更新。如果更新频率不超过1小时一次,那么对电池寿命应该不会造成多大的影响。 如果你需要比较频繁的更新,或者你不希望在设备休眠的时候执行更新,那么可以使用基于 alarm 的更新来替代 widget 自身的刷新机制。将 alarm 类型设置为 ELAPSED_REALTIME 或 RTC,将不会唤醒休眠的设备,同时请将 updatePeriodMillis 设为 0。

initialLayout 
  指向 widget 的布局资源文件

configure
  可选属性,定义了 widget 的配置 Activity。如果定义了该项,那么当 widget 创建时,会自动启动该 Activity。

previewImage
  指定预览图,该预览图在用户选择 widget 时出现,如果没有提供,则会显示应用的图标。该字段对应在 AndroidManifest.xml 中 receiver 的 android:previewImage 字段。由 Android 3.0 引入。

autoAdvanceViewId 
  指定一个子view ID,表明该子 view 会自动更新。在 Android 3.0 中引入。

resizeMode 
  指定了 widget 的调整尺寸的规则。可取的值有: "horizontal", "vertical", "none"。"horizontal"意味着widget可以水平拉伸,“vertical”意味着widget可以竖值拉伸,“none”意味着widget不能拉伸;默认值是"none"。Android 3.1 引入。

widgetCategory 
  指定了 widget 能显示的地方:能否显示在 home Screen 或 lock screen 或 两者都可以。它的取值包括:"home_screen" 和 "keyguard"。Android 4.2 引入。

initialKeyguardLayout 
  指向 widget 位于 lockscreen 中的布局资源文件。Android 4.2 引入。

 

4、APP widget布局说明

4.1、添加到lock srceen中

默认情况下(即不设置android:widgetCategory属性),Android是将widget添加到 home screen 中。
  但在Android 4.2中,若用户希望 widget 可以被添加到lock screen中,可以通过设置 widget 的 android:widgetCategory 属性包含keyguard来完成。

  当你把 widget 添加到lock screen中时,你可能对它进行定制化操作,以区别于添加到home screen中的情况。 你能够通过 getAppWidgetOptions() 来进行判断 widget 是被添加到lock screen中,还是home screen中。通过 getApplicationOptions() 获取 Bundle对象,然后读取 Bundle 的OPTION_APPWIDGET_HOST_CATEGORY值:若值为 WIDGET_CATEGORY_HOME_SCREEN, 则表示该 widget 被添加到home screen中; 若值为 WIDGET_CATEGORY_KEYGUARD,则表示该 widget 被添加到lock screen中。

 

  另外,你应该为添加到lock screen中的 widget 单独使用一个layout,可以通过 android:initialKeyguardLayout 来指定。而 widget 添加到home screen中的layout则可以通过 android:initialLayout 来指定。

 

4.2、支持的布局控件

Widget并不支持所有的布局和控件,而仅仅只是支持Android布局和控件的一个子集。
(01) App Widget支持的布局:
  FrameLayout
  LinearLayout
  RelativeLayout
  GridLayout
(02) App Widget支持的控件:
  AnalogClock
  Button
  Chronometer
  ImageButton
  ImageView
  ProgressBar
  TextView
  ViewFlipper
  ListView
  GridView
  StackView
  AdapterViewFlipper

 

5、示例

1、Androidmanifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.skywang.widget"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="14"
        android:targetSdkVersion="17" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        
        <!-- 声明widget对应的AppWidgetProvider -->
        <receiver android:name=".ExampleAppWidgetProvider" >
            <intent-filter>
                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
                <action android:name="com.skywang.widget.UPDATE_ALL"/>
            </intent-filter>
            <meta-data android:name="android.appwidget.provider"
                android:resource="@xml/example_appwidget_info" />
        </receiver>
        
        <service android:name=".ExampleAppWidgetService" >
            <intent-filter>
                <action android:name="android.appwidget.action.EXAMPLE_APP_WIDGET_SERVICE" />
            </intent-filter>
        </service>
        
    </application>

</manifest>

 说明

(01) ExampleAppWidgetProvider是继承于的AppWidgetProvider类,用来响应widget的添加、删除、更新等操作。
(02) android.appwidget.action.APPWIDGET_UPDATE,必须要显示声明的action!因为所有的widget的广播都是通过它来发送的;要接收widget的添加、删除等广播,就必须包含它。
(03) action android:name="com.skywang.widget.UPDATE_ALL,这是我自己添加了,是为了接收服务所发送的更新图片的广播。
(04) <meta-data> 指定了 AppWidgetProviderInfo 对应的资源文件
    android:name -- 指定metadata名,通过android.appwidget.provider来辨别data。
    android:resource -- 指定 AppWidgetProviderInfo 对应的资源路径。即,xml/example_appwidget_info.xml。
(05) ExampleAppWidgetService 是用于更新widget中的图片的服务。
(06) android.appwidget.action.EXAMPLE_APP_WIDGET_SERVICE 用于启动服务的action。

 

2、编辑AppWidgetProviderInfo 对应的资源文件

在当前工程下新建xml目录(若xml目录不存在的话);并在xml目录下新建example_appwidget_info.xml文件

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:minWidth="180dp"
    android:minHeight="180dp"
    android:previewImage="@drawable/preview"
    android:initialLayout="@layout/example_appwidget"
    android:resizeMode="horizontal|vertical"
    android:widgetCategory="home_screen|keyguard">
    
    <!--
    android:minWidth : 最小宽度
    android:minHeight : 最小高度
    android:updatePeriodMillis : 更新widget的时间间隔(ms),"86400000"为1个小时
    android:previewImage : 预览图片
    android:initialLayout : 加载到桌面时对应的布局文件
    android:resizeMode : widget可以被拉伸的方向。horizontal表示可以水平拉伸,vertical表示可以竖直拉伸
    android:widgetCategory : widget可以被显示的位置。home_screen表示可以将widget添加到桌面,keyguard表示widget可以被添加到锁屏界面。
    android:initialKeyguardLayout : 加载到锁屏界面时对应的布局文件
     -->
    
</appwidget-provider>

 说明:

(01) android:previewImage,用于指定预览图片。即搜索到widget时,查看到的图片。若没有设置的话,系统为指定一张默认图片。
(02) android:updatePeriodMillis 更新widget的时间间隔(ms)。

 

3、编辑widget布局文件

新建layout/example_appwidget.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="match_parent"
    android:orientation="vertical" >
  
    <LinearLayout 
        android:layout_width="wrap_content"  
        android:layout_height="wrap_content"
        android:layout_gravity="center" 
        android:orientation="horizontal" >
        
        <TextView  
            android:layout_width="wrap_content"  
            android:layout_height="wrap_content"  
            android:text="HomeScreen Widget" />    
        
        <Button
            android:id="@+id/btn_show"
            android:layout_width="wrap_content"  
            android:layout_height="wrap_content"  
            android:text="Show" />
    </LinearLayout> 
        
    <ImageView
        android:id="@+id/iv_show"
        android:layout_width="wrap_content"  
        android:layout_height="wrap_content" 
        android:layout_gravity="center"/> 
        
</LinearLayout>

 4、编辑ExampleAppWidgetProvider.java

public class ExampleAppWidgetProvider extends AppWidgetProvider {
    private static final String TAG = "ExampleAppWidgetProvider";

    private boolean DEBUG = false; 
    // 启动ExampleAppWidgetService服务对应的action
    private final Intent EXAMPLE_SERVICE_INTENT = 
            new Intent("android.appwidget.action.EXAMPLE_APP_WIDGET_SERVICE");
    // 更新 widget 的广播对应的action
    private final String ACTION_UPDATE_ALL = "com.skywang.widget.UPDATE_ALL";
    // 保存 widget 的id的HashSet,每新建一个 widget 都会为该 widget 分配一个 id。
    private static Set idsSet = new HashSet();
    // 按钮信息
    private static final int BUTTON_SHOW = 1;
    // 图片数组
    private static final int[] ARR_IMAGES = { 
        R.drawable.sample_0, 
        R.drawable.sample_1, 
        R.drawable.sample_2, 
        R.drawable.sample_3, 
        R.drawable.sample_4, 
        R.drawable.sample_5,
        R.drawable.sample_6,
        R.drawable.sample_7,
    };
    
    // onUpdate() 在更新 widget 时,被执行,
    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        Log.d(TAG, "onUpdate(): appWidgetIds.length="+appWidgetIds.length);

        // 每次 widget 被创建时,对应的将widget的id添加到set中
        for (int appWidgetId : appWidgetIds) {
            idsSet.add(Integer.valueOf(appWidgetId));
        }
        prtSet();
    }
    
    // 当 widget 被初次添加 或者 当 widget 的大小被改变时,被调用 
    @Override  
    public void onAppWidgetOptionsChanged(Context context,  
            AppWidgetManager appWidgetManager, int appWidgetId,  
            Bundle newOptions) {
        Log.d(TAG, "onAppWidgetOptionsChanged");
        super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId,  
                newOptions);  
    }  
    
    // widget被删除时调用  
    @Override  
    public void onDeleted(Context context, int[] appWidgetIds) {  
        Log.d(TAG, "onDeleted(): appWidgetIds.length="+appWidgetIds.length);

        // 当 widget 被删除时,对应的删除set中保存的widget的id
        for (int appWidgetId : appWidgetIds) {
            idsSet.remove(Integer.valueOf(appWidgetId));
        }
        prtSet();
        
        super.onDeleted(context, appWidgetIds);  
    }

    // 第一个widget被创建时调用  
    @Override  
    public void onEnabled(Context context) {  
        Log.d(TAG, "onEnabled");
        // 在第一个 widget 被创建时,开启服务
        context.startService(EXAMPLE_SERVICE_INTENT);
        
        super.onEnabled(context);  
    }  
    
    // 最后一个widget被删除时调用  
    @Override  
    public void onDisabled(Context context) {  
        Log.d(TAG, "onDisabled");

        // 在最后一个 widget 被删除时,终止服务
        context.stopService(EXAMPLE_SERVICE_INTENT);

        super.onDisabled(context);  
    }
    
    
    // 接收广播的回调函数
    @Override  
    public void onReceive(Context context, Intent intent) {  

        final String action = intent.getAction();
        Log.d(TAG, "OnReceive:Action: " + action);
        if (ACTION_UPDATE_ALL.equals(action)) {
            // “更新”广播
            updateAllAppWidgets(context, AppWidgetManager.getInstance(context), idsSet);
        } else if (intent.hasCategory(Intent.CATEGORY_ALTERNATIVE)) {
            // “按钮点击”广播
            Uri data = intent.getData();
            int buttonId = Integer.parseInt(data.getSchemeSpecificPart());
            if (buttonId == BUTTON_SHOW) {
                Log.d(TAG, "Button wifi clicked");
                Toast.makeText(context, "Button Clicked", Toast.LENGTH_SHORT).show();
            }
        }
        
        super.onReceive(context, intent);  
    }  

    // 更新所有的 widget 
    private void updateAllAppWidgets(Context context, AppWidgetManager appWidgetManager, Set set) {

        Log.d(TAG, "updateAllAppWidgets(): size="+set.size());
        
        // widget 的id
        int appID;
        // 迭代器,用于遍历所有保存的widget的id
        Iterator it = set.iterator();

        while (it.hasNext()) {
            appID = ((Integer)it.next()).intValue();    
            // 随机获取一张图片
            int index = (new java.util.Random().nextInt(ARR_IMAGES.length));
            
            if (DEBUG) Log.d(TAG, "onUpdate(): index="+index);            
            // 获取 example_appwidget.xml 对应的RemoteViews            
            RemoteViews remoteView = new RemoteViews(context.getPackageName(), R.layout.example_appwidget);
            
            // 设置显示图片
            remoteView.setImageViewResource(R.id.iv_show, ARR_IMAGES[index]);
            
            // 设置点击按钮对应的PendingIntent:即点击按钮时,发送广播。
            remoteView.setOnClickPendingIntent(R.id.btn_show, getPendingIntent(context,
                    BUTTON_SHOW));

            // 更新 widget
            appWidgetManager.updateAppWidget(appID, remoteView);        
        }        
    }

    private PendingIntent getPendingIntent(Context context, int buttonId) {
        Intent intent = new Intent();
        intent.setClass(context, ExampleAppWidgetProvider.class);
        intent.addCategory(Intent.CATEGORY_ALTERNATIVE);
        intent.setData(Uri.parse("custom:" + buttonId));
        PendingIntent pi = PendingIntent.getBroadcast(context, 0, intent, 0 );
        return pi;
    }

    // 调试用:遍历set
    private void prtSet() {
        if (DEBUG) {
            int index = 0;
            int size = idsSet.size();
            Iterator it = idsSet.iterator();
            Log.d(TAG, "total:"+size);
            while (it.hasNext()) {
                Log.d(TAG, index + " -- " + ((Integer)it.next()).intValue());
            }
        }
    }
}

 说明

(01) 当我们创建第一个widget到桌面时,会执行onEnabled()。在onEnabled()中通过 context.startService(EXAMPLE_SERVICE_INTENT) 启动服务ExampleAppWidgetService。服务的作用就是每隔5秒发送一个ACTION_UPDATE_ALL广播给我们,用于更新widget中的图片。
       仅仅当我们创建第一个widget时才会启动服务,因为onEnabled()只会在第一个widget被创建时才执行。
(02) 当我们删除最后一个widget到桌面时,会执行onDisabled()。在onDisabled()中通过 context.stopService(EXAMPLE_SERVICE_INTENT) 终止服务ExampleAppWidgetService。
       仅仅当我们删除最后一个widget时才会终止服务,因为onDisabled()只会在最后一个widget被删除时才执行。
(03) 本工程中,每添加一个widget都会执行onUpdate()。例外情况:在定义android:configure的前提下,第一次添加widget时不会执行onUpdate(),而是执行android:configure中定义的activity。
(04) onReceive()中,处理两个广播:更新桌面的widget 以及 响应按钮点击广播。
       当收到ACTION_UPDATE_ALL广播时,调用updateAllAppWidgets()来更新所有的widget。 
       当收到的广播的categery为Intent.CATEGORY_ALTERNATIVE,并且scheme为BUTTON_SHOW时,对应是按钮点击事件。按钮的监听是在updateAllAppWidgets()中注册的。

 

参考:http://blog.csdn.net/sasoritattoo/article/details/17616597

 
精彩科技工作室

Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=326777121&siteId=291194637