Android轮播图-自定义无限滚动的广告Banner控件

今天闲来无事,梳理一下关于Android中广告banner图的一些技巧。

一般来说,我们的广告Banner要满足这样几个条件:

  • Banner单个Item无限循环
  • 自动滚动
  • 响应单独item点击事件
  • 触摸禁止滑动
  • 指示条
  • 画廊效果

ok,下面我们就针对这几点一一来进行分析

Banner单个Item无限循环

首先我们要实现一个自动滚动和手动均可滚动的Banner,很显然,我们需要使用到ViewPager(当然也可以使用RecyclerView,不过我个人感觉在使用难度上,前者会小一点)。实现无限循环的方式有这样两种:

假无限

这种方法是在原数据队头队尾各新增一条数据,新的数据集首位为原数据末尾数据,其末尾为原数据首位数据,中间为原数据,如图所示(图出处不详):

此循环ViewPager的思路是将第0位item与倒数第二位映射,将最后一位与第一位元素映射,在选中第0位或者最后一位时,设置为相应的位置。这种方法可以实现循环滚动,但是存在问题,重置ViewPager位置时不能使用切换动画,如果使用了自定义动画,切换会出现问题,因此该方式不作考虑。

真无限

另一种是实现思路就是本篇博客采用的方式。对ViewPager的适配器进行一些改造,直接看代码:

public static class BannerAdapter<T> extends PagerAdapter {

    private List<T> data;//数据源
    private boolean notify = false;//手动更改了数据源标志位,解决notifyDataSetChanged不起作用的问题
    private BaseBannerViewHolder<T> holder;//生成对应View并处理View事件
    private boolean isInfinity;//是否允许无限轮播
    private OnPageClickListener onPageClickListener;//item点击回调

    public BannerAdapter(Context context,boolean isInfinity) {
        this.isInfinity = isInfinity;
    }

    @NonNull
    @Override
    public Object instantiateItem(@NonNull ViewGroup container, int position) {
        View view = getView(container,position);
        container.addView(view);
        return view;
    }

    /**
         * 生成对应的View
         * @param container
         * @param position
         * @return
         */
    private View getView(ViewGroup container, final int position) {
        View view = holder.createView();
        view.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                if(onPageClickListener != null) {
                    onPageClickListener.onPageClick(position % getRealCount());
                }
            }
        });
        holder.bind(position % getRealCount(),data.get(position % getRealCount()));
        return view;
    }

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

    /**
         * 无限模式返回Int最大值,否则返回真实个数
         * @return
         */
    @Override
    public int getCount() {
        if(isInfinity) {
            return Integer.MAX_VALUE;
        } else {
            return getRealCount();
        }
    }

    /**
         * 返回数据源真实个数
         * @return
         */
    public int getRealCount() {
        return data == null ? 0 : data.size();
    }

    /**
         * 获取初始化第一个item位置
         * @return
         */
    public int getFirstPosition() {
        if(getRealCount() == 0) {
            throw new IllegalArgumentException();
        }
        if(isInfinity) {
            //无限模式下从Int最大值中间开始,若从0开始则无法左滑
            int center = Integer.MAX_VALUE/2;
            center = center - center % getRealCount();
            return center;
        } else {
            return 0;
        }
    }

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

    /**
         * 更新数据源,设置刷新标志
         * @param data
         */
    public void setData(List<T> data) {
        this.data = data;
        this.notify = true;
        notifyDataSetChanged();
        this.notify = false;
    }

    /**
         * 重写该方法以防止notifyDataSetChanged不生效
         * @param object
         * @return
         */
    @Override
    public int getItemPosition(@NonNull Object object) {
        if(notify) {
            return POSITION_NONE;
        } else {
            return super.getItemPosition(object);
        }
    }

    public void setHolder(BaseBannerViewHolder<T> holder) {
        this.holder = holder;
    }

    public void setOnPageClickListener(OnPageClickListener onPageClickListener) {
        this.onPageClickListener = onPageClickListener;
    }
}

代码注释非常清楚了,主要逻辑在于getCountgetFirstPosition这两个方法,getCount返回一个极大数可以保证无限滑动的可能性,getFirstPosition从中间开始取是为了保证刚开始能够左滑。这里我将Adapter生成View的逻辑交给了ViewHolder去完成,对应的代码如下:

/**
 * @author 小米Xylitol
 * @email xiaomi987@hotmail.com
 * @desc ViewPager视图生成工具
 * @date 2018-06-07 16:26
 */
public interface BaseBannerViewHolder<T> {
    View createView();
    void bind(int position,T bean);
}

public class BannerViewHolder implements BaseBannerViewHolder<String>{

    private Context context;
    private TextView tvBanner;
    private ImageView ivBanner;

    public BannerViewHolder(Context context) {
        this.context = context;
    }

