在智能手机的桌面上,经常会看到一个蓝色的小圆圈,它安静地待在那里,或许在暗示着日历应用自上次查看以来有所更新。这不是关于那个小蓝点的故事,而是关于图片中心那对神秘数字的故事,它们恰好与今天的日期(7月29日)相吻合。
Google日历可能是能找到的最好的日历应用(或服务),因为它可以在所有设备上无缝同步,其二,它的图标背景上有友好的小数字,正确显示了每个月的日期,无论何时查看,这个神秘而有趣的小功能,正是试图弄清楚的。在尝试为波斯日历实现完全相同的功能时被吸引进来。
好的,让开始工作。在网上找到了一个Google日历的APK(由于版权等原因,不会在这里提供),并对其进行了解压缩。接下来,尝试了如下简单的命令:
grep -Ri dynamic . | grep icon
这是输出的重要部分:
./res/values-v26/arrays.xml: <array name="calendar_icons_dynamic_nexus_round">
这是该文件的重要部分:
<array name="calendar_icons_dynamic">
<item>@drawable/logo_calendar_01_adaptive</item>
<item>@drawable/logo_calendar_02_adaptive</item>
<item>@drawable/logo_calendar_03_adaptive</item>
<item>@drawable/logo_calendar_04_adaptive</item>
<item>@drawable/logo_calendar_05_adaptive</item>
</array>
让看看res/drawable-v26/logo_calendar_03_adaptive.xml:
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/adaptive_background"/>
<foreground>
<layer-list>
<item android:drawable="@drawable/adaptive_base"/>
<item android:drawable="@drawable/calendar_date_03_adaptive"/>
</layer-list>
</foreground>
</adaptive-icon>
非常直观,对吧?这些资源都是png格式,包含了想到的内容:adaptive_base和calendar_date_03_adaptive。
在那之后,深入研究了.smali文件,并浏览了将apk转换为jar后的实际代码(使用了dex2jar,然后使用jd-gui)。经过数小时的代码导航,没有找到任何上面提到的drawables的引用。这就是为什么把日历应用放在一边,转而去研究像素启动器的源代码。在名为DynamicIconProvider的类中,可以找到以下内容:
private boolean fe(String paramString) {
return "com.google.android.calendar".equals(paramString);
}
public Drawable getIcon(LauncherActivityInfo paramLauncherActivityInfo, int paramInt, boolean paramBoolean) {
// ...
}
突然之间,如果瞥一眼这个文件的其他部分,一切都变得有意义了:
public DynamicIconProvider(Context paramContext) {
IntentFilter localIntentFilter = new IntentFilter("android.intent.action.DATE_CHANGED");
localIntentFilter.addAction("android.intent.action.TIME_SET");
localIntentFilter.addAction("android.intent.action.TIMEZONE_CHANGED");
paramContext.registerReceiver(this.gv, localIntentFilter, null, new Handler(LauncherModel.getWorkerLooper()));
this.mContext = paramContext;
this.mPackageManager = paramContext.getPackageManager();
}
注意动作类路径。还发现了这个c.class,它几乎是不可读的解压缩函数/类名,可能负责更新时钟应用(deskclock)的界面。这里有一些有趣的部分:
localPackageManager.getApplicationInfo("com.google.android.deskclock", 8320);
localb.fW = localBundle.getInt("com.google.android.apps.nexuslauncher.HOUR_LAYER_INDEX", -1);
localb.fY = localBundle.getInt("com.google.android.apps.nexuslauncher.MINUTE_LAYER_INDEX", -1);
localb.fZ = localBundle.getInt("com.google.android.apps.nexuslauncher.SECOND_LAYER_INDEX", -1);
localb.fS = localBundle.getInt("com.google.android.apps.nexuslauncher.DEFAULT_HOUR", 0);
localb.fT = localBundle.getInt("com.google.android.apps.nexuslauncher.DEFAULT_MINUTE", 0);
localb.fU = localBundle.getInt("com.google.android.apps.nexuslauncher.DEFAULT_SECOND", 0);
值得注意的是,日历应用和deskclock应用自适应图标之间的设计决策差异。为了进一步理解这一点,让看看deskclock的launcher_clock.xml的一部分:
<foreground>
<layer-list>
<item>
<rotate android:drawable="@mipmap/launcher_clock_hour" android:fromDegrees="300.0" android:toDegrees="5300.0" android:pivotX="50.0%" android:pivotY="50.0%"/>
</item>
<item>
<rotate android:drawable="@mipmap/launcher_clock_minute" android:fromDegrees="60.0" android:toDegrees="60060.0" android:pivotX="50.0%" android:pivotY="50.0%"/>
</item>
<item>
<rotate android:drawable="@mipmap/launcher_clock_second" android:fromDegrees="180.0" android:toDegrees="6180.0" android:pivotX="50.0%" android:pivotY="50.0%" android:level="300"/>
</item>
<item android:drawable="@mipmap/launcher_clock_top"/>
<item android:drawable="@mipmap/launcher_clock_banner"/>
</layer-list>
</foreground>
除了这个,代码库中没有其他对launcher_clock_second的引用。有趣的是,可以看到两种非常不同的解决方案来解决一个相当相似的问题:动态启动器图标。由于在线文本渲染的复杂性,作者们决定预先准备好日历应用所需的所有图标。然而,在deskclock的情况下,如果他们事先创建drawables,将会有太多的图标,这导致了将这种看似无关(就启动器而言)的逻辑引入到像素启动器的代码中:
bool1 = bool2;
if (this.fV.getDrawable(this.fW).setLevel((i + (12 - j)) % 12 * 60 + this.fX.get(12))) {
bool1 = true;
}