1.概述
上一期我们已经写了一篇 打造炫酷通用的ViewPager指示器 - 玩转字体变色 可是这种效果虽然绚烂可以装装A和C之间,但是在实际的大多数效果中并不常见,只是在内涵段子中有这个效果而已,那么这一期我们就用Adapter适配器模式适配所有的效果,堪称终结者。附视频地址:http://pan.baidu.com/s/1dENNO33

这里写图片描述
2.效果实现
我还是还是拿上一个实例来做演示吧。这里我贴几种常见的效果,首先声明Android自带的有这个控件叫TabLayout,大家可以自己用用试试看好用不?我也用过但是不做任何评价,自己造的轮子还是想怎么用就怎么用。

这里写图片描述

这里写图片描述

这里写图片描述
还有一些奇葩的效果如每个头部Item布局不一样,还有上面是图片下面是文字选中的效果各不相同等等,我们都要去适配。
2.2 实现思路:
我在老早的时候用过ViewPageIndicator,还没毕业出来工作的时候,好不好用我也不做评价,就是那个时候搞了一晚上没搞出来第二天一看原来是activity的Theme主题没有配置,大家手上肯定也有类似的效果也都可以用,只是以个人的理解来自己造一个轮子。
2.2.1 控件肯定是继承ScrollView因为可以左右滑动,如果再去自定义ViewGroup肯定不划算。
2.2.2 怎样才能适合所有的效果,难道我们把所有可能出现的效果都写一遍吗?这的确不太可能,所以肯定采用Adapter适配器模式。
2.2.3 我们先动起来从简单的入手,先做到动态的添加不同的布局条目再说吧。
2.3 自定义TrackIndicatorView动态添加布局:
这里为了适配所有效果,所以决定采用适配器Adapter设计模式,上面也提到过。至于什么是适配器模式大家需要看一下这个 Android设计模式源码解析之适配器(Adapter)模式 这是理论篇,但是仔细看过我博客的哥们应该知道我其实 Adapter设计模式理论与实践相结合写过很多效果和框架了。这里不做过多的讲解,写着写着看着看着就会了就理解了。
2.3.1 我们再也不能直接传字符串数组或是传对象数组过去让自定义View去处理了,所以我们先确定一个自定义的Adapter类,getCount() 和 getView(int position,ViewGroup parent) 先用这两个方法吧后面想到了再说。
1/** 2 * Created by Darren on 2016/12/7. 3 * Email: 240336124@qq.com 4 * Description: 指示器的适配器 5 */ 6public abstract class IndicatorBaseAdapter{ 7 // 获取总的条数 8 public abstract int getCount(); 9 10 // 根据当前的位置获取View 11 public abstract View getView(int position,ViewGroup parent); 12}
2.3.2 然后我们来实现指示器的自定义View,TrackIndicatorView 继承自 HorizontalScrollView 。然后我们利用传递过来的Adapter再去动态的添加,我这里就直接上代码吧
1/** 2 * Created by Darren on 2016/12/13. 3 * Email: 240336124@qq.com 4 * Description: ViewPager指示器 5 */ 6 7public class TrackIndicatorView extends HorizontalScrollView { 8 9 // 自定义适配器 10 private IndicatorBaseAdapter mAdapter; 11 12 // Item的容器因为ScrollView只允许加入一个孩子 13 private LinearLayout mIndicatorContainer; 14 15 public TestIndicator(Context context) { 16 this(context, null); 17 } 18 19 public TestIndicator(Context context, AttributeSet attrs) { 20 this(context, attrs, 0); 21 } 22 23 public TestIndicator(Context context, AttributeSet attrs, int defStyleAttr) { 24 super(context, attrs, defStyleAttr); 25 // 初始化Indicator容器用来存放item 26 mIndicatorContainer = new LinearLayout(context); 27 addView(mIndicatorContainer); 28 } 29 30 public void setAdapter(IndicatorBaseAdapter adapter) { 31 if (adapter == null) { 32 throw new NullPointerException("Adapter cannot be null!"); 33 } 34 this.mAdapter = adapter; 35 36 // 获取Item个数 37 int count = mAdapter.getCount(); 38 39 // 动态添加到布局容器 40 for (int i = 0; i < count; i++) { 41 View indicatorView = mAdapter.getView(i, mIndicatorContainer); 42 mIndicatorContainer.addView(indicatorView); 43 } 44 } 45}
效果可想而知,可以写一个Activity测试一下,目前可以动态的添加多个不同样式的布局,如果超出一个屏幕可以左右滑动,我这里就不做演示,待会一起吧。
2.3.3 动态的制定指示器Item的宽度:
目前我们虽然能够动态的去添加各种布局,但是Item的宽度是任意的,我们需要在布局文件中指定一屏显示多少个,如果没有指定那么就获取Item中最宽的一个,如果不够一屏显示就默认显示一屏。我们需要使用自定义属性,这里就不做过多的讲,实在不行大家就自己去看看有关自定义属性的博客或是直接google搜索一下。
1 // 获取一屏显示多少个Item,默认是0 2 private int mTabVisibleNums = 0; 3 4 // 每个Item的宽度 5 private int mItemWidth = 0; 6 7 public TrackIndicatorView(Context context, AttributeSet attrs, int defStyleAttr) { 8 super(context, attrs, defStyleAttr); 9 // 之前代码省略... 10 // 获取自定义属性值 一屏显示多少个 11 TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.TrackIndicatorView); 12 mTabVisibleNums = array.getInt(R.styleable.TrackIndicatorView_tabVisibleNums, 13 mTabVisibleNums); 14 array.recycle(); 15 } 16 17 @Override 18 protected void onLayout(boolean changed, int l, int t, int r, int b) { 19 super.onLayout(changed, l, t, r, b); 20 if (changed) { 21 // 指定Item的宽度 22 mItemWidth = getItemWidth(); 23 int itemCounts = mAdapter.getCount(); 24 for (int i = 0; i < itemCounts; i++) { 25 // 指定每个Item的宽度 26 mIndicatorContainer.getChildAt(i).getLayoutParams().width = mItemWidth; 27 } 28 Log.e(TAG, "mItemWidth -> " + mItemWidth); 29 } 30 } 31 32 /** 33 * 获取每一个条目的宽度 34 */ 35 public int getItemWidth() { 36 int itemWidth = 0; 37 // 获取当前控件的宽度 38 int width = getWidth(); 39 if (mTabVisibleNums != 0) { 40 // 在布局文件中指定一屏幕显示多少个 41 itemWidth = width / mTabVisibleNums; 42 return itemWidth; 43 } 44 // 如果没有指定获取最宽的一个作为ItemWidth 45 int maxItemWidth = 0; 46 int mItemCounts = mAdapter.getCount(); 47 // 总的宽度 48 int allWidth = 0; 49 50 for (int i = 0; i < mItemCounts; i++) { 51 View itemView = mIndicatorContainer.getChildAt(i); 52 int childWidth = itemView.getMeasuredWidth(); 53 maxItemWidth = Math.max(maxItemWidth, childWidth); 54 allWidth += childWidth; 55 } 56 57 itemWidth = maxItemWidth; 58 59 // 如果不足一个屏那么宽度就为 width/mItemCounts 60 if (allWidth < width) { 61 itemWidth = width / mItemCounts; 62 } 63 return itemWidth; 64 }
目前我们各种情况都测试了一下,一种是直接在布局文件中指定一屏可见显示4个,一种是不指定就默认以最大的Item的宽度为准,最后一种就是不指定又不足一个屏幕默认就显示一屏。看一下效果吧

这里写图片描述
2.4结合ViewPager
接下来我们就需要结合ViewPager了,也就需要实现一系列重要的效果:
2.4.1. 当ViewPager滚动的时候头部需要自动将当前Item滚动到最中心;
2.4.2. 点击Item之后ViewPager能够切换到对应的页面;
2.4.3. 需要页面切换之后需要回调,让用户切换当前选中的状态,需要在Adapter中增加方法;
2.4.4. 有些效果需要加入指示器,但并不是每种效果都需要
2.4.1. 当ViewPager滚动的时候头部自动将当前Item滚动到最中心
我们目前不光需要Adapter,还需要一个参数就是ViewPager,需要监听ViewPager的滚动事件
1 /** 2 * 重载一个setAdapter的方法 3 * @param adapter 适配器 4 * @param viewPager 联动的ViewPager 5 */ 6 public void setAdapter(IndicatorBaseAdapter adapter, ViewPager viewPager) { 7 // 直接调用重载方法 8 setAdapter(adapter); 9 10 // 为ViewPager添加滚动监听事件 11 this.mViewPager = viewPager; 12 mViewPager.addOnPageChangeListener(this); 13 } 14 15 @Override 16 public void onPageScrolled(int position, float positionOffset, 17 int positionOffsetPixels) { 18 // 在ViewPager滚动的时候会不断的调用该方法 19 Log.e(TAG,"position --> "+position+" positionOffset --> "+positionOffset); 20 // 在不断滚动的时候让头部的当前Item一直保持在最中心 21 indicatorScrollTo(position,positionOffset); 22 } 23 24 /** 25 * 不断的滚动头部 26 */ 27 private void indicatorScrollTo(int position, float positionOffset) { 28 // 当前的偏移量 29 int currentOffset = (int) ((position + positionOffset) * mItemWidth); 30 // 原始的左边的偏移量 31 int originLeftOffset = (getWidth()-mItemWidth)/2; 32 // 当前应该滚动的位置 33 int scrollToOffset = currentOffset - originLeftOffset; 34 // 调用ScrollView的scrollTo方法 35 scrollTo(scrollToOffset,0); 36 }
目前我们滚动ViewPager的时候,当前指示器条目会一直保持在最中心,activity的代码我就没贴出来了,这个待会可以下载我的源码看看。我们看看效果

这里写图片描述
2.4.2. 点击Item之后ViewPager能够切换到对应的页面
1 public void setAdapter(IndicatorBaseAdapter adapter) { 2 if (adapter == null) { 3 throw new NullPointerException("Adapter cannot be null!"); 4 } 5 this.mAdapter = adapter; 6 7 // 获取Item个数 8 int count = mAdapter.getCount(); 9 10 // 动态添加到布局容器 11 for (int i = 0; i < count; i++) { 12 View indicatorView = mAdapter.getView(i, mIndicatorContainer); 13 mIndicatorContainer.addView(indicatorView); 14 switchIndicatorClick(indicatorView,i); 15 } 16 } 17 18 /** 19 * Indicator条目点击对应切换ViewPager 20 */ 21 private void switchIndicatorClick(View indicatorView, final int position) { 22 indicatorView.setOnClickListener(new OnClickListener() { 23 @Override 24 public void onClick(View v) { 25 if(mViewPager != null){ 26 // 对应切换ViewPager 27 mViewPager.setCurrentItem(position); 28 } 29 // IndicatorItem对应滚动到最中心 30 indicatorSmoothScrollTo(position); 31 } 32 }); 33 } 34 35 /** 36 * 滚动到当前的位置带动画 37 */ 38 private void indicatorSmoothScrollTo(int position) { 39 // 当前的偏移量 40 int currentOffset = ((position) * mItemWidth); 41 // 原始的左边的偏移量 42 int originLeftOffset = (getWidth()-mItemWidth)/2; 43 // 当前应该滚动的位置 44 int scrollToOffset = currentOffset - originLeftOffset; 45 // smoothScrollTo 46 smoothScrollTo(scrollToOffset,0); 47 }
我们运行起来之后会发现一个问题,我们点击会切换对应的ViewPager但是这个时候还是会调用onPageScrolled()方法,这个就比较dan疼,所以我们必须解决,如果是点击我就不让其执行onPageScrolled()里面的代码。
2.4.3. 需要页面切换之后需要回调,让用户切换当前选中的状态,需要在Adapter中增加方法
在Adapter中增加两个回调方法,一个是高亮当前选中方法highLightIndicator(View view) ,恢复默认方法restoreIndicator(View view),这两个方法可以不用写成抽象的,为了方便我们干脆使用泛型
1/** 2 * Created by Darren on 2016/12/7. 3 * Email: 240336124@qq.com 4 * Description: 指示器的适配器 5 */ 6public abstract class IndicatorBaseAdapter<Q extends View>{ 7 // 获取总的条数 8 public abstract int getCount(); 9 10 // 根据当前的位置获取View 11 public abstract Q getView(int position, ViewGroup parent); 12 13 // 高亮当前位置 14 public void highLightIndicator(Q indicatorView){ 15 16 } 17 18 // 重置当前位置 19 public void restoreIndicator(Q indicatorView){ 20 21 } 22}
TrackIndicatorView
1 @Override 2 public void onPageSelected(int position) { 3 // 重置上一个位置的状态 4 View lastView = mIndicatorContainer.getChildAt(mCurrentPosition); 5 mAdapter.restoreIndicator(lastView); 6 // 高亮当前位置的状态 7 mCurrentPosition = position; 8 highLightIndicator(mCurrentPosition); 9 } 10 11 /** 12 * 高亮当前位置 13 */ 14 private void highLightIndicator(int position) { 15 View currentView = mIndicatorContainer.getChildAt(position); 16 mAdapter.highLightIndicator(currentView); 17 }

这里写图片描述
一步两步一步两步总算是快到头了,接下来我们只需要加入指示器就可以了,当前这里面涉及到属性动画,如果不是很了解那就去看一下我的视频或者去google官网看一下吧。
2.4.4. 有些效果需要加入指示器,但并不是每种效果都需要
1/** 2 * Created by Darren on 2016/12/7. 3 * Email: 240336124@qq.com 4 * Description: 指示器的容器包括下标 5 */ 6 7public class IndicatorContainer extends RelativeLayout { 8 private LinearLayout mIndicatorContainer; 9 private Context mContext; 10 // 底部跟踪的View 11 private View mBottomTrackView; 12 private String TAG = "IndicatorContainer"; 13 // 距离左边的初始距离 14 private int mInitLeftMargin = 0; 15 private RelativeLayout.LayoutParams mBottomTrackParams; 16 private int mTabWidth; 17 18 public IndicatorContainer(Context context) { 19 this(context, null); 20 } 21 22 public IndicatorContainer(Context context, AttributeSet attrs) { 23 this(context, attrs, 0); 24 } 25 26 public IndicatorContainer(Context context, AttributeSet attrs, int defStyleAttr) { 27 super(context, attrs, defStyleAttr); 28 this.mContext = context; 29 } 30 31 @Override 32 public void addView(View child) { 33 if (mIndicatorContainer == null) { 34 // 初始化容器 35 mIndicatorContainer = new LinearLayout(mContext); 36 RelativeLayout.LayoutParams params = new LayoutParams( 37 ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); 38 super.addView(mIndicatorContainer, params); 39 } 40 mIndicatorContainer.addView(child); 41 } 42 43 public int getIndicatorCount() { 44 return mIndicatorContainer.getChildCount(); 45 } 46 47 public View getIndicatorAt(int index) { 48 return mIndicatorContainer.getChildAt(index); 49 } 50 51 52 /** 53 * 添加底部跟踪指示器 54 * @param bottomTrackView 55 */ 56 public void addBottomTrackView(View bottomTrackView) { 57 if (bottomTrackView == null) return; 58 mBottomTrackView = bottomTrackView; 59 super.addView(mBottomTrackView); 60 61 // 指定一个规则添加到底部 62 mBottomTrackParams = (LayoutParams) mBottomTrackView.getLayoutParams(); 63 mBottomTrackParams.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM); 64 65 // 计算和指定指示器的宽度 66 int width = mBottomTrackParams.width; 67 mTabWidth = mIndicatorContainer.getChildAt(0).getLayoutParams().width; 68 if (width == ViewGroup.LayoutParams.MATCH_PARENT) { 69 width = mTabWidth; 70 } 71 // 计算跟踪的View初始左边距离 72 if (width < mTabWidth) { 73 mInitLeftMargin = (mTabWidth - width) / 2; 74 } 75 mBottomTrackParams.leftMargin = mInitLeftMargin; 76 } 77 78 /** 79 * 底部指示器移动到当前位置 80 */ 81 public void bottomTrackScrollTo(int position, float offset) { 82 if (mBottomTrackView == null) return; 83 // Log.e(TAG,"position --> "+position+" offset --> "+offset); 84 mBottomTrackParams.leftMargin = (int) (mInitLeftMargin + (position + offset) * mTabWidth); 85 mBottomTrackView.setLayoutParams(mBottomTrackParams); 86 } 87 88 /** 89 * 开启一个动画移动到当前位置 90 */ 91 public void smoothScrollToPosition(int position) { 92 if (mBottomTrackView == null) return; 93 // 获取当前指示器距左边的距离 94 final int mCurrentLeftMargin = mBottomTrackParams.leftMargin; 95 // 计算出最终的距离 96 final int finalLeftMargin = mTabWidth * position + mInitLeftMargin; 97 // 用于动画执行的事件 98 final int distance = finalLeftMargin - mCurrentLeftMargin; 99 // 利用属性动画不断的更新距离 100 ObjectAnimator animator = ObjectAnimator.ofFloat(mBottomTrackView, "leftMargin", 101 mCurrentLeftMargin, finalLeftMargin).setDuration(Math.abs(distance)); 102 animator.setInterpolator(new DecelerateInterpolator()); 103 animator.start(); 104 // 添加动画监听不断的更新 leftMargin 105 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 106 @Override 107 public void onAnimationUpdate(ValueAnimator animation) { 108 float currentLeftMargin = (float) animation.getAnimatedValue(); 109 // Log.e(TAG, "current --> " + currentLeftMargin); 110 setBottomTrackLeftMargin((int) currentLeftMargin); 111 } 112 }); 113 } 114 115 /** 116 * 设置底部跟踪指示器的左边距离 117 */ 118 public void setBottomTrackLeftMargin(int bottomTrackLeftMargin) { 119 mBottomTrackParams.leftMargin = bottomTrackLeftMargin; 120 mBottomTrackView.setLayoutParams(mBottomTrackParams); 121 } 122}
最后我们看看一些奇葩的一些需求,这是录制的效果

这里写图片描述
