- 文档
- 教程
- 移动开发
【免费下载链接】android-training-course-in-chinese
Android官方培训课程中文版
本篇文章基于《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。
具体操作分三步:
- 决定需要向后兼容的类:确定应用要使用哪些新类;
- 镜像新类的 public 接口:根据新类中的公共接口创建抽象类;
- 最大化前向兼容:在创建抽象接口时,应尽可能多地为新 API 创建镜像,这样未来当这些接口不再需要时,废弃它们会更容易。
抽象层建立之后,任何数量的实现都可以在运行时创建并按需选择。出于向后兼容的目的,这些实现可以按所需 API 级别划分:一个实现使用最新发布的 API,其余实现则使用较旧的 API。
创建抽象的 Tab 接口:先定义功能需求
在动手写代码之前,首先要决定应用需要哪些功能、依赖哪些具体的 API 接口。以"顶层分段 Tabs(top-level section tabs)"为例,课程假设了以下三条功能需求:
- 显示图标和文本的 Tab 指示器;
- Tabs 可以与一个 Fragment 实例相关联;
- 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); }这段代码演示了完整的调用链:
setContentView(R.layout.main)—— 资源系统按版本加载对应布局;TabHelper.createInstance(this)—— 工厂按 SDK 版本返回TabHelperHoneycomb或TabHelperEclair;tabHelper.setUp()—— 初始化 ActionBar 导航模式或 TabHost;tabHelper.newTab("photos").setText(...)—— 通过基类的newTab()创建对应版本的CompatTab,并用流式 API 设置文案;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官方培训课程中文版
相关推荐
CANN Runtime Kernel 加载与执行实战:从二进制加载到任务下发的完整流程(基于 0_launch_kernel 样例)
CANN Runtime Kernel 加载与执行实战:从二进制加载到任务下发的完整流程(基于 0_launch_kernel 样例) 本篇文章以 CANN R
文档教程移动开发Android官方培训课程中文版:为 Action Bar 添加操作按钮(Action Buttons)与响应事件完整指南
Android官方培训课程中文版:为 Action Bar 添加操作按钮(Action Buttons)与响应事件完整指南 本指南对应《Android官方培训课
文档教程移动开发Android官方培训课程中文版教程
Android官方培训课程中文版教程 1. 项目介绍 Android官方培训课程中文版(Android Training Course in Chinese)是
文档教程移动开发
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考