教你步步为营掌握自定义View

作者: 工程师milter | 来源:发表于2016-08-10 15:29 被阅读25838次
图片来自互联网,如侵删.jpg

国内自定义View的文章汗牛充栋,但是,即使你全部看完它们也未必能掌握这一知识点(实际上,我就几乎看完了所有的国内文章)。为什么?一言以蔽之,你是得其术不明其道。(本文不打算讲自定义属性和事件处理,因为太多的文章讲这些了)

一、自定义View,你真的掌握了吗?

什么?你说你掌握了自定义View?来来来,回答老衲如下问题:

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

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

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

假如以上问题你都能准确地回答出来,那么,恭喜你!我觉得你的自定义View已经学到家了,如果有那么几个问题你还搞不清楚,或者不是很确定,那么,请上终南山,闭关三个月,继续参悟自定义View的内在玄机。

为什么看了那么多文章,还是无法愉快地与自定义View玩耍?是那些文章不好吗?非也,是你没有掌握学习自定义View的正确姿势(即使你会很多姿势,也木有用,嘎嘎)。你看那些作者,轻轻松松整出一个漂亮的自定义View,你依葫芦画瓢也整出一个,就觉得自己好像也会了,年轻人,你太傲娇了!你想过没有,写这些文章的人是怎么掌握自定义View的?请把这个问题在心中默念三遍。以后读任何技术文章,都问自己这样的问题,相信不久的将来,你也会成为Android大牛的,至少也是小壮牛一头!因为,你已经从学习别人的知识,进入到学习别人的方法的境界了,功力怎能不大增!

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

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

以上三个问题,从抽象到具体,我觉得适用于学习任何技术知识,只是每个问题的问法可能因具体技术而有所调整,总体上就是从概念上,从默认实现上,从自己定制上去提问,比如你学习RecyclerView,也可以问以上三个问题,按照这三个问题的顺序一个一个搞懂了,也就完全掌握了这一知识点。

下面,我们就一个问题一个问题地来解答。

二、从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.

这句话言简意赅,高屋建瓴,一针见血,力透纸背,入木三分,令人销魂佩服!需要我们认真体会,它包含三层含义:

  • View是用户接口组件的基本构建块。通俗讲,在Android中,一个用户与一个应用的交互,其实就是与这个应用中的许许多多的View的交互,这些View既可以是简单的View,也可以是若干View组合而成的一个复合View。由此我们可以明白,所谓View是基本构件块,原因就在于它是复合View(就是ViewGroup)的基本组成单元。这层含义,就是告诉你,View就是用来与用户交互的,那么很自然地,我们要问,我们用户在哪里与View交互,以及怎样与View交互呢?
  • View在屏幕上占据一个矩形区域。这是说,既然View是用户与应用交互的基本构建块,而用户使用Android设备时,主要是通过一个触摸屏来交互的,相应的,Andorid的设计者们,就让一个View就在屏幕上占据一个矩形区域,用户在这个区域中发生的交互动作(点击、滑动、拖动等),就是与这个View的交互。什么?为什么不让View占据一个圆形区域或者五角星区域呢?当然是为了简单。这就解决了在哪里与View交互的问题。很自然地,我们又想问,View在屏幕上占据一个矩形区域,这个区域的大小、位置怎么确定,它们会不会变化,谁来决定这个变化呢?如果这个变化不是由View自己来决定的,而是其他外界因素决定的,View又要怎样响应这种变化呢?不要急,后面都会有答案。
  • View通过绘制自己与事件处理两种方式与用户交互。这是解决了如何交互的问题。简单讲,View与用户交互就两个办法,一个是改变自己的模样,也就是通过绘制自己与用户交互,比如,当用户点击自己时,就改变自己的背景颜色,以此来告诉用户:“本View已经响应你的点击了!”第二个方式就是事件处理,比如,当用户点击View时,就完成一定的任务,然后弹出一个Toast,告诉用户该View完成了什么任务,这样,用户也就知道这次交互结果如何。

看到没,这就是官方文档的魅力,短短一句话,胜君读千篇水文。现在我们明白了,设计View,主要是为了让应用能够与用户交互,要想完成交互,这个View就要在屏幕上占据一个矩形区域,然后利用这块屏幕区域与用户交互,交互的方式就两种,绘制自己与事件处理。

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

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

View是怎样被显示到屏幕上的?

View在屏幕上的位置是怎样决定的?

View所占据的矩形大小是怎样决定的?

屏幕上肯定不止一个View,View之间互相知道对方吗?它们之间能协作吗?

View完成与用户的交互后,能够自动隐藏,在需要交互的时候重新显示在屏幕上吗?

......

现在我们就一点点来讲,学习的同时,最好能够用心体会Google工程师设计时的思路。

这样学习效果最好。

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

