通用的广告栏控件

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

简介

  • 一个简单的通用广告栏控件。可轻松实现循环滚动,自动滚动以及自定义的翻页效果。手指滑动自动停止滚动,手指离开自动开始滚动。支持各种网络库的加载方式。话不多说源码地址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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,335评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,895评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,766评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,918评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,042评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,169评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,219评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,976评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,393评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,711评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,876评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,562评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,193评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,903评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,142评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,699评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,764评论 2 351

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,900评论 25 707
  • 时间作废白衣 你是恶魔之眼 一眼看进地狱 一脚踏灭火焰 一念便生恶意 爱情坠入尘灰 与你堕入淤泥
    夏乔enjoy阅读 230评论 0 3
  • 后晌 细风穿过静默的阳光 轻浅的霾不理会 云彩躲开灰蒙蒙的世界 在荒原上撒了几粒白杨的种子 念了一句偈语,滴下两滴...
    风言无语阅读 445评论 25 85
  • 放学回来,女儿说道:“今天上学迟到了几分钟,忐忑地坐在座位,打开课本,悄悄抬头观察下老师的脸色,一眼望去,老师的眼...
    童心不改阅读 184评论 1 0
  • 涂一只可爱的小猫咪。 工具: 纸:飞乐鸟彩铅纸 笔:阵点红辉 其他:铅笔、橡皮
    插画师雨山阅读 211评论 0 1