package com.example.qxb_810.viewdrawdemo;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.ViewTreeObserver;
import butterknife.BindView;
import butterknife.ButterKnife;
public class MainActivity extends AppCompatActivity {
@BindView(R.id.mv_test)
MyView mvTest;
// 因为View的生命周期和Avticivt并不同步,所以在Activity的onCreate,onStart,onResume中均无法获取View的宽高
// 解决办法: 1. onWindowFocusChanged() 方法中获取 2. view.post() 3. 使用ViewTreeObserver接口
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
// 方法2: 通过post将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable的时候,view已经初始化好了
mvTest.post(new Runnable() {
@Override
public void run() {
int measuredHeight = mvTest.getMeasuredHeight();
int measuredWidth = mvTest.getMeasuredWidth();
}
});
}
// 方法1
// 在这个方法里面可以获取View测量后的宽/高,这个方法含义是View已经初始化完毕,可以获取宽高
// 需要注意在Activity窗口获取或者失去焦点的时候都会调用该方法。另外频繁的onResume和onPause也会调用。
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
int measuredHeight = mvTest.getMeasuredHeight();
int measuredWidth = mvTest.getMeasuredWidth();
}
@Override
protected void onStart() {
super.onStart();
// 方法3
// 使用OnGlobalLayoutListener() 接口回调。当View树的状态发生改变或者树内部的View的可见性发生改变时onGlobalLayout() 将会被回调。
// 需要注意随着view状态改变会调用多次
ViewTreeObserver viewTreeObserver = mvTest.getViewTreeObserver();
viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
mvTest.getViewTreeObserver().removeOnGlobalLayoutListener(this);
int measuredHeight = mvTest.getMeasuredHeight();
int measuredWidth = mvTest.getMeasuredWidth();
}
});
}
}
package com.example.qxb_810.viewdrawdemo;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;
import android.widget.LinearLayout;
/**
* author wlhao
* email wlhao@iflytek.com
*/
public class MyView extends View{
public MyView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
// MeasureSpec 代表一个32位int值,高2位代表SpecMode,后30位代表SpecSize
// MeasureSpec.makeMeasureSpec(size, Mode);
// Mode 有三种模式 : UNSPECIFIED: 表示一种测量的状态, EXACTLY : 适用 match_parent 或者精确数值, AT_MOST : 适用于 wrap_content
// 父容器影响View的MeasureSpec的创建过程。测量过程中系统会将View的layoutParams根据父容器所施加的规则转换成对应的MeasureSpec。
// 即MeasureSpec 由LayoutParams和父容器一起才能决定View的MeasureSpec,从而进一步决定View的宽/高
// 我理解的测量流程 ---
// 1. 首先系统创建MeasureSpec(通过某种方式获取xml布局中的Layout_params,判断其布局方式,并设置成对应的Mode和size,再通过makeMeasureSpec()创建出MeasuerSpec)
// 2. 对ViewGroup: 会调用其measureChildWidthMargins方法,在这个方法中会根据父容器的一些属性构造子元素的MeasuerSpec(这个顺序在子View.onMeasure()之前),并调用子元素的measure()方法进行绘制。
// 所以ViewGroup中的子元素的MeasuerSpec被重新创建,受到margin和padding等属性影响。 具体方法在getChildMeasureSpec()方法中,没仔细看。
// 3. View的measure过程 : measure()会调用onMesure(),onMeasure()会调用setMeasuredDimension()设置View宽高的测量值。setMeasuredDimension()会调用getDefaultSize()获取测量后的宽高并设置。
// 4. ViewGroup的measure过程:ViewGroup除了完成自己的measure过程之外还需要遍历去调用子元素的measure方法。通过measureChilderen()调用measureChilde()对子元素进行measure()以完成遍历子元素measuer
// 但是其ViewGroup自身的OnMeasure()并没有实现,因为无法满足众多ViewGroup的布局要求,因此都在其子类中单独实现。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 处理直接继承View/ViewGroup时如果是 wrap_content 属性失效 ---- 即按照源码如果设置成wrap_content会默认成match_parent,这里只需要给他指定一个默认的宽/高即可
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST){
setMeasuredDimension(200, 200);
}else if(widthMode == MeasureSpec.AT_MOST){
setMeasuredDimension(200, heightSize);
}else if (heightMode == MeasureSpec.AT_MOST){
setMeasuredDimension(widthSize, 200);
}
}
// 定位
// 1. layout() 方法确定View本身位置,onLayout() 方法按确定所有子元素位置
// 即 layout 方法流程: 首先通过setFrame方法设定View的四个顶点,确定后View在父容器的位置也就确定,之后调用onLayout方法,供父容器确定子元素的位置
// 2. 父容器通过layout确定位置之后,通过onLayout会调用子元素的layout来确定子元素的位置。依次传递,实现整个View树的layout过程。
// 注:View的测量宽/高和最终宽/高有什么区别?即View的getMeasureWidth/getMeasureHeight方法和getWidth/getHeight的区别
// 答:在View的默认实现中两者相同。只不过前者形成于measure过程,后者形成在layout过程。
// 1. 如下情况,会使最终宽高永远大于测量宽高100px
// protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
// super.onLayout(changed, left, top, right+100, bottom+100);
// }
// 2. 当View需要多次measure时,那么前几次的measure可能和最终宽高有所差异,但就最终结果看来,并无二异。
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
}
// 1. 绘制过程如下: 绘制背景background.draw(canvas) --> 绘制自己(onDraw) --> 绘制children(dispatchDraw) --> 绘制装饰(onDrawScrollBars)
// 2. View的绘制通过dispatchDraw来实现,dispatchDraw会遍历所有的子元素的draw方法,从而一层层传递。
// 3. View中存在一个方法:setWillNotDraw() 即如果一个View不需要绘制任何内容,可以将其标记位设置为true,系统会进行相应的优化。
// 这个标示为默认View是不开启,ViewGroup是开启的。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 解决直接继承View的时候padding属性失效。
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();
// 重新计算得到的宽高
int width = getWidth() - paddingLeft - paddingRight;
int height = getHeight() - paddingTop - paddingTop;
int radius = Math.min(width, height) / 2;
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
canvas.drawCircle(paddingLeft + width / 2, paddingTop + height / 2, radius, paint);
}
/*
自定义View注意事项:
1. 如果自定义控件直接继承View或者ViewGroup,需要在onMeasure中对wrap_content做特殊处理,否则外界使用该属性时无法达到预期效果(可能会实现match_parent效果)
修改方法参考上面onMeasure方法
2. 直接继承View的控件,需要在draw方法中处理padding(解决方法在onDraw)。直接继承ViewGroup的控件,需要在onMeasure和onLayout中考虑padding和子元素margin对其造成的影响。不然会失效。
3. View中提供了post系列方法,所以尽量在自定义View中少用Handler,除非很明确需要Handler来发送消息。
4. View中的两个对应方法: onAttchedToWindow/onDetachedFromWindow --- 当包含此View的Activity启动/ 退出或者remove时会调用。可以在该方法中启动/停止一些线程和动画。
5.
*/
}