安卓自学——ViewPager与FragmentTabHost实现拖动翻页

使用ViewPager 和 FragmentTabHost 实现滑动标签页翻动

示例效果:
实现翻页效果

主要思路分为两个方面:
1. ViewPager 实现左右拖动切换 Fragment,FragmentTabHost 点击底部按钮切换 Fragment;
2. 将 ViewPager 的翻页动作与 FragmentTabHost 的页面切换进行关联,反过来又将 FragmentTabHost 的点击切换与 ViewPager 的翻页进行关联,这样就能实现点击和拖拽翻页的同步了;

后面会有详细代码,demo链接:https://github.com/hry712/Android_ViewPager_FragmentTabHost_Demo.git

一、主界面layout

布局如下:

    <?xml version="1.0" encoding="utf-8"?>
    <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 实现拖动切换 Fragment 并与 FragmentTabHost 进行关联

需要为 ViewPager 自定义 adapter ,用以装填要切换的 Fragment ,并根据拖动事件的触发返回相应的 Fragment,自定义 adapter 继承自FragmentPagerAdapter(谷歌官方推荐使用提供的标准FragmentPagerAdapterFragmentStatePagerAdapter,后者适合于标签页较多的情况),代码如下:

    public class MyFragmentAdapter extends FragmentPagerAdapter {
        // 在 MainActivity 中会初始化各个 Fragment 构成列表一并传入到 adapter 中处理
        private List<Fragment> fragments;

        public MyFragmentAdapter(FragmentManager fm, List<Fragment> 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);
    }

三、FragmentTabHost 点击切换 Fragment 并与 ViewPager 关联

“绑定”也许并不准确,实际上是在一个接口方法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<Fragment> 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简单的纵向排列:

<?xml version="1.0" encoding="utf-8"?>
<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 绑定此配置(配置文件中已经引用了图片资源):

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <!--Non focused states-->
    <item android:state_focused="false"
        android:state_selected="false"
        android:state_pressed="false"
        android:drawable="@drawable/index_unselected" />
    <!--Focused states-->
    <item android:state_focused="true"
        android:state_selected="false"
        android:state_pressed="false"
        android:drawable="@drawable/index_selected" />
    <!--Pressed-->
    <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 再根据需要进行各种界面绘制,示例中只包含了一个 <TextView> 用来显示文字。

补充

在效果图中可以看到label颜色并没有随着Tab的切换,而切换到与icon一致的颜色,label的颜色切换与icon类似,先在 /res/drawable/ 目录下创建一个相应的 XX_selector.xml ,然后在 <TextView> 控件中设置属性 android:textColor 属性值为 @drawable/XX_selector。label 的 selector.xml 配置示例如下:

<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- Non focused states -->
    <!-- 在 res/values/colors.xml 中label颜色在未选中时为黑色 -->
    <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" />
    <!-- Focused states -->
    <!-- 在 res/values/colors.xml 中label颜色在选中时为红色 -->
    <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" />
    <!-- Pressed -->
    <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 中供这里调用

  • 5
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值