Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

北京时间 (BJClock)

一个黑底白字、沉浸式全屏的"北京时间数字时钟" Android APP。 用 Capacitor 8 把一个纯 HTML 页面打包成原生 APK,并实现了类似"游戏模式"的沉浸式体验 —— 系统栏平时完全隐藏,从屏幕边缘下滑时临时浮现几秒后自动收回。


项目是什么

一个极简的 Android 时钟应用:

  • 打开就是一个纯黑背景的页面,中央是大号白色的北京时间(HH:mm:ss,每秒跳动)
  • 下方显示中文日期 + 星期,再往下是一行"中国标准时间 UTC+8"
  • 状态栏和导航栏默认完全隐藏,时钟占满整个屏幕(包括刘海/挖孔区域)
  • 从屏幕顶部或底部边缘向下滑,系统栏会临时浮现,几秒不用它又会自动缩回去
  • 全程离线运行,不联网,时间是从设备时间换算成 UTC+8 得到的

本质上是一个只有一个 HTML 页面的 Web 应用,通过 Capacitor 装进了一个原生 Android 壳里。


为什么不用 Python 起个 HTTP 服务,而是用 Capacitor?

做这个项目时考虑过最朴素的方案:在手机上用 Python http.server 起个本地服务,然后用浏览器打开 localhost:8000。但这个方案有几个硬伤:

方案 问题
Python + 本地 HTTP 服务 ① 得先开 Termux、起服务、再开浏览器输地址,步骤繁琐;② 浏览器顶部永远有地址栏和工具栏,做不到真正全屏;③ 不能切后台,服务一停页面就没了;④ 没有一个可以从桌面点开、有自己图标的"APP"
Capacitor 打包成 APK ① 编译出一个独立 APK,装上就是正经 APP,桌面点图标直接进;② 可以调用原生 API 真正控制系统栏,实现沉浸式;③ 离线运行,不依赖任何后台进程;④ 体验和原生 APP 完全一致

所以选择了 Capacitor —— 它的定位就是"用 Web 技术写页面,套一层原生壳变成 APP"。页面部分你写的还是 HTML/CSS/JS,但跑在一个由原生 Activity 托管的 WebView 里,并且可以通过插件桥接调用原生能力(比如本项目用到的系统栏控制)。

你记不清的"那个工具"就是 Capacitor(对标 Cordova / PhoneGap,Ionic 团队出品,目前是这个领域的主流方案)。


技术栈一览

技术 版本
页面层 纯 HTML + CSS + 原生 JavaScript(无任何框架)
Web 容器 Capacitor(core / cli / android 三件套) 8.4.2
原生壳 Java MainActivity 继承 Capacitor 的 BridgeActivity
沉浸式控制 AndroidX WindowCompat + WindowInsetsControllerCompat core 1.17.0
构建系统 Gradle + Capacitor Gradle 插件
最低支持 Android 7.0 minSdk 24
目标版本 Android 16 targetSdk 36

项目结构

BJClock/
├── src/
│   └── index.html              # ★ 全部页面逻辑:时钟渲染 + 时间换算 + 全屏按钮
├── capacitor.config.json       # Capacitor 配置:APP 名、webDir、SystemBars 插件配置
├── package.json                # 依赖:@capacitor/core / cli / android
│
└── android/                    # Capacitor 生成的原生 Android 工程
    ├── app/src/main/
    │   ├── java/com/bjclock/app/
    │   │   └── MainActivity.java   # ★ 沉浸式核心代码(详见下文)
    │   ├── res/values/
    │   │   └── styles.xml          # ★ 启动主题:透明系统栏 + 挖孔屏适配
    │   └── AndroidManifest.xml     # 入口声明 + 屏幕配置变化处理
    ├── variables.gradle            # SDK 与 AndroidX 依赖版本
    └── ...

真正"自己写的代码"只有三个文件:src/index.htmlMainActivity.javastyles.xml。其余都是 Capacitor 模板生成的工程骨架。


核心知识点 1:怎么把一个 HTML 页面变成 APK(Capacitor 流程)

整体流程是四步,理解了这套流程任何"网页变 APP"的需求都能套:

① 写网页

src/index.html 就是普通网页,没什么特殊。关键逻辑是把设备时间换算成北京时间

const now = new Date();
// 先拿到 UTC 时间(本地时间 + 本地时区偏移)
const utc = now.getTime() + now.getTimezoneOffset() * 60000;
// UTC 再加 8 小时 = 北京时间,无论设备在哪个时区都正确
const bj = new Date(utc + 8 * 3600000);

这一步的意义:就算手机设的是纽约时区,显示的依然是北京时间,而不是简单读 new Date()

② 初始化 Capacitor 工程

npm init -y
npm install @capacitor/core @capacitor/cli @capacitor/android

然后在 capacitor.config.json 里告诉 Capacitor 三件事:APP 叫什么、网页在哪、插件怎么配:

{
  "appId": "com.bjclock.app",
  "appName": "北京时间",
  "webDir": "src",
  "plugins": {
    "SystemBars": {
      "insetsHandling": "disable",
      "style": "DARK",
      "hidden": true
    }
  }
}

这里的 SystemBarsCapacitor 8 内置的核心插件(不用单独装),负责初步隐藏系统栏。style: "DARK" 表示系统栏图标用浅色(因为我们背景是黑的)。

③ 生成原生 Android 工程

npx cap add android    # 生成完整的 android/ 原生工程(只跑一次)
npx cap sync           # 把 webDir 里的网页拷进原生工程,以后每次改网页都跑这个

cap add android 会生成一个标准的 Gradle 工程,里面带一个继承 BridgeActivityMainActivity —— 这个 Activity 里嵌着一个 WebView,Capacitor 会把你 src/ 下的网页加载进去。

④ 编译出 APK

需要一个 Android SDK + JDK 环境,然后:

cd android
./gradlew assembleDebug      # 产出 debug 包,可直接装
#
./gradlew assembleRelease    # 产出 release 包(需签名才能装)

产物路径:android/app/build/outputs/apk/debug/app-debug.apk

本项目最终在 Termux 里用 commandlinetools 装的 Android SDK 完成了编译(详见下文"Termux 里怎么搞 Android 编译环境"),把生成的 APK 重命名成了 北京时间.apk


核心知识点 2:沉浸式全屏("游戏模式")是怎么实现的

这是本项目最关键的原生知识点,涉及三层配合,缺一不可。目标是:系统栏默认隐藏、内容占满全屏(含挖孔区)、下滑边缘系统栏临时浮现。

第 1 层:让内容画到系统栏下面(edge-to-edge)

MainActivity.javaonCreate 里:

// 关键!告诉系统"我的内容要自己负责系统栏区域,别帮我留边"
WindowCompat.setDecorFitsSystemWindows(window, false);

// 系统栏背景设成透明,否则会是一条灰/白条
window.setStatusBarColor(Color.TRANSPARENT);
window.setNavigationBarColor(Color.TRANSPARENT);
  • setDecorFitsSystemWindows(false):Android 15 (API 35) 默认开启 edge-to-edge,但之前的版本必须手动调这个,否则 WebView 会被限制在状态栏和导航栏之间的"安全区"里。
  • 这一步只是让内容能"延伸"到系统栏的底下,系统栏本身还在,只是变成透明的浮在内容上。

第 2 层:刘海 / 挖孔屏适配

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {  // API 28+
    WindowManager.LayoutParams attrs = window.getAttributes();
    attrs.layoutInDisplayCutoutMode =
            WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
    window.setAttributes(attrs);
}

现在很多手机有挖孔/刘海屏。默认情况下,系统不允许你的内容画到挖孔那条区域(怕挡住摄像头)。LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES 的意思是"短边(顶部/底部)的挖孔区域我也照画",这样时钟才能真正占满物理屏幕的每一个角落。

同一个设置也在 styles.xml 的启动主题里配了一份,保证从启动那一刻起就是全屏,不会有"先白边后全屏"的闪烁:

<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
    <item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
    <item name="android:statusBarColor">@android:color/transparent</item>
    <item name="android:navigationBarColor">@android:color/transparent</item>
    <item name="android:windowBackground">@android:color/black</item>
</style>

第 3 层:真正隐藏系统栏 + "下滑临时浮现"(游戏模式的核心)

这才是"顶部框平时缩进去、下拉才出来"的实现,MainActivity.javahideSystemBars()

private void hideSystemBars() {
    Window window = getWindow();
    View decor = window.getDecorView();
    // 拿到系统栏控制器(AndroidX 兼容封装,底层根据 API 版本自动选实现)
    WindowInsetsControllerCompat controller =
            WindowCompat.getInsetsController(window, decor);

    // 黑底 → 系统栏图标用白色,不然黑底上看不见图标
    controller.setAppearanceLightStatusBars(false);
    controller.setAppearanceLightNavigationBars(false);

    // ★ 关键行为:从边缘滑出时系统栏"临时"显示,几秒后自动收回
    controller.setSystemBarsBehavior(
            WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);

    // 隐藏状态栏 + 导航栏
    controller.hide(WindowInsetsCompat.Type.systemBars());
}

三个要点逐个拆:

  1. WindowInsetsControllerCompat:AndroidX 提供的系统栏控制器,兼容到 API 21+。不用它就得自己写 View.SYSTEM_UI_FLAG_* 那套老 API(已废弃且难维护)。
  2. BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE:这就是"游戏模式"的灵魂。设置之后,隐藏的系统栏不会彻底消失,而是从屏幕边缘做一次"滑入"手势时短暂浮现,过几秒自动收回去。很多视频/游戏 APP 的全屏就是这个行为。
  3. controller.hide(Type.systemBars()):一次性隐藏状态栏 + 导航栏。

