UI 01: 自定义TextView,
自定义View 简介
自定义View,可以认为继承自View,系统没有的效果。
自定义属性,不能使用系统原有的属性名。
BaseLine计算
2. onMeasure 获取宽高的模式
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
MeasureSpec.AT_MOST: 布局中指定了wrap_content
MeasureSpec.EXACTLY: 制定了确定的值 100dp、match_parent/fill_parent
MeasureSpec.UNSPECIFIED: 尽可能的大。ListView/ScrollView 在测量布局的时候会用 UNSPECIFIED.
ScrollView 嵌套ListView,ListView只显示一个item?
ScrollView 传过来是UNSPECIFIED,
// ListView
if(heightMode== MeasureSpec.UNSPECIFIED){
heightSize = mListPadding.top+mListPadding.bottom+ childHeight+ getVerticalFadingEdgeLength()*2;
}
if(heightMode== MeasureSpec.AT_MOST) {
heightSize = measureHeightOfChildren(widthMeasureSpec, 0,NO_POSITION, heightSize,-1);
}
// 解决办法: 让其进入if(heightMode== MeasureSpec.AT_MOST)。
// MYListView extends ListView
onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 解决现实不全的问题。构建新的32bit值。高度是最大值。
heightMeasureSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE>>2, MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
widthMeasureSpec:包含2个信息:模式占前面2位;值占后面30位。
3.onDraw()
canvas.drawText();
canvas.drawCircle();
canvas.drawArc();
4.onTouch() 处理事件分发, 使用了责任链的设计模式。
返回true,才会不断调用 move,up等事件。
否则,只调用一次down事件,然后再也进不来。
// 处理跟用户的交互,手指触摸等等。事件分发事件拦截。
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: // 按下
break;
case MotionEvent.ACTION_MOVE: // 移动
break;
case MotionEvent.ACTION_UP: // 手指抬起
break;
}
return super.onTouchEvent(event); // 交给父类处理
}
5.自定义属性 attrs.xml
自定义是属性,用来配置的。一般用于自定义属性。
- 5.1 在res/values/attrs.xml 新建文件;
<declare-styleable>, 配置attr的name和 format;
string 文字,color颜色,dimension 宽高文字大小,integer数字,reference资源drawable; - 5.2 在布局中使用自定义属性:
xmlns:app="http://schemas.android.com/apk/res-auto"
app:text="Tomcat" - 5.3 在自定义View中获取属性
自定义TextView 02
布局加载到Activity:
LayoutInflater解析布局,实例化View是通过反射构造函数来创建。
LayoutInflater 调用的是哪个构造函数?
2.1 指定宽高(三种测量模式)
基线计算: Paint.FontMetricsInt fontMetrics = mPaint.getFontMetricsInt();
top 表示基线到文字最上面的位置的距离,是一个负值。
bottom 是基线到最下面的距离,是一个正值。
如果想要给予一个位子竖直居中,name居中位置的坐标假设为centerY
baseLineY = centerY - (fm.bottom -fm.top)/2- fm.top;
这样就可以确定基线的坐标。
center = (bottom-top) /2;
baselineY = (bottom-top) /2 - (fm.bottom -fm.top)/2- fm.top
= (bottom + top - fm.bottom - fm.top)/2;
int dy = (fontMetrics.bottom - fontMetrics.top)/2 - fontMetrics.bottom;
int baseLineY = getHeight()/2 + dy; // 中心点centerY = getHeight()/2;
默认继承自 ViewGroup 默认不会调用onDraw()方法。模板设计模式。
但是设置背景了,会显示出来。
//
if(!dirtyOpaque) onDraw(canvas); // ViewGroup 默认进不来。
dispatchDraw(canvas);
onDrawForeground(canvas);
dirtyOpaque 是 false 才行 其实就是由 privateFlags -> mPrivateFlags
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags 是在哪里赋值的:在View的构造函数中调用 computeOpaqueFlags()方法。
// setBackgroundDrawable() 会调用下面的方法。
protected void computeOpaqueFlags() {
// Opaque if:
// - 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;
}
}
ViewGroup 为什么出不来 ViewGroup()--> initViewGroup()
private void initViewGroup() {
// ViewGroup doesn't draw by default
if (!debugDraw()) {
setFlags(WILL_NOT_DRAW, DRAW_MASK); // View的方法
}
// ...
}
// 导致 mPrivateFlags 会重新赋值
思路: 目的就是改变 mPrivateFlags的值
- 1.onDraw--> 改成 dispatchDraw()
- 2.可以背景透明的背景,导致重新计算 mPrivateFlags
- 3.setWillNotDraw(false) 表示要绘制东西。
- 4.设置获取焦点
// View.java
public void setWillNotDraw(boolean willNotDraw) {
setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}
自定义属性
<!--name 自定义View的名字 TextView-->
<declare-styleable name="TextView">
<!-- name 属性名称
format 格式: string 文字 color 颜色
dimension 宽高 字体大小 integer 数字
reference 资源(drawable)
-->
<attr name="darrenText" format="string"/>
<attr name="darrenTextColor" format="color"/>
<attr name="darrenTextSize" format="dimension"/>
<attr name="darrenMaxLength" format="integer"/>
<!-- background 自定义View都是继承自View , 背景是由View管理的-->
<!--<attr name="darrenBackground" format="reference|color"/>-->
<!-- 枚举 -->
<attr name="darrenInputType">
<enum name="number" value="1"/>
<enum name="text" value="2"/>
<enum name="password" value="3"/>
</attr>
</declare-styleable>
layout中调用,设置自定义属性
<com.ex.mydemo2.TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#F00"
android:padding="10dp"
app:darrenText="湖南Tom girl Jery dog!"
app:darrenTextColor="#000"
app:darrenTextSize="30sp" />
layout中调用,设置自定义属性
// 自定义组件 TextView
public class TextView extends View {
private String mText;
private int mTextSize = 15;
private int mTextColor = Color.BLACK;
private Paint mPaint;
// 代码中 new 的时候调用
public TextView(Context context) {
this(context, null);
}
// 在布局layout中使用
public TextView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
// 不会自动调用,API>21才能使用。如有默认style时,在第二个构造函数中调用。
public TextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
/**
* 统一样式。
* <style name="default">
* <item name="layout_width"></item>
* <item name="layout_height"></item>
* <style/>
* <com.darren.view_day1.TextView
* style=@"style/default"
* android:text="Welcome"/>
*/
// 在layout中使用,使用了style。调用第三个构造方法。但是会有style
// 不会自动调用,如有默认style时,在第二个构造函数中调用。
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_darrenText);
mTextColor = array.getColor(R.styleable.TextView_darrenTextColor, mTextColor);
mTextSize = array.getDimensionPixelSize(R.styleable.TextView_darrenTextSize, sp2px(mTextSize));
array.recycle();
mPaint = new Paint();
mPaint.setAntiAlias(true); // 抗锯齿
mPaint.setTextSize(mTextSize);
mPaint.setColor(mTextColor);
// 默认给一个透明的背景
// setBackgroundColor(Color.TRANSPARENT);
setWillNotDraw(false);
}
private int sp2px(int sp) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp,
getResources().getDisplayMetrics());
}
// 自定义的测量方法,指定控件的宽高
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
// 1. 确定的值,不需要计算
int width = MeasureSpec.getSize(widthMeasureSpec);
// 2.wrap_content 需要计算, 宽度(与字体长度、字体大小有关),用画笔来测量
if (widthMode == MeasureSpec.AT_MOST) {
Rect bounds = new Rect();
// 获取文本的rect
mPaint.getTextBounds(mText, 0, mText.length(), bounds);
width = bounds.width() + getPaddingLeft() + getPaddingRight(); // 宽度考虑padding值
}
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); // 设置控件的宽高
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 画文字 text x y paint
// x 就是开始的位置 0
// y 基线 baseLine 求? getHeight()/2知道的 centerY
//dy 代表的是:高度的一半到 baseLine的距离
Paint.FontMetricsInt fontMetrics = mPaint.getFontMetricsInt();
// top 是一个负值 bottom 是一个正值. bottom的值代表是 bottom是baseLine到文字底部的距离(正值)
// 必须要清楚的,可以自己打印就好:
// dy = 中心线 到 基线的距离.
int dy = (fontMetrics.bottom - fontMetrics.top)/2 - fontMetrics.bottom;
int baseLine = getHeight()/2 + dy; // 中心点centerY = getHeight()/2;
int x = getPaddingLeft();
canvas.drawText(mText, x, baseLine, mPaint); // baseLine文字绘制的基线
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
break;
}
return super.onTouchEvent(event);
}
}