自定义View从入门到放弃(一)
感谢 鸿洋大神 Android 自定义View (一)
效果图,类似TextView控件,增加点击时产生随机数。
随便唠叨几句,学Android也有两年了,现在才来真正的学习自定义View,真够搞笑的。
自定义View步骤:
1、attrs.xml 中自定义View的属性
2、在View的构造方法中获取我们的属性
3、重新onMesure
4、重写onDraw
第三步不是必须的。
准备工作
1、分析需要定义的属性
从效果图来看,我们要添加的属性大致需要这几个 textContent
、textSize
、textColor
2、实现逻辑
需求比较简单,就是点击切换随机数,实现view的点击事件即可
开始
1、在res/values目录下新建 attrs.xml文件添加自定义属性
<declare-styleable name="CustomViewStyle">
<attr name="textSize" format="dimension" />
<attr name="textColor" format="color" />
<attr name="textContent" format="string" />
</declare-styleable>
从name可以看出自定义字体颜色、字体大小、字体内容三个属性,其中format分别表示该属性可取值的类型
这里留个坑,后面针对format去整理一下
2、新建View,在构造方法中获取属性值
public class CustomTextView extends View {
private static final String TAG = "CustomView";
/**
* 文本内容
*/
private String mTextContent;
/**
* 文本颜色
*/
private int mTextColor;
/**
* 文本大小
*/
private float mTextSize;
public Context mContext;
private Rect mBound;
/**
* 画笔
*/
private Paint mPaint;
/**
* 直接new一个Custom View 实例的时候,会调用第一个构造函数
*/
public CustomTextView(Context context) {
this(context, null);
}
/**
* 在xml布局文件中调用Custom View的时候,会调用第二个构造函数
*/
public CustomTextView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
LogUtil.d(TAG + "--CustomView 2");
}
public CustomTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
LogUtil.d(TAG + "--CustomView 3");
// 方式一
// TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CustomViewStyle, defStyleAttr, 0);
// 方式二
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomViewStyle);
mContext = context;
initView(context, typedArray);
}
public void initView(Context context, TypedArray typedArray) {
// 获取设置属性
mTextContent = typedArray.getString(R.styleable.CustomViewStyle_textContent);
mTextColor = typedArray.getColor(R.styleable.CustomViewStyle_textColor, ContextCompat.getColor(context, R.color.colorAccent));
mTextSize = typedArray.getDimension(R.styleable.CustomViewStyle_textSize, 15);
// typedArray 回收
typedArray.recycle();
mPaint = new Paint();
mPaint.setTextSize(mTextSize);
mBound = new Rect();
mPaint.getTextBounds(mTextContent, 0, mTextContent.length(), mBound);
}
});
}
....
3、重写onDraw方法
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 画文本背景
mPaint.setColor(ContextCompat.getColor(mContext, R.color.default_color));
canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint);
/**
* 获取布局宽高
*/
int width = getWidth();
int height = getHeight();
// 重新获取文本大小
mPaint.setTextSize(mTextSize);
mPaint.getTextBounds(mTextContent, 0, mTextContent.length(), mBound);
int boundWidth = mBound.width();
int boundHeight = mBound.height();
LogUtil.d(TAG + "--onDraw width=" + width + ",height=" + height);
LogUtil.d(TAG + "--onDraw boundWidth=" + boundWidth + ",boundHeight=" + boundHeight);
mPaint.setColor(mTextColor);
canvas.drawText(mTextContent, getWidth() / 2 - mBound.width() / 2, getHeight() / 2 + mBound.height() / 2, mPaint);
}
在xml中添加自定义的CustomTextView
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!--通过自定义字段调用自定义属性,一定要在最外层布局上添加 xmlns:app="http://schemas.android.com/apk/res-auto"-->
<com.evan.evanzchcustomview.view.CustomTextView
android:layout_width="300dp"
android:layout_height="200dp"
app:textColor="@color/colorPrimary"
app:textContent="1234"
app:textSize="34sp" />
</RelativeLayout>
这个时候查看好像没问题,但是如果我们分别设置自定义View的宽高为wrap_content
显示不符合预期,明明设置的是wrap_content,结果却是全屏显示,原因是因为系统帮我们测量的高度和宽度都是MATCH_PARNET,当我们设置明确的宽度和高度时,系统帮我们测量的结果就是我们设置的结果,当我们设置为WRAP_CONTENT,或者MATCH_PARENT系统帮我们测量的结果就是MATCH_PARENT的长度。
所以,设置WRAP_CONTENT 要想正确显示,就要重写onMesure 方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
LogUtil.d(TAG + "--onMeasure textContent=" + mTextContent);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width;
int height;
// 如果模式为EXACTLY、即指定特定确切的大小,宽度就为设置的宽度
if (widthMode == MeasureSpec.EXACTLY) {
LogUtil.d(TAG + "--widthMode EXACTLY");
width = widthSize;
} else {
// 如果模式为AT_MOST,如wrap_content,则宽度为文本宽度+文本左右padding值
LogUtil.d(TAG + "--widthMode AT_MOST");
// 获取文本的宽高
int paddingStart = getPaddingStart();
int paddingEnd = getPaddingEnd();
mPaint.setTextSize(mTextSize);
mPaint.getTextBounds(mTextContent, 0, mTextContent.length(), mBound);
width = mBound.width() + paddingStart + paddingEnd;
}
// 高度通宽度一致
if (heightMode == MeasureSpec.EXACTLY) {
LogUtil.d(TAG + "--heightMode EXACTLY");
height = heightSize;
} else {
LogUtil.d(TAG + "--heightMode AT_MOST");
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();
mPaint.setTextSize(mTextSize);
mPaint.getTextBounds(mTextContent, 0, mTextContent.length(), mBound);
height = mBound.height() + paddingTop + paddingBottom;
}
setMeasuredDimension(width, height);
}
上面代码涉及到 MeasureSpec 的三种模式
- UNSPECIFIED:不对View大小做限制,如:ListView,ScrollView
- EXACTLY:确切的大小,如:100dp或者march_parent
- AT_MOST:大小不可超过某数值,如:wrap_content
在运行程序,发现效果和预期一致
最后要实现点击切换数字
在构造方法中设置监听事件,点击时切换文本,再通过postInvalidate刷新界面即可
this.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
randomText();
// Android的invalidate与postInvalidate都是用来刷新界面的。
//在UI主线程中,用invalidate();本质是调用View的onDraw()绘制。
//主线程之外,用postInvalidate()。
postInvalidate();
}
});
随机生成四个数字
public void randomText() {
Set<Integer> integerSet = new HashSet<>();
Random random = new Random();
while (integerSet.size() < 4) {
int i = random.nextInt(10);
integerSet.add(i);
}
StringBuilder stringBuilder = new StringBuilder();
for (Integer num : integerSet) {
stringBuilder.append(num);
}
mTextContent = stringBuilder.toString();
}
最后就大功告成,实现文章开头的那种效果,最后贴一下全部代码
public class CustomTextView extends View {
private static final String TAG = "CustomView";
/**
* 文本内容
*/
private String mTextContent;
/**
* 文本颜色
*/
private int mTextColor;
/**
* 文本大小
*/
private float mTextSize;
public Context mContext;
private Rect mBound;
/**
* 画笔
*/
private Paint mPaint;
/**
* 直接new一个Custom View 实例的时候,会调用第一个构造函数
*/
public CustomTextView(Context context) {
this(context, null);
}
/**
* 在xml布局文件中调用Custom View的时候,会调用第二个构造函数
*/
public CustomTextView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
LogUtil.d(TAG + "--CustomView 2");
}
public CustomTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
LogUtil.d(TAG + "--CustomView 3");
// 方式一
// TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CustomViewStyle, defStyleAttr, 0);
// 方式二
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomViewStyle);
mContext = context;
initView(context, typedArray);
}
public void initView(Context context, TypedArray typedArray) {
// 获取设置属性
mTextContent = typedArray.getString(R.styleable.CustomViewStyle_textContent);
mTextColor = typedArray.getColor(R.styleable.CustomViewStyle_textColor, ContextCompat.getColor(context, R.color.colorAccent));
mTextSize = typedArray.getDimension(R.styleable.CustomViewStyle_textSize, 15);
typedArray.recycle();
mPaint = new Paint();
mPaint.setTextSize(mTextSize);
mBound = new Rect();
mPaint.getTextBounds(mTextContent, 0, mTextContent.length(), mBound);
this.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
randomText();
// Android的invalidate与postInvalidate都是用来刷新界面的。
//在UI主线程中,用invalidate();本质是调用View的onDraw()绘制。
//主线程之外,用postInvalidate()。
postInvalidate();
}
});
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPaint.setColor(ContextCompat.getColor(mContext, R.color.default_color));
canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint);
/**
* 获取布局宽高
*/
int width = getWidth();
int height = getHeight();
// 重新获取文本大小
mPaint.setTextSize(mTextSize);
mPaint.getTextBounds(mTextContent, 0, mTextContent.length(), mBound);
int boundWidth = mBound.width();
int boundHeight = mBound.height();
LogUtil.d(TAG + "--onDraw width=" + width + ",height=" + height);
LogUtil.d(TAG + "--onDraw boundWidth=" + boundWidth + ",boundHeight=" + boundHeight);
mPaint.setColor(mTextColor);
canvas.drawText(mTextContent, getWidth() / 2 - mBound.width() / 2, getHeight() / 2 + mBound.height() / 2, mPaint);
}
/**
* EXACTLY:一般是设置了明确的值或者是MATCH_PARENT
* AT_MOST:表示子布局限制在一个最大值内,一般为WARP_CONTENT
* UNSPECIFIED:表示子布局想要多大就多大,很少使用
*
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
LogUtil.d(TAG + "--onMeasure textContent=" + mTextContent);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width;
int height;
if (widthMode == MeasureSpec.EXACTLY) {
LogUtil.d(TAG + "--widthMode EXACTLY");
width = widthSize;
} else {
LogUtil.d(TAG + "--widthMode AT_MOST");
// 获取文本的宽高
int paddingStart = getPaddingStart();
int paddingEnd = getPaddingEnd();
mPaint.setTextSize(mTextSize);
mPaint.getTextBounds(mTextContent, 0, mTextContent.length(), mBound);
width = mBound.width() + paddingStart + paddingEnd;
}
if (heightMode == MeasureSpec.EXACTLY) {
LogUtil.d(TAG + "--heightMode EXACTLY");
height = heightSize;
} else {
LogUtil.d(TAG + "--heightMode AT_MOST");
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();
mPaint.setTextSize(mTextSize);
mPaint.getTextBounds(mTextContent, 0, mTextContent.length(), mBound);
height = mBound.height() + paddingTop + paddingBottom;
}
setMeasuredDimension(width, height);
}
public void randomText() {
Set<Integer> integerSet = new HashSet<>();
Random random = new Random();
while (integerSet.size() < 4) {
int i = random.nextInt(10);
integerSet.add(i);
}
StringBuilder stringBuilder = new StringBuilder();
for (Integer num : integerSet) {
stringBuilder.append(num);
}
mTextContent = stringBuilder.toString();
}
}
网友评论