实现安卓无限轮播组件Banner

前言

前些天需要使用到安卓的banner,也就是现在主流app主页的无限轮播的横幅,现在已经有很多好的开源项目可以直接使用,不过我还是想自己去实现一遍。因为是访问的网络数据,实际过程中还是有些坑的,所以还是记录一下。

具体实现

首先还是来看看最终的效果,gif是有些卡顿,跑起来还是很流畅的


BannViewDemo

了解到,现在实现这种横幅,基本上是2种方式,一种是使用RecyclerView的横向滚动去实现,因为横幅是从一个页面直接跳转到下一页,用RecyclerView需要监听滑动的过程,计算滑动的距离,然后进行跳转,后面官方考虑到这一点提供了PagerSnapHelper这个工具类来解决这个问题,这里就不多说了。
这篇博客主要就是写的就是第二种方式,使用ViewPager去实现。

首先,要用ViewPager实现无限轮播,可以使PagerAdapter的getCount方法返回Integer.MAX_VALUE。也就是让页面数量返回一个Integer的最大值,这样在滑动过程中产生一种无限循环的假象,首先写一个抽象基类,继承自PagerAdapter

定义适配器

public abstract class BannerViewBaseAdapter extends PagerAdapter {

    private List mList;
    private View mView;

    public BannerViewBaseAdapter() {
        mList = new ArrayList<>();
    }

    /**
     * 返回Integer的最大值
     */
    @Override
    public int getCount() {
        return Integer.MAX_VALUE;
    }

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

    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        Log.d("I am postion cx cx xcx", String.valueOf(position));
        if (getSize() != 0) {
            if (mList.size() <= (position % getSize())) {
                for (int i = mList.size();i <= position % getSize();++i) {
                    mList.add(getView(container,i));
                }
            }
            mView = mList.get(position % getSize());
            if (mView.getParent() != null) {
                container.removeView(mView);
            }
            container.addView(mView);
        }

        return mView;
    }

    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        // 滑动下一张图时当前的图
        if (getSize() != 0 && position != 0) {
            container.removeView(mList.get(position % getSize()));
        }
    }

    /**
     *  获取要显示的View
     * @param container
     * @param position
     * @return
     */
    public abstract View getView(ViewGroup container,int position);

    /**
     *  获取实际ItemView的数量
     * @return
     */
    public abstract int getSize();

这里主要是重写instantiateItem方法,进行添加页面的逻辑,首先要判断getSize不为0,因为实际网络加载时可能是一个异步的耗时操作,如果执行到下面的计算时,除数为0肯定会报错的,其它类似地方也都是这样,然后注意到下面的position%getSize,这里是用当前位置对实际item的数量进行模运算取余,得到的值就是当前item的实际位置(前面说过无限轮播是不停的增加页面,造成轮播的假象,实际位置就是指在进行轮播的几个item中,当前处的位置),接下来就判断,如果item不在集合中,就把view添加到一个List集合里面,最后要防止同一个view的重复添加,所以每次添加前需要移除这个view。
接着重写destroyItem方法,每次循环跳转时都要销毁掉之前的view。下面两个抽象方法就是用来获取到具体的值了。

然后就是我们的具体视图的适配器

public class BannerViewAdapter extends BannerViewBaseAdapter {

    private List mBeansList;
    private Context mContext;

    public BannerViewAdapter(List bannerBeans) {
        this.mBeansList = bannerBeans;
    }

    @Override
    public View getView(ViewGroup container, int position) {
        AppCompatImageView imageView;
        TextView title;

        if (mContext == null) {
            mContext = container.getContext();
        }
        View mView = LayoutInflater.from(mContext).inflate(R.layout.banner_item_layout,null);

        final TestBean bean = mBeansList.get(position);
        imageView = mView.findViewById(R.id.image);
        title = mView.findViewById(R.id.banner_title);
        title.setText(bean.getTitle());
        Glide.with(mContext).load(bean.getImageId())
                .error(R.drawable.ic_launcher_background)
                .into(imageView);
        notifyDataSetChanged();

        imageView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(mContext,"你点击了"+bean.getTitle(),Toast.LENGTH_SHORT).show();
            }
        });
        return mView;
    }

    @Override
    public int getSize() {
        return mBeansList.size();
    }
}

这里基础抽象基类,逻辑比较简单,没什么好讲的。

视图绘制

然后重点就是自定义的BannerView类了


public class BannerView extends FrameLayout implements ViewPager.OnPageChangeListener {

    private ViewPager mViewPager;

    /**
     *  圆点布局
     */
    private LinearLayout mPointContainer;

    private BannerViewBaseAdapter mAdapter;

    /**
     *  圆点数量
     */
    private int mPointCount;

    /**
     *  圆点图片
     */
    private ImageView[] mPoints;

    /**
     *  最后一个圆点
     */
    private int mLastPos;

    /**
     *  当前是否触摸
     */
    private boolean isTouch = false;

    private ScheduledExecutorService executorService;

