自己动手搭建Banner轮播器

本文详细介绍了如何自定义实现轮播图Banner组件,包括无限轮播的原理及实现方式,通过ViewPager进行图片切换,并结合自定义ViewGroup实现滑动及自动播放功能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

源代码引用地址:https://github.com/yiyibb/Zhihu

首先来看轮播效果图

Paste_Image.png
Paste_Image.png

整体是个recyclerview,头部布局为banner轮播图,此处的banner是继承Framelayout实现的。代码后面会具体说明,这里我们先看轮播图的xml文件的结构,总共三部分首先是背景图片,可实现自动滑动,另外是标题,还有最下面的 圆点,都会随着图片的移动而移动。
布局文件.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
             xmlns:tools="http://schemas.android.com/tools"
             android:id="@+id/bannerLayout"
             android:layout_width="match_parent"
             android:layout_height="@dimen/space_200">
    
    <com.yiyi.zhihu.ui.widget.Banner.BannerViewPaper
        android:id="@+id/viewPager"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
    
    <TextView
        android:id="@+id/banner_title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        android:gravity="left"
        android:maxLines="3"
        android:padding="@dimen/space_16"
        android:textColor="@color/colorWhite"
        android:textSize="@dimen/textSize_20"
        android:textStyle="bold"
        tools:text="图片轮播器"
        tools:textColor="@color/colorLightBlue"/>
    
    <LinearLayout
        android:id="@+id/indicator"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|center_horizontal"
        android:layout_marginBottom="@dimen/space_10"
        android:layout_marginTop="@dimen/space_10"
        android:gravity="center"
        android:orientation="horizontal" >


    </LinearLayout>
    
</FrameLayout>

看重点主要是BannerViewPaper,这个类仅仅继承了ViewPager,代码如下:

public class BannerViewPaper extends ViewPager {

    private boolean scrollable = true;

    public BannerViewPaper(Context context) {
        super(context);
    }

    public BannerViewPaper(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        return this.scrollable && super.onTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return this.scrollable && super.onInterceptTouchEvent(ev);
    }

    public void setScrollable(boolean scrollable) {
        this.scrollable = scrollable;
    }
}

滑动事件以及点击事件设定了scrollable。
接下来看重点banner,其实这也算是自定义viewgroup了,继承了framelayout,实现了滑动以及自动播放功能。看一下代码吧,emm,代码一如既往地多,但是我会一点点来分析,踏踏实实,耐耐心心,做技术本该如此,不是吗?

public class Banner extends FrameLayout implements ViewPager.OnPageChangeListener {

    private static final String TAG = "Banner";

    private int indicatorSize;
    private int mIndicatorWidth;
    private int mIndicatorHeight;
    private int mIndicatorMargin;
    private int bannerStyle = BannerConfig.CIRCLE_INDICATOR;
    private int delayTime = BannerConfig.TIME;
    private int scrollTime = BannerConfig.DURATION;
    private boolean isAutoPlay = BannerConfig.IS_AUTO_PLAY;
    private boolean isScroll = BannerConfig.IS_SCROLL;
    private int mIndicatorSelectedResId = R.drawable.selected_radius;
    private int mIndicatorUnselectedResId = R.drawable.unselected_radius;
    private int titleHeight;
    private int titleBackground;
    private int titleTextColor;
    private int titleTextSize;
    private int count = 0;
    private int currentItem;
    private int lastPosition = 1;

    private Context mContext;

    private BannerPagerAdapter mAdapter;

    private List<View> imageViews;
    private List<ImageView> indicatorImages;
    private List imageUrls;
    private List<String> titles;

    private BannerViewPaper mViewPaper;
    private TextView bannerTitle;
    private LinearLayout indicator;
    private BannerScroller mScroller;
    private DisplayMetrics dm;

    private ImageLoaderInterface imageLoader;

    private OnBannerClickListener mOnBannerClickListener;
    private ViewPager.OnPageChangeListener mOnPageChangeListener;

    private WeakHandler handler = new WeakHandler();

    public Banner(Context context) {
        this(context, null);
    }

