天天看點

Android例子—Widget的簡單使用介紹一 介紹二 開發一個桌面小工具的步驟

Widget是安卓的一個桌面小工具元件—視窗小部件,是微型應用程式視圖,可以嵌入到其他應用程式(如主螢幕)和接收定期更新。

一 介紹

AppWidgetProvider是Android中提供的用于實作桌面小工具的類,其本質是一個廣播,即BroadcastReceiver。下面是類的繼承關系。

Android例子—Widget的簡單使用介紹一 介紹二 開發一個桌面小工具的步驟

是以,在實際的使用中,把AppWidgetProvider當成一個BroadcastReceiver就可以了,這樣許多功能就很好了解了。

二 開發一個桌面小工具的步驟

  1. 建立一個類繼承 AppWidgetProvider 并重寫相應方法 預設實作了onReceive 方法。
  2. 在清單檔案進行注冊。
  3. 在res目錄下建立xml檔案夾 配置widget相關資訊。
  4. 建立widget展示布局。
  5. 建立widget的配置檔案(可選)。
  6. 更新資料。

大概就是上述的一些步驟。如果在添加的widget之前需要進行一些配置 則需要實作第5步。

第一步:建立類繼承AppWidgetProvider

public class MyWidget extends AppWidgetProvider {

    @Override
    public void onReceive(Context context, Intent intent) {
        super.onReceive(context, intent);
    }

    /**
     * 第一個widget被添加調用
     * @param context
     */
    @Override
    public void onEnabled(Context context) {
        super.onEnabled(context);

        context.startService(new Intent(context, WidgetService.class));

    }

    /**
     * widget被添加 || 更新時調用
     * @param context
     */
    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        super.onUpdate(context, appWidgetManager, appWidgetIds);
        context.startService(new Intent(context, WidgetService.class));
    }

    /**
     * 最後一個widget被删除時調用
     * @param context
     */
    @Override
    public void onDisabled(Context context) {
        super.onDisabled(context);
        context.stopService(new Intent(context, WidgetService.class));
    }

    /**
     * widget被删除時調用
     * @param context
     * @param appWidgetIds
     */
    @Override
    public void onDeleted(Context context, int[] appWidgetIds) {
        super.onDeleted(context, appWidgetIds);
    }

}
           

第二步:清單檔案注冊

<receiver android:name=".MyWidget">

            <intent-filter>
                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
            </intent-filter>

            <meta-data android:name="android.appwidget.provider"
                android:resource="@xml/example_appwidget_info" /> //widget的配置資訊檔案

        </receiver>
           

第三步:建立配置資訊 目錄 res/xml/xxx.xml

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:minWidth="40dp" //widget的最小寬度
    android:minHeight="40dp"//widget的最小高度
    android:updatePeriodMillis="86400000"//更新的頻率  google規定最小時間為30分鐘
    android:previewImage="@drawable/preview" //widget的預覽圖 即在添加是看到的圖檔 一般為logo
    android:initialLayout="@layout/example_appwidget" // widget的布局檔案
    android:configure="com.example.android.ExampleAppWidgetConfigure" //widget配置activity
    android:resizeMode="horizontal|vertical" //widget是否可以水準 豎直縮放 
    android:widgetCategory="home_screen|keyguard"> //widget可以被添加主螢幕或者鎖屏
</appwidget-provider>
           

以上這些資訊并不是必須 是可以選擇的。updatePeriodMillis的時間最小限定在了30分鐘,對于有些app來說可能時間太久了,那麼我們可以将updatePeriodMillis的時間設定為0,然後通過自定義service去更新widget。

Demo中的配置資訊如下:

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:initialLayout="@layout/example_appwidget"
    android:minHeight="80dp"
    android:minWidth="250dp"
    android:previewImage="@mipmap/ic_launcher"
    android:updatePeriodMillis="0"
    android:widgetCategory="home_screen|keyguard">

</appwidget-provider>
           

第四步:建立widget的展示布局,這個就根據設計,自己去碼布局了。

Demo中布局檔案内容如下:

<?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="horizontal">

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@mipmap/bg_icon"
        android:gravity="center_vertical"
        android:orientation="horizontal"
        android:paddingLeft="30dp"
        android:paddingRight="30dp">

        <LinearLayout
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:orientation="vertical">

            <TextView
                android:id="@+id/tv_date"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textColor="#000"
                android:textSize="16sp" />

            <TextView
                android:id="@+id/tv_money"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textColor="#000"
                android:textSize="16sp" />

        </LinearLayout>

        <Button
            android:id="@+id/btn_refound"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@null"
            android:gravity="right|center"
            android:text="點選" 
            android:textColor="#000"
            android:textSize="16sp" />

    </LinearLayout>

</LinearLayout>
           

就是簡單的來個TextView來顯示文案。

Demo中不需要對在添加widget進行配置是以不需要第5步。

這樣簡單的widget使用就完成了。

最後就是更新widget資料了,為了滿足自定義時間可以對widget進行更新,我們采取使用service的方式進行。同樣,建立類繼承Service。

Demo中的service代碼如下:

public class WidgetService extends Service {

    private Timer mTimer;
    private SimpleDateFormat mFormat;

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

    @Override
    public void onCreate() {
        super.onCreate();

        mFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

        mTimer = new Timer();
        mTimer.schedule(new TimerTask() {
            @Override
            public void run() {
                updateWidget(WidgetService.this);
            }
        }, , ); 

    }

    private void updateWidget(Context context) {

        RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.example_appwidget);
        long millis = System.currentTimeMillis();
        String format = mFormat.format(new Date(millis));
        remoteViews.setTextViewText(R.id.tv_date, "日  期:" + format);
        remoteViews.setTextViewText(R.id.tv_money, "毫秒值:" + millis);

        PendingIntent pendingIntent = PendingIntent.getActivity(context, , new Intent(this, MainActivity.class), PendingIntent.FLAG_UPDATE_CURRENT);
        remoteViews.setOnClickPendingIntent(R.id.btn_refound, pendingIntent);

        ComponentName componentName = new ComponentName(this, MyWidget.class);
        AppWidgetManager.getInstance(this).updateAppWidget(componentName, remoteViews);

    }


    @Override
    public void onDestroy() {
        super.onDestroy();

        mTimer.cancel();
        mTimer = null;
<span style="white-space:pre">  </span>stopForeground(true);

    }

}
           

在service的oncreate方法中建立一個Timer定時器,這個定時器每個5秒鐘就會執行updateWidget方法,在updateWidget方法裡面實作了具體的更新widget的邏輯。通過RemoteViews去加載布局檔案 在通過setTextViewText等方法實作對控件的控制。

這個地方使用了PendingIntent ,PendingIntent 其實和Intent效果是一樣的,都是充當信使的作用,但是差別就是 PendingIntent 定義的事件是提前預知的,就是不知道事件什麼時候發生,但是隻要發生了,就要執行PendingIntent 定義的邏輯。

最後通過AppWidgetManager.getInstance(this).updateAppWidget(componentName, remoteViews);去更新widget元件的資訊。

這樣看,大概一個widget的功能就已經完成了,建立了,資料也可以更新了,但是有一個問題就是,我們使用的是service去更新widget,那麼我們建立的service是一個背景程序,背景程序的優先級比較低,當手機記憶體不足的時候 就會優先kill掉這些程序。這個時候,widget在桌面上就不會得到更新了。那麼怎麼解決呢?

這裡的思路是提升service程序的優先級,将其提升為前台程序,這樣就可以最大程度的保證service不會被系統kill掉了。

在service的oncreate方法中建立一個通知,這樣就将此service由背景程序變為前台程序了。代碼如下:

@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
    private void improvePriority() {
        PendingIntent contentIntent = PendingIntent.getActivity(this, , new Intent(this, WidgetService.class), );
        Notification notification = new Notification.Builder(this)
                .setContentTitle("Foreground Service")
                .setContentText("Foreground Service Started.")
                .setSmallIcon(R.mipmap.ic_launcher).build();
        notification.contentIntent = contentIntent;
        startForeground(, notification); 
    }
           

在oncreate方法中調用此方法即可。 Demo源碼中startForeground(1, notification); 第一個參數寫的是0 這個是不行的 谷歌文檔提示 must not be 0 , 寫0之後就not working了。但是這樣,當SDK<18時,通知欄不會顯示該通知,當SDK>=18時,通知欄就會顯示該通知 了。關于這個問題請移步到消除前台程序通知 本人未做實踐,不過的确是一種思路。

這樣基本就完成了widget的建立了。這個時候可能又會問了,萬一此時service還是被kill了怎麼辦呢?

一種方式是我們重寫service的onStartCommand方法 并傳回START_STICKY,這樣如果是系統自動将service kill了,在一段時間後 在記憶體比較充裕的心情下 系統會自動啟動這個service的。

@Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        return START_STICKY;
    }
           

另外一種方式就是通過廣播來監聽此service,安卓系統提供了一個時間改變的廣播接受者— ACTION_TIME_TICK 每隔一分鐘 就會發送一個廣播。由于此廣播是系統級别的廣播,是以不可以通過清單檔案配置監聽,隻能通過代碼動态的建立監聽。

建立一個類繼承BroadcastReceiver 并且重寫onReceive 方法 ,這樣每隔一分鐘 onReceive方法就會執行一次,在onReceive方法中判斷service是否已經在運作,如果沒有在運作就去開啟service,否則就上面操作都不需要做。

public class WidgetBroadcastReceiver extends BroadcastReceiver {

    public static final String SERVICE_NAME = "com.ppdai.widgetdemo.WidgetService";

    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        if (action.equals(Intent.ACTION_TIME_TICK)) {
            if (!isServiceWork(context, SERVICE_NAME)) {
                context.startService(new Intent(context, WidgetService.class));
            }
        }
    }

    /**
     * 判斷service是否在運作
     * @param mContext
     * @param serviceName
     * @return
     */
    public boolean isServiceWork(Context mContext, String serviceName) {
        boolean isWork = false;
        ActivityManager myAM = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
        List<ActivityManager.RunningServiceInfo> myList = myAM.getRunningServices();
        if (myList.size() <= ) {
            return false;
        }
        for (int i = ; i < myList.size(); i++) {
            String mName = myList.get(i).service.getClassName().toString();
            if (mName.equals(serviceName)) {
                isWork = true;
                break;
            }
        }
        return isWork;
    }
}
           

然後在需要的地方 動态的注冊此廣播:

WidgetBroadcastReceiver mReceiver = new WidgetBroadcastReceiver();
        IntentFilter filter = new IntentFilter();
        filter.addAction(Intent.ACTION_TIME_TICK);
        registerReceiver(mReceiver, filter);