
简介WorldClock是一款面向安卓TV平台的世界时钟应用工程基于Java开发适用于TV应用开发学习者、Java入门者以及需要快速了解跨时区时间展示与遥控器交互设计的开发者项目体量不大也可作为课程设计中的参考示例。资源包共收录43个文件压缩后约3.06MB核心包含21个XML布局与配置、10个Java源码文件另有PNG图片素材、Gradle构建脚本、README说明及License源码按src主目录、构建配置、文档分层存放便于对照工程理解文件组织方式src下的业务逻辑与res/layout中的界面布局分离定位较为直观。实现层面重点涉及地理定位与城市时区检索、java.time日期时间API处理夏令时转换、ScheduledExecutorService定时同步网络时间、大屏横屏布局和遥控器焦点导航等电视端典型场景同时保留了build.gradle依赖配置和proguard-rules.pro混淆规则可导入常用开发工具直接编译与修改。项目已有217人浏览学习体积克制、结构完整既适合初学者逐文件研读也适合有海外业务或跨时区协作需求的团队快速迁移复用。 把一台吃灰的电视盒子彻底用起来是我开始WorldClock这个项目最直接的动力。家里的N1盒子刷了安卓TV之后平时也就是看视频大屏的利用率其实很低。有一次远程办公帮客户排查问题同事分别在日本、英国和美国我在手机、电脑之间来回切时区十分钟里看了七八次时间——那感觉太蠢了。其实绝大多数时候我只想知道“那边现在是几点”根本不需要打开日历、也不需要逐个添加国家时钟去反复查询。把世界时钟做成一块常驻电视屏幕的信息面板反而才是更符合直觉的用法。这就是WorldClock项目的起点一个运行在安卓TV上的世界时钟开机常驻抬眼就能看到几个关键城市的时间。这篇文章面向两类读者一是家里有安卓TV盒子、想让它发挥更多作用的人二是想练手TV应用开发、尤其对时区数据处理感兴趣的开发者。我会把从需求分析、技术选型、界面设计到真机调优的过程完整讲一遍重点说清楚那些写代码时容易踩的坑。1. 为什么在电视上放一个世界时钟需求场景与产品定位很多人听到“世界时钟”的第一反应是手机自带应用不就有吗但我做这个项目的出发点恰恰是——手机端的世界时钟和电视端的世界时钟根本是两个产品。1.1 世界时钟在手机和电视上完全不是同一个产品手机App的世界时钟使用路径是“打开—查—退出”用户带着明确目的界面必须信息紧凑、操作零成本。电视端世界时钟的使用路径则是“人在客厅顺手抬眼”它更像一块挂在墙上的钟表而不是一个待查询的工具。这个差异直接决定了产品形态手机端列表加搜索用户主动打开查完就退电视端单屏常驻信息一眼扫完不需要搜索也能工作手机端可以频繁增删城市配置成本低电视端用遥控器输入城市名是灾难配置频率必须尽量低最好一次设置好就再也不用动。想清楚这些再动手写代码你会发现很多看起来“高级”的功能其实根本不需要。第一版我只做了六张城市卡片常驻屏幕每张卡片显示城市名、当地时间和与本地时区的差值。没有闹钟没有世界地图没有城市搜索。1.2 给谁用跨时区协作的家庭用户和App开发者这个项目解决的痛点核心是“高频瞥一眼”而不是“低频查一次”。远程办公时代一个团队分布在三四个时区是很常见的事。客厅电视如果常年开着把世界时钟放在屏幕上比每次开会前掏手机换算高效得多。对开发者来说这个项目还有一个价值它的业务逻辑清晰但技术深度恰好卡在“不算简单、也不至于复杂”的位置——时区处理涉及IANA数据库、夏令时、UTC绝对时间TV端交互涉及焦点导航、大屏布局、常驻稳定性再加上真机调试链路几乎把TV应用开发的常见问题都覆盖了一遍。作为上手TV开发的练手项目非常合适。1.3 功能边界第一版做哪些不做哪些第一版只做四件事城市时间卡片、本地时间对比、日期显示、白天黑夜指示。不做的事情也很明确不做闹钟、不做秒表、不做时区换算工具、不做城市增删界面。我的思路是先把“常驻信息面板”这个核心体验打磨好后续再考虑配置功能。这个取舍背后有个很实际的原因TV端用遥控器做文本输入体验非常糟糕。与其做一个输入操作很痛苦的“完整产品”不如做一个零操作、打开就用的专用工具。2. 开发环境与硬件选型拿什么跑安卓TV应用WorldClock的开发环境和普通Android项目没有本质区别但有几个TV特有的细节处理不好会浪费很多时间。2.1 用标准Android Studio开发TV应用的注意点技术栈我选择了Kotlin加传统View体系没有用Jetpack Compose for TV。原因很简单Compose for TV在焦点管理上还在快速迭代而传统View的D-pad焦点体系非常成熟单屏信息展示类应用用传统View实现成本更低、调试更直接。项目创建时minSdk建议设为26以上。因为世界时钟的核心时间计算我需要用java.time包也就是LocalDateTime、ZoneId、Instant这一套API。如果minSdk低于26就得引入ThreeTenABP兼容库多一层依赖不说API行为还有细微差异。模拟器也可以跑TV应用但时区数据在模拟器上可能与真机不一致而且模拟器没法真实模拟电视盒子的性能瓶颈。我的建议是逻辑阶段用模拟器界面和性能验证直接上真机。2.2 在N1盒子这类设备上的安装调试链路我用来实测的硬件是一台N1盒子已经刷好了安卓TV系统。这类盒子的优势是性能对付轻量应用绰绰有余价格也便宜拿来当TV开发调试机很划算。应用装到盒子上的链路很简单先确保盒子和电脑在同一个局域网然后在电脑上执行adb connect 192.168.x.x adb install -r app-debug.apk第一次连接时盒子会弹出是否允许USB调试的确认框需要用遥控器点一下“允许”。之后就可以纯命令行操作了。这里有个非常常见的坑adb connect显示连接成功但adb devices状态是offline。我遇到的原因通常是盒子上的调试授权被重置了或者电脑端adb key不匹配。解决方法是到盒子的开发者选项里关掉USB调试再重新打开然后重新adb connect。如果还不行就把电脑上~/.android/adbkey删掉重新生成再连接一次就好了。2.3 适配TV的Manifest声明桌面找不到图标是最大的坑如果一个应用只是通过adb装进盒子那么缺几个声明也能跑但如果你想在TV桌面上正常显示图标或者以后要上传到商店必须做好两点声明。首先是leanback特性uses-feature android:nameandroid.software.leanback android:requiredfalse /对应地触摸屏特性要声明为非必需uses-feature android:nameandroid.hardware.touchscreen android:requiredfalse /其次启动Activity必须声明LEANBACK_LAUNCHER类别intent-filter action android:nameandroid.intent.action.MAIN / category android:nameandroid.intent.category.LEANBACK_LAUNCHER / /intent-filter如果你的Activity只写了LAUNCHER而没有写LEANBACK_LAUNCHER在手机上能正常打开在TV桌面上就是找不到入口。这个坑我实际踩过排查了十分钟才反应过来。3. 时区计算的正确姿势WorldClock的灵魂在数据不在UI世界时钟最核心的技术点其实不是界面而是“怎么保证每个城市显示的时间永远正确”。这听起来简单实际做起来有不少细节。3.1 城市不是时区先建一份IANA ZoneId映射表时区数据的第一原则城市和时区ID一一对应而不是和偏移量一一对应。因为偏移量会变——夏令时、历史调整、某些国家突然修改时区规则都会让固定偏移失效。我把支持的城市硬编码在JSON里应用启动时加载{ cities: [ { city: 北京, zoneId: Asia/Shanghai }, { city: 东京, zoneId: Asia/Tokyo }, { city: 伦敦, zoneId: Europe/London }, { city: 纽约, zoneId: America/New_York }, { city: 洛杉矶, zoneId: America/Los_Angeles }, { city: 悉尼, zoneId: Australia/Sydney } ] }有个很多人不知道的细节北京在IANA数据库中并没有独立的Asia/Beijing条目全国统一使用Asia/Shanghai。如果你硬要写一个Asia/Beijing系统也能识别但底层映射的其实是同一个规则。做全球城市映射表时最稳妥的方式就是用IANA的ZoneId字符串不要自创。3.2 以UTC绝对时间为准别依赖系统默认时区世界时钟最容易犯的错是拿到“本机时间”之后用它去推算其他城市。比如有人会写// 错误示例 val localTime Calendar.getInstance() val offset TimeZone.getTimeZone(America/New_York).getOffset(System.currentTimeMillis()) val nyTime localTime.timeInMillis offset这段代码的问题有两个第一Calendar.getInstance()和TimeZone.getDefault()拿到的都是设备当前时区如果用户把盒子设成北京时间这串逻辑勉强能跑一旦盒子本身在别的时区推算是错的。第二把“时间”当成了“数字加减法”忽略了时区切换时日期可能跨天、夏令时可能让偏移量变化这些复杂情况。正确做法是所有城市统一从UTC绝对时间出发用目标时区ID格式化val zoneId ZoneId.of(America/New_York) val now Instant.now() val zonedDateTime now.atZone(zoneId) val timeText DateTimeFormatter.ofPattern(HH:mm).format(zonedDateTime) val dateText DateTimeFormatter.ofPattern(M月d日 EEEE, Locale.CHINESE).format(zonedDateTime) val offsetText zonedDateTime.offset.id // 比如 -05:00Instant.now()永远是绝对时间它不依赖任何时区atZone()负责把绝对时间映射到指定时区的本地表示。这套逻辑在任何时区的设备上跑结果都一样。3.3 夏令时与历史变化时间不是线性的如果你在代码里写死了“纽约是UTC-5”那一年里有一半的时间显示都会错一小时。美国、欧洲、澳大利亚都有夏令时在夏令时生效期间偏移会变成UTC-4。用java.time判断夏令时很简单val zoneId ZoneId.of(America/New_York) val now Instant.now() val zonedDateTime now.atZone(zoneId) // 当前是否处于夏令时 val isDST zoneId.rules.isDaylightSavings(now) // 当前偏移量包含夏令时 val offsetSeconds zonedDateTime.offset.totalSeconds用这套API你根本不需要自己维护“什么时候切夏令时”的规则系统的时区数据库会更新。这也是为什么我强调minSdk 26——Android 8.0以后的java.time实现会随系统更新时区规则旧API的TimeZone虽然也能用但语义混乱坑很多。3.4 刷新策略精确到分钟做分钟对齐时钟应用有一个最常见的过度设计用每秒刷新来显示秒针。对世界时钟来说绝大多数场景精确到分钟就够了而且每秒刷新会让盒子CPU一直处于工作状态十分没必要。我采用的策略是“分钟对齐”每次刷新后计算距离下一分钟整还剩多少毫秒睡到那个时刻再刷新。private fun scheduleNextMinute() { val now System.currentTimeMillis() val delay 60_000L - (now % 60_000L) handler.postDelayed({ refreshAllCityCards() scheduleNextMinute() }, delay 50) // 多等50ms确保分钟确实已经切换 }这段代码是这个项目里性价比最高的一段它保证了刷新动作严格对齐分钟边界不会因为定时器固定周期导致时间漂移。实测在N1盒子上整个应用运行一整天CPU占用可以忽略不计完全不用频繁整屏重绘。4. 电视大屏的界面与交互设计10英尺之外也要看得清电视端的UI设计和手机完全不同。手机屏幕离眼睛30厘米电视屏幕离眼睛3米手机可以触摸电视只能用遥控器。WorldClock的界面设计要同时解决“看得清”和“操作顺”两个问题。4.1 遥控器焦点导航与信息架构TV端没有鼠标没有触摸所有交互都依赖D-pad方向键和OK键。界面上所有可聚焦的元素必须能用方向键顺畅地移动焦点不能出现按右却跳到下方这种情况。对WorldClock这种单屏信息面板来说最稳妥的做法是把六张城市卡片做成固定网格每张卡片一个焦点配合focusabletrue就能工作。城市卡片本身不需要被“点击”但要存在焦点这样在遥控器上会有一个可感知的选中状态。我做了两级信息层级默认是高亮显示的城市名、时间以及处于焦点时的阴影和边框放大效果。如果未来要支持更多城市不要做成滚动列表——用遥控器在长列表里滚动非常痛苦。更好的方案是分页展示或者用“上一页/下一页”切换城市组。4.2 字号、对比度与深色主题的取舍客厅使用场景决定了三米外必须看清。我实际调过的字号参数大致如下核心时间数字140sp到160sp城市名36sp到40sp日期和时区偏移24sp到28sp。背景我选择了深色底#0F1115时间文字用高亮白#FFFFFF辅助信息用中等灰#B0B0B0白天黑夜指示用暖黄色和冷蓝色区分。深色主题的另一个好处是客厅晚上看不会刺眼。字体选择上有一个容易被忽略的细节默认中文字体在大屏上笔画偏细视觉重量不够。我指定了android:fontFamilysans-serif-condensed数字显示会更紧凑有力而且和城市中文名混排时不会显得凌乱。4.3 防烧屏与常亮模式时钟类应用的必修课WolrdClock要“常驻屏幕”就必须处理两个矛盾一是屏幕不要休眠二是OLED屏长时间固定显示会造成烧屏。屏幕常亮的做法是让窗口持有FLAG_KEEP_SCREEN_ONwindow.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)这个flag比反复唤醒屏幕更高效而且系统在“界面不可见”时会自动清除它不用担心放在后台时还亮着屏。烧屏问题是OLED电视的实打实风险。我的处理方案有三个每隔一段时间把整个布局微移几个像素避免固定像素持续点亮提供“夜间待机模式”在深夜自动切换成只显示一行极小时间弱化静态区域允许系统屏保照常启动如果你不在乎一直显示让屏保在几分钟后覆盖屏幕反而能保护面板。这三个方案实际用了前两个第三点只做了配置项。如果你用的是LCD电视烧屏风险小很多可以忽略微移逻辑。4.4 低端盒子性能适配减少不必要的重绘N1盒子是2018年的硬件四核A53加2GB内存对于时钟应用绰绰有余但前提是你的代码别乱来。最容易踩的坑有两个一是用ObjectAnimator或ValueAnimator做每帧动画二是每次刷新都让整个布局requestLayout。前者会持续拉高GPU占用后者会引发布局重排。最稳妥的做法是刷新时只setText更新TextView不做任何布局动画。白天黑夜指示的切换也直接用setImageResource更新图标不需要任何动画过渡。这样即使盒子同时跑着其他任务WorldClock也能稳定在60帧——应该说根本不用关心帧率因为没有动画需要渲染。5. 真机实测中的三个坑与扩展方向代码写完不等于项目完成真机运行时暴露的问题才是最有价值的经验。5.1 装机后最常遇到的三个问题第一个问题桌子桌面找不到应用图标。原因就是我前面说的LEANBACK_LAUNCHER没有声明表现是adb安装成功后在TV桌面翻了两页都没看到入口。排查思路先确认Activity的intent-filter是否包含LEANBACK_LAUNCHER再确认leanback特性是否声明为非必需。两个都对了图标就会出现。第二个问题屏幕运行几分钟后自动变黑。盒子的系统设置里默认有休眠策略有些固件的默认休眠时间是2分钟或5分钟。虽然FLAG_KEEP_SCREEN_ON可以让自己的应用保持唤醒但部分盒子固件对第三方应用的电源管理有额外拦截。实测下来FLAG_KEEP_SCREEN_ON在N1上有效但在另一个杂牌盒子上无效屏幕照样熄灭。处理方法是到盒子的设置里把休眠时间改成“永不”或者在应用启动时引导用户修改这个设置。第三个问题中文字体发虚。在小屏手机上不明显的锯齿放大到电视上会很明显。解决方法是设置android:includeFontPaddingfalse同时使用比较粗的字重。这个问题的排查链路很直接先换字体再调padding基本能解决。5.2 开机自启与桌面快捷方式既然做“常驻信息面板”最好能开机自动启动。实现上有一个常规思路注册BOOT_COMPLETED广播接收器。receiver android:name.BootReceiver android:enabledtrue android:exportedtrue intent-filter action android:nameandroid.intent.action.BOOT_COMPLETED / /intent-filter /receiver但很多TV固件对第三方应用的BOOT_COMPLETED广播有严格限制实测在部分固件上收不到这个广播。更可靠的方案是使用第三方桌面的“开机直达”功能或者把WorldClock设置成桌面主应用。我自己的方案是在当贝桌面的设置里把WorldClock加入开机自启列表稳定运行了好几个月比只靠广播可靠得多。5.3 从世界时钟到家庭信息屏的扩展方向第一版做完之后很自然想到扩展。WorldClock已经把骨架搭好了一个常驻大屏、每N分钟刷新一次、纯信息展示。在这个骨架上可以继续叠加很多模块天气模块接入一个天气API在时间卡片下方显示温度同样走定时刷新逻辑万年历模块传统的农历、节气和节假日信息大屏展示观感很好家庭信息模块接入家庭的待办事项、快递进度甚至智能家居设备状态分钟级动画翻页时钟效果让常驻界面更有观赏性。其中我比较推荐先扩展天气因为和世界时钟的场景天然契合——看时间是刚需看天气是顺手。两个模块共用一个刷新框架开发成本很低。如果想把项目做得更通用可以考虑把“城市列表”抽出来做成可配置项通过同一局域网下的手机Web页面管理城市TV端只负责展示。这样既避开了遥控器打字难的问题又保留了自定义能力。这也是我下一版打算做的方向。本文还有配套的精品资源点击获取