这样,在Window中的View们被组织起来了,一个巨大的ViewGroup(以后,我们不再用复合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的位置的吧。

1、确定每个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,它按照
自己的小弟数量,把自己竖向裁成不同的区域,如下图所示:

LinearLayout-sample.png

虽然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安排位置。

2、确定View大小

下面,是要确定View的大小了,这是一个开发者、View与ViewGroup三方相互商量的过程。(这里的讲解可能与一般的文章不同,是我个人的理解,一般的文章都不会说是三方商量,而是直接说View与ViewGroup两方商量)

第一步,开发者在书写布局文件时,会为一个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对象的要求,现在,你赶紧根据这个要求确定下自己要多大空间,只许少,不许多哦。”

然后,这两个对象将会传到子View的protected 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的总体态度:

  1. EXACTLY 表示,ViewGroup对View说,你只能用100dp,原因是多样的,可能是你的使用者说要你完全占据我的空间,而我只有100dp。也可能这是你的使用者的要求,他需要你占这么大的空间,而我恰好也有这么多的空间,你的使用者让你占这么大的空间,肯定有他自己的考虑,你不能不理不顾,不然你达不到他的要求,他可能就不用你了。
  2. AT_MOST表示,你最多只能用100dp,这是因为你的使用者说让你占据wrap_content的大小,让我跟你商量,我又不知道你到底要占多大区域,但是我告诉你,我只有100dp,你最多也只能用这么多哈。(这里,可以看出,当使用者在布局文件中要求一个View是wrap_content时,此时,View的大小决定权就交给View自己了,默认的View类中的实现,比较粗暴,就是将此时ViewGroup提供的空间全占据,完全没有真正根据自己的内容来确定大小,为什么这么粗暴?因为View是一个基类,所有的组件都是它的子类,每个子类的content都各不相同,View怎么可能知道content的大小呢,所以,它把wrap_content情况下,自己尺寸大小的决定权下放给了不同的子组件,让它们自己根据自己的内容去决定自己的大小,同样,我们自定义View时,也要考虑这一点)
  3. 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的文章提到过!)
