View的工作原理
-
ViewRoot对应于ViewRootImpl类,是连接Windowmanager和DecorView的纽带,View的三大流程(measure-测量View的宽高,通过getMeasureWidth和getMeasureHeight方法能拿到宽高,一般来说等于最终宽高、layout-确定View的位置即四个顶点的坐标和View的最终宽高、draw-决定View在屏幕上的显示)均由ViewRoot完成。
-
MeasureSpec用来测量View的宽高,它是一个32位int值,高两位表示测量模式(specMode),低30表示这个模式下的规格大小(specSize),由父容器和LayoutParams共同决定。规则如下图:
View创建Measure值的规则.png
-
specMode分三类:无限制(UNSPECIFIED),要多大给多大;精确大小(EXACTLY),对应`LayoutParams的match_parent和具体数值;最大值(ATMOST),不超过这个范围,对应LayoutParams的wrap_parent。
-
直接继承View的自定义控件,要在OnMeasure方法中对wrap_content做特殊处理,如下所示:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int defautWidth=20,defaultHeight=20;//wrap_content下设置的默认值
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(defautWidth,defaultHeight);
}else if(widthMode == MeasureSpec.AT_MOST){
setMeasuredDimension(defautWidth,heightSize);
}else if(heightMode == MeasureSpec.AT_MOST){
setMeasuredDimension(widthSize,defaultHeight);
}else{
super.onMeasure(widthMeasureSpec,heightMeasureSpec);
}
}
-
ViewGroup是一个抽象类,自定义 Viewgroup要重写OnMeasure方法,同时提供了一个measureChildren的方法,在里面去循环遍历调用子元素的measure方法。
-
系统可能会多次measure才能确定宽高,要拿到View的宽高最好在onlayout方法里取。
-
在Activity的创建过程中去拿View的宽高都是0,因为Activity创建过程和View的测量过程不是同步的,要拿到宽高可以用下面几种方案:
(1)onWindowFoucsChanged,不过要注意,这个方法会被频繁调用,典型代码如下:
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if(hasFocus){
int width = view.getMeasuredWidth();
int heigt = view.getMeasuredHeight();
}
}
(2)view.post(runnable)。典型代码如下:
@Override
protected void onStart() {
super.onStart();
view.post(new Runnable(){
@Override
public void run() {
int width = view.getMeasuredWidth();
int heigt = view.getMeasuredHeight();
}
});
}
(3)ViewTreeObserver。典型代码如下:
@Override
protected void onStart() {
super.onStart();
ViewTreeObserver viewTreeObserver = view.getViewTreeObserver();
viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
int width = view.getMeasuredWidth();
int heigt = view.getMeasuredHeight();
}
});
}
(4)view.measure(int widthMeasureSpec , int heightMeasureSpec)。根据view的LayoutParams来处理,具体情况如下:
1.match_parent,直接放弃,无法拿到父容器的剩余空间。
2.具体数值,如下:
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec,heightMeasureSpec);
3.wrap_content,如下:
int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1, MeasureSpec.AT_MOST);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1, MeasureSpec.AT_MOST);
view.measure(widthMeasureSpec,heightMeasureSpec);
- 这几种方案里面,post是最常用的,GlobalLayoutListener的方式在布局发生变化的时候都会调用,在里面尽量不要生成大量对象,且一定记得remove。此外,post还可以切换到主线程。post为什么能拿到宽高并切换线程呢,具体看下它的实现:
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}
getRunQueue().post(action);
return true;
}
下面分情况分析:
-
如果attachInfo不等于空,就直接调用它的handler发送消息,不等于空就放入HandlerActionQueue的mActions里面等待调用。HandlerActionQueue有一个executeActions方法,会把队列里面的消息全部发送出去。executeActions是在什么时候调用呢,view的dispatchAttachedToWindow,这个方法是在ViewRootImpl的performTraversals调用。在performTraversals里面,executeActions之后才会调用performMeasure、performLayout和performDraw,那么为什么post能拿到宽高呢,因为executeActions只是把消息放到handler的消息队列里面,performTraversals本来就是一个消息的处理,执行完才会去执行下一个消息,所以post的消息被处理的时候,已经测量好了。
-
那么attachInfo 是在什么时候初始化的呢?答案也在dispatchAttachedToWindow里面,dispatchAttachedToWindow又是在哪里调用的呢?viewParent的实现类ViewRootImpl的performTraversals里面调用,并把mAttachInfo传进去,这个mAttachInfo是在ViewRootImpl的构造方法里面初始化的,它持有的mHandler是一个常量,由于ViewRootImpl是在主线程里面初始化的,所以handler处理消息也是在主线程,那么post就能做到线程切换。
-
其实viewTreeObserver.addOnGlobalLayoutListener也在performTraversals有调用,都在performMeasure和performLayout之后,所以也能拿到宽高。如下:
mAttachInfo.mTreeObserver.dispatchOnGlobalLayout();
-
自定义View主要分为四种方式:继承View重写onDraw方法;继承ViewGroup派生特殊的Layout;继承特定的View(如TextView);继承特定的ViewGroup(如LinearLayout)。
-
自定义View的注意事项:
(1)让View支持wrap_content。如果不在onMeasure中对wrap_content做处理,那么wrap_content是不起作用的,相当于match_parent。因为view默认的onMeasure方法如下:
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
getDefaultSize方法就是根据mode返回不同的size值,可以看出AT_MOST和EXACTLY都是返回的父容器可提供的最大值,UNSPECIFIED一般用于系统测量,这里返回的是传入的getSuggestedMinimumWidth,如下:
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
getSuggestedMinimumWidth里面返回的判断逻辑如下:
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
mMinWidth /mMinHeight ,即xml文件中的minWidth和minHeight,mBackground.getMinimumWidth()表示背景图片的实际大小,例如shape这一类背景是没有实际大小的,bitmap则有。
(2)如果有必要,支持padding。继承View的控件不在draw中处理padding的话,padding不起作用;继承ViewGroup的控件在onMeasure和onLayout中要考虑padding和子元素的margin带来的影响。
(3)尽量不要在View中使用handler,因为View本身提供了post系列方法。
(4)View中有线程和动画要及时停止,在onDetachedFromWindow方法中处理,否则可能内存泄漏。
(5)合理处理滑动冲突。
示例:
1.继承View:
public class CircleView extends View{
private int defalultColor = Color.parseColor("#000000");
private int circleColor = defalultColor;
private float defalultRidus = 5.0f;
private float ridus = defalultRidus;
private Paint paint;
private Context context;
public CircleView(Context context) {
this(context,null);
}
public CircleView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
this.context=context;
init(attrs);
}
private void init(AttributeSet attrs){
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
if(attrs!=null){
TypedArray typedArray = context.getResources().obtainAttributes(attrs,R.styleable.CircleView);
circleColor = typedArray.getColor(R.styleable.CircleView_circleColor, defalultColor);
ridus = typedArray.getFloat(R.styleable.CircleView_ridus,defalultRidus);
typedArray.recycle();
}
paint.setColor(circleColor);
ridus = DisPlayUtils.dp2px(context,ridus);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int defautWidth=200,defaultHeight=200;//wrap_content下设置的默认值
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(defautWidth,defaultHeight);
}else if(widthMode == MeasureSpec.AT_MOST){
setMeasuredDimension(defautWidth,heightSize);
}else if(heightMode == MeasureSpec.AT_MOST){
setMeasuredDimension(widthSize,defaultHeight);
}else{
super.onMeasure(widthMeasureSpec,heightMeasureSpec);
}
}
@Override
protected void onDraw(Canvas canvas) {
int width = getWidth()-getPaddingLeft()-getPaddingRight();
int height = getHeight()-getPaddingBottom()-getPaddingTop();
canvas.drawCircle(getPaddingLeft()+width/2,getPaddingTop()+height/2,ridus,paint);
}
}
2.继承ViewGroup:
public class HorizontalScrollView extends ViewGroup{
private Context context;
private Scroller scroller;
private VelocityTracker velocityTracker;//记录滑动速度
private int lastX=0,lastY=0;//记录上次滑动的坐标
private int lastInterceptX=0,lastInterceptY=0;//记录上次intercept中的坐标
private int mChildIndex=0;
private int mChildWidth=0;
private int mChildSize = 0;
public HorizontalScrollView(Context context) {
this(context,null);
}
public HorizontalScrollView(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public HorizontalScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.context=context;
init();
}
private void init(){
scroller = new Scroller(context);
velocityTracker = VelocityTracker.obtain();
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean isIntercept =false;
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
isIntercept = false;
if(!scroller.isFinished()){
scroller.abortAnimation();
isIntercept=true;
}
break;
case MotionEvent.ACTION_MOVE:
int delX = x-lastInterceptX;
int delY = y-lastInterceptY;
if(Math.abs(delX)>Math.abs(delY)){//横向移动的距离大于纵向的就拦截自己处理
isIntercept = true;
}else{
isIntercept = false;
}
break;
case MotionEvent.ACTION_UP:
isIntercept=false;
break;
}
lastX = x;
lastY = y;
lastInterceptX = x;
lastInterceptY = y;
return isIntercept;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
velocityTracker.addMovement(event);
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
if(!scroller.isFinished()){
scroller.abortAnimation();
}
break;
case MotionEvent.ACTION_MOVE:
int delX = x-lastX;
scrollBy(-delX,0);
break;
case MotionEvent.ACTION_UP:
int scrollX = getScrollX();
velocityTracker.computeCurrentVelocity(1000);
float xVelocity = velocityTracker.getXVelocity();
if(Math.abs(xVelocity)>50){
mChildIndex = xVelocity>0 ? mChildIndex-1 : mChildIndex+1;
}else{
mChildIndex = (scrollX+mChildWidth/2)/mChildWidth;
}
mChildIndex = Math.max(0,Math.min(mChildIndex,mChildSize-1));
int dx = getTotalViewWidth() - scrollX;
smoothScrollBy(dx,0);
velocityTracker.clear();
break;
}
lastX = x;
lastY = y;
return true;
}
private int getTotalViewWidth(){
int total = 0;
for(int i=0;i<mChildIndex;i++){
View view = getChildAt(i);
MarginLayoutParams lp = (MarginLayoutParams) view.getLayoutParams();
total+=view.getMeasuredWidth()+lp.leftMargin+lp.rightMargin;
}
total += getPaddingLeft() + getPaddingRight();
return total;
}
private void smoothScrollBy(int dx,int dy){
scroller.startScroll(getScrollX(),0,dx,0,500);
invalidate();
}
@Override
public void computeScroll() {
if(scroller.computeScrollOffset()){
scrollTo(scroller.getCurrX(),scroller.getCurrY());
postInvalidate();
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec,heightMeasureSpec);
int measureWidth = 0 , measureHeight = 0;
int childCount = getChildCount();
measureChildren(widthMeasureSpec,heightMeasureSpec);
int widthSpedMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpedMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();
if(childCount==0){
if(widthSpedMode == MeasureSpec.AT_MOST && heightSpedMode == MeasureSpec.AT_MOST){
setMeasuredDimension(paddingLeft+paddingRight,paddingTop+paddingBottom);
}else if(widthSpedMode == MeasureSpec.AT_MOST){
setMeasuredDimension(paddingLeft+paddingRight,heightSpecSize );
}else if(heightSpedMode == MeasureSpec.AT_MOST){
setMeasuredDimension(widthSpecSize ,paddingTop+paddingBottom);
}else{
setMeasuredDimension(widthSpecSize ,heightSpecSize );
}
}else{
if(widthSpedMode == MeasureSpec.AT_MOST){
for(int i=0;i<childCount;i++) {
View view = getChildAt(i);
MarginLayoutParams lp = (MarginLayoutParams) view.getLayoutParams();
measureWidth+=view.getMeasuredWidth()+lp.leftMargin+lp.rightMargin;
}
measureWidth+=paddingLeft+paddingRight;
}else{
measureWidth = widthSpecSize;
}
if(heightSpedMode == MeasureSpec.AT_MOST){
for(int i=0;i<childCount;i++) {
View view = getChildAt(i);
MarginLayoutParams lp = (MarginLayoutParams) view.getLayoutParams();
measureHeight = Math.max(measureHeight,view.getMeasuredHeight()+lp.topMargin+lp.bottomMargin);
}
measureHeight+=paddingBottom+paddingTop;
}else{
measureHeight = heightSpecSize;
}
setMeasuredDimension(measureWidth,measureHeight);
}
}
@Override
protected void onLayout(boolean isChanged, int left, int top, int right, int bottom) {
int mLeft = getPaddingLeft();
int childCount = getChildCount();
mChildSize = childCount;
for(int n=0;n<childCount;n++){
View childView = getChildAt(n);
mChildWidth = childView.getMeasuredWidth();
MarginLayoutParams lp =(MarginLayoutParams) childView.getLayoutParams();
if(childView.getVisibility() != GONE){
int childWidth = childView.getMeasuredWidth();
int mTop = getPaddingTop()+lp.topMargin;
mLeft+=lp.leftMargin;
childView.layout(mLeft,mTop,mLeft+childWidth,mTop+childView.getMeasuredHeight());
mLeft+=lp.rightMargin+childWidth;
}
}
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {//要支持margin必须重写这个方法
return new MarginLayoutParams(context,attrs);
}
@Override
protected void onDetachedFromWindow() {//释放资源,关闭线程等
velocityTracker.recycle();
super.onDetachedFromWindow();
}
}