ViewPagerIndicator集成分页指示器,其实就是标题栏和ViewPager的联动效果,大家先看一下效果图直观了解:(图侵删)
这篇文章将会教大家怎么简单快速地制作自己的ViewPagerIndicator,同时说明制作思路,让大家可以轻易的扩展和定制自己想要的效果。
由于文章的主要目的在于介绍整体思路,所以实现的界面效果可能不是很好看,不过大家看过这篇文章以后,一定可以自己修改出好看的效果的。
话不多说,先来说明整体思路。
对比上图,对于整个控件而言,显然下面显示内容的,是一个ViewPager,通过ViewPager我们轻易的得到翻页的效果,那么难点在于上面的标题导航栏,总的来说我们要实现三点:
1,使用ViewPager翻页的时候,导航栏相应的标题会有变化(例如上图的蓝色的下划线,或者背景颜色的变化),来提示用户,现在是哪个标题下的内容
2,点击导航栏标题,ViewPager会翻到对应页,另外当我们点击某个title时,我们希望整个视图可以移动到以这个title为中心。
3,当导航栏的标题过多,超出屏幕宽度,我们可以滑动导航栏找到后边的其他标题
要实现上面三个效果,我先来说第三个的实现
我使用HorizontalScrollView来实现,HorizontalScrollView可以水平拖动,假设HorizontalScrollView里面包含着一系列的TextView,这个样式不就是我想要的标题栏的效果吗?
由于HorizontalScrollView继承自FrageLayout,所以里面只能包含一个子控件,一般是LinearLayout,然后再让LinearLayout去包含TextView就可以了
另外还要讲HorizontalScrollView的HorizontalScrollBarEnabled设置为false,用于隐藏它原本的水平方向的滚动条
OK,看起来我们第三个问题解决了
现在来思考第一个问题,要title跟随ViewPager变化,我们很自然想到要去监听ViewPager的翻页事件,使用ViewPager.OnPageChangeListener,由于title的数目跟ViewPager中Fragement的数目一样多,翻到那个,我们将对于index的title(也就是Textview)的背景变色就可以了
由于ViewPager.OnPageChangeListener的onPageSelected(int position)中的参数position会为我们提供这个index
OK,第一个问题貌似也没有那么难。
现在来考虑第二个问题,这里涉及两个滑动。
一个是ViewPager的滑动,正如我们上面所说,TextView和ViewPager中Fragement一一对应
为了响应点击,显然我要每个TextView设置一个OnClickListener
但是TextView怎么知道自己的index呢?TextView本身是没有这个属性的,我们可以继承TextView,然后添加一个index属性不就完了吗?
有了index,我们在onclick方法里面,调用ViewPager的setCurrentItem(item)方法,就可以让ViewPager滑动到正确的位置
上面所说动画效果是ViewPager自带的,但是第二个滑动,就是HorizontalScrollView本身的滑动,HorizontalScrollView我们可以手动滑动,但是怎么样才能让它自动滑到我们需要的位置呢?
HorizontalScrollView提供了一个smoothScrollTo(int x, int y)方法,使用这个方法,我们可以将HorizontalScrollView滑动到任意位置。
问题使我们怎么确定这个位置,由于每个TextView里面的文字数目可能不同,意味着TextView的宽度各不相同,这样要怎么计算位置呢?
我们可以使用getLeft()方法获得目标TextView距离左边的长度,这样就不用管之前的TextView的宽度了,因为getLeft()相当于获得了它们的和,但是移动到getLeft()就超过了,我们希望它移动到中间位置,那么getLeft()还有减去(HorizontalScrollView.getWidth()-TextView.getWidth())/2
至于为什么这样算,大家不明白的话,可以看图:我不再做过多解释
计算出smoothScrollTo()的位置以后,调用这个函数就好了,这样就实现了标题栏和ViewPager的联动效果。
原理讲解到这里,下面我们来直接看代码。
先来看构造函数和相关属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
public
class
MyIndicator
extends
HorizontalScrollView
implements
ViewPager.OnPageChangeListener{
private
ViewPager mViewPager;
private
MyLinearLayout myLinearLayout;
ViewPager.OnPageChangeListener mListener;
public
MyIndicator(Context context) {
super
(context);
init(context);
}
public
MyIndicator(Context context, AttributeSet attrs) {
super
(context, attrs);
init(context);
}
public
MyIndicator(Context context, AttributeSet attrs,
int
defStyle) {
super
(context, attrs, defStyle);
init(context);
}
private
void
init(Context mcontext){
setHorizontalScrollBarEnabled(
false
);
//隐藏自带的滚动条
//添加linearLayout
myLinearLayout =
new
MyLinearLayout(mcontext);
myLinearLayout.setOrientation(LinearLayout.HORIZONTAL);
addView(myLinearLayout,
new
ViewGroup.LayoutParams(WRAP_CONTENT, MATCH_PARENT));
}
}
|
从上面的代码我们可以看到,我继承HorizontalScrollView来自定义了一个控件,这个控件就是我们上面说的导航栏,并且隐藏了它的滚动条
另外我们实现了ViewPager.OnPageChangeListener接口,因为我们要监听ViewPager的滑页行为,从而去改变导航栏的状态
所以我们也可以看到,MyIndicator持有ViewPager的引用
但是有人会问,既然我们为ViewPager设置了监听器为MyIndicator,如果我们还想要监听ViewPager怎么办呢?
所以我们为MyIndicator提供了一个方法
1
2
3
|
public
void
setOnPageChangeListener(ViewPager.OnPageChangeListener listener){
mListener = listener;
}
|
这样就可以为ViewPager设置监听器了,而这个listener的调用,需要我们在MyIndicator实现的ViewPager.OnPageChangeListener接口的方法的最后主动调用
也就是这样写:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
@Override
public
void
onPageScrolled(
int
position,
float
positionOffset,
int
positionOffsetPixels) {
if
(mListener!=
null
) mListener.onPageScrolled(position,positionOffset,positionOffsetPixels);
}
@Override
public
void
onPageSelected(
int
position) {
setCurrentItem(position);
if
(mListener!=
null
) mListener.onPageSelected(position);
}
@Override
public
void
onPageScrollStateChanged(
int
state) {
if
(mListener!=
null
) mListener.onPageScrollStateChanged(state);
}
|
看完了初始化的工作,我们可以看看怎么使用这个MyIndicator
首先在xml布局文件里面,很简单,直接使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
<
LinearLayout
android:orientation
=
"vertical"
android:layout_width
=
"fill_parent"
android:layout_height
=
"fill_parent"
>
<
com.example.kaiyicky.myapplication.MyIndicator
android:id
=
"@+id/indicator"
android:layout_height
=
"wrap_content"
android:layout_width
=
"fill_parent"
/>
<
android.support.v4.view.ViewPager
android:id
=
"@+id/pager"
android:layout_width
=
"fill_parent"
android:layout_height
=
"0dp"
android:layout_weight
=
"1"
/>
</
LinearLayout
>
|
然后在Activity里面这样
1
2
3
4
5
6
7
8
9
10
|
@Override
protected
void
onCreate(Bundle savedInstanceState) {
super
.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ViewPager pager = (ViewPager)findViewById(R.id.pager);
MyIndicator indicator = (MyIndicator)findViewById(R.id.indicator);
indicator.setViewPager(pager);
}
|
通过一个setViewPager()方法使MyIndicator持有ViewPager的引用就可以了
OK,接下来继续看MyIndicator怎么写,看setViewPager()方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
public
void
setViewPager(ViewPager viewPager){
setViewPager(viewPager,
0
);
}
public
void
setViewPager(ViewPager viewPager,
int
initPos){
if
(mViewPager == viewPager) {
return
;
}
if
(mViewPager !=
null
) {
mViewPager.setOnPageChangeListener(
null
);
}
final
PagerAdapter adapter = viewPager.getAdapter();
if
(adapter ==
null
) {
throw
new
IllegalStateException(
"ViewPager does not have adapter instance."
);
}
mViewPager = viewPager;
viewPager.setOnPageChangeListener(
this
);
notifyDataSetChanged();
setCurrentItem(initPos);
}
|
在上面的代码中我们可以看到,我们检查了ViewPager的Adapter是否为空,如果是要抛出异常
说明我们必须在调用setViewPager()之前为ViewPager设置Adapter
为什么呢?因为导航栏的标题数目跟ViewPager的页面数目是一样的,而FragmentPagerAdapter里面的getCount()方法返回了这个数目,如果没有设置Adapter
MyIndicator就不知道怎么绘制导航栏了,因为连标题数目都不清楚
对于Adapter,我们可以写一个简单的,例如
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
class
GoogleMusicAdapter
extends
FragmentPagerAdapter {
public
GoogleMusicAdapter(FragmentManager fm) {
super
(fm);
}
@Override
public
Fragment getItem(
int
position) {
return
TestFragment.newInstance(CONTENT[position % CONTENT.length]);
}
@Override
public
CharSequence getPageTitle(
int
position) {
return
CONTENT[position % CONTENT.length].toUpperCase();
}
@Override
public
int
getCount() {
return
CONTENT.length;
}
}
|
其中TestFragment是这样的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
public
final
class
TestFragment
extends
Fragment {
private
static
final
String KEY_CONTENT =
"TestFragment:Content"
;
public
static
TestFragment newInstance(String content) {
TestFragment fragment =
new
TestFragment();
StringBuilder builder =
new
StringBuilder();
for
(
int
i =
0
; i <
20
; i++) {
builder.append(content).append(
" "
);
}
builder.deleteCharAt(builder.length() -
1
);
fragment.mContent = builder.toString();
return
fragment;
}
private
String mContent =
"???"
;
@Override
public
void
onCreate(Bundle savedInstanceState) {
super
.onCreate(savedInstanceState);
if
((savedInstanceState !=
null
) && savedInstanceState.containsKey(KEY_CONTENT)) {
mContent = savedInstanceState.getString(KEY_CONTENT);
}
}
@Override
public
View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
TextView text =
new
TextView(getActivity());
text.setGravity(Gravity.CENTER);
text.setText(mContent);
text.setTextSize(
20
* getResources().getDisplayMetrics().density);
text.setPadding(
20
,
20
,
20
,
20
);
LinearLayout layout =
new
LinearLayout(getActivity());
layout.setLayoutParams(
new
LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));
layout.setGravity(Gravity.CENTER);
layout.addView(text);
return
layout;
}
@Override
public
void
onSaveInstanceState(Bundle outState) {
super
.onSaveInstanceState(outState);
outState.putString(KEY_CONTENT, mContent);
}
}
|
其实就是一个 创建Fragment的工具类
最后在Activity修改一下
1
2
3
4
5
6
7
8
9
10
11
12
13
|
@Override
protected
void
onCreate(Bundle savedInstanceState) {
super
.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
FragmentPagerAdapter adapter =
new
GoogleMusicAdapter(getSupportFragmentManager());
ViewPager pager = (ViewPager)findViewById(R.id.pager);
pager.setAdapter(adapter);
MyIndicator indicator = (MyIndicator)findViewById(R.id.indicator);
indicator.setViewPager(pager);
}
|
回过头来看setViewPager()方法,然后就是调用了两个函数,首先是notifyDataSetChanged()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
private
void
notifyDataSetChanged(){
myLinearLayout.removeAllViews();
PagerAdapter mAdapter = mViewPager.getAdapter();
int
count = mAdapter.getCount();
for
(
int
i=
0
;i<count;i++){
addTab(i,mAdapter.getPageTitle(i));
}
requestLayout();
}
private
void
addTab(
int
index,CharSequence text) {
TabView tabView =
new
TabView(getContext());
tabView.index = index;
tabView.setFocusable(
true
);
tabView.setOnClickListener(mTabClickListener);
tabView.setText(text);
tabView.setTextSize(
30
);
tabView.setPadding(
20
,
0
,
20
,
0
);
myLinearLayout.addView(tabView);
}
|
这个函数其实就是起到添加标题的作用
我们通过Adapter获得了数目,然后逐个调用addTab()将标题栏添加进LinearLayout
有人会问myLinearLayout是什么,目前在MyIndicator里面其实就是一个LinearLayout,我独立出来是为了大家以后方便扩展,代码如下
1
2
3
4
5
6
|
public
class
MyLinearLayout
extends
LinearLayout {
public
MyLinearLayout(Context context) {
super
(context);
setWillNotDraw(
false
);
}
}
|
然后来看addTab()做了什么,顾名思义,就是添加tab,前面原理分析的时候,我们已经说过tab其实是TextView,但是要标记index,所以我们要继承TextView自定义一个控件
1
2
3
4
5
6
7
8
9
10
|
private
class
TabView
extends
TextView {
public
int
index;
public
TabView(Context context,
int
index){
this
(context);
this
.index = index;
}
public
TabView(Context context) {
super
(context);
}
}
|
可以看到,其实只是为textView增加了Index属性
到此为止,还不涉及动画效果,但是大家在模拟器上看,就可以看到标题栏的出现,而且标题的数目,会跟你ViewPager中Fragment数目一样
下面来谈论动画效果的实现
上面我们记得,setViewPager()方法里面,还有一个setCurrentItem()方法,另外onPageSelected()里面也有调用这个方法
其实这个方法就是来实现换页的动态效果的,onPageSelected()里面调用,可以在viewPager滑动的时候换页
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
public
void
setCurrentItem(
int
item) {
if
(mViewPager ==
null
) {
throw
new
IllegalStateException(
"ViewPager has not been bound."
);
}
int
mSelectedTabIndex = item;
mViewPager.setCurrentItem(item);
final
int
tabCount = myLinearLayout.getChildCount();
for
(
int
i =
0
; i < tabCount; i++) {
//遍历标题,改变选中的背景
final
View child = myLinearLayout.getChildAt(i);
final
boolean
isSelected = (i == item);
child.setSelected(isSelected);
if
(isSelected) {
child.setBackgroundColor(Color.RED);
animateToTab(item);
//动画效果
}
else
{
child.setBackgroundColor(Color.TRANSPARENT);
}
}
}
|
其实这个方法也很简单,首先实现ViewPager的滑动,只有调用ViewPager的setCurrentItem()方法就好了
接下来遍历每个标题,使选中的标题背景色变成红色,其他背景色变成蓝色
可是这样还不够,我们还有标题栏自动滑动,使标题处于正中间
于是我们又了aniateToTab()方法
如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
private
Runnable mTabSelector;
private
void
animateToTab(
final
int
position) {
final
View tabView = myLinearLayout.getChildAt(position);/获取目标标题栏对象
if
(mTabSelector !=
null
) {
removeCallbacks(mTabSelector);
}
mTabSelector =
new
Runnable() {
public
void
run() {
final
int
scrollPos = tabView.getLeft() - (getWidth() - tabView.getWidth()) /
2
;
//计算要滑动到的位置
smoothScrollTo(scrollPos,
0
);
mTabSelector =
null
;
}
};
post(mTabSelector);
//在主线程执行动画
}
|
和一开始就说明得原理一样,我们计算出来要smoothScrollTo的最终位置,然后调用这个方法就好了
只有写在runnable里面,是为了保证在主线程调用
OK,到此为止,我们就实现了滑动ViewPager,标题栏也会滑动的效果了,不信大家现在可以测试一下自己的代码
接下来就是点击标题,也会自动滑动,为了让TextView能点击,我为每个TextView都设置了OnClickListener
1
2
3
4
5
6
7
8
|
private
final
OnClickListener mTabClickListener =
new
OnClickListener() {
public
void
onClick(View view) {
TabView tabView = (TabView)view;
final
int
oldSelected = mViewPager.getCurrentItem();
final
int
newSelected = tabView.index;
setCurrentItem(newSelected);
}
};
|
监听器里面更简单,就是获得目标标题栏的index,然后调用setCurrentItem()就可以了
这样就实现了点击滑动的效果,点击标题栏,Viewpager也会跟着翻页哦
整个控件就说完了,如果大家事先明白了我的思路,看起代码来应该很流畅
最后贴出MyIndicator的完整代码,大家可以随意改造,实现自己需要的效果啊!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
|
public
class
MyIndicator
extends
HorizontalScrollView
implements
ViewPager.OnPageChangeListener{
private
ViewPager mViewPager;
private
MyLinearLayout myLinearLayout;
ViewPager.OnPageChangeListener mListener;
private
final
OnClickListener mTabClickListener =
new
OnClickListener() {
public
void
onClick(View view) {
TabView tabView = (TabView)view;
final
int
oldSelected = mViewPager.getCurrentItem();
final
int
newSelected = tabView.index;
setCurrentItem(newSelected);
}
};
public
MyIndicator(Context context) {
super
(context);
init(context);
}
public
MyIndicator(Context context, AttributeSet attrs) {
super
(context, attrs);
init(context);
}
public
MyIndicator(Context context, AttributeSet attrs,
int
defStyle) {
super
(context, attrs, defStyle);
init(context);
}
private
void
init(Context mcontext){
setHorizontalScrollBarEnabled(
false
);
//隐藏自带的滚动条
//添加linearLayout
myLinearLayout =
new
MyLinearLayout(mcontext);
myLinearLayout.setOrientation(LinearLayout.HORIZONTAL);
addView(myLinearLayout,
new
ViewGroup.LayoutParams(WRAP_CONTENT, MATCH_PARENT));
}
public
void
setViewPager(ViewPager viewPager){
setViewPager(viewPager,
0
);
}
public
void
setViewPager(ViewPager viewPager,
int
initPos){
if
(mViewPager == viewPager) {
return
;
}
if
(mViewPager !=
null
) {
mViewPager.setOnPageChangeListener(
null
);
}
final
PagerAdapter adapter = viewPager.getAdapter();
if
(adapter ==
null
) {
throw
new
IllegalStateException(
"ViewPager does not have adapter instance."
);
}
mViewPager = viewPager;
viewPager.setOnPageChangeListener(
this
);
notifyDataSetChanged();
setCurrentItem(initPos);
}
private
void
notifyDataSetChanged(){
myLinearLayout.removeAllViews();
PagerAdapter mAdapter = mViewPager.getAdapter();
int
count = mAdapter.getCount();
for
(
int
i=
0
;i<count;i++){
addTab(i,mAdapter.getPageTitle(i));
}
requestLayout();
}
private
void
addTab(
int
index,CharSequence text) {
TabView tabView =
new
TabView(getContext());
tabView.index = index;
tabView.setFocusable(
true
);
tabView.setOnClickListener(mTabClickListener);
tabView.setText(text);
tabView.setTextSize(
30
);
tabView.setPadding(
20
,
0
,
20
,
0
);
myLinearLayout.addView(tabView);
}
public
void
setCurrentItem(
int
item) {
if
(mViewPager ==
null
) {
throw
new
IllegalStateException(
"ViewPager has not been bound."
);
}
int
mSelectedTabIndex = item;
mViewPager.setCurrentItem(item);
final
int
tabCount = myLinearLayout.getChildCount();
for
(
int
i =
0
; i < tabCount; i++) {
//遍历标题,改变选中的背景
final
View child = myLinearLayout.getChildAt(i);
final
boolean
isSelected = (i == item);
child.setSelected(isSelected);
if
(isSelected) {
child.setBackgroundColor(Color.RED);
animateToTab(item);
//动画效果
}
else
{
child.setBackgroundColor(Color.TRANSPARENT);
}
}
}
private
Runnable mTabSelector;
private
void
animateToTab(
final
int
position) {
final
View tabView = myLinearLayout.getChildAt(position);
if
(mTabSelector !=
null
) {
removeCallbacks(mTabSelector);
}
mTabSelector =
new
Runnable() {
public
void
run() {
final
int
scrollPos = tabView.getLeft() - (getWidth() - tabView.getWidth()) /
2
;
smoothScrollTo(scrollPos,
0
);
mTabSelector =
null
;
}
};
post(mTabSelector);
}
public
void
setOnPageChangeListener(ViewPager.OnPageChangeListener listener){
mListener = listener;
}
@Override
public
void
onPageScrolled(
int
position,
float
positionOffset,
int
positionOffsetPixels) {
if
(mListener!=
null
) mListener.onPageScrolled(position,positionOffset,positionOffsetPixels);
}
@Override
public
void
onPageSelected(
int
position) {
setCurrentItem(position);
if
(mListener!=
null
) mListener.onPageSelected(position);
}
@Override
public
void
onPageScrollStateChanged(
int
state) {
if
(mListener!=
null
) mListener.onPageScrollStateChanged(state);
}
private
class
TabView
extends
TextView {
public
int
index;
public
TabView(Context context,
int
index){
this
(context);
this
.index = index;
}
public
TabView(Context context) {
super
(context);
}
}
}
|