1.概述
上一期我们已经写了一篇 打造炫酷通用的ViewPager指示器 - 玩转字体变色 可是这种效果虽然绚烂可以装装A和C之间,但是在实际的大多数效果中并不常见,只是在内涵段子中有这个效果而已,那么这一期我们就用Adapter适配器模式适配所有的效果,堪称终结者。附视频地址:http://pan.baidu.com/s/1dENNO33
2.效果实现
2.1 整合上一个实例:
我还是还是拿上一个实例来做演示吧。这里我贴几种常见的效果,首先声明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) 先用这两个方法吧后面想到了再说。
/** * Created by Darren on 2016/12/7. * Email: 240336124@qq.com * Description: 指示器的适配器 */public abstract class IndicatorBaseAdapter{ public abstract int getCount(); public abstract View getView(int position,ViewGroup parent);}
2.3.2 然后我们来实现指示器的自定义View,TrackIndicatorView 继承自 HorizontalScrollView 。然后我们利用传递过来的Adapter再去动态的添加,我这里就直接上代码吧
/** * Created by Darren on 2016/12/13. * Email: 240336124@qq.com * Description: ViewPager指示器 */public class TrackIndicatorView extends HorizontalScrollView { private IndicatorBaseAdapter mAdapter; private LinearLayout mIndicatorContainer; public TestIndicator(Context context) { this(context, null); } public TestIndicator(Context context, AttributeSet attrs) { this(context, attrs, 0); } public TestIndicator(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mIndicatorContainer = new LinearLayout(context); addView(mIndicatorContainer); } public void setAdapter(IndicatorBaseAdapter adapter) { if (adapter == null) { throw new NullPointerException("Adapter cannot be null!"); } this.mAdapter = adapter; int count = mAdapter.getCount(); for (int i = 0; i < count; i++) { View indicatorView = mAdapter.getView(i, mIndicatorContainer); mIndicatorContainer.addView(indicatorView); } }}
- 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
- 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
效果可想而知,可以写一个Activity测试一下,目前可以动态的添加多个不同样式的布局,如果超出一个屏幕可以左右滑动,我这里就不做演示,待会一起吧。
2.3.3 动态的制定指示器Item的宽度:
目前我们虽然能够动态的去添加各种布局,但是Item的宽度是任意的,我们需要在布局文件中指定一屏显示多少个,如果没有指定那么就获取Item中最宽的一个,如果不够一屏显示就默认显示一屏。我们需要使用自定义属性,这里就不做过多的讲,实在不行大家就自己去看看有关自定义属性的博客或是直接google搜索一下。
private int mTabVisibleNums = 0; private int mItemWidth = 0; public TrackIndicatorView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.TrackIndicatorView); mTabVisibleNums = array.getInt(R.styleable.TrackIndicatorView_tabVisibleNums, mTabVisibleNums); array.recycle(); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); if (changed) { mItemWidth = getItemWidth(); int itemCounts = mAdapter.getCount(); for (int i = 0; i < itemCounts; i++) { mIndicatorContainer.getChildAt(i).getLayoutParams().width = mItemWidth; } Log.e(TAG, "mItemWidth -> " + mItemWidth); } } /** * 获取每一个条目的宽度 */ public int getItemWidth() { int itemWidth = 0; int width = getWidth(); if (mTabVisibleNums != 0) { itemWidth = width / mTabVisibleNums; return itemWidth; } int maxItemWidth = 0; int mItemCounts = mAdapter.getCount(); int allWidth = 0; for (int i = 0; i < mItemCounts; i++) { View itemView = mIndicatorContainer.getChildAt(i); int childWidth = itemView.getMeasuredWidth(); maxItemWidth = Math.max(maxItemWidth, childWidth); allWidth += childWidth; } itemWidth = maxItemWidth; if (allWidth < width) { itemWidth = width / mItemCounts; } return itemWidth; }
- 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
- 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
目前我们各种情况都测试了一下,一种是直接在布局文件中指定一屏可见显示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的滚动事件
/** * 重载一个setAdapter的方法 * @param adapter 适配器 * @param viewPager 联动的ViewPager */ public void setAdapter(IndicatorBaseAdapter adapter, ViewPager viewPager) { setAdapter(adapter); this.mViewPager = viewPager; mViewPager.addOnPageChangeListener(this); } @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { Log.e(TAG,"position --> "+position+" positionOffset --> "+positionOffset); indicatorScrollTo(position,positionOffset); } /** * 不断的滚动头部 */ private void indicatorScrollTo(int position, float positionOffset) { int currentOffset = (int) ((position + positionOffset) * mItemWidth); int originLeftOffset = (getWidth()-mItemWidth)/2; int scrollToOffset = currentOffset - originLeftOffset; scrollTo(scrollToOffset,0); }
- 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
- 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
目前我们滚动ViewPager的时候,当前指示器条目会一直保持在最中心,activity的代码我就没贴出来了,这个待会可以下载我的源码看看。我们看看效果
2.4.2. 点击Item之后ViewPager能够切换到对应的页面
public void setAdapter(IndicatorBaseAdapter adapter) { if (adapter == null) { throw new NullPointerException("Adapter cannot be null!"); } this.mAdapter = adapter; int count = mAdapter.getCount(); for (int i = 0; i < count; i++) { View indicatorView = mAdapter.getView(i, mIndicatorContainer); mIndicatorContainer.addView(indicatorView); switchIndicatorClick(indicatorView,i); } } /** * Indicator条目点击对应切换ViewPager */ private void switchIndicatorClick(View indicatorView, final int position) { indicatorView.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if(mViewPager != null){ mViewPager.setCurrentItem(position); } indicatorSmoothScrollTo(position); } }); } /** * 滚动到当前的位置带动画 */ private void indicatorSmoothScrollTo(int position) { int currentOffset = ((position) * mItemWidth); int originLeftOffset = (getWidth()-mItemWidth)/2; int scrollToOffset = currentOffset - originLeftOffset; smoothScrollTo(scrollToOffset,0); }
- 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
- 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
我们运行起来之后会发现一个问题,我们点击会切换对应的ViewPager但是这个时候还是会调用onPageScrolled()方法,这个就比较dan疼,所以我们必须解决,如果是点击我就不让其执行onPageScrolled()里面的代码。
2.4.3. 需要页面切换之后需要回调,让用户切换当前选中的状态,需要在Adapter中增加方法
在Adapter中增加两个回调方法,一个是高亮当前选中方法highLightIndicator(View view) ,恢复默认方法restoreIndicator(View view),这两个方法可以不用写成抽象的,为了方便我们干脆使用泛型
/** * Created by Darren on 2016/12/7. * Email: 240336124@qq.com * Description: 指示器的适配器 */public abstract class IndicatorBaseAdapter<Q extends View>{ public abstract int getCount(); public abstract Q getView(int position, ViewGroup parent); public void highLightIndicator(Q indicatorView){ } public void restoreIndicator(Q indicatorView){ }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
TrackIndicatorView
@Override public void onPageSelected(int position) { View lastView = mIndicatorContainer.getChildAt(mCurrentPosition); mAdapter.restoreIndicator(lastView); mCurrentPosition = position; highLightIndicator(mCurrentPosition); } /** * 高亮当前位置 */ private void highLightIndicator(int position) { View currentView = mIndicatorContainer.getChildAt(position); mAdapter.highLightIndicator(currentView); }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
一步两步一步两步总算是快到头了,接下来我们只需要加入指示器就可以了,当前这里面涉及到属性动画,如果不是很了解那就去看一下我的视频或者去google官网看一下吧。
2.4.4. 有些效果需要加入指示器,但并不是每种效果都需要
/** * Created by Darren on 2016/12/7. * Email: 240336124@qq.com * Description: 指示器的容器包括下标 */public class IndicatorContainer extends RelativeLayout { private LinearLayout mIndicatorContainer; private Context mContext; private View mBottomTrackView; private String TAG = "IndicatorContainer"; private int mInitLeftMargin = 0; private RelativeLayout.LayoutParams mBottomTrackParams; private int mTabWidth; public IndicatorContainer(Context context) { this(context, null); } public IndicatorContainer(Context context, AttributeSet attrs) { this(context, attrs, 0); } public IndicatorContainer(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); this.mContext = context; } @Override public void addView(View child) { if (mIndicatorContainer == null) { mIndicatorContainer = new LinearLayout(mContext); RelativeLayout.LayoutParams params = new LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); super.addView(mIndicatorContainer, params); } mIndicatorContainer.addView(child); } public int getIndicatorCount() { return mIndicatorContainer.getChildCount(); } public View getIndicatorAt(int index) { return mIndicatorContainer.getChildAt(index); } /** * 添加底部跟踪指示器 * @param bottomTrackView */ public void addBottomTrackView(View bottomTrackView) { if (bottomTrackView == null) return; mBottomTrackView = bottomTrackView; super.addView(mBottomTrackView); mBottomTrackParams = (LayoutParams) mBottomTrackView.getLayoutParams(); mBottomTrackParams.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM); int width = mBottomTrackParams.width; mTabWidth = mIndicatorContainer.getChildAt(0).getLayoutParams().width; if (width == ViewGroup.LayoutParams.MATCH_PARENT) { width = mTabWidth; } if (width < mTabWidth) { mInitLeftMargin = (mTabWidth - width) / 2; } mBottomTrackParams.leftMargin = mInitLeftMargin; } /** * 底部指示器移动到当前位置 */ public void bottomTrackScrollTo(int position, float offset) { if (mBottomTrackView == null) return; mBottomTrackParams.leftMargin = (int) (mInitLeftMargin + (position + offset) * mTabWidth); mBottomTrackView.setLayoutParams(mBottomTrackParams); } /** * 开启一个动画移动到当前位置 */ public void smoothScrollToPosition(int position) { if (mBottomTrackView == null) return; final int mCurrentLeftMargin = mBottomTrackParams.leftMargin; final int finalLeftMargin = mTabWidth * position + mInitLeftMargin; final int distance = finalLeftMargin - mCurrentLeftMargin; ObjectAnimator animator = ObjectAnimator.ofFloat(mBottomTrackView, "leftMargin", mCurrentLeftMargin, finalLeftMargin).setDuration(Math.abs(distance)); animator.setInterpolator(new DecelerateInterpolator()); animator.start(); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float currentLeftMargin = (float) animation.getAnimatedValue(); setBottomTrackLeftMargin((int) currentLeftMargin); } }); } /** * 设置底部跟踪指示器的左边距离 */ public void setBottomTrackLeftMargin(int bottomTrackLeftMargin) { mBottomTrackParams.leftMargin = bottomTrackLeftMargin; mBottomTrackView.setLayoutParams(mBottomTrackParams); }}
- 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
- 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
最后我们看看一些奇葩的一些需求,这是录制的效果,最后附视频地址:http://pan.baidu.com/s/1dENNO33
0 0