Android無限廣告輪播 - ViewPager源碼分析

1.概述


這其實是我第一篇想寫的博客,可能是因為我遇到了太多的坑,那個時候剛入行下了很多Demo發現怎么也改不動,可能是能力有限,這次就做一個具體的實現和徹底的封裝。
  上次講了Android Studio自定義模板 做開發竟然可以如此輕松,內涵段子項目中的熱吧其實還有一個廣告輪播的功能沒寫,這里就以這個項目為例吧,附視頻講解地址:http://pan.baidu.com/s/1skOdHzn
  
  

這里寫圖片描述

2.ViewPager源碼分析


傳遞數據的方式決定采用Adapter設計模式,網上很多都采用直接傳String圖片路徑數組,但是這種方式面臨很多問題如:需要將接口進行轉化,別人下了我們控件用到項目中也難以自定義。至于什么是Adapter設計模式,可以去看一下我的這篇博客:Android設計模式源碼解析之適配器(Adapter)模式,這里就不多講了,至于有什么好處待會看代碼每個人的體會也會不一樣。下面先熟悉一下ViewPager的源碼:
  
  2.1 setAdapter方法:
  
  調用ViewPager的setAdapter函數即可將ViewPager與PagerAdapter關聯起來,我們先去查看ViewPager的setAdapter方法。

public void setAdapter(PagerAdapter adapter) {
    //1.如果已經設置過PagerAdapter,即mAdapter != null,
    // 則做一些清理工作
    if (mAdapter != null) {
        //2.清除觀察者
        mAdapter.setViewPagerObserver(null);
        //3.回調startUpdate函數,告訴PagerAdapter開始更新要顯示的頁面
        mAdapter.startUpdate(this);
        //4.如果之前保存有頁面,則將之前所有的頁面destroy掉
        for (int i = 0; i < mItems.size(); i++) {
            final ItemInfo ii = mItems.get(i);
            mAdapter.destroyItem(this, ii.position, ii.object);
        }
        //5.回調finishUpdate,告訴PagerAdapter結束更新
        mAdapter.finishUpdate(this);
        //6.將所有的頁面清除
        mItems.clear();
        //7.將所有的非Decor View移除,即將頁面移除
        removeNonDecorViews();
        //8.當前的顯示頁面重置到第一個
        mCurItem = 0;
        //9.滑動重置到(0,0)位置
        scrollTo(0, 0);
    }

    //10.保存上一次的PagerAdapter
    final PagerAdapter oldAdapter = mAdapter;
    //11.設置mAdapter為新的PagerAdapter
    mAdapter = adapter;
    //12.設置期望的適配器中的頁面數量為0個
    mExpectedAdapterCount = 0;
    //13.如果設置的PagerAdapter不為null
    if (mAdapter != null) {
        //14.確保觀察者不為null,觀察者主要是用于監視數據源的內容發生變化
        if (mObserver == null) {
            mObserver = new PagerObserver();
        }
        //15.將觀察者設置到PagerAdapter中
        mAdapter.setViewPagerObserver(mObserver);
        mPopulatePending = false;
        //16.保存上一次是否是第一次Layout
        final boolean wasFirstLayout = mFirstLayout;
        //17.設定當前為第一次Layout
        mFirstLayout = true;
        //18.更新期望的數據源中頁面個數
        mExpectedAdapterCount = mAdapter.getCount();
        //19.如果有數據需要恢復
        if (mRestoredCurItem >= 0) {
            //20.回調PagerAdapter的restoreState函數
            mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader);
            setCurrentItemInternal(mRestoredCurItem, false, true);
            //21.標記無需再恢復
            mRestoredCurItem = -1;
            mRestoredAdapterState = null;
            mRestoredClassLoader = null;
        } else if (!wasFirstLayout) {//如果在此之前不是第一次Layout
            //22.由于ViewPager并不是將所有頁面作為子View,
            // 而是最多緩存用戶指定緩存個數*2(左右兩邊,可能左邊或右邊沒有那么多頁面)
            //因此需要創建和銷毀頁面,populate主要工作就是這些
            populate();
        } else {
            //23.重新布局(Layout)
            requestLayout();
        }
    }
    //24.如果PagerAdapter發生變化,并且設置了OnAdapterChangeListener監聽器
    // 則回調OnAdapterChangeListener的onAdapterChanged函數
    if (mAdapterChangeListener != null && oldAdapter != adapter) {
        mAdapterChangeListener.onAdapterChanged(oldAdapter, adapter);
    }
}

