自定义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 

 

posted @ 2016-07-17 14:43  环游世界  阅读(1303)  评论(0编辑  收藏  举报