2020-02-26 17:31:11

ViewPager2:打造Banner控件

10 / 0 / 0 / 0

作者: Android开发中文站 来源: Android开发中文站

基于ViewPager2实现无限轮播功能。支持传入RecyclerView.Adapter 即可实现无限轮播,原理上支持任何ReyclerView.Apdater框架。Viewpager2已经发布正式版,性能也优越于ViewPager,看过ViewPager2的源码知道内部使用的ReyclerView作为核心实现,使用LinearLayoutManager实现横竖滚动,是的,ViewPager2支持垂直滚动了。

一.介绍ViewPager2的使用

  • 点击查看ViewPager2介绍

ViewPager2的一些api变动

  • FragmentStateAdapter替换了原来的FragmentStatePagerAdapter
  • RecyclerView.Adapter替换了原来的 PagerAdapter
  • registerOnPageChangeCallback替换了原来的addPageChangeListener

Step 1.依赖ViewPager2

implementation "androidx.viewpager2:viewpager2:1.0.0"  

注意:这里是用的androidx库, 如果你的项目中还在使用support库的话, 需要将support库迁移至androidx才可以正常使用。

Step 2.xml

<androidx.viewpager2.widget.ViewPager2  
        android:id="@+id/viewpager2"  
        android:layout_width="match_parent"  
        android:layout_height="150dp"/>  

Step 3.自定义RecyclerView.Adapter

//ReyclerView的使用方式,自定义adapter  
public class ImageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>   
//或者使用其他三方框架,如:BRVAH  
public class ImageAdapter extends BaseQuickAdapter<String, BaseViewHolder> {  
    public ImageAdapter() {  
        super(R.layout.item_image);  
    }  
    @Override  
    protected void convert(@NonNull BaseViewHolder helper, String item) {  
        Glide.with(mContext)  
                .load(item)  
                .into((ImageView) helper.getView(R.id.img));  
    }  
}  

Step 4.在页面中使用ViewPager2

 ViewPager2 viewPager2 = findViewById(R.id.viewpager2);  
 ImageAdapter pager2Adapter = new ImageAdapter();  
 pager2Adapter.addData(Utils.getData(2));  
 viewPager2.setAdapter(pager2Adapter);  

二.使用ViewPager2版本Banner

基本使用的功能,请下载apk体验更流畅

描述普通样式两边缩放

一屏三页

IndicatorViewIndicatorStyle

INDICATOR_CIRCLE INDICATOR_CIRCLE_RECT

INDICATOR_BEZIER INDICATOR_DASH

INDICATOR_BIG_CIRCLE

效果图12

收集更多的效果

Indicator查看simple代码

...

Banner使用步骤

Step 1.依赖banner

项目地址:https://github.com/zguop/banner/blob/master/README_pager2.md

Step 2.xml

<com.to.aboomy.pager2.Banner  
    android:id="@+id/banner"  
    android:layout_width="match_parent"  
    android:layout_height="150dp"/>  

Step 3.自定义RecyclerView.Adapter

//自定义adapter  
public class ImageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>   
//或者使用其他三方框架,都是支持的,如:BRVAH  
public class ImageAdapter extends BaseQuickAdapter<String, BaseViewHolder> {  
    public ImageAdapter() { super(R.layout.item_image); }  
    @Override  
    protected void convert(@NonNull BaseViewHolder helper, String item) {  
        Glide.with(mContext)  
                .load(item)  
                .into((ImageView) helper.getView(R.id.img));  
    }  
}  

Step 4.在页面中使用Banner

 @Override  
    protected void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        setContentView(R.layout.activity_main);  
        banner = findViewById(R.id.banner);  
        //使用内置Indicator  
        IndicatorView indicator = new IndicatorView(this)  
              .setIndicatorColor(Color.DKGRAY)  
              .setIndicatorSelectorColor(Color.WHITE);  

        //创建adapter  
          ImageAdapter adapter = new ImageAdapter();  

          //传入RecyclerView.Adapter 即可实现无限轮播  
         banner.setIndicator(indicator)  
              .setAdapter(adapter);  
    }  

简单设置一屏三页效果

//设置左右页面露出来的宽度及item与item之间的宽度  
.setPageMargin(UIUtil.dip2px(this, 20), UIUtil.dip2px(this, 10))  
//内置ScaleInTransformer,设置切换缩放动画  
.setPageTransformer(true, new ScaleInTransformer())  

Banner提供的方法介绍

方法名描述

setPageTransformer(ViewPager2.PageTransformer transformer) 设置viewpager2的自定义动画,支持多个添加

setOuterPageChangeListener(ViewPager2.OnPageChangeCallback listener) 设置viewpager2的滑动监听

setAutoTurningTime(long autoTurningTime) 设置自动轮播时长

setAutoPlay(boolean autoPlay) 设置是否自动轮播,大于1页可以轮播

setIndicator(Indicator indicator) 设置indicator

setIndicator(Indicator indicator, boolean attachToRoot) 设置indicator

setAdapter(@Nullable RecyclerView.Adapter adapter) 加载数据,此方法时开始轮播的方法,请再最后调用

setAdapter(@Nullable RecyclerView.Adapter adapter, int startPosition) 重载方法,设置轮播的起始位置

isAutoPlay() 是否无限轮播

getCurrentPager() 获取viewPager2当前位置

startTurning() 开始轮播

stopTurning() 停止轮播

setPageMargin(int multiWidth, int pageMargin) 设置一屏多页

setPageMargin(int leftWidth, int rightWidth, int pageMargin) 设置一屏多页,方法重载

setOffscreenPageLimit(int limit) 同viewPager2用法

setOrientation(@ViewPager2.Orientation int orientation) 设置viewpager2滑动方向

ViewPager2 getViewPager2() 获取viewpager2

RecyclerView.Adapter getAdapter() 获取apdater

setPagerScrollDuration(long pagerScrollDuration) 设置viewpager2的切换时长

三.核心的轮播思想

查看banner安利一款轮播控件,与ViewPager版本基本一致,采用count+2的方式,实现无限轮播。我们以数据源四张图片举个实际例子:needCount(6) = count(4) + 2 ,实际轮播的图片是有6张,存放在banner中对应:

我们可以看到在实际的index=0是图片的最后一张,index=5是图片的第一张,我们只要当右滑动到index=5时,通过 viewPager.setCurrentItem(1, false);切换至第一张,当左滑懂到index=0,通过viewPager.setCurrentItem(count, false);切换到实际图片的最后一张,进行过渡实现了循环轮播的一个效果。一屏三页,还是以4张图片举个例子,一屏三页,一次要展示三张图片,相当左右两边都加载了一张图片,也就是多加载了2张图片,需要的数量:needCount(8) = count(4) + 4 ,实际轮播是有8张,存放在banner对应:

同样的控制滑动到最后一张图片和第一张图片对应的索引位置,实现轮播的效果,这里就不多说了,具体可查看项目代码实现。

四.如何支持任意ReyclerView.Adapter就可以实现无限轮播?

