Android自定义View:View(二)

什么?你说你掌握了自定义View?来来来,试着回答以下问题:java

  • Google提出View这个概念的目的是什么?
  • View这个概念与Activtiy、Fragment以及Drawable之间是一种什么样的关系?
  • View可以感知Activity的生命周期事件吗?为何?

什么?你说这些问题太抽象?来来来,继续回答以下问题:android

  • View的生命周期是什么?
  • 当View所在的Activity进入stop状态后,View去哪了?若是我在一个后台线程中持有一个View的引用,我此时可以改变它的状态吗?为何?
  • View可以与其余的View交叉重叠吗?重叠区域发生的点击事件交给谁去处理呢?可不能够重叠的两个View都处理?
  • View控制一个Drawable的方法途径有哪些?Drawable能不能与View通讯?若是能如何通讯?
  • 假如View所在的ViewGroup中的子View减小了,View所以得到了更大的空间,View如何及时有效地利用这些空间,改变本身的绘制?
  • 假如我要在View中动态地注册与解除广播接收器,应该在哪里完成呢?
  • 假如个人手机带键盘(自带或者外接),你的自定义View应该如何响应键盘事件。
  • AnimationDrawable做为View的背景,会自动进行动画,View在其中扮演了怎样的角色?

其实,说了这么多,到底怎样才能学好自定义View?其实只需掌握三个问题,就能够轻松搞定它:bash

  • 问题一:从Android系统设计者的角度,View这个概念到底是作什么的?
  • 问题二:Android系统中那个View类,它有哪些默认功能和行为,能干什么,不能干什么?(知己知彼,才好自定义!)
  • 问题三:我要改变这个View的行为,外观,确定是覆写View类中的方法,可是怎么覆写,覆写哪些方法可以改变哪些行为?

从Android系统设计者的角度,View这个概念到底是作什么的?

关于这个问题,最权威的固然是官方文档,以下:数据结构

This class represents the basic building block for user interface components. A View occupies a rectangular area on the screen and is responsible for drawing and event handling.ide

可见这句话,包含三层含义:布局

  • View是用户接口组件的基本构建块。通俗讲,在Android中,一个用户与一个应用的交互,其实就是与这个应用中的许许多多的View的交互,这些View既能够是简单的View,也能够是若干View组合而成的一个ViewGroup。由此咱们能够明白,所谓View是基本构件块,缘由就在于它是复合View(就是ViewGroup)的基本组成单元。这层含义,就是告诉你,View就是用来与用户交互的,那么很天然地,咱们要问,咱们用户在哪里与View交互,以及怎样与View交互呢?post

  • View在屏幕上占据一个矩形区域。这是说,既然View是用户与应用交互的基本构建块,而用户使用Android设备时,主要是经过一个触摸屏来交互的,相应的,Andorid的设计者们,就让一个View就在屏幕上占据一个矩形区域,用户在这个区域中发生的交互动做(点击、滑动、拖动等),就是与这个View的交互。什么?为何不让View占据一个圆形区域或者五角星区域呢?固然是为了简单。这就解决了在哪里与View交互的问题。很天然地,咱们又想问,View在屏幕上占据一个矩形区域,这个区域的大小、位置怎么肯定,它们会不会变化,谁来决定这个变化呢?若是这个变化不是由View本身来决定的,而是其余外界因素决定的,View又要怎样响应这种变化呢?不要急,后面都会有答案。动画

  • View经过绘制本身与事件处理两种方式与用户交互。这是解决了如何交互的问题。简单讲,View与用户交互就两个办法,一个是改变本身的模样,也就是经过绘制本身与用户交互,好比,当用户点击本身时,就改变本身的背景颜色,以此来告诉用户:“本View已经响应你的点击了!”第二个方式就是事件处理,好比,当用户点击View时,就完成必定的任务,而后弹出一个Toast,告诉用户该View完成了什么任务,这样,用户也就知道此次交互结果如何。ui

如今咱们明白了,设计View,主要是为了让应用可以与用户交互,要想完成交互,这个View就要在屏幕上占据一个矩形区域,而后利用这块屏幕区域与用户交互,交互的方式就两种,绘制本身与事件处理。spa

Android系统中的View类,它有哪些默认功能和行为?能干什么,不能干什么?

解决了第一个问题,咱们极可能有更多的疑问,咱们想知道:

  • View是怎样被显示到屏幕上的?
  • View在屏幕上的位置是怎样决定的?
  • View所占据的矩形大小是怎样决定的?
  • 屏幕上确定不止一个View,View之间互相知道对方吗?它们之间能协做吗?
  • View完成与用户的交互后,可以自动隐藏,在须要交互的时候从新显示在屏幕上吗?

首先,一个用户界面,上面有许多View,既有基本View,也有复合View,把它们组织起来还让它们很好地协做确实是一个难题,Google的解决方案是:首先,一套完整的用户界面用一个Window来表示,Window这个概念和咱们在计算机上所说的Window很类似。Window负责管理全部的View们,怎么管理?很简单,借鉴复合View的思路,Window首先加载一个超级复合View,用它来包含全部的其余View,这个超级复合View就叫作DecorView。可是这个DecorView除了包含咱们的用户界面上那些View,还包含了做为一个Window特有的View,叫作titlebar,这个咱们就不细说了。

这样,在Window中的View们被组织起来了,造成一个巨大的ViewGroup,下面又有若干ViewGroup和若干View,每一个ViewGroup下面又有若干ViewGroup和若干View,很像数据结构中的树,叶子节点就是基本View。

好了,这些View已经被组织起来了,DecorView已经可以彻底控制它们了,同时,DecorView掌握着可以分配给这些View的屏幕区域,包括区域的大小和位置。

咱们知道,屏幕的大小是有限的,一个Window的DecorView可以控制的屏幕区域更加有限,AndroidN中引入多Window机制后,DecorView能掌控的屏幕区域更加小了,由于屏幕上有多个Window将成为常态。这些有限的区域还要被Window特有的View(titlebar)占去一小部分,剩下的才是留给用户界面上的View们分的,若是你是DecorView,你确定为难了,如何将这些有限的屏幕区域分给这些View们?分给他们后还得为每一个View排好在屏幕上的位置,难上加难。

停一停,想想,若是是你,你怎么解决这个问题?

首先,不一样的View是为了完成特定的交互任务的,好比,Button就是用来点击的,TextView就是用来显示字符的,等等。

DecorView知道,不一样的View为了完成本身的交互任务所须要的屏幕区域大小是不一样的,因此DecorView在肯定给每一个View分配的屏幕区域大小时,是容许View参与进来,与它一块儿商量的。可是每一个View在屏幕区域中的位置就不能让View本身来决定了,而是由DecorView一手操办,这个比较简单,咱们就先来看看DecorView是怎样决定每一个View的位置的吧。

肯定每一个View的位置

咱们在Activity中,调用了setContentView(View),实际上就是将用户界面的全部的View交给了DecorView中的一个FrameLayout,这个FrameLayou表明着能够分配给用户界面使用的屏幕区域。而用户界面View既能够是一个简单的View,也能够是一个ViewGroup,若是是一个简单的View,好比就是一个TextView,那么这个TextView就会占据整个FrameLayout的屏幕区域,也就是说,此时用户在FrameLayout的屏幕区域内的全部交互都是与这个TextView交互。可是更常见的状况时,咱们的用户界面是一个ViewGroup(想一想经常使用的布局五大金刚),里面包含着其余的ViewGroup和View。这个时候,首先这个ViewGroup就会占据FrameLayout所表明的屏幕区域,剩下的任务,就是这个ViewGroup给它内部的小弟们(各类ViewGroup和各类View)分配区域了。至于怎么分,不一样的ViewGroup有不一样的分法,整体来看,可说是有总有分。所谓总,举例来说,像vertical的LinearLayout,它按照 本身的小弟数量,把本身竖向裁成不一样的区域,以下图所示:

vertical_linearLayout

虽然View没法决定本身在ViewGroup中的位置,可是开发者在使用View时,能够向ViewGroup表达本身所用的View要放在哪里,以vertical LinearLayout为例,开发者书写布局文件时,子View在LinearLayout中的出现顺序将决定它们在屏幕上的上下顺序,同时还能够借助layout_margin ,layout_gravity等配置进一步调整子View在分给本身的矩形区域中的位置。

咱们能够理解,layout_*之类的配置虽然在书写上与View的属性在一块儿,但它们并非View的属性,它们只是使用该View的使用者用来细化调整该View在ViewGroup中的位置的,同时,这些值在Inflate时,是由ViewGroup读取,而后生成一个ViewGroup特定的LayoutParams对象,再把这个对象存入子View中的,这样,ViewGroup在为该子View安排位置时,就能够参考这个LayoutParams中的信息了。进一步思考,咱们发现,调用inflate时,除了输入布局文件的id外,通常要求传入parent ViewGroup,传入这个参数的目的,就是为了读取布局文件中的layout配置信息,若是没有传入,这些信息将会丢失。

不一样的ViewGroup拥有不一样的LayoutParams内部类,这是由于,它们容许子View调整本身的位置的方式是不同的,具体讲就是配置子View时,容许使用的layout_*是不同的,好比,RelativeLayout就容许layout_toRightOf等配置,其余的ViewGroup没有这些配置。

肯定View位置的过程,是被包装在View 的layout方法中,这样也很容易理解,对于基本View而言,这个方法是没有用的,因此都是空的,你能够查看下ImageView、TextView等的源代码,验证下这一点。对于ViewGroup而言,它们会用该方法为本身的子View安排位置。

肯定View大小

要肯定View的大小,这是一个开发者ViewViewGroup三方相互商量的过程。

  • 第一步,开发者在书写布局文件时,会为一个View写上android:layout_width="\*\*\*" android:layout_height="\*\*\*"两个配置,这是开发者向ViewGroup表达的,我这个View须要的大小是多少。星号的取值有三种:

    • 具体值:如50dp,很简单,很少讲
    • match_parent:表示开发者向ViewGroup说,把你全部的屏幕区域都给这个View吧。
    • wrap_parent:表示开发者向ViewGroup说,只要给这个View够他展现本身的空间就行,至于到底给多少,你直接跟View沟通吧,看它怎么说。
  • 第二步,ViewGroup收到了开发者对View大小的说明,而后ViewGroup会综合考虑本身的空间大小以及开发者的请求,而后生成两个MeasureSpec对象(width与height)传给View,这两个对象是ViewGroup向子View提出的要求,就至关于告诉子View:“我已经与你的使用者(开发者)商量过了,如今把咱们商量肯定的结果告诉你,你的宽度不能违反width MeasureSpec对象的要求,你的高度不能违反height MeasureSpec对象的要求,如今,你赶忙根据这个要求肯定下本身要多大空间,只许少,不准多哦。

而后,这两个MeasureSpec对象将会传到子Viewprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法中。子View能怎么办呢?它确定是要先看看ViewGroup的要求是什么吧,因而,它从传入的两个对象中解译出以下信息:

int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize =  MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize =  MeasureSpec.getSize(heightMeasureSpec);
复制代码

Mode与Size一块儿,准确表达出了ViewGroup的要求。下面咱们举例说明,假设Size是100dp, Mode的取值有三种,它们表明了ViewGroup的整体态度:

  • EXACTLY表示,ViewGroup对View说,你只能用100dp,缘由是多样的,多是你的使用者说要你彻底占据个人空间,而我只有100dp。也可能这是你的使用者的要求,他须要你占这么大的空间,而我刚好也有这么多的空间,你的使用者让你占这么大的空间,确定有他本身的考虑,你不能不理不顾,否则你达不到他的要求,他可能就不用你了。
  • AT_MOST表示,你最多只能用100dp,这是由于你的使用者说让你占据wrap_content的大小,让我跟你商量,我又不知道你到底要占多大区域,可是我告诉你,我只有100dp,你最多也只能用这么多哈。(这里,能够看出,当使用者在布局文件中要求一个View是wrap_content时,此时,View的大小决定权就交给View本身了,默认的View类中的实现,比较粗暴,就是将此时ViewGroup提供的空间全占据,彻底没有真正根据本身的内容来肯定大小,为何这么粗暴?由于View是一个基类,全部的组件都是它的子类,每一个子类的content都各不相同,View怎么可能知道content的大小呢,因此,它把wrap_content状况下,本身尺寸大小的决定权下放给了不一样的子组件,让它们本身根据本身的内容去决定本身的大小,一样,咱们自定义View时,也要考虑这一点)
  • UNSPECIFIED表示,你本身看着办,把你最理想的大小告诉我,我考虑考虑。
  • 第三步,好了,子View已经清楚地理解了ViewGroup和它的使用者对它的大小的指望和要求了。下步就要在该要求下来肯定本身的大小并告诉ViewGroup了。(废话,不告诉ViewGroup大小,它怎么给你安排位置(layout),没法给你layout,你也就占据不了一块屏幕区域,占不了屏幕区域,你就没法与用户交互,没法与用户交互,要你何用啊!)

关于子View怎么肯定本身的大小,不一样的View有不一样的态度,可是有几点基本的规矩是要遵照的:

  • 规矩一就是,不要违反ViewGroup的规定,最后设置的尺寸必定要在ViewGroup要求的范围内(不管是宽度仍是高度),可是你说,假如我就是想要更大的空间,难道就没有办法了吗,我能不能遵照要求的状况下,同时告诉ViewGroup,虽然我告诉你的我要求的尺寸是遵守你的旨意来的,但实际上我是委屈求全的,我真实想要的大小不是这样的,你能不能再考虑一下。答案是:有。那就是以下调用:
resolveSizeAndState((int)(wantedWidth), widthMeasureSpec, 0);

resolveSizeAndState((int) (wantedHeight), heightMeasureSpec, 0);
复制代码