    public Banner(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public Banner(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.mContext = context;
        titles = new ArrayList<>();
        imageUrls = new ArrayList();
        imageViews = new ArrayList<>();
        indicatorImages = new ArrayList<>();

        dm = context.getResources().getDisplayMetrics();
        indicatorSize = dm.widthPixels / 80;
        initView(context, attrs);
    }

    private void initView(Context context, AttributeSet attrs) {
        imageViews.clear();
        View view = LayoutInflater.from(context).inflate(R.layout.banner, this, true);
        mViewPaper = (BannerViewPaper) view.findViewById(R.id.viewPager);
        bannerTitle = (TextView) view.findViewById(R.id.banner_title);
        indicator = (LinearLayout) view.findViewById(R.id.indicator);

        handleTypedArray(context, attrs);
        initViewPaperScroll();
    }

    private void handleTypedArray(Context context, AttributeSet attrs) {
        if (attrs == null) {
            return;
        }
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.Banner);
        mIndicatorWidth = typedArray.getDimensionPixelSize(R.styleable.Banner_indicator_width, indicatorSize);
        mIndicatorHeight = typedArray.getDimensionPixelSize(R.styleable.Banner_indicator_height, indicatorSize);
        mIndicatorMargin = typedArray.getDimensionPixelSize(R.styleable.Banner_indicator_margin, BannerConfig.PADDING_SIZE);
        mIndicatorSelectedResId = typedArray.getResourceId(R.styleable.Banner_indicator_drawable_selected, R.drawable.selected_radius);
        mIndicatorUnselectedResId = typedArray.getResourceId(R.styleable.Banner_indicator_drawable_unselected, R.drawable.unselected_radius);
        delayTime = typedArray.getInt(R.styleable.Banner_delay_time, BannerConfig.TIME);
        scrollTime = typedArray.getInt(R.styleable.Banner_scroll_time, BannerConfig.DURATION);
        isAutoPlay = typedArray.getBoolean(R.styleable.Banner_is_auto_play, BannerConfig.IS_AUTO_PLAY);
        titleBackground = typedArray.getColor(R.styleable.Banner_title_background, BannerConfig.TITLE_BACKGROUND);
        titleHeight = typedArray.getDimensionPixelSize(R.styleable.Banner_title_height, BannerConfig.TITLE_HEIGHT);
        titleTextColor = typedArray.getColor(R.styleable.Banner_title_textcolor, BannerConfig.TITLE_TEXT_COLOR);
        titleTextSize = typedArray.getDimensionPixelSize(R.styleable.Banner_title_textsize, BannerConfig.TITLE_TEXT_SIZE);
        typedArray.recycle();
    }

    private void initViewPaperScroll() {
        try {
            Field mField = ViewPager.class.getDeclaredField("mScroller");
            mField.setAccessible(true);
            mScroller = new BannerScroller(mViewPaper.getContext());
            mScroller.setDuration(scrollTime);
            mField.set(mViewPaper, mScroller);
        } catch (Exception e) {
            Log.e(TAG, e.getMessage());
        }
    }

    public Banner isAutoPlay(boolean isAutoPlay) {
        this.isAutoPlay = isAutoPlay;
        return this;
    }

    public Banner setImageLoader(ImageLoaderInterface imageLoader) {
        this.imageLoader = imageLoader;
        return this;
    }

    public Banner setDelayTime(int delayTime) {
        this.delayTime = delayTime;
        return this;
    }

    public Banner setBannerTitles(List<String> titles) {
        this.titles = titles;
        bannerTitle.setText(titles.get(0));
        return this;
    }

    public Banner setImages(List<?> imageUrls) {
        this.imageUrls = imageUrls;
        this.count = imageUrls.size();
        return this;
    }

    public void update(List<?> imageUrls, List<String> titles) {
        this.imageUrls.clear();
        this.titles.clear();
        this.imageUrls.addAll(imageUrls);
        this.titles.addAll(titles);
        this.count = this.imageUrls.size();
        start();
    }

    public void update(List<?> imageUrls) {
        this.imageUrls.clear();
        this.imageUrls.addAll(imageUrls);
        this.count = this.imageUrls.size();
        start();
    }