1.为什么不封装一个类似BaseRecyclerAdapter方便Banner中View的创建?
主要考虑到RecyclerView的常用性,相信大家各自的项目都有类似于BaseRecyclerAdapter的封装,加上市面上各种ReyclerAdapter框架,因此个人觉得在banner中封装一个类似BaseRecyclerAdapter的类,提供出来实现是没有太大的必要的,而且也满足不了大部分的需求。2.基于ReyclerView.Apdater包装类实现,支持任意Adapter框架
Banner内部实现BannerAdapterWrapper,ReyclerView.Apdater包装代理类,BannerAdapterWrapper内部通过toRealPosition(position)返回真实索引,调用到委托给它的ReyclerView.Apdater返回其真实position,具体请查看banner源码实现,下面贴上关键代码:

    public void setAdapter(@Nullable RecyclerView.Adapter adapter) {  
        setAdapter(adapter, 0);  
    }  
    public void setAdapter(@Nullable RecyclerView.Adapter adapter, int startPosition) {  
        bannerAdapterWrapper.registerAdapter(adapter);  
        initPagerCount();  
        startPager(startPosition);  
    }  

 private class BannerAdapterWrapper extends RecyclerView.Adapter<RecyclerView.ViewHolder> {  

        private RecyclerView.Adapter adapter;  

        @NonNull  
        @Override  
        public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {  
            return adapter.onCreateViewHolder(parent, viewType);  
        }  

        @Override  
        public void onBindViewHolder(@NonNull ViewHolder holder, int position) {  
            adapter.onBindViewHolder(holder, toRealPosition(position));  
        }  

        @Override  
        public int getItemViewType(int position) {  
            return adapter.getItemViewType(toRealPosition(position));  
        }  

        @Override  
        public int getItemCount() {  
            return needCount;  
        }  

        void registerAdapter(RecyclerView.Adapter adapter) {  
            if (this.adapter != null) {  
                this.adapter.unregisterAdapterDataObserver(itemDataSetChangeObserver);  
            }  
            this.adapter = adapter;  
            if (this.adapter != null) {  
                this.adapter.registerAdapterDataObserver(itemDataSetChangeObserver);  
            }  
        }  
    }  
