自定义View

1、自定义的步骤

  • 最近在学自定义View,内容很多,首先先从创建类继承View说起

1.1、自定义View的构造方法
既然要继承View那就必须得重写构造方法,构造方法有四个,只要继承其中一个就够了。

// 在代码里new的时候就会调用该构造函数
public MyTextView(Context context) {
super(context);
}

// 在布局layout中使用该自定义控件,展示layout页面时就会调用
public MyTextView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}

// 在layout布局中使用style属性,或者在attr.xml定义主题来使用该style
public MyTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}

// 很少用到,因为第三个构造方法已经有指定主题了。第四个参数应该也是和主题相关
public MyTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}

这四种我没有都使用过,用到前两种比较多,什么时候调用在代码里也写得比较明了。第三种我还没怎么遇到过,不过看了文章大概知道怎么用了!

  • 第三种构造方法的使用方式:

(1)直接在layout xml文件设置style属性,展示时就会自动调用第三个构造方法

//在style.xml
    <style name="defualt">
        <!-- Customize your theme here. -->
        <item name="layout_width">wrap_content</item>
        <item name="layout_height">wrap_content</item>
        <item name="textColor">@color/colorAccent</item>
    </style>

    <com.darren.view_day01.TextView
        android:text="Darren"
        style="@style/defualt"
		 />

按照上面这种编码规范,就会调用第三个构造方法。
(2)可以在第二个构造方法中调用第三个构造方法。指定styleAttr就行

   public CustomTextView(Context context, @Nullable AttributeSet attrs) {
      this(context, attrs, R.attr.textViewColorStyle);
   }

第四种构造方法基本不用,没用过,暂时不讲。

1.2、重写onMeasure方法,测量控件宽高
这里突然讲到onMeasure方法,有点突兀,但是这和View绘制流程有关系,至于流程是什么不展开说,就是绘制过程中会执行到onMeasure方法。onMeasure方法才是设置最终的宽高,layout里面设置的宽高还不算。
(1)要先获取宽高的模式。这里“宽高的模式”就很懵逼了,MeasureSpec这个类它有三种测量模式,大家先让我介绍这三种测量模式是什么:
①MeasureSpec.EXACTLY 当用户在xml给一个确定的长宽值,或者match_parent代码就会有这个模式
②MeasureSpec.AT_MOST 当用户在xml写一个wrap_content。就会有这个模式
③MeasureSpec.UNSPECIFIED 很少用到,这里先不说

很明显,这个模式就是和xml布局文件设置的宽高是有关系的,而onMeasure就是设置该控件最终的宽高。
需要计算的地方就是布局文件写wrap_content的时候,就要具体计算,控件的宽高然后设置最终大小,比如:
(1)自定义TextView。大小就根据字体和字数长度来计算
(2)圆。大小就是根据半径计算。

代码:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
	super.onMeasure(widthMeasureSpec, heightMeasureSpec);
	int widthSize = 0;
	int heightSize = 0;
	// 获取宽的模式 和 高的模式
	int widthMode = MeasureSpec.getMode(widthMeasureSpec);
	int heightMode = MeasureSpec.getMode(heightMeasureSpec);
	// EXACTLY没有做if逻辑判断,是因为这里不考虑UNSPECIFIED模式
	widthSize = MeasureSpec.getSize(widthMeasureSpec); // 获取用户输入的大小 或 match_parent 即 EXACTLY模式了。
	if(MeasureSpec.AT_MOST == widthMode) {
		// wrap_content 计算具体的宽
	}
	heightSize = MeasureSpec.getSize(heightMeasureSpec);
	if(MeasureSpec.AT_MOST == heightMode) {
		// 计算具体的高
	}
	setMeasuredDimension(widthSize,heightSize); // 设置最终控件的宽高
}

1.3、如何实现自定义属性:

  • 在values新建attrs.xml文件,添加自定义属性
<declare-styleable name="myTextViewStyle">
<!-- 文本内容-->
<attr name="textViewcontent" format="string"/>
<!-- 文本颜色-->
<attr name="textViewColor" format="color"/>
<attr name="textViewSize" format="dimension"/>
<!-- 文本背景,关联颜色?-->
<attr name="textViewBackground" format="reference|color"/>
</declare-styleable>
</resources>

(1)这样在layout布局文件,使用该自定义控件的时候,就可以给这些自定义属性赋值了。

  • 前提是要声明命名空间:
xmlns:zwk="http://schemas.android.com/apk/res-auto"
  • 为自定义属性赋值
<com.android.my_test_project.ViewTest.MyTextView
android:background="@color/fontBlue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
zwk:textViewcontent="内容内容"/>
  • 在代码中的构造方法,就可以通过TypedArray获取attrs里面的自定义属性。在layout里有设置到的值,就可以直接获取到该值,没有设置到的就用默认值,注意,string类型是没有默认值的,注意空指针的问题
public MyTextView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
// 获取属性
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.myTextViewStyle); // 这个myTextViewStyle是“declare-styleable name”声明的
content = typedArray.getString(R.styleable.myTextViewStyle_textViewcontent); // 注意string是没有默认值的
mTextColor = typedArray.getColor(R.styleable.myTextViewStyle_textViewColor,mTextColor);
mTextSize = typedArray.getDimensionPixelSize(R.styleable.myTextViewStyle_textViewSize,sp2px(mTextSize));

mPaint = new Paint();
mPaint.setAntiAlias(true);
// 设置字体的大小和颜色
mPaint.setTextSize(mTextSize);
mPaint.setColor(mTextColor);
// 回收
typedArray.recycle();
}

2、如何实现一个自定义的TextView

2.1、自定义属性如何实现

  • 首先要想想这个控件有什么属性,有字体大小、字体颜色、文字内容。
<declare-styleable name="myTextViewStyle">
<!-- 文本内容-->
<attr name="textViewcontent" format="string"/>
<!-- 文本颜色-->
<attr name="textViewColor" format="color"/>
<attr name="textViewSize" format="dimension"/>
</declare-styleable>
  • 在layout布局使用该控件时,可以对声明的自定义属性赋值
<com.android.my_test_project.ViewTest.MyTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
zwk:textViewcontent="内容内容"
zwk:textViewColor="@color/black"
zwk:textViewSize="20sp"/>
  • 在代码获取属性,同1.3获取属性操作一致。获取完属性,就可以设置paint的样式了。
public MyTextView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
// 获取属性
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.myTextViewStyle); // 这个myTextViewStyle是“declare-styleable name”声明的
content = typedArray.getString(R.styleable.myTextViewStyle_textViewcontent); // 注意string是没有默认值的
mTextColor = typedArray.getColor(R.styleable.myTextViewStyle_textViewColor,mTextColor);
mTextSize = typedArray.getDimensionPixelSize(R.styleable.myTextViewStyle_textViewSize,sp2px(mTextSize));

mPaint = new Paint();
mPaint.setAntiAlias(true);
// 设置字体的大小和颜色
mPaint.setTextSize(mTextSize);
mPaint.setColor(mTextColor);
rect = new Rect(); // 矩形对象
// 回收
typedArray.recycle();
invalidate();
}

2.2、onMeasure方法该如何测量控件宽高

  • 如果用户没有输入具体的值而是wrap_content。具体控件,长宽就要具体计算,TextView的宽度,就是由字体大小和字数长度决定。
    (1)字体大小,就是画笔大小。
    (2)字体长度,用rect矩形获取长度。
// 在构造方法初始化paint和rect
mPaint = new Paint();
mPaint.setAntiAlias(true);
// 设置字体的大小和颜色
mPaint.setTextSize(mTextSize);
mPaint.setColor(mTextColor);
rect = new Rect(); // 矩形对象
if(MeasureSpec.AT_MOST == widthMode) {
// wrap_content 用画笔来测量文字的宽
mPaint.getTextBounds(content,0,content.length(),rect); // 为rect矩形填充信息
// 记得要加上左内边距 右内边距。
widthSize = rect.width() + getPaddingLeft() + getPaddingRight();
}

2.3、在onDraw方法中,绘制字体需要注意的基线高度。

  • 使用canvas画字体,使用canvas.drawText();四个参数含义
// text就是要画的文本内容
// x就是画的初始位置的横坐标。即左边界横坐标
// baseLine 就是字体的基线(中线)停留在哪个纵坐标上
canvas.drawText(text,x,baseLine,paint);

在这里插入图片描述
基线就是字体的中线位置在哪个纵坐标上。注意这里左上角为原点。
(1)先说x初始位置横坐标先,x=左内边距,初始位置从左内边距的位置开始画
(2)那基线怎么确认?
从上面图可以看到(top + bottom)/2(注意这里top和bottom有正负值,用绝对值相加除2最好)就是字体高度的一半值,字体高度一半值再减去bottom值,就是一半高度要往下移动的距离。(有点绕,但结合图还是比较清楚的)

