最新消息:欢迎访问Android开发中文站!商务联系微信:loading_in

教你轻松自定义ViewPagerIndicator

开发进阶 AndroidChina 10650浏览 1评论

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的联动效果。

原理讲解到这里,下面我们来直接看代码。

先来看构造函数和相关属性

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提供了一个方法

public void setOnPageChangeListener(ViewPager.OnPageChangeListener listener){
      mListener = listener;
}

这样就可以为ViewPager设置监听器了,而这个listener的调用,需要我们在MyIndicator实现的ViewPager.OnPageChangeListener接口的方法的最后主动调用

也就是这样写:

@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布局文件里面,很简单,直接使用

<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里面这样

@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()方法

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,我们可以写一个简单的,例如

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是这样的

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修改一下

@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()

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,我独立出来是为了大家以后方便扩展,代码如下

public class MyLinearLayout extends LinearLayout {
     public MyLinearLayout(Context context) {
          super(context);
          setWillNotDraw(false);
     }
}

然后来看addTab()做了什么,顾名思义,就是添加tab,前面原理分析的时候,我们已经说过tab其实是TextView,但是要标记index,所以我们要继承TextView自定义一个控件

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滑动的时候换页

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()方法

如下

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

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的完整代码,大家可以随意改造,实现自己需要的效果啊!

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

您必须 登录 才能发表评论!

网友最新评论 (1)

  1. 源码有么?
    Luckylll2015-09-22 17:25