Widget是安卓的一個桌面小工具元件—視窗小部件,是微型應用程式視圖,可以嵌入到其他應用程式(如主螢幕)和接收定期更新。
一 介紹
AppWidgetProvider是Android中提供的用于實作桌面小工具的類,其本質是一個廣播,即BroadcastReceiver。下面是類的繼承關系。
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsICdzFWRoRXdvN1LclHdpZXYyd2LcBzNvwVZ2x2bzNXak9CX90TQNNkRrFlQKBTSvwFbslmZvwFMwQzLcVmepNHdu9mZvwFVywUNMZTY18CX052bm9CX90TQPRTQE9UMjpWT4FEVkZXUYpVd1kmYr50MZV3YyI2cKJDT29GRjBjUIF2LcRHelR3LcJzLctmch1mclRXY39DNxMTMxYDNxEDOyITM2EDMy8CX0Vmbu4GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.jpg)
是以,在實際的使用中,把AppWidgetProvider當成一個BroadcastReceiver就可以了,這樣許多功能就很好了解了。
二 開發一個桌面小工具的步驟
- 建立一個類繼承 AppWidgetProvider 并重寫相應方法 預設實作了onReceive 方法。
- 在清單檔案進行注冊。
- 在res目錄下建立xml檔案夾 配置widget相關資訊。
- 建立widget展示布局。
- 建立widget的配置檔案(可選)。
- 更新資料。
大概就是上述的一些步驟。如果在添加的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);