前言:最近有的招聘要求有要熟悉view的绘制原理,我也一直想看看自定义控件是怎么弄的,好久之前学的都忘了,现在看来其实两者是强相关的,不熟悉绘制原理,自定义控件也是徒劳。
参考博客:点击打开链接
demo地址:点击打开链接
1. hello world
学习什么新东西hello world是必不可少的,参考上面的博客先写个hello world的自定义View玩玩。
demo:
package com.example.demo_21_custom_view; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; import android.util.AttributeSet; import android.view.View; public class HelloWorldTextView extends View { /** * 需要绘制的文字 */ private String mText; /** * 文本的颜色 */ private int mTextColor; /** * 文本的大小 */ private int mTextSize; /** * 绘制时控制文本绘制的范围 */ private Rect mBound; private Paint mPaint; public HelloWorldTextView(Context context) { this(context, null); } public HelloWorldTextView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public HelloWorldTextView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mText = "hello world"; mTextColor = Color.BLACK; mTextSize = 100; mPaint = new Paint(); mPaint.setTextSize(mTextSize); mPaint.setColor(mTextColor); //获得绘制文本的宽和高 mBound = new Rect(); mPaint.getTextBounds(mText, 0, mText.length(), mBound); } @Override protected void onDraw(Canvas canvas) { //绘制文字 canvas.drawText(mText, getWidth() / 2 - mBound.width() / 2, getHeight() / 2 + mBound.height() / 2, mPaint); } }
这个onDraw方法就是将文字显示在组件的中心位置。
布局文件:
<?xml version="1.0" encoding="utf-8"?> <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" tools:context=".MainActivity"> <com.example.demo_21_custom_view.HelloWorldTextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="20sp" android:text="" android:background="#ff0000"/> </LinearLayout>
2. 自定义属性
其实上面view的构造方法有很多是写死的,没有从配置文件中配置,这是就需要自定义属性登场了。
2.1 配置文件
在res/values文件夹下新建attrs.xml,如下所示:
attrs.xml:
<?xml version="1.0" encoding="utf-8"?> <resources> <attr name="mText" format="string" /> <attr name="mTextColor" format="color" /> <attr name="mTextSize" format="dimension" /> <declare-styleable name="MyTextView"> <attr name="mText"/> <attr name="mTextColor"/> <attr name="mTextSize"/> </declare-styleable> </resources>
2.2 布局文件引用
添加自定义的的命名空间(jiatai:xmlns:openxu="http://schemas.android.com/apk/res-auto"
),并用自己的命名空间设置自己需要的属性,试了下名称无所谓,属性名称对的上就行。
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:jiatai="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <com.example.demo_21_custom_view.HelloWorldTextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="20sp" android:text="" android:background="#ff0000" jiatai:mTextSize="25sp" jiatai:mText="hello world attr" jiatai:mTextColor ="#0000ff"/> </LinearLayout
2.3 修改构造方法
将属性写死改为从布局文件中读取
public HelloWorldTextView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); /*mText = "hello world"; mTextColor = Color.BLACK; mTextSize = 100;*/ //获取自定义属性的值 TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.MyTextView, defStyleAttr, 0); mText = a.getString(R.styleable.MyTextView_mText); mTextColor = a.getColor(R.styleable.MyTextView_mTextColor, Color.BLACK); mTextSize = a.getDimension(R.styleable.MyTextView_mTextSize, 100); a.recycle(); //注意回收 mPaint = new Paint(); mPaint.setTextSize(mTextSize); mPaint.setColor(mTextColor); //获得绘制文本的宽和高 mBound = new Rect(); mPaint.getTextBounds(mText, 0, mText.length(), mBound); }
2.4 效果
可以看出字体颜色和字符串都得到了相应的改变
3. 重写onMeasure方法
参考博客中提及之前的自定义控件虽然声明是wrap_content的但实际效果是match_parent,修改方法就是重写onMeasure。
3.1 对应代码
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); //获取宽的模式 int heightMode = MeasureSpec.getMode(heightMeasureSpec); //获取高的模式 int widthSize = MeasureSpec.getSize(widthMeasureSpec); //获取宽的尺寸 int heightSize = MeasureSpec.getSize(heightMeasureSpec); //获取高的尺寸 Log.v(TAG, "宽的模式:"+widthMode); Log.v(TAG, "高的模式:"+heightMode); Log.v(TAG, "宽的尺寸:"+widthSize); Log.v(TAG, "高的尺寸:"+heightSize); int width; int height ; if (widthMode == MeasureSpec.EXACTLY) { //如果match_parent或者具体的值,直接赋值 width = widthSize; } else { //如果是wrap_content,我们要得到控件需要多大的尺寸 float textWidth = mBound.width(); //文本的宽度 //控件的宽度就是文本的宽度加上两边的内边距。内边距就是padding值,在构造方法执行完就被赋值 width = (int) (getPaddingLeft() + textWidth + getPaddingRight()); Log.v(TAG, "文本的宽度:"+textWidth + "控件的宽度:"+width); } //高度跟宽度处理方式一样 if (heightMode == MeasureSpec.EXACTLY) { height = heightSize; } else { float textHeight = mBound.height(); height = (int) (getPaddingTop() + textHeight + getPaddingBottom()); Log.v(TAG, "文本的高度:"+textHeight + "控件的高度:"+height); } //保存测量宽度和测量高度 setMeasuredDimension(width, height); }
3.2 对应log
3.3 效果图
4. 实现自动换行
研究了下参考博客的代码,稍微重构并修改了下我发现有误的地方,自动换行其实还是由onMeasure算出控件的宽和高,由onDraw画出所有字符。
4.1 过长字符串分行
private void initTextToLines(int widthSize){ float textWidth = mBound.width(); //文本的宽度 if(mTextList.size()==0){ //将文本分段 int padding = getPaddingLeft() + getPaddingRight(); int specWidth = widthSize - padding; //能够显示文本的最大宽度 if(textWidth<specWidth){ //说明一行足矣显示 lineNum = 1; mTextList.add(mText); }else{ //超过一行 isOneLines = false; spLineNum = textWidth/specWidth; if((spLineNum+"").contains(".")){ lineNum = Integer.parseInt((spLineNum+"").substring(0,(spLineNum+"").indexOf(".") ))+1; }else{ lineNum = spLineNum; } int lineLength = (int)(mText.length()/spLineNum); Log.v(TAG, "文本:"+mText); Log.v(TAG, "文本总长度:"+mText.length()); Log.v(TAG, "能绘制文本的宽度:"+lineLength); Log.v(TAG, "需要绘制:"+lineNum+"行"); Log.v(TAG, "lineLength:"+lineLength); for(int i = 0; i<lineNum; i++){ String lineStr; if(mText.length()<lineLength){ lineStr = mText.substring(0, mText.length()); }else{ lineStr = mText.substring(0, lineLength); } Log.v(TAG, "lineStr:"+lineStr); mTextList.add(lineStr); if(!TextUtils.isEmpty(mText)) { if(mText.length()<lineLength){ //mText = mText.substring(0, mText.length()); }else{ mText = mText.substring(lineLength, mText.length()); } }else{ break; } } } } }
4.2 根据行数和字符串宽度设置组件高和宽
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); //获取宽的模式 int heightMode = MeasureSpec.getMode(heightMeasureSpec); //获取高的模式 int widthSize = MeasureSpec.getSize(widthMeasureSpec); //获取宽的尺寸 int heightSize = MeasureSpec.getSize(heightMeasureSpec); //获取高的尺寸 Log.v(TAG, "宽的模式:"+widthMode); Log.v(TAG, "高的模式:"+heightMode); Log.v(TAG, "宽的尺寸:"+widthSize); Log.v(TAG, "高的尺寸:"+heightSize); initTextToLines(widthSize); int width; int height ; if (widthMode == MeasureSpec.EXACTLY) { //如果match_parent或者具体的值,直接赋值 width = widthSize; } else { //如果是wrap_content,我们要得到控件需要多大的尺寸 float textWidth = mBound.width(); if(isOneLines){ //控件的宽度就是文本的宽度加上两边的内边距。内边距就是padding值,在构造方法执行完就被赋值 width = (int) (getPaddingLeft() + textWidth + getPaddingRight()); }else{ //如果是多行,说明控件宽度应该填充父窗体 width = widthSize; } Log.v(TAG, "文本的宽度:"+textWidth + "控件的宽度:"+width); } //高度跟宽度处理方式一样 if (heightMode == MeasureSpec.EXACTLY) { height = heightSize; } else { float textHeight = mBound.height(); if(isOneLines){ height = (int) (getPaddingTop() + textHeight + getPaddingBottom()); }else{ //如果是多行 height = (int) (getPaddingTop() + textHeight*lineNum + getPaddingBottom());; } Log.v(TAG, "文本的高度:"+textHeight + "控件的高度:"+height); } //保存测量宽度和测量高度 setMeasuredDimension(width, height); }
4.3 绘制多行字符串
@Override protected void onDraw(Canvas canvas) { //绘制文字 //canvas.drawText(mText, getWidth() / 2 - mBound.width() / 2, getHeight() / 2 + mBound.height() / 2, mPaint); //绘制文字 for(int i = 0; i<mTextList.size(); i++){ mPaint.getTextBounds(mTextList.get(i), 0, mTextList.get(i).length(), mBound); Log.v(TAG, "mBound.h:"+mBound.height()); Log.v(TAG, "在X:" + (getWidth() / 2 - mBound.width() / 2)+" Y:"+(getPaddingTop() + (mBound.height() *i))+" 绘制:"+mTextList.get(i)); canvas.drawText(mTextList.get(i), (getWidth() / 2 - mBound.width() / 2), (getPaddingTop() + (mBound.height() *i)), mPaint); } }
4.4 效果
4.5 改为靠左对齐
改一下onDraw画字符串的起始位置就好了,不要中间,要边界,即getPaddingLeft()
@Override protected void onDraw(Canvas canvas) { //绘制文字 //canvas.drawText(mText, getWidth() / 2 - mBound.width() / 2, getHeight() / 2 + mBound.height() / 2, mPaint); //绘制文字 for(int i = 0; i<mTextList.size(); i++){ mPaint.getTextBounds(mTextList.get(i), 0, mTextList.get(i).length(), mBound); Log.v(TAG, "mBound.h:"+mBound.height()); Log.v(TAG, "在X:" + (getWidth() / 2 - mBound.width() / 2)+" Y:"+(getPaddingTop() + (mBound.height() *i))+" 绘制:"+mTextList.get(i)); canvas.drawText(mTextList.get(i), (getPaddingLeft()), (getPaddingTop() + (mBound.height() *i)), mPaint); } }效果:
4.6 改为靠右对齐
将x改成靠左对齐时离右边的距离即可,即改一下onDraw方法x的设定,改为
(getWidth() - mBound.width() - getPaddingLeft())
效果:
emmm,还是能玩得起来的嘛
5. 总结
今天主要就是学习下自定义控件大致怎么做,不想深入到源码层面,简单看来就是onMeasure算出控件高和宽,onDraw进行控件具体内容的绘制。这其实都是就控件本身讨论的,没有将控件放置于布局中进行讨论,控件位于何地如何确定也未提及,待续。。。