主要思路分为两个方面:
1. ViewPager 实现左右拖动切换 Fragment,FragmentTabHost 点击底部按钮切换 Fragment;
2. 将 ViewPager 的翻页动作与 FragmentTabHost 的页面切换进行关联,反过来又将 FragmentTabHost 的点击切换与 ViewPager 的翻页进行关联,这样就能实现点击和拖拽翻页的同步了;
后面会有详细代码,demo链接:https://github.com/hry712/Android_ViewPager_FragmentTabHost_Demo.git
布局如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.geekschoole.waimai.controllers.MainActivity">
<android.support.v4.view.ViewPager
android:id="@+id/pager_fragments"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
android.support.v4.view.ViewPager>
<FrameLayout
android:id="@+id/frame_tabContent"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">FrameLayout>
<android.support.v4.app.FragmentTabHost
android:id="@+id/tabhost_pages"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android.support.v4.app.FragmentTabHost>
LinearLayout>
需要为 ViewPager 自定义 adapter ,用以装填要切换的 Fragment ,并根据拖动事件的触发返回相应的 Fragment,自定义 adapter 继承自FragmentPagerAdapter
(谷歌官方推荐使用提供的标准FragmentPagerAdapter
或FragmentStatePagerAdapter
,后者适合于标签页较多的情况),代码如下:
public class MyFragmentAdapter extends FragmentPagerAdapter {
// 在 MainActivity 中会初始化各个 Fragment 构成列表一并传入到 adapter 中处理
private List fragments;
public MyFragmentAdapter(FragmentManager fm, List fragments) {
super(fm);
this.fragments = fragments;
}
// 官方文档中介绍只需重载 getItem 和 getCount 即可使用
// 该方法返回一个与特定位置相关的 Fragment
@Override
public Fragment getItem(int position) {
return fragments.get(position);
}
// 返回可用视图的总数
@Override
public int getCount() {
return fragments.size();
}
}
MainActivity.class
中创建 ViewPager 的代码如下:
pager = (ViewPager) findViewById(R.id.pager_fragments);
// fragmentList 是包括了已初始化并要进行切换的 Fragment 列表
pager.setAdapter(new MyFragmentAdapter(getSupportFragmentManager(),
fragmentList));
为了响应拖动切换事件,MainActivity
需实现 ViewPager.OnPageChangeListener
接口,其下3个接口方法实现如下,注意到在onPageSelected()
中, ViewPager 的切换引起 FragmentTabHost 同步切换也是在此实现:
// 当滚动状态发生改变时调用,特别适合在用户开始拖动时触发
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
// 当前页滚动时会调用此方法
@Override
public void onPageScrollStateChanged(int state) {
}
// 当新页面变为选中状态时会调用此方法
@Override
public void onPageSelected(int position) {
TabWidget widget = fragmentTabHost.getTabWidget();
// 在查找取得焦点的view时,descendant focusability定义了view group与其后代的联系
int oldFocusability = widget.getDescendantFocusability();
widget.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
// 这里关联到 fragmentTabHost 一起切换
fragmentTabHost.setCurrentTab(position);
widget.setDescendantFocusability(oldFocusability);
}
“绑定”也许并不准确,实际上是在一个接口方法onTabChanged()
(接口为 FragmentTabHost.OnTabChangeListener
)中令 ViewPager 的当前页与 FragmentTabHost 切换时同步改变,实现“绑定”作用。
在 MainActivity 中初始化 FragmentTabHost:
// 下面都是在准备 FragmentTabHost 的创建
fragmentTabHost = (FragmentTabHost);
findViewById(R.id.tabhost_pages);
// 要求 MainActivity 实现 FragmentTabHost.OnTabChangeListener 接口的 onTabChanged 方法
fragmentTabHost.setOnTabChangedListener(this);
// 官方文档中要求在从视图层完成inflate后,必须调用setup方法继续完成FragmentTabHost初始化
fragmentTabHost.setup(this, getSupportFragmentManager(), R.id.frame_tabContent);
// 至此,FragmentTabHost 已经创建完成,下面要向其装填底部栏的几个按钮
// fragmentArr[] 中保存了自定义的几个 Fragment 类用作 Tab 页
int count = fragmentsArr.length;
for (int i = 0; i < count; i++) {
// 使用了自定义的 getTabItemViewById() 方法
// 这里的 TabSpec 设置了 label 和 icon,icon的生成封装在了 getTabItemViewById() 中
TabHost.TabSpec tabSpec = fragmentTabHost.newTabSpec(TabNameArr[i]).setIndicator(getTabItemViewById(i));
// 将底部按钮与 fragment 关联起来
fragmentTabHost.addTab(tabSpec, fragmentsArr[i], null);
fragmentTabHost.getTabWidget().getChildAt(i).setBackgroundResource(R.drawable.bottom_switcher);
}
实现 FragmentTabHost.OnTabChangeListener
接口的 onTabChanged()
方法如下:
@Override
public void onTabChanged(String s) {
// 通过这个方法令 fragmentTabHost 触发 ViewPager 同步变化
pager.setCurrentItem(fragmentTabHost.getCurrentTab());
}
一个Tab页包含一个 Tab 指示器,content,用于跟踪它的 tag,TabSpec 就是用来选择这些内容。
Tab指示器有两种形式:
1. 设置一个 label
2. 设置一个 label 和 icon
Tab 内容有3种:
1. View的id
2. 创建视图内容的 TabHost.TabContentFactory
3. 启动 Activity 的 Intent
自定义的 getTabItemViewById()
方法如下:
// 解析单个Tab页按钮的XML布局,将icon和label的具体内容依次装填进去生成一个新的view供 TabSpec 使用
private View getTabItemViewById(int index) {
// bottom_tab_switcher.xml 是每个标签页下对应的图标和文字组合的小布局
View view = layoutInflater.inflate(R.layout.bottom_tab_switcher, null);
ImageView imageViewTabIcon = (ImageView) view.findViewById(R.id.imgvw_bottom_tabIcon);
// index 变量布局用于索引预制在数组变量中的tab命名字符串和图片点击动作响应xml文件
imageViewTabIcon.setImageResource(ImageViewArr[index]);
TextView textViewTabName = (TextView) view.findViewById(R.id.tv_bottom_tabText);
textViewTabName.setText(TabNameArr[index]);
return view;
}
MainActivity.class
实现 onTabChanged()
接口方法如下:
// 当标签页切换时会调用此方法
@Override
public void onTabChanged(String s) {
// viewpager 的 setCurrentItem 方法用于设置当前选中页面
pager.setCurrentItem(fragmentTabHost.getCurrentTab());
}
MainActivity.class
完整代码如下:
package com.geekschoole.waimai.controllers;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentTabHost;
import android.support.v4.view.ViewPager;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TabHost;
import android.widget.TabWidget;
import android.widget.TextView;
import com.geekschoole.waimai.views.IndexFragment;
import com.geekschoole.waimai.views.OrderFragment;
import com.geekschoole.waimai.R;
import com.geekschoole.waimai.views.UserFragment;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends AppCompatActivity
implements ViewPager.OnPageChangeListener, FragmentTabHost.OnTabChangeListener{
private FragmentTabHost fragmentTabHost;
private LayoutInflater layoutInflater;
private Class fragmentsArr[] = {IndexFragment.class, OrderFragment.class, UserFragment.class};
private int ImageViewArr[] = {R.drawable.bottom_index_tab_selector,
R.drawable.bottom_order_tab_selector,
R.drawable.bottom_user_tab_selector};
private String TabNameArr[] = {"Index", "Order", "User"};
private List fragmentList = new ArrayList<>();
private ViewPager pager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 控件初始化,并将 ViewPager 与 FragmentTabHost 进行绑定
initView();
// 创建3个Fragment,通过Adapter添加到 ViewPager 中作为Tab页
initTabs();
}
private void initView() {
layoutInflater = LayoutInflater.from(this);
pager = (ViewPager) findViewById(R.id.pager_fragments);
pager.addOnPageChangeListener(this);
fragmentTabHost = (FragmentTabHost) findViewById(R.id.tabhost_pages);
fragmentTabHost.setOnTabChangedListener(this);
fragmentTabHost.setup(this, getSupportFragmentManager(), R.id.frame_tabContent);
int count = fragmentsArr.length;
for (int i = 0; i < count; i++) {
TabHost.TabSpec tabSpec = fragmentTabHost.newTabSpec(TabNameArr[i]).setIndicator(getTabItemViewById(i));
fragmentTabHost.addTab(tabSpec, fragmentsArr[i], null);
fragmentTabHost.getTabWidget().getChildAt(i).setBackgroundResource(R.drawable.bottom_switcher);
}
}
private View getTabItemViewById(int index) {
View view = layoutInflater.inflate(R.layout.bottom_tab_switcher, null);
ImageView imageViewTabIcon = (ImageView) view.findViewById(R.id.imgvw_bottom_tabIcon);
imageViewTabIcon.setImageResource(ImageViewArr[index]);
TextView textViewTabName = (TextView) view.findViewById(R.id.tv_bottom_tabText);
textViewTabName.setText(TabNameArr[index]);
return view;
}
private void initTabs() {
// 这里的添加顺序对 tab 页的先后顺序有影响
fragmentList.add(new IndexFragment());
fragmentList.add(new OrderFragment());
fragmentList.add(new UserFragment());
pager.setAdapter(new MyFragmentAdapter(getSupportFragmentManager(),
fragmentList));
fragmentTabHost.getTabWidget().setDividerDrawable(null);
}
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
@Override
public void onPageScrollStateChanged(int state) {
}
@Override
public void onPageSelected(int position) {
TabWidget widget = fragmentTabHost.getTabWidget();
int oldFocusability = widget.getDescendantFocusability();
widget.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
fragmentTabHost.setCurrentTab(position);
widget.setDescendantFocusability(oldFocusability);
}
@Override
public void onTabChanged(String s) {
pager.setCurrentItem(fragmentTabHost.getCurrentTab());
}
}
底部单个Tab图标和label的组合布局 bottom_tab_switcher.xml
如下(位于 res/layout/ 中),就是一个icon和一个label简单的纵向排列:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center">
<ImageView
android:id="@+id/imgvw_bottom_tabIcon"
android:layout_width="30dp"
android:layout_height="30dp"
android:focusable="false"
android:padding="3dp"/>
<TextView
android:id="@+id/tv_bottom_tabText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Index"
android:textSize="10sp"
/>
LinearLayout>
底部每个 icon 点击时的图片切换配置bottom_index_tab_selector.xml
示例如下,需事先为每个图标准备一套在选中和未选中时的 icon 图片资源放置于 res/drawable/drawable-XXXdpi 下,在 getTabItemViewById()
方法的 imageViewTabIcon.setImageResource(ImageViewArr[index]);
中会为每个 icon 绑定此配置(配置文件中已经引用了图片资源):
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_focused="false"
android:state_selected="false"
android:state_pressed="false"
android:drawable="@drawable/index_unselected" />
<item android:state_focused="true"
android:state_selected="false"
android:state_pressed="false"
android:drawable="@drawable/index_selected" />
<item android:state_selected="true"
android:state_pressed="true"
android:drawable="@drawable/index_selected" />
<item android:drawable="@drawable/index_selected" />
selector>
至于切换的几个 Tab ,里面使用的 Fragment 可自行创建空白或关联有xml 的 fragment 再根据需要进行各种界面绘制,示例中只包含了一个
用来显示文字。
在效果图中可以看到label颜色并没有随着Tab的切换,而切换到与icon一致的颜色,label的颜色切换与icon类似,先在 /res/drawable/ 目录下创建一个相应的 XX_selector.xml ,然后在
控件中设置属性 android:textColor 属性值为 @drawable/XX_selector。label 的 selector.xml 配置示例如下:
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_focused="false"
android:state_selected="false"
android:state_pressed="false"
android:color="@color/unselectedText" />
<item android:state_focused="false"
android:state_selected="true"
android:state_pressed="false"
android:color="@color/selectedText" />
<item android:state_focused="true"
android:state_selected="false"
android:state_pressed="false"
android:color="@color/selectedText" />
<item android:state_focused="true"
android:state_selected="true"
android:state_pressed="false"
android:color="@color/selectedText" />
<item android:state_selected="true"
android:state_pressed="true"
android:color="@color/selectedText" />
<item android:state_pressed="true" android:color="@color/selectedText" />
selector>
如果是在阿里图标库中找现成的图标,可事先选择图标的16进制颜色值并下载,将这个颜色值保存到 colors.xml 中供这里调用