天天看點

打造炫酷通用的ViewPager訓示器 - Adapter模式适配所有

1.概述

  上一期我們已經寫了一篇 打造炫酷通用的ViewPager訓示器 - 玩轉字型變色 可是這種效果雖然絢爛可以裝裝A和C之間,但是在實際的大多數效果中并不常見,隻是在内涵段子中有這個效果而已,那麼這一期我們就用Adapter擴充卡模式适配所有的效果,堪稱終結者。附視訊位址:http://pan.baidu.com/s/1dENNO33

  

  

打造炫酷通用的ViewPager訓示器 - Adapter模式适配所有

2.效果實作 

2.1 整合上一個執行個體:

  我還是還是拿上一個執行個體來做示範吧。這裡我貼幾種常見的效果,首先聲明Android自帶的有這個控件叫TabLayout,大家可以自己用用試試看好用不?我也用過但是不做任何評價,自己造的輪子還是想怎麼用就怎麼用。

打造炫酷通用的ViewPager訓示器 - Adapter模式适配所有
打造炫酷通用的ViewPager訓示器 - Adapter模式适配所有
打造炫酷通用的ViewPager訓示器 - Adapter模式适配所有

  

  

  還有一些奇葩的效果如每個頭部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: [email protected]
 * Description:  訓示器的擴充卡
 */
public abstract class IndicatorBaseAdapter{
    // 擷取總的條數
    public abstract int getCount();

    // 根據目前的位置擷取View
    public abstract View getView(int position,ViewGroup parent);
}
           

  2.3.2 然後我們來實作訓示器的自定義View,TrackIndicatorView 繼承自 HorizontalScrollView 。然後我們利用傳遞過來的Adapter再去動态的添加,我這裡就直接上代碼吧  

/**
 * Created by Darren on 2016/12/13.
 * Email: [email protected]
 * 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, );
    }

    public TestIndicator(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        // 初始化Indicator容器用來存放item
        mIndicatorContainer = 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 = ; i < count; i++) {
            View indicatorView = mAdapter.getView(i, mIndicatorContainer);
            mIndicatorContainer.addView(indicatorView);
        }
    }
}
           

  效果可想而知,可以寫一個Activity測試一下,目前可以動态的添加多個不同樣式的布局,如果超出一個螢幕可以左右滑動,我這裡就不做示範,待會一起吧。

  

  2.3.3 動态的制定訓示器Item的寬度:

  

  目前我們雖然能夠動态的去添加各種布局,但是Item的寬度是任意的,我們需要在布局檔案中指定一屏顯示多少個,如果沒有指定那麼就擷取Item中最寬的一個,如果不夠一屏顯示就預設顯示一屏。我們需要使用自定義屬性,這裡就不做過多的講,實在不行大家就自己去看看有關自定義屬性的部落格或是直接google搜尋一下。

// 擷取一屏顯示多少個Item,預設是0
    private int mTabVisibleNums = ;

    // 每個Item的寬度
    private int mItemWidth = ;

    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) {
            // 指定Item的寬度
            mItemWidth = getItemWidth();
            int itemCounts = mAdapter.getCount();
            for (int i = ; i < itemCounts; i++) {
                // 指定每個Item的寬度
                mIndicatorContainer.getChildAt(i).getLayoutParams().width = mItemWidth;
            }
            Log.e(TAG, "mItemWidth -> " + mItemWidth);
        }
    }

    /**
     * 擷取每一個條目的寬度
     */
    public int getItemWidth() {
        int itemWidth = ;
        // 擷取目前控件的寬度
        int width = getWidth();
        if (mTabVisibleNums != ) {
            // 在布局檔案中指定一螢幕顯示多少個
            itemWidth = width / mTabVisibleNums;
            return itemWidth;
        }
        // 如果沒有指定擷取最寬的一個作為ItemWidth
        int maxItemWidth = ;
        int mItemCounts = mAdapter.getCount();
        // 總的寬度
        int allWidth = ;

        for (int i = ; i < mItemCounts; i++) {
            View itemView = mIndicatorContainer.getChildAt(i);
            int childWidth = itemView.getMeasuredWidth();
            maxItemWidth = Math.max(maxItemWidth, childWidth);
            allWidth += childWidth;
        }

        itemWidth = maxItemWidth;

        // 如果不足一個屏那麼寬度就為  width/mItemCounts
        if (allWidth < width) {
            itemWidth = width / mItemCounts;
        }
        return itemWidth;
    }
           

  目前我們各種情況都測試了一下,一種是直接在布局檔案中指定一屏可見顯示4個,一種是不指定就預設以最大的Item的寬度為準,最後一種就是不指定又不足一個螢幕預設就顯示一屏。看一下效果吧

打造炫酷通用的ViewPager訓示器 - Adapter模式适配所有

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);
    }

    @Override
    public 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)/;
        // 目前應該滾動的位置
        int scrollToOffset = currentOffset - originLeftOffset;
        // 調用ScrollView的scrollTo方法
        scrollTo(scrollToOffset,);
    }
           

  目前我們滾動ViewPager的時候,目前訓示器條目會一直保持在最中心,activity的代碼我就沒貼出來了,這個待會可以下載下傳我的源碼看看。我們看看效果

  

打造炫酷通用的ViewPager訓示器 - Adapter模式适配所有

  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 = ; 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){
                    // 對應切換ViewPager
                    mViewPager.setCurrentItem(position);
                }
                // IndicatorItem對應滾動到最中心
                indicatorSmoothScrollTo(position);
            }
        });
    }

    /**
     * 滾動到目前的位置帶動畫
     */
    private void indicatorSmoothScrollTo(int position) {
        // 目前的偏移量
        int currentOffset =  ((position) * mItemWidth);
        // 原始的左邊的偏移量
        int originLeftOffset = (getWidth()-mItemWidth)/;
        // 目前應該滾動的位置
        int scrollToOffset = currentOffset - originLeftOffset;
        // smoothScrollTo
        smoothScrollTo(scrollToOffset,);
    }
           

  我們運作起來之後會發現一個問題,我們點選會切換對應的ViewPager但是這個時候還是會調用onPageScrolled()方法,這個就比較dan疼,是以我們必須解決,如果是點選我就不讓其執行onPageScrolled()裡面的代碼。

  

  2.4.3. 需要頁面切換之後需要回調,讓使用者切換目前選中的狀态,需要在Adapter中增加方法

在Adapter中增加兩個回調方法,一個是高亮目前選中方法highLightIndicator(View view) ,恢複預設方法restoreIndicator(View view),這兩個方法可以不用寫成抽象的,為了友善我們幹脆使用泛型

/**
 * Created by Darren on 2016/12/7.
 * Email: [email protected]
 * Description:  訓示器的擴充卡
 */
public abstract class IndicatorBaseAdapter<Q extends View>{
    // 擷取總的條數
    public abstract int getCount();

    // 根據目前的位置擷取View
    public abstract Q getView(int position, ViewGroup parent);

    // 高亮目前位置
    public void highLightIndicator(Q indicatorView){

    }

    // 重置目前位置
    public void restoreIndicator(Q indicatorView){

    }
}
           

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);
    }
           
打造炫酷通用的ViewPager訓示器 - Adapter模式适配所有

  一步兩步一步兩步總算是快到頭了,接下來我們隻需要加入訓示器就可以了,目前這裡面涉及到屬性動畫,如果不是很了解那就去看一下我的視訊或者去google官網看一下吧。

  

  2.4.4. 有些效果需要加入訓示器,但并不是每種效果都需要

  

/**
 * Created by Darren on 2016/12/7.
 * Email: [email protected]
 * Description:  訓示器的容器包括下标
 */

public class IndicatorContainer extends RelativeLayout {
    private LinearLayout mIndicatorContainer;
    private Context mContext;
    // 底部跟蹤的View
    private View mBottomTrackView;
    private String TAG = "IndicatorContainer";
    // 距離左邊的初始距離
    private int mInitLeftMargin = ;
    private RelativeLayout.LayoutParams mBottomTrackParams;
    private int mTabWidth;

    public IndicatorContainer(Context context) {
        this(context, null);
    }

    public IndicatorContainer(Context context, AttributeSet attrs) {
        this(context, attrs, );
    }

    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().getLayoutParams().width;
        if (width == ViewGroup.LayoutParams.MATCH_PARENT) {
            width = mTabWidth;
        }
        // 計算跟蹤的View初始左邊距離
        if (width < mTabWidth) {
            mInitLeftMargin = (mTabWidth - width) / ;
        }
        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();
        // 添加動畫監聽不斷的更新 leftMargin
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public 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);
    }
}
           

 最後我們看看一些奇葩的一些需求,這是錄制的效果

 

打造炫酷通用的ViewPager訓示器 - Adapter模式适配所有

附視訊位址:http://pan.baidu.com/s/1dENNO33