美文网首页Android开发经验谈Android技术知识Android开发
Android无限广告轮播 - ViewPager源码分析

Android无限广告轮播 - ViewPager源码分析

作者: 你也不知道 | 来源:发表于2020-05-11 12:54 被阅读0次

    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

    相关文章

      网友评论

        本文标题:Android无限广告轮播 - ViewPager源码分析

        本文链接:https://www.haomeiwen.com/subject/udhunhtx.html