ViewPager系列之 仿魅族應(yīng)用的廣告BannerView

前言

Banner廣告位是APP 中的一個(gè)非常重要的位置,為什么呢?因?yàn)樗軒?lái)money。是一個(gè)公司很重要的一個(gè)營(yíng)收點(diǎn)。像那種用戶數(shù)基數(shù)特別大的產(chǎn)品,如facebook、twitter、QQ、微信等等。Banner廣告位日營(yíng)收估計(jì)得上千萬(wàn)美刀(猜的,不知道具體數(shù)據(jù))。一個(gè)漂亮的Banner往往能夠吸引用戶的眼球,引導(dǎo)用戶點(diǎn)擊,從而提高轉(zhuǎn)化率。遺憾的是現(xiàn)在的大多數(shù)產(chǎn)品的Banner都是千篇一律的,沒(méi)有什么亮點(diǎn)可言。但是前幾天在魅族手機(jī)上發(fā)現(xiàn)了一個(gè)效果不錯(cuò)的Banner,魅族所有自家的APP所用的Banner 引起了我的注意。效果是這樣子的:

meizuapp.gif

看到這個(gè)Banner 第一眼就吸引了我,隨后就反復(fù)的體驗(yàn)了幾次了,感覺(jué)這種Banner的效果還不錯(cuò)。最后想著高仿一個(gè)和這種效果差不多的BannerView 。那么本文就講一下如何實(shí)現(xiàn)這樣一個(gè)BannerView。最終實(shí)現(xiàn)的效果如下:

MZBannerView.gif

目錄

本文會(huì)講實(shí)現(xiàn)仿魅族Banner效果所要用到的一些關(guān)鍵知識(shí)點(diǎn),目錄如下圖所示。所有的效果已經(jīng)封裝成一個(gè)庫(kù)。詳細(xì)代碼請(qǐng)看github: https://github.com/pinguo-zhouwei/MZBannerView

本文目錄.png

仿魅族Banner 效果

在開始實(shí)現(xiàn)魅族Banner效果之前,我們先來(lái)整理一下實(shí)現(xiàn)一個(gè)BannerView的思路,首先需要用ViewPager,其次讓ViewPager無(wú)限輪播。其實(shí)BannerView就是一個(gè)無(wú)限輪播的ViewPager,然后做一些封裝處理,讓使用更加簡(jiǎn)單就ok。

現(xiàn)在我們?cè)趤?lái)看一下魅族的這個(gè)Banner。他與普通的banner的區(qū)別是當(dāng)前頁(yè)顯示了前一頁(yè)和后一頁(yè)的部分內(nèi)容。

ViewPager展示多頁(yè).png

拋開切換時(shí)的動(dòng)畫先不說(shuō),要實(shí)現(xiàn)這個(gè)效果的第一步就是要讓ViewPager在一個(gè)頁(yè)面顯示多頁(yè)的內(nèi)容(當(dāng)前頁(yè)+前后頁(yè)部分)。

1 . ViewPager展示多頁(yè)
要讓ViewPager頁(yè)面展示多頁(yè)的內(nèi)容,就要用到ViewGroup的一個(gè)強(qiáng)大的屬性。這個(gè)屬性雖然強(qiáng)大,但是也不常用,可能有些小伙伴不知道(之前我也沒(méi)用過(guò)...),那就是clipChildren屬性。這個(gè)屬性有什么作用呢,我們看一下它的文檔介紹:

/**
     * By default, children are clipped to their bounds before drawing. This
     * allows view groups to override this behavior for animations, etc.
     *
     * @param clipChildren true to clip children to their bounds,
     *        false otherwise
     * @attr ref android.R.styleable#ViewGroup_clipChildren
     */```

**clipChildren:** 默認(rèn)值為true, 子View 的大小只能在父View規(guī)定的范圍之內(nèi),比如父View的高為50,子View的高為60 ,那么多處的部分就會(huì)被裁剪。如果我們?cè)O(shè)置這個(gè)值為false的話,那么多處的部分就不會(huì)被裁剪了。

這里我們就可以利用這個(gè)屬性來(lái)實(shí)現(xiàn)了這個(gè)效果了,我們?cè)O(shè)置ViewPager的父布局的`clipChildren`為false。然后設(shè)置ViewPager 左右一定的邊距,那么左右就空出了一定的區(qū)域,利用`clipChildren` 屬性,就能讓前后頁(yè)面的部分顯示在當(dāng)前頁(yè)了。布局如下:
```java
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:clipChildren="false"
    android:orientation="vertical"
    >


   <android.support.v4.view.ViewPager
       android:id="@+id/view_pager"
       android:layout_width="match_parent"
       android:layout_height="200dp"
       android:layout_marginLeft="30dp"
       android:layout_marginRight="30dp"
       />
