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

  • onMeasure、onLayout、onDraw等View、ViewGroup相关布局函数;
  • dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent等触摸事件处理函数(包括Click等事件触发);

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

一、需求

  1. 控件每次展示一张图片,隔一段时间换播下一张,如果当前是最后一张则展示第一张;
  2. 用户手指触摸控件,停止轮播,用户滑动手指,则根据方向展示相应下一张或前一张图片;
  3. 如果是第一张图片继续往前滑动,需要展示最后一张图片,如果最后一张图片往后滑动,展示第一张图片;
  4. 最下方需要有小白点来指示当前是第几张图片;

最终效果如图:

二、初设计

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

  1. 布局
    HorizontalScrollView——LinearLayout——ImageView List
    同时需要在HorizontalScrollView上画小白点指示当前页

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

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

三、具体实现

1. 设计布局

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

public class ADPager extends HorizontalScrollView{private LinearLayout container = null;private LinearLayout.LayoutParams imgLayoutParams = null;public ADPager(Context context) {super(context);init();}private void init(){Context ctx = getContext();container = new LinearLayout(ctx);ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);container.setLayoutParams(layoutParams);//横向布局container.setOrientation(LinearLayout.HORIZONTAL);imgLayoutParams = new LinearLayout.LayoutParams(getWidth(),getHeight());this.addView(container);this.setSmoothScrollingEnabled(true);//不显示滑动条this.setHorizontalScrollBarEnabled(false);}public void setImageList(int imgArray[]){int size = imgArray.length;if(size > 1){//如果大于一张图片,第一张前放最后一张图片this.container.addView(makeImageView(imgArray[size - 1]));}for(int imgId:imgArray){this.container.addView(makeImageView(imgId));}if(size > 1){//如果大于一张图片,最后一张后放第一张图片this.container.addView(makeImageView(imgArray[0]));}}public ImageView makeImageView(int resourceId){ImageView imageView;Context ctx = getContext();imageView = new ImageView(ctx);imageView.setImageResource(resourceId);imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);imageView.setLayoutParams(imgLayoutParams);return imageView;}

在onCreate中调用初始化图片:

    int imgIdArray[] = {R.drawable.img1,R.drawable.img2,R.drawable.img3};ADPager adPager = (ADPager)findViewById(R.id.adpager);adPager.setImageList(imgIdArray);

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

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

