为什么写这篇文章,因为在网上看到的绝大多数BannerView实现了右无限轮播图,甚至没有实现无限轮播图,说成是无限轮播图,实现了左右无限轮播图的,并没有做性能上的优化。
先看张效果图
device-2018-05-11-173850.gif
工程目录图
project.png
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(); }@Overrideprotected 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<View> 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 */@Overrideprotected 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;@Overridepublic 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去设置动画,会有坑的存在。哪位大神看到了,望赐教!