View

ViewPager详解

2022-05-24  本文已影响0人  独自闯天涯的码农

一、ViewPager介绍

二、ViewPager弊端分析

1、普通viewpager的实现弊端

如果你不使用setoffscreenpagelimit(int limit)这个方法去设置默认加载数的话是会默认加载页面的左右两页的,也就是说当你进入viewpager第一页的时候第二页和第一页是会被一起加载的,这样同时加载就会造成一些问题,试想我们如果设置了setoffscreenpagelimit为3的话,那么进入viewpager以后就会同时加载4个fragment,像我们平时的项目中在这些fragment中一般都是会发送网络请求的,也就是说我们有4个fragment同时发送网络请求去获取数据,这样的结果显而易见给用户的体验是不好的(如:浪费用户流量,造成卡顿等等)。

2、懒加载的实现弊端

  1. 概念:当需要时才加载,加载之后一直保持该对象。
  2. 而关于Fragment实现的PagerAdapter都没有完全保存其引用和状态。FragmentPageAdapter需要重建视图,FragmentStatePageAdapter使用状态恢复,View都被销毁,但是恢复的方式不同,而通常我们想得到的结果是,Fragment一旦被加载,其视图也不会被销毁,即不会再重新走一遍生命周期,而且ViewPager为了实现滑动效果,都是预加载左右两侧的页面。
  3. 我们通常想要实现的两种效果:不提供滑动,需要时才构造,并且只走一遍生命周期,避免在Fragment中做过多的状态保存和恢复。

3、ViewPager预加载

ViewPager的预加载机制。默认缓存当前页面的前后各一个。

public void setOffscreenPageLimit(int limit) {
    if (limit < 1) {
        Log.w("ViewPager", "Requested offscreen page limit " + limit + " too small; defaulting to " + 1);
        limit = 1;
    }

    if (limit != this.mOffscreenPageLimit) {
        this.mOffscreenPageLimit = limit;
        this.populate();
    }
}

4、预加载实现

/**
 * <pre>
 *     @author yangchong
 *     blog  : https://github.com/yangchong211
 *     time  : 2017/7/22
 *     desc  : 懒加载
 *     revise: 懒加载时机:onCreateView()方法执行完毕 + setUserVisibleHint()方法返回true
 * </pre>
 */
public abstract class BaseLazyFragment extends BaseFragment {

    /*
     * 预加载页面回调的生命周期流程:
     * setUserVisibleHint() -->onAttach() --> onCreate()-->onCreateView()-->
     *              onActivityCreate() --> onStart() --> onResume()
     */

    /**
     * 懒加载过
     */
    protected boolean isLazyLoaded = false;
    /**
     * Fragment的View加载完毕的标记
     */
    private boolean isPrepared = false;

    /**
     * 第一步,改变isPrepared标记
     * 当onViewCreated()方法执行时,表明View已经加载完毕,此时改变isPrepared标记为true,并调用lazyLoad()方法
     */
    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        isPrepared = true;
        //只有Fragment onCreateView好了
        //另外这里调用一次lazyLoad()
        lazyLoad();
    }


    /**
     * 第二步
     * 此方法会在onCreateView()之前执行
     * 当viewPager中fragment改变可见状态时也会调用
     * 当fragment 从可见到不见,或者从不可见切换到可见,都会调用此方法
     * true表示当前页面可见,false表示不可见
     */
    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        LogUtil.d("setUserVisibleHint---"+isVisibleToUser);
        //只有当fragment可见时,才进行加载数据
        if (isVisibleToUser){
            lazyLoad();
        }
    }

    /**
     * 调用懒加载
     * 第三步:在lazyLoad()方法中进行双重标记判断,通过后即可进行数据加载
     */
    private void lazyLoad() {
        if (getUserVisibleHint() && isPrepared && !isLazyLoaded) {
            showFirstLoading();
            onLazyLoad();
            isLazyLoaded = true;
        } else {
            //当视图已经对用户不可见并且加载过数据,如果需要在切换到其他页面时停止加载数据,可以覆写此方法
            if (isLazyLoaded) {
                stopLoad();
            }
        }
    }

    /**
     * 视图销毁的时候讲Fragment是否初始化的状态变为false
     */
    @Override
    public void onDestroyView() {
        super.onDestroyView();
        isLazyLoaded = false;
        isPrepared = false;
    }

    /**
     * 第一次可见时,操作该方法,可以用于showLoading操作,注意这个是全局加载loading
     */
    protected void showFirstLoading() {
        LogUtil.i("第一次可见时show全局loading");
    }

    /**
     * 停止加载
     * 当视图已经对用户不可见并且加载过数据,但是没有加载完,而只是加载loading。
     * 如果需要在切换到其他页面时停止加载数据,可以覆写此方法。
     * 存在问题,如何停止加载网络
     */
    protected void stopLoad(){

    }

    /**
     * 第四步:定义抽象方法onLazyLoad(),具体加载数据的工作,交给子类去完成
     */
    @UiThread
    protected abstract void onLazyLoad();
}

onLazyLoad()加载数据条件
getUserVisibleHint()会返回是否可见状态,这是fragment实现懒加载的关键,只有fragment 可见才会调用onLazyLoad() 加载数据。
isPrepared参数在系统调用onActivityCreated时设置为true,这时onCreateView方法已调用完毕(一般我们在这方法里执行findviewbyid等方法),确保 onLazyLoad()方法不会报空指针异常。
isLazyLoaded确保ViewPager来回切换时BaseFragment的initData方法不会被重复调用,onLazyLoad在该Fragment的整个生命周期只调用一次,第一次调用onLazyLoad()方法后马上执行 isLazyLoaded = true。
然后再继承这个BaseLazyFragment实现onLazyLoad() 方法就行。他会自动控制当fragment 展现出来时,才会加载数据

懒加载配合状态管理器
/**
 * <pre>
 *     @author yangchong
 *     blog  : https://github.com/yangchong211
 *     time  : 2017/7/20
 *     desc  : fragment的父类
 *     revise: 注意,该类具有懒加载
 * </pre>
 */
public abstract class BaseStateFragment extends BaseLazyFragment {

    protected StateLayoutManager statusLayoutManager;
    private View view;

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState) {
        if(view==null){
            view = inflater.inflate(R.layout.base_state_view, container , false);
            initStatusLayout();
            initBaseView(view);
        }
        return view;
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        initView(view);
        initListener();
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
    }

    /**
     * 获取到子布局
     * @param view              view
     */
    private void initBaseView(View view) {
        LinearLayout llStateView = view.findViewById(R.id.ll_state_view);
        llStateView.addView(statusLayoutManager.getRootLayout());
    }


    /**
     * 初始化状态管理器相关操作
     */
    protected abstract void initStatusLayout();

    /**
     * 初始化View的代码写在这个方法中
     * @param view              view
     */
    public abstract void initView(View view);

    /**
     * 初始化监听器的代码写在这个方法中
     */
    public abstract void initListener();

    /**
     * 第一次可见状态时,showLoading操作,注意下拉刷新操作时不要用该全局loading
     */
    @Override
    protected void showFirstLoading() {
        super.showFirstLoading();
        showLoading();
    }

    /*protected void initStatusLayout() {
        statusLayoutManager = StateLayoutManager.newBuilder(activity)
                .contentView(R.layout.common_fragment_list)
                .emptyDataView(R.layout.view_custom_empty_data)
                .errorView(R.layout.view_custom_data_error)
                .loadingView(R.layout.view_custom_loading_data)
                .netWorkErrorView(R.layout.view_custom_network_error)
                .build();
    }*/


    /*---------------------------------下面是状态切换方法-----------------------------------------*/


    /**
     * 加载成功
     */
    protected void showContent() {
        if (statusLayoutManager!=null){
            statusLayoutManager.showContent();
        }
    }

    /**
     * 加载无数据
     */
    protected void showEmptyData() {
        if (statusLayoutManager!=null){
            statusLayoutManager.showEmptyData();
        }
    }

    /**
     * 加载异常
     */
    protected void showError() {
        if (statusLayoutManager!=null){
            statusLayoutManager.showError();
        }
    }

    /**
     * 加载网络异常
     */
    protected void showNetWorkError() {
        if (statusLayoutManager!=null){
            statusLayoutManager.showNetWorkError();
        }
    }

    /**
     * 加载loading
     */
    protected void showLoading() {
        if (statusLayoutManager!=null){
            statusLayoutManager.showLoading();
        }
    }
}