什么觀察者模式我們可以先不去管,只需要關注我們想要分析的內容即可。我們可以看到這個populate主要創建和銷毀頁面,里面又調用這個方法populate(int newCurrentItem) 而newCurrentItem表示當需要定位顯示的頁面。我們先看看源碼:
  
  
2.2. populate(int newCurrentItem)方法

    void populate(int newCurrentItem) {
    ItemInfo oldCurInfo = null;
    if (mCurItem != newCurrentItem) {
        oldCurInfo = infoForPosition(mCurItem);
        mCurItem = newCurrentItem;
    }

    if (mAdapter == null) {
        //對子View的繪制順序進行排序,優先繪制Decor View
        //再按照position從小到大排序
        sortChildDrawingOrder();
        return;
    }

    //如果我們正在等待populate,那么在用戶手指抬起切換到新的位置期間應該推遲創建子View,
    // 直到滾動到最終位置再去創建,以免在這個期間出現差錯
    if (mPopulatePending) {
        if (DEBUG) Log.i(TAG, "populate is pending, skipping for now...");
        //對子View的繪制順序進行排序,優先繪制Decor View
        //再按照position從小到大排序
        sortChildDrawingOrder();
        return;
    }

    //同樣,在ViewPager沒有attached到window之前,不要populate.
    // 這是因為如果我們在恢復View的層次結構之前進行populate,可能會與要恢復的內容有沖突
    if (getWindowToken() == null) {
        return;
    }
    //回調PagerAdapter的startUpdate函數,
    // 告訴PagerAdapter開始更新要顯示的頁面
    mAdapter.startUpdate(this);

    final int pageLimit = mOffscreenPageLimit;
    //確保起始位置大于等于0,如果用戶設置了緩存頁面數量,第一個頁面為當前頁面減去緩存頁面數量
    final int startPos = Math.max(0, mCurItem - pageLimit);
    //保存數據源中的數據個數
    final int N = mAdapter.getCount();
    //確保最后的位置小于等于數據源中數據個數-1,
    // 如果用戶設置了緩存頁面數量,第一個頁面為當前頁面加緩存頁面數量
    final int endPos = Math.min(N - 1, mCurItem + pageLimit);

    //判斷用戶是否增減了數據源的元素,如果增減了且沒有調用notifyDataSetChanged,則拋出異常
    if (N != mExpectedAdapterCount) {
        //resName用于拋異常顯示
        String resName;
        try {
            resName = getResources().getResourceName(getId());
        } catch (Resources.NotFoundException e) {
            resName = Integer.toHexString(getId());
        }
        throw new IllegalStateException("The application's PagerAdapter changed the adapter's" +
                " contents without calling PagerAdapter#notifyDataSetChanged!" +
                " Expected adapter item count: " + mExpectedAdapterCount + ", found: " + N +
                " Pager id: " + resName +
                " Pager class: " + getClass() +
                " Problematic adapter: " + mAdapter.getClass());
    }

    //定位到當前獲焦的頁面,如果沒有的話,則添加一個
    int curIndex = -1;
    ItemInfo curItem = null;
    //遍歷每個頁面對應的ItemInfo,找出獲焦頁面
    for (curIndex = 0; curIndex < mItems.size(); curIndex++) {
        final ItemInfo ii = mItems.get(curIndex);
        //找到當前頁面對應的ItemInfo后,跳出循環
        if (ii.position >= mCurItem) {
            if (ii.position == mCurItem) curItem = ii;
            break;
        }
    }
    //如果沒有找到獲焦的頁面,說明mItems列表里面沒有保存獲焦頁面,
    // 需要將獲焦頁面加入到mItems里面
    if (curItem == null && N > 0) {
        curItem = addNewItem(mCurItem, curIndex);
    }

    //默認緩存當前頁面的左右兩邊的頁面,如果用戶設定了緩存頁面數量,
    // 則將當前頁面兩邊都緩存用戶指定的數量的頁面
    //如果當前沒有頁面,則我們啥也不需要做
    if (curItem != null) {
        float extraWidthLeft = 0.f;
        //左邊的頁面
        int itemIndex = curIndex - 1;
        //如果當前頁面左邊有頁面,則將左邊頁面對應的ItemInfo取出,否則左邊頁面的ItemInfo為null
        ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
        //保存顯示區域的寬度
        final int clientWidth = getClientWidth();
        //算出左邊頁面需要的寬度,注意,這里的寬度是指實際寬度與可視區域寬度比例,
        // 即實際寬度=leftWidthNeeded*clientWidth
        final float leftWidthNeeded = clientWidth <= 0 ? 0 :
                2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth;
        //從當前頁面左邊第一個頁面開始,左邊的頁面進行遍歷
        for (int pos = mCurItem - 1; pos >= 0; pos--) {
            //如果左邊的寬度超過了所需的寬度,并且當前當前頁面位置比第一個緩存頁面位置小
            //這說明這個頁面需要Destroy掉
            if (extraWidthLeft >= leftWidthNeeded && pos < startPos) {
                //如果左邊已經沒有頁面了,跳出循環
                if (ii == null) {
                    break;
                }
                //將當前頁面destroy掉
                if (pos == ii.position && !ii.scrolling) {
                    mItems.remove(itemIndex);
                    //回調PagerAdapter的destroyItem
                    mAdapter.destroyItem(this, pos, ii.object);
                    if (DEBUG) {
                        Log.i(TAG, "populate() - destroyItem() with pos: " + pos +
                                " view: " + ((View) ii.object));
                    }
                    //由于mItems刪除了一個元素
                    //需要將索引減一
                    itemIndex--;
                    curIndex--;
                    ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
                }
            } else if (ii != null && pos == ii.position) {
                //如果當前位置是需要緩存的位置,并且這個位置上的頁面已經存在
                //則將左邊寬度加上當前位置的頁面
                extraWidthLeft += ii.widthFactor;
                //mItems往左遍歷
                itemIndex--;
                //ii設置為當前遍歷的頁面的左邊一個頁面
                ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
            } else {//如果當前位置是需要緩存,并且這個位置沒有頁面
                //需要添加一個ItemInfo,而addNewItem是通過PagerAdapter的instantiateItem獲取對象
                ii = addNewItem(pos, itemIndex + 1);
                //將左邊寬度加上當前位置的頁面
                extraWidthLeft += ii.widthFactor;
                //由于新加了一個元素,當前的索引號需要加1
                curIndex++;
                //ii設置為當前遍歷的頁面的左邊一個頁面
                ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
            }
        }
        //同理,右邊需要添加緩存的頁面
        //......

       // 省略右邊添加緩存頁面代碼  

       //......

        calculatePageOffsets(curItem, curIndex, oldCurInfo);
    }

    if (DEBUG) {
        Log.i(TAG, "Current page list:");
        for (int i = 0; i < mItems.size(); i++) {
            Log.i(TAG, "#" + i + ": page " + mItems.get(i).position);
        }
    }
    //回調PagerAdapter的setPrimaryItem,告訴PagerAdapter當前顯示的頁面
    mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null);
    //回調PagerAdapter的finishUpdate,告訴PagerAdapter頁面更新結束
    mAdapter.finishUpdate(this);


    //檢查頁面的寬度是否測量,如果頁面的LayoutParams數據沒有設定,則去重新設定好
    final int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
        final View child = getChildAt(i);
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        lp.childIndex = i;
        if (!lp.isDecor && lp.widthFactor == 0.f) {
            // 0 means requery the adapter for this, it doesn't have a valid width.
            final ItemInfo ii = infoForChild(child);
            if (ii != null) {
                lp.widthFactor = ii.widthFactor;
                lp.position = ii.position;
            }
        }
    }
    //重新對頁面排序
    sortChildDrawingOrder();
    //如果ViewPager被設定為可獲焦的,則將當前顯示的頁面設定為獲焦
    if (hasFocus()) {
        View currentFocused = findFocus();
        ItemInfo ii = currentFocused != null ? infoForAnyChild(currentFocused) : null;
        if (ii == null || ii.position != mCurItem) {
            for (int i = 0; i < getChildCount(); i++) {
                View child = getChildAt(i);
                ii = infoForChild(child);
                if (ii != null && ii.position == mCurItem) {
                    if (child.requestFocus(View.FOCUS_FORWARD)) {
                        break;
                    }
                }
            }
        }
    }
}

