教你轻松自定义ViewPagerIndicator

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
       xmlns:android = "http://schemas.android.com/apk/res/android"
       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);
                 }
           }
    }

转载请注明:Android开发中文站 » 教你轻松自定义ViewPagerIndicator

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值