制作支持View,图片轮播的Banner

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


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+1?0:(position-1));
            }
        }
        @Override
        public void onPageScrollStateChanged(int state) {
            final int position = viewPager.getCurrentItem();
            if(onPageChangeListener!=null){
                onPageChangeListener.onPageScrollStateChanged(state);
            }
            if(state==ViewPager.SCROLL_STATE_IDLE){
                if(position==0){
                    loopViewPagerScroller.setSudden(true);
                    viewPager.setCurrentItem(viewNumber,true);
                    loopViewPagerScroller.setSudden(false);
                }else if(position==(viewNumber+1)){
                    loopViewPagerScroller.setSudden(true);
                    viewPager.setCurrentItem(1,true);
                    loopViewPagerScroller.setSudden(false);
                }
            }
        }
    });
    this.addView(this.viewPager);
}

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

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);
    viewPager.setCurrentItem(1);
}

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


image.png

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=getActualPosition(position);
        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);
    }

    private int getActualPosition(int position) {
        if (position == 0) {
            return this.mData.size()-1;
        } else if(position==mData.size()+1){
            return 0;
        } else {
            return position-1;
        }
    }

    @Override
    public int getCount() {
        return mData!=null&&!mData.isEmpty()?mData.size()+2:0;
    }

    @Override
    public boolean isViewFromObject(@NonNull View arg0, @NonNull Object arg1) {
        return arg0==arg1;
    }

}

这里在getCount()方法里返回mData.size()+2个数量,在getActualPosition()返回的是正确的position位置,当postion等于0的时候,返回的是最后一个View的位置,那个就会显示最后一个View,当position等于mData.size()+1时,返回的是第一个View,也就是说在第一个View左边加最后一个View,在最后一个View右边加第一个View,这样就可以做到首尾无缝连接,不过这样是不够的,在上面的initViewPage()方法里有如下代码:


if(state==ViewPager.SCROLL_STATE_IDLE){
    if(position==0){
        loopViewPagerScroller.setSudden(true);
        viewPager.setCurrentItem(viewNumber,true);
        loopViewPagerScroller.setSudden(false);
    }else if(position==(viewNumber+1)){
        loopViewPagerScroller.setSudden(true);
        viewPager.setCurrentItem(1,true);
        loopViewPagerScroller.setSudden(false);
    }
}

在返回的postion==0是最后一个View然后用viewPager.setCurrentItem()调整到最后一个的真实位置,当viewNumber+1是第一个View,通过viewPager.setCurrentItem()调整到第一个View的真实位置。这样就做到无限循环。

有了上面这个类就可以实现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既支持普通的View,当然也有懒人专用的传个数组就可实现图片轮播,整个项目我已经生产一个库,具体的源码和用法,怎么引用请参见github.

https://github.com/jack921/LoopViewPagers

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容