1. Key widgets
Tab是Android支持的一种UI布局,Android里面的原生应用Music就是用Tab的方式实现的,API中也有封装的比较好的TabActivity,但是在后来的版本中3.0以后的版本它就是Deprecated的了。因为Tab被绑定到ActionBar上面了。但是我们还是可以按照TabActivity的实现方式来自己实现多Tab效果。现在很多应用都有仿制iOS的底层Tab的那种,这是违反Android设计规范的,但是仍有大把大把的应用在这么干,无论是用户量巨大的如微信,随手记等,还有不计其数的应用。其实要实现Tab在下面,上面有Header导航的UI一点都不难,用TabHost和TabWidget就可以办到。
这个布局就可以实现Tab在Bottom上。
效果:
代码:
<?xml version="1.0" encoding="utf-8"?>
<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:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<FrameLayout
android:id="@android:id/tabcontent"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1" >
</FrameLayout>
<TabWidget
android:id="@android:id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="-4dp"
android:layout_weight="0" >
</TabWidget>
</LinearLayout>
</TabHost>
2. Make it right
现在看起来还不是很像iOS那种感觉,主要原因就是每个Tab的Indicator还是Android的,这个也需要再定制一下,用一个LinearLayout来当做布局,里面是一个ImageView作为Icon,另一个TextView做为它的Title,这样看起来就像那么回事了。
效果:
代码:
layout/tab_indicator.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:background="@drawable/tab_indicator_bg"
android:orientation="vertical" >
<ImageView android:id="@+id/tab_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="3dp"
android:layout_gravity="center_horizontal"/>
<TextView android:id="@+id/tab_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/tab_title_style"
android:singleLine="true"/>
</LinearLayout>
Java:
for (final Tab tab : mTabs) {
TabSpec spec = tabHost.newTabSpec(tab.mTag);
View indicator = mFactory.inflate(R.layout.tab_indicator, null, false);
ImageView icon = (ImageView) indicator.findViewById(R.id.tab_icon);
icon.setImageResource(tab.mIcon);
TextView title = (TextView) indicator.findViewById(R.id.tab_title);
title.setText(tab.mTitle);
spec.setIndicator(indicator);
spec.setContent(new TabContentFactory() {
@Override
public View createTabContent(String tag) {
TextView tv = new TextView(getActivity());
tv.setText(tab.mTitle);
return tv;
}
});
tabHost.addTab(spec);
}
3. Fill tab content with Fragment
光有Tab还没有用,用Tab的目录就是把一组类似且平行的内容展示出来,每个Tab的内容可以用一个Fragment来实现。Fragment是没有办法直接加入到TabHost当中的,但是可以先在TabHost中加入一些ViewGroup作为Tab内容的Stub,然后再把每个Stub替换成Fragment即可。
4. Adding Header bar
对于Header bar,没有办法使用Android标准的ActionBar。简单的方法就是用一个布局来实现:一个LinearLayout包括Title和Icon就可以了,然后每个Fragment再在布局中Include进来就可以了。
5. Styling
这个是比较关键的一步,前面都是形似而非神似,要想真正看起来跟iOS差不多,还必须进行一些风格上的调整。背景色和选中的状态都可以用ShapDrawable来定义。当然要想做到跟iOS一模一样,还是需要专门制作图片。
最终效果:
6. 疑难杂症
Tab内容加载Fragment的方式通常是监听TabChange,当切换到某个Tab的时候再创建其相应的Fragment然后再添加到FragmentManager中。但是这样会有一个问题就是第一次切换到某个Tab是会有闪烁,原因就是,是第一次切换到Tab时,才要创建Fragment,再添加到Stack里面,这会有一定的时间,因此在显示出新Tab页前会有一小段视觉残留在原来的界面上,会感觉到闪。可以添加动画,让Fragment加入是一个动态过程,但这会带来新的问题,就是当Fragment已经加入了后,再次切换Tab,它是不会重新创建的,因此就没有动画了。Okay,可以加入之前先把老的Remove掉,但是是始终看起来是有问题。
另一种方法就是在Activity创建的时候就把所有Fragment创建并加入到Tab中去。这样当切换到某个Tab时,Fragment已经加载完了,就感觉不到闪了。但其实这并不是好的方法,因为某个Tab涉及的数据量比较大时就可以会有问题,用户使用过程中并不是每个Tab都会访问到。
6. Play with Fragments
a. when to replace and when to add when FragmentTransaction
When a Fragment is the root of a stack of fragments, you should replace the container view group; otherwise, you should add it stack. In our example, for instance, for each content of tab, the framelayout, should be replaced by fragment; But if adding next level fragment to a tab, we should add that fragment to root of each tab.
b. Fragment into Fragment
通常情况下,以XML的方式,Fragment是不能嵌套的,如果指定了Id或者Tag,比如:
布局A:
<fragment class="com.example.CategoryFragment"...../>
Activity加载了布局A;
布局B:
<fagment class="com.example.DetailsFragment"
android:id="@+id/details" ..../>
但,CategoryFragment又加载布局B,
一切都正常,但是当从Activity进入了DetailsFragment后,退出再次进入DetailsFragment时就会有如下异常抛出:
原因在于Infalte布局的时候,会按<fragment>生成一个Fragment对象,并加入到Fragment Stack中,如果指定了Id或者Tag,就会把Id或者Tag作为Fragment的属性,然后加入;Fragment对象一旦创建并加入了Stack,对象并不一定会被销毁,这是为了下次使用更加方便.那么当再次Infalte布局的时候,遇到<fragment>会再次创建一个Fragment对象,并尝试加入.因为先前加入的对象仍然存在,所以会报出Duplicate Id和Tag的异常.但如果不指定Id或Tag,就不会有异常.因为没有Id和Tag就像匿名一样,既然是没有标识的Fragment,自然也就不存在Duplicate的问题.
StackOverflow有一些讨论.
http://stackoverflow.com/questions/6847460/fragments-within-fragments
http://stackoverflow.com/questions/14083950/duplicate-id-tag-null-or-parent-id-with-another-fragment-for-com-google-androi
有一些方法,比如在Fragment退出时,或者什么时候把先前的Fragment移除掉(Remove),但是效果都不理想.
最好的方式就是,动态创建和使用Fragment.因为既然为Fragment加上了Id和Tag,证明需要在代码中访问和获取Fragment对象,既然这样,为什么不直接在代码中创建Fragment对象呢.在代码中创建,就可以先判断下,如果Fragment池中已有此Id或得Tag(FragmentManager#getFragmentById() FragmentManager#getFragmentTag())就不需要创建和添加.此为最好解法.
c. lesson learned
1. Only include <fragment> in layout files when you do not need to access the Fragment object. Otherwise, put a Viewgroup Stub into layout, and create and add Fragment object dynamically.
2. When Fragment is root screen of your stack, replace the view group container with Fragment; otherwise add it to stack.
代码是在: https://github.com/alexhilton/vulcan
一些有用的链接:
http://stackoverflow.com/questions/13408709/android-tabs-at-the-bottom-with-fragmenttabhost
http://stackoverflow.com/questions/2395661/android-tabs-at-the-bottom
http://android.codeandmagic.org/2011/07/android-tabs-with-fragments/
http://www.cnblogs.com/yuyb1990/archive/2013/03/10/2952981.html
https://github.com/mta452/iTab
http://joshclemm.com/blog/?p=136