美文网首页Android技术知识Android开发经验谈Android开发
一个支持Fragment,View,图片轮播的Banner

一个支持Fragment,View,图片轮播的Banner

作者: Jack921 | 来源:发表于2018-09-07 10:33 被阅读79次

    之前有一个项目中有用到轮播,不过不是简单的轮播图片就完了,而是要轮播很多个View,一开始我的想法和大家一样在github在一个算了,哈哈,不过在试用了很多个项目之后都觉得不能完全满足我的需求,大部分还是针对于图片轮播的场景,所以是时候自己搞一个既支持图片,也支持各种自己定义的View,也支持fragment,同时也可以选择不同实现方式的指示器或者干脆去掉,适应个各种需求场景。


    show.gif

    这就是他的效果,看似和普通的轮播也没有什么区别,不过后续介绍你就知道功能的强大,你可以用它不单单只是实现轮播功能。下面先源码讲解先。

    LoopViewPager

    LoopViewPager是这个库的关键类,其内部最基本的实现类其实还是android自带的ViewPager,代码如下:

    public void initViewPage(Context context){
        mHandler=new Handler();
        this.viewPager=new ViewPager(context);
        this.viewPager.setOffscreenPageLimit(2);
        loopViewPagerScroller = new LoopViewPagerScroller(context);
        loopViewPagerScroller.setScrollDuration(2000);
        loopViewPagerScroller.initViewPagerScroll(viewPager);
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
            viewPager.setId(viewPager.hashCode());
            } else {
            viewPager.setId(View.generateViewId());
        }
        loopRunnable=new Runnable() {
            @Override
            public void run() {
                viewPager.setCurrentItem(currentItem);
                currentItem++;
                mHandler.postDelayed(loopRunnable,delayTime);
            }
        };
        viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
            @Override
            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
                if(onPageChangeListener!=null){
                    onPageChangeListener.onPageScrolled(position,positionOffset,positionOffsetPixels);
                }
                if(indicatorCanvasView!=null){
                    indicatorCanvasView.onPageScrolled(position,positionOffset,positionOffsetPixels);
                }
            }
            @Override
            public void onPageSelected(int position) {
                currentItem=position;
                if(onPageChangeListener!=null){
                    onPageChangeListener.onPageSelected(position);
                }
                if(indicatorView!=null){
                    indicatorView.changeIndicator(position%viewNumber);
                }
                }
                @Override
                public void onPageScrollStateChanged(int state) {
                    if(onPageChangeListener!=null){
                        onPageChangeListener.onPageScrollStateChanged(state);
                    }
                }
            });
        this.addView(this.viewPager);
    }
    

    在这里我们知道,LoopViewPager里面其实最主要就是包裹着ViewPage而已,至于指示器后面在讲。那么一个简单的ViewPage是怎么实现无限轮播的呢,关键setData()方法里,如下代码:

    public void setData(FragmentManager fragmentManager, List<Fragment> listFragment){
        viewNumber=listFragment.size();
        initIndicator(getContext());
        this.loopFragmentPagerAdapter=new LoopFragmentPagerAdapter(fragmentManager,listFragment);
        this.viewPager.setAdapter(this.loopFragmentPagerAdapter);
    }
    
    public void setData(Context context, List<T> mData, CreateView mCreatView){
        viewNumber=mData.size();
        initIndicator(getContext());
        LoopViewPagerAdapter loopViewPagerAdapter=new LoopViewPagerAdapter(context,mData,mCreatView,onClickListener);
        viewPager.setAdapter(loopViewPagerAdapter);
    }
    

    在上面的代码里有两个关键的类,分别是LoopFragmentPagerAdapter和LoopViewPagerAdapter,分别实现的是Fragment的无限轮播和View的无限轮播,有这两个基础类,基本就可以为所欲为了


    image.png

    LoopFragmentPagerAdapter

    看这个名字就知道是针对Fragment的循坏轮播的,先看代码,代码如下:

    public class LoopFragmentPagerAdapter extends FragmentPagerAdapter {
        public List<Fragment> listFragment;
    
        public LoopFragmentPagerAdapter(FragmentManager fm, List<Fragment> listData) {
            super(fm);
            this.listFragment=listData;
        }
    
        @Override
        public Object instantiateItem(ViewGroup container, int position) {
            position = position % listFragment.size();
            return super.instantiateItem(container, position);
        }
    
        @Override
        public Fragment getItem(int i) {
            return this.listFragment.get(i);
        }
    
        @Override
        public int getCount() {
            return Integer.MAX_VALUE;
        }
    
    }
    

    这个Adapter很简单,首先getCount()里设置最大值Integer.MAX_VALUE,这个Adapter就会不断的滚动,不过滚动要怎么实现每次滚到正确的View?用position = position % listFragment.size();
    滚动的坐标求余Fragment的个数既求得正确的Fragemnt的坐标,代码很少,很简单,这样就可以Fragment的循环滚动。

    LoopViewPagerAdapter

    这是针对View其中包括ImageView的轮播的,代码如下:

    public class LoopViewPagerAdapter<T> extends PagerAdapter {
        private OnPageClickListener onClickListener;
        private CreateView mCreateView;
        private Context context;
        private List<T> mData;
    
        public LoopViewPagerAdapter(Context context, List<T> list, CreateView createView, OnPageClickListener onClickListener){
            this.onClickListener=onClickListener;
            this.mCreateView=createView;
            this.context=context;
            this.mData=list;
        }
    
        @Override
        public Object instantiateItem(ViewGroup container, int position) {
            position=position%mData.size();
            if(mCreateView==null){
                return new View(context);
            }
            View view=mCreateView.createView(position);
            final int finalPosition = position;
            view.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                   if(onClickListener!=null){
                       onClickListener.onClick(view, finalPosition);
                   }
                }
            });
            ViewParent vp = view.getParent();
            if (vp != null) {
                ViewGroup parent = (ViewGroup)vp;
                parent.removeView(view);
            }
            mCreateView.updateView(view,position,mData.get(position));
            container.addView(view);
            return view;
        }
    
        @Override
        public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
              container.removeView((View)object);
              mCreateView.deleteView(position);
        }
    
        @Override
        public int getCount() {
            return Integer.MAX_VALUE;
        }
    
        @Override
        public boolean isViewFromObject(@NonNull View arg0, @NonNull Object arg1) {
            return arg0==arg1;
        }
    
    }
    

    这里还是那两个方法通过getCount()等于Integer.MAX_VALUE让View无限滚动,在instantiateItem()方法里通过position=position%mData.size();获取正确位置,再返回正确的View,假如是单纯的图片就返回ImageView而已,通过回调mCreateView.createView()获取View,回调updateView()刷新View,在addView()
    添加View,在destroyItem()方法里删除不用View防止内存不足,在回调deleteView()做相应的逻辑处理,都很简单。

    有了上面这个两个类就可以实现Fragment和View的循环轮播。

    讲完轮播,接着就是指示器,指示器我也写了两个,一种是简单的IndicatorView,没什么动画,直接图片切换,一种是实现指示器滑动动画的IndicatiorCanvasView。

    IndicatorView

    先讲简单的指示器,代码如下:

    public class IndicatorView extends LinearLayout {
        private Context context;
        private int loopNowIndicatorImg;
        private int loopIndicatorImg;
        private IndicatorAnimator indicatorAnimator;
    
        public IndicatorView(Context context, int loopNowIndicatorImg,
                             int loopIndicatorImg, IndicatorAnimator indicatorAnimator) {
            this(context,null);
            this.loopNowIndicatorImg=loopNowIndicatorImg;
            this.loopIndicatorImg=loopIndicatorImg;
            this.indicatorAnimator=indicatorAnimator;
        }
    
        public IndicatorView(Context context, @Nullable AttributeSet attrs) {
            this(context,attrs,0);
        }
    
        public IndicatorView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            this.context=context;
            setOrientation(HORIZONTAL);
        }
    
        public void initView(int viewSize){
            for(int i=0;i<viewSize;i++){
                ImageView imageView=new ImageView(context);
                LayoutParams layoutParams=new LayoutParams(
                        ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
                layoutParams.gravity= Gravity.CENTER;
                imageView.setLayoutParams(layoutParams);
                if(i==0){
                    imageView.setImageResource(this.loopNowIndicatorImg);
                }else{
                    imageView.setImageResource(this.loopIndicatorImg);
                }
                addView(imageView);
            }
        }
    
        public void changeIndicator(int select){
            if(getChildCount()==0){
                return;
            }
            for(int i=0;i<getChildCount();i++){
                ((ImageView)getChildAt(i)).setImageResource(this.loopIndicatorImg);
            }
            ImageView imageView=(ImageView)getChildAt(select);
            imageView.setImageResource(this.loopNowIndicatorImg);
            if(this.indicatorAnimator!=null){
                indicatorAnimator.indicatorView(imageView);
            }
        }
    }
    

    这是很简单的指示器,首先集成LinearLayout,在通过initView()遍历ImageView,再通过addView添加,这就完成了指示器界面初始化。当ViewPage每滑动一次都会调用changeIndicator()方法,这里先遍历把所有的View都设为未选择状态,再把选中的ImageView设为选中的图片就行了,每什么说的。

    IndicatiorCanvasView

    public class IndicatiorCanvasView extends LinearLayout {
        private int select_origin;
        private float positionOffsetData;
        private Bitmap originBitmap;
        private ImageView firstView;
        private ImageView secondView;
        private Context context;
        private int numView;
    
        private int[] firstViewLocation=new int[2];
        private int[] secondViewLocation=new int[2];
        private int originMargin=0;
    
        public IndicatiorCanvasView(Context context,int origin,int select_origin) {
            this(context,null);
            originBitmap=BitmapFactory.decodeResource(context.getResources(), origin);
            this.select_origin=select_origin;
            this.context=context;
        }
    
        public IndicatiorCanvasView(Context context, @Nullable AttributeSet attrs) {
            this(context, attrs,0);
        }
    
        public IndicatiorCanvasView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
                @Override
                public void onGlobalLayout() {
                    firstView.getLocationInWindow(firstViewLocation);
                    secondView.getLocationInWindow(secondViewLocation);
                    originMargin=secondViewLocation[0]-firstViewLocation[0];
                }
            });
        }
    
        public void initView(int size){
            this.numView=size;
            for(int i=0;i<size;i++){
                ImageView originView=new ImageView(context);
                LayoutParams layoutParams=new LayoutParams(
                        ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT);
                layoutParams.gravity= Gravity.CENTER;
                originView.setLayoutParams(layoutParams);
                originView.setImageResource(select_origin);
                if(i==0){
                    firstView=originView;
                }else if(i==1){
                    secondView=originView;
                }
                addView(originView);
            }
        }
    
        @Override
        protected void dispatchDraw(Canvas canvas) {
            super.dispatchDraw(canvas);
            canvas.translate(this.positionOffsetData,0);
            canvas.drawBitmap(originBitmap,0,0,new Paint());
        }
    
        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels){
            int num=position%this.numView;
            this.positionOffsetData=(num*originMargin)+originMargin*positionOffset;
            invalidate();
        }
    
    }
    

    首先initView()方法还是和之前一样,遍历ImageView再addView();重头戏在于当ViewPage滑动时会回调onPageScrolled()方法,而positionOffset是他的滑动比例,originMargin是两个指示点的距离,而originMargin是怎么算的能,如下代码:

    getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
            firstView.getLocationInWindow(firstViewLocation);
            secondView.getLocationInWindow(secondViewLocation);
            originMargin=secondViewLocation[0]-firstViewLocation[0];
        }
    });
    

    既拿到第一个指示点和第二个指示点的位置,然后相减,就是两点之间的间距。在通过
    (numoriginMargin)+originMarginpositionOffset拿到滑动的距离,调invalidate()方法刷新。
    刷新是会回调:dispatchDraw(Canvas canvas)方法。

    @Override
    protected void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);
        canvas.translate(this.positionOffsetData,0);
        canvas.drawBitmap(originBitmap,0,0,new Paint());
    }
    

    计算出来的值通过canvas.translate()移动canvas原点,这你在我自定义的文章见多了吧,再通过canvas.drawBitmap()动态画出移动的点。这就实现了点的动画。

    这基本就是整个循坏Banner的所有重点。这个Banner既支持Fragment,也支持普通的View,当然也有懒人专用的传个数组就可实现图片轮播,整个项目我已经生产一个库,具体的源码和用法,怎么引用请参见github.

    https://github.com/jack921/LoopViewPagers

    相关文章

      网友评论

      • riceeeeeeee:楼主,这个无限轮播向后一个滑没问题,向前一个滑会出现fragment无法显示的情况,好什么解决办法吗
        Jack921:已修复,也代码了,谢谢反馈

      本文标题:一个支持Fragment,View,图片轮播的Banner

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