Android無限廣告輪播 - 自定義BannerView

1.概述


這其實是我第一篇想寫的博客,可能是因為我遇到了太多的坑灭贷,那個時候剛入行下了很多Demo發(fā)現(xiàn)怎么也改不動略贮,可能是能力有限逃延,這次就做一個具體的實現(xiàn)和徹底的封裝。
  上次講了Android無限廣告輪播-ViewPager源碼分析讽膏,有了源碼分析我們對ViewPager就有了一個大概的了解拄丰,那么再來封裝成自定義View料按,就會簡單許多,附視頻講解地址:http://pan.baidu.com/s/1skOdHzn
  
  

這里寫圖片描述

2.效果封裝


2.1 自定義BannerViewPager extends ViewPager:
  我們要利用Adapter設計模式,那么目前這個階段,需要的方法就是根據(jù)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就可以實現(xiàn)ViewPager的效果迫皱,可以手動切換,這里就先不看效果和敬。
  
  
2.2. 實現(xiàn)自動輪播
  實現(xiàn)自動輪播比較簡單戏阅,實現(xiàn)的方式有多種可以用定時器Timer奕筐、Handler發(fā)送消息、start Thread的行芭逝,這里我采用Handler發(fā)送消息的方法旬盯。

    // 2.實現(xiàn)自動輪播 - 發(fā)送消息的msgWhat
    private final int SCROLL_MSG = 0x0011;

    // 2.實現(xiàn)自動輪播 - 頁面切換間隔時間
    private int mCutDownTime = 3500;

    // 2.實現(xiàn)自動輪播 - 發(fā)送消息Handler
    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            // 每隔*s后切換到下一頁
            setCurrentItem(getCurrentItem() + 1);
            // 不斷循環(huán)執(zhí)行
            startRoll();
        }
    };
    
    /**
     * 2.實現(xiàn)自動輪播
     */
    public void startRoll(){
        // 清除消息
        mHandler.removeMessages(SCROLL_MSG);
        // 消息  延遲時間  讓用戶自定義  有一個默認  3500
        mHandler.sendEmptyMessageDelayed(SCROLL_MSG,mCutDownTime);
        Log.e(TAG,"startRoll");
    }

    /**
     * 2.銷毀Handler停止發(fā)送  解決內存泄漏
     */
    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        mHandler.removeMessages(SCROLL_MSG);
        mHandler = null;
    }

我們看一下效果吧瓢捉,但是發(fā)現(xiàn)Gif錄制根本捕捉不到切換的效果办成,因為自動切換速度太快了搂漠,這里還是不貼效果了,下面我就需要改變切換的速度靶壮。
  
2.3. 改變切換速率

