ViewPager系列之 仿魅族应用的广告BannerView

6783dlki 4年前
   <h3>前言</h3>    <p>Banner广告位是APP 中的一个非常重要的位置,为什么呢?因为它能带来money。是一个公司很重要的一个营收点。像那种用户数基数特别大的产品,如非死book、推ter、QQ、微信等等。Banner广告位日营收估计得上千万美刀(猜的,不知道具体数据)。一个漂亮的Banner往往能够吸引用户的眼球,引导用户点击,从而提高转化率。遗憾的是现在的大多数产品的Banner都是千篇一律的,没有什么亮点可言。但是前几天在魅族手机上发现了一个效果不错的Banner,魅族所有自家的APP所用的Banner 引起了我的注意。效果是这样子的:</p>    <p><img src="https://simg.open-open.com/show/2896c90ea2e5b323452ef7b89ea221f1.gif"></p>    <p>meizuapp.gif</p>    <p>看到这个Banner 第一眼就吸引了我,随后就反复的体验了几次了,感觉这种Banner的效果还不错。最后想着高仿一个和这种效果差不多的BannerView 。那么本文就讲一下如何实现这样一个BannerView。最终实现的效果如下:</p>    <p><img src="https://simg.open-open.com/show/64f3937e83e9eea89c578eeb876222f4.gif"></p>    <p>MZBannerView.gif</p>    <h3>目录</h3>    <p>本文会讲实现仿魅族Banner效果所要用到的一些关键知识点,目录如下图所示。所有的效果已经封装成一个库。详细代码请看github: <a href="/misc/goto?guid=4959749533228460492" rel="nofollow,noindex">https://github.com/pinguo-zhouwei/MZBannerView</a></p>    <p><img src="https://simg.open-open.com/show/25962184f2d6db371aa122e4a882e145.png"></p>    <p>本文目录.png</p>    <h3>仿魅族Banner 效果</h3>    <p>在开始实现魅族Banner效果之前,我们先来整理一下实现一个BannerView的思路,首先需要用ViewPager,其次让ViewPager无限轮播。其实BannerView就是一个无限轮播的ViewPager,然后做一些封装处理,让使用更加简单就ok。</p>    <p>现在我们在来看一下魅族的这个Banner。他与普通的banner的区别是当前页显示了前一页和后一页的部分内容。</p>    <p><img src="https://simg.open-open.com/show/051292be9427f7df66990d2b74452d41.png"></p>    <p>ViewPager展示多页.png</p>    <p>抛开切换时的动画先不说,要实现这个效果的第一步就是要让ViewPager在一个页面显示多页的内容(当前页+前后页部分)。</p>    <p>1 . ViewPager展示多页</p>    <p>要让ViewPager页面展示多页的内容,就要用到ViewGroup的一个强大的属性。这个属性虽然强大,但是也不常用,可能有些小伙伴不知道(之前我也没用过...),那就是 clipChildren 属性。这个属性有什么作用呢,我们看一下它的文档介绍:</p>    <pre>  <code class="language-java">/**       * 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       */</code></pre>    <p>clipChildren:默认值为true, 子View 的大小只能在父View规定的范围之内,比如父View的高为50,子View的高为60 ,那么多处的部分就会被裁剪。如果我们设置这个值为false的话,那么多处的部分就不会被裁剪了。</p>    <p>这里我们就可以利用这个属性来实现了这个效果了,我们设置ViewPager的父布局的 clipChildren 为false。然后设置ViewPager 左右一定的边距,那么左右就空出了一定的区域,利用 clipChildren 属性,就能让前后页面的部分显示在当前页了。布局如下:</p>    <pre>  <code class="language-xml"><?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></code></pre>    <p>这样就能实现ViewPager 展示前后页面的部分内容。</p>    <p>2 . 自定义ViewPager.PageTransformer动画</p>    <p>上面实现了ViewPager当前页面显示前后页的部分内容,但是从最开始魅族的Banner效果我们可以看出,滑动的时候是有 一个放大缩小的动画的。左右显示的部分有一定比例的缩小。这就要用到ViewPager.PageTransformer了。</p>    <p>ViewPager.PageTransformer 干什么的呢?ViewPager.PageTransformer 是用来做ViewPager切换动画的,它是一个接口,里面只有一个方法 transformPage 。</p>    <pre>  <code class="language-java">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);      }</code></pre>    <p>虽然只有一个方法,但是它很强大,它能反映出在ViewPager滑动过程中,各个View的位置变化。我们拿到了这些位置变化,就能在这个过程中对View做各种各样的动画了。</p>    <p>要自定义动画,我们就来需要知道positon这个值的变化区间。从官方给的ViewPager的两个示例我们知道,position的变换有三个区间,[-Infinity,-1),[-1,1],(1.Infinity)。</p>    <p>[-Infinity,-1):已经在屏幕之外,看不到了</p>    <p>(1.Infinity):已经在屏幕之外,看不到了。</p>    <p>[-1,1]:这个区间是我门操作View动画的重点区间。</p>    <p>比如:从A页面滑动到B页面,A 页面的位置变化为:0 ->-1,B页面的的位置变化为:0-> 1 。</p>    <p>了解了这个方法的变化后,我们就来自定义我们的切换动画,这里很简单,我们只需要一个scale动画。代码如下:</p>    <pre>  <code class="language-java">/**   * 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);          }      }    }</code></pre>    <p>效果图是这样的:</p>    <p><img src="https://simg.open-open.com/show/1ae260ec6defda289c454ff06c810809.png"></p>    <p>仿魅族Banner效果图.png</p>    <p>到此,我们仿魅族Banner的静态效果就实现了。接下来我们就要让Banner动起来,实现无限轮播效果。</p>    <h3>图片轮播</h3>    <p>上面我们已经实现了Bannerd的静态展示和切换动画,那么我们现在就需要让Banner动起来,实现无限轮播。</p>    <p>ViewPager实现Banner无效轮播效果有2种方案,第一种是:在列表的最前面插入最后一条数据,在列表末尾插入第一个数据,造成循环的假象。第二种方案是:采用getCount 返回 Integer.MAX_VALUE。结下来分别看一下这两种方案。</p>    <p>1 . 在列表的最前面插入最后一条数据,在列表末尾插入第一个数据,造成循环的假象。</p>    <p>这种方法是怎么做的呢?,是这样的:假如我们的列表有3条数据,用三个页面展示,分别编号为1,2,3。我们再创建一个新的列表,长度为真实列表的长度+2,也就是5。在最前面插入最后一条数据,然后在末尾插入第一条数据。新列表就变成了这样了,3-1-2-3-1。如果当前滑到的是0位置(页面3),那就通过ViewPager的 setCurrentItem(int item, boolean smoothScroll) 方法神不知鬼不觉的切换到3位置(页面3),当滑到4的位置时(页面1),也用这个方法滑到1位置(页面1)。这样给我们的感觉就是无限轮播了。来一张图辅助理解一下。</p>    <p><img src="https://simg.open-open.com/show/a484f32efbc79c3a741a9e4e0ac30b4d.png"></p>    <p>轮播切换示意图.png</p>    <p>2 . 采用getCount 返回 Integer.MAX_VALUE</p>    <p>让ViewPager 的Adapter getCount 方法返回一个很大的数(这里用Integer.MAX_VALUE),理论上可以无限滑动。当显示完一个真实列表的周期后,又从真实列表的0位置显示数据,造成无限循环轮播的假象。开始时调用 mViewPager.setCurrentItem(Integer.MAX_VALUE /2)设置选中中间位置,这样最开始就可以向左滑动。关键代码:</p>    <pre>  <code class="language-java">int currentItem = getStartSelectItem();    //设置当前选中的Item   mViewPager.setCurrentItem(currentItem);     private int getStartSelectItem(){              // 我们设置当前选中的位置为Integer.MAX_VALUE / 2,这样开始就能往左滑动              // 但是要保证这个值与getRealPosition 的 余数为0,因为要从第一页开始显示              int currentItem = Integer.MAX_VALUE / 2;              if(currentItem % getRealCount()  ==0 ){                  return currentItem;              }              // 直到找到从0开始的位置              while (currentItem % getRealCount() == 0){                  currentItem++;              }              return currentItem;          }</code></pre>    <p>3 . 两种方案选哪一种?</p>    <p>两种方案我都试了一下,都可以实现轮播,但是第一种 方案在有切换动画的时候是有问题的,因为上面我们说了滑动到最后一页切换到第一页时,用的是ViewPager的 setCurrentItem(int item, boolean smoothScroll) 方法,smoothScroll 的值为false,这样界面就感觉不到我们偷偷的切换。但是这样切换就没有了动画。这样每次切换就会很生硬,因此就抛弃这种方法。选择第二种方案。</p>    <p>轮播我们采用Hanlder的postDelayed方法,关键代码如下:</p>    <pre>  <code class="language-java">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);              }          }      };</code></pre>    <p>在Adapter instantiateItem(ViewGroup container, final int position) 中,现在的这个position是一个很大的数字,我们需要将它转换成一个真实的position,否则会越界报错。</p>    <pre>  <code class="language-java">final int realPosition = position % getRealCount();</code></pre>    <pre>  <code class="language-java">/**           * 获取真实的Count           * @return           */          private int getRealCount(){              return  mDatas==null ? 0:mDatas.size();          }</code></pre>    <p>通过以上就实现了仿魅族的BannerView,但是这还没完,虽然功能实现了,要想在任何地方拿来就可以使用,简单方便,我们还需要进一步的封装。</p>    <h3>封装轮子:MZBannerView</h3>    <p>通过上面几步就可以实现仿魅族的BannerView,但是为了使用方便,我们将它封装成一个库,前面一篇文章讲了,如何封装一个通用的ViewPager(文章地址: <a href="/misc/goto?guid=4959749533313581219" rel="nofollow,noindex">ViewPager系列之 打造一个通用的ViewPager</a> )。既然要想Banner使用方便,我们也需要封装得通用,可扩展。因为我们的Banner也是用ViewPager 实现的,因此,我们可用上一篇文章的方法,封装一个通用的BannerView。</p>    <p>MZBannerView 有以下功能:</p>    <p>1 . 仿魅族BannerView 效果。</p>    <p>2 . 当普通Banner 使用</p>    <p>3 . 当普通ViewPager 使用。</p>    <p>4 . 当普通ViewPager使用(有魅族Banner效果)</p>    <p>自定义属性</p>    <table>     <thead>      <tr>       <th>属性名</th>       <th>属性意义</th>       <th>取值</th>      </tr>     </thead>     <tbody>      <tr>       <td>open_mz_mode</td>       <td>是否开启魅族模式</td>       <td>true 为魅族Banner效果,false 则普通Banner效果</td>      </tr>      <tr>       <td>canLoop</td>       <td>是否轮播</td>       <td>true 轮播,false 则为普通ViewPager</td>      </tr>      <tr>       <td>indicatorPaddingLeft</td>       <td>设置指示器距离左侧的距离</td>       <td>单位为 dp 的值</td>      </tr>      <tr>       <td>indicatorPaddingRight</td>       <td>设置指示器距离右侧的距离</td>       <td>单位为 dp 的值</td>      </tr>      <tr>       <td>indicatorAlign</td>       <td>设置指示器的位置</td>       <td>有三个取值:left 左边,center 剧中显示,right 右侧显示</td>      </tr>     </tbody>    </table>    <p>通过 open_mz_mode 和 canLoop 这两个属性来控制MZBannerView 是用作Banner还是普通ViewPager,有4种组合方式</p>    <p>1,仿魅族BannerView(默认的模式)</p>    <pre>  <code class="language-java">app:open_mz_mode="true"   app:canLoop="true"</code></pre>    <p>2, 普通BannerView</p>    <pre>  <code class="language-java">app:open_mz_mode="false"   app:canLoop="true"</code></pre>    <p>3 ,普通ViewPager (有魅族Banner的切换动画)</p>    <pre>  <code class="language-java">app:open_mz_mode="true"   app:canLoop="false"</code></pre>    <p>4, 普通ViewPager</p>    <pre>  <code class="language-java">app:open_mz_mode="false"   app:canLoop="false"</code></pre>    <p>使用方法:</p>    <p>1 . xml 布局文件</p>    <pre>  <code class="language-java"><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"         /></code></pre>    <p>2 . activity中代码:</p>    <pre>  <code class="language-java">mMZBanner = (MZBannerView) view.findViewById(R.id.banner);         // 设置页面点击事件          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]);          }         // 设置数据          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) {              // 返回页面布局文件              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) {              // 数据绑定              mImageView.setImageResource(data);          }      }</code></pre>    <p>3 .如果是当Banner使用,注意在onResume 中调用start()方法,在onPause中调用 pause() 方法。如果当普通ViewPager使用,则不需要。</p>    <pre>  <code class="language-java">@Override      public void onPause() {          super.onPause();          mMZBanner.pause();//暂停轮播      }        @Override      public void onResume() {          super.onResume();          mMZBanner.start();//开始轮播      }</code></pre>    <p>其他对外API</p>    <pre>  <code class="language-java">/******************************************************************************************************/      /**                             对外API                                                               **/      /******************************************************************************************************/      //开始轮播       start()      //停止轮播       pause()        //设置BannerView 的切换时间间隔       setDelayedTime(int delayedTime)      // 设置页面改变监听器      addPageChangeLisnter(ViewPager.OnPageChangeListener onPageChangeListener)        //添加Page点击事件       setBannerPageClickListener(BannerPageClickListener bannerPageClickListener)      //设置是否显示Indicator      setIndicatorVisible(boolean visible)      // 获取ViewPager      ViewPager getViewPager()      // 设置 Indicator资源      setIndicatorRes(int unSelectRes,int selectRes)      //设置页面数据      setPages(List<T> datas,MZHolderCreator mzHolderCreator)      //设置指示器显示位置      setIndicatorAlign(IndicatorAlign indicatorAlign)      //设置ViewPager(Banner)切换速度      setDuration(int duration)</code></pre>    <p>因为是对ViewPager的包装,所有要设置某些ViewPager的属性,可以通过getViewPager 获取到ViewPager再设置对应属性</p>    <p>效果图:</p>    <p>1, BannerView 轮播效果图:</p>    <p><img src="https://simg.open-open.com/show/86700a363eb5a09c9fb9fd70e194fb47.gif"></p>    <p>仿魅族Banner效果.gif</p>    <p>2 普通ViewPager效果图:</p>    <p><img src="https://simg.open-open.com/show/958eddda79e7af042a0d7ac5ebf307ba.gif"></p>    <p>MZBanner普通ViewPager效果.gif</p>    <h3>总结</h3>    <p>本文讲了如何实现一个仿魅族Banner效果。其中讲了一些关键的点和关键代码。其实普通的BannerView 是一样的,只是少了动画而已。最后,将这些功能封装成了一个通用的BannerView 控件。 这个控件既有仿魅族Banner的效果,又可以当普通Banner使用。而且还可以当作一个普通的ViewPager使用。</p>    <p> </p>    <p>项目主页:<a href="http://www.open-open.com/lib/view/home/1496585426285">http://www.open-open.com/lib/view/home/1496585426285</a></p>    <p> </p>