美文网首页安卓进阶AndroidAndroid开发
Android无限广告轮播 - 自定义BannerView

Android无限广告轮播 - 自定义BannerView

作者: 红橙Darren | 来源:发表于2017-01-12 16:26 被阅读2083次

    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);

    /**
     * description:
     *      广告轮播的ViewPager
     * Created by 曾辉 on 2016/11/17.
     * QQ:240336124
     * Email: 240336124@qq.com
     * Version:1.0
     */
    public class BannerViewPager extends ViewPager {
    
        private Context mContext;
    
        private BannerAdapter mAdapter;
        
        public BannerViewPager(Context context) {
            this(context, null);
        }
    
        public BannerViewPager(Context context, AttributeSet attrs) {
            super(context, attrs);
            this.mContext = context;
        }
    
        public void setAdapter(BannerAdapter adapter) {
            this.mAdapter = adapter;
            setAdapter(new BannerPagerAdapter());
        }
    
        private class BannerPagerAdapter extends PagerAdapter {
    
            @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, final int position) {
                View bannerView = mAdapter.getView(position);
                container.addView(bannerView );
                return bannerView;
            }
    
            @Override
            public void destroyItem(ViewGroup container, int position, Object object) {
                // 销毁回调的方法  移除页面即可
                container.removeView((View) object);
            }
        }
    }
    

    这样我们只要给他设置一个BannerAdapter就可以实现ViewPager的效果,可以手动切换,这里就先不看效果。
      
      
    2.2. 实现自动轮播
      实现自动轮播比较简单,实现的方式有多种可以用定时器Timer、Handler发送消息、start Thread的行,这里我采用Handler发送消息的方法。

        // 2.实现自动轮播 - 发送消息的msgWhat
        private final int SCROLL_MSG = 0x0011;
    
        // 2.实现自动轮播 - 页面切换间隔时间
        private int mCutDownTime = 3500;
    
        // 2.实现自动轮播 - 发送消息Handler
        private Handler mHandler = new Handler(){
            @Override
            public void handleMessage(Message msg) {
                // 每隔*s后切换到下一页
                setCurrentItem(getCurrentItem() + 1);
                // 不断循环执行
                startRoll();
            }
        };
        
        /**
         * 2.实现自动轮播
         */
        public void startRoll(){
            // 清除消息
            mHandler.removeMessages(SCROLL_MSG);
            // 消息  延迟时间  让用户自定义  有一个默认  3500
            mHandler.sendEmptyMessageDelayed(SCROLL_MSG,mCutDownTime);
            Log.e(TAG,"startRoll");
        }
    
        /**
         * 2.销毁Handler停止发送  解决内存泄漏
         */
        @Override
        protected void onDetachedFromWindow() {
            super.onDetachedFromWindow();
            mHandler.removeMessages(SCROLL_MSG);
            mHandler = null;
        }
    

    我们看一下效果吧,但是发现Gif录制根本捕捉不到切换的效果,因为自动切换速度太快了,这里还是不贴效果了,下面我就需要改变切换的速度。
      
    2.3. 改变切换速率

    如果看过上篇文章的源码就知道,我们会调用Scroller的mScroller.startScroll(sx, sy, dx, dy, duration)的这个方法,如果我们需要改变速率就只能改变duration执行切换页面动画的时间,可是我们根本拿不到这个值,那么就只能修改mScroller这个属性,可又发现他是private的有点头大,但是我们可以利用反射设置mScroller;

        
        // 3.改变ViewPager切换的速率 - 自定义的页面切换的Scroller
        private BannerScroller mScroller;
    
        public BannerViewPager(Context context, AttributeSet attrs) {
            super(context, attrs);
    
            try {
                // 3.改变ViewPager切换的速率
                // 3.1 duration 持续的时间  局部变量
                // 3.2.改变 mScroller private 通过反射设置
                Field field = ViewPager.class.getDeclaredField("mScroller");
                // 设置参数  第一个object当前属性在哪个类  第二个参数代表要设置的值
                mScroller = new BannerScroller(context);
                // 设置为强制改变private
                field.setAccessible(true);
                field.set(this,mScroller);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        /**
         * 3.设置切换页面动画持续的时间
         */
        public void setScrollerDuration(int scrollerDuration){
            mScroller.setScrollerDuration(scrollerDuration);
        }
    

    现在效果差不多了,可以看到能够无限轮播,能够自动轮播,并且页面切换的速度也可以了,接下来就只需要处理点的指示器和文字的描述:
      

    这里写图片描述
     
    2.4. 自定义BannerView加入点指示和广告描述
      接下来我们又自定义一个BannerView里面包含当前自定义好的BannerViewPager和点的指示LinearLayout以及广告描述TextView。
    package com.example.hui.androidtemplate.banner;
    
    import android.content.Context;
    import android.util.AttributeSet;
    import android.widget.LinearLayout;
    import android.widget.RelativeLayout;
    import android.widget.TextView;
    
    import com.example.hui.androidtemplate.R;
    
    /**
     * description:
     * <p/>
     * Created by 曾辉 on 2016/11/18.
     * QQ:240336124
     * Email: 240336124@qq.com
     * Version:1.0
     */
    public class BannerView extends RelativeLayout{
        // 4.自定义BannerView - 轮播的ViewPager
        private BannerViewPager mBannerVp;
        // 4.自定义BannerView - 轮播的描述
        private TextView mBannerDescTv;
        // 4.自定义BannerView - 点的容器
        private LinearLayout mDotContainerView;
        // 4.自定义BannerView - 自定义的BannerAdapter
        private BannerAdapter mAdapter;
    
        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);
    
            // 把布局加载到这个View里面
            inflate(context, R.layout.ui_banner_layout,this);
    
            initView();
        }
    
        /**
         * 初始化View
         */
        private void initView() {
            mBannerVp = (BannerViewPager) findViewById(R.id.banner_vp);
            mBannerDescTv = (TextView) findViewById(R.id.banner_desc_tv);
            mDotContainerView = (LinearLayout) findViewById(R.id.dot_container);
        }
    
        /**
         * 4.设置适配器
         */
        public void setAdapter(BannerAdapter adapter){
            mBannerVp.setAdapter(adapter);
        }
    
        /**
         * 4.开始滚动
         */
        public void startRoll() {
            mBannerVp.startRoll();
        }
    }
    
    

    2.5. 初始化点的指示器

        /**
         * 5.初始化点的指示器
         */
        private void initDotIndicator() {
            // 获取广告的数量
            int count = mAdapter.getCount();
    
            // 让点的位置在右边
            mDotContainerView.setGravity(Gravity.RIGHT);
    
            for (int i = 0;i<count;i++){
                // 不断的往点的指示器添加圆点
                DotIndicatorView indicatorView = new DotIndicatorView(mContext);
                // 设置大小
                LinearLayout.LayoutParams params = new 
                    LinearLayout.LayoutParams(dip2px(8),dip2px(8));
                // 设置左右间距
                params.leftMargin = params.rightMargin = dip2px(2);
                indicatorView.setLayoutParams(params);
    
                if(i == 0) {
                    // 选中位置
                    indicatorView.setDrawable(mIndicatorFocusDrawable);
                }else{
                    // 未选中的
                    indicatorView.setDrawable(mIndicatorNormalDrawable);
                }
                mDotContainerView.addView(indicatorView);
            }
        }
    
        /**
         * 5.把dip转成px
         */
        private int dip2px(int dip) {
            return (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
                    dip,getResources().getDisplayMetrics());
        }
    

    2.6. 阶段性的Bug修复

         /**
         * 4.设置适配器
         */
        public void setAdapter(BannerAdapter adapter){
            mAdapter = adapter;
            mBannerVp.setAdapter(adapter);
            // 5.初始化点的指示器
            initDotIndicator();
    
            // 6.Bug修复
            mBannerVp.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener(){
                @Override
                public void onPageSelected(int position) {
                    // 监听当前选中的位置
                    pageSelect(position);
                }
            });
    
            // 6.初始化的时候获取第一条的描述
            String firstDesc = mAdapter.getBannerDesc(0);
            mBannerDescTv.setText(firstDesc);
        }
    
    
        /**
         * 6.页面切换的回调
         * @param position
         */
        private void pageSelect(int position) {
            // 6.1 把之前亮着的点 设置为默认
            DotIndicatorView oldIndicatorView = (DotIndicatorView)
                    mDotContainerView.getChildAt(mCurrentPosition);
            oldIndicatorView.setDrawable(mIndicatorNormalDrawable);
    
    
            // 6.2 把当前位置的点 点亮  position 0 --> 2的31次方
            mCurrentPosition = position%mAdapter.getCount();
            DotIndicatorView currentIndicatorView = (DotIndicatorView)
                    mDotContainerView.getChildAt(mCurrentPosition);
            currentIndicatorView.setDrawable(mIndicatorFocusDrawable);
    
            // 6.3设置广告描述
            String bannerDesc = mAdapter.getBannerDesc(mCurrentPosition);
            mBannerDescTv.setText(bannerDesc);
        }
    

    2.7. 把指示器的点绘制成圆

    /**
     * description:  圆的指示器
     *    圆点指示器
     * Created by 曾辉 on 2016/11/18.
     * QQ:240336124
     * Email: 240336124@qq.com
     * Version:1.0
     */
    public class DotIndicatorView extends View {
    
        private Drawable drawable;
    
        public DotIndicatorView(Context context) {
            this(context, null);
        }
    
        public DotIndicatorView(Context context, AttributeSet attrs) {
            this(context, attrs, 0);
        }
    
        public DotIndicatorView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            if(drawable != null){
                /*drawable.setBounds(0,0,getMeasuredWidth(),getMeasuredHeight());
                drawable.draw(canvas);*/
                // 7.把指示器变成圆形
                // 画圆
                Bitmap bitmap = drawableToBitmap(drawable);
    
                // 把Bitmap变为圆的
                Bitmap circleBitmap = getCircleBitmap(bitmap);
    
                // 把圆形的Bitmap绘制到画布上
                canvas.drawBitmap(circleBitmap,0,0,null);
            }
        }
    
        /**
         * 7.获取圆形bitmap
         */
        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);
    
            // 取圆和Bitmap矩形的交集
            paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
            // 再把原来的Bitmap绘制到新的圆上面
            canvas.drawBitmap(bitmap,0,0,paint);
    
            return circleBitmap;
        }
    
        /**
         * 7.从drawable中得到Bitmap
         * @param drawable
         * @return
         */
        private Bitmap drawableToBitmap(Drawable drawable) {
            // 如果是BitmapDrawable类型
            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.setBounds(0,0,getMeasuredWidth(),getMeasuredHeight());
            drawable.draw(canvas);
    
            return outBitmap;
        }
    
        /**
         * 5.设置Drawable
         */
        public void setDrawable(Drawable drawable) {
            this.drawable = drawable;
            // 重新绘制View
            invalidate();
        }
    }
    
    

    2.8. 设置自定义属性

        /**
         * 8.初始化自定义属性
         */
        private void initAttribute(AttributeSet attrs) {
            TypedArray array = mContext.obtainStyledAttributes(attrs, R.styleable.BannerView);
    
            // 获取点的位置
            mDotGravity = array.getInt(R.styleable.BannerView_dotGravity, mDotGravity);
            // 获取点的颜色(默认、选中)
            mIndicatorFocusDrawable = array.getDrawable(R.styleable.BannerView_dotIndicatorFocus);
            if(mIndicatorFocusDrawable == null){
                // 如果在布局文件中没有配置点的颜色  有一个默认值
                mIndicatorFocusDrawable = new ColorDrawable(Color.RED);
            }
            mIndicatorNormalDrawable = array.getDrawable(R.styleable.BannerView_dotIndicatorNormal);
            if(mIndicatorNormalDrawable == null){
                // 如果在布局文件中没有配置点的颜色  有一个默认值
                mIndicatorNormalDrawable = new ColorDrawable(Color.WHITE);
            }
            // 获取点的大小和距离
            mDotSize = (int) array.getDimension(R.styleable.BannerView_dotSize,dip2px(mDotSize));
            mDotDistance = (int) array.getDimension(R.styleable.BannerView_dotDistance,dip2px(mDotDistance));
            array.recycle();
        }
    

    2.9. 自适应高度

        // 8.自适应高度 动态指定高度
        if(mHeightProportion == 0 || mWidthProportion == 0){
             return;
        }
        // 动态指定宽高  计算高度
        int width = getMeasuredWidth();
        // 计算高度
        int height = (int) (width*mHeightProportion/mWidthProportion);
        // 指定宽高
        getLayoutParams().height = height;
    

    2.10. 内存优化
      写完之后,可以了就大功告成但是这个时候我们要去优化,还不好用不?容易扩展不?内存优化好没?在这里就不多写了,如回收Bitmap,界面复用,管理Activity生命周期等等,一切都在视频里面。

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

    相关文章

      网友评论

      • 任振铭:楼主,我把这个BannerView和你之前的上拉刷新下拉加载的Recyclerview结合在一起用的时候,发现在页面初次启动时轮播条开始轮播的第一次滑动的时候 ,以及下滑列表轮播条隐藏然后在滑倒上边让轮播条显示再次启动轮播的时候,轮播条切换的动画效果没了,然后再次轮播切换的时候就有了动画效果,很匪夷所思
        红橙Darren:@雨林沐风_09cd 我这些代码bug多得很,有了视频,就没改了
        任振铭:@雨林沐风_09cd 原来这不是楼主的bug,原生的recyclerview和viewpager就有这种问题,好在我已经解决了
        红橙Darren:@雨林沐风_09cd 应该是生命周期那里没处理好
      • a6c7667b5f08:好人一生平安
      • 0483aff081d9:视频少了一节...第11的视频没了
        红橙Darren: @花歹 我找找
      • android0226:支持楼主的文章 希望楼主越来越好
        红橙Darren:@android0226 都行只要考虑了界面复用,一般替换的效果如果快速连续滑动到最后一张可能会造成不连贯
        android0226:楼主你好,现在市面上关于无限轮播的有两种做法 一种是getCount()返回一个最大值,还有一种是灵活的采用替换 请问这两种有什么优劣没
        红橙Darren:@android0226 谢谢大兄弟,希望我们一起越来越好:smile: :smile:

      本文标题:Android无限广告轮播 - 自定义BannerView

      本文链接:https://www.haomeiwen.com/subject/ydzdbttx.html