Android自定義帶動(dòng)畫無限自動(dòng)輪播的Banner控件

顯示的效果請(qǐng)往下看,先說一下需求,可以自動(dòng)輪播,按下停止輪播,松手開始輪播,不可見時(shí)停止輪播,自動(dòng)輪播時(shí)帶動(dòng)畫,手動(dòng)滑動(dòng)時(shí)不帶動(dòng)畫,點(diǎn)擊時(shí)要有水波紋效果,下拉刷新回到第一頁,頁面不能卡頓等等。參考了一些網(wǎng)上的想法,結(jié)合自己的認(rèn)知,總結(jié)如下。


自定義BannerView控件

為了以后開發(fā)的方便,這里將Banner封裝成一個(gè)控件來使用,以后就可以直接在布局里引用。

做這種輪播效果一般采用的都是ViewPager,所以這個(gè)控件也是對(duì)ViewPager的封裝,為了解耦,這里沒有把適配器放在BannerView,只是提供了基類適配器和接口,使用很方便。

講一下具體如何實(shí)現(xiàn)封裝:
第一步:自定義控件,實(shí)現(xiàn)構(gòu)造,初始化屬性和視圖。

  • 基于需求,先定義了頁面邊距,主頁面占比,縮放比例,輪播時(shí)長(zhǎng),是否動(dòng)畫輪播等等屬性,通過引用attrs的方式,在布局里賦值,并且提供set方法在代碼里設(shè)置。
   private void initAttrs(Context context, AttributeSet attrs) {
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.BannerView);
        pageMargin = (int) a.getDimension(R.styleable.BannerView_bannerPageMargin, pageMargin);
        pagePercent = a.getFloat(R.styleable.BannerView_bannerPagePercent, pagePercent);
        scaleMin = a.getFloat(R.styleable.BannerView_bannerPageScale, scaleMin);
        alphaMin = a.getFloat(R.styleable.BannerView_bannerPageAlpha, alphaMin);
        ...
        a.recycle();
    }
  • 定義了基本屬性后,將ViewPager視圖填充到控件容器里,ViewPager布局里需要使用clipChildren屬性來控制系統(tǒng)繪制View的范圍,在ViewPager和它的父容器里都設(shè)置clipChildren=false。這樣指定了主頁面占比后,左右兩邊的Page就會(huì)進(jìn)行繪制。
    private void initView() {
        mRootView = LayoutInflater.from(getContext()).inflate(R.layout.banner_view, this);
        mViewPager = (ViewPager) mRootView.findViewById(R.id.viewPager);
        LayoutParams params = (LayoutParams) mViewPager.getLayoutParams();
        params.width = (int) (getScreenWidth() * pagePercent);
        params.gravity = Gravity.CENTER;
        mViewPager.setLayoutParams(params);
        mViewPager.setPageMargin(pageMargin);
        mViewPager.setPageTransformer(false, new BannerPageTransformer());
        mViewPager.setOffscreenPageLimit(5);
        // 自動(dòng)輪播任務(wù)
        mScrollTask = new AutoScrollTask();
        // 如果動(dòng)畫輪播
        if (isAnimScroll) {
            setAnimationScroll((int) mAnimDuration);
        }
    }

第二步:滑動(dòng)動(dòng)畫實(shí)現(xiàn)

  • 眾所周知,谷歌已經(jīng)提供了ViewPager的滑動(dòng)動(dòng)畫設(shè)置接口setPageTransformer(),并且他自己也實(shí)現(xiàn)了三種基本的滑動(dòng)滑動(dòng)。需要自定義動(dòng)畫時(shí)只需實(shí)現(xiàn)ViewPager.PageTransformer接口,并實(shí)現(xiàn)transformPage()方法。

  • 設(shè)計(jì)稿中的動(dòng)畫要求是,滑動(dòng)時(shí)左側(cè)縮小,主頁隨著滑動(dòng)百分比縮小,右側(cè)隨著滑動(dòng)百分比放大。transformPage方法中提供了主頁所在的position=0.0,左側(cè)位置百分比position<0,右側(cè)位置百分比position>0,通過這個(gè)position可以動(dòng)態(tài)計(jì)算出各個(gè)頁面的縮放比。

    public void transformPage(View page, float position) {
       // 不同位置的縮放和透明度
       float scale = (position < 0)
                ? ((1 - scaleMin) * position + 1)
                : ((scaleMin - 1) * position + 1);
       float alpha = (position < 0)
                ? ((1 - alphaMin) * position + 1)
                : ((alphaMin - 1) * position + 1);
       // 保持左右兩邊的圖片位置中心
       if (position < 0) {
            ViewCompat.setPivotX(page, page.getWidth());
            ViewCompat.setPivotY(page, page.getHeight() / 2);
        } else {
            ViewCompat.setPivotX(page, 0);
            ViewCompat.setPivotY(page, page.getHeight() / 2);
        }
        Log.d(TAG, "transformPage: scale=" + scale);
        ViewCompat.setScaleX(page, scale);
        ViewCompat.setScaleY(page, scale);
        ViewCompat.setAlpha(page, Math.abs(alpha));
    }