    public Banner start() {
        setImagesList(imageUrls);
        if (isAutoPlay) {
            startAutoPlay();
        }
        return this;
    }

    private void createIndicator() {//滚动的圆点
        indicatorImages.clear();
        indicator.removeAllViews();
        for (int i = 0; i < count; ++i) {
            ImageView imageView = new ImageView(mContext);
            imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
            LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(mIndicatorWidth, mIndicatorHeight);
            params.leftMargin = mIndicatorMargin;
            params.rightMargin = mIndicatorMargin;
            if (i == 0) {
                imageView.setImageResource(mIndicatorSelectedResId);
            } else {
                imageView.setImageResource(mIndicatorUnselectedResId);
            }
            indicatorImages.add(imageView);
            indicator.addView(imageView, params);
        }
    }

    private void setData() {
        currentItem = 1;
        if (mAdapter == null) {
            mAdapter = new BannerPagerAdapter();
        }
        mViewPaper.setAdapter(mAdapter);
        mViewPaper.setFocusable(true);
        mViewPaper.setCurrentItem(currentItem);
        mViewPaper.addOnPageChangeListener(this);
        if (isScroll && count > 1) {
            mViewPaper.setScrollable(true);
        } else {
            mViewPaper.setScrollable(false);
        }
    }

    private void setImagesList(List<?> imageUrls) {
        if (imageUrls == null || imageUrls.size() <= 0) {
            Log.e(TAG, "please set the images data");
            return;
        }

        imageViews.clear();
        createIndicator();//滚动小圆球

        for (int i = 0; i <= count + 1; ++i) {
            View imageView = null;
            if (imageLoader != null) {
                imageView = imageLoader.creteImageView(mContext);
            }

            if (imageView == null) {
                imageView = new ImageView(mContext);
            }

            ((ImageView)imageView).setScaleType(ImageView.ScaleType.CENTER_CROP);
            Object url = null;
            // 无限轮播实现原理
            if (i == 0) {
                url = imageUrls.get(count - 1);
            } else if (i == count + 1) {
                url = imageUrls.get(0);
            } else {
                url = imageUrls.get(i - 1);
            }

            imageViews.add(imageView);
            if (imageLoader != null) {
                imageLoader.displayImage(mContext, url, imageView);
            } else {
                Log.e(TAG, "please set image loader");
            }
        }
        setData();
    }

    public void startAutoPlay() {
        handler.removeCallbacks(task);
        handler.postDelayed(task, delayTime);
    }

    public void stopAutoPlay() {
        handler.removeCallbacks(task);
    }

    private final Runnable task = new Runnable() {
        @Override
        public void run() {
            if (count > 1 && isAutoPlay) {
                currentItem = currentItem % (count + 1) + 1;
                if (currentItem == 1) {
                    mViewPaper.setCurrentItem(currentItem);
                    handler.postDelayed(task, delayTime);
                    Log.i("困了1",mViewPaper.getCurrentItem()+"");
                } else if (currentItem == count+1) {
                    //mViewPaper.setCurrentItem(currentItem,false);
                    handler.post(task);
                    Log.i("困了1",mViewPaper.getCurrentItem()+"");
                }  else {
                    mViewPaper.setCurrentItem(currentItem);
                    handler.postDelayed(task, delayTime);
                    Log.i("困了2",mViewPaper.getCurrentItem()+"");
                }
            }
        }
    };

