毕业即将五年了,博客开的不算晚,但也没认真写过几篇文章。趁着五年之际,梳理下自己的知识点。

这段时间先从自定义view理起。

自定义view作为一个衡量合格程序员的标准,一直以来也算是从码农晋级高级程序员的一块试金石。

以我个人的理解,自定义view可以划为两种。一是自定义组件,即在现有的安卓提供的控件的基础上,糅合几种控件,提供实现工程所需功能并符合工程ui统一规范的组件。第二种就是对view或者安卓系统提供的控件进行扩展,以实现特定的功能。我们主要来讲解第二种。

今天先来实现一个最简单的自定义view,一个类似TextView的字符串居中显示的view。通过实现该view,来初窥自定义view。

自定义view一般分为四步:

1:自定义属性。即在工程values文件夹下新建attrs文件,并在其中声明所需的属性

2:在我们自定义view的构造方法中获取自定义属性

3:重写onMeasure()方法,测量view尺寸(不是必需)

4:重写onDraw()方法,绘制view

下面按步骤来,依次贴出对应的代码

先看第一步

<attr name="text" format="string" />
<attr name="color" format="color" />
<attr name="textsize" format="dimension" />

<declare-styleable name="FirstDefinedComponent">
    <attr name="text" />
    <attr name="color" />
    <attr name="textsize" />
</declare-styleable>

自定义了字体,字体颜色,字体大小3个属性,format是属性的取值类型

然后创建我们的自定义view,并在其构造方法中获取我们自定义的属性

public class FirstDefinedComponent extends View {
    private String mText;
    private float mTextSize;
    private int mTextColor;
    private Paint mPaint;
    private Rect mBound;
    public FirstDefinedComponent(Context context) {
        this(context,null);
    }

    public FirstDefinedComponent(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs,0);
    }

    public FirstDefinedComponent(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initData(context,attrs,defStyleAttr);
    }

    /**
     * 初始化数据
     * @param context
     * @param attrs
     * @param defStyleAttr
     */
    private void initData(Context context, @Nullable AttributeSet attrs, int defStyleAttr){
        setListener();
        TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.FirstDefinedComponent,defStyleAttr,0);
        int n = a.getIndexCount();
        for(int i = 0;i < n;i++){
            int attr = a.getIndex(i);
          switch (a.getIndex(i)){
              case R.styleable.FirstDefinedComponent_text:
                  mText = a.getString(attr);
                  break;
              case R.styleable.FirstDefinedComponent_color:
                  mTextColor = a.getColor(attr, Color.BLACK);
                  break;
              case R.styleable.FirstDefinedComponent_textsize:
                  mTextSize = a.getDimension(attr,TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,16,getResources().getDisplayMetrics()));
                  break;
          }
        }
        a.recycle();
        mPaint = new Paint();
        mPaint.setTextSize(mTextSize);
        mBound = new Rect();
        mPaint.getTextBounds(mText,0,mText.length(),mBound);
    }

其中mBound是为了来计算自定义view内容边界。方面下面来确认绘制字符串范围。

接下来重写onDraw()方法

@Override
  1. protected void onDraw(Canvas canvas) {
  2. mPaint.setColor(Color.YELLOW);
  3. canvas.drawRect(0,0,getMeasuredWidth(),getMeasuredHeight(),mPaint);
  4. mPaint.setColor(mTextColor);
  5. canvas.drawText(mText,getWidth()/2 - mBound.width()/2,getHeight()/2 + mBound.height()/2,mPaint);
  6. }

2、3行是绘制自定义view边界,边界取系统计算尺寸大小。4、5行绘制view内容。

此时我们在布局中引入我们自定义的view

<definedview.FirstDefinedComponent
    android:layout_width="200dp"
    android:layout_height="100dp"
    android:padding="20dp"
    app:color="@color/colorAccent"
    app:text="1234"
    app:textsize="30dp"/>

color、text、textsize即为我们自定义的属性,运行效果如下

自定义view(一)

已经实现了我们的刚开始要求实现的功能。但是如果我们在布局中修改自定义view的宽高为wrap_conten,此时再次运行,会发现如下结果

自定义view(一)

这是因为当我们设置明确的宽度和高度时,系统帮我们测量的结果就是我们设置的结果,当我们设置为WRAP_CONTENT,或者MATCH_PARENT系统帮我们测量的结果就是MATCH_PARENT的长度。

所以为了防止这种情况,我们要在设置WRAP_CONTENT时自己测量view的大小,所以需要重写onMeasure()方法,如下:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int width,height;
    int wideMode = MeasureSpec.getMode(widthMeasureSpec);
    int wideSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    if(MeasureSpec.EXACTLY == wideMode){
        width = wideSize;
    }else{
        mPaint.getTextBounds(mText,0,mText.length(),mBound);
        width = getPaddingLeft() + mBound.width() + getPaddingRight();
    }
    if(MeasureSpec.EXACTLY == heightMode){
        height = heightSize;
    }else {
        mPaint.getTextBounds(mText,0,mText.length(),mBound);
        height = getPaddingTop() + mBound.height() + getPaddingBottom();
    }
    setMeasuredDimension(width,height);
}

MeasureSpec的specMode,一共有三种类型:

EXACTLY:一般是设置了明确的值或者是MATCH_PARENT

AT_MOST:表示子布局限制在一个最大值内,一般为WARP_CONTENT

UNSPECIFIED:表示子布局想要多大就多大,很少使用

所以重写的onMeasure方法中,当为EXACTLY时,我们取系统帮我们测量的大小,其他情况下,我们就自己测量view的宽高,并调用setMeasuredDimension()方法,其值即为onDraw()方法中传入的参数。

现在我们再次运行布局中宽高为WRAP_CONTENT的情况,运行结果就正常了

相关文章: