打造炫酷通用的ViewPager指示器 Adapter模式适配所有 1
###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();// 根據當前的位置獲取Viewpublic 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;// Item的容器因為ScrollView只允許加入一個孩子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個數int count = mAdapter.getCount();// 動態添加到布局容器for (int i = 0; i < count; i++) {View indicatorView = mAdapter.getView(i, mIndicatorContainer);mIndicatorContainer.addView(indicatorView);}} } 復制代碼效果可想而知,可以寫一個Activity測試一下,目前可以動態的添加多個不同樣式的布局,如果超出一個屏幕可以左右滑動,我這里就不做演示,待會一起吧。 2.3.3 動態的制定指示器Item的寬度: 目前我們雖然能夠動態的去添加各種布局,但是Item的寬度是任意的,我們需要在布局文件中指定一屏顯示多少個,如果沒有指定那么就獲取Item中最寬的一個,如果不夠一屏顯示就默認顯示一屏。我們需要使用自定義屬性,這里就不做過多的講,實在不行大家就自己去看看有關自定義屬性的博客或是直接google搜索一下。
// 獲取一屏顯示多少個Item,默認是0private int mTabVisibleNums = 0;// 每個Item的寬度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();}@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++) {// 指定每個Item的寬度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;}// 如果沒有指定獲取最寬的一個作為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;// 如果不足一個屏那么寬度就為 width/mItemCountsif (allWidth < width) {itemWidth = width / mItemCounts;}return itemWidth;} 復制代碼目前我們各種情況都測試了一下,一種是直接在布局文件中指定一屏可見顯示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);// 為ViewPager添加滾動監聽事件this.mViewPager = viewPager;mViewPager.addOnPageChangeListener(this);}@Overridepublic void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {// 在ViewPager滾動的時候會不斷的調用該方法Log.e(TAG,"position --> "+position+" positionOffset --> "+positionOffset);// 在不斷滾動的時候讓頭部的當前Item一直保持在最中心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;// 調用ScrollView的scrollTo方法scrollTo(scrollToOffset,0);} 復制代碼目前我們滾動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;// 獲取Item個數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() {@Overridepublic void onClick(View v) {if(mViewPager != null){// 對應切換ViewPagermViewPager.setCurrentItem(position);}// IndicatorItem對應滾動到最中心indicatorSmoothScrollTo(position);}});}/*** 滾動到當前的位置帶動畫*/private void indicatorSmoothScrollTo(int position) {// 當前的偏移量int currentOffset = ((position) * mItemWidth);// 原始的左邊的偏移量int originLeftOffset = (getWidth()-mItemWidth)/2;// 當前應該滾動的位置int scrollToOffset = currentOffset - originLeftOffset;// smoothScrollTosmoothScrollTo(scrollToOffset,0);} 復制代碼我們運行起來之后會發現一個問題,我們點擊會切換對應的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();// 根據當前的位置獲取Viewpublic abstract Q getView(int position, ViewGroup parent);// 高亮當前位置public void highLightIndicator(Q indicatorView){}// 重置當前位置public void restoreIndicator(Q indicatorView){} } 復制代碼TrackIndicatorView
@Overridepublic 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);} 復制代碼 一步兩步一步兩步總算是快到頭了,接下來我們只需要加入指示器就可以了,當前這里面涉及到屬性動畫,如果不是很了解那就去看一下我的視頻或者去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;// 底部跟蹤的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);// 指定一個規則添加到底部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;}// 計算跟蹤的View初始左邊距離if (width < mTabWidth) {mInitLeftMargin = (mTabWidth - width) / 2;}mBottomTrackParams.leftMargin = mInitLeftMargin;}/*** 底部指示器移動到當前位置*/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);}/*** 開啟一個動畫移動到當前位置*/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();// 添加動畫監聽不斷的更新 leftMarginanimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {float currentLeftMargin = (float) animation.getAnimatedValue();// Log.e(TAG, "current --> " + currentLeftMargin);setBottomTrackLeftMargin((int) currentLeftMargin);}});}/*** 設置底部跟蹤指示器的左邊距離*/public void setBottomTrackLeftMargin(int bottomTrackLeftMargin) {mBottomTrackParams.leftMargin = bottomTrackLeftMargin;mBottomTrackView.setLayoutParams(mBottomTrackParams);} }復制代碼最后我們看看一些奇葩的一些需求,這是錄制的效果,最后附視頻地址:http://pan.baidu.com/s/1dENNO33
總結
以上是生活随笔為你收集整理的打造炫酷通用的ViewPager指示器 Adapter模式适配所有 1的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: iOS 生日字符串转化年龄
- 下一篇: ganglia-介绍安装(二)