    private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case 0:
                    postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            mViewPager.setCurrentItem(mViewPager.getCurrentItem()+1);
                        }
                    },1000);
                    break;
                default:
                    break;
            }
        }
    };

    public BannerView(@NonNull Context context, AttributeSet attributeSet) {
        super(context,attributeSet);
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        initView();
    }

    private void initView() {
        mViewPager = findViewById(R.id.views_container);
        mPointContainer = findViewById(R.id.point_container);
        mViewPager.addOnPageChangeListener(this);
    }

    public void setAdapter(BannerViewAdapter adapter) {
        this.mAdapter = adapter;
        mPointCount = mAdapter.getSize();
        mViewPager.setAdapter(mAdapter);
        Log.d("sddsccdsvdsvdv", String.valueOf(mPointCount*100));
        initPoint();
        /**
         *  防止第二次刷新后 显示空白页面
         */
        mViewPager.setCurrentItem(mPointCount*100+3);
        startScroll();
    }

    /**
     *  加载圆点
     */
    private void initPoint() {
       if (mPointCount == 0) {
           return;
       }

       mPoints = new ImageView[mPointCount];
       // 清chu所有圆点
       mPointContainer.removeAllViews();
       for (int i=0;i < mPointCount;i++) {
           ImageView view = new ImageView(getContext());
           view.setImageResource(R.drawable.point_normal);
           mPointContainer.addView(view);
           mPoints[i] = view;
       }
       if (mPoints[0] != null) {
           mPoints[0].setImageResource(R.drawable.point_selected);
       }
       mLastPos = 0;
    }
    /**
     * 改变圆点位置
     */
    private void changePoint(int currentPoint) {
        if (mLastPos == currentPoint) {
            return;
        }
        mPoints[currentPoint].setImageResource(R.drawable.point_selected);
        mPoints[mLastPos].setImageResource(R.drawable.point_normal);
        mLastPos = currentPoint;
    }

    public void startScroll() {

        executorService = new ScheduledThreadPoolExecutor(1);
        executorService.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                if (isTouch) {
                    return;
                }
                handler.sendEmptyMessage(0);
            }
        },1000,3000, TimeUnit.MILLISECONDS);
    }

    public void cancelScroll() {
        if (executorService != null) {
            executorService.shutdown();
        }
    }

    @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {

    }

    @Override
    public void onPageSelected(int position) {
        if (mPointCount != 0) {
            changePoint(position % mPointCount);
        }
    }

    @Override
    public void onPageScrollStateChanged(int state) {

    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                isTouch = true;
                break;
            case MotionEvent.ACTION_UP:
                isTouch = false;
                break;
            default:
                break;
        }
        return super.dispatchTouchEvent(ev);
    }
}

BannerView继承FrameLayout,实现滑动监听的接口,这里先看看initView方法,这里给viewpager设置了适配器,然后接着就是加载圆点指示器的方法,逻辑也比较简单,加载前注意先移除掉之前的圆点,防止刷新后点的数量重复添加。然后就是第一次加载时,时ViewPager跳转到mPoinCoun*100+x的位置,也是为了防止首次加载无法向左滑动,就不是无限循环的假象了。然后就让我们的ViewPager执行定时滑动任务,定时任务有很多的实现方式,可以使用Timer+handler的方式,可以使用CountDownTimer类来实现,这里的话,我使用的是线程池来进行的定时任务,后面分别传入预加载与跳转周期,然后里面需要进行判断当前是否触摸屏幕,所以要重写dispatchEvent方法,监听当前的动作,然后向handler发送消息,再进行页面滑动,这里可能会有疑惑,线程池已经设置了定时任务,为什么还要向handler去发送消息,进行延时处理,handler里面才是真正的滚动延时,除了是非Ui线程不进行Ui更新的操作 也是因为在我们刷新后,线程池会造成阻塞,无法正常执行。我使用了其它几种方式,还是出现了各种问题,这里就不多说了。

整个BannerView的流程就走完了
再看看布局文件 banner_item_layout




    

    

banner_view_layout



    

    

        
    


使用的话就直接给adapter添加数据,然后实例化给BannerView设置adapter就可以了。这里很要注意一点,就是每次刷新之前记得手动关闭线程池,也就是调用BannView中的cancelScroll()方法,不然会造成进行网络加载,第二次刷新加载数据时,banner直接出现空白页面,这肯定不是我们想要的。 另外,配合RecyclerView使用时,可以把BannerView动态添加到RecyclerView的头部,直接放到布局中,如果要实现RecyclerView上滑时,banner跟着一起滚动的话,可以使用NestedScrollView,但是会造成嵌套滑动冲突,这一点也没看到好的解决办法,设置了NestedScrollingEnable属性,但是会导致RecyclerView无法上拉加载,如果一次性加载完数据的话,RecyclerView的复用和回收机制就没起到作用了,很容易出现OOM,这也不是我们想看到的,所以最好还是配合RecyclerView使用。

总结

总的来说,实现一个并不困难,难在实际过程中会出现各种各样的问题,也正是常说的,debug时间远远多于写代码的是时间(说到底还是经验不足的,理解不够深的缘故)。废话不多说,最后该Demo地址github

你可能感兴趣的:(实现安卓无限轮播组件Banner)