假设你想在你的应用中用action bar tabs作为顶级导航的基本形式,不幸的是,ActionBar APIs只能在 Android 3.0 或以后的版本(API Level 11+)中支持。因此,如果的你应用想在较早版本的平台上运行的话,你的应用的实现方式需要在支持新版本的API的同时也要支持老版本API。
在这节课程中,我们构建一个标签用户接口组件,这是一个可以用指定方式实现的向后兼容的抽象类。
在Java编程语言中,抽象化是指一个或多个接口,或者是抽象类隐藏了他们的具体实现。对于较新版本的Android平台,你可以利用抽象化来构建可以识别版本的组件,在新版本的设备上使用用新版本的API实现。
在使用这种方法时,你首先要确定你要实现向后兼容的新版本类,然后基于这个新类的公共接口创建一个抽象类。在定义抽象类(或接口)时,你应该尽可能多的与新的接口所定义的方法保持一致,最大限度的向前兼容,在将来不需要这个抽象层时也能够轻易的去掉。
这些为新的API构建的抽象类创建以后,在运行时我们可以创建和选择任意数量的实现类。为了达到向后兼容的目的,这些实现类能够根据API Level进行判断,因此,应用中的实现方式可以依据API Level来选择使用最近发布的API,或者使用较早版本的API。
为了创建一个向后兼容的Tabs版本,你首先要考虑在你的应用中需要哪些特性和API。对于顶层的Tabs,假设你的应用有如下的功能需求:
1.Tab显示需要显示文本和图标
2.这些Tab能够与fragment实例联合在一起使用
3.activity能够监听tab发生变化时的事件
事先定义好这些需求可以帮助你控制抽象层所实现的范围,这就意味着你能很快的创建一个新的实现并且很快的使用这些向后兼容的实现方法。 ActionBar 和 ActionBar.Tab 是Tabs两个重要的APIs,为了能够做到版本识别(Version-Aware),这些APIs是需要抽象的。 这个示例工程需要向后兼容Eclair (API level 5)并且还应该具有Honeycomb (API Level 11)的新的优点和特性,下图是满足这个需求的类结构图:
下面开始构建你的抽象层,创建一个抽象类来显示一个Tab,这个抽象类的功能类似 ActionBar.Tab 接口:
public abstract class CompatTab { public abstract CompatTab setText(int resId); public abstract Object getTab(); public abstract CharSequence getText(); /** * 下面也可以实现一些比较复杂的tab */ /* 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();*/ }
接下来,我们需要创建一个抽象类来将Tabs创建和添加到一个activity,就像 ActionBar.newTab() 和ActionBar.addTab():
public abstract class TabHelper { // Usage is TabHelper.createInstance(activity) public static TabHelper createInstance(Activity activity) { } public abstract void addTab(CompatTab tab); public abstract void setUp(); public abstract CompatTab newTab(String tag); }
用新版本API实现CompatTab和TabHelper实际上是一种代理模式的实现,因为上面创建的抽象类映射了新的APIs(类结构,方法语句等.),实现类只需要简单的代理新APIs中的方法和返回结果。
我们可以直接使用这些新的APIs,因为有类的延迟加载,他们在旧版本中不会产生崩溃。类在第一次访问实例化时,或者类的静态属性或方法第一次被访问时才会加载并初始化。因此,只要我们没有在Honeycomb之前版本的设备上实例化Honeycomb的特定实现,Dalvik 虚拟机就不会抛出任何VerifyError异常。
对于这种实现方法一个比较好的命名规范是,把API Level或者与API一致的平台版本名字添加在类名中,例如本地化的实现类CompatTabHoneycomb 和TabHelperHoneycomb ,因为他们依赖于在Android 3.0 (API level 11) 或者更新的版本中使用的APIs。
CompatTabHoneycomb是抽象类CompatTab的实现,TabHelperHoneycomb用来引用单个Tabs,CompatTabHoneycomb简单的代理ActionBar.Tab的所有调用方法。
下面开始使用ActionBar.Tab的API实现CompatTabHoneycomb:
public class CompatTabHoneycomb extends CompatTab { // The native tab object that this CompatTab acts as a proxy for. ActionBar.Tab mTab; @Override public CharSequence getText() { return null; } protected CompatTabHoneycomb(Activity activity, String tag) { // Proxy to new ActionBar.newTab API mTab = activity.getActionBar().newTab(); mTab.setText(tag); //下面这个也应该封装一下的,现在懒得做了,先这样吧。 mTab.setTabListener(new ActionBar.TabListener() { @Override public void onTabSelected(ActionBar.Tab tab, FragmentTransaction ft) { } @Override public void onTabUnselected(ActionBar.Tab tab, FragmentTransaction ft) { } @Override public void onTabReselected(ActionBar.Tab tab, FragmentTransaction ft) { } }); } public CompatTab setText(int resId) { // Proxy to new ActionBar.Tab.setText API mTab.setText(resId); return this; } @Override public Object getTab() { return mTab; } // Do the same for other properties (icon, callback, etc.) }
为了保持向后兼容,我们需要用老版本平台的UI框架特性来实现新本版的UI框架特性,这是一件非常具有挑战性的任务。在很多情况下,我们是完全可以做到这一点的,请看下边的例子:
Action bars能够用一个 horizontal LinearLayout在你Activity Layout中实现,这个LinearLayout可以添加一个自定义的标题或者是Views,加上image buttons,执行的动作可以在设备的Menu button来显示。
Action bar tabs 可以用 horizontal LinearLayout 加上按钮或者用TabWidget UI来实现。
NumberPicker和Switch部件可以用Spinner和ToggleButton部件分别实现。
ListPopupWindow和PopupMenu部件可以用PopupWindow部件来实现。
一般情况下,我们无法找到一个完美的方案,可以把新UI组件的完全的移植到旧版本的设备上。在这个问题上我们应该多考虑一下用户体验,使用老版本设备的用户可能对新版本的设计模式并不熟悉。因此我们在实现的时候要考虑相同的功能实现尽量用用户熟悉的方式来实现。很多情况下我们不必过于担心这个问题,如果这个新的UI组件在应用程序的环境中设计的比较优秀(比如:Action Bar)或者交互模式非常简单和直观的(比如:Swip Views应用 ViewPager)。
用较早版本API实现Tabs
我们可以用TabWidget和TabHost(我们也可以用horizontally laid-out Button部件)来实现ActionBar标签。因为我们使用了Android 2.0 (Eclair)以下的APIs,所以我们的实现类名字叫做TabHelperEclair和CompatTabEclair:
实现CompatTabEclair时需要在变量中保存tab的属性,比如:text、icon等。因为我们已经没有现成的ActionBar.Tab可以帮助来处理这些属性了。
public class CompatTabEclair extends CompatTab { protected Activity mActivity; private String mTag; public CompatTabEclair(Activity activity,String tag){ this.mActivity=activity; this.mTag=tag; } // 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; } @Override public Object getTab() { return mTag; } public CharSequence getText(){ return mText; } // Do the same for other properties (icon, callback, etc.) }
现在我们有CompatTab和TabHelper的两个实现类了,一个用在Android 3.0或者更新的版本的APIs中,一个用在Android 2.0或更新版本的APIs中,下面我们将讨论如何创建这两个实现类的切换逻辑,创建可以做到版本识别的布局,并最终使用这些向后兼容的UI组件。
基于当前的设备版本,TabHelper抽象类扮演着创建TabHelper和CompatTab实例工厂的角色:
public abstract class TabHelper { // Usage is TabHelper.createInstance(activity) public static TabHelper createInstance(Activity activity) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { return new TabHelperHoneycomb(activity); } else { return new TabHelperEclair(activity); } } public abstract void addTab(CompatTab tab); public abstract void setUp(); public abstract CompatTab newTab(String tag); }
创建一个版本识别的Activity Layout
下一步是在你的Activity中提供两个分别支持两种标签实现方式的布局,对于老版本(TabHelperEclair)的实现方式,我们需要在布局中包含TabWidget和TabHost,作为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>
对于TabHelperHoneycomb的实现,我们需要用到FrameLayout来放置tab的内容,因为tab的指示器是由ActionBar提供的:
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" />
在运行的时候,Android根据平台的版本,决定来展示main.xml中的哪一个布局,这里的逻辑跟上段中TabHelper选择实现类的逻辑是相同的。
在Activity中使用TabHelper
在Activity的onCreate()方法里,我们可以获得一TabHelper对象,然后添加标签,代码如下:
@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); }
当运行应用时,这些代码会调用相应的布局,实例化TabHelperHoneycomb或TabHelperEclair相应的对象。实际中使用的类对于Activity来讲是透明的,因为他们都是来自共同的接口TabHelper.
下边是运行在Android 2.3和Android 4.0设备上的两张截图:
Demo地址:https://github.com/xuwt/CompatibleTab