//如何切换状态呢?
showContent();
showEmptyData();
showError();
showLoading();
showNetWorkError();

//或者这样操作也可以
statusLayoutManager.showLoading();
statusLayoutManager.showContent();

参考:ViewPager懒加载

三、ViewPager源码

public class ViewPager extends ViewGroup {

/**
销毁旧的Adapter数据,用新的Adaper更新UI
清除旧的Adapter,对已加载的item调用destroyItem,
将自身滚动到初始位置this.scrollTo(0, 0)
设置PagerObserver: mAdapter.setViewPagerObserver(mObserver);
调用populate()方法计算并初始化View(这个方法后面会详细介绍)
如果设置了OnAdapterChangeListener,进行回调
*/
public void setAdapter(@Nullable PagerAdapter adapter) {
        if (mAdapter != null) {
            mAdapter.setViewPagerObserver(null);
            mAdapter.startUpdate(this);
            for (int i = 0; i < mItems.size(); i++) {
                final ItemInfo ii = mItems.get(i);
                mAdapter.destroyItem(this, ii.position, ii.object);
            }
            mAdapter.finishUpdate(this);
            mItems.clear();
            removeNonDecorViews();
            mCurItem = 0;
            scrollTo(0, 0);
        }

        final PagerAdapter oldAdapter = mAdapter;
        mAdapter = adapter;
        mExpectedAdapterCount = 0;

        if (mAdapter != null) {
            if (mObserver == null) {
                mObserver = new PagerObserver();
            }
            mAdapter.setViewPagerObserver(mObserver);
            mPopulatePending = false;
            final boolean wasFirstLayout = mFirstLayout;
            mFirstLayout = true;
            mExpectedAdapterCount = mAdapter.getCount();
            if (mRestoredCurItem >= 0) {
                mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader);
                setCurrentItemInternal(mRestoredCurItem, false, true);
                mRestoredCurItem = -1;
                mRestoredAdapterState = null;
                mRestoredClassLoader = null;
            } else if (!wasFirstLayout) {
                populate();
            } else {
                requestLayout();
            }
        }

        // Dispatch the change to any listeners
        if (mAdapterChangeListeners != null && !mAdapterChangeListeners.isEmpty()) {
            for (int i = 0, count = mAdapterChangeListeners.size(); i < count; i++) {
                mAdapterChangeListeners.get(i).onAdapterChanged(this, oldAdapter, adapter);
            }
        }
    }

/**
该方法是ViewPager非常重要的方法,主要根据参数newCurrentItem和mOffscreenPageLimit计算出需要初始化的页面和需要销毁页面,然后通过调用Adapter的instantiateItem和destroyItem两个方法初始化新页面和销毁不需要的页面!
根据newCurrentItem和mOffscreenPageLimit计算要加载的page页面,计算出startPos和endPos
根据startPos和endPos初始化页面ItemInfo,先从缓存里面获取,如果没有就调用addNewItem方法,实际调用mAdapter.instantiateItem
将不需要的ItemInfo移除: mItems.remove(itemIndex),并调用mAdapter.destroyItem方法
设置LayoutParams参数(包括position和widthFactor),根据position排序待绘制的View列表:mDrawingOrderedChildren,重写了getChildDrawingOrder方法
最后一步获取当前显示View的焦点:currView.requestFocus(View.FOCUS_FORWARD)
*/
void populate(int newCurrentItem) {
        ItemInfo oldCurInfo = null;
        if (mCurItem != newCurrentItem) {
            oldCurInfo = infoForPosition(mCurItem);
            mCurItem = newCurrentItem;
        }

        if (mAdapter == null) {
            sortChildDrawingOrder();
            return;
        }

        // Bail now if we are waiting to populate.  This is to hold off
        // on creating views from the time the user releases their finger to
        // fling to a new position until we have finished the scroll to
        // that position, avoiding glitches from happening at that point.
        if (mPopulatePending) {
            if (DEBUG) Log.i(TAG, "populate is pending, skipping for now...");
            sortChildDrawingOrder();
            return;
        }

        // Also, don't populate until we are attached to a window.  This is to
        // avoid trying to populate before we have restored our view hierarchy
        // state and conflicting with what is restored.
        if (getWindowToken() == null) {
            return;
        }

        mAdapter.startUpdate(this);

        final int pageLimit = mOffscreenPageLimit;
        final int startPos = Math.max(0, mCurItem - pageLimit);
        final int N = mAdapter.getCount();
        final int endPos = Math.min(N - 1, mCurItem + pageLimit);

        if (N != mExpectedAdapterCount) {
            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());
        }

        // Locate the currently focused item or add it if needed.
        int curIndex = -1;
        ItemInfo curItem = null;
        for (curIndex = 0; curIndex < mItems.size(); curIndex++) {
            final ItemInfo ii = mItems.get(curIndex);
            if (ii.position >= mCurItem) {
                if (ii.position == mCurItem) curItem = ii;
                break;
            }
        }

        if (curItem == null && N > 0) {
            curItem = addNewItem(mCurItem, curIndex);
        }

        // Fill 3x the available width or up to the number of offscreen
        // pages requested to either side, whichever is larger.
        // If we have no current item we have no work to do.
        if (curItem != null) {
            float extraWidthLeft = 0.f;
            int itemIndex = curIndex - 1;
            ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
            final int clientWidth = getClientWidth();
            final float leftWidthNeeded = clientWidth <= 0 ? 0 :
                    2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth;
            for (int pos = mCurItem - 1; pos >= 0; pos--) {
                if (extraWidthLeft >= leftWidthNeeded && pos < startPos) {
                    if (ii == null) {
                        break;
                    }
                    if (pos == ii.position && !ii.scrolling) {
                        mItems.remove(itemIndex);
                        mAdapter.destroyItem(this, pos, ii.object);
                        if (DEBUG) {
                            Log.i(TAG, "populate() - destroyItem() with pos: " + pos
                                    + " view: " + ((View) ii.object));
                        }
                        itemIndex--;
                        curIndex--;
                        ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
                    }
                } else if (ii != null && pos == ii.position) {
                    extraWidthLeft += ii.widthFactor;
                    itemIndex--;
                    ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
                } else {
                    ii = addNewItem(pos, itemIndex + 1);
                    extraWidthLeft += ii.widthFactor;
                    curIndex++;
                    ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
                }
            }

            float extraWidthRight = curItem.widthFactor;
            itemIndex = curIndex + 1;
            if (extraWidthRight < 2.f) {
                ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
                final float rightWidthNeeded = clientWidth <= 0 ? 0 :
                        (float) getPaddingRight() / (float) clientWidth + 2.f;
                for (int pos = mCurItem + 1; pos < N; pos++) {
                    if (extraWidthRight >= rightWidthNeeded && pos > endPos) {
                        if (ii == null) {
                            break;
                        }
                        if (pos == ii.position && !ii.scrolling) {
                            mItems.remove(itemIndex);
                            mAdapter.destroyItem(this, pos, ii.object);
                            if (DEBUG) {
                                Log.i(TAG, "populate() - destroyItem() with pos: " + pos
                                        + " view: " + ((View) ii.object));
                            }
                            ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
                        }
                    } else if (ii != null && pos == ii.position) {
                        extraWidthRight += ii.widthFactor;
                        itemIndex++;
                        ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
                    } else {
                        ii = addNewItem(pos, itemIndex);
                        itemIndex++;
                        extraWidthRight += ii.widthFactor;
                        ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
                    }
                }
            }

            calculatePageOffsets(curItem, curIndex, oldCurInfo);

            mAdapter.setPrimaryItem(this, mCurItem, curItem.object);
        }

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

        mAdapter.finishUpdate(this);

        // Check width measurement of current pages and drawing sort order.
        // Update LayoutParams as needed.
        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();

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


/**
当调用Adapter的notifyDataSetChanged时,会触发这个方法,该方法会重新计算当前页面的position,
移除需要销毁的页面的ItemInfo对象,然后再调用populate方法刷新页面
循环mItems(每个page对应的ItemInfo对象),调用int newPos = mAdapter.getItemPosition方法
当newPos等于PagerAdapter.POSITION_UNCHANGED表示当前页面不需要更新,不用销毁,当newPos等于PagerAdapter.POSITION_NONE时,需要更新,移除item,调用mAdapter.destroyItem
循环完成后,最后计算出显示页面的newCurrItem,调用setCurrentItemInternal(newCurrItem, false, true)方法更新UI(实际调用populate方法重新计算页面信息)
*/
void dataSetChanged() {
        // This method only gets called if our observer is attached, so mAdapter is non-null.

        final int adapterCount = mAdapter.getCount();
        mExpectedAdapterCount = adapterCount;
        boolean needPopulate = mItems.size() < mOffscreenPageLimit * 2 + 1
                && mItems.size() < adapterCount;
        int newCurrItem = mCurItem;

        boolean isUpdating = false;
        for (int i = 0; i < mItems.size(); i++) {
            final ItemInfo ii = mItems.get(i);
            final int newPos = mAdapter.getItemPosition(ii.object);

            if (newPos == PagerAdapter.POSITION_UNCHANGED) {
                continue;
            }

            if (newPos == PagerAdapter.POSITION_NONE) {
                mItems.remove(i);
                i--;

                if (!isUpdating) {
                    mAdapter.startUpdate(this);
                    isUpdating = true;
                }

                mAdapter.destroyItem(this, ii.position, ii.object);
                needPopulate = true;

                if (mCurItem == ii.position) {
                    // Keep the current item in the valid range
                    newCurrItem = Math.max(0, Math.min(mCurItem, adapterCount - 1));
                    needPopulate = true;
                }
                continue;
            }

            if (ii.position != newPos) {
                if (ii.position == mCurItem) {
                    // Our current item changed position. Follow it.
                    newCurrItem = newPos;
                }

                ii.position = newPos;
                needPopulate = true;
            }
        }

        if (isUpdating) {
            mAdapter.finishUpdate(this);
        }

        Collections.sort(mItems, COMPARATOR);

        if (needPopulate) {
            // Reset our known page widths; populate will recompute them.
            final int childCount = getChildCount();
            for (int i = 0; i < childCount; i++) {
                final View child = getChildAt(i);
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                if (!lp.isDecor) {
                    lp.widthFactor = 0.f;
                }
            }

            setCurrentItemInternal(newCurrItem, false, true);
            requestLayout();
        }
    }


/*
滑动到指定页面,内部会触发OnPageChangeListener
*/
private void scrollToItem(int item, boolean smoothScroll, int velocity,
            boolean dispatchSelected) {
        final ItemInfo curInfo = infoForPosition(item);
        int destX = 0;
        if (curInfo != null) {
            final int width = getClientWidth();
            destX = (int) (width * Math.max(mFirstOffset,
                    Math.min(curInfo.offset, mLastOffset)));
        }
        if (smoothScroll) {
            smoothScrollTo(destX, 0, velocity);
            if (dispatchSelected) {
                dispatchOnPageSelected(item);
            }
        } else {
            if (dispatchSelected) {
                dispatchOnPageSelected(item);
            }
            completeScroll(false);
            scrollTo(destX, 0);
            pageScrolled(destX);
        }
    }

