构造函数
一个参数(Context context)
的构造函数会在代码里面new的时候调用,
两个参数(Context context, AttributeSet attrs)
的构造函数在布局layout中使用(调用),
三个参数(Context context, AttributeSet attrs, int defStyleAttr)
的构造函数在布局layout中使用(调用),但是会有style。
public MyView(Context context) {
this(context, null);
}
public MyView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
xml布局通过LayoutInflate,解析的时候,实例化View是通过反射,显示加载到我们Activity。
measure
布局的宽高都是由这个方法指定,需要测量,获取宽高的模式。
@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);// 获取后面30位
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
}
MeasureSpec:32位的二进制数字,前两位代表mode(测量模式),后面30位才是他们的实际宽高的数值(size)。
三种模式:
- MeasureSpec.UNSPECIFIED:二进制00,默认值,父控件没有给子view任何限制,子View可以设置为任意大小。
- MeasureSpec.EXACTLY:二进制01,表示父控件已经确切的指定了子View的大小。比如100dp、match_parent、fill_parent。
- MeasureSpec.AT_MOST:二进制10,表示子View具体大小没有尺寸限制,但是存在上限,上限一般为父View大小。wrap_content。
一般情况,wrap_content,即MeasureSpec.AT_MOST才需要重新计算。onMeasure方法最终调用setMeasuredDimension确定宽高值。
layout
draw
用于绘制
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//canvas可以画文本、画弧、画圆等等
}
ViewGroup不会触发onDraw方法
解决办法:
方式一:把onDraw方法替换为dispatchDraw;方式二:设置透明的背景
setBackgroundColor(Color.TRANSPARENT);
;方式三:设置setWillNotDraw(false);
主要三个方法
onDraw绘制自己;dispatchDraw绘制孩子;绘制背景
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
Path
void moveTo (float x1, float y1):直线的开始点;即将直线路径的绘制点定在(x1,y1)的位置;
void lineTo (float x2, float y2):直线的结束点,又是下一次绘制直线路径的开始点;lineTo()可以一直用;
void close ():如果连续画了几条直线,但没有形成闭环,调用Close()会将路径首尾点连接起来,形成闭环;
Paint
相关属性
setAntiAlias(true);//抗锯齿功能
setColor(Color.RED);//设置画笔颜色
//Style.FILL:填充内部;Style.FILL_AND_STROKE填充内部和描边;Style.STROKE仅描边
setStyle(Style.FILL);//设置填充样式
setStrokeWidth(30);//设置画笔宽度
//参数:radius:阴影的倾斜度;dx:水平位移;dy:垂直位移
setShadowLayer(float radius, float dx, float dy, int color)//添加阴影
//设置线冒样式,Cap.ROUND(圆形线冒)、Cap.SQUARE(方形线冒)、Paint.Cap.BUTT(无线冒)
setStrokeCap(Paint.Cap cap)
setAlpha(int a) //设置画笔透明度
reset() //重置画笔
和文字相关
//相对位置
paint.setTextAlign(Paint.Align.LEFT);//Paint.Align.LEFT、Paint.Align.CENTER、Paint.Align.RIGHT
setTextSize(float textSize) //设置文字大小
setFakeBoldText(boolean fakeBoldText) //设置是否为粗体文字
setStrikeThruText(boolean strikeThruText) //设置带有删除线效果
setUnderlineText(boolean underlineText) //设置下划线
setTextSkewX(float skewX) //设置字体水平倾斜度,普通斜体字是-0.25,可见往右斜
draw图形
画点
//参数:float X:点的X坐标; float Y:点的Y坐标
drawPoint (float x, float y, Paint paint)
多个点
//参数:pts:点的合集{x1,y1,x2,y2,x3,y3,……}; offset:集合中跳过的数值个数,
//注意不是点的个数,一个点是两个数值; count:参与绘制的数值的个数。
drawPoints (float[] pts, Paint paint)
drawPoints (float[] pts, int offset, int count, Paint paint)
画直线
//参数:startX:开始点X坐标; startY:开始点Y坐标; stopX:结束点X坐标; stopY:结束点Y坐标
drawLine (float startX, float startY, float stopX, float stopY, Paint paint)
多条直线
//参数:pts是点的集合,每两个点形成一条直线,pts的组织方式为{x1,y1,x2,y2,x3,y3,……}
drawLines (float[] pts, Paint paint)
drawLines (float[] pts, int offset, int count, Paint paint)
矩形工具类RectF和Rect
//根据四个点构造出一个矩形;
RectF(float left, float top, float right, float bottom)
Rect(int left, int top, int right, int bottom)
画矩形
drawRect (float left, float top, float right, float bottom, Paint paint)
drawRect (RectF rect, Paint paint)
drawRect (Rect r, Paint paint)
画圆角矩形
//参数:rx:生成圆角的椭圆的X轴半径; ry:生成圆角的椭圆的Y轴半径
drawRoundRect (RectF rect, float rx, float ry, Paint paint)
画圆形
//参数:cx:圆心点X轴坐标 ; cy:圆心点Y轴坐标; radius:圆的半径
drawCircle (float cx, float cy, float radius, Paint paint)
画椭圆
//椭圆是根据矩形生成的,以矩形的长为椭圆的X轴,矩形的宽为椭圆的Y轴,建立的椭圆图形
drawOval (RectF oval, Paint paint)
画弧
弧当然也是根据矩形来生成的
//参数:oval:生成椭圆的矩形; startAngle:弧开始的角度,X轴正方向为0度,顺时针为正;
//sweepAngle:弧持续的角度,顺时针; useCenter:是否有弧的两边,True,有两边,False,只有一条弧。
drawArc (RectF oval, float startAngle, float sweepAngle, boolean useCenter, Paint paint)
画三角
if (mPath == null) {
// 画路径
mPath = new Path();
mPath.moveTo(getWidth() / 2, 0);
mPath.lineTo(0, (float) ((getWidth()/2)*Math.sqrt(3)));
mPath.lineTo(getWidth(), (float) ((getWidth()/2)*Math.sqrt(3)));
mPath.close();// 把路径闭合
}
canvas.drawPath(mPath, mPaint);
画图片
canvas.drawBitmap(bitmap, x, y, paint);
drawText画文字
//text:要绘制的文字; x:绘制原点x坐标; y:基线y坐标; paint:用来做画的画笔
drawText(String text, float x, float y, Paint paint)
从上往下依次为:top、ascent、baseLine、descent、bottom。
Paint paint = new Paint();
Paint.FontMetrics fm = paint.getFontMetrics();
Paint.FontMetricsInt fmInt = paint.getFontMetricsInt();
FontMetricsInt和FontMetrics完全相同,只是得到的值的类型不一样而已,FontMetricsInt中的四个成员变量的值都是Int类型,而FontMetrics得到的四个成员变量的值则都是float类型的。
获取基线
Paint.FontMetricsInt fontMetrics = paint.getFontMetricsInt();
int dy = (fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.bottom;
int baseLine = getHeight() / 2 + dy;
获取文字宽度
int width = paint.measureText(String text);
onTouch
处理跟用户交互,手指触摸,事件分发事件拦截等等。
@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);
}
可以通过event.getX()
获取相对于当前控件的位置,event.getRawX()
获取距离屏幕的x位置。
自定义属性
在res/values下面新建attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--name 自定义View的名字MyView-->
<declare-styleable name="MyView">
<!-- name属性名称
format格式: string 文字、color 颜色、dimension 宽高 字体大小、
integer 数字、reference 资源(drawable)-->
<attr name="myText" format="string"/>
<attr name="myTextColor" format="color"/>
<attr name="myTextSize" format="dimension"/>
<attr name="myMaxLength" format="integer"/>
<attr name="myBackground" format="reference|color"/>
<!-- 枚举 -->
<attr name="myInputType">
<enum name="number" value="1"/>
<enum name="text" value="2"/>
<enum name="password" value="3"/>
</attr>
</declare-styleable>
</resources>
在布局中使用,声明命名空间,然后在自己的自定义View中使用
xmlns:app="http://schemas.android.com/apk/res-auto"
<com.view.MyView
app:myText="jt"
app:myTextColor="@color/colorAccent"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
在自定义View中获取属性
//获取自定义属性
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.MyView);
mText = array.getString(R.styleable.MyView_myText);
mTextColor = array.getColor(R.styleable.MyView_myTextColor, mTextColor);
mTextSize = array.getDimensionPixelSize(R.styleable.MyView_myTextSize, mTextSize);
//回收
array.recycle();
系统有的自定义属性,我们是不能重新定义。
其他方法
onFinishInflate
setContentView布局解析完毕之后会执行这个方法。而View的绘制流程是在Activity的onResume()之后才调用的。
requestDisallowInterceptTouchEvent
改变的其实就是mGroupFlags的值
//事件分发
//dispatchTouchEvent->onInterceptTouchEvent->onTouchEvent
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
//请求所有父控件及祖宗控件不要拦截事件
getParent().requestDisallowInterceptTouchEvent(true);
return super.dispatchTouchEvent(ev);
}
canChildScrollUp
SwipeRefreshLayout中方法,判断是否滚动到了最顶部。
public boolean canChildScrollUp() {
if (mChildScrollUpCallback != null) {
return mChildScrollUpCallback.canChildScrollUp(this, mTarget);
}
if (android.os.Build.VERSION.SDK_INT < 14) {
if (mTarget instanceof AbsListView) {
final AbsListView absListView = (AbsListView) mTarget;
return absListView.getChildCount() > 0
&& (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0)
.getTop() < absListView.getPaddingTop());
} else {
return ViewCompat.canScrollVertically(mTarget, -1) || mTarget.getScrollY() > 0;
}
} else {
return ViewCompat.canScrollVertically(mTarget, -1);
}
}
GestureDetector
快速滑动,手势处理。
在ScrollView中嵌入ListView
在ScrollView添加一个ListView会导致listview控件显示不全,通常只会显示一条,这是因为两个控件的滚动事件冲突导致。
ListView,ScrollView在测量子布局的时候会用UNSPECIFIED,需要重写listview中的onMeasure。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 解决显示不全的问题
heightMeasureSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE>> 2,MeasureSpec.AT_MOST);
//heightMeasureSpec:32位的值,30位 是 Integer.MAX_VALUE,2位是MeasureSpec.AT_MOST
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
invalidate源码分析
invlidate()
一路向上,不断调用invalidateChild(this, damage)
,调到最外层ViewRootImpl
。最外层调用draw()
、dispatchDraw()
,一路往下画,最终画到当前调用invaldate的View的onDraw()
方法。invlidate() 牵连着整个layout布局中的View。
ViewRootImpl中的重要方法
performTraversals()、performMeasure()、performLayout()、performDraw()
为什么不能在子线程中更新UI
ViewRootImpl中checkThread()方法用来检测线程。
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
mThread在构造函数中初始化的主线程mainThread
View的绘制流程
setContentView创建DecorView,把我们的布局加载到了DecorView。
Activity的启动流程
performLaunchActivity -> Activity.onCreate()handleResumeActivity() -> performResumeActivity() ->
Activity的onResume()方法 -> wm.addView(decor, l); 才开始把我们的 DecorView 加载到 WindowManager,
-> View的绘制流程在这个时候才开始 measure() layout() draw()
addView
wm.addView(decor, l); -> WindowManangerImpl.addView()-> root.setView(view, wparams, panelParentView); ->
requestLayout() -> scheduleTraversals()-> doTraversal() -> performTraversals()
onmeasure:测量是从外往里递归
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);-> mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);->
onMeasure(widthMeasureSpec, heightMeasureSpec); 测量开始 -> measureVertical(int widthMeasureSpec, int heightMeasureSpec)->
measureChildWithMargins
childWidthMeasureSpec,childHeightMeasureSpec测量模式是通过getChildMeasureSpec计算。调用setMeasuredDimension()这个时候我们布局才真正指定宽度和高度,mMeasuredWidth和mMeasuredHeight才开始有值。
layout:摆放子布局for循环所有子View, 前提不是GONE,调用child.layout()
performLayout :View -> layout() -> onLayout()
draw
performDraw() : View -> draw() -> drawBackground();//画背景 onDraw(canvas);// 画自己 ViewGroup 默认情况下不会调用
dispatchDraw(canvas);// 画子View 不断的循环调用子View的 draw()
流程小结:
第一步performMeasure():用于指定和测量layout中所有控件的宽高,对于ViewGroup,先去测量里面的子孩子,根据子孩子的宽高再来计算和指定自己的宽高,对于View,它的宽高是由自己和父布局决定的。
第二步performLayout(): 用于摆放子布局,for循环所有子View,用child.layout()摆放ChildView。
第三步performDraw(): 用于绘制自己还有子View,对于ViewGroup首先绘制自己的背景,for循环绘制子View调用子View的draw()方法, 对于View绘制自己的背景,绘制自己显示的内容(TextView)。
细节:
View的绘制流程是在onResume() 之后才开始,如果要获取View的高度,前提肯定需要调用测量方法,测量完毕之后才能获取宽高。
addView、setVisibility、等,会调用requestLayout()重新走一遍View的绘制流程。
View的Touch事件分发
自定义view
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e("TAG","onTouchEvent -> "+event.getAction());
return super.onTouchEvent(event);
}
添加listener
view.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.e("TAG", "onTouch -> " + event.getAction());
return false;
}
});
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.e("TAG", "onClick");
}
});
现象:
OnTouchListener返回false时:
OnTouchListener.DOWN -> onTouchEvent.DOWN -> OnTouchListener.MOVE -> onTouchEvent.MOVE -> OnTouchListener.UP->
onTouchEvent.UP-> OnClickListener
OnTouchListener返回true时:
OnTouchListener.DOWN -> OnTouchListener.MOVE -> OnTouchListener.UP
OnTouchListener没有,onTouchEvent返回true时:
onTouchEvent.DOWN -> onTouchEvent.MOVE -> onTouchEvent.UP
自定义view中添加dispatchTouchEvent时候,如果不写super,就什么都不走了:
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
//super.dispatchTouchEvent(event);
return true;
}
dispatchTouchEvent事件分发
ListenerInfo li = mListenerInfo;
ListenerInfo: 存放了关于View的所有Listener信息,如:OnTouchListener、OnClickListener。
boolean result = false;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED //是否是enable
&& li.mOnTouchListener.onTouch(this, event)) {//如果onTouch是false,result=false;如果是true,result=true
result = true;
}
if (!result && onTouchEvent(event)) {//如果result=false就会执行onTouchEvent,如果result=true就不会执行onTouchEvent
result = true;
}
return result;
在View的onTouchEvent中的case MotionEvent.ACTION_UP
:里面调用了performClick()
添加了点击事件li.mOnClickListener.onClick(this)
。
onTouchEvent方法
ViewGroup的事件分发
自定义View和上面view一样,自定义ViewGroup:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.e("TAG", "ViewGroup dispatchTouchEvent -> " + ev.getAction());
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.e("TAG", "ViewGroup onInterceptTouchEvent -> " + ev.getAction());
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e("TAG", "ViewGroup onTouchEvent -> " + event.getAction());
return super.onTouchEvent(event);
}
自定义ViewGroup包裹自定义View,点击View时候:
DOWN -> ViewGroup.dispatchTouchEvent -> ViewGroup.onInterceptTouchEvent -> View.dispatchTouchEvent -> View.onTouch -> View.onTouchEvent ->
MOVE -> ViewGroup.dispatchTouchEvent -> ViewGroup onInterceptTouchEvent -> View.dispatchTouchEvent -> View.onTouch -> View.onTouchEvent ->
Up -> ViewGroup.dispatchTouchEvent -> ViewGroup onInterceptTouchEvent -> View.dispatchTouchEvent -> View.onTouch -> View.onTouchEvent ->
View.onclick
去掉自定义View的onClick:
ViewGroup.dispatchTouchEvent -> ViewGroup.onInterceptTouchEvent ->
View.dispatchTouchEvent -> View.onTouch -> View onTouchEvent ->
ViewGroup.onTouchEvent
自定义View的onTouchEvent()方法里面返回true时:
DOWN -> ViewGroup.dispatchTouchEvent -> ViewGroup.onInterceptTouchEvent -> View.dispatchTouchEvent -> View.onTouch -> View.onTouchEvent ->
MOVE -> ViewGroup.dispatchTouchEvent -> ViewGroup onInterceptTouchEvent -> View.dispatchTouchEvent -> View.onTouch -> View.onTouchEvent ->
Up -> ViewGroup.dispatchTouchEvent -> ViewGroup onInterceptTouchEvent -> View.dispatchTouchEvent -> View.onTouch -> View.onTouchEvent
自定义ViewGroup的onInterceptTouchEvent()返回true时:
ViewGroup.dispatchTouchEvent -> ViewGroup.onInterceptTouchEvent -> ViewGroup.onTouchEvent
dispatchTouchEvent
onInterceptTouchEvent
onInterceptTouchEvent()默认情况下返回false。
onTouchEvent
如果子View没有一个地方返回true,只会进来一次只会响应DOWN事件,代表不需要消费该事件,如果你想响应MOVE,UP必须找个地方ture。
对于ViewGroup,如果想拦截子View的Touch事件,可以覆写onInterceptTouchEvent返回true,执行该ViewGroup的onTouchEvent方法;如果子View没有消费Touch事件,也会调用该ViewGroup的onTouchEvent方法。