1.概述
这其实是我第一篇想写的博客,可能是因为我遇到了太多的坑,那个时候刚入行下了很多Demo发现怎么也改不动,可能是能力有限,这次就做一个具体的实现和彻底的封装。
上次讲了Android无限广告轮播-ViewPager源码分析,有了源码分析我们对ViewPager就有了一个大概的了解,那么再来封装成自定义View,就会简单许多,附视频讲解地址:http://pan.baidu.com/s/1skOdHzn

这里写图片描述
2.效果封装
2.1 自定义BannerViewPager extends ViewPager:
我们要利用Adapter设计模式,那么目前这个阶段,需要的方法就是根据PagerAdapter位置获取当前View,所以BannerAdapter里面就只需要一个方法那就是getView(int position);
1/** 2 * description: 3 * 广告轮播的ViewPager 4 * Created by 曾辉 on 2016/11/17. 5 * QQ:240336124 6 * Email: 240336124@qq.com 7 * Version:1.0 8 */ 9public class BannerViewPager extends ViewPager { 10 11 private Context mContext; 12 13 private BannerAdapter mAdapter; 14 15 public BannerViewPager(Context context) { 16 this(context, null); 17 } 18 19 public BannerViewPager(Context context, AttributeSet attrs) { 20 super(context, attrs); 21 this.mContext = context; 22 } 23 24 public void setAdapter(BannerAdapter adapter) { 25 this.mAdapter = adapter; 26 setAdapter(new BannerPagerAdapter()); 27 } 28 29 private class BannerPagerAdapter extends PagerAdapter { 30 31 @Override 32 public int getCount() { 33 // 返回一个很大的值,确保可以无限轮播 34 return Integer.MAX_VALUE; 35 } 36 37 @Override 38 public boolean isViewFromObject(View view, Object object) { 39 // 这么写就对了,看了源码应该就明白 40 return view == object; 41 } 42 43 @Override 44 public Object instantiateItem(ViewGroup container, final int position) { 45 View bannerView = mAdapter.getView(position); 46 container.addView(bannerView ); 47 return bannerView; 48 } 49 50 @Override 51 public void destroyItem(ViewGroup container, int position, Object object) { 52 // 销毁回调的方法 移除页面即可 53 container.removeView((View) object); 54 } 55 } 56}
这样我们只要给他设置一个BannerAdapter就可以实现ViewPager的效果,可以手动切换,这里就先不看效果。
2.2. 实现自动轮播
实现自动轮播比较简单,实现的方式有多种可以用定时器Timer、Handler发送消息、start Thread的行,这里我采用Handler发送消息的方法。
1 // 2.实现自动轮播 - 发送消息的msgWhat 2 private final int SCROLL_MSG = 0x0011; 3 4 // 2.实现自动轮播 - 页面切换间隔时间 5 private int mCutDownTime = 3500; 6 7 // 2.实现自动轮播 - 发送消息Handler 8 private Handler mHandler = new Handler(){ 9 @Override 10 public void handleMessage(Message msg) { 11 // 每隔*s后切换到下一页 12 setCurrentItem(getCurrentItem() + 1); 13 // 不断循环执行 14 startRoll(); 15 } 16 }; 17 18 /** 19 * 2.实现自动轮播 20 */ 21 public void startRoll(){ 22 // 清除消息 23 mHandler.removeMessages(SCROLL_MSG); 24 // 消息 延迟时间 让用户自定义 有一个默认 3500 25 mHandler.sendEmptyMessageDelayed(SCROLL_MSG,mCutDownTime); 26 Log.e(TAG,"startRoll"); 27 } 28 29 /** 30 * 2.销毁Handler停止发送 解决内存泄漏 31 */ 32 @Override 33 protected void onDetachedFromWindow() { 34 super.onDetachedFromWindow(); 35 mHandler.removeMessages(SCROLL_MSG); 36 mHandler = null; 37 }
我们看一下效果吧,但是发现Gif录制根本捕捉不到切换的效果,因为自动切换速度太快了,这里还是不贴效果了,下面我就需要改变切换的速度。
2.3. 改变切换速率
如果看过上篇文章的源码就知道,我们会调用Scroller的mScroller.startScroll(sx, sy, dx, dy, duration)的这个方法,如果我们需要改变速率就只能改变duration执行切换页面动画的时间,可是我们根本拿不到这个值,那么就只能修改mScroller这个属性,可又发现他是private的有点头大,但是我们可以利用反射设置mScroller;
1 // 3.改变ViewPager切换的速率 - 自定义的页面切换的Scroller 2 private BannerScroller mScroller; 3 4 public BannerViewPager(Context context, AttributeSet attrs) { 5 super(context, attrs); 6 7 try { 8 // 3.改变ViewPager切换的速率 9 // 3.1 duration 持续的时间 局部变量 10 // 3.2.改变 mScroller private 通过反射设置 11 Field field = ViewPager.class.getDeclaredField("mScroller"); 12 // 设置参数 第一个object当前属性在哪个类 第二个参数代表要设置的值 13 mScroller = new BannerScroller(context); 14 // 设置为强制改变private 15 field.setAccessible(true); 16 field.set(this,mScroller); 17 } catch (Exception e) { 18 e.printStackTrace(); 19 } 20 } 21 22 /** 23 * 3.设置切换页面动画持续的时间 24 */ 25 public void setScrollerDuration(int scrollerDuration){ 26 mScroller.setScrollerDuration(scrollerDuration); 27 }
现在效果差不多了,可以看到能够无限轮播,能够自动轮播,并且页面切换的速度也可以了,接下来就只需要处理点的指示器和文字的描述:

这里写图片描述
2.4. 自定义BannerView加入点指示和广告描述
接下来我们又自定义一个BannerView里面包含当前自定义好的BannerViewPager和点的指示LinearLayout以及广告描述TextView。
1package com.example.hui.androidtemplate.banner; 2 3import android.content.Context; 4import android.util.AttributeSet; 5import android.widget.LinearLayout; 6import android.widget.RelativeLayout; 7import android.widget.TextView; 8 9import com.example.hui.androidtemplate.R; 10 11/** 12 * description: 13 * <p/> 14 * Created by 曾辉 on 2016/11/18. 15 * QQ:240336124 16 * Email: 240336124@qq.com 17 * Version:1.0 18 */ 19public class BannerView extends RelativeLayout{ 20 // 4.自定义BannerView - 轮播的ViewPager 21 private BannerViewPager mBannerVp; 22 // 4.自定义BannerView - 轮播的描述 23 private TextView mBannerDescTv; 24 // 4.自定义BannerView - 点的容器 25 private LinearLayout mDotContainerView; 26 // 4.自定义BannerView - 自定义的BannerAdapter 27 private BannerAdapter mAdapter; 28 29 public BannerView(Context context) { 30 this(context, null); 31 } 32 33 public BannerView(Context context, AttributeSet attrs) { 34 this(context, attrs, 0); 35 } 36 37 public BannerView(Context context, AttributeSet attrs, int defStyleAttr) { 38 super(context, attrs, defStyleAttr); 39 40 // 把布局加载到这个View里面 41 inflate(context, R.layout.ui_banner_layout,this); 42 43 initView(); 44 } 45 46 /** 47 * 初始化View 48 */ 49 private void initView() { 50 mBannerVp = (BannerViewPager) findViewById(R.id.banner_vp); 51 mBannerDescTv = (TextView) findViewById(R.id.banner_desc_tv); 52 mDotContainerView = (LinearLayout) findViewById(R.id.dot_container); 53 } 54 55 /** 56 * 4.设置适配器 57 */ 58 public void setAdapter(BannerAdapter adapter){ 59 mBannerVp.setAdapter(adapter); 60 } 61 62 /** 63 * 4.开始滚动 64 */ 65 public void startRoll() { 66 mBannerVp.startRoll(); 67 } 68}
2.5. 初始化点的指示器
1 /** 2 * 5.初始化点的指示器 3 */ 4 private void initDotIndicator() { 5 // 获取广告的数量 6 int count = mAdapter.getCount(); 7 8 // 让点的位置在右边 9 mDotContainerView.setGravity(Gravity.RIGHT); 10 11 for (int i = 0;i<count;i++){ 12 // 不断的往点的指示器添加圆点 13 DotIndicatorView indicatorView = new DotIndicatorView(mContext); 14 // 设置大小 15 LinearLayout.LayoutParams params = new 16 LinearLayout.LayoutParams(dip2px(8),dip2px(8)); 17 // 设置左右间距 18 params.leftMargin = params.rightMargin = dip2px(2); 19 indicatorView.setLayoutParams(params); 20 21 if(i == 0) { 22 // 选中位置 23 indicatorView.setDrawable(mIndicatorFocusDrawable); 24 }else{ 25 // 未选中的 26 indicatorView.setDrawable(mIndicatorNormalDrawable); 27 } 28 mDotContainerView.addView(indicatorView); 29 } 30 } 31 32 /** 33 * 5.把dip转成px 34 */ 35 private int dip2px(int dip) { 36 return (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 37 dip,getResources().getDisplayMetrics()); 38 }
2.6. 阶段性的Bug修复
1 /** 2 * 4.设置适配器 3 */ 4 public void setAdapter(BannerAdapter adapter){ 5 mAdapter = adapter; 6 mBannerVp.setAdapter(adapter); 7 // 5.初始化点的指示器 8 initDotIndicator(); 9 10 // 6.Bug修复 11 mBannerVp.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener(){ 12 @Override 13 public void onPageSelected(int position) { 14 // 监听当前选中的位置 15 pageSelect(position); 16 } 17 }); 18 19 // 6.初始化的时候获取第一条的描述 20 String firstDesc = mAdapter.getBannerDesc(0); 21 mBannerDescTv.setText(firstDesc); 22 } 23 24 25 /** 26 * 6.页面切换的回调 27 * @param position 28 */ 29 private void pageSelect(int position) { 30 // 6.1 把之前亮着的点 设置为默认 31 DotIndicatorView oldIndicatorView = (DotIndicatorView) 32 mDotContainerView.getChildAt(mCurrentPosition); 33 oldIndicatorView.setDrawable(mIndicatorNormalDrawable); 34 35 36 // 6.2 把当前位置的点 点亮 position 0 --> 2的31次方 37 mCurrentPosition = position%mAdapter.getCount(); 38 DotIndicatorView currentIndicatorView = (DotIndicatorView) 39 mDotContainerView.getChildAt(mCurrentPosition); 40 currentIndicatorView.setDrawable(mIndicatorFocusDrawable); 41 42 // 6.3设置广告描述 43 String bannerDesc = mAdapter.getBannerDesc(mCurrentPosition); 44 mBannerDescTv.setText(bannerDesc); 45 }
2.7. 把指示器的点绘制成圆
1/** 2 * description: 圆的指示器 3 * 圆点指示器 4 * Created by 曾辉 on 2016/11/18. 5 * QQ:240336124 6 * Email: 240336124@qq.com 7 * Version:1.0 8 */ 9public class DotIndicatorView extends View { 10 11 private Drawable drawable; 12 13 public DotIndicatorView(Context context) { 14 this(context, null); 15 } 16 17 public DotIndicatorView(Context context, AttributeSet attrs) { 18 this(context, attrs, 0); 19 } 20 21 public DotIndicatorView(Context context, AttributeSet attrs, int defStyleAttr) { 22 super(context, attrs, defStyleAttr); 23 } 24 25 @Override 26 protected void onDraw(Canvas canvas) { 27 if(drawable != null){ 28 /*drawable.setBounds(0,0,getMeasuredWidth(),getMeasuredHeight()); 29 drawable.draw(canvas);*/ 30 // 7.把指示器变成圆形 31 // 画圆 32 Bitmap bitmap = drawableToBitmap(drawable); 33 34 // 把Bitmap变为圆的 35 Bitmap circleBitmap = getCircleBitmap(bitmap); 36 37 // 把圆形的Bitmap绘制到画布上 38 canvas.drawBitmap(circleBitmap,0,0,null); 39 } 40 } 41 42 /** 43 * 7.获取圆形bitmap 44 */ 45 private Bitmap getCircleBitmap(Bitmap bitmap) { 46 // 创建一个Bitmap 47 Bitmap circleBitmap = Bitmap.createBitmap(getMeasuredWidth(), getMeasuredHeight(), Bitmap.Config.ARGB_8888); 48 Canvas canvas = new Canvas(circleBitmap); 49 50 Paint paint = new Paint(); 51 // 设置抗锯齿 52 paint.setAntiAlias(true); 53 paint.setFilterBitmap(true); 54 // 设置仿抖动 55 paint.setDither(true); 56 57 // 在画布上面画个圆 58 canvas.drawCircle(getMeasuredWidth()/2,getMeasuredHeight()/2,getMeasuredWidth()/2,paint); 59 60 // 取圆和Bitmap矩形的交集 61 paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); 62 // 再把原来的Bitmap绘制到新的圆上面 63 canvas.drawBitmap(bitmap,0,0,paint); 64 65 return circleBitmap; 66 } 67 68 /** 69 * 7.从drawable中得到Bitmap 70 * @param drawable 71 * @return 72 */ 73 private Bitmap drawableToBitmap(Drawable drawable) { 74 // 如果是BitmapDrawable类型 75 if(drawable instanceof BitmapDrawable){ 76 return((BitmapDrawable)drawable).getBitmap(); 77 } 78 79 // 其他类型 ColorDrawable 80 // 创建一个什么也没有的bitmap 81 Bitmap outBitmap = Bitmap.createBitmap(getMeasuredWidth(),getMeasuredHeight(), Bitmap.Config.ARGB_8888); 82 // 创建一个画布 83 Canvas canvas = new Canvas(outBitmap); 84 85 // 把drawable化到Bitmap上 86 drawable.setBounds(0,0,getMeasuredWidth(),getMeasuredHeight()); 87 drawable.draw(canvas); 88 89 return outBitmap; 90 } 91 92 /** 93 * 5.设置Drawable 94 */ 95 public void setDrawable(Drawable drawable) { 96 this.drawable = drawable; 97 // 重新绘制View 98 invalidate(); 99 } 100}
2.8. 设置自定义属性
1 /** 2 * 8.初始化自定义属性 3 */ 4 private void initAttribute(AttributeSet attrs) { 5 TypedArray array = mContext.obtainStyledAttributes(attrs, R.styleable.BannerView); 6 7 // 获取点的位置 8 mDotGravity = array.getInt(R.styleable.BannerView_dotGravity, mDotGravity); 9 // 获取点的颜色(默认、选中) 10 mIndicatorFocusDrawable = array.getDrawable(R.styleable.BannerView_dotIndicatorFocus); 11 if(mIndicatorFocusDrawable == null){ 12 // 如果在布局文件中没有配置点的颜色 有一个默认值 13 mIndicatorFocusDrawable = new ColorDrawable(Color.RED); 14 } 15 mIndicatorNormalDrawable = array.getDrawable(R.styleable.BannerView_dotIndicatorNormal); 16 if(mIndicatorNormalDrawable == null){ 17 // 如果在布局文件中没有配置点的颜色 有一个默认值 18 mIndicatorNormalDrawable = new ColorDrawable(Color.WHITE); 19 } 20 // 获取点的大小和距离 21 mDotSize = (int) array.getDimension(R.styleable.BannerView_dotSize,dip2px(mDotSize)); 22 mDotDistance = (int) array.getDimension(R.styleable.BannerView_dotDistance,dip2px(mDotDistance)); 23 array.recycle(); 24 }
2.9. 自适应高度
1 // 8.自适应高度 动态指定高度 2 if(mHeightProportion == 0 || mWidthProportion == 0){ 3 return; 4 } 5 // 动态指定宽高 计算高度 6 int width = getMeasuredWidth(); 7 // 计算高度 8 int height = (int) (width*mHeightProportion/mWidthProportion); 9 // 指定宽高 10 getLayoutParams().height = height;
2.10. 内存优化
写完之后,可以了就大功告成但是这个时候我们要去优化,还不好用不?容易扩展不?内存优化好没?在这里就不多写了,如回收Bitmap,界面复用,管理Activity生命周期等等,一切都在视频里面。

这里写图片描述
如果实在还是看不太懂,可以看一下我录的频,可以看一下整个系统架构也可以了解一下整个项目的其他东西:http://pan.baidu.com/s/1skOdHzn。
