news 2026/10/6 2:08:00

Android 官方培训课程中文版:创建向后兼容的 UI —— 以 Action Bar Tabs 为例的抽象层实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Android 官方培训课程中文版:创建向后兼容的 UI —— 以 Action Bar Tabs 为例的抽象层实战
  • 文档
  • 教程
  • 移动开发

【免费下载链接】android-training-course-in-chinese

Android官方培训课程中文版

项目地址:https://gitcode.com/gh_mirrors/an/android-training-course-in-chinese
点击查看免费下载

本篇文章基于《Android 官方培训课程中文版》仓库(android-training-course-in-chinese)中 ui/backward-compatible-ui/index.md 及其四节子课程整理而成,完整呈现"在新版本 Android 上使用新 UI 组件与 API,同时保证应用在旧版本设备上正常运行"的经典做法。文中以 Android 3.0(API 等级 11)引入的 [Action Bar Tabs] 为贯穿全篇的指导案例,手把手带你走完"抽象出新 API → 代理至新 API → 用旧 API 实现同等效果 → 运行时按版本选择实现"的完整四步流程;学完之后,你将掌握一套可复用到 NumberPicker、Switch、ListPopupWindow、PopupMenu 等任何新组件的向后兼容设计模式。

课程背景:为什么需要"向后兼容的 UI"

自 Android 系统持续迭代以来,每个大版本都会带来一批全新的 UI 组件和 API。但新 API 往往只在较新的平台版本上可用——例如ActionBar及其ActionBar.Tab系列 API 只在 Android 3.0(API 等级 11,代号 Honeycomb)之后才出现。如果应用希望同时覆盖旧设备(如 Android 2.x)和新设备,就需要一套机制:

  • 在新版本设备上使用最新的 API,享受原生的交互与外观;
  • 在旧版本设备上回退到旧 API 实现的等价功能;
  • 对上层业务代码透明,Activity 不需要关心自己运行在哪个平台版本上。

本课程给出的答案是:用面向特定版本实现的抽象类(abstract class)构建一个 tab 形式的用户界面,以此提供向后兼容性。这套思路不仅适用于 Action Bar Tabs,也可以平移到任何其他 UI 组件和 API 功能上,因此具有很高的普适价值。

整个课程在仓库中由五篇文档组成:课程索引、抽象出新的 APIs、代理至新的 APIs、使用旧的 APIs 实现新 API 的效果、使用能感知版本的组件,并附带五张配套示意图与截图(类结构图三张、运行截图两张)。

第一课:抽象出新的 APIs —— 定义自己的中间层

为抽象做准备

在 Java 中,抽象的核心是"创建一个或多个接口或抽象类,隐藏具体实现细节"。面对不断演进的新版本 Android API,你可以用抽象构建能感知版本的组件(version-aware component):它在新设备上使用当前的 API,在旧设备上回退到兼容的旧 API。

具体操作分三步:

  1. 决定需要向后兼容的类:确定应用要使用哪些新类;
  2. 镜像新类的 public 接口:根据新类中的公共接口创建抽象类;
  3. 最大化前向兼容:在创建抽象接口时,应尽可能多地为新 API 创建镜像,这样未来当这些接口不再需要时,废弃它们会更容易。

抽象层建立之后,任何数量的实现都可以在运行时创建并按需选择。出于向后兼容的目的,这些实现可以按所需 API 级别划分:一个实现使用最新发布的 API,其余实现则使用较旧的 API。

创建抽象的 Tab 接口:先定义功能需求

在动手写代码之前,首先要决定应用需要哪些功能、依赖哪些具体的 API 接口。以"顶层分段 Tabs(top-level section tabs)"为例,课程假设了以下三条功能需求:

  1. 显示图标和文本的 Tab 指示器;
  2. Tabs 可以与一个 Fragment 实例相关联;
  3. Activity 可以监听到 Tab 变化。

提前准备这些需求能帮你控制抽象层的范围——范围越小,需要创建的具体实现越少,就能越快投入实际使用。针对上述需求,需要抽象出来的核心 API 是ActionBar和ActionBar.Tab(即前文类图中的CompatTab与TabHelper两个抽象类)。示例项目的兼容目标为:与 Eclair(API 等级 5)保持一致,同时利用 Honeycomb(API 等级 11)中的新 Tab 功能。