</LinearLayout>

這樣就能實(shí)現(xiàn)ViewPager 展示前后頁(yè)面的部分內(nèi)容。

2 . 自定義ViewPager.PageTransformer動(dòng)畫
上面實(shí)現(xiàn)了ViewPager當(dāng)前頁(yè)面顯示前后頁(yè)的部分內(nèi)容,但是從最開始魅族的Banner效果我們可以看出,滑動(dòng)的時(shí)候是有 一個(gè)放大縮小的動(dòng)畫的。左右顯示的部分有一定比例的縮小。這就要用到ViewPager.PageTransformer了。

ViewPager.PageTransformer 干什么的呢?ViewPager.PageTransformer 是用來(lái)做ViewPager切換動(dòng)畫的,它是一個(gè)接口,里面只有一個(gè)方法transformPage

 public interface PageTransformer {
        /**
         * Apply a property transformation to the given page.
         *
         * @param page Apply the transformation to this page
         * @param position Position of page relative to the current front-and-center
         *                 position of the pager. 0 is front and center. 1 is one full
         *                 page position to the right, and -1 is one page position to the left.
         */
        void transformPage(View page, float position);
    }

雖然只有一個(gè)方法,但是它很強(qiáng)大,它能反映出在ViewPager滑動(dòng)過(guò)程中,各個(gè)View的位置變化。我們拿到了這些位置變化,就能在這個(gè)過(guò)程中對(duì)View做各種各樣的動(dòng)畫了。

要自定義動(dòng)畫,我們就來(lái)需要知道positon這個(gè)值的變化區(qū)間。從官方給的ViewPager的兩個(gè)示例我們知道,position的變換有三個(gè)區(qū)間,[-Infinity,-1),[-1,1],(1.Infinity)。
[-Infinity,-1):已經(jīng)在屏幕之外,看不到了
(1.Infinity): 已經(jīng)在屏幕之外,看不到了。
** [-1,1]:** 這個(gè)區(qū)間是我門操作View動(dòng)畫的重點(diǎn)區(qū)間。
我們來(lái)看一下官方對(duì)于position的解釋:

官方的解釋:The position parameter indicates where a given page is located relative to the center of the screen. It is a dynamic property that changes as the user scrolls through the pages. When a page fills the screen, its position value is 0. When a page is drawn just off the right side of the screen, its position value is 1. If the user scrolls halfway between pages one and two, page one has a position of -0.5 and page two has a position of 0.5.

**根據(jù)解釋,也就是說(shuō)當(dāng)前停留的頁(yè)面的位置為 0,右邊屏幕之外繪制的這個(gè)頁(yè)面位置為 1。那么,A 頁(yè)面滑到 B 頁(yè)面有 2 種情況:第一種:左邊劃出屏幕,那么 A:0 -> -1,B :1 -> 0。第二種:右邊劃出屏幕,A:0->1, B :-1-> 0 **

了解了這個(gè)方法的變化后,我們就來(lái)自定義我們的切換動(dòng)畫,這里很簡(jiǎn)單,我們只需要一個(gè)scale動(dòng)畫。代碼如下:

/**
 * Created by zhouwei on 17/5/26.
 */

public class CustomTransformer implements ViewPager.PageTransformer {
    private static final float MIN_SCALE = 0.9F;
    @Override
    public void transformPage(View page, float position) {

        if(position < -1){
            page.setScaleY(MIN_SCALE);
        }else if(position<= 1){
            //
            float scale = Math.max(MIN_SCALE,1 - Math.abs(position));
            page.setScaleY(scale);
            /*page.setScaleX(scale);

            if(position<0){
                page.setTranslationX(width * (1 - scale) /2);
            }else{
                page.setTranslationX(-width * (1 - scale) /2);
            }*/

        }else{
            page.setScaleY(MIN_SCALE);
        }
    }

}