什么?好奇的你想看看究竟是怎样设置标记的?来来来,满足你:
```java
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的绘制,非常简单,就是一个方法onDraw,后面的自定义View实战部分会细说,这里先略过了。

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

四、我要改变这个View的行为,外观,肯定是覆写View类中的方法,但是怎么覆写,覆写哪些方法能够改变哪些行为?

好了,View的位置和大小怎么确定我们都清楚了,现在,是时候开始自定义View了。
首先,关于View所要具备的一般功能,View类中都有了基本的实现,比如确定位置,它有layout方法,当然,这个只适用于ViewGroup,实现自己的ViewGroup时,才需要修改该方法。确定大小,它有onMeasure方法,如果你不满意默认的确认大小的方法,也可以自己定义。改变默认的绘制,就覆写onDraw方法。下面,我们通过一张图,来看看,自定义View时,我们最可能需要修改的方法是哪些:

View-Method-For-Override.png

把这些方法都搞明白了,你也就理解了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还实现了三个接口,如下:

View-Hierachy.png

三个接口是:
Drawable.Callback
KeyEvent.Callback
AccessibilityEventSource

每个接口都有自己的作用。

KeyEvent回调接口,是用来处理键盘事件的,这与onTouchEvent用来处理触摸事件是相对的。

Drawable回调接口是用来让View中的Drawable能够与View通信的,尤其是AnimationDrawable,更是必须依赖该回调才能实现动画效果,关于这一点,我深入地研究了FrameWork的源码,对AnimationDrawable如何实现动画,有了深入彻底的掌握,我也在考虑要不要就此写一篇文章,看大家需要吧,如果本文赞数过百,我就写,绝不食言。

第三个回调接口,我没有细致研究,不便多说。

写到这里你应该发现,我们的第三个问题,自定义View,应该覆写哪些方法,能够实现哪些功能也已经解决了。

五、光说不练假把式,实战自定义View

说了这么多,不自定一个View,怎么对的起你辛苦读到这里呢。好,我们现在就来自定义一个钟表,而且可以自己走的。如下图所示:

screenshot0.png

这个时钟可是能够走动的哈。下面我们就开始吧。首先,准备三张图片资源,如下:

clock_dial.png clock_hand_hour.png clock_hand_minute.png

聪明如你,一看就应该知道这是做什么用的了。准备图片时,使用了一个小技巧,就是时针和分针,你所看到的图像只是图片的一半,在图像的下方,还有同样大小的空白,这个是做什么用的呢?主要是为了绘制图片时的方便,待会儿就可以明白了。

材料齐全,开工!

public class AnalogClock extends View {   

      private Time mCalendar;    //用来记录当前时间

      //用来存放三张图片资源
      private Drawable mHourHand;  
      private Drawable mMinuteHand; 
      private Drawable mDial;   

    //用来记录表盘图片的宽和高,
    //以便帮助我们在onMeasure中确定View的大
    //小,毕竟,我们的View中最大的一个Drawable就是它了。
       private int mDialWidth; 
       private int mDialHeight;   


//用来记录View是否被加入到了Window中,我们在View attached到
//Window时注册监听器,监听时间的变更,并根据时间的变更,改变自己
//的绘制,在View从Window中剥离时,解除注册,因为我们不需要再监听
//时间变更了,没人能看得到我们的View了。
       private boolean mAttached;    
  
//看名字
        private float mMinutes;    
        private float mHour;    

//用来跟踪我们的View 的尺寸的变化,
//当发生尺寸变化时,我们在绘制自己
//时要进行适当的缩放。
        private boolean mChanged;
...
}

下面,我们来确定自定义View 的构造方法,查看View类,我们知道,View类有四个构造方法,我们相应地,也写四个构造方法,并且初始化相关变量:

//第一个构造方法
public AnalogClock(Context context) {   
     this(context, null);
}
//第二个构造方法
public AnalogClock(Context context, AttributeSet attrs) {  
      this(context, attrs, 0);
}
//第三个构造方法
public AnalogClock(Context context, AttributeSet attrs, int defStyleAttr) { 
      this(context, attrs, defStyleAttr, 0);
}
//第四个构造方法
public AnalogClock(Context context, AttributeSet attrs, 
int defStyleAttr, int defStyleRes) {    

    super(context, attrs, defStyleAttr, defStyleRes);    
    final Resources r = context.getResources();  
    if (mDial == null) {    
          mDial = context.getDrawable(R.drawable.clock_dial);  
    }  
    if (mHourHand == null) {        
        mHourHand = context.getDrawable(R.drawable.clock_hand_hour);   
    }     
    if (mMinuteHand == null) {      
          mMinuteHand = 
                context.getDrawable(R.drawable.clock_hand_minute);   
     }  
  
     mCalendar = new Time(); 

    mDialWidth = mDial.getIntrinsicWidth();   
    mDialHeight = mDial.getIntrinsicHeight();}

请注意,以上为自定义View设置的构造方法是适用性最广的一种写法,这样写,可以确保我们的自定义View能够被最大多数的开发者使用,是一种最佳实践。
接下来,确定我们的自定义View 的大小,也就是改写onMeasure方法:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {   

         int widthMode = MeasureSpec.getMode(widthMeasureSpec);  
         int widthSize =  MeasureSpec.getSize(widthMeasureSpec);  

         int heightMode = MeasureSpec.getMode(heightMeasureSpec);  
         int heightSize =  MeasureSpec.getSize(heightMeasureSpec); 
  
         float hScale = 1.0f;  
         float vScale = 1.0f;   
 
         if (widthMode != MeasureSpec.UNSPECIFIED && widthSize < mDialWidth) {       
             hScale = (float) widthSize / (float) mDialWidth;   
         }   
         if (heightMode != MeasureSpec.UNSPECIFIED && heightSize < mDialHeight) {       
             vScale = (float )heightSize / (float) mDialHeight;  
          }    
         float scale = Math.min(hScale, vScale);    
        setMeasuredDimension(
              resolveSizeAndState((int) (mDialWidth * scale), widthMeasureSpec, 0),           
             resolveSizeAndState((int) (mDialHeight * scale), heightMeasureSpec, 0)
        );
}


在该方法中,我们的View想要的尺寸当然就是与表盘一样大的尺寸,这样可以保证我们的View有最佳的展示,可是如果ViewGroup给的尺寸比较小,我们就根据表盘图片的尺寸,进行适当的按比例缩放。注意,这里我们没有直接使用ViewGroup给我们的较小的尺寸,而是对我们的表盘图片的宽高进行相同比例的缩放后,设置的尺寸,这样的好处是,可以防止表盘图片绘制时的拉伸或者挤压变形。

确定了大小,是不是就可以绘制了,先不着急,我们先要处理两件事,一件就是让我们的自定义View能够感知自己尺寸的变化,这样每次绘制时,可以先判断下尺寸是否发生了变化,如果有变化,就及时调整我们的绘制策略。代码如下:

protected void onSizeChanged(int w, int h, int oldw, int oldh) {    
       super.onSizeChanged(w, h, oldw, oldh);   
       mChanged = true;
}

我们会在onDraw使用mChanged变量的。

第二件事就是让我们的View能够监听时间变化,并及时更新该View中的mCalendar变量,然后根据它来更新自身的绘制。为此,我们先写一个更新时间的方法,代码如下:


private void onTimeChanged() {    
        mCalendar.setToNow();  

        int hour = mCalendar.hour;   
        int minute = mCalendar.minute;  
        int second = mCalendar.second;   
        /*这里我们为什么不直接把minute设置给mMinutes,而是要加上
            second /60.0f呢,这个值不是应该一直为0吗?
            这里又涉及到Calendar的 一个知识点,
            也就是它可以是Linient模式,
            此模式下,second和minute是可能超过60和24的,具体这里就不展开了,
            如果不是很清楚,建议看看Google的官方文档中讲Calendar的部分*/
         mMinutes = minute + second / 60.0f;    
         mHour = hour + mMinutes / 60.0f;   
         mChanged = true;
}

然后我们还要实现一个广播接收器,接收系统发出的时间变化广播,然后更新该View的mCalendar,如下:

private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {   
       @Override  
        public void onReceive(Context context, Intent intent) {    
            //这个if判断主要是用来在时区发生变化时,更新mCalendar的时区的,这
            //样,我们的自定义View在全球都可以使用了。
            if (intent.getAction().equals(Intent.ACTION_TIMEZONE_CHANGED)) {            
                  String tz = intent.getStringExtra("time-zone");         
                   mCalendar = new Time(TimeZone.getTimeZone(tz).getID());    
            }     
          //进行时间的更新  
             onTimeChanged();     
          //invalidate当然是用来引发重绘了。
           invalidate();   
         }
};

现在,我们要给我们的View动态地注册广播接收器,没错,我们就是要在
onAttachedToWindow和onDetachedFromWindow中完成这一功能。代码如下:

@Override
protected void onAttachedToWindow() {   
       super.onAttachedToWindow();    
      if (!mAttached) {      
          mAttached = true;      
          IntentFilter filter = new IntentFilter();        
        //这里确定我们要监听的三种系统广播
          filter.addAction(Intent.ACTION_TIME_TICK);   
          filter.addAction(Intent.ACTION_TIME_CHANGED);        
          filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);        
          getContext().registerReceiver(mIntentReceiver,   filter); 
       }   
       
        mCalendar = new Time();   
        onTimeChanged();
}

@Override
protected void onDetachedFromWindow() {    
          super.onDetachedFromWindow();  
          if (mAttached) {     
               getContext().unregisterReceiver(mIntentReceiver);     
               mAttached = false;   
           }
}

万事具备,只欠东风,开始绘制我们的View吧。代码如下:

@Override
protected void onDraw(Canvas canvas) {   
         super.onDraw(canvas);  

      //View尺寸变化后,我们用changed变量记录下来,
    //同时,恢复mChanged为false,以便继续监听View的尺寸变化。
          boolean changed = mChanged;   
          if (changed) {      
                mChanged = false;   
           }   
        /* 请注意,这里的availableWidth和availableHeight,
           每次绘制时是可能变化的,
           我们可以从mChanged变量的值判断它是否发生了变化,
           如果变化了,说明View的尺寸发生了变化,
           那么就需要重新为时针、分针设置Bounds,
           因为我们需要时针,分针始终在View的中心。*/
           int availableWidth = super.getRight() - super.getLeft();   
           int availableHeight = super.getBottom() - super.getTop();  


        /* 这里的x和y就是View的中心点的坐标,
          注意这个坐标是以View的左上角为0点,向右x,向下y的坐标系来计算的。
          这个坐标系主要是用来为View中的每一个Drawable确定位置。
          就像View的坐标是用parent的左上角为0点的坐标系计算得来的一样。
          简单来讲,就是ViewGroup用自己左上角为0点的坐标系为
          各个子View安排位置,
          View同样用自己左上角为0点的坐标系
          为它里面的Drawable安排位置。
          注意不要搞混了。*/

           int x = availableWidth / 2;    
           int y = availableHeight / 2;   

           final Drawable dial = mDial;  
           int w = dial.getIntrinsicWidth();   
           int h = dial.getIntrinsicHeight();   
            boolean scaled = false;   

        /*如果可用的宽高小于表盘图片的宽高,
           就要进行缩放,不过这里,我们是通过坐标系的缩放来实现的。
          而且,这个缩放效果影响是全局的,
          也就是下面绘制的表盘、时针、分针都会受到缩放的影响。*/
           if (availableWidth < w || availableHeight < h) {     
                 scaled = true;      
                  float scale = Math.min((float) availableWidth / (float) w,   
                              (float) availableHeight / (float) h);     
                 canvas.save();    
                 canvas.scale(scale, scale, x, y);  
             }    

         /*如果尺寸发生变化,我们要重新为表盘设置Bounds。
           这里的Bounds就相当于是为Drawable在View中确定位置,
           只是确定的方式更直接,直接在View中框出一个与Drawable大小
           相同的矩形,
           Drawable就在这个矩形里绘制自己。
           这里框出的矩形,是以(x,y)为中心的,宽高等于表盘图片的宽高的一个矩形,
           不用担心表盘图片太大绘制不完整,
            因为我们已经提前进行了缩放了。*/
          if (changed) {       
                 dial.setBounds(x - (w / 2), y - (h / 2), x + (w / 2), y + (h / 2)); 
           }    
          dial.draw(canvas);    

          canvas.save();   
          /*根据小时数,以点(x,y)为中心旋转坐标系。
            如果你对来回旋转的坐标系感到头晕,摸不着头脑,
            建议你看一下**徐宜生**《安卓群英传》中讲解2D绘图部分中的Canvas一节。*/

           canvas.rotate(mHour / 12.0f * 360.0f, x, y);  
           final Drawable hourHand = mHourHand;   

          //同样,根据变化重新设置时针的Bounds
           if (changed) {     
                   w = hourHand.getIntrinsicWidth();    
                   h = hourHand.getIntrinsicHeight();      
          
            /* 仔细体会这里设置的Bounds,我们所画出的矩形,
                同样是以(x,y)为中心的
                矩形,时针图片放入该矩形后,时针的根部刚好在点(x,y)处,
                因为我们之前做时针图片时,
                已经让图片中的时针根部在图片的中心位置了,
                虽然,看起来浪费了一部分图片空间(就是时针下半部分是空白的),
                但却换来了建模的简单性,还是很值的。*/
                  hourHand.setBounds(x - (w / 2), y - (h / 2), x + (w / 2), y + (h / 2));  
             }    
              hourHand.draw(canvas);  
              canvas.restore();  
  
              canvas.save();    
            //根据分针旋转坐标系
              canvas.rotate(mMinutes / 60.0f * 360.0f, x, y);   
              final Drawable minuteHand = mMinuteHand;   

              if (changed) {     
                       w = minuteHand.getIntrinsicWidth();    
                       h = minuteHand.getIntrinsicHeight();    
                       minuteHand.setBounds(x - (w / 2), y - (h / 2), x + (w / 2), y + (h / 2)); 
               }   
               minuteHand.draw(canvas);    
                canvas.restore();    
            //最后,我们把缩放的坐标系复原。
              if (scaled) {      
                   canvas.restore();   
              }

}

大功告成,现在我们的时钟终于完成了,任何开发者都可以使用我们的View,获得一个不断走动的模拟时钟。该View的完整代码已经上传到Github,猛戳https://github.com/like4hub/CustomViewForClock。(注:该时钟的实现,主要参考了AOSP中模拟时钟)

相关文章

  • 教你步步为营掌握自定义ViewGroup

    本篇是《教你步步为营掌握自定义View》一文的姊妹篇。 一、自定义ViewGroup必须清楚的基本原理 在学习一个...

  • 教你步步为营掌握自定义View

    国内自定义View的文章汗牛充栋,但是,即使你全部看完它们也未必能掌握这一知识点(实际上,我就几乎看完了所有的国内...

  • 自定义view

    目标: 1、掌握自定义view的流程2、掌握自定义view的三个方法3、掌握自定义view实现方式4、掌握自定义v...

  • 自定义View

    目标: 1、掌握自定义view的流程2、掌握自定义view的三个方法3、掌握自定义view实现方式4、掌握自定义v...

  • 自定义view

    目标: 1、掌握自定义view的流程2、掌握自定义view的三个方法3、掌握自定义view实现方式4、掌握自定义v...

  • 手把手教你写一个完整的自定义View

    前言 自定义View是Android开发者必须了解的基础 今天,我将手把手教你写一个自定义View,并理清自定义V...

  • (二)Android 基础知识面试相关总结

    1. 必须掌握的最重要的技能-自定义控件 自定义view也有几种实现类型,分别为继承自View完全自定义、继承自...

  • android自定义view

    前言 网上有很多自定义View的说明的文章,首先通过以下问题,测试自己是否掌握自定义View: 1、Google提...

  • 重新设计 Android View 体系?

    作者:leobert-lan 关于 Android View 体系,大家从如何自定义 View 到相关源码的掌握,...

  • View 的工作原理

    目的 介绍 View 的工作原理,为了更好地自定义 View(这才是学习的重点),需要掌握 View 的底层工作原...

网友评论

  • 薇薇浆糊:这是看的你的第一篇文章,已经决定关注你了,写的很好,会一直看下去
    工程师milter:@薇薇浆糊 感谢关注!
  • yhy123:楼主的这种讲解方式非常能够接受,很有趣,不会觉得乏味看不下去,很想继续学习,希望楼主以后多出些这样的文章。感谢作者。
  • 大猫猴来了:就喜欢这种大白话讲懂道理的技术文章
  • 316763d4a71e:楼主写的很棒,我觉得这种从设计者思路出发来理解View的绘制流程是最好的方式。
  • Jetkoal:写的很好.对我的帮助很大,谢谢作者:clap: :clap: :clap:
  • d9f946d06394:对不起,我来晚了!!!自定义View入门到放弃两次了,看你的文章竟然懂了,并且Android源码也能看懂了!太神奇~ 特意登录来感谢!
    工程师milter:@静听花开_d066 感谢你的认同!能帮到你我也很高兴
  • 李月半Android:写的太好了
  • 845eccbb9660:想问一下作者,ondraw方法中上来就将changed改为false的话,后面根据changed来修改指针的bounds和表盘的bounds还会执行吗
  • json_6d65:在下来晚了。文章很不错。
    工程师milter:@json_6d65 确实有点晚,我已经转机器学习了
  • 风影_638f:一直是痛点
    工程师milter:@风影_638f 希望你已经突破了
  • WeberLisper:写得非常棒!
    工程师milter:@WeberLisper 多谢鼓励!
  • mrFessible:关于OnMeasure那部分写的很容易理解,谢谢啦。
    mrFessible:@milter 好:flushed:
    工程师milter:@mrFessible 点个赞呗
  • 7fefc35f1a5f:对我帮助很大,望作者多写类似文章,我会持续关注。
  • 风信子_b84e:嗯嗯,人生两件事,认识自己,做自己:blush:
  • 4be416c16c45:不知道为啥,安卓7.1的模拟机上运行不起来,而且代码划横杠了
  • 猪屿乖乖小bb仔:今天晒网,下次来看
  • 516021dd7081:有用,弥补了一部门之前对自定义View的一些盲点,有些具体的细节,还需要自己细细推敲和实践。
  • D13954:新手表示看不懂
    工程师milter: @小木邪 不着急,到一定程度自然会看懂
  • b9cb8c1bdf63:棒棒哒
  • bingoCode:super.getright() -super.getleft() 这个我知道是算宽度 但是为啥是这样用啊
    工程师milter: @MTW quite right!
    消沉沉:@bingoCode 可能是获得view在父类中的left和right(也就是layout里面的left和right),right-left就是左边的距离减去右边的距离=控件实际的宽度。我是这样理解的。:+1:
  • a21396047406:收我为徒吧😄
    工程师milter: @zhang106787 我也是在学习中,大家多交流
  • aad3a0c8a574:厉害啊,代码都是自己写的吗?
  • eb15e719394c:Drawable.Callback
    KeyEvent.Callback
    AccessibilityEventSource 这三个文章的链接都坏了
    eb15e719394c:@milter 正确的链接是https://developer.android.com/reference/android/graphics/drawable/Drawable.Callback.html,你不信自己打开你的文章的链接看看
    eb15e719394c:@milter 我能翻墙,这个链接是不是有问题,跳到了简书404.(http://www.jianshu.com/reference/android/graphics/drawable/Drawable.Callback.html)
    工程师milter: @瓦良格舰长 需要翻墙
  • 12d44aaa64f6:然而你并没有回答文章开始处提出的问题,例如:
    View能够与其他的View交叉重叠吗?重叠区域发生的点击事件交给谁去处理呢?可不可以重叠的两个View都处理?

    把问题放出来,但是又不回答。
    工程师milter: @吴志权 评论中有,请自行查阅
  • jarbir:6666
  • 某某的某某徐:发现一个问题加上了秒针,怎么时针不能实时更新的。而是要等一段时间才能更新。
  • 6c1e2188d9f1:感谢,android :+1: 新人敬上
    工程师milter: @JianMa 大家共同进步!
  • Nex_LiuHao:难得好文!继续加油!
  • 帝_国:学习了 :+1: 谢谢分享
  • lyLvYan:我加上了秒针,然而秒针并不能正常走,mSecond是等于second吗?还是也需要加
    lyLvYan:@milter 下午测试秒针不动呀,不知道为啥,onDraw里面秒针的代码和分针一样的
    工程师milter: @lyLvYan 是的,你可以自己测试下
  • 微凉_gg:赞,写的很好,如果最后再系统的回答下文章开头你提到的那几个问题就完美了。
    微凉_gg:@milter 额,在54楼,之前没看到! :disappointed_relieved: :disappointed_relieved:
    工程师milter: @微凉_gg 评论中有回答
  • 工程师milter:关于本文前面提出的问题,简单回答一下:
    Q1:google提出view概念的目的是给android app提供用户交互的机制。
    Q2、Q3、Q7:android framework采用的是层次架构:从上到下是:Activity、Fragment
    View
    Drawable
    上层知道下层,下层却不知道上层。上层可以直接使用支配下层,下层却无法支配使用上层,下层与上层的通信主要靠回调。所以View处于Activity、Fragment与Drawable中间,意味着View不能够感知Activity的生命周期,但是View可以完全控制Drawable,控制的手段定义在Drawable中,凡是Drawable提供的方法,都是View控制Drawable的手段,最典型的,在本文中也使用了的就是setBounds方法。正如View无法感知Activity的声明周期一样,Drawable同样无法感知View的生命周期。但是View实现了Drawable.Callback接口,Drawable可以通过这个接口与View通信。本文中有说明
    Q4:View的生命周期请见本文View-Method-For-Override一图,这张图来自google官方文档,如果看不懂,可以查看文档获得相关说明,如果还是看不懂,欢迎留言讨论。

    Q5:Activity进入stop状态后,它的窗口会被最新呈现的窗口挡住,窗口中的view也因此无法被我们看见,如果此时在后台线程中更新一个view是可以的,前提是要提交到UI线程中,但通常意义不大,因为此时用户无法看到view的改变,而且,当这个Activity从stop状态中进入resume时,一般都会重新更新view,以便继续与用户交互,所以,在stop状态下对view的更新没有什么意义。
    Q6:View直接是可以重叠,重叠区域的点击事件由谁处理取决于它们的parent 在dispatch这个点击事件时,先dispatch给谁。能不能都处理呢?一般情况下是不可以的,但是在最新的CoordinateLayout中,可以通过behavior实现这一需求。具体内容太多,请自行搜索。

    Q8:View利用这些空间的方法很简单啊,就是在onSizeChanged方法中在新的宽高下绘制自己 。新的宽高由其parent ViewGroup在其他子View被移除后,重新layout时确定。本文的案例中就利用了这个方法。

    可以看到,本文前面提出的问题,大部分都可以在本文中直接找到答案,没有直接答案的,也给出了思路和线索,帮助大家去自行查找答案。比如,关于View的重叠情况下,事件处理,本文已经说明,请查看本人另一篇事件分发的文章:“可能是讲解View事件分发最好的文章”。
  • 7812c2da4ed6:看了楼主的文章,确实有不少独到之处,收获很大。期待关于AnimationDrawable大作
  • M星空:好久没有看到这么精美的文章了,忍不住想赞一个!!!!!
    工程师milter: @M星空 多宣传我的博客,就是对我最大的鼓励!
  • 浮华染流年: 前面的 文字描述部分看懂了
  • 1cbdf4fedc83:要优雅,不要污,宝宝已看完,Get重点 :blush:
    工程师milter: @Zhou_sir 请自行加上😀😀
    1cbdf4fedc83:加上秒针的话就可以用了
    工程师milter: @Zhou_sir 觉得好就转发,给我继续的动力😀
  • akira晓:不太明白为什么还要比较一下宽和高的缩放率 能解释下么
  • ChangQin:这一波很6
    工程师milter: @若梦忆月 必须6😀
  • Ronnie_火老师:非常给力,深入骨髓!
    工程师milter: @pxc0215 谢谢鼓励!觉得好就多多转发!
  • b9763664515c:楼主写的很好!大赞!
    工程师milter: @TimeLQuen 谢谢鼓励!
  • 5c760ca5a402:醍醐灌顶
    工程师milter: @AlphaHans 多谢鼓励?
    工程师milter: @AlphaHans 😀
  • Clendy:写的非常不错!感觉可以出书了!希望大神能够把“AnimationDrawable如何实现动画”的知识也写一篇文章分享下~~ 顶起~~
  • tpkeeper:是先measure还是先layout呢?
    tpkeeper:@milter 嗯,已拜读
    工程师milter: @tpkeeper 必须先measure,建议看看姊妹篇
  • hackware:也就算个总结,没什么深入的东西,有些问题,没有给出答案,不过还是赞一个
  • 61b125a42651: :smiley: 很感谢
  • 谭冉冉:写的真好,比爱哥和hongyang 清晰多了
    工程师milter: @谭冉冉 get it !
    谭冉冉:@milter http://aigestudio.com/ 他写过 Android 源码设计模式解析与实战这本书和hongyang是基佬
    工程师milter: @谭冉冉 恕我鄙陋,爱哥是?
  • 6bca4073be84:文章的内容对得起步步为营 这个词语,希望博主写一篇学习思维和方法的文章
    工程师milter: @不如学习 幸会幸会!
    6bca4073be84:@milter 恩,支持你 :blush: :blush: :blush: 说来还真是缘分,之前看你在知乎上发表过回答,然后这次搜索自定义View的知识,没想到又遇到你 :smile:
    工程师milter: @不如学习 等JavaScript系列完结会考虑写一篇
  • iceIC:评论和文章一样好看~ :grin:
    话说有微信公众号啥的咩.
    工程师milter: @iceIC 木有哈
  • q2nAmor:getIntrinsicWidth() 这个方法不应该是返回视图实际上表现出来的宽度吗... 而不应该是图片实际大小的宽度.... 你这儿说/*如果可用的宽高小于表盘图片的实际宽高,........*/ 是不是不对? :grin:
    工程师milter: @流苏没魅力 你说的对的,我已经改过来了。谢谢指正!
  • 8e47b82fb2be:文风赞一个!不过好像有个小问题,没记错的话,layout_width=match_parent对应的mode应该是EXACTLY吧
    8e47b82fb2be:@milter :blush::blush::blush:
    工程师milter:@小二哥的幸福生活 谢谢指正,已经修改! :smile:
  • shixinBook:还想在请教您一下,请问您平时看的书和资料有哪些,我感觉我现在对自定义view总体有个了解,但是有很多细节和方法没有学习到!比如说postInvalidateDelayed()这个方法!由于没有接触所以根本不懂它的用法,就会对理解造成影响,就想问一下哪里可以学习这些知识点!
    shixinBook:@milter 好的!谢谢您,这么晚了还回复。👍
    工程师milter:@shixinBook 再就是上stackoverflow上把这个方法往搜索框中一丢就是
    工程师milter:@shixinBook 源代码里面的注释
  • shixinBook:博主写的真好,这一段时间正在研究这样,看了你的文章思路更加的清晰,话说点赞数已经过了100了,下一篇准备好了吗?翘首以待呀!
    shixinBook:@milter 没看到哈!博主真厉害,很期待你接着写。
    工程师milter: @shixinBook 已经写了哈,自定义ViewGroup
  • 静静De欧巴:看了一半感觉写的真心赞!!! 谢谢老师,学习了!!! :+1:
    工程师milter: @静静旳Oppa 谢谢鼓励,我很喜欢看千与千寻的。
  • fe2a5bcdf6ea:太棒了
    工程师milter: @蹦蹦跳跳真可爱 嗯,我也这么觉得。欢迎多多分享给需要的人,也帮我宣传下,谢谢!
  • 780a261caf0f:"国内自定义View的文章汗牛充栋" 从这第一句话我就知道此篇大概是万千水文中的一篇(试试看把你认为国内最好的关于View自定义的博客top20推荐下) 你要是把本文作为自己笔记没啥好说的 但是本着以“分享”精神去“教育”广大android开发者 这篇还是水了 首先你的标题应该是某个专题的标题 “步步为营”不是你这样一蹴而就 其次文章夹杂太多口头话不专业的描述 初学者根本不知道你在说什么 进阶开发者看得味同嚼蜡 专业的工程师很重要的素质就是讲术语 (在别的地方看到有人开口闭口把activity叫活动,嗯 开启一个活动)
    9d13dac0b326:作者所说的步步为营是文章从头到尾步步为营,像你说的,非得几篇文章之间才能叫步步为营吗?作者开头说的话也并没有看不起其他文章的意思,你非得往极端了想那就没办法了。
    36cf8531f05d:本人倒是觉得作者解释的很通透。尤其是MeasureSpec那一段的中文解释让人清晰明了。看过很多别人关于这部分的解释,都只是照字面意思来,让人看了不知所以然。
  • d5962134cef8:大哥你好, 你在文章留下的那些问题好像还没有回答哦
    工程师milter:@maxwell0401 有几个问题在自定义ViewGroup中回答了
    工程师milter: @maxwell0401 大部分都回答了,剩下的靠自己啦😊
  • 24隋心所欲:写得很棒,深入浅出,通俗易懂。请问能转载到我的个人博客中吗?
    工程师milter: @24隋心所欲 拿去就是,开头给俺留个名号和原文地址就行。
  • Liu积土成山:言简意赅,通俗易懂,白话文.good
    工程师milter: @SarnathL 谢谢夸奖,觉得好就多多分享哈
  • 范尼斯特鲁伊呀:非常棒,有种醍醐灌顶的感觉😄
    工程师milter: @范尼斯特鲁伊呀 觉得好就推荐给身边需要的朋友,让大家都受益哈!😊
  • 7133cd0cb3bf:怒赞,必须得给你凑齐100个赞 :smile: 文章写的很好,以前也陆续了解过很多,但是这文章让我有了更深的理解,这个理解是指从设计的角度,知其然知其所以然,理解其为什么存在。非常感谢!最后想说的是文章配个赏心悦目的美图是一个很大的进步 :+1:
    工程师milter: @笑中带泪 谢谢!😊
    工程师milter:@笑中带泪 感谢鼓励!我会继续努力的!
  • 91ff408ee169:学习啦 谢谢楼主 嘎嘎
  • 来秋先生:太好了
    工程师milter: @来秋先生 谢谢!
  • BoomHe:赶紧破百 :+1:
  • breakingbad:3.final Drawable dial = mDial;

    这里为啥还要新建一个dial变量,有点多余,直接用mDial不就得了?
    breakingbad:@milter 原来如此,谢谢解答哈😁
    工程师milter:@breakingbad 如果你对java语言中的final关键字能提供的不变性语义不是很清楚,你可以看看《Java并发编程实战》一书中讲解final的部分,具体章节我记不太清了。
    工程师milter:@breakingbad 关键是final,它可以确保能够完整地绘制完mDial,可以防止正在绘制过程中时mDial被销毁,和匿名内部类访问局部变量时为它加上final的意图是一样的。
  • breakingbad:2. int availableWidth = super.getRight() - super.getLeft();
    int availableHeight = super.getBottom() - super.getTop();

    getWidth不就是等于.getRight() - getLeft()吗?
    getHeight不就是等于getBottom() - getTop()吗?
    而且,为什么还要调用super方法?
    工程师milter: @breakingbad 那就可以去掉super
    breakingbad:@milter 试过了,这两者的值是相等的。没看出啥问题来
    工程师milter:@breakingbad 你可以试试
  • breakingbad:看完了, :+1: 有几个问题:
    1.onAttachedToWindow和onDetachedFromWindow不是成对调用的吗?为什么还需要mAttached变量?去掉mAttached变量的判断会有什么问题?
    风清阳final:@milter 请教一下,低版本下可能出现什么错误?
    breakingbad:@milter OK 明白✌
    工程师milter:@breakingbad 在低版本上可能出现错误,这是Android的一个已知的issue,这样的变量能够确保view能够完全掌握自己的状态,是一种防御性编程
  • breakingbad:91了骚年
    breakingbad:@milter good boy😍
    工程师milter:@breakingbad 过百必践诺
  • 沫沫么么哒Die:挺好的,老师让讲ppt正好不知道说什么
    沫沫么么哒Die: @milter 好的
    工程师milter: @沫沫么么哒Die 拿去不谢!
  • 4672b0b08300:看的时候好像觉得那里不对,跑起来才反应过来,哦,没有秒针 :smile: ,写的很好.
    工程师milter:@辣酱gg 你可以自己加上秒针 :smile:
  • fight2048:快点快点,点赞数已破78了,快点过百,别让作者跑了
    穷格万物:挺厉害的,我看到一半就不懂了,就看不下去了,真是没耐心。
    工程师milter:@langmanleguang O(∩_∩)O
  • 04da40e51179:给力
    工程师milter:@04da40e51179 😊
  • 3a4c43382d3f:我是专门来点赞的~
    工程师milter: @Fett 谢谢
  • ceeb8b12e7b4:非常不错,看了很多篇自定义view,你这边有别样特点
    工程师milter:@再南方 我就是在看了很多篇之后写的哈。
  • GoodGoodStuday:厉害了,
    工程师milter:@小宝拜财神 一般,过奖了。
  • 7d1bc704ea3c:写的很好,感谢分享
    工程师milter:@菜鸟闯天下 你也可以把它分享给其他需要的人。(*^__^*) 嘻嘻……
  • 亦枫:真心喜欢,期待一个自定义ViewGroup实战案例,这样就完美了。
    工程师milter: @亦枫 感谢夸奖!
  • 一朵致远:前面三分之一比较容易懂
    工程师milter:@一朵致远 多读几遍会好些
  • 加油码农:写的很好,希望再写一些关于自定义view的例子出来,来让我们学习。
    工程师milter:@加油雷哥 看大家需求,赞数过百就再写一个自定义ViewGroup的如何?
  • zjujunge:写的很详细
    工程师milter:@DarkCode 谢谢,对他人有帮助,让我很高兴!
    93aa29d75292:宝宝正在研究自定义View,可感觉难啊难,看了你这篇文章后,感觉收获颇多,希望楼主能够继续写类似这样的文章,赞一个!!! :smile:
    工程师milter: @zjujunge 谢谢,希望能分享给被这个知识点困住的人
  • 旋哥:好

本文标题:教你步步为营掌握自定义View

本文链接:https://www.haomeiwen.com/subject/rjsdsttx.html