自定义View入门还是很简单的,但是很少有程序员能做好它,因为涉及的面太广,网上经常有写文章标题是一篇就能搞定自定义View的,简直是too young too simple……可以说自定义View是从入门到懵逼,哈哈,其实没那么恐怖,慢慢积累就好了。
自定义View可以分为三类:继续View Group来组合多个子View;继承特定的View(如TextView,ImageView, ViewPager等);直接继承自View
组合和继承用的比较多,组合一般就是自定义一个View,继承自ViewGroup(如RelativeLayout,LinearLayout,FrameLayout), 这里就不多做介绍了。继承特定的View(如TextView,ViewPager等),添加特殊的一些事件或者逻辑处理。
本文重点总结一下直接继承自View的方式。
1. 自定义属性
这个就比较简单,在res/values/attr.xml中自定义属性,然后在自定义View的构造方法中通过TypedArray引入既可。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="titleText" format="string" />
<attr name="titleTextColor" format="color" />
<attr name="titleTextSize" format="dimension" />
<declare-styleable name="CustomTitleView">
<attr name="titleText" />
<attr name="titleTextColor" />
<attr name="titleTextSize" />
</declare-styleable>
</resources>
format属性类型汇总:string,color,demension,integer,enum,reference,float,boolean,fraction,flag
/**
* 文本
*/
private String mTitleText;
/**
* 文本的颜色
*/
private int mTitleTextColor;
/**
* 文本的大小
*/
private int mTitleTextSize;
/**
* 绘制时控制文本绘制的范围
*/
private Rect mBound;
private Paint mPaint;
public CustomTitleView(Context context, AttributeSet attrs)
{
this(context, attrs, 0);
}
public CustomTitleView(Context context)
{
this(context, null);
}
/**
* 获得我自定义的样式属性
*
* @param context
* @param attrs
* @param defStyle
*/
public CustomTitleView(Context context, AttributeSet attrs, int defStyle)
{
super(context, attrs, defStyle);
/**
* 获得我们所定义的自定义样式属性
*/
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CustomTitleView, defStyle, 0);
int n = a.getIndexCount();
for (int i = 0; i < n; i++)
{
int attr = a.getIndex(i);
switch (attr)
{
case R.styleable.CustomTitleView_titleText:
mTitleText = a.getString(attr);
break;
case R.styleable.CustomTitleView_titleTextColor:
// 默认颜色设置为黑色
mTitleTextColor = a.getColor(attr, Color.BLACK);
break;
case R.styleable.CustomTitleView_titleTextSize:
// 默认设置为16sp,TypeValue也可以把sp转化为px
mTitleTextSize = a.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));
break;
}
}
a.recycle();
/**
* 获得绘制文本的宽和高
*/
mPaint = new Paint();
mPaint.setTextSize(mTitleTextSize);
// mPaint.setColor(mTitleTextColor);
mBound = new Rect();
mPaint.getTextBounds(mTitleText, 0, mTitleText.length(), mBound);
}
2. 重写onMesure
该方法也可以不重写,大多情况下,只要不是wrap_content,父容器都能正确的计算其尺寸,如果自定的View在布局文件中使用时,宽高指定为warp_content时,就需要我们重写onMesure
,自定义ViewGroup的onMeasure,当其宽和高为warp_content时,要根据所有子View已经margin来计算其宽高值。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
/**
* 获得此ViewGroup上级容器为其推荐的宽和高,以及计算模式
*/
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
int width;
int height ;
if (widthMode == MeasureSpec.EXACTLY)
{
width = widthSize;
} else
{
mPaint.setTextSize(mTitleTextSize);
mPaint.getTextBounds(mTitle, 0, mTitle.length(), mBounds);
float textWidth = mBounds.width();
int desired = (int) (getPaddingLeft() + textWidth + getPaddingRight());
width = desired;
}
if (heightMode == MeasureSpec.EXACTLY)
{
height = heightSize;
} else
{
mPaint.setTextSize(mTitleTextSize);
mPaint.getTextBounds(mTitle, 0, mTitle.length(), mBounds);
float textHeight = mBounds.height();
int desired = (int) (getPaddingTop() + textHeight + getPaddingBottom());
height = desired;
}
}
ViewGroup会为childView指定测量模式,下面简单介绍下三种测量模式:
测量模式 | 含义 |
---|---|
EXACTLY | 表示设置了精确的值,一般当childView设置其宽、高为精确值、match_parent 时,ViewGroup会将其设置为EXACTLY |
AT_MOST | 表示子布局被限制在一个最大值内,一般当childView设置其宽、高为wrap_content 时,ViewGroup会将其设置为AT_MOST |
UNSPECIFIED | 表示子布局想要多大就多大,一般出现在AadapterView的item的heightMode中、ScrollView的childView的heightMode中;此种模式比较少见 |
注:上面的每一行都有一个一般,意思上述不是绝对的,对于childView的mode的设置还会和ViewGroup的测量mode有一定的关系;当然了,这是第一篇自定义ViewGroup,而且绝大部分情况都是上面的规则,所以为了通俗易懂,暂不深入讨论其他内容。
3. 重写onDraw
onDraw就是自定义View的核心部分了,通常,我们在自定义的构造函数里,会初始化好Paint(画笔对象),Canvas(画布对象),最后都是通过canvas.drawXXX( XXX, paint) 来进行绘制。
mPaint.setStrokeWidth(4);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(Color.CYAN);
mPaint.setTextSize(mTitleTextSize);
canvas可以调用各个方法来使用画笔对象进行绘制,除了一些长方形、圆等基本图形,canvas.drawPath可以绘制更为复杂的图形,包括内塞尔曲线等,具体可以参考下文:
canvas.drawText()
canvas.drawBitmap()
canvas.drawPath(path, mPaint);
在onDraw中,要注意以下几点
:
- 避免耗时操作,因为onDraw通常会被多次触发,
避免内存泄露,主要针对View中含有线程或动画的情况:当View退出或不可见时,记得及时停止该View包含的线程和动画,否则会造成内存泄露问题。
- 启动线程/ 动画:使用
view.onAttachedToWindow()
,因为该方法调用的时机是当包含View的Activity启动的时刻- 停止线程/ 动画:使用
view.onDetachedFromWindow()
,因为该方法调用的时机是当包含View的Activity退出或当前View被remove的时刻如果是自定义ViewGroup,要处理好支持支持padding & margin
- 避免过度绘制,Canvas对象给我们提供了很便利的方法clipRect就可以很好的去解决这类问题。
避免过度绘制可以参考鸿洋大神的这篇博客:
Android UI性能优化实战 识别绘制中的性能问题
主要思想就是,如果一个子View不需要全部显示出来,那么就可以利用canvas.clipRect()方法
@Override
protected void onDraw(Canvas canvas)
{
super.onDraw(canvas);
canvas.save();
canvas.translate(20, 120);
for (int i = 0; i < mCards.length; i++)
{
canvas.translate(120, 0);
canvas.save();
if (i < mCards.length - 1)
{
canvas.clipRect(0, 0, 120, mCards[i].getHeight());
}
canvas.drawBitmap(mCards[i], 0, 0, null);
canvas.restore();
}
canvas.restore();
}
4. 重写onLayout
onLayout的作用:对其所有childView进行定位(设置childView的绘制区域),所以只有自定义ViewGroup时,才需要重写onLayout
// abstract method in viewgroup
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b)
{
int cCount = getChildCount();
int cWidth = 0;
int cHeight = 0;
MarginLayoutParams cParams = null;
/**
* 遍历所有childView根据其宽和高,以及margin进行布局
*/
for (int i = 0; i < cCount; i++)
{
View childView = getChildAt(i);
cWidth = childView.getMeasuredWidth();
cHeight = childView.getMeasuredHeight();
cParams = (MarginLayoutParams) childView.getLayoutParams();
int cl = 0, ct = 0, cr = 0, cb = 0;
switch (i)
{
case 0:
cl = cParams.leftMargin;
ct = cParams.topMargin;
break;
case 1:
cl = getWidth() - cWidth - cParams.leftMargin
- cParams.rightMargin;
ct = cParams.topMargin;
break;
case 2:
cl = cParams.leftMargin;
ct = getHeight() - cHeight - cParams.bottomMargin;
break;
case 3:
cl = getWidth() - cWidth - cParams.leftMargin
- cParams.rightMargin;
ct = getHeight() - cHeight - cParams.bottomMargin;
break;
}
cr = cl + cWidth;
cb = cHeight + ct;
childView.layout(cl, ct, cr, cb);
}
}
最后,要想能做好自定义View,以上介绍的方法和注意点还远远不够,像是Path、Matrix、事件分发和冲突解决、ViewDragHelper、GestureDetector等等都是要了解一些
可以参考以下博文: