Android 官方培训课程中文版:创建向后兼容的 UI —— 以 Action Bar Tabs 为例的抽象层实战
文档教程移动开发【免费下载链接】android-training-course-in-chineseAndroid官方培训课程中文版项目地址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.0API 等级 11引入的 [Action Bar Tabs] 为贯穿全篇的指导案例手把手带你走完抽象出新 API → 代理至新 API → 用旧 API 实现同等效果 → 运行时按版本选择实现的完整四步流程学完之后你将掌握一套可复用到 NumberPicker、Switch、ListPopupWindow、PopupMenu 等任何新组件的向后兼容设计模式。课程背景为什么需要向后兼容的 UI自 Android 系统持续迭代以来每个大版本都会带来一批全新的 UI 组件和 API。但新 API 往往只在较新的平台版本上可用——例如ActionBar及其ActionBar.Tab系列 API 只在 Android 3.0API 等级 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 接口。以顶层分段 Tabstop-level section tabs为例课程假设了以下三条功能需求显示图标和文本的 Tab 指示器Tabs 可以与一个 Fragment 实例相关联Activity 可以监听到 Tab 变化。提前准备这些需求能帮你控制抽象层的范围——范围越小需要创建的具体实现越少就能越快投入实际使用。针对上述需求需要抽象出来的核心 API 是ActionBar和ActionBar.Tab即前文类图中的CompatTab与TabHelper两个抽象类。示例项目的兼容目标为与 EclairAPI 等级 5保持一致同时利用 HoneycombAPI 等级 11中的新 Tab 功能。定义抽象类 CompatTabActionBar.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);定义抽象类 TabHelperActionBar 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为何能安全引用新 APICompatTab与TabHelper的新版具体子类本质上是一种代理proxy实现它们只负责把方法调用转发给真正的ActionBar.Tab/ActionBar对象。由于抽象类早已定义了与新 API 一致的类结构与方法签名这些子类几乎不需要任何业务逻辑。你可以在具体子类中直接引用新 API而不会在旧设备上崩溃原因是 Dalvik 虚拟机采用延迟类加载lazy class loading类只有在首次被访问实例化类对象、访问静态属性或调用静态方法时才会被加载和初始化。因此只要不在 Honeycomb 之前的设备上实例化 Honeycomb 相关实现虚拟机就不会抛出VerifyError异常。命名约定课程推荐了一个清晰的命名约定把具体子类依赖的 API 等级或版本名称附加在接口名之后。例如本地的 Tab 实现可以命名为CompatTabHoneycomb—— 依赖 Android 3.0API 等级 11及之后版本的 APITabHelperHoneycomb—— 同上。同理旧版实现命名为CompatTabEclair/TabHelperEclair。实现 CompatTabHoneycombCompatTabHoneycomb是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.TabsetText()则直接转发到mTab.setText(resId)并返回this以维持流式调用。其余属性icon、callback 等照此模式一一实现即可。实现 TabHelperHoneycombTabHelperHoneycomb是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 实现 TabsCompatTabEclair 与 TabHelperEclair回到 Tab 案例。旧实现可以基于TabWidget和TabHost构建当然用水平排列的Button也是备选对应类为TabHelperEclair和CompatTabEclair——它们的实现只依赖不迟于 Android 2.0Eclair的 API因此可以安全地运行在 API 等级 510 的设备上。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.0Honeycomb 代理一套面向 Android 2.0Eclair 旧实现。接下来就是最关键的一步——在运行时按平台版本做选择。第四课使用能感知版本的组件 —— 工厂切换与版本化布局添加切换逻辑把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或TabHelperEclairnewTab(String tag)—— 同样按版本返回对应版本的CompatTab且该方法不标记为 abstract而是由抽象基类统一实现保证两个分支创建的 tab 类型始终与 TabHelper 类型匹配这正是上一课addTab()中向下转型能安全成立的原因。由于真正的新 API 类CompatTabHoneycomb、TabHelperHoneycomb只会在分支为真时才被实例化结合第二课讲到的延迟类加载机制旧设备上不会触发VerifyError。创建能感知版本的 Activity 布局两个实现需要不同的界面布局因此要为res/layout/与res/layout-v11/分别提供一份main.xml。旧版本布局API 等级 510 使用必须包含TabHost、TabWidget以及承载 tab 内容的容器res/layout/main.xml!-- This layout is for API level 5-10 only. -- TabHost xmlns:androidhttp://schemas.android.com/apk/res/android android:idandroid:id/tabhost android:layout_widthmatch_parent android:layout_heightmatch_parent LinearLayout android:orientationvertical android:layout_widthmatch_parent android:layout_heightmatch_parent android:padding5dp TabWidget android:idandroid:id/tabs android:layout_widthmatch_parent android:layout_heightwrap_content / FrameLayout android:idandroid:id/tabcontent android:layout_widthmatch_parent android:layout_height0dp android:layout_weight1 / /LinearLayout /TabHost新版本布局API 等级 11 使用只需要一个FrameLayout内容容器即可——因为ActionBar自身已经提供了 tab 栏的渲染res/layout-v11/main.xmlFrameLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:idandroid:id/tabcontent android:layout_widthmatch_parent android:layout_heightmatch_parent /两个布局都使用了系统内置资源 IDandroid: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或TabHelperEclairtabHelper.setUp()—— 初始化 ActionBar 导航模式或 TabHosttabHelper.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、TabHelper3. 新版代理在新 API 基础上做方法转发CompatTabHoneycomb、TabHelperHoneycomb4. 旧版实现用旧 API 实现同等功能CompatTabEclair、TabHelperEclair5. 版本分发工厂方法 版本化资源目录运行时选择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.mdMaterial Design 的兼容性方案它们与本文共同构成了一次编写、处处适配的完整知识面本文介绍的TabHelper工厂模式其思想与 Android 官方后来力推的Support Library如FragmentActivity、ViewPager一脉相承——后者本质上就是把版本感知下沉到框架层让开发者连工厂方法都不必自己写。理解本文的抽象层原理将帮助你更好地理解 Support Library 的设计动机与内部机制。把本文的 Tab 案例替换为你应用真正需要的新组件重复抽象 → 新版代理 → 旧版实现 → 版本分发四步即可让应用从容跨越 Android 平台的多个版本断层。赞分享文档教程移动开发【免费下载链接】android-training-course-in-chineseAndroid官方培训课程中文版项目地址https://gitcode.com/gh_mirrors/an/android-training-course-in-chinese点击查看免费下载相关推荐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是文档教程移动开发上一篇蓝牙水控器开源控制革命性智能热水管理方案下一篇Dr. Memory系统调用追踪Windows平台上的strace终极替代方案创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考