Android 开发必然会面对自定义View, 因为许多时候android sdk里现有的View 并不能满足我们项目的需求。很多的Android入门程序猿来说对于Android自定义View,可能都是比较恐惧的,但是这又是高手进阶的必经之路。我抛砖引玉一下,总结下自定义View的步骤:
1、自定义View的属性
2、在View的构造方法中获得我们自定义的属性
3、重写onMesure方法
4、重写onLayout方法
5、重写onDraw方法
其实上面的几个步骤都是非必需同时需要的,完全可以根据自己具体的需要而定。比如,如果你实现了上面1,2步,那么你的自定义view就能像android原生View一样在xml布局文件里使用一些自定义的属性,然后结合你在自己view里面对这些属性的处理,达到控制你的view的目的。onMesure方法里面用来测试和决定你的自定义View的宽高,当然如果你的自定义view是一个ViewGroup,即里面还包含子view, 则在onMesure里面是测量子View宽高,以决定自己宽高的绝佳地方。onlayout方法是父类决定子view在其父类中的位置的地方,所以如果你的view里面没有子view,当然就不需要重写此方法。而onDraw方法是你需要手动绘制一些图案或文字的地方。
在具体举例自定义view前,先看一下我自定义view的效果:
大家都知道这两天最火的莫过于王宝强离婚事件了,甚至比同期的奥运事件还火,这里我们定义一个能在图片上添加自定义标签的功能View。如上图,我们在上图中添加了两个标签,要实现上图功能,我们可以分两步:
一,自定义一个标签View,我们这里叫做LabelView;
二,自定义一个图片上能添加LabelView的View,我们这里叫做LabelImageView。
这里LabelView有两种样式,即上图的单行模式和两行模式。
第一步,自定义View的属性。我们先在app的value目录下定义一个label_view_attrs.xml文件,用于控制LabelView的一些有用的属性:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="LabelView">
<!--标签第一行文字-->
<attr name="labelText1" format="string" />
<!--标签第二行文字-->
<attr name="labelText2" format="string"/>
<!--标签文字的颜色-->
<attr name="labelTextColor" format="color"/>
<!--标签文字的大小-->
<attr name="labelTextSize" format="dimension"/>
<!--文字下划线的颜色-->
<attr name="labelLineColor" format="color"/>
<!--文字下划线的大小-->
<attr name="labelLineSize" format="dimension"/>
<!--标签头部的颜色-->
<attr name="labelHeaderColor" format="color"/>
<!--标签头部的半径-->
<attr name="labelHeaderRadius" format="dimension"/>
<!--标签文字的方向,即文字是在标签头的左边还是右边-->
<attr name="labelDirect" >
<enum name="right" value="0"/>
<enum name="left" value="1"/>
</attr>
</declare-styleable>
</resources>
第二步,在View的构造方法中获得我们自定义的属性。新建LaberView.java文件,这就是我们的自定义View实现的代码,它必须要继承View或其他现有的View,这里我们继承View就可以了。而在LaberView的构造函数里我们就可以获取上面label_view_attrs.xml的属性:
public LabelView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//定义一个标签数据管理者
mLabelDataManager = new LabelDataManager(context);
/**
* 获得我们所定义的自定义样式属性
*/
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.LabelView, defStyleAttr, 0);
int n = a.getIndexCount();
for (int i = 0; i < n; i++)
{
int attr = a.getIndex(i);
switch (attr)
{
case R.styleable.LabelView_labelText1:
mLabelDataManager.setLabelText1(a.getString(attr));
break;
case R.styleable.LabelView_labelText2:
mLabelDataManager.setLabelText2(a.getString(attr));
break;
case R.styleable.LabelView_labelTextColor:
mLabelDataManager.mLabelTextColor =a.getColor(attr, Color.WHITE);
break;
case R.styleable.LabelView_labelTextSize:
// 默认设置为16sp
int labelTextSize = a.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));
mLabelDataManager.mLabelTextSize = labelTextSize;
break;
case R.styleable.LabelView_labelLineColor:
mLabelDataManager.mLabelLineColor = a.getColor(attr,Color.WHITE);
break;
case R.styleable.LabelView_labelLineSize:
mLabelDataManager.mLabelLineSize = a.getDimensionPixelSize(attr,2);
break;
case R.styleable.LabelView_labelHeaderColor:
mLabelDataManager.mLabelHeaderColor = a.getColor(attr,Color.WHITE);
break;
case R.styleable.LabelView_labelHeaderRadius:
mLabelDataManager.mLabelHeaderRadius = a.getDimensionPixelSize(attr,7);
mLabelDataManager.setLabelHeaderShaderRadius(mLabelDataManager.mLabelHeaderRadius +6);
break;
case R.styleable.LabelView_labelDirect:
mLabelDirection = a.getInt(attr,RIGHT_LABEL);
break;
}
}
//回收内存
a.recycle();
mLabelDataManager.build();
}
这里我定义了一个LabelDataManager对象,负责管理计算这些属性变量。通过上面代码我们就可以在xml里面获得我们设置的属性值,比如文字的大小,颜色等。于是xml的布局文件里我们就可以如下使用我们的LabelImageView了:
<app.study.nick.com.demo.view.LabelView
xmlns:labelview="http://schemas.android.com/apk/res-auto"
android:id="@+id/label4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_centerInParent="true"
android:padding="4dp"
labelview:labelText1="宝宝不哭"
labelview:labelText2="宝宝,顶顶顶顶"
labelview:labelTextColor="@color/white"
labelview:labelTextSize="12sp"
labelview:labelLineColor="@color/white"
labelview:labelLineSize="2dp"
labelview:labelHeaderColor="@color/white"
labelview:labelHeaderRadius="5dp"
labelview:labelDirect="right" />
//EXACTLY:一般是设置了明确的值或者是MATCH_PARENT
//AT_MOST:表示子布局限制在一个最大值内,一般为WARP_CONTENT
//UNSPECIFIED:表示子布局想要多大就多大,很少使用
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int reallyWidth;
int reallyHeight;
if (widthMode == MeasureSpec.EXACTLY)
{
//如果设置了固定的宽度,就用这个固定的宽度
reallyWidth = widthSize;
} else {
//没有固定宽度,就自己计算Label所要占多宽,再加上两边padding
reallyWidth = getPaddingLeft() + mLabelDataManager.getLabelTotalWidth() + getPaddingRight();
}
if (heightMode == MeasureSpec.EXACTLY)
{
//如果设置了固定的高度,就用这个固定的高度
reallyHeight = heightSize;
} else {
//没有固定高度,就自己计算Label所要占多高,再加上两边padding
reallyHeight = getPaddingTop() + mLabelDataManager.getLabelTotalHeight() + getPaddingBottom();
}
//设置宽高
setMeasuredDimension(reallyWidth,reallyHeight);
}
onMeasure方法里最需要注意的就是MeasureSpec的三种模式了,EXACTLY, AT_MOST还有UNSPECIFIED三种模式,它是根据你在xml布局文件里的宽高设置决定的,除却UNSPECIFIED不谈,其他两种mode:当父布局是EXACTLY时,子控件确定大小或者match_parent,mode都是EXACTLY,子控件是wrap_content时,mode为AT_MOST;当父布局是AT_MOST时,子控件确定大小,mode为EXACTLY,子控件wrap_content或者match_parent时,mode为AT_MOST。所以在确定控件大小时,需要判断MeasureSpec的mode,不能直接用MeasureSpec的size。在进行一些逻辑处理以后,调用setMeasureDimension()方法,将测量得到的宽高传进去供layout使用。
需要明白的一点是 ,测量所得的宽高不一定是最后展示的宽高,最后宽高确定是在onLayout方法里,layou(left,top,right,bottom),不过一般都是一样的。
第四步,重写onLayout方法。这里我们不需要重写onLayout方法,因为LabelView只是一个单纯的view,它不是一个view容器,没有子view,而onLayout方法里主要是具体摆放子view的位置,水平摆放或者垂直摆放,所以在单纯的自定义view是不需要重写onLayout方法,不过需要注意的一点是,子view的margin属性是否生效就要看parent是否在自身的onLayout方法进行处理,而view的padding属性是需要自己在onDraw方法中处理生效的。
第五步,重写onDraw方法。onDraw 是自定义view的重头戏,一般自定义控件耗费心思最多的就是这个方法了,需要在这个方法里,用Paint在Canvas上画出你想要的图案,这样一个自定义view才算结束。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//画标签的下划线
mLabelDataManager.drawLabelLine(canvas);
//画标签的文字
mLabelDataManager.drawLabelText(canvas);
//画标签的头部阴影
mLabelDataManager.drawLabelHeaderShader(canvas);
//画标签的头部圆
mLabelDataManager.drawLabelHeader(canvas);
}
public void drawLabelHeader(Canvas canvas){
//画圆
canvas.drawCircle(mLabelHeaderCenterPoint.x,mLabelHeaderCenterPoint.y,mLabelHeaderRadius,mHeaderPaint);
}
public void drawLabelHeaderShader(Canvas canvas){
//设置阴影
Shader mShader = new RadialGradient((float) mLabelHeaderCenterPoint.x,(float) mLabelHeaderCenterPoint.y,
(float) mLabelHeaderShaderRadius,0x7f000000,0x3f000000,Shader.TileMode.CLAMP);
mHeaderShaderPaint.setShader(mShader);
canvas.drawCircle(mLabelHeaderCenterPoint.x,mLabelHeaderCenterPoint.y,mLabelHeaderShaderRadius,mHeaderShaderPaint);
}
public void drawLabelLine(Canvas canvas){
if(mLabelLinePath1 != null){
//画路径
canvas.drawPath(mLabelLinePath1,mLinePaint);
}
}
public void drawLabelText(Canvas canvas){
if(mLabelText1StartPoint != null){
//画文字
canvas.drawText(getLabelText1(),mLabelText1StartPoint.x,mLabelText1StartPoint.y,mTextPaint);
}
if(mLabelText2StartPoint != null){
canvas.drawText(getLabelText2(),mLabelText2StartPoint.x,mLabelText2StartPoint.y,mTextPaint);
}
}
onDraw方法其实就是调用canvas的一些绘制各种图案的方法而已。所以最费心思的就是你需要去计算出你要绘制的各图案的起始和结束位置而已。下面我们看看效果:
到这里自定义一个LabelView就已经基本完成了,是不是很简单。如果你想让上面的标签头动起来,怎么办呢?用invalidate()方法还是用postInvalidate()方法或是requestLayout()方法呢?
requestLayout: 当我们调用requestLayout的时候,从方法名字可以知道,“请求布局”,那就是说,如果调用了这个方法,那么对于一个子View来说,应该会重新进行布局流程。但是,真实情况略有不同,如果子View调用了这个方法,其实会从View树重新进行一次测量、布局、绘制这三个流程,最终就会显示子View的最终情况。
invalidate:当子View调用了invalidate方法后,会为该View添加一个标记位,同时不断向父容器请求刷新,父容器通过计算得出自身需要重绘的区域,直到传递到根View中,最终进行开始View树重绘流程(只绘制需要重绘的视图)。
postInvalidate:这个方法与invalidate方法的作用是一样的,都是使View树重绘,但两者的使用条件不同,postInvalidate是在非UI线程中调用,invalidate则是在UI线程中调用。postInvalidate其实是内部通过handler给主线程发送更新view消息而已。
public void startAnimation() {
mAnimator = ValueAnimator.ofFloat(0.4f, 1);
mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float scale = (float) animation.getAnimatedValue();
//改变标签圆半径
mLabelDataManager.changeLabelHeaderRaduis(scale);
//请求重绘
postInvalidate();
}
});
//设置动画持续时间
mAnimator.setDuration(1000);
//设置动画的动作,先加速后减速
mAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
// 重复次数 -1表示无限循环
mAnimator.setRepeatCount(-1);
// 重复模式, RESTART: 重新开始 REVERSE:恢复初始状态再开始
mAnimator.setRepeatMode(ValueAnimator.REVERSE);
//启动动画
mAnimator.start();
}
这里利用了android系统自带的ValueAnimator动画,来实现循环缩放标签头部圆。这里需要注意的是,如果更多动画效果还是不建议用自定义View实现,因为自定义View的onDraw执行在UI线程中,会占用UI线程,过多复杂动画会导致APP卡顿,这时候可以使用自定义SurfaceView代替View,因为surfaceView的绘图是可以在其他线程中运行的,不占用UI线程。