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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,458评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,454评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,171评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,062评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,440评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,661评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,906评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,609评论 0 200
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,379评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,600评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,085评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,409评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,072评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,088评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,860评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,704评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,608评论 2 270

推荐阅读更多精彩内容