1. 入门实例 —— 自定义TextView
基于前边的 [自定义View简介 - onMeasure()、onDraw()、自定义属性(ST)],那么这节课我们写一个自定义TextView作为入门自定义View的一个入门。
2. 效果图
3. onMeasure()方法 —— 测量文字宽高
测量文字
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 如果自定义TextView中的宽高给的是确定的值,比如10dp、20dp、match_parent,这个时候不需要计算,给的多少就是多少
// 如果自定义TextView中的宽高给的是wrap_content,则需要计算宽高
int widthMode = MeasureSpec.getMode(widthMeasureSpec) ;
int heightMode = MeasureSpec.getMode(heightMeasureSpec) ;
// 1\. 如果文字的大小给的是确定的值,比如10dp、20dp、match_parent,这个时候不需要计算,给的多少就是多少
int width = MeasureSpec.getSize(widthMeasureSpec) ;
// 2\. 如果文字的大小给的是wrap_content,则需要计算大小
if (widthMode == MeasureSpec.AT_MOST){
// 区域
// 计算的宽度 与字体的大小、字体长度有关
Rect bounds = new Rect() ;
// 获取TextView文本的区域
// 参数1:要测量的文字 参数2:表示从位置0开始 参数3:表示到整个文字的长度
mPaint.getTextBounds(mText , 0 , mText.length() , bounds);
width = bounds.width() + getPaddingLeft() + getPaddingRight() ;
}
int height = MeasureSpec.getSize(heightMeasureSpec) ;
if (heightMode == MeasureSpec.AT_MOST){
// 区域
Rect bounds = new Rect() ;
mPaint.getTextBounds(mText , 0 , mText.length() , bounds);
height = bounds.height() + getPaddingTop() + getPaddingBottom() ;
}
// 设置控件的宽高,这里就是给文字设置宽高
setMeasuredDimension(width , height);
}
4. onDraw()方法 ——绘制文字
画文字 —— drawText(mText , x, baseLine, mPaint);
参数1:画的文字;
参数2:是起点;
int x = getPaddingLeft() ;
参数3:基线baseLine
// dy:是文字高度的一半到基线baseLine的位置
// top:是baseLine到文字顶部的距离,是一个负值
// bottom:是baseLine到文字底部的距离,是一个正值
Paint.FontMetricsInt fontMetrics = mPaint.getFontMetricsInt();
int dy = (fontMetrics.bottom - fontMetrics.top)/2 - fontMetrics.bottom ;
int baseLine = getHeight()/2 + dy ;
参数4:画笔
代码如下:
/**
* 绘制文字
* @param canvas
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 中心点:getHeight()/2
// 参数1:文字 参数2:x 参数3:y 参数4:画笔
// x:是文字开始的距离
// y:是基线 baseLine 是要求的? getHeight()/2是中心位置 已知
// dy:是高度的一半到基线baseLine的位置
// top:是baseLine到文字顶部的距离,是一个负值
// bottom:是baseLine到文字底部的距离,是一个正值
Paint.FontMetricsInt fontMetrics = mPaint.getFontMetricsInt();
int dy = (fontMetrics.bottom - fontMetrics.top)/2 - fontMetrics.bottom ;
int baseLine = getHeight()/2 + dy ;
int x = getPaddingLeft() ;
canvas.drawText(mText , x, baseLine, mPaint);
}
5. 思考:如果让自定义的TextView继承 LinearLayout,请问文字能否出来?
不能出来,LinearLayout属于ViewGroup,而ViewGroup默认不会调用onDraw()方法,就比如有时候我们在自定义ViewGroup中想要画的东西画不出来,因为它不会触发onDraw()方法。
平时我们都是去说调用onDraw()方法去绘制文字、绘制其他东西,分析源码可以知道其实是调用draw(Canvas canvas)方法,我们自定义View都是继承自View,而在源码中View调用draw()方法,并且draw()方法中采用了 模板设计模式,里边有以下几个方法
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
要想让文字能出来,看下边的源码分析:
1>:必须要让dirtyOpaque为false,因为如果dirtyOpaque为false,那么if (!dirtyOpaque) 为true,那么onDraw(canvas);方法才会执行;
2>:而dirtyOpaque 是由privateFlags决定的,而privateFlags就是mPrivateFlags,这句代码决定的:
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
3>:而mPrivateFlags是这样赋值的,在View的构造方法中调用 computeOpaqueFlags()方法:
/**
* @hide
*/
protected void computeOpaqueFlags() {
// Opaque if: 如果你有一个background背景,并且这个背景是不透明的,并且没有scrollbars之类的
// - Has a background
// - Background is opaque
// - Doesn't have scrollbars or scrollbars overlay
if (mBackground != null && mBackground.getOpacity() == PixelFormat.OPAQUE) {
mPrivateFlags |= PFLAG_OPAQUE_BACKGROUND;
} else {
mPrivateFlags &= ~PFLAG_OPAQUE_BACKGROUND;
}
final int flags = mViewFlags;
if (((flags & SCROLLBARS_VERTICAL) == 0 && (flags & SCROLLBARS_HORIZONTAL) == 0) ||
(flags & SCROLLBARS_STYLE_MASK) == SCROLLBARS_INSIDE_OVERLAY ||
(flags & SCROLLBARS_STYLE_MASK) == SCROLLBARS_OUTSIDE_OVERLAY) {
mPrivateFlags |= PFLAG_OPAQUE_SCROLLBARS;
} else {
mPrivateFlags &= ~PFLAG_OPAQUE_SCROLLBARS;
}
}
上边是让 自定义TextView继承自 LinearLayout,而LinearLayout又继承自ViewGroup,所以这里这样去写:
为什么自定义TextView继承自 ViewGroup时,文字不能出来?
是因为 在ViewGroup源码中的构造方法 调用了initVIewGroup()方法,而initViewGroup()方法是这样的:
private void initViewGroup() {
// ViewGroup doesn't draw by default
if (!debugDraw()) {
setFlags(WILL_NOT_DRAW, DRAW_MASK);
}
}
这里的setFlags(WILL_NOT_DRAW, DRAW_MASK);方法是 View的方法,意思就是 你不要给我draw,而在setFlags()方法中会重新给mPrivateFlags赋值的,
所以initViewGroup() 方法会导致 mPrivateFlags会被重新赋值,而一旦mPrivateFlags值被改变,那么这个if语句就进不来,所以就不会调用onDraw()方法,所以文字就不会出来;
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
但是如果你在布局文件中设置了background背景时,文字就会出来,是因为当你调用setBackgroundDrawable()方法时,computeOpaqueFlags()方法又会被重新调用,就又会去重新计算一次,所以只要你一设置背景,文字就会出来。
4>:那么怎样可以解决这个问题?
思路就是: 只要改变mPrivateFlags值就可以了
方法一:复写dispatchDraw()方法;
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
// 中心点:getHeight()/2
// 参数1:文字 参数2:x 参数3:y 参数4:画笔
// x:是文字开始的距离
// y:是基线 baseLine 是要求的? getHeight()/2是中心位置 已知
// dy:是高度的一半到基线baseLine的位置
// top:是baseLine到文字顶部的距离,是一个负值
// bottom:是baseLine到文字底部的距离,是一个正值
Paint.FontMetricsInt fontMetrics = mPaint.getFontMetricsInt();
int dy = (fontMetrics.bottom - fontMetrics.top)/2 - fontMetrics.bottom ;
int baseLine = getHeight()/2 + dy ;
int x = getPaddingLeft() ;
canvas.drawText(mText , x, baseLine, mPaint);
}
方法二:设置透明的背景,前提是别人没有设置背景否则你会把别人的背景给覆盖;
因为只要你设置背景,不管是在布局文件中设置android:background="#00FF00"还是在代码中设置背景,它都会重新去调用computeOpaqueFlags()方法,重新去计算的;
public TextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// 获取自定义属性
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.TextView);
// 获取文字
mText = array.getString(R.styleable.TextView_jackchentext) ;
// 获取文字颜色
mTextColor = array.getColor(R.styleable.TextView_jackchentextColor , mTextColor) ; // mTextColor表示上边定义的默认黑色
// 获取文字大小
mTextSize = array.getDimensionPixelSize(R.styleable.TextView_jackchentextSize , sp2px(mTextSize)) ;
// 回收
array.recycle();
mPaint = new Paint() ;
// 设置抗锯齿
mPaint.setAntiAlias(true);
mPaint.setDither(true);
// 设置文字大小和颜色
mPaint.setTextSize(mTextSize);
mPaint.setColor(mTextColor);
// 方法2:默认给一个透明的背景
// setBackgroundColor(Color.TRANSPARENT);
// 方法3:调用下边方法
setWillNotDraw(false);
}
方法三:调用setWillNotDraw(false)方法即可,代码见方法二中写的。
具体代码如下:
/**
* Email: 2185134304@qq.com
* Created by JackChen 2018/3/17 12:08
* Version 1.0
* Params:
* Description: 自定义TextView
*/
public class TextView extends LinearLayout {
/**
* 解决 自定义TextView 继承 LinearLayout(RelativeLayout、ViewGroup),文字不显示的3种解决方法
* 第一种:复写dispatchDraw()方法;
* 第二种:在布局文件中设置背景或者在代码中设置背景;
* 第三种:在代码中设置
*/
// 文字
private String mText ;
// 文字大小
private int mTextSize = 15 ;
// 文字颜色
private int mTextColor = Color.BLACK ;
// 画笔
private Paint mPaint ;
public TextView(Context context) {
this(context,null);
}
public TextView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
public TextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// 获取自定义属性
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.TextView);
// 获取文字
mText = array.getString(R.styleable.TextView_jackchentext) ;
// 获取文字颜色
mTextColor = array.getColor(R.styleable.TextView_jackchentextColor , mTextColor) ; // mTextColor表示上边定义的默认黑色
// 获取文字大小
mTextSize = array.getDimensionPixelSize(R.styleable.TextView_jackchentextSize , sp2px(mTextSize)) ;
// 回收
array.recycle();
mPaint = new Paint() ;
// 设置抗锯齿
mPaint.setAntiAlias(true);
mPaint.setDither(true);
// 设置文字大小和颜色
mPaint.setTextSize(mTextSize);
mPaint.setColor(mTextColor);
// 方法2:默认给一个透明的背景
// setBackgroundColor(Color.TRANSPARENT);
// 方法3:调用下边方法
setWillNotDraw(false);
}
private int sp2px(int sp) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,sp,
getResources().getDisplayMetrics());
}
/**
* 测量文字
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 如果自定义TextView中的宽高给的是确定的值,比如10dp、20dp、match_parent,这个时候不需要计算,给的多少就是多少
// 如果自定义TextView中的宽高给的是wrap_content,则需要计算宽高
int widthMode = MeasureSpec.getMode(widthMeasureSpec) ;
int heightMode = MeasureSpec.getMode(heightMeasureSpec) ;
// 1\. 如果文字的大小给的是确定的值,比如10dp、20dp、match_parent,这个时候不需要计算,给的多少就是多少
int width = MeasureSpec.getSize(widthMeasureSpec) ;
// 2\. 如果文字的大小给的是wrap_content,则需要计算大小
if (widthMode == MeasureSpec.AT_MOST){
// 区域
// 计算的宽度 与字体的大小、字体长度有关
Rect bounds = new Rect() ;
// 获取TextView文本的区域
// 参数1:要测量的文字 参数2:表示从位置0开始 参数3:表示到整个文字的长度
mPaint.getTextBounds(mText , 0 , mText.length() , bounds);
width = bounds.width() + getPaddingLeft() + getPaddingRight() ;
}
int height = MeasureSpec.getSize(heightMeasureSpec) ;
if (heightMode == MeasureSpec.AT_MOST){
// 区域
Rect bounds = new Rect() ;
mPaint.getTextBounds(mText , 0 , mText.length() , bounds);
height = bounds.height() + getPaddingTop() + getPaddingBottom() ;
}
// 设置控件的宽高,这里就是给文字设置宽高
setMeasuredDimension(width , height);
}
/**
* 绘制文字
* @param canvas
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 中心点:getHeight()/2
// 参数1:文字 参数2:x 参数3:y 参数4:画笔
// x:是文字开始的距离
// y:是基线 baseLine 是要求的? getHeight()/2是中心位置 已知
// dy:是高度的一半到基线baseLine的位置
// top:是baseLine到文字顶部的距离,是一个负值
// bottom:是baseLine到文字底部的距离,是一个正值
Paint.FontMetricsInt fontMetrics = mPaint.getFontMetricsInt();
int dy = (fontMetrics.bottom - fontMetrics.top)/2 - fontMetrics.bottom ;
int baseLine = getHeight()/2 + dy ;
int x = getPaddingLeft() ;
canvas.drawText(mText , x, baseLine, mPaint);
}
// /**
// *
// * 绘制文字
// * @param canvas
// */
// @Override
// protected void dispatchDraw(Canvas canvas) {
// super.dispatchDraw(canvas);
// // 中心点:getHeight()/2
// // 参数1:文字 参数2:x 参数3:y 参数4:画笔
// // x:是文字开始的距离
// // y:是基线 baseLine 是要求的? getHeight()/2是中心位置 已知
//
// // dy:是高度的一半到基线baseLine的位置
// // top:是baseLine到文字顶部的距离,是一个负值
// // bottom:是baseLine到文字底部的距离,是一个正值
// Paint.FontMetricsInt fontMetrics = mPaint.getFontMetricsInt();
// int dy = (fontMetrics.bottom - fontMetrics.top)/2 - fontMetrics.bottom ;
//
// int baseLine = getHeight()/2 + dy ;
//
// int x = getPaddingLeft() ;
//
// canvas.drawText(mText , x, baseLine, mPaint);
// }
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
break;
}
invalidate();
return super.onTouchEvent(event);
}
}
activity_main.xml布局文件如下:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.jackchen.view_day02.MainActivity">
<!--android:background="#00FF00"-->
<com.jackchen.view_day02.TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:jackchentext="JackChen"
app:jackchentextSize="18sp"
app:jackchentextColor="@color/colorAccent"
android:padding="10dp"
android:layout_margin="10dp"
/>
</RelativeLayout>
自定义属性资源文件attrs.xml文件如下:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- 这里的name最好就是自己自定义View的名字就可以-->
<declare-styleable name="TextView">
<!-- name是属性名称,format是格式
string 表示文字
color 表示文字颜色
dimension 表示宽高、字体大小
integer 表示数字,相当于int
reference 表示资源 (drawable)
-->
<attr name="jackchentext" format="string"/>
<attr name="jackchentextColor" format="color"/>
<attr name="jackchentextSize" format="dimension"/>
<attr name="jackchenmaxLength" format="integer"/>
<!-- 因为自定义View都是继承自View , 背景background都是View给我们管理的-->
<!--<attr name="jackchenbackground" format="reference|color"/>-->
<!-- 枚举 -->
<attr name="jackchenBnputType">
<enum name="number" value="1"/>
<enum name="text" value="2"/>
<enum name="password" value="3"/>
</attr>
</declare-styleable>
</resources>
具体代码已上传至github:
https://github.com/shuai999/View_day02_2.git
作者:世道无情
链接:https://www.jianshu.com/p/5c58f178643d
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。