换个姿势聊聊自定义 View

你真的了解自定义 View 吗?

  自定义 View 在 Android 中是一个老生常谈的问题了,Google 百度 随便一搜都是一大堆。不置可否,其中有些文章确实讲解详细透彻值得细细品味,但往往我们是比葫芦画瓢,学习了作者的姿势却没有学习到作者在做这些姿势的本质。
  到这里你可能就不服啦!我可是苦练了十八般姿势,什么方形的,五角形的 View 都分分钟给你定义出来。
  好好,那我们就先来看几个问题?

  • Google 为什么会提出 View 概念?它的作用是什么?
  • View 的大小,在屏幕中的位置由谁控制?怎么控制?
  • View 默认行为和功能有哪些?它可以做什么?不能做什么
  • View 的生命周期是什么?它能不能感知到 Activity 的生命周期?
  • 当 Activity 处于 stop 状态时,View 去那了?如果一个后台线程持有该View的引用,现在能够改变View的状态?
  • View能够与其他View重叠吗?重叠区域的点击事件谁处理?能不能重叠的两个View都处理?View 之间能相互感知吗?
  • View 与 Drawable 的关系?两者能否通信?如果能,通过什么方式通信?
  • AnimationDrawable 作为 View 的背景,会自动进行动画,View 在其中扮演了怎样的角色?
    如果你能够准确的回到出以上的问题,那么恭喜你,你对自定义View的理解已炉火纯青,也就不必浪费时间再继续往下看了。(大牛,走好,常来哈!),如果你对某些问题的答案还比较模糊,那我们就一起针对以上的问题挨个解答一下。

Google 为什么提出 View 概念?它的作用是什么?

  想最快的认识一个类(包括很多其他事物),最好的方法莫过于查看官方文档了。Google 是这样介绍 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. View is the base class for widgets, which are used to create interactive UI components (buttons, text fields, etc).

  这段话可谓言简意赅,至少向我们表达了三个意思:
  

  • View是用户界面组件的基本构建块。也就是说我们平时用 Android 手机所看到的复杂或简单的界面都是由一个或多个View组合而成。那么问题就来了,View 是如何确定大小,确定显示的位置的呢?我们稍后道来。
  • View 会占据屏幕的一个矩形区域,并且负责绘制和事件处理。想一想,View 是如何绘制自身的及怎样去处理事件?(请不要问为什么是矩形而不是五角星一类的/(ㄒoㄒ)/~~)。
  • View 通过 绘制和事件处理两种方式来和用户交互。这个简单好理解,比如:TextView 绘制几个文字我们便可以从中读取到一些信息;点击 Button 处理回调中的方法及执行了我们的“命令”。

View有什么功能?我们可以用来做什么?

  通过以上我们了解到,View 是用来构建 Android 可交互界面的,并且留下很多疑问。我们来一一进行解答!
  
  既然我们看到的界面是多个 View 组合而成,那 View 的大小和位置是怎么确定的呢?
  大家可以想一想,如果让你来为一帮小弟分配钱财,你会怎么办?
  Google 是怎么来解决的:首先一个完整的用户界面用一个 Window 来表示,(小弟们同属 Window 黑帮呦)。Window会加载一个超级复合View(DecorView,小弟们的Boss ),用该View来包裹其他所有View(TitleBar这里先忽略)。这样整个 Window 中的 View 便被统一起来了,整个像一个树结构,DecorView 对应根节点,叶子节点就是每个基本View 了。
   不同功能的View是需要占据不同的大小屏幕尺寸的,DecorView是深知这点的(想要小弟们团结,在分配资源的时候就需要根据某权重来划分比例哦)。所以在计算 View 的大小时,是需要 View 参与的,而在确定 View 的位置时则是由 ViewGroup 一手操办的。

ViewGroup 怎么来确定子 View 的位置呢?

  在Activity中,我们调用 setContentView()实际是将我们的 View 全部交给 Window 的一个 FrameLayout。通常我们是用一个 ViewGroup 包裹子 View\ViewGroup(想一想我们的五大布局),至于 ViewGroup 怎么来对子View进行位置的安排,不同的 ViewGroup 有不同的规则。以 LinearLayout 为例,如果设置为纵向排列,则 LinearLayout 就会让子View 从上到下的挨个排列。
  虽然 View 在不能决定 View 在父布局中的位置,但是开发者在使用View的时候可以向父布局表达自己希望View显示的位置。比如:在 LinearLayout 中可以通过改变 View的次序来改变显示的位置,通过 layout_margin,layout_gravity 来对位置进行微调。由此可以看出,layout_* 此类配置虽然是写在View里的,但却不是View的属性。此类配置是让View的父布局对View的位置进行调整的。同时这类配置在 inflate 的时候会被 ViewGroup 读取生产 LayoutParams.

View怎么来确定大小呢?

  上边其实说到,View的大小 ViewGroup 不能一手做主,View 呢也不能胡作非为。其实 View 的大小是由三方面共同决定的:开发者,ViewGroup,View自身。(很对博文讲解中是没有开发者的,原理一样,这里只是思考的角度略有不同)。

第一步:开发者表达期望

开始的时候在布局文件里为View设置 layout_width=”##” layout_height=”##”,这是开发者向ViewGroup表达的希望View的大小。设置一般有三种情况:

  • xx dp:具体的数值,给多少是多少喽,很简单不多讲。
  • wrap_content:自适应,开发者的意愿是:刚好够 View 的显示就行了,至于具体是多少 ViewGroup 和 View 你们就自己商量吧。
  • match_parent:充满父布局,开发者的意愿是:ViewGroup 把你所有的空间都给这个 View 吧。
第二步:ViewGroup 和 View 相互磋商

经过第一步后,ViewGroup通过结合开发者的期望和自身的空间的大小,会给子View传递两个 MeasureSpec 对象(包含宽度和高度信息)这就相当于 ViewGroup 告诉 View : 我现在把我与开发者商量的结果告诉你,你必须要遵守我们的结果,你的宽,高不得超过 WidthMeasureSpec 和 HeightMeasureSpec 的要求,一定要遵守哦,要不开发者就不用你啦(那 View 还这么去实现自己的 View 价值观!!)。所以呢,View得到这个要求时就不得不认真的对待一下啦。View 这么读懂两个 MeasureSpec 呢?

//宽度 mode 和 size
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
//高度 mode 和 size
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);

通过 Mode 和 Size 两个参数 View 便能够很准确的理解出开发者对自己的要求。
Mode 分为三类:

  • MeasureSpec.EXACTLY:表示一个确定的值,你必须要用我给定你的 size。原因可能是多样的:可能开发者明确告诉你大小要 100dp, 也可能是开发者让你填充我,我刚好有 100dp。总之你不能任性必须是要遵守的。
  • MeasureSpec.AT_MOST:表示了一个上限值,这是因为开发者要你 wrap_content ,大小让我跟你商量,我就把我最大的可用空间告诉你,你最多只能用这么多。这是开发者的意思,你不能不管不顾,你参考一下再把你到底需要多少告诉我,也是只许少,不许多哦!
  • MeasureSpec.UNSPECIFIED:这样吧,你把你最理想的大小告诉我,我考虑考虑。
    通常情况下,mode 与 View 的设置和 ViewGroup 存在以下关系:
父布局大小
View Layout设置
EXACTLYAT_MOSTUNSPECIFIED
dp/px(固定值)mode = EXACTLY
size = 固定值
mode = EXACTLY
size = 固定值
mode = EXACTLY
size = 固定值
wrap_contentmode = AT_MOST
size = 父布局大小
mode = AT_MOST
size = 父布局大小
mode = UNSPECIFIED
size = 0
match_parentmode = EXACTLY
size= 父布局大小
mode = AT_MOST
size = 父布局大小
mode = UNSPECIFIED
size = 0
第三步 确定最终结果

经过第二部的协商,View 已经计算出了自己的大小就剩下设置了(废话,不设置不是白TM计算了)。但这个时候有个问题了,那些被“压迫强制”变小的 View 就真的只能这样委屈求全,上诉五门了吗? NO!View 可以通过标记来告诉ViewGroup 自己是被压迫的,自己还想要更大的空间,不过至于ViewGroup 会不会理会,不同的ViewGroup处理方式不一样。具体写法如下:

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) {
                    // MEASUERD_STATE_TOO_SMALL 即为 View 期望更大空间的标志 
                    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);
    }
位置和大小确定了,那 View 的外观(内容)和事件呢?

先来看一下 Google 对关于创建自定义View的介绍:

 To implement a custom view, you will usually begin by providing overrides for some of the standard methods that the framework calls on all views. You do not need to override all of these methods. In fact, you can start by just overriding onDraw(android.graphics.Canvas).

 创建一个自定义 View 你通常需要覆写几个回调方法。不过呢,你甚至只覆写 onDraw 方法就可以自定义一个 View .其实在自定义View中有关绘制的方法只有 onDraw 一个(SurfaceView 除外)。

具体可能会覆写那些方法呢?Google 为什么提供了一个表格,我们必须熟知这些方法,他们的作用以及何时被调用。
如果图片不清晰大家可以来官网查看 http://www.android-doc.com/reference/android/view/View.html

可能需要覆写的方法
通过方法名后边的描述,已经大致可以看出这个方法的回调时间。
通过打印日志我们可以很清晰的看到整个调用过程,下图中蓝色部分为 Activity 的方法,橘色部分为 View 的方法。
这里写图片描述

根据上图便可以回答我们平时开发中经常遇到的一些问题:

  • 为什么在 onCreate 中的到的 View 的大小(宽高值)都是 0? 这是Android新手经常遇到和困惑的一个问题,在这里就很一目了然了,onMeasure 还没有执行啊,还没有计算 View 的大小呢!!
  • View 不是“任性”的自娱自乐不顾 Activity 的变化,它是可以感知到 Activity 生命周期的,并响应其变化。
  • Activity 在进入后台的时候,其中的 View 并没有消失不见,而是跟随 Window 一起变为不可见。
  • View 的可见范围为 onWindowVisibilityChanged 变为 visible 和 invisiable 之间,因此如果我们要注册一个广播监听来改变View的内容,则最好的时机是在 onWindowVisibilityChanged 变为 visible 时注册监听器,在 invisiable 时注销广播监听。实际上但 Activity 处于 stop 状态时,我们是不能改变View 的状态的,View不会设置为可见,也不会去调用 onDraw 绘制方法。
  • 当View调用 onDetachedFromWindow 时则表示View所在的Activity已经销毁,这个时候应该停止 View 的一切操作,如释放内存,清空消息队列等。

写在最后

回顾一下我们主要讲了View的概念,它在Android中的作用,它的一些默认行为和我们在自定义View是应该注意的一些方法。View的另外两个比较重要的部分,Touch 事件和 Drawable,我们以后慢慢道来(一次写太长大家读者也容易厌倦(其实我写的类/(ㄒoㄒ)/~~))

Android 菜鸟一枚,在写博客时难免会借鉴网上大神之作,如涉及版权问题,请及时联系博主!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值