为什么写这篇文章,因为在网上看到的绝大多数BannerView实现了右无限轮播图,甚至没有实现无限轮播图,说成是无限轮播图,实现了左右无限轮播图的,并没有做性能上的优化。
先看张效果图
工程目录图
BannerAdapter:banner轮播图的适配器,因为服务器返回的列表图片的url,显示的时候需要转成IamgeViw;
BannerScroller:设置切换页面的持续时间;
BannerView:继承RelativeLayout,包含BannViewPager和底部DotIndicatorView指示器;
BannerViewPager:继承ViewPager,设置ViewPager的适配器Adpter和动画;
DotIndicatorView:底部指示器;
DotIndicatorView类
public class DotIndicatorView extends View {
//形状
private int mShape;
// 矩形
public static final int SHAPE_REC = 1;
// 圆形
public static final int SHAPE_CIRCLE = 2;
private Drawable mDrawable;
public DotIndicatorView(Context context) {
this(context, null);
}
public DotIndicatorView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public DotIndicatorView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.DotIndicatorView);
//默认是圆形
mShape = typedArray.getInteger(R.styleable.DotIndicatorView_shape, SHAPE_CIRCLE);
typedArray.recycle();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mDrawable != null) {
Bitmap bitmap = drawableToBitmap(mDrawable);
if (mShape == SHAPE_CIRCLE) {
Bitmap circleBitmap = getCircleBitmap(bitmap);
canvas.drawBitmap(circleBitmap, 0, 0, null);
} else if (mShape == SHAPE_REC) {
Bitmap recBitmap = getRecBitmap(bitmap);
canvas.drawBitmap(recBitmap, 0, 0, null);
}
}
}
public void setDrawable(Drawable drawable) {
mDrawable = drawable;
invalidate();
}
/**
* drawable转bitmap
*
* @param drawable
* @return
*/
private Bitmap drawableToBitmap(Drawable drawable) {
if (drawable instanceof BitmapDrawable) {
return (( BitmapDrawable ) drawable).getBitmap();
}
//其他类型 ColorDrawable
//创建一个什么也没有的Bitmap;
Bitmap outBitmap = Bitmap.createBitmap(getMeasuredWidth(),
getMeasuredHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(outBitmap);
//把drawable画到Bitmap上 --》将drawable绘制在canvas内部
drawable.setBounds(0, 0, getMeasuredWidth(), getMeasuredHeight());
drawable.draw(canvas);
return outBitmap;
}
public void setShape(int shape) {
mShape = shape;
}
public int getShape() {
return mShape;
}
/**
* 圆形
*
* @param bitmap
* @return
*/
private Bitmap getCircleBitmap(Bitmap bitmap) {
//创建一个Bitmap
Bitmap circleBitmap = Bitmap.createBitmap(getMeasuredWidth(),
getMeasuredHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(circleBitmap);
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setFilterBitmap(true);
//防止抖动
paint.setDither(true);
//在画布上绘制一个圆
canvas.drawCircle(getMeasuredWidth() / 2, getMeasuredHeight() / 2, getMeasuredWidth() / 2, paint);
//设置画笔的图层,PorterDuff.Mode.SRC_IN 取图层交集的上层
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
//在把原来的bitmap绘制到圆上面
canvas.drawBitmap(bitmap, 0, 0, paint);
//回收Bitmap
bitmap.recycle();
return circleBitmap;
}
/**
* 带圆角的矩形
*
* @param bitmap
* @return
*/
private Bitmap getRecBitmap(Bitmap bitmap) {
Bitmap recBitmap = Bitmap.createBitmap(getMeasuredWidth(), getMeasuredHeight(),
Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(recBitmap);
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setFilterBitmap(true);
//防止抖动
paint.setDither(true);
//在画布上绘制一个圆角的矩形
canvas.drawRoundRect(new RectF(0, 0, getMeasuredWidth(), getMeasuredHeight()),
DensityUtil.dip2px(this.getContext(), 2), DensityUtil.dip2px(this.getContext(), 2), paint);
//设置画笔的图层,PorterDuff.Mode.SRC_IN 取图层交集的上层
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
//在把原来的bitmap绘制到圆上面
canvas.drawBitmap(bitmap, 0, 0, paint);
//回收Bitmap
bitmap.recycle();
return recBitmap;
}
}
一般底部会有两种类型指示器,一是矩形的,二是圆形的,这个类实现了如何自定义矩形和圆形指示器,其实这个类也可以实现圆形的和带圆角的矩形的图片,用PorterDuffXfermode图层的概念。
BannerAdapter类
public abstract class BannerAdapter {
/**
* 根据位置获取ViewPager的子View
*
* @param position
* @return
*/
public abstract View getView(int position, View convertView);
/**
* 返回数量
*
* @return
*/
public abstract int getCount();
}
BannerAdapter这个类是轮播图的适配器,因为服务器返回的列表图片的url,显示的时候需要转成IamgeViw,用适配器设计模式转一下。
BannerViewPager类
public class BannerViewPager extends ViewPager {
private static final String TAG = BannerViewPager.class.getSimpleName();
private static final int SCROLL_MSG = 0x011;
private BannerAdapter mBannerAdapter;
private int mCutDownTime = 3000;
private BannerScroller mBannerScroller;
//内存优化界面复用
private List mConvertView;
@SuppressLint("HandlerLeak")
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case SCROLL_MSG:
setCurrentItem(getCurrentItem() + 1);
startLoop();
break;
}
}
};
public BannerViewPager(Context context) {
this(context, null);
}
public BannerViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
//改变ViewPager切换的速率
try {
//获取ViewPager的私有的属性mScroller
Field field = ViewPager.class.getDeclaredField("mScroller");
mBannerScroller = new BannerScroller(context);
//设置强制改变
field.setAccessible(true);
//设置参数 第一个参数object当前属性的那个类 第二参数需要设置的值
field.set(this, mBannerScroller);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
mConvertView = new ArrayList<>();
}
/**
* 设置切换页面的持续时间
*
* @param scrollerDuration
*/
public void setScrollerDuration(int scrollerDuration) {
mBannerScroller.setScrollerDuration(scrollerDuration);
}
public void setAdapter(BannerAdapter adapter) {
this.mBannerAdapter = adapter;
setAdapter(new BannerPagerAdapter());
//管理Activity的生命周期
(( Activity ) (getContext())).getApplication().registerActivityLifecycleCallbacks(mDefaultActivityLifecycleCallbacks);
}
/**
* 开启轮播
*/
public void startLoop() {
mHandler.removeMessages(SCROLL_MSG);
mHandler.sendEmptyMessageDelayed(SCROLL_MSG, mCutDownTime);
}
/**
* 销毁Handler
*/
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mHandler.removeMessages(SCROLL_MSG);
mHandler = null;
}
private class BannerPagerAdapter extends PagerAdapter {
/**
* 给一个很大的值,为了实现无限轮播
* 这个方法是返回ViewPager有多少个View
*/
@Override
public int getCount() {
return Integer.MAX_VALUE;
}
@NonNull
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
//Adapter设计模式为了完全让用户自定义
//position 0-2的31次方
Log.i(TAG, "instantiateItem:position=" + position + "mBannerAdapter.getCount()=" + mBannerAdapter.getCount());
//position % mBannerAdapter.getCount() 求模
View bannerItemView = mBannerAdapter.getView(position % mBannerAdapter.getCount(), getConvertView());
container.addView(bannerItemView);
return bannerItemView;
}
@Override
public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
return view == object;
}
@Override
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
container.removeView(( View ) object);
mConvertView.add(( View ) object);
}
}
private float mDownX;
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mDownX = ev.getX();
mHandler.removeMessages(SCROLL_MSG);
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
//左滑动到第一张,跳转到getCount() - 1
if (this.getCurrentItem() == 0) {
if (ev.getX() - mDownX > 0) {
this.setCurrentItem(mBannerAdapter.getCount() - 1);
Log.i(TAG, "onTouchEvent: " + this.getCurrentItem());
}
}
mHandler.sendEmptyMessageDelayed(SCROLL_MSG, mCutDownTime);
break;
}
return super.onTouchEvent(ev);
}
/**
* 处理页面复用
*
* @return
*/
public View getConvertView() {
for (int i = 0; i < mConvertView.size(); i++) {
if (mConvertView.get(i).getParent() == null) {
return mConvertView.get(i);
}
}
return null;
}
/**
* 管理Activity的生命周期
*/
DefaultActivityLifecycleCallbacks mDefaultActivityLifecycleCallbacks = new DefaultActivityLifecycleCallbacks() {
@Override
public void onActivityResumed(Activity activity) {
super.onActivityResumed(activity);
if (activity == getContext()) {
//开启轮播
mHandler.sendEmptyMessageDelayed(SCROLL_MSG, mCutDownTime);
}
}
@Override
public void onActivityPaused(Activity activity) {
super.onActivityPaused(activity);
if (activity == getContext()) {
//停止轮播
mHandler.removeMessages(SCROLL_MSG);
}
}
};
}
继承PagerAdapter实现getCount()这个方法,这个方法返回的是ViewPager有多少个View。为了实现无限轮播图返回了Integer.MAX_VALUE,用户不会手残一直向右滑动吧,造成溢出吧,哈。DefaultActivityLifecycleCallbacks 去监听Activity的生命周期,为什么要监听呢?因为当用户点击home键的时候,此时应用会在后台,但是ViewPager里面的ImageView还会循环,所以在Activity执行onPaused()的时候,停止轮播。getConvertView()这个方法是处理界面复用的,意思是跟RecycleView或者ListView实现列表滑动一样的,需要界面复用。最后,小编想实现一个左滑动到position=0,也就是第一张的时候,想跳转到getCount-1张,具体的做法是想在onTouchEvent()方法监听,手指按下记录下mDownX,手指抬起的时候ev.getX(),用ev.getX() - mDownX > 0坐下判断。在设置下 setCurrentItem(mBannerAdapter.getCount() - 1);
发现并没有实现。也不知道这是为什么,但是我认为这种思路没错,哪位大神看到了,请给出具体解决方案。
BannerView类
public class BannerView extends RelativeLayout {
private BannerViewPager mBannerViewPager;
//底部的指示器的View
private LinearLayout mDotContainerView;
//适配器
private BannerAdapter mAdapter;
private Context mContext;
//选中的drawable
private Drawable mIndicatorFocusDrawable;
//未被选中的drawable
private Drawable mIndicatorNormalDrawable;
//当前页面的位置
private int mCurrentPosition;
//指示器的位置
private int mDotGravity = -1;
//指示器的大小
private int mDotSize = 6;
//指示器的间距
private int mDotDistance = 2;
//底部颜色默认透明
private int mBottomColor = Color.TRANSPARENT;
private View mBannerBottomView;
public BannerView(Context context) {
this(context, null);
}
public BannerView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public BannerView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
inflate(context, R.layout.banner_layout, this);
this.mContext = context;
initAttribute(attrs);
initView();
}
/**
* 初始化自定义属性
*
* @param attrs
*/
private void initAttribute(AttributeSet attrs) {
TypedArray typedArray = mContext.obtainStyledAttributes(attrs, R.styleable.BannerView);
mDotGravity = typedArray.getInt(R.styleable.BannerView_dotGravity, -1);
mIndicatorFocusDrawable = typedArray.getDrawable(R.styleable.BannerView_dotIndicatorFocus);
if (mIndicatorFocusDrawable == null) {
mIndicatorFocusDrawable = new ColorDrawable(Color.RED);
}
mIndicatorNormalDrawable = typedArray.getDrawable(R.styleable.BannerView_dotIndicatorNormal);
if (mIndicatorNormalDrawable == null) {
mIndicatorNormalDrawable = new ColorDrawable(Color.WHITE);
}
mDotSize = ( int ) typedArray.getDimension(R.styleable.BannerView_dotSize, DensityUtil.dip2px(mContext, 6));
mDotDistance = ( int ) typedArray.getDimension(R.styleable.BannerView_dotDistance, DensityUtil.dip2px(mContext, 2));
mBottomColor = typedArray.getColor(R.styleable.BannerView_bottomColor, mBottomColor);
typedArray.recycle();
}
/**
* 初始化View
*/
private void initView() {
mBannerViewPager = findViewById(R.id.bannerViewPager);
mDotContainerView = findViewById(R.id.dot_container);
mBannerBottomView = findViewById(R.id.bannerBottomView);
mBannerBottomView.setBackgroundColor(mBottomColor);
mBannerViewPager.setPageTransformer(false, new SlidePageTransformer());
}
/**
* 设置适配器adapter
*
* @param adapter 适配器
*/
public void setAdapter(BannerAdapter adapter) {
this.mAdapter = adapter;
mBannerViewPager.setAdapter(adapter);
mBannerViewPager.setCurrentItem(mBannerViewPager.getChildCount() / 2);
initDotIndicator();
mBannerViewPager.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
@Override
public void onPageSelected(int position) {
//监听下当前的位置
super.onPageSelected(position);
DotIndicatorView dotIndicatorView = ( DotIndicatorView ) mDotContainerView.
getChildAt(mCurrentPosition);
dotIndicatorView.setDrawable(mIndicatorNormalDrawable);
mCurrentPosition = position % mAdapter.getCount();
DotIndicatorView mCurrentIndicatorView = ( DotIndicatorView ) mDotContainerView.
getChildAt(mCurrentPosition);
mCurrentIndicatorView.setDrawable(mIndicatorFocusDrawable);
}
});
}
public void startLoop() {
mBannerViewPager.startLoop();
}
public void setScrollerDuration(int scrollerDuration) {
mBannerViewPager.setScrollerDuration(scrollerDuration);
}
/**
* 初始化指示器
*/
private void initDotIndicator() {
//获取广告位的数量
int count = mAdapter.getCount();
//设置指示器的位置
mDotContainerView.setGravity(getDotGravity());
for (int i = 0; i < count; i++) {
DotIndicatorView dot = new DotIndicatorView(mContext);
//设置指示器的形状
dot.setShape(1);
LinearLayout.LayoutParams param = null;
//矩形
if (dot.getShape() == 1) {
//给指示器指定大小
param = new LinearLayout.LayoutParams(mDotSize * 3, DensityUtil.dip2px(this.getContext(), 2));
//圆形
} else if (dot.getShape() == 2) {
param = new LinearLayout.LayoutParams(mDotSize, mDotSize);
}
//设置间距
param.leftMargin = param.rightMargin = mDotDistance;
dot.setLayoutParams(param);
if (i == 0) {
dot.setDrawable(mIndicatorFocusDrawable);
} else {
dot.setDrawable(mIndicatorNormalDrawable);
}
mDotContainerView.addView(dot);
}
}
public int getDotGravity() {
switch (mDotGravity) {
case 0:
return Gravity.CENTER;
case 1:
return Gravity.RIGHT;
case -1:
return Gravity.LEFT;
}
return Gravity.RIGHT;
}
SlidePageTransformer类
public class SlidePageTransformer implements ViewPager.PageTransformer {
@Override
public void transformPage(@NonNull View page, float position) {
if (position > 0 && position <= 1) {
page.setPivotX(0);
page.setScaleX(1 - position);
} else if (position >= -1 && position < 0) {
page.setPivotX(page.getWidth());
page.setScaleX(1 + position);
}
}
}
BannerView这个类主要是一些自定义属性,底部指示器的大小、颜色、间距等等。主要说下这个 mBannerViewPager.setPageTransformer(false, new SlidePageTransformer());这个给ViewPager设置了一个平滑的缩放的动画,但是看到了一个ViewPager设置动画的一个坑,发现滑到第一张的时候,在向右滑动的时候,图片会滑出一点边缘。也不知道为什么?我认为我的代码没有问题,也听说Android的源码ViewPager去设置动画,会有坑的存在。哪位大神看到了,望赐教!
项目的完整代码https://github.com/StevenYan88/BannerView