關(guān)于Android桌面小部件的官方教程當(dāng)然就是Android開發(fā)者文檔,這里以一個火影迷感興趣的圖騰設(shè)計(jì)一款桌面時(shí)鐘,拋磚引玉。
效果圖
效果圖|from Google Nexus 6P with Screen Size 1440x2560
準(zhǔn)備素材
小部件預(yù)覽圖
素材-預(yù)覽圖|720x720
widget_bg.png
素材-鐘面|720x720
widget_hour_00.png
素材-時(shí)針|720x720
widget_min_00.png
素材-分針|720x720
widget_sec_00.png
素材-秒針|720x720
四張切圖使用AI繪制導(dǎo)出,規(guī)格均為720X720,放置于Android工程res/drawable-xxxhdpi目錄下,這樣時(shí)鐘大小較合適,原因參見表一及Android Screen Matching。
表一
name | icon size | scope | 代表屏幕 | scale |
---|---|---|---|---|
ldpi | 36x36 | 0~120dpi | 現(xiàn)今鮮有設(shè)備 | 0.75 |
mdpi | 48x48 | 120~160dpi | 320x480 | 1 |
hddpi | 72x72 | 160~240dpi | 480x800 | 1.5 |
xhdpi | 96x96 | 240~320dpi | 720x1280 | 2 |
xxhdpi | 144x144 | 320~480dpi | 1080x1920 | 3 |
xxxhdpi | 192x192 | 480~640dpi | 1440x2560 | 4 |
編寫時(shí)鐘布局 app_widget_clock.xml
注意,App Widget使用的是RemoteViews,僅支持有限的內(nèi)置控件,自定義控件一律不支持,并且對控件的操作均要通過RemoteViews的有限方法來執(zhí)行,接下來我們會用到RemoteViews的setImageViewBitmap方法,留意下面的代碼。
<?xml version="1.0" encoding="utf-8"?>
<!--Widget支持的控件-->
<!--FrameLayout-->
<!--LinearLayout-->
<!--RelativeLayout-->
<!--GridLayout-->
<!--AnalogClock-->
<!--Button-->
<!--Chronometer-->
<!--ImageButton-->
<!--ImageView-->
<!--ProgressBar-->
<!--TextView-->
<!--ViewFlipper-->
<!--ListView-->
<!--GridView-->
<!--StackView-->
<!--AdapterViewFlipper-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="8dp"
>
<!--表盤-->
<ImageView
android:id="@+id/background"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@drawable/widget_bg"
/>
<!--秒針-->
<ImageView
android:id="@+id/time_s"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@drawable/widget_sec_00"
/>
<!--分針-->
<ImageView
android:id="@+id/time_m"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@drawable/widget_min_00"
/>
<!--時(shí)針-->
<ImageView
android:id="@+id/time_h"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@drawable/widget_hour_00"
/>
</FrameLayout>
編寫AppWidgetProvider
編寫ClockAppWidgetProvider.java
public class ClockAppWidgetProvider extends AppWidgetProvider {
public ClockAppWidgetProvider() {
super();
}
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
super.onUpdate(context, appWidgetManager, appWidgetIds);
// 調(diào)用的間隔由res/xml/app_widget_info_clock.xml下的updatePeriodMillis決定
// 下面的for循環(huán)是update app widgets的標(biāo)準(zhǔn)寫法
// N是桌面上該小部件的數(shù)目
final int N = appWidgetIds.length;
for (int i = 0; i < N; i++) {
// 對每一個小部件進(jìn)行更新
int appWidgetId = appWidgetIds[i];
RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.app_widget_clock);
// TODO 對remoteViews進(jìn)行操作,比如添加點(diǎn)擊事件跳轉(zhuǎn)系統(tǒng)時(shí)鐘
appWidgetManager.updateAppWidget(appWidgetId, remoteViews);
}
// TODO 啟動ClockService
}
@Override public void onDeleted(Context context, int[] appWidgetIds) {
super.onDeleted(context, appWidgetIds);
// TODO 任意一個小部件被移除時(shí)調(diào)用
}
@Override public void onEnabled(Context context) {
}
@Override public void onDisabled(Context context) {
// 所有桌面小部件被移除時(shí)調(diào)用
// TODO 注銷ClockService
}
}
編寫res/xml/app_widget_info_clock.xml
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/app_widget_clock"
android:minHeight="250dp"
android:minWidth="250dp"
android:previewImage="@drawable/widget_clock_preview"
android:resizeMode="horizontal|vertical"
android:updatePeriodMillis="86400000"
>
</appwidget-provider>
- initialLayout初始化布局
- minHeight & minWidth的設(shè)置參見小部件設(shè)計(jì)
// cellCountInRowOrColumn - 小部件的行數(shù)或列數(shù)
valueInDP = 70 × cellCountInRowOrColumn ? 30
- previewImage是小部件的預(yù)覽圖,長按桌面查看小部件列表時(shí)顯示的那個icon就是它
- resizeMode可伸縮的方向
- updatePeriodMillis小部件的刷新間隔,單位是秒,默認(rèn)是一天
Manifest的聲明
<receiver android:name=".ui.widget.clock.ClockAppWidgetProvider">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/app_widget_info_clock"/>
</receiver>
編寫ClockService
ClockService用于接收系統(tǒng)時(shí)間,并根據(jù)時(shí)間的時(shí)分秒值轉(zhuǎn)動我們的時(shí)針、分針和秒針。
// 時(shí)分秒針角度推導(dǎo)
// 假設(shè)rawS、rawM、rawH分別為秒、分、時(shí)(12小時(shí)制)的數(shù)值
// angleS、angleM、angleH分別為秒、分、時(shí)針的轉(zhuǎn)動角度
angleS = 360 / 60 × rawS
= 360 / 60 × realS
其中,令 realS = rawS
angleM = 360 / 60 × rawM + 360 / 3600 × rawS
= 360 / 60 × (rawM + rawS / 60)
= 360 / 60 × (rawM + realS / 60)
= 360 / 60 × realM
其中,令 realM = rawM + realS / 60
angleH = 360 / 12 × rawH + 360 / 12 / 3600 × (60 × rawM + rawS)
= 360 / 12 × (rawH + rawM / 60 + rawS / 3600)
= 360 / 12 × (rawH + (rawM + rawS / 60) / 60)
= 360 / 12 × (rawH + realM / 60)
= 360 / 12 × realH
其中,令 realH = rawH + realM / 60
編寫時(shí)鐘任務(wù)
private final class MyTimerTask extends TimerTask {
@Override public void run() {
// 獲取Widgets管理器
AppWidgetManager widgetManager = AppWidgetManager.getInstance(getApplicationContext());
// widgetManager所操作的Widget對應(yīng)的遠(yuǎn)程視圖即當(dāng)前Widget的layout文件
RemoteViews remoteView = new RemoteViews(getPackageName(), R.layout.app_widget_clock);
// 見公式推導(dǎo)
Calendar calendar = Calendar.getInstance();
int rawS = calendar.get(Calendar.SECOND);
int rawM = calendar.get(Calendar.MINUTE);
int rawH = calendar.get(Calendar.HOUR);
float realS = rawS;
float realM = rawM + realS / 60.0f;
float realH = rawH + realM / 60.0f;
// 計(jì)算時(shí)分秒針的角度
float rotateS = 360f / 60f * realS;
float rotateM = 360f / 60f * realM;
float rotateH = 360f / 12f * realH;
// 根據(jù)角度轉(zhuǎn)動時(shí)分秒針
if (null == mHandS || mHandS.isRecycled()) {
mHandS = BitmapFactory.decodeResource(getApplicationContext().getResources(), R.drawable.widget_sec_00);
}
if (null == mHandM || mHandM.isRecycled()) {
mHandM = BitmapFactory.decodeResource(getApplicationContext().getResources(), R.drawable.widget_min_00);
}
if (null == mHandH || mHandH.isRecycled()) {
mHandH = BitmapFactory.decodeResource(getApplicationContext().getResources(), R.drawable.widget_hour_00);
}
// RemoteViews的內(nèi)置方法操作控件
// 對內(nèi)置控件的操作,RemoteViews僅提供有限的幾個方法,這里我們用到其中一個:setImageViewBitmap
remoteView.setImageViewBitmap(R.id.time_s, rotateBitmap(mHandS, rotateS));
remoteView.setImageViewBitmap(R.id.time_m, rotateBitmap(mHandM, rotateM));
remoteView.setImageViewBitmap(R.id.time_h, rotateBitmap(mHandH, rotateH));
// 當(dāng)點(diǎn)擊Widgets時(shí)觸發(fā)的事件
ComponentName componentName = new ComponentName(getApplicationContext(), ClockAppWidgetProvider.class);
widgetManager.updateAppWidget(componentName, remoteView);
}
}
Bitmap旋轉(zhuǎn)
/**
* 旋轉(zhuǎn)
*
* @param source
* @param degree from 0f to 360f
* @return
*/
private Bitmap rotateBitmap(Bitmap source, float degree) {
if (null == source) {
return null;
}
int size = source.getWidth();
Matrix matrix = new Matrix();
matrix.reset();
matrix.setRotate(degree, size / 2, size / 2);
return Bitmap.createBitmap(source, 0, 0, size, size, matrix, true);
}
ClockService啟動MyTimerTask
public class ClockService extends Service {
private Timer mTimer;
@Override public void onCreate() {
super.onCreate();
mTimer = new Timer();
// 1000ms執(zhí)行一次
mTimer.schedule(new MyTimerTask(), 0, 1000);
}
// TODO 其它生命周期方法
}
記得在Manifest.xml里聲明ClockService
<service android:name=".ui.widget.clock.ClockService"/>