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

来源:互联网 发布:m1协同软件 编辑:程序博客网 时间:2024/06/01 23:46

使用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 中供这里调用

1 0
原创粉丝点击