效果圖是這樣的:

仿魅族Banner效果圖.png

到此,我們仿魅族Banner的靜態(tài)效果就實(shí)現(xiàn)了。接下來(lái)我們就要讓Banner動(dòng)起來(lái),實(shí)現(xiàn)無(wú)限輪播效果。

圖片輪播

上面我們已經(jīng)實(shí)現(xiàn)了Bannerd的靜態(tài)展示和切換動(dòng)畫,那么我們現(xiàn)在就需要讓Banner動(dòng)起來(lái),實(shí)現(xiàn)無(wú)限輪播。

ViewPager實(shí)現(xiàn)Banner無(wú)效輪播效果有2種方案,第一種是:在列表的最前面插入最后一條數(shù)據(jù),在列表末尾插入第一個(gè)數(shù)據(jù),造成循環(huán)的假象。第二種方案是:采用getCount 返回 Integer.MAX_VALUE。結(jié)下來(lái)分別看一下這兩種方案。

1 . 在列表的最前面插入最后一條數(shù)據(jù),在列表末尾插入第一個(gè)數(shù)據(jù),造成循環(huán)的假象。

這種方法是怎么做的呢?,是這樣的:假如我們的列表有3條數(shù)據(jù),用三個(gè)頁(yè)面展示,分別編號(hào)為1,2,3。我們?cè)賱?chuàng)建一個(gè)新的列表,長(zhǎng)度為真實(shí)列表的長(zhǎng)度+2,也就是5。在最前面插入最后一條數(shù)據(jù),然后在末尾插入第一條數(shù)據(jù)。新列表就變成了這樣了,3-1-2-3-1。如果當(dāng)前滑到的是0位置(頁(yè)面3),那就通過(guò)ViewPager的setCurrentItem(int item, boolean smoothScroll)方法神不知鬼不覺(jué)的切換到3位置(頁(yè)面3),當(dāng)滑到4的位置時(shí)(頁(yè)面1),也用這個(gè)方法滑到1位置(頁(yè)面1)。這樣給我們的感覺(jué)就是無(wú)限輪播了。來(lái)一張圖輔助理解一下。

輪播切換示意圖.png

**2 . 采用getCount 返回 Integer.MAX_VALUE **

讓ViewPager 的Adapter getCount 方法返回一個(gè)很大的數(shù)(這里用Integer.MAX_VALUE),理論上可以無(wú)限滑動(dòng)。當(dāng)顯示完一個(gè)真實(shí)列表的周期后,又從真實(shí)列表的0位置顯示數(shù)據(jù),造成無(wú)限循環(huán)輪播的假象。開始時(shí)調(diào)用 mViewPager.setCurrentItem(Integer.MAX_VALUE /2)設(shè)置選中中間位置,這樣最開始就可以向左滑動(dòng)。關(guān)鍵代碼:

 int currentItem = getStartSelectItem();
         
//設(shè)置當(dāng)前選中的Item
 mViewPager.setCurrentItem(currentItem);

 private int getStartSelectItem(){
            // 我們?cè)O(shè)置當(dāng)前選中的位置為Integer.MAX_VALUE / 2,這樣開始就能往左滑動(dòng)
            // 但是要保證這個(gè)值與getRealPosition 的 余數(shù)為0,因?yàn)橐獜牡谝豁?yè)開始顯示
            int currentItem = Integer.MAX_VALUE / 2;
            if(currentItem % getRealCount()  ==0 ){
                return currentItem;
            }
            // 直到找到從0開始的位置
            while (currentItem % getRealCount()  != 0){
                currentItem++;
            }
            return currentItem;
        }

3 . 兩種方案選哪一種?

兩種方案我都試了一下,都可以實(shí)現(xiàn)輪播,但是第一種 方案在有切換動(dòng)畫的時(shí)候是有問(wèn)題的,因?yàn)樯厦嫖覀冋f(shuō)了滑動(dòng)到最后一頁(yè)切換到第一頁(yè)時(shí),用的是ViewPager的setCurrentItem(int item, boolean smoothScroll)方法,smoothScroll 的值為false,這樣界面就感覺(jué)不到我們偷偷的切換。但是這樣切換就沒(méi)有了動(dòng)畫。這樣每次切換就會(huì)很生硬,因此就拋棄這種方法。選擇第二種方案。