第三步:無限自動(dòng)輪播和輪播動(dòng)畫處理

  • 無限輪播可用線程池,定時(shí)器,Handler等等實(shí)現(xiàn),這里采用最簡(jiǎn)單的Handler實(shí)現(xiàn)。首先自定義一個(gè)輪播任務(wù),實(shí)現(xiàn)Runnable接口,在run方法里使用Handler發(fā)送延時(shí)消息來不停輪播,并且提供start方法和stop方法開啟和停止輪播。在初始化視圖完畢時(shí),開啟輪播。
   @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        // 視圖初始化完畢,開始輪播任務(wù)
        if (mScrollTask == null) mScrollTask = new AutoScrollTask();
        if (isAutoScroll) startAutoScroll();
    }
  • ViewPager滑動(dòng)是沒有延時(shí)的,谷歌也沒有提供具體的接口去實(shí)現(xiàn)延時(shí)滑動(dòng)。所以這里使用反射去重新設(shè)置ViewPager的滑動(dòng)世間。自動(dòng)輪播時(shí),需求動(dòng)畫延時(shí)滑動(dòng),但是手動(dòng)滑動(dòng)時(shí)需要原生的滑動(dòng),所以根據(jù)使用時(shí)間差和滑動(dòng)時(shí)間來控制自動(dòng)和手動(dòng)滑動(dòng)。
 public void startScroll(int startX, int startY, int dx,
                                        int dy, int duration) {
      // 如果手動(dòng)滾動(dòng),則加速滾動(dòng)
         // TODO 使用這種設(shè)置極不穩(wěn)定,需要抽離
         if (System.currentTimeMillis() - mRecentTouchTime > mScrollDuration && isAnimScroll) {
             // 動(dòng)畫滑動(dòng)
             duration = during;
         } else {
             // 手勢(shì)滾動(dòng)
             duration /= 2;

         }
         super.startScroll(startX, startY, dx, dy, duration);
     }

BannerView的基類適配器封裝

  • BannerView封裝的是ViewPager,所以基類適配器BannerBaseAdapter封裝的也是PagerAdapter,由于適配器涉及到數(shù)據(jù)和視圖,所以基類里將所有都封裝好,只留數(shù)據(jù)和視圖留給子類去實(shí)現(xiàn)。除此之外,頁面的點(diǎn)擊,按下和抬起也在適配器實(shí)現(xiàn)并通過接口暴露出來。
  • 實(shí)現(xiàn)父類適配后,只需要指定數(shù)據(jù)類型和實(shí)現(xiàn)展示視圖轉(zhuǎn)換數(shù)據(jù)的方法,加載完數(shù)據(jù)之后,通過setData方法來重新更新數(shù)據(jù)即可。
private class BannerAdapter extends BannerBaseAdapter<BannerBean> {
        public BannerAdapter(Context context) {
            super(context);
        }
        @Override
        protected int getLayoutResID() {
            return R.layout.item_banner;
        }
        @Override
        protected void convert(View convertView, BannerBean data) {
            setImage(R.id.pageImage, data.imageRes);
            setText(R.id.pageText, data.title);
        }
  }

使用教程

  • 拷貝BannerView全路徑到布局里,使用attrs指定屬性
 <com.pinger.widget.banner.BannerView
      android:id="@+id/bannerView"
      android:layout_width="match_parent"
      android:layout_height="200dp"
      app:bannerPageAlpha="1.0"
      app:bannerPageMargin="8dp"
      app:bannerPagePercent="0.8"
      app:bannerPageScale="0.8"
      app:bannerAnimScroll="true"
      app:bannerAutoScroll="true"
      app:bannerScrollDuration="4000"
      app:bannerAnimDuration="1500"/>
  • 在代碼中設(shè)置適配器和設(shè)置頁面的觸摸監(jiān)聽,初始化數(shù)據(jù)之后,更新數(shù)據(jù)
  final BannerView bannerView = (BannerView) findViewById(R.id.bannerView);
  bannerView.setAdapter(mAdapter = new BannerAdapter(this));
  initData();  // 初始化數(shù)據(jù)
  mAdapter.setData(mDatas);
  mAdapter.setOnPageTouchListener(...);

Banner相關(guān)請(qǐng)查看最新開源項(xiàng)目BannerView

歡迎大家訪問我的簡(jiǎn)書,博客GitHub。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,323評(píng)論 25 708
  • 內(nèi)容抽屜菜單ListViewWebViewSwitchButton按鈕點(diǎn)贊按鈕進(jìn)度條TabLayout圖標(biāo)下拉刷新...
    皇小弟閱讀 47,176評(píng)論 22 665
  • 前言 目前市場(chǎng)上的APP中,輪播圖可以說是很常見的。一個(gè)好的輪播圖,基本上適用于所有的APP。是時(shí)候打造一個(gè)自己的...
    帶心情去旅行閱讀 17,603評(píng)論 15 93
  • 來到這個(gè)我度過整個(gè)青春的地方,那一抹一年中我最愛的嫩綠蔓延開來。每年最愛這個(gè)時(shí)候,沒有綠色甚濃的滿溢,沒有...
    tudou619閱讀 754評(píng)論 1 1
  • 清晨,照例在爸爸的“喊”聲中醒來。我睜開睡眼惺忪的雙眼,打著未睡足的哈欠,開始穿衣服。廚房的抽油煙機(jī)發(fā)出響亮的呼呼...
    清風(fēng)一隅閱讀 401評(píng)論 3 1

友情鏈接更多精彩內(nèi)容