定义抽象类 CompatTab(ActionBar.Tab 的镜像)

构建 Tab 抽象层的第一步,是创建一个代表单个 tab 的抽象类,它是ActionBar.Tab接口的镜像:

public abstract class CompatTab { ... public abstract CompatTab setText(int resId); public abstract CompatTab setIcon(int resId); public abstract CompatTab setTabListener( CompatTabListener callback); public abstract CompatTab setFragment(Fragment fragment); public abstract CharSequence getText(); public abstract Drawable getIcon(); public abstract CompatTabListener getCallback(); public abstract Fragment getFragment(); ... }

这里特意使用抽象类而不是接口,是为了简化诸如"tab 对象与 Activity 关联"这类公共功能(代码片段中未完整显示)。注意,CompatTab的 setter 方法都返回CompatTab自身,这是典型的**流式接口(fluent interface)**设计,便于后续链式调用:

tabHelper.newTab("photos").setText(R.string.tab_photos);

定义抽象类 TabHelper(ActionBar Tab 管理方法的镜像)

第二步是定义一个抽象类,用来把 tab 创建并添加到 Activity 中,镜像ActionBar.newTab()与ActionBar.addTab()等方法:

public abstract class TabHelper { ... public CompatTab newTab(String tag) { // This method is implemented in a later lesson. } public abstract void addTab(CompatTab tab); ... }

说明:newTab()在这里只有声明,具体实现将在"使用能感知版本的组件"一课中补全——它本身就是版本分支的切换点。

至此,抽象层骨架已经成型。下一步就是分别创建新 API 实现与旧 API 实现两个分支。

第二课:代理至新的 APIs —— 为新平台编写 Proxy 实现

延迟类加载与 VerifyError:为何能安全引用新 API

CompatTab与TabHelper的"新版"具体子类本质上是一种代理(proxy)实现:它们只负责把方法调用转发给真正的ActionBar.Tab/ActionBar对象。由于抽象类早已定义了与新 API 一致的类结构与方法签名,这些子类几乎不需要任何业务逻辑。

你可以在具体子类中直接引用新 API,而不会在旧设备上崩溃,原因是 Dalvik 虚拟机采用延迟类加载(lazy class loading):类只有在首次被访问(实例化类对象、访问静态属性或调用静态方法)时才会被加载和初始化。因此,只要不在 Honeycomb 之前的设备上实例化 Honeycomb 相关实现,虚拟机就不会抛出VerifyError异常。

命名约定

课程推荐了一个清晰的命名约定:把具体子类依赖的 API 等级或版本名称附加在接口名之后。例如,本地的 Tab 实现可以命名为:

  • CompatTabHoneycomb—— 依赖 Android 3.0(API 等级 11)及之后版本的 API;
  • TabHelperHoneycomb—— 同上。

(同理,旧版实现命名为CompatTabEclair/TabHelperEclair。)

实现 CompatTabHoneycomb

CompatTabHoneycomb是CompatTab的具体实现,代表一个单独的 tab。它内部持有一个原生ActionBar.Tab对象(字段mTab),所有方法都是对该对象的简单转发:

public class CompatTabHoneycomb extends CompatTab { // The native tab object that this CompatTab acts as a proxy for. ActionBar.Tab mTab; ... protected CompatTabHoneycomb(FragmentActivity activity, String tag) { ... // Proxy to new ActionBar.newTab API mTab = activity.getActionBar().newTab(); } public CompatTab setText(int resId) { // Proxy to new ActionBar.Tab.setText API mTab.setText(resId); return this; } ... // Do the same for other properties (icon, callback, etc.) }

从源码可以看出,构造器通过activity.getActionBar().newTab()拿到原生ActionBar.Tab,setText()则直接转发到mTab.setText(resId),并返回this以维持流式调用。其余属性(icon、callback 等)照此模式一一实现即可。

实现 TabHelperHoneycomb

TabHelperHoneycomb是TabHelper的具体实现,它把方法调用代理到从宿主 Activity 获取的ActionBar对象上:

public class TabHelperHoneycomb extends TabHelper { ActionBar mActionBar; ... protected void setUp() { if (mActionBar == null) { mActionBar = mActivity.getActionBar(); mActionBar.setNavigationMode( ActionBar.NAVIGATION_MODE_TABS); } } public void addTab(CompatTab tab) { ... // Tab is a CompatTabHoneycomb instance, so its // native tab object is an ActionBar.Tab. mActionBar.addTab((ActionBar.Tab) tab.getTab()); } // The other important method, newTab() is part of // the base implementation. }

这里有两个值得注意的细节:

  • setUp()中通过setNavigationMode(ActionBar.NAVIGATION_MODE_TABS)把 ActionBar 切换到 Tabs 导航模式,这是 Honeycomb 上展示 tab 的前提;
  • addTab()接收的是CompatTab,但实际运行时它必然是CompatTabHoneycomb实例,因此取出其内部的ActionBar.Tab原生对象再交给mActionBar.addTab()。这个"向下转型"成立的前提,正是创建CompatTab时(newTab()工厂方法)保证返回了匹配当前平台版本的具体实现。

第三课:使用旧的 APIs 实现新 API 的效果 —— 为旧平台编写回退实现

决定替代方案:给每个新组件找一个旧实现

在以向后兼容的方式使用新 UI 功能时,最具挑战的任务是为旧平台版本设计解决方案。好消息是,很多新 UI 组件都可以用旧 UI 框架中的既有功能实现。课程给出的对照表如下:

新版本组件(Android 3.0+)旧版本替代方案
Action Bar水平的、包含图片按钮的LinearLayout(可作为 Activity 的自定义标题栏或普通视图);下拉行为可用设备菜单按钮实现
Action Bar 的 tab 页包含按钮的水平LinearLayout,或TabWidgetUI 控件
NumberPicker、Switch分别用Spinner、ToggleButton实现
ListPopupWindow、PopupMenu用PopupWindow实现

课程同时提醒:这些方案通常不是"一刀切"的解决方案,必须注意用户体验。在旧设备上,用户可能不熟悉新的界面设计模式和 UI 组件,应思考如何用熟悉的控件实现相同的功能。不过在很多情况下用户并不会注意到差异——尤其是当新 UI 组件在应用生态中非常突出(如 Action Bar),或者交互模型本身简单直观(如用ViewPager滑动页面)时。

使用旧 APIS 实现 Tabs:CompatTabEclair 与 TabHelperEclair

回到 Tab 案例。旧实现可以基于TabWidget和TabHost构建(当然,用水平排列的Button也是备选),对应类为TabHelperEclair和CompatTabEclair——它们的实现只依赖不迟于 Android 2.0(Eclair)的 API,因此可以安全地运行在 API 等级 5~10 的设备上。

CompatTabEclair与新版实现的区别在于:旧平台没有ActionBar.Tab对象负责数据存储,因此 tab 的文本、图标等属性必须保存在对象自身的实例变量中:

public class CompatTabEclair extends CompatTab { // Store these properties in the instance, // as there is no ActionBar.Tab object. private CharSequence mText; ... public CompatTab setText(int resId) { // Our older implementation simply stores this // information in the object instance. mText = mActivity.getResources().getText(resId); return this; } ... // Do the same for other properties (icon, callback, etc.) }

注意setText()通过mActivity.getResources().getText(resId)把资源 ID 解析成CharSequence再缓存起来——这是CompatTabEclair与CompatTabHoneycomb在实现策略上的根本差异:一个"存起来",一个"转发出去"。

TabHelperEclair则利用TabHost的方法来创建TabHost.TabSpec对象并驱动 tab 页面切换:

public class TabHelperEclair extends TabHelper { private TabHost mTabHost; ... protected void setUp() { if (mTabHost == null) { // Our activity layout for pre-Honeycomb devices // must contain a TabHost. mTabHost = (TabHost) mActivity.findViewById( android.R.id.tabhost); mTabHost.setup(); } } public void addTab(CompatTab tab) { ... TabSpec spec = mTabHost .newTabSpec(tag) .setIndicator(tab.getText()); // And optional icon ... mTabHost.addTab(spec); } // The other important method, newTab() is part of // the base implementation. }

这里的关键点:

  • setUp()通过mActivity.findViewById(android.R.id.tabhost)找到布局中的TabHost并调用setup()——这意味着旧平台的 Activity 布局必须包含TabHost(对应下一课中res/layout/main.xml的设计);
  • addTab()用newTabSpec(tag)+setIndicator(tab.getText())构造TabSpec,再交给mTabHost.addTab();
  • 两处实现都通过if (mTabHost == null)做幂等保护,避免重复初始化。

至此,CompatTab和TabHelper都拥有了两套实现:一套面向 Android 3.0+(Honeycomb 代理),一套面向 Android 2.0+(Eclair 旧实现)。接下来就是最关键的一步——在运行时按平台版本做选择。

第四课:使用能感知版本的组件 —— 工厂切换与版本化布局

添加切换逻辑:把TabHelper变成工厂类

TabHelper抽象类现在要承担双重角色:既是接口定义者,又是根据设备平台版本创建适当实现的工厂。切换依据是Build.VERSION.SDK_INT与Build.VERSION_CODES.HONEYCOMB的比较:

public abstract class TabHelper { ... // Usage is TabHelper.createInstance(activity) public static TabHelper createInstance(FragmentActivity activity) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { return new TabHelperHoneycomb(activity); } else { return new TabHelperEclair(activity); } } // Usage is mTabHelper.newTab("tag") public CompatTab newTab(String tag) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { return new CompatTabHoneycomb(mActivity, tag); } else { return new CompatTabEclair(mActivity, tag); } } ... }

逻辑非常直白:

  • createInstance(FragmentActivity)—— 静态工厂方法,根据 SDK 版本返回TabHelperHoneycomb或TabHelperEclair;
  • newTab(String tag)—— 同样按版本返回对应版本的CompatTab,且该方法不标记为 abstract,而是由抽象基类统一实现,保证两个分支创建的 tab 类型始终与 TabHelper 类型匹配(这正是上一课addTab()中"向下转型"能安全成立的原因)。

由于真正的新 API 类(CompatTabHoneycomb、TabHelperHoneycomb)只会在分支为真时才被实例化,结合第二课讲到的延迟类加载机制,旧设备上不会触发VerifyError。

创建能感知版本的 Activity 布局

两个实现需要不同的界面布局,因此要为res/layout/与res/layout-v11/分别提供一份main.xml。

旧版本布局(API 等级 5~10 使用),必须包含TabHost、TabWidget以及承载 tab 内容的容器:

res/layout/main.xml:

<!-- This layout is for API level 5-10 only. --> <TabHost xmlns:android="http://schemas.android.com/apk/res/android" android:id="@android:id/tabhost" android:layout_width="match_parent" android:layout_height="match_parent"> <LinearLayout android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="5dp"> <TabWidget android:id="@android:id/tabs" android:layout_width="match_parent" android:layout_height="wrap_content" /> <FrameLayout android:id="@android:id/tabcontent" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" /> </LinearLayout> </TabHost>

新版本布局(API 等级 11+ 使用),只需要一个FrameLayout内容容器即可——因为ActionBar自身已经提供了 tab 栏的渲染:

res/layout-v11/main.xml:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@android:id/tabcontent" android:layout_width="match_parent" android:layout_height="match_parent" />

两个布局都使用了系统内置资源 ID:@android:id/tabhost、@android:id/tabs、@android:id/tabcontent——这正是TabHelperEclair.setUp()中findViewById(android.R.id.tabhost)与 tab 内容切换所依赖的约定 ID。运行期间,Android 资源系统会根据平台版本自动选择layout还是layout-v11中的main.xml,与上一节选择TabHelper具体实现的逻辑如出一辙:把"版本适配"同时落实在代码层与资源层。

在 Activity 中使用 TabHelper

最后,在 Activity 的onCreate()中获取TabHelper并添加 tabs。对业务代码来说,整个过程完全透明:

@Override public void onCreate(Bundle savedInstanceState) { setContentView(R.layout.main); TabHelper tabHelper = TabHelper.createInstance(this); tabHelper.setUp(); CompatTab photosTab = tabHelper .newTab("photos") .setText(R.string.tab_photos); tabHelper.addTab(photosTab); CompatTab videosTab = tabHelper .newTab("videos") .setText(R.string.tab_videos); tabHelper.addTab(videosTab); }

这段代码演示了完整的调用链:

  1. setContentView(R.layout.main)—— 资源系统按版本加载对应布局;
  2. TabHelper.createInstance(this)—— 工厂按 SDK 版本返回TabHelperHoneycomb或TabHelperEclair;
  3. tabHelper.setUp()—— 初始化 ActionBar 导航模式或 TabHost;
  4. tabHelper.newTab("photos").setText(...)—— 通过基类的newTab()创建对应版本的CompatTab,并用流式 API 设置文案;
  5. tabHelper.addTab(...)—— 把 tab 加入 ActionBar 或 TabHost。

运行时,设备会自动显示对应的界面布局、实例化对应的实现对象;而具体使用哪个类,对 Activity 来说是不可见的(opaque),因为它们共享同一个TabHelper/CompatTab接口。以下是同一套代码在 Android 2.3 与 Android 4.0 上的运行效果:

从两张截图可以直观看到:同样的Tab Demo应用,在 2.3 设备上渲染为TabWidget风格的页签,在 4.0 设备上则由 ActionBar 原生渲染——功能一致,观感随平台"自适应"。

模式总结:五步复用的向后兼容设计流程

回顾整条课程脉络,这套"向后兼容 UI"方法论可以提炼为可复用的五步:

步骤核心动作产出
1. 界定需求列出应用需要的功能与对应新 API功能清单、抽象范围
2. 抽象镜像新 API 的 public 接口,创建抽象类CompatTab、TabHelper
3. 新版代理在新 API 基础上做方法转发CompatTabHoneycomb、TabHelperHoneycomb
4. 旧版实现用旧 API 实现同等功能CompatTabEclair、TabHelperEclair
5. 版本分发工厂方法 + 版本化资源目录,运行时选择createInstance()、layout-v11/

贯穿始终的三个技术支柱是:

  • 抽象接口镜像:抽象类尽可能完整地镜像新 API,保证前向兼容、便于未来废弃;
  • 延迟类加载:只要不在旧设备上实例化新实现,Dalvik 就不会抛VerifyError,这是新旧代码能共存的安全前提;
  • 资源版本化:res/layout-v11/等限定符目录让布局与代码的版本判断相互配合,共同完成"能感知版本"的组件设计。

仓库导读与延伸阅读

本篇课程位于本仓库的 ui/backward-compatible-ui/ 目录下,按阅读顺序包含五篇文档与五张配套图片(三张类结构图、两张运行截图),并在 SUMMARY.md 中被组织为"UI"分类下的一个完整小节。从仓库结构看,课程原文还提供了配套示例工程(TabCompat 示例 zip),感兴趣的读者可结合文档中的类结构图自行推演实现。

延伸学习建议:

  • 仓库中 ui/backward-compatible-ui/index.md 给出了课程总览,abstract.md、new-impl.md、older-impl.md、using-component.md 四篇分别对应本篇文章的四课;
  • 与"兼容性"主题相关的还有 basics/supporting-devices/index.md(兼容不同设备)、ui/multiscreen/index.md(兼容不同屏幕)以及 material/compatibility.md(Material Design 的兼容性方案),它们与本文共同构成了"一次编写、处处适配"的完整知识面;
  • 本文介绍的TabHelper工厂模式,其思想与 Android 官方后来力推的Support Library(如FragmentActivity、ViewPager)一脉相承——后者本质上就是把"版本感知"下沉到框架层,让开发者连工厂方法都不必自己写。理解本文的抽象层原理,将帮助你更好地理解 Support Library 的设计动机与内部机制。

把本文的 Tab 案例替换为你应用真正需要的新组件,重复"抽象 → 新版代理 → 旧版实现 → 版本分发"四步,即可让应用从容跨越 Android 平台的多个版本断层。

  • 文档
  • 教程
  • 移动开发

【免费下载链接】android-training-course-in-chinese

Android官方培训课程中文版

项目地址:https://gitcode.com/gh_mirrors/an/android-training-course-in-chinese
点击查看免费下载

相关推荐

上一篇:蓝牙水控器开源控制:革命性智能热水管理方案
下一篇:Dr. Memory系统调用追踪:Windows平台上的strace终极替代方案

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!