/**
这个方法主要用于计算每个页面对应ItemInfo的offset变量,这个变量用于记录当前view在所有缓存View中(包含当前显示页)的索引,用于布局的时候计算该View应该放在哪个位置
在populate方法中更新完页面数据后,会调用该方法计算所有页面的offset
*/
private void calculatePageOffsets(ItemInfo curItem, int curIndex, ItemInfo oldCurInfo) {
        final int N = mAdapter.getCount();
        final int width = getClientWidth();
        final float marginOffset = width > 0 ? (float) mPageMargin / width : 0;
        // Fix up offsets for later layout.
        if (oldCurInfo != null) {
            final int oldCurPosition = oldCurInfo.position;
            // Base offsets off of oldCurInfo.
            if (oldCurPosition < curItem.position) {
                int itemIndex = 0;
                ItemInfo ii = null;
                float offset = oldCurInfo.offset + oldCurInfo.widthFactor + marginOffset;
                for (int pos = oldCurPosition + 1;
                        pos <= curItem.position && itemIndex < mItems.size(); pos++) {
                    ii = mItems.get(itemIndex);
                    while (pos > ii.position && itemIndex < mItems.size() - 1) {
                        itemIndex++;
                        ii = mItems.get(itemIndex);
                    }
                    while (pos < ii.position) {
                        // We don't have an item populated for this,
                        // ask the adapter for an offset.
                        offset += mAdapter.getPageWidth(pos) + marginOffset;
                        pos++;
                    }
                    ii.offset = offset;
                    offset += ii.widthFactor + marginOffset;
                }
            } else if (oldCurPosition > curItem.position) {
                int itemIndex = mItems.size() - 1;
                ItemInfo ii = null;
                float offset = oldCurInfo.offset;
                for (int pos = oldCurPosition - 1;
                        pos >= curItem.position && itemIndex >= 0; pos--) {
                    ii = mItems.get(itemIndex);
                    while (pos < ii.position && itemIndex > 0) {
                        itemIndex--;
                        ii = mItems.get(itemIndex);
                    }
                    while (pos > ii.position) {
                        // We don't have an item populated for this,
                        // ask the adapter for an offset.
                        offset -= mAdapter.getPageWidth(pos) + marginOffset;
                        pos--;
                    }
                    offset -= ii.widthFactor + marginOffset;
                    ii.offset = offset;
                }
            }
        }

        // Base all offsets off of curItem.
        final int itemCount = mItems.size();
        float offset = curItem.offset;
        int pos = curItem.position - 1;
        mFirstOffset = curItem.position == 0 ? curItem.offset : -Float.MAX_VALUE;
        mLastOffset = curItem.position == N - 1
                ? curItem.offset + curItem.widthFactor - 1 : Float.MAX_VALUE;
        // Previous pages
        for (int i = curIndex - 1; i >= 0; i--, pos--) {
            final ItemInfo ii = mItems.get(i);
            while (pos > ii.position) {
                offset -= mAdapter.getPageWidth(pos--) + marginOffset;
            }
            offset -= ii.widthFactor + marginOffset;
            ii.offset = offset;
            if (ii.position == 0) mFirstOffset = offset;
        }
        offset = curItem.offset + curItem.widthFactor + marginOffset;
        pos = curItem.position + 1;
        // Next pages
        for (int i = curIndex + 1; i < itemCount; i++, pos++) {
            final ItemInfo ii = mItems.get(i);
            while (pos < ii.position) {
                offset += mAdapter.getPageWidth(pos++) + marginOffset;
            }
            if (ii.position == N - 1) {
                mLastOffset = offset + ii.widthFactor - 1;
            }
            ii.offset = offset;
            offset += ii.widthFactor + marginOffset;
        }

        mNeedCalculatePageOffsets = false;
    }


}

四、ViewPager转换动画实现

  /**
每当滚动可见/附加页面时都会调用 PageTransformer。这为应用程序提供了使用动画属性对页面视图应用自定义转换的机会。
由于仅从 Android 3.0 及更高版本开始支持属性动画,因此在早期平台版本的 ViewPager 上设置 PageTransformer 将被忽略。
*/
public interface PageTransformer {
        /**
        * 对给定页面应用属性转换。
         * @param page 将转换应用到此页面
         * @param position 页面相对于当前front-and-center的位置
         * 寻呼机的位置。 0 是前面和中间。 1 是一满
         * 页位置向右,-1 是向左一页位置。
         */
        void transformPage(@NonNull View page, float position);
    }

五、PagerAdapter的使用

