配色: 字号:
使用HorizontalScrollView实现广告栏Banner及相关原理分析
2016-11-08 | 阅:  转:  |  分享 
  
使用HorizontalScrollView实现广告栏Banner及相关原理分析

现在的App中,广告栏Banner的使用还是挺广泛的,用于展示各种广告、活动推荐等。使用HorizontalScrollView可以很简单的实现一个可自动播放、可滑动、可点击的广告栏Banner,这个也可以做为一个例子,来学习自定义控件的制作。相关原理主要包括两个方面:



onMeasure、onLayout、onDraw等View、ViewGroup相关布局函数;

dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent等触摸事件处理函数(包括Click等事件触发);

这两部分搞懂后,制作自定义控件就得心应手了。



一、需求



控件每次展示一张图片,隔一段时间换播下一张,如果当前是最后一张则展示第一张;

用户手指触摸控件,停止轮播,用户滑动手指,则根据方向展示相应下一张或前一张图片;

如果是第一张图片继续往前滑动,需要展示最后一张图片,如果最后一张图片往后滑动,展示第一张图片;

最下方需要有小白点来指示当前是第几张图片;

最终效果如图:

这里写图片描述



二、初设计



HorizontalScrollView本来就是一个横向滑动组件,使用它可以很方便的实现滑动及相应的动画效果,所以选择用它来写这个控件,我看网上也有使用ViewPager实现,原理都是大同小异;下面是按上面的需求做的初始设计,在实现的过程中还会碰到其他问题,需要按情况解决。



布局

HorizontalScrollView——LinearLayout——ImageViewList

同时需要在HorizontalScrollView上画小白点指示当前页



定时滚动

添加一个定时器,每隔一段时间滑动到下一页,注意最后一页的循环处理。



用户事件

添加事件监控,触摸停止定时器及滑动事件处理



三、具体实现



1.设计布局



xml中的布局只有最外层控件,其他的LinearLayout和ImageView都是动态添加进去的,实现如下:



publicclassADPagerextendsHorizontalScrollView{



privateLinearLayoutcontainer=null;

privateLinearLayout.LayoutParamsimgLayoutParams=null;

publicADPager(Contextcontext){

super(context);

init();

}

privatevoidinit(){

Contextctx=getContext();

container=newLinearLayout(ctx);

ViewGroup.LayoutParamslayoutParams=newViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT);

container.setLayoutParams(layoutParams);

//横向布局

container.setOrientation(LinearLayout.HORIZONTAL);



imgLayoutParams=newLinearLayout.LayoutParams(getWidth(),getHeight());



this.addView(container);

this.setSmoothScrollingEnabled(true);

//不显示滑动条

this.setHorizontalScrollBarEnabled(false);

}



publicvoidsetImageList(intimgArray[]){

intsize=imgArray.length;

if(size>1){

//如果大于一张图片,第一张前放最后一张图片

this.container.addView(makeImageView(imgArray[size-1]));

}

for(intimgId:imgArray){

this.container.addView(makeImageView(imgId));

}

if(size>1){

//如果大于一张图片,最后一张后放第一张图片

this.container.addView(makeImageView(imgArray[0]));

}

}



publicImageViewmakeImageView(intresourceId){

ImageViewimageView;

Contextctx=getContext();

imageView=newImageView(ctx);

imageView.setImageResource(resourceId);

imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);

imageView.setLayoutParams(imgLayoutParams);

returnimageView;



在onCreate中调用初始化图片:



intimgIdArray[]={R.drawable.img1,R.drawable.img2,R.drawable.img3};

ADPageradPager=(ADPager)findViewById(R.id.adpager);

adPager.setImageList(imgIdArray);

然后运行就碰到了第一个坑,根本没有图片被展示出来,原因是:在初始化时,我们尝试使用getWidth和getHeight函数来获取宽度和高度,然后设置图片大小,但在View还没有展示出来时,其实通过这两个函数是不能获取宽高的,比如在onCreate/onStart/onResume中,详见:

Activity中获取view的高度和宽度为0的原因以及解决方案



在上面的文章中,也提到了几种获取的方式,但我们是自定义控件,还有他方式来获取:



@Override

protectedvoidonMeasure(intwidthMeasureSpec,intheightMeasureSpec){

super.onMeasure(widthMeasureSpec,heightMeasureSpec);

imgLayoutParams.width=getMeasuredWidth();

imgLayoutParams.height=getMeasuredHeight();

}

这种方法使用了onMeasure函数,现在只要知道这个函数是用来测量自己及子View的大小就可以了,后面还会系统总结。



现在已经可以展示出图片,且可以自由滑动,当然,现在还简陋的很:

这里写图片描述

只不过有多张图片时,我们显示的是最后一张图片,是因为我们为了第一张图片还可以往前滑动,在前面添加的,所以我们需要在初始时,滑动到第一张图片展示:



publicvoidscrollToPage(intpage,booleanisSmooth){

if(page<0){

page=mTotalSize-1;

}elseif(page>=mTotalSize){

page=0;

}

//设置当前页

mCurrPage=page;

intwidth=getWidth();

//因为第一张前面加了一张,所以页数需要+1。而只有一张图片时,scrollTo其实没有产生效果

if(isSmooth){

this.smoothScrollTo((page+1)width,0);

}else{

this.scrollTo((page+1)width,0);

}

}



@Override

protectedvoidonLayout(booleanchanged,intl,intt,intr,intb){

super.onLayout(changed,l,t,r,b);

scrollToPage(0,false);

}

2.添加小白点



小白点用来指示当前是哪张图片,位于中间下方,且不随图片滑动而移动。所以只能画在最上层的HorizontalScrollView上。这里我们在onDraw画控件函数中,直接在画版上画:



privatevoidinitPaint(){

mStrokePaint=newPaint();

//抗锯齿

mStrokePaint.setAntiAlias(true);

//空心线宽

mStrokePaint.setStrokeWidth(1.0f);

//中空

mStrokePaint.setStyle(Paint.Style.STROKE);

//颜色

mStrokePaint.setColor(Color.WHITE);



mFillPaint=newPaint();

mFillPaint.setAntiAlias(true);

mFillPaint.setStyle(Paint.Style.FILL_AND_STROKE);

mFillPaint.setColor(Color.WHITE);

}



@Override

protectedvoiddispatchDraw(Canvascanvas){

super.dispatchDraw(canvas);

intwidth=getWidth();



floatdensity=getContext().getResources().getDisplayMetrics().density;

//半径转换为像素

intradiusInPixel=(int)(CIRCLE_RADIUSdensity);

//白点间隔

intmargin=radiusInPixel;

//白点区域总宽度

inttotalWidth=radiusInPixel2mTotalSize+margin(mTotalSize-1);

//初始第一个点位置

intoffsetX=getScrollX()+width/2-totalWidth/2+radiusInPixel;

intoffsetY=(int)(getHeight()-density10-radiusInPixel);

//开始画点

for(inti=0;i
if(i==mCurrPage){

canvas.drawCircle(offsetX,offsetY,radiusInPixel,mFillPaint);

}else{

canvas.drawCircle(offsetX,offsetY,radiusInPixel,mStrokePaint);

}

offsetX+=radiusInPixel2+margin;

}

可以看到我代码中其实是使用dispatchDraw来画的,而不是上面说的onDraw函数。是因为我在实现时又踩了一个坑,因为View会先调用onDraw来画自己的东西,然后调用dispatchDraw去画孩子(当然,View没有孩子,这个只有在ViewGroup中才有用)



//Step3,drawthecontent

if(!dirtyOpaque)onDraw(canvas);



//Step4,drawthechildren

dispatchDraw(canvas);

如果在onDraw中去画点,则会被后来画的孩子遮挡住,这个可以将Container不添加到父节点中去来测试,可以看到我们画的圆。所以应该在dispatchDraw中,画完孩子然后去画点。

当然,在网上也看到如果没有背景会跳过onDraw直接调用dispatchDraw的说法,实验结果并不是这样。



3.自动滑动



这个比较简单,不过其中也碰到了一个坑,内存泄漏问题。这个可以看下前面的一个杂记:Android内存泄露杂记2016-02-26

具体就是匿名Runnable引用外部数据,后来使用WeakReference解决,代码如下:



publicstaticclassAutoPlayRunableimplementsRunnable{

privateWeakReferencereference=null;

publicAutoPlayRunablewww.baiyuewang.net(ADPageradPager){

reference=newWeakReference(adPager);

}

@Override

publicvoidrun(){

ADPageradPager=reference.get();

if(adPager!=null){

intpage=adPager.getCurrPage();

adPager.scrollToPage(page+1,true);

adPager.postDelayed(adPager.getAutoPlayRunnable(),AUTO_PLAY_DUATION);

}

}

}

现在广告就可以自动滚动起来了:

这里写图片描述



4.触摸事件



但是现在还无法通过触摸来顺畅控制广告移动,就和上图一样。因为HorizontalScrollView自己处理了触摸事件,通过手指来自由滑动。但这不是我们想要的结果,我们需要的是通过触摸,可以左右滑动,但超过一半,就应该显示下一张,或者没超过一半退回,而不是停在中间。然后就是手指滑动的够快,就算不超过一半也需要到下一张,就和有惯性一样。想要实现这样的结果,我们需要重写触摸事件处理,幸运的是,Android是支持这样做的。

这需要使用到几个触摸事件接口,并对其流程足够了解。一共涉及3个接口,如下:

View里,有两个回调函数:



1.publicbooleandispatchTouchEvent(MotionEventev);

2.publicbooleanonwww.wang027.comTouchEvent(MotionEventev);

ViewGroup里,有三个回调函数:



1.publicbooleandispatchTouchEvent(MotionEventev);

2.publicbooleanonInterceptTouchEvent(MotionEventev);

3.publicbooleanonTouchEvent(MotionEventev);

在Activity里,有两个回调函数:



1.publicbooleandispatchTouchEvent(MotionEventev);

2.publicbooleanonTouchEvent(MotionEventev);

事件传递默认是从父节点开始,直到传递到View。也就是说传递过程是Activity-ViewGroup-View。

触摸事件是由一系列的ACTION_DOWN、ACTION_MOVE…MOVE…MOVE、ACTION_UP的过程

对上面的接口来说,事件包含三个处理方式,一是分发(dispatchTouchEvent),二是拦截(onInterceptTouchEvent),一个是消费(onTouchEvent),并都有其返回值。



分发:dispatchTouchEvent返回true则顺序下发会中断(一般表示事件被消费),后续节点接收不到事件(分发是深度优先的),返回false继续分发

拦截:onInterceptTouchEvent比较复杂,returntrue可以拦截DOWN、MOVE、UP事件

拦截DOWN事件,则表示事件完全由当前ViewGroup来处理,后续MOVE、UP事件也会来找当前ViewGroup

拦截MOVE、UP事件,则表示后续事件由当前ViewGroup来处理,之前处理事件的View会收到一个ACTION_CANCEL事件

消费:onTouchEvent事件,如果前面没有被拦截:

View(如果可点击)默认返回true,表示消费事件

消费Down事件,则后续MOVE、UP事件都会来找当前View

没消费Down事件,则其他事件也没有你什么事了,不会传递给你的

许多事件依赖于onTouchEvent处理UP事件,如Click事件

配上伪代码:



ViewmTarget=null;//保存捕获Touch事件处理的View

publicbooleandispatchTouchEvent(MotionEventev){



//....其他处理,在此不管



if(ev.getAction()==KeyEvent.ACTION_DOWN){

//每次Down事件,都置为Null



if(!onInterceptTouchEvent()){

mTarget=null;

View[]views=getChildView();

for(inti=0;i
if(views[i].dispatchTouchEvent(ev))

mTarget=views[i];

returntrue;

}

}

}

//当子View没有捕获down事件时,ViewGroup自身处理。这里处理的Touch事件包含Down、Up和Move

if(mTarget==null){

returnsuper.dispatchTouchEvent(ev);

}

//...其他处理,在此不管

if(onInterceptTouchEvent()){

//...其他处理,在此不管

}

//这一步在Action_Down中是不会执行到的,只有Move和UP才会执行到。

returnmTarget.dispatchTouchEvent(ev);



}

更具体的知识储备:

Android中的dispatchTouchEvent()、onInterceptTouchEvent()和onTouchEvent()

Android:30分钟弄明白Touch事件分发机制



现在我们就可以来想上面的问题了:



很明显,ScrollView通过处理Move事件,来进行子View的滑动及动画,所以不能动这个事件;

然后还需要处理UP事件来确定手指抬起后,展示哪张图片;

最后,还需要得到用户手指滑动的速度,速度快则直接展示下一页;

还有别忘了,手指落下时移除自动播放,抬起时开始自动播放。(考虑一下,为什么移除自动播放不能放在onTouchEvent事件中去处理?)

最后代码如下:



@Override

publicbooleanonInterceptTouchEvent(MotionEventev){

switch(ev.getAction()){

caseMotionEvent.ACTION_DOWN:

//停止自动播放

removeCallbacks(mAutoPlayRunnable);

break;

}

returnsuper.onInterceptTouchEvent(ev);



}



@Override

publicbooleanonTouchEvent(MotionEventev){

if(mVelocityTracker==null){

mVelocityTracker=VelocityTracker.obtain();

}

mVelocityTracker.addMovement(ev);

switch(ev.getAction()){

caseMotionEvent.ACTION_CANCEL:

caseMotionEvent.ACTION_UP:

intscrollX=getScrollX();

intwidth=getWidth();

//计算手指每秒移动像素

mVelocityTracker.computeCurrentVelocity(1000,2000);

floatspeedX=mVelocityTracker.getXVelocity();

intpage;

if(Math.abs(speedX)>1000){

//移动速度够大

page=scrollX/width;

if(speedX>0){

page-=1;

}

}else{

//缓慢移动,按当前哪张图展示多就显示哪张

page=(int)Math.round(scrollX1.0/width)-1;

}

scrollToPage(page,true);

//开启自动播放

postDelayed(mAutoPlayRunnable,AUTO_PLAY_DUATION);

//直接返回,不让ScrollView处理事件

returntrue;

}

returnsuper.onTouchEvent(ev);

}

5、点击事件



这一步就很简单了,我们只需要给最下层的View添加点击事件就可以了。因为前面提到过的,View如果消费了UP事件用于Click事件,就不会传递给上层的ScrollView了。这里其实就也可以回答上面提出的问题,为什么不能在onTouchEvent中来取消自动播放,因为View默认会消费掉DOWN事件,是传递不到ScrollView中的。



四、结语



至此,广告栏Banner就已经做好了。其中涉及的各种知识,在自定义控件中,都是必须的,只有熟练掌握,才能写出属于自己的个性化控件,少踩几个坑。

献花(0)
+1
(本文系thedust79首藏)