毕业即将五年了,博客开的不算晚,但也没认真写过几篇文章。趁着五年之际,梳理下自己的知识点。
这段时间先从自定义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
- protected void onDraw(Canvas canvas) {
- mPaint.setColor(Color.YELLOW);
- canvas.drawRect(0,0,getMeasuredWidth(),getMeasuredHeight(),mPaint);
- mPaint.setColor(mTextColor);
- canvas.drawText(mText,getWidth()/2 - mBound.width()/2,getHeight()/2 + mBound.height()/2,mPaint);
- }
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的宽高为wrap_conten,此时再次运行,会发现如下结果
这是因为当我们设置明确的宽度和高度时,系统帮我们测量的结果就是我们设置的结果,当我们设置为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的情况,运行结果就正常了