Android桌面小部件開發(fā)——月之眼時(shí)鐘

關(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>
// 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"/>

源碼參考

Github源碼

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,501評論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,673評論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,610評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,939評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,668評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,004評論 1 329
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,001評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,173評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,705評論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,426評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,656評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,139評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,833評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,247評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,580評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,371評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,621評論 2 380

推薦閱讀更多精彩內(nèi)容