在Android中,控件主要以ViewGroup和View的形式存在。ViewGroup控件可以包含多个View控件,该复合控件负责其内部所有子控件的测量和绘制,并传递交互事件。如图,
在Android的移动开发中,每个Activity都包含了一个PhoneWindow对象,该对象将DecorView设置为应用窗口的根View。该视图上的所有监听事件都通过WindowManagerService来进行接收,并通过Activity来回调相应的onClickListener。DecorView将屏幕分为了两部分,TitleView和ContentView,而我们的setContentView方法则用于处理这个位置的布局显示。我们的activity_main即处于这样一个ID为content的FrameLayout中。如图:
自定义View流程,
一个32位int值,高2位代表测量模式,低30位代表测量的大小,用于辅助View的测量,模式如下:
EXACTLY(精准模式):表示父控件已经确切的指定了子View的大小。
AT_MOST(最大模式):依据子控件的变化而变化,但存在上限
UNSPECIFIED(自定义模式):大小设置无限制,比较扯淡吧我觉得
首先必须重写onMeasure方法,至于为什么,源码走一波
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
在AS中通过Ctrl+B来观看onMeasure的源码,
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
显然,这里将自定义的MeasureSpec对象作为参数传递给setMeasuredDimension.
再来看看getDefaultSize的作用
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
getDefaultSize则是根据对应的测量模式选取对应的Size。
在该方法的第二个参数,是一个getSuggestedMinimumWidth()或getSuggestedMinimumHeight()方法,以getSuggestedMinimumWidth()为例,
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
官方的解释是:Returns the suggested minimum width that the view should use. This
returns the maximum of the view’s minimum width。最合适的宽度就这样诞生了,,
由于View类默认支持EXACTLY模式,所以为了实现自定义View的wrapContent,就必须使用其他模式,即重写onMeasure方法。
其实onMeasure的重写很简单,就是通过Google提供的MeasureSpec获取Mode和Size,之后依据获得的Mode选取xml设定的数据,之后将数据传递给setMeasuredDimension即可。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
//从MeasureSpec中获取Mode和Size
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width;
int height ;
//依据模式获取预设的width和height
if (widthMode == MeasureSpec.EXACTLY)
{
width = widthSize;
} else
{
mPaint.setTextSize(mTitleTextSize);
mPaint.getTextBounds(mTitleText, 0, mTitleText.length(), mBound);
float textWidth = mBound.width();
int desired = (int) (getPaddingLeft() + textWidth + getPaddingRight());
width = desired;
}
if (heightMode == MeasureSpec.EXACTLY)
{
height = heightSize;
} else
{
mPaint.setTextSize(mTitleTextSize);
mPaint.getTextBounds(mTitleText, 0, mTitleText.length(), mBound);
float textHeight = mBound.height();
int desired = (int) (getPaddingTop() + textHeight + getPaddingBottom());
height = desired;
}
//殊途同归,传递我们自定义的数据
setMeasuredDimension(width, height);
}
对于Android的2D绘图,我们主要借助于graphics包,其中主要包含了Canvas类、Paint类、Color类和Bitmap类等。
绘制图形必然要用到颜色,在Android开发中对颜色的使用和获取有疑问的看这篇文章:颜色
要绘制图形,首先得调整画笔,按照自己的开发需要设置画笔的相关属性。
setAntiAlias: 设置画笔的锯齿效果。
setColor: 设置画笔颜色 。
setARGB: 设置画笔的a,r,p,g值。
setAlpha: 设置Alpha值 。
setTextSize: 设置字体尺寸。
setStyle: 设置画笔风格,空心或者实心。
setStrokeWidth: 设置空心的边框宽度。
getColor: 得到画笔的颜色 。
getAlpha: 得到画笔的Alpha值
详细参见:Paint详解
Canvas我们可以称之为画布,能够在上面绘制各种东西,是安卓平台2D图形绘制的基础,在使用该类前需要设置好paint。
除了进行基本的绘制外,Canvas也可以进行一些基本操作。位移、缩放、旋转、倾斜以及快照和回滚。
有需要自行参见:Canvas的基本操作
一个简单的圆形绘制示例
Paint paint = new Paint();
paint.setColor(Color.CYAN);
paint.setStrokeWidth(3);
paint.setAntiAlias(true);
//空心
paint.setStyle(Paint.Style.STROKE);
canvas.drawCircle(160,60,50,paint);
paint.setStyle(Paint.Style.FILL);
canvas.drawCircle(160,180,50,paint);
自定义属性流程
该类的构造方法最多可有四个参数,
public void CustomView(Context context) {}
public void CustomView(Context context, AttributeSet attrs) {}
public void CustomView(Context context, AttributeSet attrs, int defStyleAttr) {}
public void CustomView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {}
在构造方法中AttributeSet中保存的是该自定义View所有属性的值名称和值,获取其中属性代码:
int count = attrs.getAttributeCount();
for (int i = 0; i < count; i++) {
String attrName = attrs.getAttributeName(i);
String attrVal = attrs.getAttributeValue(i);
Log.e(TAG, "attrName = " + attrName + " , attrVal = " + attrVal);
}
通过这种方式我们只可以获取那些在xml中直接定义的属性,比如android:text = "horzion"
,但对于诸如android:text = “@String/name”则会直接反馈给你一段R文件中的标识码,为了简化工作这里引入TypeArray。
TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CustomTitleView, defStyle, 0);
此时,typeArray中的集合便是经过简化处理,可以直接获取使用。
一个完整的示例:
public CustomTitleView(Context context, AttributeSet attrs, int defStyle)
{
super(context, attrs, defStyle);
//获得我们所定义的自定义样式属性
TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CustomTitleView, defStyle, 0);
int n = typedArray.getIndexCount();
for (int i = 0; i < n; i++)
{
int attr = typedArray.getIndex(i);
switch (attr)
{
case R.styleable.CustomTitleView_titleText:
mTitleText = typedArray.getString(attr);
break;
case R.styleable.CustomTitleView_titleTextColor1:
// 默认颜色设置为黑色
mTitleTextColor = typedArray.getColor(attr, Color.BLACK);
break;
case R.styleable.CustomTitleView_titleTextSize:
// 默认设置为16sp,TypeValue也可以把sp转化为px
mTitleTextSize = typedArray.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));
break;
}
}
typedArray.recycle();//记得回收
}
此外,declare-styleable标签既能告诉系统这是自定义的属性,又会帮助我们在R.java中生成一些常量方便我们使用。
最后,在获取完所需的属性后,我们将其设置到对应的paint上即可,创建paint并调用onDraw方法进行重绘。
在UI线程外,使用postInvalidate方法启动绘制,当需要重绘的时候则可以直接在UI线程中中使用inValidate方法实现重绘。
详细参见:postinvalidate 和invalidate的区别
自定义完View之后,一般会对外暴露一些接口,用于控制View的状态等,或者监听View的变化.
直接在CustomView的构造方法在即可为View设置监听事件
//设置监听事件
this.setOnClickListener(new OnClickListener()
{
@Override
public void onClick(View v)
{
mTitleText = randomText();
postInvalidate();
}
});
源码地址:Custom
原来学RecyclerView,走到半截发觉对自定义分割线有很多不理解的地方,于是有转过头来重学自定义View,看了群英传和开发艺术探索,以及网络博客诸多,终于写够了这篇基础的自定义。
关于更深入的Canvas学习,可以参加这位大牛的最后一篇文章,Canvas之图片以及张洪洋一整个系列自定义View实战
路漫漫其修远兮,吾将上下而求索。纪念第一篇MarkDown博客