第 4 层:焦点恢复时重新隐藏(很多人漏掉的一步)

@Override
public void onWindowFocusChanged(boolean hasFocus) {
    super.onWindowFocusChanged(hasFocus);
    if (hasFocus && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
        hideSystemBars();   // 重新拿到焦点时再次隐藏
    }
}

:APP 切到后台再切回来、或者关了一个弹窗之后,系统会自作主张把系统栏恢复显示。如果不重写 onWindowFocusChanged 在重新获得焦点时再次调用 hideSystemBars(),沉浸式就"破功"了。这是做沉浸式最容易漏的一个细节。

网页侧的配合

index.html 里也有两处和沉浸式呼应:

<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
  • viewport-fit=cover:告诉 WebView"内容可以铺满整个视口,包括刘海/挖孔遮挡区",和原生侧的第 2 层对应。
body {
    height: 100vh;
    height: 100dvh;   /* 动态视口高度,规避移动端浏览器地址栏伸缩导致的高度抖动 */
}

核心知识点 3:AndroidManifest 里那个超长的 configChanges 是什么

<activity
    android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation|density"
    ... />

默认情况下,手机旋转屏幕、切换深浅色模式、改字体大小、键盘弹出这些事件都会让 Activity 销毁重建——对一个跑着 JS 定时器的 WebView 来说,每次重建页面都会白屏闪一下、时钟还会重置。

把这一系列事件写进 configChanges 等于告诉系统:"这些变化我自己处理,别重启我的 Activity"。Capacitor 模板默认就配好了这一串,一般不用动。


在 Termux 里搭建 Android 编译环境(本项目实际走的路)

没有电脑,整个编译是在手机 Termux 里完成的,步骤:

# 1. 装 JDK 17(Gradle 编译需要)
pkg install openjdk-17

# 2. 下载 Android 命令行工具并解压到 ~/.android-sdk
#    (commandlinetools-linux-*_latest.zip,谷歌官网可下)
mkdir -p ~/.android-sdk/cmdline-tools
# 解压后目录结构应为 ~/.android-sdk/cmdline-tools/latest/bin/...

# 3. 配环境变量
export ANDROID_HOME=~/.android-sdk
export PATH=$PATH:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools

# 4. 用 sdkmanager 装编译必需组件
sdkmanager "platform-tools" "platforms;android-36" "build-tools;36.0.0"

# 5. 接受协议
yes | sdkmanager --licenses

# 6. 告诉工程 SDK 在哪(android/local.properties,这个文件不该进 git)
echo "sdk.dir=$HOME/.android-sdk" > android/local.properties

# 7. 编译
cd android && ./gradlew assembleDebug

⚠️ Termux 里跑 Gradle 偶尔会遇到内存不足或文件锁问题,给 gradle.propertiesorg.gradle.jvmargs=-Xmx2048morg.gradle.daemon=false 会稳定不少。


目录速查 / 常用命令

# 改了网页之后同步到原生工程
npx cap sync

# 编译 debug APK
cd android && ./gradlew assembleDebug

# 编译 release APK(需要先在 android/app/ 配签名 keystore)
cd android && ./gradlew assembleRelease

# 清理构建产物
cd android && ./gradlew clean

复刻这个项目你需要知道的最小知识清单

  1. Capacitor 三段式:写网页 → cap add android 生成原生壳 → cap sync 同步 → Gradle 编译
  2. edge-to-edgeWindowCompat.setDecorFitsSystemWindows(window, false) + 系统栏设透明
  3. 挖孔屏LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES(Java 和启动主题各配一份)
  4. 沉浸式/游戏模式WindowInsetsControllerCompat + BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
  5. 防破功:重写 onWindowFocusChanged 重新隐藏
  6. 网页配合viewport-fit=cover + 100dvh
  7. Termux 编译:openjdk-17 + commandlinetools + sdkmanager + local.properties

Native C 极简版(12 KB APK)

native-c/ 目录下有一个用纯 C + NDK Native Activity 重写的版本,APK 体积从 4 MB 压缩到 12 KB(缩小 330 倍),LED 数码管外观(尖角段、鬼影底纹、冒号闪烁)。

指标 Capacitor 版(本目录) Native C 版(native-c/)
APK 大小 4 MB 12 KB
代码 HTML + Java + Capacitor 框架 纯 C:main.c + clock_draw.h
DEX 8.5 MB (零 Java 代码)
渲染 WebView 直接操作像素缓冲
沉浸式 有(手势下滑呼出系统栏) 无(简单全屏)

详见 native-c/README.md


License

MIT

About

北京时间数字时钟 Android APP — Capacitor 8 打包纯 HTML 页面为 APK,沉浸式全屏(edge-to-edge + 挖孔屏适配 + 游戏模式系统栏下滑浮现),附详实中文开发笔记

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages