Android桌面小部件开发——月之眼时钟
g347068j
8年前
<p>关于Android桌面小部件的官方教程当然就是 <a href="/misc/goto?guid=4959716184790844775" rel="nofollow,noindex">Android开发者文档</a> ,这里以一个火影迷感兴趣的图腾设计一款桌面时钟,抛砖引玉。</p> <h2><strong>效果图</strong></h2> <p style="text-align:center"><img src="https://simg.open-open.com/show/3162547734dc2982f19c364db249b2b5.gif"></p> <p style="text-align:center">效果图|from Google Nexus 6P with Screen Size 1440x2560</p> <h2><strong>准备素材</strong></h2> <h3><strong>小部件预览图</strong></h3> <p><img src="https://simg.open-open.com/show/902f5cf7648ea30c58908c53a04babc0.png"></p> <p style="text-align:center">素材-预览图|720x720</p> <h3><strong>widget_bg.png</strong></h3> <p><img src="https://simg.open-open.com/show/4842bb28db05b8bf8a2b43f5630c5110.png"></p> <p style="text-align:center">素材-钟面|720x720</p> <h3><strong>widget_hour_00.png</strong></h3> <p><img src="https://simg.open-open.com/show/a660e2a77864ac79700114f20ea68164.png"></p> <p style="text-align:center">素材-时针|720x720</p> <h3><strong>widget_min_00.png</strong></h3> <p><img src="https://simg.open-open.com/show/633595170bb9bba79c151caa7871d22f.png"></p> <p style="text-align:center">素材-分针|720x720</p> <h3><strong>widget_sec_00.png</strong></h3> <p><img src="https://simg.open-open.com/show/da99ae8f4535aedde737468426ed261f.png"></p> <p style="text-align:center">素材-秒针|720x720</p> <p>四张切图使用AI绘制导出,规格均为720X720,放置于Android工程res/drawable-xxxhdpi目录下,这样时钟大小较合适。</p> <p>表一</p> <table> <thead> <tr> <th>name</th> <th>icon size</th> <th>scope</th> <th>代表屏幕</th> <th>scale</th> </tr> </thead> <tbody> <tr> <td>ldpi</td> <td>36x36</td> <td>0~120dpi</td> <td>现今鲜有设备</td> <td>0.75</td> </tr> <tr> <td>mdpi</td> <td>48x48</td> <td>120~160dpi</td> <td>320x480</td> <td>1</td> </tr> <tr> <td>hddpi</td> <td>72x72</td> <td>160~240dpi</td> <td>480x800</td> <td>1.5</td> </tr> <tr> <td>xhdpi</td> <td>96x96</td> <td>240~320dpi</td> <td>720x1280</td> <td>2</td> </tr> <tr> <td>xxhdpi</td> <td>144x144</td> <td>320~480dpi</td> <td>1080x1920</td> <td>3</td> </tr> <tr> <td>xxxhdpi</td> <td>192x192</td> <td>480~640dpi</td> <td>1440x2560</td> <td>4</td> </tr> </tbody> </table> <h2><strong>编写时钟布局 app_widget_clock.xml</strong></h2> <p>注意,App Widget使用的是RemoteViews,仅支持有限的内置控件,自定义控件一律不支持,并且对控件的操作均要通过RemoteViews的有限方法来执行,接下来我们会用到RemoteViews的setImageViewBitmap方法,留意下面的代码。</p> <pre> <code class="language-java"><?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" /> <!--时针--> <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></code></pre> <h2><strong>编写AppWidgetProvider</strong></h2> <h3><strong>编写ClockAppWidgetProvider.java</strong></h3> <pre> <code class="language-java">public class ClockAppWidgetProvider extends AppWidgetProvider { public ClockAppWidgetProvider() { super(); } @Override public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { super.onUpdate(context, appWidgetManager, appWidgetIds); // 调用的间隔由res/xml/app_widget_info_clock.xml下的updatePeriodMillis决定 // 下面的for循环是update app widgets的标准写法 // N是桌面上该小部件的数目 final int N = appWidgetIds.length; for (int i = 0; i < N; i++) { // 对每一个小部件进行更新 int appWidgetId = appWidgetIds[i]; RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.app_widget_clock); appWidgetManager.updateAppWidget(appWidgetId, remoteViews); } // TODO 启动ClockService } @Override public void onDeleted(Context context, int[] appWidgetIds) { super.onDeleted(context, appWidgetIds); // TODO 任意一个小部件被移除时调用 } @Override public void onEnabled(Context context) { } @Override public void onDisabled(Context context) { // 所有桌面小部件被移除时调用 // TODO 注销ClockService } }</code></pre> <h3><strong>编写res/xml/app_widget_info_clock.xml</strong></h3> <pre> <code class="language-java"><?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></code></pre> <ul> <li>initialLayout初始化布局</li> <li>minHeight & minWidth的设置参见 <a href="/misc/goto?guid=4959716184880998911" rel="nofollow,noindex">小部件设计</a> <pre> <code class="language-java">// cellCountInRowOrColumn - 小部件的行数或列数 valueInDP = 70 × cellCountInRowOrColumn − 30</code></pre> </li> <li>previewImage是小部件的预览图,长按桌面查看小部件列表时显示的那个icon就是它</li> <li>resizeMode可伸缩的方向</li> <li>updatePeriodMillis小部件的刷新间隔,单位是秒,默认是一天</li> </ul> <h3><strong>Manifest的声明</strong></h3> <pre> <code class="language-java"><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></code></pre> <h2><strong>编写ClockService</strong></h2> <p>ClockService用于接收系统时间,并根据时间的时分秒值转动我们的时针、分针和秒针。</p> <pre> <code class="language-java">// 时分秒针角度推导 // 假设rawS、rawM、rawH分别为秒、分、时(12小时制)的数值 // angleS、angleM、angleH分别为秒、分、时针的转动角度 angleS = 360 / 60 × rawS = 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</code></pre> <h3><strong>编写时钟任务</strong></h3> <pre> <code class="language-java">private final class MyTimerTask extends TimerTask { @Override public void run() { // 获取Widgets管理器 AppWidgetManager widgetManager = AppWidgetManager.getInstance(getApplicationContext()); // widgetManager所操作的Widget对应的远程视图即当前Widget的layout文件 RemoteViews remoteView = new RemoteViews(getPackageName(), R.layout.app_widget_clock); // 见公式推导 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; // 计算时分秒针的角度 float rotateS = 360f / 60f * realS; float rotateM = 360f / 60f * realM; float rotateH = 360f / 12f * realH; // 根据角度转动时分秒针 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的内置方法操作控件 // 对内置控件的操作,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)); // 当点击Widgets时触发的事件 ComponentName componentName = new ComponentName(getApplicationContext(), ClockAppWidgetProvider.class); widgetManager.updateAppWidget(componentName, remoteView); } }</code></pre> <h3><strong>Bitmap旋转</strong></h3> <pre> <code class="language-java">/** * 旋转 * * @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); }</code></pre> <h3><strong>ClockService启动MyTimerTask</strong></h3> <pre> <code class="language-java">public class ClockService extends Service { private Timer mTimer; @Override public void onCreate() { super.onCreate(); mTimer = new Timer(); // 1000ms执行一次 mTimer.schedule(new MyTimerTask(), 0, 1000); } // TODO 其它生命周期方法 }</code></pre> <p>记得在Manifest.xml里声明ClockService</p> <pre> <code class="language-java"><service android:name=".ui.widget.clock.ClockService"/></code></pre> <p> </p> <p> </p> <p> </p> <p>来自:http://www.jianshu.com/p/2e75b695459a</p> <p> </p>