輪播我們采用Hanlder的postDelayed方法,關(guān)鍵代碼如下:


    private final Runnable mLoopRunnable = new Runnable() {
        @Override
        public void run() {
            if(mIsAutoPlay){
                mCurrentItem = mViewPager.getCurrentItem();
                mCurrentItem++;
                if(mCurrentItem == mAdapter.getCount() - 1){
                    mCurrentItem = 0;
                    mViewPager.setCurrentItem(mCurrentItem,false);
                    mHandler.postDelayed(this,mDelayedTime);
                }else{
                    mViewPager.setCurrentItem(mCurrentItem);
                    mHandler.postDelayed(this,mDelayedTime);
                }
            }else{
                mHandler.postDelayed(this,mDelayedTime);
            }
        }
    };

在Adapter instantiateItem(ViewGroup container, final int position) 中,現(xiàn)在的這個(gè)position是一個(gè)很大的數(shù)字,我們需要將它轉(zhuǎn)換成一個(gè)真實(shí)的position,否則會(huì)越界報(bào)錯(cuò)。

 final int realPosition = position % getRealCount();
        /**
         * 獲取真實(shí)的Count
         * @return
         */
        private int getRealCount(){
            return  mDatas==null ? 0:mDatas.size();
        }

通過(guò)以上就實(shí)現(xiàn)了仿魅族的BannerView,但是這還沒(méi)完,雖然功能實(shí)現(xiàn)了,要想在任何地方拿來(lái)就可以使用,簡(jiǎn)單方便,我們還需要進(jìn)一步的封裝。

封裝輪子:MZBannerView

通過(guò)上面幾步就可以實(shí)現(xiàn)仿魅族的BannerView,但是為了使用方便,我們將它封裝成一個(gè)庫(kù),前面一篇文章講了,如何封裝一個(gè)通用的ViewPager(文章地址:ViewPager系列之 打造一個(gè)通用的ViewPager)。既然要想Banner使用方便,我們也需要封裝得通用,可擴(kuò)展。因?yàn)槲覀兊腂anner也是用ViewPager 實(shí)現(xiàn)的,因此,我們可用上一篇文章的方法,封裝一個(gè)通用的BannerView。

MZBannerView 有以下功能:
1 . 仿魅族BannerView 效果。
2 . 當(dāng)普通Banner 使用
3 . 當(dāng)普通ViewPager 使用。
4 . 當(dāng)普通ViewPager使用(有魅族Banner效果)

自定義屬性

屬性名 屬性意義 取值
open_mz_mode 是否開啟魅族模式 true 為魅族Banner效果,false 則普通Banner效果
canLoop 是否輪播 true 輪播,false 則為普通ViewPager
indicatorPaddingLeft 設(shè)置指示器距離左側(cè)的距離 單位為 dp 的值
indicatorPaddingRight 設(shè)置指示器距離右側(cè)的距離 單位為 dp 的值
indicatorAlign 設(shè)置指示器的位置 有三個(gè)取值:left 左邊,center 劇中顯示,right 右側(cè)顯示

通過(guò)open_mz_modecanLoop這兩個(gè)屬性來(lái)控制MZBannerView 是用作Banner還是普通ViewPager,有4種組合方式
1,仿魅族BannerView(默認(rèn)的模式)

app:open_mz_mode="true"
app:canLoop="true"

2, 普通BannerView

app:open_mz_mode="false"
app:canLoop="true"

3 ,普通ViewPager (有魅族Banner的切換動(dòng)畫)

app:open_mz_mode="true"
app:canLoop="false"

4, 普通ViewPager

app:open_mz_mode="false"
app:canLoop="false"

使用方法:
1 . xml 布局文件

 <com.zhouwei.mzbanner.MZBannerView
       android:id="@+id/banner"
       android:layout_width="match_parent"
       android:layout_height="200dp"
       android:layout_marginTop="10dp"
       app:open_mz_mode="true"
       app:canLoop="true"
       app:indicatorAlign="center"
       app:indicatorPaddingLeft="10dp"
       />