    public View createView() {
        View view = LayoutInflater.from(context).inflate(R.layout.item_banner,null,false);
        tvBanner = view.findViewById(R.id.tv_banner);
        ivBanner = view.findViewById(R.id.iv_banner);
        return view;
    }

    public void bind(int position,String s) {
        if(position % 3 == 0) {
            ivBanner.setBackgroundColor(0xffff0000);
        } else if(position % 3 == 1) {
            ivBanner.setBackgroundColor(0xff00ff00);
        } else {
            ivBanner.setBackgroundColor(0xff0000ff);
        }
        tvBanner.setText(s);
    }
}

简单的逻辑,不再多说,哦对了别忘了在给对应View设置数据时使用的position需要转成对应的realPosition,不然会出现数组越界的问题

自动滚动

自动滚动功能,我这里使用了我们的老朋友Handler,使用Handler发出演示意图,并在意图中在此发出一个延时意图,代码如下:

private Handler handler = new Handler();

private Runnable loopRunnable = new Runnable() {
    @Override
    public void run() {
        if(isLoop && !isTouch) {
            int currentItem = viewPager.getCurrentItem();
            currentItem++;
            viewPager.setCurrentItem(currentItem,true);
            startScroll();
        }
    }
};

/**
     * 开始自动滚动
     */
public void startScroll() {
    handler.removeCallbacksAndMessages(null);
    if(isLoop) {
        handler.postDelayed(loopRunnable, delayMillis);
    }
}
/**
     * 暂停自动滚动
     */
public void stopScroll() {
    handler.removeCallbacksAndMessages(null);
}

在相应的初始化逻辑中调用startScroll即可开始自动滚动,若不需要滚动时,调用stopScroll即可停止自动滚动。

触摸禁止滑动

这个功能较为简单,重写OnTouchListener,判断手势,然后作相应的处理即可

viewPager.setOnTouchListener(new OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_MOVE:
                isTouch = true;
                stopScroll();
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                isTouch = false;
                startScroll();
                break;
        }
        return false;
    }
});

可以看到我们定义了一个手指触摸的标志位,在手指按下和滑动时置为true,并停止自动滚动,在手机抬起和取消触摸时将其设置为false,并且开启自动滚动

响应单独item点击事件

响应单独的点击事件,我这里写了一个接口来做:

public interface OnPageClickListener {
    void onPageClick(int position);
}

在Adapter中对相应的View进行设置:

/**
         * 生成对应的View
         * @param container
         * @param position
         * @return
         */
private View getView(ViewGroup container, final int position) {
    View view = holder.createView();
    view.setOnClickListener(new OnClickListener() {
        @Override
        public void onClick(View v) {
            if(onPageClickListener != null) {
                onPageClickListener.onPageClick(position % getRealCount());
            }
        }
    });
    holder.bind(position % getRealCount(),data.get(position % getRealCount()));
    return view;
}

指示条

指示条这个东西因为样式有很多差异的,有些Banner喜欢使用数字表示,有些喜欢使用圆点,我这里选择用接口的方式来实现:

/**
 * @author 小米Xylitol
 * @email xiaomi987@hotmail.com
 * @desc 指示器
 * @date 2018-06-07 17:15
 */
public interface PagerIndicator {
    void setCurrentItem(int position);

    void attachView(ViewGroup viewGroup);
}

可以看到只有两个方法,一个是将indicator指示器加入到BannerView,一个是设置当前滑动的位置,在BannerView中在相应的位置进行调用:

/**
     * 配置指示器
     */
private void initIndicator() {
    if(indicator != null) {
        indicator.attachView(this);
        viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
            @Override
            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {

            }

            @Override
            public void onPageSelected(int position) {
                indicator.setCurrentItem(position % adapter.getRealCount());
            }

            @Override
            public void onPageScrollStateChanged(int state) {

            }
        });
    }
}

/**
     * 设置指示器
     * @param indicator
     */
public void setIndicator(PagerIndicator indicator) {
    this.indicator = indicator;
    initIndicator();
}

我这里实现了一个圆点的指示器,如下:

/**
 * @author 小米Xylitol
 * @email xiaomi987@hotmail.com
 * @desc 圆点指示器
 * @date 2018-06-07 17:36
 */
public class CirclePageIndicator implements PagerIndicator {

    private int size;
    private List<ImageView> imageViewList = new ArrayList<>();
    private int currentItem = 0;

    public CirclePageIndicator(int size) {
        this.size = size;
    }

    @Override
    public void setCurrentItem(int position) {
        this.currentItem = position;
        for(int i = 0;i < imageViewList.size();i++) {
            ImageView imageView = imageViewList.get(i);
            if(i == currentItem) {
                imageView.setImageResource(R.drawable.indicator_selected);
            } else {
                imageView.setImageResource(R.drawable.indicator_normal);
            }
        }
    }

