前言
前些天需要使用到安卓的banner,也就是现在主流app主页的无限轮播的横幅,现在已经有很多好的开源项目可以直接使用,不过我还是想自己去实现一遍。因为是访问的网络数据,实际过程中还是有些坑的,所以还是记录一下。
具体实现
首先还是来看看最终的效果,gif是有些卡顿,跑起来还是很流畅的
了解到,现在实现这种横幅,基本上是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