2 . activity中代碼:

        mMZBanner = (MZBannerView) view.findViewById(R.id.banner);
       // 設(shè)置頁(yè)面點(diǎn)擊事件
        mMZBanner.setBannerPageClickListener(new MZBannerView.BannerPageClickListener() {
            @Override
            public void onPageClick(View view, int position) {
                Toast.makeText(getContext(),"click page:"+position,Toast.LENGTH_LONG).show();
            }
        });
   
        List<Integer> list = new ArrayList<>();
        for(int i=0;i<RES.length;i++){
            list.add(RES[i]);
        }
       // 設(shè)置數(shù)據(jù)
        mMZBanner.setPages(list, new MZHolderCreator<BannerViewHolder>() {
            @Override
            public BannerViewHolder createViewHolder() {
                return new BannerViewHolder();
            }
        });

 public static class BannerViewHolder implements MZViewHolder<Integer> {
        private ImageView mImageView;
        @Override
        public View createView(Context context) {
            // 返回頁(yè)面布局文件
            View view = LayoutInflater.from(context).inflate(R.layout.banner_item,null);
            mImageView = (ImageView) view.findViewById(R.id.banner_image);
            return view;
        }

        @Override
        public void onBind(Context context, int position, Integer data) {
            // 數(shù)據(jù)綁定
            mImageView.setImageResource(data);
        }
    }

3 .如果是當(dāng)Banner使用,注意在onResume 中調(diào)用start()方法,在onPause中調(diào)用 pause() 方法。如果當(dāng)普通ViewPager使用,則不需要。

 @Override
    public void onPause() {
        super.onPause();
        mMZBanner.pause();//暫停輪播
    }

    @Override
    public void onResume() {
        super.onResume();
        mMZBanner.start();//開始輪播
    }

其他對(duì)外API

    /******************************************************************************************************/
    /**                             對(duì)外API                                                               **/
    /******************************************************************************************************/
    //開始輪播
     start()
    //停止輪播
     pause()

    //設(shè)置BannerView 的切換時(shí)間間隔
     setDelayedTime(int delayedTime)
    // 設(shè)置頁(yè)面改變監(jiān)聽器
    addPageChangeLisnter(ViewPager.OnPageChangeListener onPageChangeListener)

    //添加Page點(diǎn)擊事件
     setBannerPageClickListener(BannerPageClickListener bannerPageClickListener)
    //設(shè)置是否顯示Indicator
    setIndicatorVisible(boolean visible)
    // 獲取ViewPager
    ViewPager getViewPager()
    // 設(shè)置 Indicator資源
    setIndicatorRes(int unSelectRes,int selectRes)
    //設(shè)置頁(yè)面數(shù)據(jù)
    setPages(List<T> datas,MZHolderCreator mzHolderCreator)
    //設(shè)置指示器顯示位置
    setIndicatorAlign(IndicatorAlign indicatorAlign)
    //設(shè)置ViewPager(Banner)切換速度
    setDuration(int duration)

因?yàn)槭菍?duì)ViewPager的包裝,所有要設(shè)置某些ViewPager的屬性,可以通過(guò)getViewPager 獲取到ViewPager再設(shè)置對(duì)應(yīng)屬性

效果圖:
1, BannerView 輪播效果圖:

仿魅族Banner效果.gif

2 普通ViewPager效果圖:

MZBanner普通ViewPager效果.gif

總結(jié)

本文講了如何實(shí)現(xiàn)一個(gè)仿魅族Banner效果。其中講了一些關(guān)鍵的點(diǎn)和關(guān)鍵代碼。其實(shí)普通的BannerView 是一樣的,只是少了動(dòng)畫而已。最后,將這些功能封裝成了一個(gè)通用的BannerView 控件。這個(gè)控件既有仿魅族Banner的效果,又可以當(dāng)普通Banner使用。而且還可以當(dāng)作一個(gè)普通的ViewPager使用。

更多詳細(xì)代碼和使用方法請(qǐng)看github:https://github.com/pinguo-zhouwei/MZBannerView

最后,可能還有不完善的地方,如有問(wèn)題,歡迎留言和提Issues。如果你覺(jué)得不錯(cuò),歡迎star和 Fxxk 。。。。啊呸。。。是fork。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容