    /**
     * 返回真实的位置
     *
     * @param position
     * @return 下标从0开始
     */
    public int toRealPosition(int position) {
        int realPosition = (position - 1) % count;
        if (realPosition < 0) {
            realPosition += count;
        }
        return realPosition;
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if  (isAutoPlay) {
            int action = ev.getAction();
            if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL
                    || action == MotionEvent.ACTION_OUTSIDE) {
                startAutoPlay();
            } else if (action == MotionEvent.ACTION_DOWN) {
                stopAutoPlay();
            }
        }
        return super.dispatchTouchEvent(ev);
    }

    class BannerPagerAdapter extends PagerAdapter {
        @Override
        public int getCount() {
            return imageViews.size();
        }

        @Override
        public boolean isViewFromObject(View view, Object object) {
            return view == object;
        }

        @Override
        public Object instantiateItem(ViewGroup container, final int position) {
            container.addView(imageViews.get(position));
            View view = imageViews.get(position);
            if (mOnBannerClickListener != null) {
                view.setOnClickListener(new OnClickListener() {
                    @Override
                    public void onClick(View view) {
                        mOnBannerClickListener.OnBannerClick(toRealPosition(position));
                    }
                });
            }
            return view;
        }

        @Override
        public void destroyItem(ViewGroup container, int position, Object object) {
            container.removeView((View)object);
        }
    }

    @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
        if (mOnPageChangeListener != null) {//不执行
            mOnPageChangeListener.onPageScrolled(position, positionOffset, positionOffsetPixels);
            Log.i("梦醒","对象为空1");
        }
    }

    @Override
    public void onPageSelected(int position) {
        if (mOnPageChangeListener != null) {//不执行
            mOnPageChangeListener.onPageSelected(position);
            Log.i("梦醒","对象为空2");
        }
        indicatorImages.get((lastPosition - 1 + count) % count).setImageResource(mIndicatorUnselectedResId);
        indicatorImages.get((position - 1 + count) % count).setImageResource(mIndicatorSelectedResId);
        lastPosition = position;

        if (position == 0) position = count;
        if (position > count) position = 1;
        bannerTitle.setText(titles.get(position - 1));
    }

    @Override
    public void onPageScrollStateChanged(int state) {
        if (mOnPageChangeListener != null) {
            mOnPageChangeListener.onPageScrollStateChanged(state);
        }
        currentItem = mViewPaper.getCurrentItem();
        switch (state) {
            /* SCROLL_STATE_DRAGGING(1)表示用户手指“按在屏幕上并且开始拖动”的状态(手指按下但是还没有拖动的时候还不是这个状态,
                                           只有按下并且手指开始拖动后log才打出。)
            SCROLL_STATE_IDLE(0)滑动动画做完的状态。
            SCROLL_STATE_SETTLING(2)在“手指离开屏幕”的状态。*/
            case 0://No operation
               /* if (currentItem == 0) {
                    mViewPaper.setCurrentItem(count,false);
                } else if (currentItem == count + 1) {
                    mViewPaper.setCurrentItem(1, false);
                }
                Log.i("梦醒0",mViewPaper.getCurrentItem()+"");*/
                break;
            case 1://start Sliding正在滑动
                /*if (currentItem == count + 1) {
                    mViewPaper.setCurrentItem(1, false);
                } else if (currentItem == 0) {
                    mViewPaper.setCurrentItem(count, false);
                }
                Log.i("梦醒1",mViewPaper.getCurrentItem()+"");*/
                break;
            case 2://end Sliding
                /*if (currentItem == 0) {
                    mViewPaper.setCurrentItem(count,false);
                } else if (currentItem == count + 1) {
                    mViewPaper.setCurrentItem(1, false);
                }
                Log.i("梦醒2",mViewPaper.getCurrentItem()+"");*/
                break;
        }
    }

    public void setOnPageChangeListener(ViewPager.OnPageChangeListener onPageChangeListener) {
        mOnPageChangeListener = onPageChangeListener;
    }

    public Banner setOnBannerClickListener(OnBannerClickListener listener) {
        this.mOnBannerClickListener = listener;
        return this;
    }

    public interface OnBannerClickListener {
        public void OnBannerClick(int position);
    }
}

找关键,挑重点,重要的几个方法就这么多
initView();
startAutoPlay();
task;
介绍一下整体流程,说到底banner的实现使用就是viewpager,利用定时让viewpager自动播放,难点是无限轮播,这里我也只能算是看懂了代码,,但是不能理会他的精髓,直接让我写,我还是写不出来,这就是大神的能力吧。
首先initview初始化布局文件,这里面有两个方法一个是 handleTypedArray用于配置布局控件中的风格,高度长度,背景等等。另一个方法是initViewPaperScroll()这个方法是利用Java反射机制控制viewpager的滑动时间。再往后边看就是一些设定参数的方法,没啥可讲的。
后面重点是start方法,这个方法里面先是初始化了数据。关键代码如下:

for (int i = 0; i <= count + 1; ++i) {
            View imageView = null;
            if (imageLoader != null) {
                imageView = imageLoader.creteImageView(mContext);
            }

            if (imageView == null) {
                imageView = new ImageView(mContext);
            }

            ((ImageView)imageView).setScaleType(ImageView.ScaleType.CENTER_CROP);
            Object url = null;
            // 无限轮播实现原理
            if (i == 0) {
                url = imageUrls.get(count - 1);
            } else if (i == count + 1) {
                url = imageUrls.get(0);
            } else {
                url = imageUrls.get(i - 1);
            }

            imageViews.add(imageView);
            if (imageLoader != null) {
                imageLoader.displayImage(mContext, url, imageView);
            } else {
                Log.e(TAG, "please set image loader");
            }
        }

这个轮播图片总数为5,也就是count为5。画了一张草图

Paste_Image.png
Paste_Image.png

图上面是从网络上获取到的url集合,图下面是viewpager对应的imageview的六张图片。下面来看runable延时任务,每隔一段时间就会执行一次,但是由于这里是无限轮播,所以会有一些特殊处理

private final Runnable task = new Runnable() {
        @Override
        public void run() {
            if (count > 1 && isAutoPlay) {
                currentItem = currentItem % (count + 1) + 1;
                if (currentItem == 1) {
                    mViewPaper.setCurrentItem(currentItem);
                    handler.postDelayed(task, delayTime);
                } else if (currentItem == count+1) {
                    handler.post(task);
                }  else {
                    mViewPaper.setCurrentItem(currentItem);
                    handler.postDelayed(task, delayTime);
                }
            }
        }
    };

这里其实是一个循环,因为 currentItem每次都会除以6取余加1,初始化currentItem为1,需要注意的是 currentItem是从imageviews中取的值。 currentItem会按照1—>2—>3—>4—>5—>6—>1这样循环下去。
当照片为imageviews中的最后一张时,task任务会直接执行下一次任务不做延时,也就是直接蹦到imageviews的第一张。这时候您如果仔细看的话会发现imageviews的第0张图片并没有什么卵用啊,其实这个问题是隐藏的,刚开始我也没注意到。当currentItem为1时,也就是imageviews中的第一张照片,这时候用户要是向左滑动,就会蹦到imageviews的第0张图片。这时候再来看,按照正常逻辑走 currentItem下一个又会变成1,不得不佩服作者的逻辑,这种可以被算上算法了吧,左右的操作都在这个循环里。

Tip:我对作者的源代码稍稍改变了一下,因为他这里有点小小的问题,viewpager在自动滑动的情况下,当用户向左滑动viewpager一直到倒数第一张图片,这时候用户松手,轮播会直接跳转到第二张图片然后继续滑动。我修改了onPageScrollStateChanged和task两个方法,您可以仔细研究一下作者的源码。https://github.com/yiyibb/Zhihu

还有一些次要的方法:
createIndicator:用于底部圆点的滚动,这个圆点滚动效果其实是在onPageSelected时候开始变化的,道理比较简单,也是跟上面一样像一个循环。
onPageScrollStateChanged:这个方法原作者是写了一些代码的,针对的也是用户滑动事件,简单说说这个方法对应着三种状态
SCROLL_STATE_DRAGGING(1)表示用户手指“按在屏幕上并且开始拖动”的状态(手指按下但是还没有拖动的时候还不是这个状态,只有按下并且手指开始拖动后log才打出。)
SCROLL_STATE_IDLE(0)滑动动画做完的状态。
SCROLL_STATE_SETTLING(2)在“手指离开屏幕”的状态。
源代码作者写的代码我感觉是非必要的,已经注释掉了,也可能是自己没能领会到吧。
dispatchTouchEvent:这个是触摸事件,用于拦截用户的滑动事件,没啥可讲的
OnBannerClickListener:监听回调接口
感兴趣的可以关注我最新开的公众号,重在分享!!微信搜索 开发Android的小学生

qrcode_for_gh_c686d73be7e1_430.jpg
qrcode_for_gh_c686d73be7e1_430.jpg
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值