從這個方法可以看出,ViewPager里面有多少界面都不會卡,會不斷的去銷毀和創建頁面,默認不光會創建當前頁面,還會創建相鄰的offscreenPageLimit頁面,offscreenPageLimit代表相鄰頁面的個數,可以由用戶通過setOffscreenPageLimit()方法指定,默認是1。而創建會調用PagerAdapter的instantiateItem()方法有一個過時了我們不要用過時的,而銷毀會調用PagerAdapter的destroyItem()。具體的工作流程我在這里畫個圖:
  

這里寫圖片描述

  
  
2.4. setCurrentItem(int item)方法
  我們來看看這最后一個方法吧,待會我們需要去改變切換的速率,就是頁面切換的速度不能太快。還是只關注需要分析的方法smoothScrollTo()。

     /**
     * Like {@link View#scrollBy}, but scroll smoothly instead of immediately.
     *
     * @param x the number of pixels to scroll by on the X axis
     * @param y the number of pixels to scroll by on the Y axis
     * @param velocity the velocity associated with a fling, if applicable. (0 otherwise)
     */
    void smoothScrollTo(int x, int y, int velocity) {
        if (getChildCount() == 0) {
            // Nothing to do.
            setScrollingCacheEnabled(false);
            return;
        }

        int sx;
        boolean wasScrolling = (mScroller != null) && !mScroller.isFinished();
        if (wasScrolling) {
            // We're in the middle of a previously initiated scrolling. Check to see
            // whether that scrolling has actually started (if we always call getStartX
            // we can get a stale value from the scroller if it hadn't yet had its first
            // computeScrollOffset call) to decide what is the current scrolling position.
            sx = mIsScrollStarted ? mScroller.getCurrX() : mScroller.getStartX();
            // And abort the current scrolling.
            mScroller.abortAnimation();
            setScrollingCacheEnabled(false);
        } else {
            sx = getScrollX();
        }
        int sy = getScrollY();
        int dx = x - sx;
        int dy = y - sy;
        if (dx == 0 && dy == 0) {
            completeScroll(false);
            // 又是這個方法,又是你我認識
            populate();
            setScrollState(SCROLL_STATE_IDLE);
            return;
        }

        setScrollingCacheEnabled(true);
        setScrollState(SCROLL_STATE_SETTLING);

        final int width = getClientWidth();
        final int halfWidth = width / 2;
        final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dx) / width);
        final float distance = halfWidth + halfWidth *
                distanceInfluenceForSnapDuration(distanceRatio);
        // 頁面切換的持續時間
        int duration;
        velocity = Math.abs(velocity);
        if (velocity > 0) {
            duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
        } else {
            final float pageWidth = width * mAdapter.getPageWidth(mCurItem);
            final float pageDelta = (float) Math.abs(dx) / (pageWidth + mPageMargin);
            duration = (int) ((pageDelta + 1) * 100);
        }
        // 切換頁面的時間  
        duration = Math.min(duration, MAX_SETTLE_DURATION);

        // Reset the "scroll started" flag. It will be flipped to true in all places
        // where we call computeScrollOffset().
        mIsScrollStarted = false;
        // 切換頁面最終會調用Scroller的startScroll()方法
        mScroller.startScroll(sx, sy, dx, dy, duration);
        ViewCompat.postInvalidateOnAnimation(this);
    }

有了這幾個方法,我們接下來就利用Adapter設計模式來實現我們的自定義無限廣告輪播。為了避免博客太長大家請看Android無限廣告輪播 - 自定義BannerView

這里寫圖片描述

  
  如果實在還是看不太懂,可以看一下我錄的頻,也可以了解一下內涵段子整個項目的其他東西:http://pan.baidu.com/s/1skOdHzn

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容