1、PagerAdapter的源码

 /**
提供适配器以填充 ViewPager 内的页面的基类。您很可能希望使用更具体的实现,
例如 androidx.fragment.app.FragmentPagerAdapter 
或 androidx.fragment.app.FragmentStatePagerAdapter。
实现 PagerAdapter 时,必须至少重写以下方法:
- instantiateItem(ViewGroup, int)
- destroyItem(ViewGroup, int, Object)
- getCount()
- isViewFromObject(View, Object)

PagerAdapter 比用于 AdapterViews 的适配器更通用。 ViewPager 没有直接提供 View 回收机制,而是使用回调来指示更新期间采取的步骤。如果需要,PagerAdapter 可以实现一种视图回收形式,或者使用更复杂的方法来管理页面视图,例如片段事务,其中每个页面都由其自己的片段表示。
ViewPager 将每个页面与一个关键对象相关联,而不是直接使用视图。此键用于跟踪和唯一标识给定页面,与它在适配器中的位置无关。调用 PagerAdapter 方法 startUpdate(ViewGroup) 表示 ViewPager 的内容即将更改。一个或多个对 instantiateItem(ViewGroup, int) 和/或 destroyItem(ViewGroup, int, Object) 的调用将跟随,并且更新结束将通过调用 finishUpdate(ViewGroup) 发出信号。在 finishUpdate 返回时,与 instantiateItem 返回的键对象关联的视图应该添加到传递给这些方法的父 ViewGroup 中,并且应该删除与传递给 destroyItem 的键关联的视图。 isViewFromObject(View, Object) 方法标识页面视图是否与给定的关键对象相关联。
一个非常简单的 PagerAdapter 可以选择使用页面 View 本身作为关键对象,在创建后从 instantiateItem(ViewGroup, int) 返回它们并将它们添加到父 ViewGroup 中。匹配的 destroyItem(ViewGroup, int, Object) 实现将从父 ViewGroup 中删除 View,并且 isViewFromObject(View, Object) 可以实现为 return view == object;。
PagerAdapter 支持数据集更改。数据集更改必须在主线程上发生,并且必须以调用 notifyDataSetChanged() 结束,类似于从 android.widget.BaseAdapter 派生的 AdapterView 适配器。数据集更改可能涉及添加、删除或更改位置的页面。如果适配器实现 getItemPosition(Object) 方法,ViewPager 将保持当前页面处于活动状态。
*/
public abstract class PagerAdapter {
    private final DataSetObservable mObservable = new DataSetObservable();
    private DataSetObserver mViewPagerObserver;

    public static final int POSITION_UNCHANGED = -1;
    public static final int POSITION_NONE = -2;

    /**
     * 返回view数量
     */
    public abstract int getCount();

    /**
     当将要开始对显示的页面进行更改时调用。
    参数:container – 显示此适配器的页面视图的包含视图。
     */
    public void startUpdate(@NonNull ViewGroup container) {
        startUpdate((View) container);
    }

    /**
     为给定位置创建页面。适配器负责将视图添加到此处给出的容器中,尽管它          只必须确保在它从 finishUpdate(ViewGroup) 返回时完成此操作。
参数:
container – 将在其中显示页面的包含视图。
position -- 要实例化的页面位置。
返回一个表示新页面的对象。这不需要是视图,但可以是页面的其他容器。
     */
    @NonNull
    public Object instantiateItem(@NonNull ViewGroup container, int position) {
        return instantiateItem((View) container, position);
    }