    @Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);imgLayoutParams.width = getMeasuredWidth();imgLayoutParams.height = getMeasuredHeight();}

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

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

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

    public void scrollToPage(int page,boolean isSmooth){if(page < 0){page = mTotalSize - 1;}else if(page >= mTotalSize){page = 0;}//设置当前页mCurrPage = page;int width = getWidth();//因为第一张前面加了一张,所以页数需要+1。而只有一张图片时,scrollTo其实没有产生效果if(isSmooth){this.smoothScrollTo((page + 1) * width, 0);}else{this.scrollTo((page + 1) * width, 0);}}@Overrideprotected void onLayout(boolean changed, int l, int t, int r, int b) {super.onLayout(changed, l, t, r, b);scrollToPage(0,false);}

2. 添加小白点

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

    private  void initPaint(){mStrokePaint = new Paint();//抗锯齿mStrokePaint.setAntiAlias(true);//空心线宽mStrokePaint.setStrokeWidth(1.0f);//中空mStrokePaint.setStyle(Paint.Style.STROKE);//颜色mStrokePaint.setColor(Color.WHITE);mFillPaint = new Paint();mFillPaint.setAntiAlias(true);mFillPaint.setStyle(Paint.Style.FILL_AND_STROKE);mFillPaint.setColor(Color.WHITE);}@Overrideprotected void dispatchDraw(Canvas canvas) {super.dispatchDraw(canvas);int width = getWidth();float density = getContext().getResources().getDisplayMetrics().density;//半径转换为像素int radiusInPixel = (int)(CIRCLE_RADIUS * density);//白点间隔int margin = radiusInPixel;//白点区域总宽度int totalWidth = radiusInPixel * 2 * mTotalSize + margin * (mTotalSize - 1);//初始第一个点位置int offsetX = getScrollX() + width / 2 - totalWidth / 2 + radiusInPixel;int offsetY = (int)(getHeight() - density * 10 - radiusInPixel);//开始画点for(int i = 0;i < mTotalSize; i++){if(i == mCurrPage){canvas.drawCircle(offsetX,offsetY,radiusInPixel,mFillPaint);}else{canvas.drawCircle(offsetX,offsetY,radiusInPixel,mStrokePaint);}offsetX += radiusInPixel * 2 + margin;}}

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

// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);// Step 4, draw the children
dispatchDraw(canvas);

如果在onDraw中去画点,则会被后来画的孩子遮挡住,这个可以将container不添加到父节点中去来测试,可以看到我们画的圆。所以应该在dispatchDraw中,画完孩子然后去画点。
       当然,在网上也看到如果没有背景会跳过onDraw直接调用dispatchDraw的说法,实验结果并不是这样。

3. 自动滑动

这个比较简单,不过其中也碰到了一个坑,内存泄漏问题。这个可以看下前面的一个杂记:Android内存泄露杂记2016-02-26
       具体就是匿名Runnable引用外部数据,后来使用WeakReference解决,代码如下:

    public static class AutoPlayRunable implements Runnable{private WeakReference<ADPager> reference = null;public AutoPlayRunable(ADPager adPager){reference = new WeakReference<ADPager>(adPager);}@Overridepublic void run() {ADPager adPager = reference.get();if(adPager != null){int page = adPager.getCurrPage();adPager.scrollToPage(page + 1,true);adPager.postDelayed(adPager.getAutoPlayRunnable(),AUTO_PLAY_DUATION);}}}

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

4.触摸事件

但是现在还无法通过触摸来顺畅控制广告移动,就和上图一样。因为HorizontalScrollView自己处理了触摸事件,通过手指来自由滑动。但这不是我们想要的结果,我们需要的是通过触摸,可以左右滑动,但超过一半,就应该显示下一张,或者没超过一半退回,而不是停在中间。然后就是手指滑动的够快,就算不超过一半也需要到下一张,就和有惯性一样。想要实现这样的结果,我们需要重写触摸事件处理,幸运的是,Android是支持这样做的。
       这需要使用到几个触摸事件接口,并对其流程足够了解。一共涉及3个接口,如下:
View里,有两个回调函数 :

1. public boolean dispatchTouchEvent(MotionEvent ev);
2. public boolean onTouchEvent(MotionEvent ev);

ViewGroup里,有三个回调函数 :

1. public boolean dispatchTouchEvent(MotionEvent ev);
2. public boolean onInterceptTouchEvent(MotionEvent ev);
3. public boolean onTouchEvent(MotionEvent ev);

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

1. public boolean dispatchTouchEvent(MotionEvent ev);
2. public boolean onTouchEvent(MotionEvent ev);

事件传递默认是从父节点开始,直到传递到View。也就是说传递过程是Activity-ViewGroup-View。
       触摸事件是由一系列的ACTION_DOWN、ACTION_MOVE…MOVE…MOVE、ACTION_UP的过程
       对上面的接口来说,事件包含三个处理方式,一是分发(dispatchTouchEvent),二是拦截(onInterceptTouchEvent),一个是消费(onTouchEvent),并都有其返回值。

  • 分发: dispatchTouchEvent返回true则顺序下发会中断(一般表示事件被消费),后续节点接收不到事件(分发是深度优先的),返回false继续分发
  • 拦截: onInterceptTouchEvent比较复杂,return true可以拦截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事件

配上伪代码:

View mTarget=null;//保存捕获Touch事件处理的View
public boolean dispatchTouchEvent(MotionEvent ev) {//....其他处理,在此不管if(ev.getAction()==KeyEvent.ACTION_DOWN){//每次Down事件,都置为Nullif(!onInterceptTouchEvent()){mTarget=null;View[] views=getChildView();for(int i=0;i<views.length;i++){if(views[i].dispatchTouchEvent(ev))mTarget=views[i];return true;}}}//当子View没有捕获down事件时,ViewGroup自身处理。这里处理的Touch事件包含Down、Up和Moveif(mTarget==null){return super.dispatchTouchEvent(ev);}//...其他处理,在此不管if(onInterceptTouchEvent()){//...其他处理,在此不管}//这一步在Action_Down中是不会执行到的,只有Move和UP才会执行到。return mTarget.dispatchTouchEvent(ev);}

更具体的知识储备:
Android中的dispatchTouchEvent()、onInterceptTouchEvent()和onTouchEvent()
Android:30分钟弄明白Touch事件分发机制

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

  • 很明显,ScrollView通过处理Move事件,来进行子View的滑动及动画,所以不能动这个事件;
  • 然后还需要处理UP事件来确定手指抬起后,展示哪张图片;
  • 最后,还需要得到用户手指滑动的速度,速度快则直接展示下一页;
  • 还有别忘了,手指落下时移除自动播放,抬起时开始自动播放。(考虑一下,为什么移除自动播放不能放在onTouchEvent事件中去处理?)

最后代码如下:

    @Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) {switch (ev.getAction()){case MotionEvent.ACTION_DOWN://停止自动播放removeCallbacks(mAutoPlayRunnable);break;}return super.onInterceptTouchEvent(ev);}@Overridepublic boolean onTouchEvent(MotionEvent ev) {if(mVelocityTracker == null){mVelocityTracker = VelocityTracker.obtain();}mVelocityTracker.addMovement(ev);switch (ev.getAction()){case MotionEvent.ACTION_CANCEL:case MotionEvent.ACTION_UP:int scrollX = getScrollX();int width = getWidth();//计算手指每秒移动像素mVelocityTracker.computeCurrentVelocity(1000,2000);float speedX = mVelocityTracker.getXVelocity();int page;if(Math.abs(speedX) > 1000){//移动速度够大page = scrollX / width;if(speedX > 0){page -= 1;}}else{//缓慢移动,按当前哪张图展示多就显示哪张page = (int)Math.round(scrollX * 1.0 / width) - 1;}scrollToPage(page, true);//开启自动播放postDelayed(mAutoPlayRunnable,AUTO_PLAY_DUATION);//直接返回,不让ScrollView处理事件return true;}return super.onTouchEvent(ev);}

5、点击事件

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

四、结语

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

PS:推荐一个好的看Android原码网站:
http://grepcode.com/

[Android]使用HorizontalScrollView实现广告栏Banner及相关原理分析相关推荐

  1. android7.0 进程管理,Android 7.0 ActivityManagerService(8) 进程管理相关流程分析(2) updateOomAdjLocked...

    前一篇博客进程管理相关流程分析(1)里, 我们介绍了AMS中updateLruProcessLocked函数相关的流程. updateLruProcessLocked只是按照进程中运行的组件,粗略地定 ...

  2. Android免Root环境下Hook框架Legend原理分析

    0x1 应用场景 现如今,免Root环境下的逆向分析已经成为一种潮流! 在2015年之前的iOS软件逆向工程领域,要想对iOS平台上的软件进行逆向工程分析,越狱iOS设备与安装Cydia是必须的!几乎 ...

  3. Android 4.4 NotificationManagerService使用详解与原理分析(二)__原理分析

    前置文章: <Android 4.4 KitKat NotificationManagerService使用详解与原理分析(一)__使用详解> 转载请务必注明出处:http://blog. ...

  4. android 动画 最顶层_【Android编程实战】StrandHogg漏洞复现及原理分析_Android系统上的维京海盗...

    0x00 StrandHogg漏洞详情 StrandHogg漏洞 CVE编号:暂无 [漏洞危害] 近日,Android平台上发现了一个高危漏洞 该漏洞允许攻击者冒充任意合法应用,诱导受害者授予恶意应用 ...

  5. Android 7.0 ActivityManagerService(5) 广播(Broadcast)相关流程分析

    本篇博客旨在分析Android中广播相关的源码流程. 一.基础知识 广播(Broadcast)是一种Android组件间的通信方式. 从本质上来看,广播信息的载体是intent.在这种通信机制下,发送 ...

  6. 58同城Android端-最小插件化框架实战和原理分析

    目录 背景 插件化需要了解的知识 2.1 类加载过程和类加载器 2.2 ClassLoader 的 findClass.findLibrary.findResource 2.3 DexClassLoa ...

  7. Android startForeground 却无notification的黑科技原理分析 以及Android7.1的修复

    契机:今天升级了Android7.1 beta版.然而升上去之后,国产的部分App简直丑态百出啊,给各位看看我的手机截图 啧啧,原来Android7.0以及以下干干净净的通知栏瞬间被这几个家伙占满.有 ...

  8. Android OTA在线升级二(升级包编译原理分析) 【转】

    本文转载自:http://blog.csdn.net/huryjiang/article/details/7590015 1 升级包的制作 基本命令: Ø  makeMtk [project[flav ...

  9. Android 系统性能优化(21)---App启动原理分析及启动时间优化

    一.启动原理解析 Android是基于Linux内核的,当手机启动,加载完Linux内核后,会由Linux系统的init祖先进程fork出Zygote进程,所有的Android应用程序进程以及系统服务 ...

最新文章

  1. sudo运行程序遇到的问题
  2. 机器人学习--栅格地图(occupancy grid map)构建
  3. mysql 数据分析的步骤_数据分析8个主要步骤
  4. 在windows下使用Xming+Putty显示Linux下软件图形界面
  5. iPhone 12/13正面对比图曝光:祖传大刘海终于有所改变
  6. 如何使用HttpContext对象
  7. 智力问答 46倒计时
  8. onfling滑动界面进行Activity切换
  9. 用js把数据从一个页面传到另一个页面
  10. 内容查询部件 之 简单美化
  11. 1.1 echo rem cd dir命令
  12. ZTree async中文乱码,ZTree reAsyncChildNodes中文乱码,zTree中文乱码
  13. CGLIB实现AOP,MethodInterceptor接口和Enhancer详解——Spring AOP(四)
  14. 使用oprofile查找性能瓶颈
  15. 用c语言写图书管理系统设计,C语言图书管理系统设计及实现.doc
  16. 发明专利授权:顺源科技模拟信号隔离放大及转换技术
  17. Android Studio仿QQ界面实现简单的功能
  18. 转向新页,控制页面大小并传值
  19. Token的简单解释
  20. 京东容器集群建设之路

热门文章

  1. 在Matlab中设计梳状滤波器
  2. Vite+ts全局引入scss与实际使用
  3. 靳小强计算机类专场讲座直播视频,靳小强:专业的意义
  4. STC 51单片机46——看门狗测试
  5. 【黑客盯上你了】黑客是如何攻击你的
  6. 软考-软件设计师 - 第9章 数据库技术基础【附补充常考知识点】
  7. 自学编程,看书还是视频?为你分析利弊
  8. 零伽壹解析 | 一份来自区块链行业的《高考志愿填报指南》
  9. html 设计好看的按钮,2款创意CSS3按钮设计效果
  10. windows操作系统未启用登录失败处理功能引发的威胁