通用的广告栏控件

作者: _小河马 | 来源:发表于2016-10-24 18:16 被阅读848次

    懒癌晚期,终于憋出一遍文章,希望后面能多写写文章吧!

    简介

    • 一个简单的通用广告栏控件。可轻松实现循环滚动,自动滚动以及自定义的翻页效果。手指滑动自动停止滚动,手指离开自动开始滚动。支持各种网络库的加载方式。话不多说源码地址https://github.com/Hemumu/SimpleBanner
    banner_demo.gif

    虽说已经有很多很好的Banner广告控件了,但是本着实现出真理的道理还是自己去模仿着实践了一下,做的时候也学到到很多知识点。平时看到很多都是一副这个我知道这个我见过,但是只有自己做的时候才知道里面的知识点和难点。知行合一!

    控件采用 ,ViewPage+ImageView 的方式来实现的,循环翻页主要采用ViewPagersetCurrentItem方法来设置当前的Item,使当前的Item永远都不是第一个或者最后一个。

    自定义ViewPageAdapter

    新建一个类SimplePageAdapter,此类继承PagerAdapter。通过重写finishUpdate来实现循环翻页。方法的官方解释为 :

     Called when the a change in the shown pages has been completed. At this point you must ensure that all of the pages have actually been added or removed from the container as appropriate.。
    

    翻译过来的意思就是:当所示页面中的更改已完成时调用。 此时,必须确保所有页面都已实际添加或从容器中删除。在此方法中判断当前Item是否翻至第一页或者最后一页,然后在通过setCurrentItem来设置当前的Item来实现循环的翻页。

    @Override
    public void finishUpdate(ViewGroup container) {
      int position = viewPager.getCurrentItem();
      if (position == 0) {
          position = viewPager.getFristItem();
      } else if (position == getCount() - 1) {
          position = viewPager.getLastItem();
      }
      try {    
          viewPager.setCurrentItem(position, false);
    }catch (IllegalStateException e){}
    

    重写getCount来设置最大的翻页数量,这里的最大数量我用的是页数的实际数量去乘一个固定值,这个值可大可小,因为到达最大值后我们在finishUpdate中又把当前的Item设置为了实际页数的数量。这里我设置的300。

    
    private final int MULTIPLE_COUNT = 300;
    /** 
     设置Count为getRealCount()*MULTIPLE_COUNT * @return
    */
    @Override
    public int getCount() {   
     return canLoop ? getRealCount()*MULTIPLE_COUNT : getRealCount();
    }
    public int getRealCount() {    return mDatas == null ? 0 : mDatas.size();}
    

    canLoop是控制是否开启循环翻页。

    重写instantiateItem,这个方法有点像ListView的getView方法,就是创建子Iitem。这里很简单就是new一个View添加到ViewGroup中,我这里新建了一个SimpleHolderCreator接口,这个接口就是去创建一个SimpleHolder,而这个SimpleHolder接口提供了两个方法createView是创建页面布局,UpdateUI是更新Item的内容。

    public interface SimpleHolderCreator<SimpleHolder> {    
      public SimpleHolder createHolder();
    }
    
    public interface SimpleHolder<T> {
      View createView(Context context);
      void UpdateUI(Context context, int position, T data);
    }
    

    用户去实现这个接口让用户自定义翻页里面的内容,可以是一个ImageView也可以是一个复杂的布局。

     @Override
    public Object instantiateItem(ViewGroup container, int position) {
      int realPosition = toRealPosition(position);
      View view = getView(realPosition, null, container);
      container.addView(view);
      return view;
     }
    
    public View getView(int position, View view, ViewGroup container) {
      SimpleHolder holder = null;
      if (view == null) {
        holder = (SimpleHolder) holderCreator.createHolder();
         view = holder.createView(container.getContext());
        view.setTag(R.id.cb_item_tag, holder);
      } else {  
        holder = (SimpleHolder<T>) view.getTag(R.id.cb_item_tag);
      }
      if (mDatas != null && !mDatas.isEmpty()) 
          holder.UpdateUI(container.getContext(), position, mDatas.get(position));
      return view;
    }
    
    

    adapter还提供把一个Postion转化为当前真实的Postion的方法,因为页面是无限循环翻页,所以页面的Posion并不是我们想要的真是Pootion。通过position % realCount来获取真实的Postion

    public int toRealPosition(int position) { 
      int realCount = getRealCount();  
      if (realCount == 0)        
          return 0;   
      int realPosition = position % realCount;    
      return realPosition;
    }
    

    接下来我们要新建一个类SimpleViewPage并继承V4包下的ViewPager类。主要设置各种事件以及对Item的操作。

    初始化方法中设置OnPageChangeListener对翻页的Scrolled和Selected监听,主要对用户设置了的OnPageChangeListener进行回调。 这里需要注意的是设置监听使用addOnPageChangeListener而老版本的setOnPageChangeListener已经废弃了。

    private void init() {    super.addOnPageChangeListener(onPageChangeListener);}
    
    private OnPageChangeListener onPageChangeListener = new OnPageChangeListener() {
      private float mPreviousPosition = -1;
      @Override
      public void onPageSelected(int position) {
        //转化为真实的Postiin
        int realPosition = mAdapter.toRealPosition(position);
         if (mPreviousPosition != realPosition) {
            mPreviousPosition = realPosition;
            //如果设置了PageChangeListener就调用onPageSelected方法
            if (mOuterPageChangeListener != null) {
              mOuterPageChangeListener.onPageSelected(realPosition);
            }
           }
          }
      @Override
      public void onPageScrolled(int position, float positionOffset,int positionOffsetPixels) {
        int realPosition = position;
        if (mOuterPageChangeListener != null) {
          //如果postion不是最后一个直接回调
          if (realPosition != mAdapter.getRealCount() - 1) {
            mOuterPageChangeListener.onPageScrolled(realPosition,positionOffset, positionOffsetPixels);
          } else {
            if (positionOffset > .5) {
               mOuterPageChangeListener.onPageScrolled(0, 0, 0);
            } else {
                mOuterPageChangeListener.onPageScrolled(realPosition,        0, 0);
             }
            }
          }
        }
    

    重写 onInterceptTouchEvent事件拦截监听,如果用户这是了不可以手动滑动翻页则拦截事件

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) { 
     if (isCanScroll)       
        return super.onInterceptTouchEvent(ev);   
     else        
        return false;
    }
    
    

    重写onTouchEvent来实现用户的单机事件,这里单击的判断比较简单,用户点击和离开的x坐标小于sens就认为用单击了某个Item()

    
    /** * 手指点击的偏移距离 */
    private static final float sens = 5
    @Override
    public boolean onTouchEvent(MotionEvent ev) { 
      if (isCanScroll) {
        if (onItemClickListener != null) {
          switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
              oldX = ev.getX();
              break;
            case MotionEvent.ACTION_UP:
              newX = ev.getX();
              if (Math.abs(oldX - newX) < sens) {
                onItemClickListener.onItemClick((getRealItem()));
              }
              oldX = 0;
              newX = 0;
              break;
            }
          }
           return super.onTouchEvent(ev);
      } else   
       return false;
    }
    
    

    设置Adapter

    public void setAdapter(PagerAdapter adapter, boolean canLoop) {    
      mAdapter = (SimplePageAdapter) adapter;    
      //设置是否循环翻页
      mAdapter.setCanLoop(canLoop);    
      mAdapter.setViewPager(this);    
      super.setAdapter(mAdapter);    
      //设置当前的Item为页面的数量    
      setCurrentItem(getFristItem(), false);
     }
    
    

    最后一个类SimpleBannerView继承LinearLayout,初始化方法中加载一个布局文件,布局文件中包含我们刚刚创建的SimpleViewPage和一个LinearLayout主要累存放翻页指示点。

    <RelativeLayout
      android:layout_width="match_parent"
      android:layout_height="match_parent">
    
      <com.helin.bannerview.view.SimpleViewPage
        android:id="@+id/cbLoopViewPager"
         android:layout_width="match_parent"
         android:layout_height="match_parent" />
    
       <!-- 翻页指示点的viewgroup -->
      <LinearLayout
        android:id="@+id/loPageTurningPoint"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_centerHorizontal="true"
        android:layout_margin="10dp"
        android:orientation="horizontal">
      </LinearLayout>
      </RelativeLayout>
    
    
    private void init(Context context) {    
      View hView = LayoutInflater.from(context).inflate(    R.layout.include_viewpage, this, true);     
      viewPager = (SimpleViewPage) hView.findViewById(R.id.cbLoopViewPager);
      loPageTurningPoint = (ViewGroup) hView.findViewById(R.id.loPageTurningPoint);  
      initViewPagerScroll();    
      adSwitchTask = new AdSwitchTask(this);
    }
    
    

    initViewPagerScroll方法是设置ViewPage的滑动速度,怎么设置呢?难道不是setxxxx么,还真的不是,我们看源码中可以看到一个叫mScroller的字段,这个字段应该是控制ViewPage的滑动的。

    {@sample frameworks/support/samples/Support13Demos/src/com/example/android/supportv13/app/ActinBarTabsPager.java *      complete} */
    public class ViewPager extends ViewGroup {
      ...
      private Scroller mScroller;
      ...
    }
    
    

    可以看到这个这段是private的也没有get和set方法,所以我们只有利用java反射机制拿到这一字段在设置为我们自己的Scroller,把mScroller设置为我们自定义继承至Scroller的类ViewPagerScroller就可以实现滑动速度的修改。有人要问了为啥要去修改这个呢,原来的不是很好么。(闲的蛋疼)其实是因为设置的ViewPage的PageTransformer也就是动画效果如果滑动速度过快那么我们自定义的动画效果也就不那么明显了,特别是3D的动画效果。所以我们手动修改了它的滑动。java反射Field用法

    ViewPagerScroller类

    public class ViewPagerScroller extends Scroller {
      private int mScrollDuration = 800;// 滑动速度,值越大滑动越慢,滑动太快会使3d效果不明显
      private boolean zero;
      ...
      @Override
      public void startScroll(int startX, int startY, int dx, int dy, int duration) {
        super.startScroll(startX, startY, dx, dy, zero ? 0 : mScrollDuration);
      }
    
      @Override
      public void startScroll(int startX, int startY, int dx, int dy) {    
        super.startScroll(startX, startY, dx, dy, zero ? 0 : mScrollDuration);
      }
      public void setScrollDuration(int scrollDuration) {    this.mScrollDuration = scrollDuration;}
      public boolean isZero() {    return zero;}
      ...
    }
    

    在初始化方法中还有这么一句adSwitchTask = new AdSwitchTask(this);,一看也知道是一个线程是吧,其实。。。他就是一个线程。负责页面的自动翻页,我们来看看这个累不累(输入法背锅)内部类

    static class AdSwitchTask implements Runnable {
      private final WeakReference<ConvenientBanner> reference;
        AdSwitchTask(ConvenientBanner convenientBanner) {
          this.reference = new WeakReference<ConvenientBanner>(convenientBanner);
        }
      @Override
       public void run() {
        ConvenientBanner convenientBanner = reference.get();
        if(convenientBanner != null){
          if (convenientBanner.viewPager != null && convenientBanner.turning) {
            int page = convenientBanner.viewPager.getCurrentItem() + 1;
            convenientBanner.viewPager.setCurrentItem(page);
            convenientBanner.postDelayed(convenientBanner.adSwitchTask, convenientBanner.autoTurningTime);
            }
          }
        }
      }
    
    

    咦,这个写法怎么那么熟悉又陌生呢,好像在大明湖畔见过啊。我们先来看看正常我们的写法是下面这样

    class TestTask implements Runnable{    
    @Override   
     public void run() {       
        if (viewPager != null && turning) {            
          int page = viewPager.getCurrentItem() + 1;            
          viewPager.setCurrentItem(page); 
          postDelayed(adSwitchTask, autoTurningTime);      
        }    
      }
    }
    
    

    这样写好像没有什么错啊,其实不然,run方法里面引用了外部类的viewPager字段,这个线程是每隔一定时间去支持一次的。假如每次执行的时候外部类ConvenientBanner所在的Activity结束了,而run方法内部却还在引用它的字段,恭喜肯定会内存泄漏的。

    为什么呢?因为在java中所有非静态的对象都会持有当前类的强引用,而静态对象则只会持有当前类的弱引用。声明为静态后,TestTask 将会持有一个ConvenientBanner的弱引用,而弱引用会很容易被gc回收,这样就能解决Activity结束后,gc却无法回收的情况。(至于为什么强引用不能够被gc自动回收,而弱引用对象为什么会被gc回收,可以自行去google) ,这也是为什么google推荐在创建handler的时候声明为static的,同样的道理。

    有两种解决方法

    • TestTask(Handler) 修改为静态类
    • 使用静态内部类,通过WeakReference实现对ConvenientBanner的弱引用

    我们肯定选择高大上的后者啊。所以就有了上面的写法!知识点get!!!
    Android中WeakReference的理解和使用请参考http://www.ithtw.com/6791.html

    我写哪了? 忘了。。。。

    设置翻页的小点

      public SimpleBannerView setPageIndicator(int[] page_indicatorId) {
        loPageTurningPoint.removeAllViews();
        mPointViews.clear();
        this.page_indicatorId = page_indicatorId;
        if(mDatas==null)return this;
        for (int count = 0; count < mDatas.size(); count++) {
          // 翻页指示的点
          ImageView pointView = new ImageView(getContext());
          pointView.setPadding(5, 0, 5, 0);
          if (mPointViews.isEmpty())
            pointView.setImageResource(page_indicatorId[1]);
          else
            pointView.setImageResource(page_indicatorId[0]);
        mPointViews.add(pointView);
        loPageTurningPoint.addView(pointView);
      }
        pageChangeListener = new SimplePageChangeListener(mPointViews,        page_indicatorId);
        viewPager.addOnPageChangeListener(pageChangeListener);
        pageChangeListener.onPageSelected(viewPager.getRealItem());
        if(onPageChangeListener != null)pageChangeListener.addOnPageChangeListener(onPageChangeListener);
        return this;
    }
      
    

    翻页小点其实就是一个LinearLayout里面放了很多ImageView。通过setImageResource修改小点的图片。这里翻页小点提供了三种模式,居中 靠左 靠右。默认靠右

      /** 
    * 指示器的方向 * @param align  
      三个方向:居左 (RelativeLayout.ALIGN_PARENT_LEFT),居中 (RelativeLayout.CENTER_HORIZONTAL),居右 (RelativeLayout.ALIGN_PARENT_RIGHT)
      @return 
    */
    
    public SimpleBannerView setPageIndicatorAlign(PageIndicatorAlign align) {
      RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams)
      loPageTurningPoint.getLayoutParams();
      layoutParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT, align ==   PageIndicatorAlign.ALIGN_PARENT_LEFT ? RelativeLayout.TRUE : 0);
      layoutParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, align ==   PageIndicatorAlign.ALIGN_PARENT_RIGHT ? RelativeLayout.TRUE : 0);
      layoutParams.addRule(RelativeLayout.CENTER_HORIZONTAL, align ==   PageIndicatorAlign.CENTER_HORIZONTAL ? RelativeLayout.TRUE : 0);
      loPageTurningPoint.setLayoutParams(layoutParams);   
      return this;
    }
    
    

    重写dispatchTouchEvent拦截事件,实现用户手指滑动的时候停止自动翻页,停止滑动继续自动翻页

    //触碰控件的时候,翻页应该停止,离开的时候如果之前是开启了翻页的话则重新启动翻页
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
      int action = ev.getAction();
      if (action == MotionEvent.ACTION_UP||action == MotionEvent.ACTION_CANCEL||action == MotionEvent.ACTION_OUTSIDE) {
      // 开始翻页
        if (canTurn)startTurning(autoTurningTime);
      } else if (action == MotionEvent.ACTION_DOWN) {
        // 停止翻页
         if (canTurn)stopTurning();
       }
      return super.dispatchTouchEvent(ev);
    
    

    设置翻页数据

    public SimpleBannerView setPages(SimpleHolderCreator creator, List<T> datas ){
      this.mDatas = datas;
      mAdapter = new SimplePageAdapter<>(mDatas,creator);
      viewPager.setAdapter(mAdapter,canLoop);
      if (page_indicatorId != null)
        setPageIndicator(page_indicatorId);
      return  this;
    }
    
    

    设置翻页动画

     public SimpleBannerView<T> setPageTransformer(ViewPager.PageTransformer transformer) {
      viewPager.setPageTransformer(true, transformer);
      return this;
    }
    

    用法很简单

     viewpage.setPages(
        new SimpleHolderCreator() {
          @Override
          public LocalImageHolderView createHolder() {
            return new LocalImageHolderView();
          }
        }, localImages)
      //自定义翻页效果,下面演示google的一个PageTransformer的代码
        .setPageTransformer(new ZoomOutPageTransformer())
       //自动翻页时间
      .startTurning(2000)
       //设置指示器的方向
      .setPageIndicatorAlign(SimpleBannerView.PageIndicatorAlign.ALIGN_PARENT_RIGHT)
       //设置两个点图片作为翻页指示器,不设置则没有指示器,可以根据自己需求自行配合自己的指示器,不需要圆点指示器可用不设
      .setPageIndicator(new int[]{R.drawable.ic_page_indicator, R.drawable.ic_page_indicator_focused});
      //点击事件
      viewpage.setOnItemClickListener(new OnItemClickListener() {
        @Override
        public void onItemClick(int position) {
          Toast.makeText(MainActivity.this, "click item " + position, Toast.LENGTH_SHORT).show();
        }
    });
    
    public class LocalImageHolderView implements SimpleHolder<Integer> {
      private ImageView imageView;
       
      @Override
      public View createView(Context context) {
        imageView = new ImageView(context);
        imageView.setScaleType(ImageView.ScaleType.FIT_XY);
        return imageView;
      }
      
      @Override
       public void UpdateUI(Context context, final int position, Integer data) {
        //这里可以使用各种图片加载框架加载。
        imageView.setImageResource(data);
      }
      
    
    
    

    基本到此控件就差不多了,源码已给出。不重复造轮子,这话虽好,但是只有你去造了这个轮子才知道这个轮子怎么造的,轮子哪里比较难造!还是那句话知行合一。你知道但是做不到就不能算知道,你要知道了而且还要能做到才牛逼。

    the end !

    感谢
    https://github.com/saiwu-bigkoo/Android-ConvenientBanner
    https://github.com/imbryk/LoopingViewPager
    ViewPage翻页动画https://github.com/ToxicBakery/ViewPagerTransforms

    相关文章

      网友评论

      本文标题:通用的广告栏控件

      本文链接:https://www.haomeiwen.com/subject/ibdluttx.html