    @Override
    public void attachView(ViewGroup viewGroup) {
        LinearLayout linearLayout = new LinearLayout(viewGroup.getContext());
        linearLayout.setGravity(Gravity.CENTER);
        linearLayout.setOrientation(LinearLayout.HORIZONTAL);
        for(int i = 0;i < size;i++) {
            ImageView imageView = new ImageView(viewGroup.getContext());
            if(i == currentItem) {
                imageView.setImageResource(R.drawable.indicator_selected);
            } else {
                imageView.setImageResource(R.drawable.indicator_normal);
            }
            imageViewList.add(imageView);
            linearLayout.addView(imageView);
            if(i != size - 1) {
                ViewGroup.MarginLayoutParams marginLayoutParams = (ViewGroup.MarginLayoutParams) imageView.getLayoutParams();
                marginLayoutParams.rightMargin = BannerView.dpToPx(5);
            }
        }
        viewGroup.addView(linearLayout);
        RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) linearLayout.getLayoutParams();
        params.width = RelativeLayout.LayoutParams.MATCH_PARENT;
        params.height = BannerView.dpToPx(15);
        params.addRule(RelativeLayout.CENTER_HORIZONTAL);
        params.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
        linearLayout.setBackgroundColor(0x33000000);
    }
}

画廊效果

画廊效果说穿了其实就是对ViewGroup中clipChildren属性的使用,该属性的作用是是否不允许子View突破父控件的大小,默认为true,即默认不突破父控件大小,我们这里要实现画廊效果,就需要给BannerView的根View设置其clipChildren属性为false,具体逻辑如下:

/**
     * 设置Banner展示左右两侧其余banner
     * @param nesting
     * @param margin
     */
public void setNesting(boolean nesting,int margin) {
    this.isNesting = nesting;
    setClipChildren(!isNesting);
    if(!isNesting) {
        MarginLayoutParams params = (MarginLayoutParams) viewPager.getLayoutParams();
        params.leftMargin = dpToPx(margin);
        params.rightMargin = dpToPx(margin);
    }
}

注意如果在设置了false之后,不对ViewPager进行margin的设置,那样还是不会出现预想的效果,因为屏幕宽度已经被占满了

使用

最后,使用起来是这样的:

<com.xylitolz.androidbannerview.BannerView
        android:id="@+id/banner_view"
        android:layout_width="match_parent"
        android:layout_height="120dp"
        app:delayMillis="2000"
        app:loop="true"
        app:infinity="true"
        />

或者在代码中使用:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    bannerView = findViewById(R.id.banner_view);

    final List<String> data = new ArrayList<>();
    data.add("Banner0");
    data.add("Banner1");
    data.add("Banner2");
    bannerView.setData(data,new BannerViewHolder(this));
    bannerView.setNesting(true,20);
    bannerView.setIndicator(new CirclePageIndicator(data.size()));
    bannerView.setOnPageClickListener(new BannerView.OnPageClickListener() {
        @Override
        public void onPageClick(int position) {
            String toast = "this is page " + data.get(position);
            Toast.makeText(MainActivity.this,toast,Toast.LENGTH_SHORT).show();
        }
    });
}

总结

以上这些就是我在做BannerView时遇到的大部分问题了,还有一些遗漏之处,具体看代码吧,哦等下,先上一个效果图:

博客中涉及到的代码地址在github,欢迎fork,与我一起改进这个控件~有任何疑问或者文章中有任何错误,请在评论区留言告诉我,不胜感激~
我的个人博客,欢迎访问,留言~

enjoy~

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Android中,可以使用第三方库来实现轮播图效果,其中一个常用的库是Banner。在使用Banner库时,可以通过设置一些属性来自定义轮播图的效果。例如,可以设置轮播间隔时间、轮播图的圆角等属性。\[1\] 此外,还可以设置轮播图的样式,内置样式有多种可供选择,可以根据需求进行设置。可以设置图片的加载方式,设置图片的网址或地址集合,设置轮播图的标题集合等。还可以设置是否自动轮播、显示器的位置等。\[2\] 如果想要实现一个简单的轮播图效果,可以使用Banner框架提供的BannerImageAdapter适配器。可以通过设置适配器来加载轮播图的数据,并在适配器中设置图片的显示方式。\[3\] 总结起来,使用Banner库可以方便地实现Android中的轮播图效果,通过设置属性和适配器来自定义轮播图的样式和数据。 #### 引用[.reference_title] - *1* *3* [Android---Banner轮播图](https://blog.csdn.net/qq_44950283/article/details/128390186)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^koosearch_v1,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [Android入门Banner轮播图的使用(详解)](https://blog.csdn.net/m0_53223811/article/details/122562021)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^koosearch_v1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值