Paint.FontMetricsInt fontMetricsInt = paint.getFontMetricsInt();
// 这里bottom和top可以用绝对值相加除2.因为bottom和top有正负值的问题。后面减bottom也是用绝对值减。
int dy = (fontMetricsInt.bottom - fontMetricsInt.top)/2 - fontMetricsInt.bottom;
// 其实上面的(|bottom|+|top|)/2 和 getHeight/2 是不一样的,前者是字体一半的高度,后者是视图一半的高度,当上下内边距不一样了就体现出来。
int baseLine = getHeight() /2 + dy; // 基线要向下移动一点,所以y轴坐标值增加一点。
Log.i(TAG, "onDraw: " + baseLine);

// 终于画出字体!
canvas.drawText(content,x,baseLine,mPaint);

2.4、为什么ViewGroup不执行onDraw方法?

  • 这里不详细扩展讲,因为涉及到源码的问题。这里先讲讲,onDraw方法,就是被draw(Canvas canvas)所调用。先看看View的源码
// 找到View类的 draw方法
@CallSuper
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

	// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas); onDraw
}

很明显onDraw方法,执不执行是决定于 dirtyOpaque这个变量是true还是false。 而这个dirtyOpaque这个变量又是受privateFlags这个int变量影响。再往上看又是受mPrivateFlags这个全局变量影响

再看看ViewGroup源码,注意ViewGroup是继承View,所以构造方法先执行父类构造方法,再执行子类构造。

public ViewGroup(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
initViewGroup();
}

private void initViewGroup() {
// ViewGroup doesn't draw by defaultz这就是为什么ViewGroup不执行onDraw的原因
if (!debugDraw()) {
setFlags(WILL_NOT_DRAW, DRAW_MASK);
}
}

从上面代码可以看出,ViewGroup初始化时,执行initViewGroup方法,里面有一个setFlags(WILL_NOT_DRAW, DRAW_MASK);。再看看setFlags这个方法,这个方法是在View里面写的,子类调用父类方法,可以:

void setFlags(int flags, int mask) {
	....
	//我看不太懂,但是这个方法里面,有对mPrivateFlags这个变量赋值了!!!,说明什么,setFlags这个方法会改变mPrivateFlags这个变量。说明就会改变了dirtyOpaque那个变量
	mPrivateFlags |= PFLAG_DRAWN;
	invalidate(true);
}

所以为什么不走onDraw方法,可能dirtyOpaque为true了。怎么控制它true和false的,那就是改变mPrivateFlags这个View的全局变量。setFlags就是改变这个变量。

现在我们要让ViewGroup强制执行onDraw方法呢?。我看到View有这个一个方法:
这个setWillNotDraw方法,就是调用setFlags方法,决定要不要走onDraw方法。传false就是DRAW_MASK啦,代表要走onDraw方法。

public void setWillNotDraw(boolean willNotDraw) {
setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}
  • 完整代码:
public class MyTextView extends View {
    private Paint paint;
    private int textSize;
    private int textColor;
    private String content;
    public MyTextView(Context context) {
        this(context,null);
    }

    public MyTextView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs,0);
    }
    public MyTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.myTextViewStyle);
        this.textSize = typedArray.getDimensionPixelSize(R.styleable.myTextViewStyle_textViewSize,sp2px(15));
        this.textColor = typedArray.getColor(R.styleable.myTextViewStyle_textViewColor, Color.BLACK);
        this.content = typedArray.getString(R.styleable.myTextViewStyle_textViewcontent);
        paint = new Paint();
        paint.setAntiAlias(true);
        paint.setTextSize(textSize);
        paint.setColor(textColor);
    }
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        Rect rect = new Rect();
        paint.getTextBounds(content,0,content.length(),rect);
        if(MeasureSpec.AT_MOST == widthMode) {
            widthSize = rect.width() + getPaddingLeft() + getPaddingRight() + 2;
        }
        if(MeasureSpec.AT_MOST == heightMode) {
            heightSize = rect.height() + getPaddingBottom() + getPaddingTop();
        }

        setMeasuredDimension(widthSize,heightSize);
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Paint.FontMetricsInt fontMetricsInt = paint.getFontMetricsInt();
        int dy = (fontMetricsInt.bottom - fontMetricsInt.top) / 2 - fontMetricsInt.bottom;
        int baseLine = getHeight()/2 + dy;
        canvas.drawText(content,getPaddingLeft(),baseLine,paint);
    }


    private int sp2px(int sp) {
        return (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,sp,
                getResources().getDisplayMetrics());
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值