自定义View
在使用了SlidingMenu后,看了一些关于SlidingMenu的原理分析,对自定义View产生了一些兴趣,准备自己写一个自定义的View。准备写一个LabelAndTextControl,包含一个Label(TextView)和一个Text(EditText)。每当需要输入用户名、密码这类,有提示和输入对应关系的控件时,就可以使用这个LabelAndTextControl了。当然可以使用下面的方式变通处理:定义一个layout,使用LinearLayout包含一个TextView和一个EditText。使用的时候include这个layout。但是这样的话,如果想要使LinearLayout固定大小就会很麻烦,更重要的是通过这个LabelAndTextControl练习一下自定义View。
自定义View的步骤可以看一下博客: Android 自定义View (一)
一、自定义View的步骤
1、在attrs.xml中定义LabelAndTextControl的属性。
2、在LabelAndTextControl中声明构造函数,在其中获取具体的属性值。
3、重写onMeasure方法
4、重写onLayout方法
5、重写onDraw方法(我没有重写这个方法)
二、实现
实现的过程中遇到了各式各样的问题,把遇到的一些比较特别的问题逐个列出来。这些问题主要集中在界面中,而属性的定义和使用(也就是上面的1和2)相对简单。
1、在attrs.xml中定义LabelAndTextControl的属性
<resources> <attr name="textSize" format="dimension"/> <attr name="labelText" format="string"/> <attr name="textText" format="string"/> <attr name="enabled" format="boolean"/> <attr name="spaceSize" format="dimension"/> <declare-styleable name="LabelAndTextControl"> <attr name="textSize"/> <attr name="labelText"/> <attr name="textText"/> <attr name="enabled"/> <attr name="spaceSize"/> </declare-styleable> </resources>
2、在LabelAndTextControl中声明构造函数,在其中获取具体的属性值。
public LabelAndTextControl(Context context) { this(context, null); } public LabelAndTextControl(Context context, AttributeSet attrs) { this(context, attrs, 0); } public LabelAndTextControl(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); mLabel = new TextView(context); addView(mLabel); mLabel.setBackgroundColor(Color.BLUE); mLabel.setSingleLine(true); mLabel.setGravity(Gravity.CENTER_VERTICAL); mText = new EditText(context); mText.setIncludeFontPadding(false); addView(mText); mText.setBackgroundColor(Color.YELLOW); mText.setSingleLine(true); //mText.setPadding(0, 3, 0, 0); mText.setGravity(Gravity.CENTER_VERTICAL); Log.i(LogTag, ">>>>>>>>>>>>>init"); showPaddingInfo(); TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.LabelAndTextControl); int textSize = ta.getInt(R.styleable.LabelAndTextControl_textSize, (int) context.getResources().getDimension(R.dimen.label_text_textSize)); setTextSize(textSize); int spaceSize = ta.getInt(R.styleable.LabelAndTextControl_spaceSize, (int) context.getResources().getDimension(R.dimen.lable_text_spaceSize)); setControlSpace(spaceSize); boolean enabled = ta.getBoolean(R.styleable.LabelAndTextControl_enabled, true); setEnabled(enabled); String labelText = ta.getString(R.styleable.LabelAndTextControl_labelText); setLabelText(labelText); String textText = ta.getString(R.styleable.LabelAndTextControl_textText); setTextText(textText); ta.recycle(); minTextWidth = (int) context.getResources().getDimension(R.dimen.lable_text_minTextWidth); Log.i(LogTag, ">>>>>>>>>>>>>over init"); showPaddingInfo(); }
3、重写onMeasure方法
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int width = getDefaultSize(mControlSpace, widthMeasureSpec); int height = getDefaultSize(0, heightMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize= MeasureSpec.getSize(heightMeasureSpec); TextPaint paint = new TextPaint(); Rect rect = new Rect(); paint.setTextSize(mTextSize); int minWidth = 20; int minHeight = 20; //mLabel paint.getTextBounds(mLabelText, 0, mLabelText.length(), rect); int lWidth = rect.width(); int lHeight = rect.height(); lWidth = Math.max(lWidth, minWidth); lHeight = Math.max(lHeight, minHeight); mLabel.measure(lWidth, lHeight);
//mText paint.getTextBounds(mTextText, 0, mTextText.length(), rect); int tWidth = rect.width(); int tHeight = rect.height(); tWidth = Math.max(tWidth, minWidth); tHeight= Math.max(tHeight, minHeight); mText.measure(tWidth, tHeight); setMeasuredDimension(width, height); }
4、重写onLayout方法
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int width = r - l; int height = b - t;
Log.i(LogTag, ">>>>>>>>>>>>>onLayout"); showPaddingInfo(); TextPaint mpaint = new TextPaint(); mpaint.setTextSize(mTextSize); Rect rect = new Rect(); mpaint.getTextBounds(mLabelText, 0, mLabelText.length(), rect); // int labelWidth = (int) Layout.getDesiredWidth(mLabelText, mpaint); int labelWidth = rect.width(); mLabel.layout(0, 0, labelWidth, height); int textStart = labelWidth + mControlSpace; int textRealHeight = mText.getHeight(); int pt = mText.getPaddingTop(); int pb = mText.getPaddingBottom(); textRealHeight = Math.max(textRealHeight, height); mText.layout(textStart, 0, width, textRealHeight); }
5、重写onDraw方法(我没有重写这个方法)
在Activity中使用LabelAndTextControl自定义View:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context="com.example.mycontrols.MainActivity" xmlns:app="http://schemas.android.com/apk/res/com.example.mycontrols" android:orientation="vertical" android:id="@+id/linear"> <com.example.mycontrols.lib.LabelAndTextControl android:id="@+id/lt3" android:layout_width="100dp" android:layout_height="20dp" app:labelText="性别:" app:textText="男" android:layout_margin="5dp" android:textSize="15sp"/> <com.example.mycontrols.lib.LabelAndTextControl android:id="@+id/lt4" android:layout_width="wrap_content" android:layout_height="wrap_content" app:labelText="性别:" app:textText="男" android:layout_margin="5dp" android:textSize="15sp"/> <com.example.mycontrols.lib.LabelAndTextControl android:id="@+id/lt5" android:layout_width="match_parent" android:layout_height="match_parent" app:labelText="性别:" app:textText="男" android:layout_margin="5dp" android:textSize="15sp"/> </LinearLayout>
效果如下:
小结:从上面的截图中可以看出,有如下三个比较严重的问题:第一,因为wrap_content占用了所有的空间,最后一个LabelAndTextControl空间没有显示出来。第二,在固定大小的时候,EditText显示不全。第三,TextView宽度不够,导致TextView显示不全。
三、解决第一个问题
因为wrap_content的时候,传递到LabelAndTextControl的onMeasure方法的width和height是match_parent的大小,导致LabelAndTextControl多大。所以要判断是wrap_content的话,要单独处理,wrap_content时,对应的MeasureSpec的AT_MOST,具体的对应关系在博客结尾处有说明。代码如下:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int width = getDefaultSize(mControlSpace, widthMeasureSpec); int height = getDefaultSize(0, heightMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize= MeasureSpec.getSize(heightMeasureSpec); TextPaint paint = new TextPaint(); Rect rect = new Rect(); paint.setTextSize(mTextSize); int minWidth = 20; int minHeight = 20; //mLabel paint.getTextBounds(mLabelText, 0, mLabelText.length(), rect); int lWidth = rect.width(); int lHeight = rect.height(); lWidth = Math.max(lWidth, minWidth); lHeight = Math.max(lHeight, minHeight); mLabel.measure(lWidth, lHeight); //mText paint.getTextBounds(mTextText, 0, mTextText.length(), rect); int tWidth = rect.width(); int tHeight = rect.height(); tWidth = Math.max(tWidth, minWidth); tHeight= Math.max(tHeight, minHeight); mText.measure(tWidth, tHeight); if (widthMode == MeasureSpec.EXACTLY) { width = widthSize; height = heightSize; } else { width = mLabel.getMeasuredWidth() + mControlSpace + mText.getMeasuredWidth(); height = Math.max(mLabel.getMeasuredHeight(), mText.getMeasuredHeight()); } setMeasuredDimension(width, height); }
效果如下:
四、解决第二个问题
EditText显示不全的问题,是因为EditText默认的情况下,会自动产生上、下、左、右4个padding,可以将padding打印出来。
private void showPaddingInfo() { Log.i(LogTag, ">>>>>>>>>>>>>mText"); Log.i(LogTag, ">>>>>>>>>>>>>" + mText.getPaddingLeft() + ">>>" + mText.getPaddingRight()); Log.i(LogTag, ">>>>>>>>>>>>>" + mText.getPaddingTop() + ">>>" + mText.getPaddingBottom()); Log.i(LogTag, ">>>>>>>>>>>>>" + mText.getPaddingStart() + ">>>" + mText.getPaddingEnd()); Log.i(LogTag, ">>>>>>>>>>>>>mLabel"); Log.i(LogTag, ">>>>>>>>>>>>>" + mLabel.getPaddingLeft() + ">>>" + mLabel.getPaddingRight()); Log.i(LogTag, ">>>>>>>>>>>>>" + mLabel.getPaddingTop() + ">>>" + mLabel.getPaddingBottom()); Log.i(LogTag, ">>>>>>>>>>>>>" + mLabel.getPaddingStart() + ">>>" + mLabel.getPaddingEnd()); }
可以打印出上下左右各为14、16、24、24。debug发现,EditText执行完构造函数的时候,4个方向的padding都是0,但是不知道构造函数执行完后又执行了什么操作,只要执行下一步,就会变成14、16、24、24。
找到了原因,可以使用以下方式来解决。在构造了EditText之后,设置四个方向的padding。
也可以使用另外的方式,像上面使用LabelAndTextControl设置wrap_content的处理方式,获取mLable和mText的高度,使LabelAndTextControl的高度等于LabelAndTextControl、mLabel、mText中最高的。
我使用的是第一种方式,比较直接,更贴近用户设置的大小。
截图如下:
五、解决第三个问题
在onLayout中使用的是通过字符的宽度来设置mLabel的显示宽度。不知道什么原因获取到的宽度为36,显示的时候就是上面的样子。而通过mLabel.getMeasuredWidth()获取到的值是72,大了一倍。通过获取mLabel.getMeasuredWidth()设置mLabel的大小。代码如下:
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int width = r - l; int height = b - t; Log.i(LogTag, ">>>>>>>>>>>>>onLayout"); showPaddingInfo(); TextPaint mpaint = new TextPaint(); mpaint.setTextSize(mTextSize); Rect rect = new Rect(); mpaint.getTextBounds(mLabelText, 0, mLabelText.length(), rect); Metrics metrics = BoringLayout.isBoring(mLabelText, mpaint); // int labelWidth = (int) Layout.getDesiredWidth(mLabelText, mpaint); int labelWidth = rect.width(); int labelRealWidth = mLabel.getPaddingLeft(); int labelRealHeight = mLabel.getHeight(); labelWidth = Math.max(labelWidth, labelRealWidth); int w2 = mLabel.getMeasuredWidth(); labelWidth = Math.max(labelWidth, w2); mLabel.layout(0, 0, labelWidth, height); int textStart = labelWidth + mControlSpace; int textRealHeight = mText.getHeight(); int pt = mText.getPaddingTop(); int pb = mText.getPaddingBottom(); textRealHeight = Math.max(textRealHeight, height); mText.layout(textStart, 0, width, textRealHeight); }
显示效果如下图:
对于wrap_content、match_parent、具体值三种方式显示的都很合适了。
六、关于测量字符串所占的宽度
测量字符串宽度有如下4中方法:
1、例子中的方式:view.getMeasuredWidth(),需要已经执行过measure,下面的说明中会提及此处。
2、使用Paint.getTextBounds(),例子中也有涉及,但是每次获取的值都比实际的要小。结果保存在该方法的参数Rect中
3、使用Paint.measureText(),需要设置Paint对应的TextSize,并将需要测量的文本作为参数传入该方法。宽度为返回值。
4、使用Paint.getTextWidths(),与3相同,也需要设置Paint对应的TextSize,并将需要测量的文本作为参数传入该方法。宽度保存在该方法的参数中,但是结果是一个数组,里面记录每个字符所占的宽度。
通过测试代码发现,1,3,4三种方法得到的结果相同,都大于2获取到的值。测试代码如下:
private void testPaintTextOnce() { final String str = mLabelText; TextPaint paint = mLabel.getPaint(); Rect bounds = new Rect(); float measureWidth ; int length = str.length(); float[] widths = new float[length]; float width; float labelMeasureWidth; StringBuilder sb; paint.getTextBounds(str, 0, length, bounds); measureWidth = paint.measureText(str); paint.getTextWidths(str, widths); sb = new StringBuilder(); width = 0; for (int j=0; j<length; j++) { sb.append(widths[j] + ","); width += widths[j]; } labelMeasureWidth = mLabel.getMeasuredWidth(); Log.e(tag , "Size:" + mTextSize + ",meaureText:" + measureWidth + ",Bound:" + bounds.width() + ",Width:" + width + ",labelMeasureWidth:" + labelMeasureWidth); }
我在该类外边动态增加TextSize,可以得到如下测试结果:
说明:
1、view.getMeasuredWidth()这个方法用于获取view的width,它的值和view.getWidth的值并不一定相同,至少我使用的过程中,这两个值是不同的。而且在onMeasure和onLayout的方法中,因为还没有开始绘制,view.getWidth()获取到的值为0,但是getMeasuredWidth()可以获取到值。但是有一个前提,要先执行了view.measure方法。
2、第一段代码的onMeasure方法中已经使用了getMeasuredWidth方法,但是onLayout却没有使用getMeasuredWdith。其实,在开始的时候,我并没有在onMeasure中使用getMeasuredWidth方法,后来没有恢复成开始的代码。
3、MeasureSpec类
MeasureSpec类有三个方法,getMode、getSize、makeMeasureSpec。分别用于获取模式、获取大小、生成MeasureSpec。
给View设置的高度、宽度值,与MeasureSpec中的AT_MOST、EXACTLY有一定的关联关系:
宽度、高度值与MeasureSpec最简单的映射关系是:
* wrap_parent -> MeasureSpec.AT_MOST
* match_parent -> MeasureSpec.EXACTLY
* 具体值 -> MeasureSpec.EXACTLY