View能够把本身想要的宽和高进行一个resolveSizeAndState处理, 就能够达到上述目的。即若是想要的大小没超过要求,一切都Ok,若是超过了,在该方法内部,就会把尺寸调整成符合ViewGroup要求的,可是也会在尺寸中设置一个标记,告诉ViewGroup,这个大小是子View委屈求全的结果。至于ViewGroup会不会理会这一标记,要看不一样的ViewGroup了。若是你实现本身的ViewGroup,最好仍是关注下这个标记,毕竟做为大哥的你,最主要的职责就是把本身的小弟(子View)安排好,让它们都满意嘛。(这一点,我没有看到任何一篇讲解自定义View的文章提到过!) 什么?好奇的你想看看到底是怎样设置标记的?来来来,知足你:

public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {  

     final int specMode = MeasureSpec.getMode(measureSpec);  
     final int specSize = MeasureSpec.getSize(measureSpec);  
     final int result;  
    
     switch (specMode) {     
        case MeasureSpec.AT_MOST:         
            if (specSize < size) {            
                 result = specSize | MEASURED_STATE_TOO_SMALL;
    
            } else {            
                 result = size;      
            }         
            break;   
            
        case MeasureSpec.EXACTLY:          
             result = specSize;      
             break;   
             
        case MeasureSpec.UNSPECIFIED:   
        
        default:        
             result = size;   
      }   
      return result | (childMeasuredState & MEASURED_STATE_MASK);
}
复制代码

上面的代码中的MEASURED_STATE_TOO_SMALL就是在子View想要的空间太大时设置的标记了。

  • 规矩二就是要在该方法中调整本身的绘制参数,这一点很好理解,毕竟ViewGroup提出了尺寸要求,要及时根据这一要求调整本身的绘制,好比,若是本身的背景图片太大,那就算算要缩放多少才合适,而且设置一个合理的缩放值。

  • 规矩三就是必定要设置本身考虑后的尺寸,若是不设置就至关于没有告诉ViewGroup本身想要的大小,这会致使ViewGroup没法正常工做,设置的办法就是在onMeasure方法的最后,调用 setMeasuredDimension方法。为何调用这个方法就能够了呢?这只是一个约定,没有必要深究了。

View的绘制

关于View的绘制,很是简单,就是一个方法onDraw。

以上,View的三个基本知识点,咱们都了解了,即View 的位置如何肯定,大小如何肯定以及如何绘制本身。这都是默认的View类中为咱们准备好的。

我要改变这个View的外观和行为,确定是覆写View类中的方法,可是怎么覆写,覆写哪些方法可以改变哪些行为?

好了,View的位置和大小怎么肯定咱们都清楚了,如今,是时候开始自定义View了。

首先,关于View所要具有的通常功能,View类中都有了基本的实现,好比肯定位置,它有layout方法,固然,这个只适用于ViewGroup,实现本身的ViewGroup时,才须要修改该方法。肯定大小,它有onMeasure方法,若是你不满意默认的确认大小的方法,也能够本身定义。改变默认的绘制,就覆写onDraw方法。下面,咱们经过一张图,来看看,自定义View时,咱们最可能须要修改的方法是哪些:

custom_view_override_mothed

把这些方法都搞明白了,你也就理解了View的生命周期了。

好比View被inflated出来后,系统会回调该View的onFinishInflate方法,你的View能够在这个方法中,作一些准备工做。

若是你的View所属的Window可见性发生了变化,系统会回调该View的onWindowVisibilityChanged方法,你也能够根据须要,在该方法中完成必定的工做,好比,当Window显示时,注册一个监听器,根据监听到的广播事件改变本身的绘制,当Window不可见时,解除注册,由于此时改变本身的绘制已经没有意义了,本身也要跟着Window变成不可见了。

当ViewGroup中的子View数量增长或者减小,致使ViewGroup给本身分配的屏幕区域大小发生变化时,系统会回调View的onSizeChanged方法,该方法中,View能够获取本身最新的尺寸,而后根据这个尺寸相应调整本身的绘制。

当用户在View所占据的屏幕区域发生了触摸交互,系统会将用户的交互动做分解成如DOWN、MOVE、UP等一系列的MotionEvent,而且把这些事件传递给View的onTouchEvent方法,View能够在这个方法中进行与用户的交互处理。固然这个是基本的流程,实际的流程会稍复杂些。

除了这些方法,View还实现了三个接口,以下:

  • Drawable.Callback:是用来让View中的Drawable可以与View通讯的,尤为是AnimationDrawable,更是必须依赖该回调才能实现动画效果。
  • KeyEvent.Callback:是用来处理键盘事件的,这与onTouchEvent用来处理触摸事件是相对的。
  • AccessibilityEventSource

目录结构

参考文章

相关文章
相关标签/搜索