    /**
     删除给定位置的页面。适配器负责从其容器中删除视图,尽管它只必须确保在从 finishUpdate(ViewGroup) 返回时完成此操作。
参数:
container -- 将从中删除页面的包含视图。
position -- 要删除的页面位置。
object – 与 instantiateItem(View, int) 返回的对象相同。was returned by
     * {@link #instantiateItem(View, int)}.
     */
    public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
        destroyItem((View) container, position, object);
    }

    /**
调用以通知适配器当前认为哪个项目是“主要的”,即作为当前页面向用户显示的一项。当适配器不包含任何项目时,不会调用此方法。
     */
    public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
        setPrimaryItem((View) container, position, object);
    }

    /**
     * 当显示页面中的更改完成时调用。在这点您必须确保所有页面都已实际添加或酌情从容器中取出。
     */
    public void finishUpdate(@NonNull ViewGroup container) {
        finishUpdate((View) container);
    }

    /**
     当将要开始对显示的页面进行更改时调用。
     */
    @Deprecated
    public void startUpdate(@NonNull View container) {
    }

    /**
     为给定位置创建页面。适配器负责将视图添加到此处给出的容器中,尽管它只必须确保在它返回时完成此操作
     */
    @Deprecated
    @NonNull
    public Object instantiateItem(@NonNull View container, int position) {
        throw new UnsupportedOperationException(
                "Required method instantiateItem was not overridden");
    }

    /**
删除给定位置的页面。适配器负责从其容器中删除视图,尽管它只必须确保在它从 finishUpdate(View) 返回时完成此操作。
     */
    @Deprecated
    public void destroyItem(@NonNull View container, int position, @NonNull Object object) {
        throw new UnsupportedOperationException("Required method destroyItem was not overridden");
    }

    /**
     调用以通知适配器当前认为哪个项目是“主要的”,即作为当前页面向用户显示的一项。
     * @deprecated Use {@link #setPrimaryItem(ViewGroup, int, Object)}
     */
    @Deprecated
    public void setPrimaryItem(@NonNull View container, int position, @NonNull Object object) {
    }

    /**
     * 废弃
     * @deprecated Use {@link #finishUpdate(ViewGroup)}
     */
    @Deprecated
    public void finishUpdate(@NonNull View container) {
    }

    /**
     确定页面视图是否与 instanceiteItem(ViewGroup, int) 返回的特定键对象相关联。 PagerAdapter 需要此方法才能正常工作。
     */
    public abstract boolean isViewFromObject(@NonNull View view, @NonNull Object object);

    /**
保存与此适配器及其页面关联的任何实例状态,如果需要重建当前 UI 状态,则应恢复这些状态
     */
    @Nullable
    public Parcelable saveState() {
        return null;
    }

    /**
恢复之前由 saveState() 保存的与此适配器及其页面关联的任何实例状态
     */
    public void restoreState(@Nullable Parcelable state, @Nullable ClassLoader loader) {
    }

    /**
 当宿主视图试图确定项目的位置是否已更改时调用。如果给定项的位置未更改,则返回 POSITION_UNCHANGED;如果该项不再存在于适配器中,则返回 POSITION_NONE。
默认实现假定项目永远不会改变位置并且总是返回 POSITION_UNCHANGED。
     */
    public int getItemPosition(@NonNull Object object) {
        return POSITION_UNCHANGED;
    }

    /**
如果支持此适配器的数据已更改并且关联的视图应更新,则应用程序应调用此方法。
     */
    public void notifyDataSetChanged() {
        synchronized (this) {
            if (mViewPagerObserver != null) {
                mViewPagerObserver.onChanged();
            }
        }
        mObservable.notifyChanged();
    }

    /**
   注册一个观察者来接收与适配器数据变化相关的回调。
     */
    public void registerDataSetObserver(@NonNull DataSetObserver observer) {
        mObservable.registerObserver(observer);
    }

    /**
    解注册
     */
    public void unregisterDataSetObserver(@NonNull DataSetObserver observer) {
        mObservable.unregisterObserver(observer);
    }

    void setViewPagerObserver(DataSetObserver observer) {
        synchronized (this) {
            mViewPagerObserver = observer;
        }
    }

    /**
ViewPager 可以调用此方法来获取标题字符串来描述指定的页面。此方法可能返回 null,表示此页面没有标题。默认实现返回 null。
参数:
position – 请求标题的位置
回报:
请求页面的标题
     */
    @Nullable
    public CharSequence getPageTitle(int position) {
        return null;
    }

    /**
返回给定页面的比例宽度,作为 ViewPager 从 (0.f-1.f] 测量的宽度的百分比
参数:
position – 请求页面的位置
回报:
给定页面位置的比例宽度
     */
    public float getPageWidth(int position) {
        return 1.f;
    }
}

2、PagerAdapter的子类

PagerAdapter有两个子类:
FragmentPagerAdapter和FragmentPagerStateAdapter

3、三种Adapter的总结

1、三种Adapter的缓存策略
  1. PagerAdapter:缓存三个,通过重写instantiateItem和destroyItem达到创建和销毁view的目的。
  2. FragmentPagerAdapter:内部通过FragmentManager来持久化每一个Fragment,在destroyItem方法调用时只是detach对应的Fragment,并没有真正移除!
  3. FragmentPagerStateAdapter:内部通过FragmentManager来管理每一个Fragment,在destroyItem方法,调用时移除对应的Fragment。
2、三个Adapter使用场景分析
  1. PagerAdapter:当所要展示的视图比较简单时适用
  2. FragmentPagerAdapter:当所要展示的视图是Fragment,并且数量比较少时适用
  3. FragmentStatePagerAdapter:当所要展示的视图是Fragment,并且数量比较多时适用

参考:PagerAdapter深度解析和实践优化

六、ViewPager2

ViewPager2实现是RecyclerView
ViewPager2的Adapter是FragmentStateAdapter

上一篇下一篇

猜你喜欢

热点阅读