打造炫酷通用的ViewPager指示器 Adapter模式适配所有 1
###1.概述
上一期我們已經(jīng)寫了一篇 打造炫酷通用的ViewPager指示器 - 玩轉(zhuǎn)字體變色 可是這種效果雖然絢爛可以裝裝A和C之間,但是在實(shí)際的大多數(shù)效果中并不常見,只是在內(nèi)涵段子中有這個(gè)效果而已,那么這一期我們就用Adapter適配器模式適配所有的效果,堪稱終結(jié)者。附視頻地址:http://pan.baidu.com/s/1dENNO33
###2.效果實(shí)現(xiàn)
2.1 整合上一個(gè)實(shí)例:
我還是還是拿上一個(gè)實(shí)例來做演示吧。這里我貼幾種常見的效果,首先聲明Android自帶的有這個(gè)控件叫TabLayout,大家可以自己用用試試看好用不?我也用過但是不做任何評(píng)價(jià),自己造的輪子還是想怎么用就怎么用。
還有一些奇葩的效果如每個(gè)頭部Item布局不一樣,還有上面是圖片下面是文字選中的效果各不相同等等,我們都要去適配。 2.2 實(shí)現(xiàn)思路:我在老早的時(shí)候用過ViewPageIndicator,還沒畢業(yè)出來工作的時(shí)候,好不好用我也不做評(píng)價(jià),就是那個(gè)時(shí)候搞了一晚上沒搞出來第二天一看原來是activity的Theme主題沒有配置,大家手上肯定也有類似的效果也都可以用,只是以個(gè)人的理解來自己造一個(gè)輪子。 2.2.1 控件肯定是繼承ScrollView因?yàn)榭梢宰笥一瑒?dòng),如果再去自定義ViewGroup肯定不劃算。 2.2.2 怎樣才能適合所有的效果,難道我們把所有可能出現(xiàn)的效果都寫一遍嗎?這的確不太可能,所以肯定采用Adapter適配器模式。 2.2.3 我們先動(dòng)起來從簡單的入手,先做到動(dòng)態(tài)的添加不同的布局條目再說吧。
2.3 自定義TrackIndicatorView動(dòng)態(tài)添加布局:
這里為了適配所有效果,所以決定采用適配器Adapter設(shè)計(jì)模式,上面也提到過。至于什么是適配器模式大家需要看一下這個(gè) Android設(shè)計(jì)模式源碼解析之適配器(Adapter)模式 這是理論篇,但是仔細(xì)看過我博客的哥們應(yīng)該知道我其實(shí) Adapter設(shè)計(jì)模式理論與實(shí)踐相結(jié)合寫過很多效果和框架了。這里不做過多的講解,寫著寫著看著看著就會(huì)了就理解了。
2.3.1 我們?cè)僖膊荒苤苯觽髯址當(dāng)?shù)組或是傳對(duì)象數(shù)組過去讓自定義View去處理了,所以我們先確定一個(gè)自定義的Adapter類,getCount() 和 getView(int position,ViewGroup parent) 先用這兩個(gè)方法吧后面想到了再說。
/*** Created by Darren on 2016/12/7.* Email: 240336124@qq.com* Description: 指示器的適配器*/ public abstract class IndicatorBaseAdapter{// 獲取總的條數(shù)public abstract int getCount();// 根據(jù)當(dāng)前的位置獲取Viewpublic abstract View getView(int position,ViewGroup parent); } 復(fù)制代碼2.3.2 然后我們來實(shí)現(xiàn)指示器的自定義View,TrackIndicatorView 繼承自 HorizontalScrollView 。然后我們利用傳遞過來的Adapter再去動(dòng)態(tài)的添加,我這里就直接上代碼吧
/*** Created by Darren on 2016/12/13.* Email: 240336124@qq.com* Description: ViewPager指示器*/public class TrackIndicatorView extends HorizontalScrollView {// 自定義適配器private IndicatorBaseAdapter mAdapter;// Item的容器因?yàn)镾crollView只允許加入一個(gè)孩子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);// 初始化Indicator容器用來存放itemmIndicatorContainer = new LinearLayout(context);addView(mIndicatorContainer);}public void setAdapter(IndicatorBaseAdapter adapter) {if (adapter == null) {throw new NullPointerException("Adapter cannot be null!");}this.mAdapter = adapter;// 獲取Item個(gè)數(shù)int count = mAdapter.getCount();// 動(dòng)態(tài)添加到布局容器for (int i = 0; i < count; i++) {View indicatorView = mAdapter.getView(i, mIndicatorContainer);mIndicatorContainer.addView(indicatorView);}} } 復(fù)制代碼效果可想而知,可以寫一個(gè)Activity測試一下,目前可以動(dòng)態(tài)的添加多個(gè)不同樣式的布局,如果超出一個(gè)屏幕可以左右滑動(dòng),我這里就不做演示,待會(huì)一起吧。 2.3.3 動(dòng)態(tài)的制定指示器Item的寬度: 目前我們雖然能夠動(dòng)態(tài)的去添加各種布局,但是Item的寬度是任意的,我們需要在布局文件中指定一屏顯示多少個(gè),如果沒有指定那么就獲取Item中最寬的一個(gè),如果不夠一屏顯示就默認(rèn)顯示一屏。我們需要使用自定義屬性,這里就不做過多的講,實(shí)在不行大家就自己去看看有關(guān)自定義屬性的博客或是直接google搜索一下。
// 獲取一屏顯示多少個(gè)Item,默認(rèn)是0private int mTabVisibleNums = 0;// 每個(gè)Item的寬度private int mItemWidth = 0;public TrackIndicatorView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);// 之前代碼省略...// 獲取自定義屬性值 一屏顯示多少個(gè)TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.TrackIndicatorView);mTabVisibleNums = array.getInt(R.styleable.TrackIndicatorView_tabVisibleNums,mTabVisibleNums);array.recycle();}@Overrideprotected void onLayout(boolean changed, int l, int t, int r, int b) {super.onLayout(changed, l, t, r, b);if (changed) {// 指定Item的寬度mItemWidth = getItemWidth();int itemCounts = mAdapter.getCount();for (int i = 0; i < itemCounts; i++) {// 指定每個(gè)Item的寬度mIndicatorContainer.getChildAt(i).getLayoutParams().width = mItemWidth;}Log.e(TAG, "mItemWidth -> " + mItemWidth);}}/*** 獲取每一個(gè)條目的寬度*/public int getItemWidth() {int itemWidth = 0;// 獲取當(dāng)前控件的寬度int width = getWidth();if (mTabVisibleNums != 0) {// 在布局文件中指定一屏幕顯示多少個(gè)itemWidth = width / mTabVisibleNums;return itemWidth;}// 如果沒有指定獲取最寬的一個(gè)作為ItemWidthint 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;// 如果不足一個(gè)屏那么寬度就為 width/mItemCountsif (allWidth < width) {itemWidth = width / mItemCounts;}return itemWidth;} 復(fù)制代碼目前我們各種情況都測試了一下,一種是直接在布局文件中指定一屏可見顯示4個(gè),一種是不指定就默認(rèn)以最大的Item的寬度為準(zhǔn),最后一種就是不指定又不足一個(gè)屏幕默認(rèn)就顯示一屏。看一下效果吧
2.4結(jié)合ViewPager 接下來我們就需要結(jié)合ViewPager了,也就需要實(shí)現(xiàn)一系列重要的效果: 2.4.1. 當(dāng)ViewPager滾動(dòng)的時(shí)候頭部需要自動(dòng)將當(dāng)前Item滾動(dòng)到最中心; 2.4.2. 點(diǎn)擊Item之后ViewPager能夠切換到對(duì)應(yīng)的頁面; 2.4.3. 需要頁面切換之后需要回調(diào),讓用戶切換當(dāng)前選中的狀態(tài),需要在Adapter中增加方法; 2.4.4. 有些效果需要加入指示器,但并不是每種效果都需要
2.4.1. 當(dāng)ViewPager滾動(dòng)的時(shí)候頭部自動(dòng)將當(dāng)前Item滾動(dòng)到最中心 我們目前不光需要Adapter,還需要一個(gè)參數(shù)就是ViewPager,需要監(jiān)聽ViewPager的滾動(dòng)事件
/*** 重載一個(gè)setAdapter的方法* @param adapter 適配器* @param viewPager 聯(lián)動(dòng)的ViewPager*/public void setAdapter(IndicatorBaseAdapter adapter, ViewPager viewPager) {// 直接調(diào)用重載方法setAdapter(adapter);// 為ViewPager添加滾動(dòng)監(jiān)聽事件this.mViewPager = viewPager;mViewPager.addOnPageChangeListener(this);}@Overridepublic void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {// 在ViewPager滾動(dòng)的時(shí)候會(huì)不斷的調(diào)用該方法Log.e(TAG,"position --> "+position+" positionOffset --> "+positionOffset);// 在不斷滾動(dòng)的時(shí)候讓頭部的當(dāng)前Item一直保持在最中心indicatorScrollTo(position,positionOffset);}/*** 不斷的滾動(dòng)頭部*/private void indicatorScrollTo(int position, float positionOffset) {// 當(dāng)前的偏移量int currentOffset = (int) ((position + positionOffset) * mItemWidth);// 原始的左邊的偏移量int originLeftOffset = (getWidth()-mItemWidth)/2;// 當(dāng)前應(yīng)該滾動(dòng)的位置int scrollToOffset = currentOffset - originLeftOffset;// 調(diào)用ScrollView的scrollTo方法scrollTo(scrollToOffset,0);} 復(fù)制代碼目前我們滾動(dòng)ViewPager的時(shí)候,當(dāng)前指示器條目會(huì)一直保持在最中心,activity的代碼我就沒貼出來了,這個(gè)待會(huì)可以下載我的源碼看看。我們看看效果
2.4.2. 點(diǎn)擊Item之后ViewPager能夠切換到對(duì)應(yīng)的頁面
public void setAdapter(IndicatorBaseAdapter adapter) {if (adapter == null) {throw new NullPointerException("Adapter cannot be null!");}this.mAdapter = adapter;// 獲取Item個(gè)數(shù)int count = mAdapter.getCount();// 動(dòng)態(tài)添加到布局容器for (int i = 0; i < count; i++) {View indicatorView = mAdapter.getView(i, mIndicatorContainer);mIndicatorContainer.addView(indicatorView);switchIndicatorClick(indicatorView,i);}}/*** Indicator條目點(diǎn)擊對(duì)應(yīng)切換ViewPager*/private void switchIndicatorClick(View indicatorView, final int position) {indicatorView.setOnClickListener(new OnClickListener() {@Overridepublic void onClick(View v) {if(mViewPager != null){// 對(duì)應(yīng)切換ViewPagermViewPager.setCurrentItem(position);}// IndicatorItem對(duì)應(yīng)滾動(dòng)到最中心indicatorSmoothScrollTo(position);}});}/*** 滾動(dòng)到當(dāng)前的位置帶動(dòng)畫*/private void indicatorSmoothScrollTo(int position) {// 當(dāng)前的偏移量int currentOffset = ((position) * mItemWidth);// 原始的左邊的偏移量int originLeftOffset = (getWidth()-mItemWidth)/2;// 當(dāng)前應(yīng)該滾動(dòng)的位置int scrollToOffset = currentOffset - originLeftOffset;// smoothScrollTosmoothScrollTo(scrollToOffset,0);} 復(fù)制代碼我們運(yùn)行起來之后會(huì)發(fā)現(xiàn)一個(gè)問題,我們點(diǎn)擊會(huì)切換對(duì)應(yīng)的ViewPager但是這個(gè)時(shí)候還是會(huì)調(diào)用onPageScrolled()方法,這個(gè)就比較dan疼,所以我們必須解決,如果是點(diǎn)擊我就不讓其執(zhí)行onPageScrolled()里面的代碼。 2.4.3. 需要頁面切換之后需要回調(diào),讓用戶切換當(dāng)前選中的狀態(tài),需要在Adapter中增加方法 在Adapter中增加兩個(gè)回調(diào)方法,一個(gè)是高亮當(dāng)前選中方法highLightIndicator(View view) ,恢復(fù)默認(rèn)方法restoreIndicator(View view),這兩個(gè)方法可以不用寫成抽象的,為了方便我們干脆使用泛型
/*** Created by Darren on 2016/12/7.* Email: 240336124@qq.com* Description: 指示器的適配器*/ public abstract class IndicatorBaseAdapter<Q extends View>{// 獲取總的條數(shù)public abstract int getCount();// 根據(jù)當(dāng)前的位置獲取Viewpublic abstract Q getView(int position, ViewGroup parent);// 高亮當(dāng)前位置public void highLightIndicator(Q indicatorView){}// 重置當(dāng)前位置public void restoreIndicator(Q indicatorView){} } 復(fù)制代碼TrackIndicatorView
@Overridepublic void onPageSelected(int position) {// 重置上一個(gè)位置的狀態(tài)View lastView = mIndicatorContainer.getChildAt(mCurrentPosition);mAdapter.restoreIndicator(lastView);// 高亮當(dāng)前位置的狀態(tài)mCurrentPosition = position;highLightIndicator(mCurrentPosition);}/*** 高亮當(dāng)前位置*/private void highLightIndicator(int position) {View currentView = mIndicatorContainer.getChildAt(position);mAdapter.highLightIndicator(currentView);} 復(fù)制代碼 一步兩步一步兩步總算是快到頭了,接下來我們只需要加入指示器就可以了,當(dāng)前這里面涉及到屬性動(dòng)畫,如果不是很了解那就去看一下我的視頻或者去google官網(wǎng)看一下吧。 2.4.4. 有些效果需要加入指示器,但并不是每種效果都需要 /*** Created by Darren on 2016/12/7.* Email: 240336124@qq.com* Description: 指示器的容器包括下標(biāo)*/public class IndicatorContainer extends RelativeLayout {private LinearLayout mIndicatorContainer;private Context mContext;// 底部跟蹤的Viewprivate 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;}@Overridepublic 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);// 指定一個(gè)規(guī)則添加到底部mBottomTrackParams = (LayoutParams) mBottomTrackView.getLayoutParams();mBottomTrackParams.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);// 計(jì)算和指定指示器的寬度int width = mBottomTrackParams.width;mTabWidth = mIndicatorContainer.getChildAt(0).getLayoutParams().width;if (width == ViewGroup.LayoutParams.MATCH_PARENT) {width = mTabWidth;}// 計(jì)算跟蹤的View初始左邊距離if (width < mTabWidth) {mInitLeftMargin = (mTabWidth - width) / 2;}mBottomTrackParams.leftMargin = mInitLeftMargin;}/*** 底部指示器移動(dòng)到當(dāng)前位置*/public void bottomTrackScrollTo(int position, float offset) {if (mBottomTrackView == null) return;// Log.e(TAG,"position --> "+position+" offset --> "+offset);mBottomTrackParams.leftMargin = (int) (mInitLeftMargin + (position + offset) * mTabWidth);mBottomTrackView.setLayoutParams(mBottomTrackParams);}/*** 開啟一個(gè)動(dòng)畫移動(dòng)到當(dāng)前位置*/public void smoothScrollToPosition(int position) {if (mBottomTrackView == null) return;// 獲取當(dāng)前指示器距左邊的距離final int mCurrentLeftMargin = mBottomTrackParams.leftMargin;// 計(jì)算出最終的距離final int finalLeftMargin = mTabWidth * position + mInitLeftMargin;// 用于動(dòng)畫執(zhí)行的事件final int distance = finalLeftMargin - mCurrentLeftMargin;// 利用屬性動(dòng)畫不斷的更新距離ObjectAnimator animator = ObjectAnimator.ofFloat(mBottomTrackView, "leftMargin",mCurrentLeftMargin, finalLeftMargin).setDuration(Math.abs(distance));animator.setInterpolator(new DecelerateInterpolator());animator.start();// 添加動(dòng)畫監(jiān)聽不斷的更新 leftMarginanimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {float currentLeftMargin = (float) animation.getAnimatedValue();// Log.e(TAG, "current --> " + currentLeftMargin);setBottomTrackLeftMargin((int) currentLeftMargin);}});}/*** 設(shè)置底部跟蹤指示器的左邊距離*/public void setBottomTrackLeftMargin(int bottomTrackLeftMargin) {mBottomTrackParams.leftMargin = bottomTrackLeftMargin;mBottomTrackView.setLayoutParams(mBottomTrackParams);} }復(fù)制代碼最后我們看看一些奇葩的一些需求,這是錄制的效果,最后附視頻地址:http://pan.baidu.com/s/1dENNO33
總結(jié)
以上是生活随笔為你收集整理的打造炫酷通用的ViewPager指示器 Adapter模式适配所有 1的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: iOS 生日字符串转化年龄
- 下一篇: ganglia-介绍安装(二)