//监听Adapter数据变化,刷新数据  
private RecyclerView.AdapterDataObserver itemDataSetChangeObserver = new RecyclerView.AdapterDataObserver() {  
    @Override  
    public final void onItemRangeChanged(int positionStart, int itemCount) { onChanged(); }  

    @Override  
    public final void onItemRangeChanged(int positionStart, int itemCount, @Nullable Object payload) { onChanged(); }  

    @Override  
    public final void onItemRangeInserted(int positionStart, int itemCount) { onChanged(); }  

    @Override  
    public final void onItemRangeRemoved(int positionStart, int itemCount) { onChanged(); }  

    @Override  
    public final void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { onChanged(); }  

    @Override  
    public void onChanged() {  
        if (viewPager2 != null && bannerAdapterWrapper != null) {  
            initPagerCount();  
            startPager(getCurrentPager());  
        }  
    }  

五.ViewPager2页面速度切换太快

怎么设置页面的切换速度,默认的太快,导致看起来像是卡顿的Issues

其实ViewPager实现的版本也有这个问题,我们先看下Viewpager是如何解决的:

//自定义scroller  
class ViewPagerScroller extends Scroller {  
    private int scrollDuration = 800;  
    ViewPagerScroller(Context context) {  
        super(context);  
    }  
    @Override  
    public void startScroll(int startX, int startY, int dx, int dy, int duration) {  
        super.startScroll(startX, startY, dx, dy, scrollDuration);  
    }  
    @Override  
    public void startScroll(int startX, int startY, int dx, int dy) {  
        super.startScroll(startX, startY, dx, dy, scrollDuration);  
    }  
    void setScrollDuration(int scrollDuration) {  
        this.scrollDuration = scrollDuration;  
    }  
}  
//反射替换了Viewpager中的成员变量mScroller  
private void initViewPagerScroll() {  
    try {  
        Field scrollerField = ViewPager.class.getDeclaredField("mScroller");  
        scrollerField.setAccessible(true);  
        scrollerField.set(this, scroller);  
    } catch (NoSuchFieldException | IllegalArgumentException e) {  
        e.printStackTrace();  
    } catch (IllegalAccessException e) {  
        e.printStackTrace();  
    }  
}  

ViewPager2内部是基于ReyclerView实现的,如何控制页面的切换速度,其实就是控制RecyclerView的切换速度,于是就搜索一番,查看如何修改ReyclerView滚动速度,通过文章发现smoothScrollToPosition是我们的核心方法,其实现是在LayoutManger中,于是通过hook方式替换掉ViewPager2中的LinearLayoutManager,自定义LinearSmoothScroller处理滑动时间。Hook方式替换ViewPager2中的LinearLayoutManager
查看Viewpager2源码,内部设置在RcyclerView上的LayoutManger是基于LinearLayoutManager的LinearLayoutManagerImpl做了扩展。

private class LinearLayoutManagerImpl extends LinearLayoutManager {  
    LinearLayoutManagerImpl(Context context) {  
        super(context);  
    }  

    @Override  
    public boolean performAccessibilityAction(@NonNull RecyclerView.Recycler recycler,  
            @NonNull RecyclerView.State state, int action, @Nullable Bundle args) {  
        if (mAccessibilityProvider.handlesLmPerformAccessibilityAction(action)) {  
            return mAccessibilityProvider.onLmPerformAccessibilityAction(action);  
        }  
        return super.performAccessibilityAction(recycler, state, action, args);  
    }  

    @Override  
    public void onInitializeAccessibilityNodeInfo(@NonNull RecyclerView.Recycler recycler,  
            @NonNull RecyclerView.State state, @NonNull AccessibilityNodeInfoCompat info) {  
        super.onInitializeAccessibilityNodeInfo(recycler, state, info);  
        mAccessibilityProvider.onLmInitializeAccessibilityNodeInfo(info);  
    }  

    @Override  
    protected void calculateExtraLayoutSpace(@NonNull RecyclerView.State state,  
            @NonNull int[] extraLayoutSpace) {  
        int pageLimit = getOffscreenPageLimit();  
        if (pageLimit == OFFSCREEN_PAGE_LIMIT_DEFAULT) {  
            // Only do custom prefetching of offscreen pages if requested  
            super.calculateExtraLayoutSpace(state, extraLayoutSpace);  
            return;  
        }  
        final int offscreenSpace = getPageSize() * pageLimit;  
        extraLayoutSpace[0] = offscreenSpace;  
        extraLayoutSpace[1] = offscreenSpace;  
    }  

    @Override  
    public boolean requestChildRectangleOnScreen(@NonNull RecyclerView parent,  
            @NonNull View child, @NonNull Rect rect, boolean immediate,  
            boolean focusedChildVisible) {  
        return false; // users should use setCurrentItem instead  
    }  
}  

定义“要hook的对象”的代理类,并且创建该类的对象

private class ProxyLayoutManger extends LinearLayoutManager {  

        //这个是ViewPager2中的LinearLayoutManagerImpl对象  
        private RecyclerView.LayoutManager linearLayoutManager;  

        ProxyLayoutManger(Context context, RecyclerView.LayoutManager layoutManager) {  
            super(context);  
            this.linearLayoutManager = layoutManager;  
        }  

        @Override  
        public boolean performAccessibilityAction(@NonNull RecyclerView.Recycler recycler,  
                                                  @NonNull RecyclerView.State state, int action, @Nullable Bundle args) {  
            return linearLayoutManager.performAccessibilityAction(recycler, state, action, args);  
        }  

        @Override  
        public void onInitializeAccessibilityNodeInfo(@NonNull RecyclerView.Recycler recycler,  
                                                      @NonNull RecyclerView.State state, @NonNull AccessibilityNodeInfoCompat info) {  
            linearLayoutManager.onInitializeAccessibilityNodeInfo(recycler, state, info);  
        }  

        @Override  
        public boolean requestChildRectangleOnScreen(@NonNull RecyclerView parent,  
                                                     @NonNull View child, @NonNull Rect rect, boolean immediate,  
                                                     boolean focusedChildVisible) {  
            return linearLayoutManager.requestChildRectangleOnScreen(parent, child, rect, immediate);  
        }  

         //核心处理页面切换速度的方法  
        @Override  
        public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {  
            LinearSmoothScroller linearSmoothScroller = new LinearSmoothScroller(recyclerView.getContext()) {  
                @Override  
                protected int calculateTimeForDeceleration(int dx) {  
                    return (int) (pagerScrollDuration * (1 - .3356));  
                }  
            };  
            linearSmoothScroller.setTargetPosition(position);  
            startSmoothScroll(linearSmoothScroller);  
        }  

        @Override  
        protected void calculateExtraLayoutSpace(@NonNull RecyclerView.State state,  
                                                 @NonNull int[] extraLayoutSpace) {  
            int pageLimit = viewPager2.getOffscreenPageLimit();  
            if (pageLimit == ViewPager2.OFFSCREEN_PAGE_LIMIT_DEFAULT) {  
                super.calculateExtraLayoutSpace(state, extraLayoutSpace);  
                return;  
            }  
            final int offscreenSpace = getPageSize() * pageLimit;  
            extraLayoutSpace[0] = offscreenSpace;  
            extraLayoutSpace[1] = offscreenSpace;  
        }  

        private int getPageSize() {  
            final RecyclerView rv = (RecyclerView) viewPager2.getChildAt(0);  
            return getOrientation() == RecyclerView.HORIZONTAL  
                    ? rv.getWidth() - rv.getPaddingLeft() - rv.getPaddingRight()  
                    : rv.getHeight() - rv.getPaddingTop() - rv.getPaddingBottom();  
        }  
    }  

成员linearLayoutManager,其实就是ViewPager2中的LinearLayoutManagerImpl对象,它复写的方法,我们在代理类中同样复写,并通过linearLayoutManager调用到其真正的实现上,并且还多复写了我们需求的核心处理切换速度的smoothScrollToPosition方法。最后替换掉ViewPager2中的LinearLayoutManagerImpl贴出核心代码

 private void initViewPagerScrollProxy(RecyclerView recyclerView) {  
        try {  
            Field LayoutMangerField = ViewPager2.class.getDeclaredField("mLayoutManager");  
            LayoutMangerField.setAccessible(true);  
            LinearLayoutManager o = (LinearLayoutManager) LayoutMangerField.get(viewPager2);  
            ProxyLayoutManger proxyLayoutManger = new ProxyLayoutManger(getContext(), o);  
            recyclerView.setLayoutManager(proxyLayoutManger);  
            LayoutMangerField.set(viewPager2, proxyLayoutManger);  
            Field pageTransformerAdapterField = ViewPager2.class.getDeclaredField("mPageTransformerAdapter");  
            pageTransformerAdapterField.setAccessible(true);  
            Object mPageTransformerAdapter = pageTransformerAdapterField.get(viewPager2);  
            if (mPageTransformerAdapter != null) {  
                Class<?> aClass = mPageTransformerAdapter.getClass();  
                Field layoutManager = aClass.getDeclaredField("mLayoutManager");  
                layoutManager.setAccessible(true);  
                layoutManager.set(mPageTransformerAdapter, proxyLayoutManger);  
            }  
            Field scrollEventAdapterField = ViewPager2.class.getDeclaredField("mScrollEventAdapter");  
            scrollEventAdapterField.setAccessible(true);  
            Object mScrollEventAdapter = scrollEventAdapterField.get(viewPager2);  
            if (mScrollEventAdapter != null) {  
                Class<?> aClass = mScrollEventAdapter.getClass();  
                Field layoutManager = aClass.getDeclaredField("mLayoutManager");  
                layoutManager.setAccessible(true);  
                layoutManager.set(mScrollEventAdapter, proxyLayoutManger);  
            }  
        } catch (NoSuchFieldException e) {  
            e.printStackTrace();  
        } catch (IllegalAccessException e) {  
            e.printStackTrace();  
        }  
    }  

最后

如果觉得喜欢那就star支持一下项目地址:https://github.com/zguop/banner/blob/master/README_pager2.md如果觉得喜欢那就star支持一下

作者:utomi
链接:https://juejin.im/post/5e49163e6fb9a07cb96ae33d

PS: 如本文对您有疑惑,可加QQ:1752338621 进行讨论。

0 条评论

0
0
官方
微信
官方微信
Q Q
咨询
意见
反馈
返回
顶部