> 文章列表 > Android打造万能的BannerView无限轮播图

Android打造万能的BannerView无限轮播图

Android打造万能的BannerView无限轮播图

效果图:

Android打造万能的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) {//创建一个BitmapBitmap 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);//回收Bitmapbitmap.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);//回收Bitmapbitmap.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<View> mConvertView;@SuppressLint("HandlerLeak")private Handler mHandler = new Handler() {@Overridepublic 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的私有的属性mScrollerField 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*/@Overridepublic int getCount() {return Integer.MAX_VALUE;}@NonNull@Overridepublic 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;}@Overridepublic boolean isViewFromObject(@NonNull View view, @NonNull Object object) {return view == object;}@Overridepublic 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() - 1if (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() {@Overridepublic void onActivityResumed(Activity activity) {super.onActivityResumed(activity);if (activity == getContext()) {//开启轮播mHandler.sendEmptyMessageDelayed(SCROLL_MSG, mCutDownTime);}}@Overridepublic 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实现列表滑动一样的,需要界面复用。

BannerView类

public class BannerView extends RelativeLayout {private BannerViewPager mBannerViewPager;//底部的指示器的Viewprivate LinearLayout mDotContainerView;//适配器private BannerAdapter mAdapter;private Context mContext;//选中的drawableprivate Drawable mIndicatorFocusDrawable;//未被选中的drawableprivate 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() {@Overridepublic 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