如果看過上篇文章的源碼就知道员萍,我們會調用Scroller的mScroller.startScroll(sx, sy, dx, dy, duration)的這個方法碎绎,如果我們需要改變速率就只能改變duration執(zhí)行切換頁面動畫的時間,可是我們根本拿不到這個值奸晴,那么就只能修改mScroller這個屬性日麸,可又發(fā)現(xiàn)他是private的有點頭大代箭,但是我們可以利用反射設置mScroller;

    
    // 3.改變ViewPager切換的速率 - 自定義的頁面切換的Scroller
    private BannerScroller mScroller;

    public BannerViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);

        try {
            // 3.改變ViewPager切換的速率
            // 3.1 duration 持續(xù)的時間  局部變量
            // 3.2.改變 mScroller private 通過反射設置
            Field field = ViewPager.class.getDeclaredField("mScroller");
            // 設置參數(shù)  第一個object當前屬性在哪個類  第二個參數(shù)代表要設置的值
            mScroller = new BannerScroller(context);
            // 設置為強制改變private
            field.setAccessible(true);
            field.set(this,mScroller);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 3.設置切換頁面動畫持續(xù)的時間
     */
    public void setScrollerDuration(int scrollerDuration){
        mScroller.setScrollerDuration(scrollerDuration);
    }

現(xiàn)在效果差不多了嗡综,可以看到能夠無限輪播,能夠自動輪播蚣旱,并且頁面切換的速度也可以了戴陡,接下來就只需要處理點的指示器和文字的描述:
  

這里寫圖片描述

 
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() {
        // 獲取廣告的數(shù)量
        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) {
                // 監(jiān)聽當前選中的位置
                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變?yōu)閳A的
            Bitmap circleBitmap = getCircleBitmap(bitmap);

            // 把圓形的Bitmap繪制到畫布上
            canvas.drawBitmap(circleBitmap,0,0,null);
        }
    }

    /**
     * 7.獲取圓形bitmap
     */
    private Bitmap getCircleBitmap(Bitmap bitmap) {
        // 創(chuàng)建一個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
        // 創(chuàng)建一個什么也沒有的bitmap
        Bitmap outBitmap = Bitmap.createBitmap(getMeasuredWidth(),getMeasuredHeight(), Bitmap.Config.ARGB_8888);
        // 創(chuàng)建一個畫布
        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.自適應高度 動態(tài)指定高度
    if(mHeightProportion == 0 || mWidthProportion == 0){
         return;
    }
    // 動態(tài)指定寬高  計算高度
    int width = getMeasuredWidth();
    // 計算高度
    int height = (int) (width*mHeightProportion/mWidthProportion);
    // 指定寬高
    getLayoutParams().height = height;

2.10. 內存優(yōu)化
  寫完之后求摇,可以了就大功告成但是這個時候我們要去優(yōu)化与境,還不好用不?容易擴展不挥转?內存優(yōu)化好沒共屈?在這里就不多寫了,如回收Bitmap域仇,界面復用寺擂,管理Activity生命周期等等怔软,一切都在視頻里面。

這里寫圖片描述

  
  如果實在還是看不太懂括改,可以看一下我錄的頻嘱能,可以看一下整個系統(tǒng)架構也可以了解一下整個項目的其他東西:http://pan.baidu.com/s/1skOdHzn虱疏。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末做瞪,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子著拭,更是在濱河造成了極大的恐慌牍帚,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,657評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件峦萎,死亡現(xiàn)場離奇詭異爱榔,居然都是意外死亡糙及,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評論 3 394
  • 文/潘曉璐 我一進店門唇聘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來迟郎,“玉大人聪蘸,你說我怎么就攤上這事】厍” “怎么了娜遵?”我有些...
    開封第一講書人閱讀 164,057評論 0 354
  • 文/不壞的土叔 我叫張陵设拟,是天一觀的道長。 經(jīng)常有香客問我镰吆,道長躲雅,這世上最難降的妖魔是什么相赁? 我笑而不...
    開封第一講書人閱讀 58,509評論 1 293
  • 正文 為了忘掉前任钮科,我火速辦了婚禮,結果婚禮上绵脯,老公的妹妹穿的比我還像新娘。我一直安慰自己赃承,他們只是感情好瞧剖,可當我...
    茶點故事閱讀 67,562評論 6 392
  • 文/花漫 我一把揭開白布抓于。 她就那樣靜靜地躺著,像睡著了一般怕品。 火紅的嫁衣襯著肌膚如雪巾遭。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,443評論 1 302
  • 那天迎罗,我揣著相機與錄音纹安,去河邊找鬼砂豌。 笑死,一個胖子當著我的面吹牛塔粒,可吹牛的內容都是我干的筐摘。 我是一名探鬼主播,決...
    沈念sama閱讀 40,251評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼馍管!你這毒婦竟也來了?” 一聲冷哼從身側響起捌锭,我...
    開封第一講書人閱讀 39,129評論 0 276
  • 序言:老撾萬榮一對情侶失蹤观谦,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后盾剩,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體替蔬,經(jīng)...
    沈念sama閱讀 45,561評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡承桥,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,779評論 3 335
  • 正文 我和宋清朗相戀三年凶异,在試婚紗的時候發(fā)現(xiàn)自己被綠了挤巡。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,902評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖母廷,靈堂內的尸體忽然破棺而出琴昆,到底是詐尸還是另有隱情,我是刑警寧澤抖拦,帶...
    沈念sama閱讀 35,621評論 5 345
  • 正文 年R本政府宣布舷暮,位于F島的核電站脚牍,受9級特大地震影響,放射性物質發(fā)生泄漏券膀。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,220評論 3 328
  • 文/蒙蒙 一蓄髓、第九天 我趴在偏房一處隱蔽的房頂上張望舒帮。 院中可真熱鬧,春花似錦玩郊、人聲如沸译红。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽刨沦。三九已至想诅,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間裁眯,已是汗流浹背讳癌。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評論 1 269
  • 我被黑心中介騙來泰國打工晌坤, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人骤菠。 一個月前我還...
    沈念sama閱讀 48,025評論 2 370
  • 正文 我出身青樓商乎,卻偏偏與公主長得像,于是被迫代替她去往敵國和親鲜戒。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,843評論 2 354

推薦閱讀更多精彩內容