移动开发中很多界面是用一个Activity里面包含一个可滑动的头部及与之对应的Fragment来实现的,比如常见的新闻APP中新闻列表就是这样。头部是分类,分类对应的Fragment中展示新闻的列表。这种界面通常的做法是用Indicator+ViewPager的组合来实现。
效果图
引用的第三方库
Indicator引用了Github上JakeWharton的ViewPagerIndicator library
示例代码
1.Activity
package com.app.acoe.demo.activity;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.support.v4.view.ViewPager;
import android.widget.TextView;
import com.app.acoe.demo.R;
import com.app.acoe.demo.adapter.TabPagerAdapter;
import com.viewpagerindicator.TabPageIndicator;
/**
* @author Acoe
* @date 2016-3-24
* @version V1.0.0
*/
public class TabPagerDemoActivity extends FragmentActivity {
/**控件*/
private TextView txtTitle;
private TabPageIndicator indicator;
private ViewPager viewPager;
/**适配器*/
private TabPagerAdapter pagerAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.tab_pager_demo_activity);
initUI();
}
/**
* 初始化界面
*/
private void initUI() {
// 标题
txtTitle = (TextView) findViewById(R.id.title_textview);
txtTitle.setText("TabPager");
// 控件
indicator = (TabPageIndicator) findViewById(R.id.indicator);
viewPager = (ViewPager) findViewById(R.id.pager);
// 设置pager
viewPager.setOffscreenPageLimit(1);
pagerAdapter = new TabPagerAdapter(getSupportFragmentManager());
viewPager.setAdapter(pagerAdapter);
indicator.setViewPager(viewPager);
}
}
2.Activity的布局文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/main_bg_color"
android:orientation="vertical" >
<include layout="@layout/title_bar_view" />
<com.viewpagerindicator.TabPageIndicator
android:id="@+id/indicator"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:paddingLeft="15dip"
android:paddingRight="15dip" />
<android.support.v4.view.ViewPager
android:id="@+id/pager"
style="@style/scroll_style" />
</LinearLayout>
3.PagerAdapter代码
适配器继承于V4包的FragmentPagerAdapter
package com.app.acoe.demo.adapter;
import com.app.acoe.demo.fragment.TabPagerFragment;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
/**
* @author Acoe
* @date 2016-3-24
* @version V1.0.0
*/
public class TabPagerAdapter extends FragmentPagerAdapter {
private String lables[];
/**
* @param fm
*/
public TabPagerAdapter(FragmentManager fm) {
super(fm);
this.lables = new String[] { "第一页", "第二页", "第三页" };
}
@Override
public Fragment getItem(int position) {
return TabPagerFragment.getInstance(position);
}
@Override
public int getCount() {
return lables == null ? 0 : lables.length;
}
@Override
public CharSequence getPageTitle(int position) {
return lables[position];
}
}
4.Fragment代码
package com.app.acoe.demo.fragment;
import android.annotation.SuppressLint;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.app.acoe.demo.R;
/**
* @author Acoe
* @date 2016-3-24
* @version V1.0.0
*/
public class TabPagerFragment extends Fragment {
private int position;
private View nowView;
private TextView txtView;
private TabPagerFragment() {
}
public static TabPagerFragment getInstance(int position) {
TabPagerFragment fragment = new TabPagerFragment();
Bundle bundle = new Bundle();
bundle.putInt("position", position);
fragment.setArguments(bundle);
return fragment;
}
@SuppressLint("InflateParams")
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
nowView = inflater.inflate(R.layout.pager_fragment, null);
return nowView;
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
initUI();
}
/**
* 初始化控件
*/
private void initUI() {
this.position = getArguments().getInt("position");
this.txtView = (TextView) nowView.findViewById(R.id.textview);
this.txtView.setText("第" + (position+1) + "个Fragment");
}
}
通常在实际中我们把传入给Fragment的bundle中放入的是一个关键性的对象,比如新闻类的传入栏目id,这样可以在Fragment给界面加载数据的时候,根据id来判断加载的是哪个标签(就是Adapter中的lables)下面的内容。实际上,在Adapter中的lables通常放的是一个存放着新闻栏目的ArrayList,栏目的名称、id都存放在这个list里面。
5.TabPageIndicator的样式修改
实际开发中,设计总会有定制的头部(就是上面图片中显示“第一页”、“第二页”那些),比如颜色或者图标、边框这些有特殊的邀请,作为开发我们有时候就需要调整代码。那么怎么办呢?请看下面:
1)上面说的那些其实就是个样式问题,那么样式当然是在styles文件里面,如下是我给TabPageIndicator使用的样式
<!-- tab indicator样式 -->
<style name="MyTheme_tab" parent="@android:style/Theme.NoTitleBar">
<item name="vpiTabPageIndicatorStyle">@style/MyWidget.TabPageIndicator</item>
<item name="android:windowNoTitle">true</item>
<item name="android:animationDuration">5000</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:textColor">@drawable/viewpager_title_textcolor</item>
</style>
<style name="MyWidget.TabPageIndicator" parent="Widget">
<item name="android:gravity">center</item>
<item name="android:background">@drawable/tab_blue_indicator</item>
<item name="android:paddingTop">15dp</item>
<item name="android:paddingBottom">15dp</item>
<item name="android:textAppearance">@style/MyTextAppearance.TabPageIndicator</item>
<item name="android:textSize">@dimen/normal_text_size</item>
<item name="android:maxLines">1</item>
</style>
<style name="MyTextAppearance.TabPageIndicator" parent="Widget">
<item name="android:textStyle">normal</item>
<item name="android:textColor">@android:color/black</item>
</style>
2)其实只需要一个就可以了,但是实际项目中可能有很多界面用到这种滑动界面,所以采用分级的style样式来定制出指定的效果。这样可以打造不同的头部效果来。
(1)
<style name="MyTextAppearance.TabPageIndicator" parent="Widget">
<item name="android:textStyle">normal</item>
<item name="android:textColor">@android:color/black</item>
</style>
上面这一段我规定了所有头部的样式,normal是我自己定义的一个TextView的样式,包含了layout_width、layout_height、textSize这些属性。没错TabPageIndicator就是一个TextView列表,里面单个标签就是一个TextView。所以实际就是在对TextView的样式进行修改。
(2)
<style name="MyWidget.TabPageIndicator" parent="Widget">
<item name="android:gravity">center</item>
<item name="android:background">@drawable/tab_blue_indicator</item>
<item name="android:paddingTop">15dp</item>
<item name="android:paddingBottom">15dp</item>
<item name="android:textAppearance">@style/MyTextAppearance.TabPageIndicator</item>
<item name="android:textSize">@dimen/normal_text_size</item>
<item name="android:maxLines">1</item>
</style>
这上面规定了头部每个标签是居中显示的,以及背景样式。tab_blue_indicator的代码如下:
<?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/tab_unselected_bg" />
<item android:state_focused="false" android:state_selected="true" android:state_pressed="false" android:drawable="@drawable/tab_selected_bg" />
<!-- Focused states -->
<item android:state_focused="true" android:state_selected="false" android:state_pressed="false" android:drawable="@drawable/tab_unselected_bg" />
<item android:state_focused="true" android:state_selected="true" android:state_pressed="false" android:drawable="@drawable/tab_selected_bg" />
<!-- Pressed -->
<!-- Non focused states -->
<item android:state_focused="false" android:state_selected="false" android:state_pressed="true" android:drawable="@drawable/tab_unselected_bg" />
<item android:state_focused="false" android:state_selected="true" android:state_pressed="true" android:drawable="@drawable/tab_selected_bg" />
<!-- Focused states -->
<item android:state_focused="true" android:state_selected="false" android:state_pressed="true" android:drawable="@drawable/tab_unselected_bg" />
<item android:state_focused="true" android:state_selected="true" android:state_pressed="true" android:drawable="@drawable/tab_selected_bg" />
</selector>
这其实就是2张.9图,用来做出选中和未选中状态下的下划线效果
然后两个padding属性来控制头部的高度,两个标签直接的左右间隔可以通过paddingLeft和paddingRight来控制。
(3)
<style name="MyTheme_tab" parent="@android:style/Theme.NoTitleBar">
<item name="vpiTabPageIndicatorStyle">@style/MyWidget.TabPageIndicator</item>
<item name="android:windowNoTitle">true</item>
<item name="android:animationDuration">5000</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:textColor">@drawable/viewpager_title_textcolor</item>
</style>
第一行知道这个style是给Activity用的后就很好了解,第二行name是固定写法,指明PageIndicator的样式是我们刚才定义的那个MyWidget.TabPageIndicator。
<item name="android:textColor">@drawable/viewpager_title_textcolor</item>
这一行就是定义标签被点击时的文字样式,看效果图,我设置的是按下显示蓝色。
如果是需要在头部样式里显示图片背景可以设置background为图片,或者文字加图片用drawableLeft、drawableRight……来设置。其实就是把TextView设置出你想要的效果就行了。
6.让样式生效
在Mainfest.xml文件中给Activity的theme设置成我们上面写好的样式就行了,如下。
<activity
android:name="com.app.acoe.demo.activity.TabPagerDemoActivity"
android:theme="@style/MyTheme_tab">
</activity>
7.扩展解读
然后如果只是上面那样写Adapter的话,在有种情况下会出问题。就是当lables内容变化时,Fragment会发生错位,这是由于Fragment重用导致的,使得lables内容改变后Fragment和lables的对应关系没得到更新。那么如何解决呢?
/**
* 由于fragment存在重用问题,tab增加或减少以后,按先后顺序新重用旧的fragment,导致tab标签和fragment内容不一致(错位现象)
* 重写instantiateItem,调用fragment的setter方法,setter方法中不能操作view,这些 View 只有在 onCreateView()事件后才能操作
* 根据setter的数据在fragment界面加载完成后刷新数据
*/
@Override
public Object instantiateItem(ViewGroup container, int position) {
InformationListFragment fragment = (InformationListFragment) super.instantiateItem(container, position);
fragment.setChannelId(lables.get(position).columnValueEnum);
return fragment;
}
/**
* 重写instantiateItem()时,此方法也必须重写,使得instantiateItem()被执行到
*/
@Override
public int getItemPosition(Object object) {
return PagerAdapter.POSITION_NONE;
}
如上,是我从项目中FragmentPagerAdapter代码里截取的。这样可以在lables内容发生变化时,将对应的Fragment中传递过去的(演示代码所示的)position也好、(实际中的)id也好,进行更新。然后在Fragment中的onResume()方法里判断position值、或者id值是否发生了变化,如果是,则重新对界面数据进行请求加载刷新这样的步骤。
源码基本上全部贴出来,就不附项目源码了。除了部分样式,这个自己随便写一个就行了,没必要完全复制。