我们每天都会使用很多应用程序,虽然它们有不同的约定,但大多数应用程序的设计都非常相似。这就是为什么许多客户要求一些其他应用程序没有的设计,使应用程序显得独特和不同。如果功能布局需求非常自定义,已经无法通过Android自带的View来创建——那么就需要使用自定义View。这意味着在大多数情况下,我们需要相当长的时间才能完成。但这并不意味着我们不应该这样做,因为实施起来非常令人兴奋和有趣。我最近遇到了类似的情况:我的任务是使用ViewPager实现一个Android应用引导页面。与iOS不同,Android没有提供这样的View,所以我不得不自己写一个自定义View来实现。我花了一些时间来实施它。幸运的是,现在许多开源项目都有类似的可重用视图,这节省了我和其他开发人员的时间。我决定基于这个视图创建一个公共库。如果你有类似的功能需求但没有时间实现它,你可以在githubrepo中找到它。使用PageIndicatorView绘图的示例!因为编写自定义View比普通View更耗时,你应该只使用自定义View来实现特定的功能但没有更简单的方法,或者你想通过自定义View解决以下问题:性能。如果你的布局中有很多View,你可以使用自定义View对其进行优化,使其更轻盈。视图层次结构很复杂。完全自定义的View需要手动绘制才能实现。如果您还没有尝试编写自定义视图,本文将教您一些绘制平面自定义视图的技巧。我会告诉你整体的视图结构,如何实现具体的功能,不再重复常见的错误,实现动画效果!我们需要知道的第一件事——View的生命周期。出于某种原因,Google没有提供View生命周期的图表。因为开发者普遍误解它,导致一些意想不到的错误和问题,所以我们需要认清这个过程。构造函数每个View的生命都是从构造函数开始的。这是绘制初始化、进行各种计算、设置默认值或执行我们需要的任何其他操作的好地方。但是,为了让我们的View更容易使用和配置,Android提供了一个很好用的AttributeSet接口。它很容易实现,绝对值得花时间去理解和实现它,因为它会帮助你(和你的团队)通过静态参数设置View,对未来的新特性或新的屏幕扩展支持会更好.首先,创建一个新文件attrs.xml。所有不同的自定义视图属性都可以放在这个文件中。如您所见,我们有一个PageIndicatorView及其唯一的属性piv_count。CustomAttributessample立即在View的构造函数中,需要获取这个属性并使用它,如下图所示。publicPageIndicatorView(Contextcontext,AttributeSetattrs){super(context,attrs);TypedArraytypedArray=getContext().obtainStyledAttributes(attrs,R.styleable.PageIndicatorView);intcount=typedArray.getInt(R.styleable.PageIndicatorView_piv_count,0);typedArray.recycle();}注意:在创建自定义属性时使用简单的前缀,以避免与其他视图上的类似属性发生名称冲突。通常我们使用缩写的视图名称,如示例中的piv_。如果您使用的是AndroidStudio,lint会建议您在使用完属性后调用recycle()方法。原因只是为了摆脱不会再次使用的低效绑定数据。【译者注:翻译的有点啰嗦,其实就是回收TypedArray,方便以后复用】onAttachedToWindow的父View调用addView(View)后,这个View会附加到一个窗口上。在这个阶段,我们的View将意识到它周围的其他视图。如果您的视图与其他视图位于同一layout.xml中,那么这是通过id(您可以通过属性设置)找到它们的好地方,同时保持全局(如果需要)引用。onMeasure这意味着是时候让我们的自定义View处理自己的尺寸了。这是非常重要的方法,因为在大多数情况下,您的View需要具有特定的大小以适合您的布局。重写此方法时,请记住最后设置MeasuredDimension(intwidth,intheight)。onMeasure处理自定义View的大小时,用户可以通过layout.xml或动态设置具体大小。为了正确计算它,我们需要做一些事情。1.计算您的View内容所需的大小(宽度和高度)。2.获取您的ViewMeasureSpec大小和模式(宽度和高度)。protectedvoidonMeasure(intwidthMeasureSpec,intheightMeasureSpec){intwidthMode=MeasureSpec.getMode(widthMeasureSpec);intwidthSize=MeasureSpec.getSize(widthMeasureSpec);inheightMode=MeasureSpec.getMode(heightMeasureSpec);intheightSize=MeasureSpec.getSize(widthMeasureSpec);intheightMode=MeasureSpec.getMode(heightMeasure));intheightSize=MeasureSpec.getSize(调整View的尺寸模式(宽高)。intwidth;if(widthMode==MeasureSpec.EXACTLY){width=widthSize;}elseif(widthMode==MeasureSpec.AT_MOST){width=Math.min(desiredWidth,widthSize);}else{width=desiredWidth;}注意:参见MeasureSpec的Value:MeasureSpec.EXACTLY表示硬编码尺寸值,所以你应该设置指定的宽度或高度。MeasureSpec.AT_MOST用于表示你的View是匹配父View的尺寸的,所以他想要多大就应该多大【译者注:此时View尺寸不能超过父View允许的最大尺寸】MeasureSpec.UNSPECIFIED其实就是视图包尺寸。因此,您可以使用上面的方法来计算所需的大小。在通过setMeasuredDimension设置最终值之前,可以检查一下这些值是否不是负数,以防万一。这避免了布局预览的一些问题。onLayout此方法为其每个子视图分配大小和位置。因此,我们正在查看没有任何子视图的平面自定义视图(继承简单视图),因此没有理由重写此方法。【译者注:实现可以参考Android上的CustomLayouts】onDraw就是神奇的地方。在这里,使用Canvas和Paint对象,您将能够绘制任何您需要的东西。onDraw参数获取一个Canvas实例,一般用来绘制不同的形状,Paint对象定义了形状的颜色。简单的说,Canvas是用来绘制对象的,而Paint是用来做造型的。它们无处不在,无论是画直线、圆形还是矩形。onDraw()—methodsexample制作一个自定义View,永远记住onDraw会花费很多时间。当布局发生一些变化时,滚动和快速滑动会引起重绘。所以这也是为什么AndroidStudio也建议:避免在onDraw中分配对象,对象应该只创建一次并在以后重复使用。onDraw()—PaintobjectrecreationonDraw()—Paintobjectreuse注意:在执行绘画时,请始终牢记重用对象,而不是创建新对象。不要依赖IDE来突出潜在的问题,而是自己有意识地去做,因为当onDraw调用内部创建对象的方法时,IDE无法识别它。同时,请不要硬编码Viewsize。其他开发者在使用时可以定义不同的大小,所以View的大小应该取决于它有哪些维度。View的更新从View的生命周期图中我们可以知道重绘View本身有两种方式。invalidate()和requestLayout()方法将帮助您在运行时动态更改视图状态。但是为什么需要两种方法呢?invalidate()用于简单地重绘视图。例如更新其文本、颜色或触摸交互性。View只会调用onDraw()方法再次更新其状态。requestLayout()方法,你可以看到它会从`onMeasure()更新视图。这意味着在您的View更新并更改其大小后,您需要再次测量它并根据新大小重新绘制。Animation在自定义View中,动画是一帧一帧的过程。这意味着如果你想让一个圆的半径变小或变大,你需要逐渐增加半径并调用invalidate()来重绘它。在自定义View动画中,ValueAnimator是你最好的朋友。下面的类将帮助您从任何值到***设置动画,甚至支持Interpolator(如果需要)。ValueAnimatoranimator=ValueAnimator.ofInt(0,100);animator.setDuration(1000);animator.setInterpolator(newDecelerateInterpolator());animator.addUpdateListener(newValueAnimator.AnimatorUpdateListener(){publicvoidonAnimationUpdate(ValueAnimatoranimation){intnewRadius.get(Animator)动画;}});注意:不要忘记在每次有新的动画值出来时调用invalidate()。
