一 概述
自定义组件是Android工程师必须了解并且经常会使用的知识点,本文就是对该知识点的简单总结。
具体而言,自定义组件有三种方式:
-
继承现有组件,拓展其功能
-
组合现有组件,实现模板化
-
直接继承View,重写onDraw方法,进行重绘
直接继承ViewGroup(或其子类),重写onLayout/onMeasure方法,进行自定义布局
二 继承现有组件,拓展其功能
步骤如下:
-
根据要实现的效果,找到功能相近的组件
-
继承该组件,实现自有功能
具体而言,以下几个方法需要注意,
构造方法
首先,如果要在XML文件中直接使用自定义组件,需要实现具有两个参数的构造方法。
在构造方法中,主要实现获取XML布局文件中写入的自定义属性的值。
自定义属性的方法如下:
在res/values目录下定义attrs.xml文件,该文件中定义了自定义组件支持的自定义属性。
具体写法为:
在resources标签下加入declare-styleable标签,并给其一个标签名,一般选择自定义类名作为标签名;
然后,在后面自定义属性,以标签attr包裹,内容包括自定义属性名name与自定义属性的类型format。
形式如下所示:
<resources> <declare-styleable name="一般为自定义组件类名"> <attr name="自定义属性名" format=""> <attr name="自定义属性名" format=""> ... ... </declare-styleable> </resources>
这就定义了自定义组件的自定义属性,之后就可以在XML布局文件中使用这些自定义属性了。
使用的时候,要在xml文件中写入xmlns,即xml命名空间,形式为:
xmlns:custom="http://schemas.android.com/apk/res-auto"
或:
xmlns:custom="http://schemas.android.com/apk/manifest中的package名"
之后就可以“custom:自定义属性名”来使用自定义属性了。
在构造方法中,通过操作TypedArray来获取自定义属性内容,代码如下:
TypedArray options = context.obtainStyledAttributes(attrs, R.styleable.PaintDoubleWordsTextView, 0, 0); int numOfOptions = options.getIndexCount(); for (int i = 0; i < numOfOptions; ++i) { int index = options.getIndex(i); switch (i) { case R.styleable.PaintDoubleWordsTextView_word0 : word0 = options.getString(index); break; case R.styleable.PaintDoubleWordsTextView_word0Size : word0Size = options.getInt(index, DEFAULT_TEXT_SIZE); break; case R.styleable.PaintDoubleWordsTextView_word1 : word1 = options.getString(index); break; case R.styleable.PaintDoubleWordsTextView_word1Size : word1Size = options.getInt(index, DEFAULT_TEXT_SIZE); break; } } options.recycle(); // 勿忘
onDraw()
onDraw() 方法的参数是一个Canvas对象,通过该对象即可实现自定义组件的内容绘制。
invalidate()
触发重绘,onDraw。
requestLayout()
触发整个绘制流程,onMeasure/onLayout/onDraw。
三 组合组件,模板化使用
这种自定义组件的方式并不创建新的组件,只是将已有组件进行组合,然后将组合好的整体当做模板来使用。
关键方法:
LayoutInflater.from(Context).inflate(自定义布局id, 为加载好的布局指定父布局);
通过以上关键代码来将自定义布局载入程序中,之后就可以通过findViewById
来获取自定义布局文件中的组件了。
步骤如下:
-
根据需求组合组件
根据需求对组件进行组合,构成模板。
此处主要是实现一个XML布局文件。
-
拓展Layout类
实现Layout类的子类,在其具有两个参数的构造方法中加载上一步实现的布局文件,然后获取其中定义的组件。
-
控制组件显示,实现事件响应
根据需求,操作上一步获取的组件,控制组件显示并实现具体的事件响应。
四 继承View & ViewGroup
继承View与继承ViewGroup是不同的。继承View是要自定义组件,而继承ViewGroup则要自定义布局。
对于自定义组件而言,所需要考虑比较少,只有两点需要注意:
-
对于Touch事件的处理
根据事件处理机制,当重写了
onTouch
方法后,要注意返回true之后会导致onTouchEvent
方法不再执行,进而影响到click的执行与滑动的效果;同时,还要注意对ACTION的DOWN处理后返回false时,会导致后续的UP、MOVE事件无法获取,也就是会阻止事件的层级传递。 -
重写
onDraw
方法绘制需要的UI根据Android绘制原理,自定义组件只需要重写
onDraw
方法即可,在其中使用所提供的Canvas画布绘制所需要的效果。至于测量与布局,自定义组件不需要考虑太多,这是由布局组件来考虑的问题。实际上,View类中的
measure
方法是一个final方法,子类无法重写;对于子类,只需要重写真正进行测量的onMeasure
方法即可。View类的
onMeasure
实现里,只是去调用了setMeasuredDimension
这个final方法去保存了组件的宽、高信息;直接继承自View的TextView则重写了onMeasure
方法去根据padding等信息来计算宽、高数据。View类中的
layout
方法则直接在注释中说明,子类不需要重写该方法,具有子组件的子类,需要重写onLayout
方法,在其中调用子组件的layout
方法。代码上看,View类中的
onLayout
方法是一个空方法,TextView中也并没有重写layout
方法(因为不需要),所重写的onLayout
方法也没有进行重要的操作。所以,在自定义View组件时,为了实现较精细的测量,一般需要重写
onMeasure
方法,对于onLayout
方法,一般不用实现。
至于自定义布局,就要考虑对于测量与布局的处理问题,
-
测量
这一步根据Android绘制原理,需要重写
onMeasure
方法,在其中获取各个子组件的dimension,计算出布局的dimension,然后调用setMeasuredDimension
方法设置好布局的尺寸。ViewGroup中没有重写
onMeasure
方法,子类必须自己进行实现。 -
布局
这一步需要重写
onLayout
方法,在其中调用各个子组件的layout
方法,让子组件完成对自己的布局。在ViewGroup中,
onLayout
方法是一个抽象方法,自定义的布局必须实现该方法,在其中对子组件的layout
方法进行调用。而ViewGroup中的layout
方法是一个final方法,不能被重写,其中则简单的调用了View的layout
方法(super.layout()
)。关于View的
layout
方法,一般在进行继承View自定义组件时不需要重写,至于View类中的onLayout
方法,是一个空方法,不进行任何操作。至于绘制,则由ViewRootImpl类中的
performTraversals
方法调用performDraw
进而调用组件的onDraw
方法实现,不需要在ViewGroup中考虑。