Android学习自定义View(二)——View和ViewGroup绘制流程以及invalidate()

简介: MainActivity如下: package cc.testviewstudy2;import android.os.Bundle;import android.

MainActivity如下:

package cc.testviewstudy2;

import android.os.Bundle;
import android.widget.LinearLayout;
import android.app.Activity;
/**
 * Demo描述:
 * 关于自定义View的学习(二)
 * 
 * View的绘制流程:onMeasure()-->onLayout()-->onDraw()
 * 
 * 学习资料:
 * 1 http://blog.csdn.net/guolin_blog/article/details/16330267
 * 2 http://androidxref.com/2.3.6/xref/frameworks/base/core/java/android/view/View.java
 * 3 http://blog.csdn.net/dawanganban/article/details/23953827
 * 4 http://blog.csdn.net/xyz_lmn/article/details/20385049
 * 5 http://blog.csdn.net/cskgnt/article/details/7988486
 *   Thank you very much
 * 
 */
public class MainActivity extends Activity {

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.main);
		init();
	}
	
	
	//验证在onDraw()里面draw出来的东西并不是一个子View
	private void init(){
		LinearLayout linearLayout=(LinearLayout) findViewById(R.id.linearLayoutTest);
		int childCount=linearLayout.getChildCount();
		System.out.println("---> childCount="+childCount);
	}

}





LinearLayoutTest如下:

package cc.testviewstudy2;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;
import android.widget.LinearLayout;
/**
 * View绘制的三个过程onMeasure()-->onLayout()-->onDraw()
 * 
 * 
 * 
 * View系统的绘制流程
 * 1 ViewRoot的performTraversals()方法中开始,在其内部调用View的measure()方法.
 * 2 完成View的measure()结束后继续执行,ViewRoot中调用View的layout()方法.
 * 3 在layout()后ViewRoot中的代码会继续执行并创建出一个Canvas对象
 *   然后调用View的draw()方法来执行具体的绘制工作
 *   
 *   
 *   private void performTraversals() {  
 *     final View host = mView;  
 *     ...  
 *     host.measure(childWidthMeasureSpec, childHeightMeasureSpec);  
 *     ...  
 *     host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());  
 *     ...  
 *     draw(fullRedrawNeeded);  
 *  }  
 *   
 * 
 * onMeasure():测量View的大小并且保存了测量的结果
 * 我们首先来看该方法的定义:
 * onMeasure(int widthMeasureSpec,int heightMeasureSpec)
 * 首先搞明白这两个值输入参数是从哪里得到的?
 * 通常情况下:
 * 这两个值是由父视图经过计算后传递给子视图的,这说明父视图会在一定程度上决定子视图的大小
 * 
 * 
 * 注意两个输入参数widthMeasureSpec和heightMeasureSpec不是一般的int而是MeasureSpec.
 * MeasureSpec包含了specMode和specSize这两部分内容.
 * 即specSize表示宽和高的大小,而specMode表示了宽和高的方式(规格).
 * 其中,specMode有三种类型:
 * 1 EXACTLY(具体的值为  1 << 30 即为:1073741824)
 * 表示父视图希望子视图的大小应该是由specSize的值来决定的,系统默认会按照这个规则来设置子视图的大小
 * 例如我们在xml文件中为控件的宽和高设置成一个具体的值或MATCH_PARENT时MeasureSpec的specMode就等于EXACTLY
 * 2 AT_MOST(具体的值为  2 << 30)
 * 表示子视图最多只能是specSize中指定的大小,开发人员应尽可能小得去设置这个视图,并且保证不会超过specSize
 * 例如我们在xml文件中为控件的宽和高设置成WRAP_CONTENT的时候,MeasureSpec的specMode就等于AT_MOST
 * 3 UNSPECIFIED(具体的值为  0)
 * 表示开发人员可以将视图按照自己的意愿设置成任意的大小,没有任何限制.
 * 这种情况极少用到
 * 
 * 
 * 了解了该方法的定义,我们来梳理一下测量(measure)的流程
 * 1 关于View的onMeasure()
 *   为了更好的理解onMeasure(),我们从View的measure()方法说起.
 *   比如有个View view;我们可调用view.measure(widthMeasureSpec, heightMeasureSpec)
 *   请注意源码中View的measure()这个方法是final的,所以我们无法在子类中去重写这个方法.
 *   那么这个方法到底干了些什么呢?假如我们要自己measure又该怎么办呢?
 *   1.1 在view.measure()内,核心代码是调用了onMeasure()
 *   1.2 我们看一下onMeasure()方法的源码:
 *       protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
 *           setMeasuredDimension(
 *              getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
 *              getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)
 *           );
 *       }
 *       在此继续看getDefaultSize()和setMeasuredDimension()方法的源码,可知,
 *       onMeasure()的执行过程:
 *       1 利用getDefaultSize()获取到了view宽高的Mode,再依据Mode得到其大小size
 *       2 利用setMeasuredDimension()方法保存了上一步中测量得到的该view的大小
 *         注意setMeasuredDimension()也是final的,不可重写;可调用
 *       所以:
 *       1 我们要自己measure的时候可以重写onMeasure()
 *       2 只有在调用了onMeasure()方法后调用getMeasuredWidth()和getMeasuredHeight()才可
 *         获取到测量所得到的控件大小,在此之前调用这两个方法获取到的值均为0或者其他错误值.
 *    
 *    这就是系统自动测量View的过程.当然我们要否定这个过程,自己修改测量的结果,可以怎么做呢?
 *    挺简单的,调用:setMeasuredDimension()即可
 *    
 *    综上所述,View大小的控制是由父视图,布局文件,以及视图本身共同完成的:
 *    1 父视图会提供给子视图参考的大小
 *    2 开发人员可在XML文件中指定视图的大小
 *    3 最后视图本身会对最终的大小进行最终的拍板
 *       
 * 
 * 2 关于ViewGroup的onMeasure()
 *   看完了View的绘制流程,再看ViewGroup的绘制流程就简单多了.
 *   我们去看ViewGroup的子类比如FrameLayout和LinearLayout的源码可知:
 *   2.1 FrameLayout继承自ViewGroup而ViewGroup又继承自View
 *       所以,在ViewGroup(XXXLayout).measure()时,还是会调用onMeasure()
 *   2.2 在onMeasure()内调用了measureChildren()
 *       从该方法名我们也可知,它要测量孩子们了.
 *       2.2.1 针对每个子View调用measureChild()进行测量
 *              2.2.1.1 综合ViewGroup的widthMeasureSpec和heightMeasureSpec以及自身的布局
 *                      得到每个子View的widthMeasureSpec和heightMeasureSpec.
 *                      然后才去调用子view.measure().
 *              2.2.1.2 每个子View调用:view.measure()这就和上面描述的
 *                      View的measure()过程一致了,在此不再赘述.
 *                      
 *   
 * 
 * 
 * 
 * 
 * 
 * 
 * 
 * 
 * 
 * onLayout():视图的布局,即确定视图的位置
 * measure过程结束后,视图的大小就已经测量好了,接下来就该是layout的过程了.
 * 在代码中是这样体现的:
 * ViewRoot的performTraversals()方法会在measure()结束后调用View的layout()方法来执行此过程
 * 如下所示:
 * host.layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight);
 * layout()四个输入参数:分别代表着左、上、右、下的坐标
 * 当然这个坐标是相对于当前视图的父视图而言的.
 * 从最后两个参数可以看到,把measure中测量出的宽度和高度传到了该layout()方法中.
 * 在layout()中的核心代码是调用了onLayout()
 * 
 * 1 View的onLayout()
 *   源码中该方法为空,所以在View中该方法无实际用处.
 *   因为布局和显示子控件是ViewGroup的工作.即父视图决定子视图的显示位置
 * 2 ViewGroup的onLayout()
 *   在ViewGroup中onLayout()是一个抽象方法:
 *   protected abstract void onLayout(boolean changed, int l, int t, int r, int b);
 *   所以我们在实现自定义ViewGroup时必须要重写该方法,否则其中的控件是无法显示的.
 *   在重写onLayout()时:
 *   2.1 得到每个子View
 *   2.2 每个子View调用layout()方法.其中r-l=控件的宽,b-t=控件的高.
 *       所以说该方法决定了控件的摆放也决定了控件的大小.
 *       2.2.1 常用childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight());
 *             注意最后两个参数就是在onMeasure()阶段测量得到的宽和高.
 *             以前有个错误的认识:
 *             在看到设置childView.measure(300, 300)后界面上View的大小改变了就觉得
 *             measure()方法除了测量的作用还可以设置View的大小.
 *             其实这样的理解是片面和误读的.
 *             是因为系统默认的情况下在onLayout()阶段会用到onMeasure()阶段测量的控件大小.
 *       2.2.2 也可不用 在onMeasure()阶段测量得到的宽和高作为参数,而按照需求给定其他值也行.
 *       
 *       
 *  在onMeasure()和onLayout()结束之后,在此比较一下getMeasureWidth()和getWidth()的区别
 *  1 获取的时机不一样
 *    getMeasureWidth()方法在onMeasure()结束后就可获取到
 *    getWidth()方法要在layout()过程结束后才能获取到
 *  2 计算方式不一样.
 *    getMeasureWidth()的值是在setMeasuredDimension()方法已经设置好的.
 *    getWidth()方法中的值则是通过视图右边的坐标减去左边的坐标计算得出的.
 *   
 * 
 * 
 * 
 * 
 * 
 * 
 * 
 * 
 * 
 * 
 * onDraw():
 * 绘制图形界面.
 * ViewRoot的performTraversals()方法会在layout()结束后调用View的draw()方法来执行此过程
 * draw()一共有六步:
 * Draw traversal performs several drawing steps which must be executed  
 * in the appropriate order:  
 * 1. Draw the background  
 * 2. If necessary, save the canvas' layers to prepare for fading  
 * 3. Draw view's content  
 * 4. Draw children  
 * 5. If necessary, draw the fading edges and restore layers  
 * 6. Draw decorations (scrollbars for instance)  
 * 
 * 第三步核心是onDraw()绘制内容
 * 第四步的核心就是dispatchDraw()绘制子View
 * 
 * 
 * 
 * 
 * 
 * 
 * 
 * 
 * 分析重绘:
 * invalidate()或者postInvalidate()都会导致整个View树重绘
 * 而且是自上而下的重绘,比如:从最外层的Layout到里层的Layout,直到每个子View.
 * 在重绘时又会调用draw()方法
 * draw()一共有六步: 
 *   Draw traversal performs several drawing steps which must be executed   
 *   in the appropriate order:   
 *   1. Draw the background   
 *   2. If necessary, save the canvas' layers to prepare for fading   
 *   3. Draw view's content   
 *   4. Draw children   
 *   5. If necessary, draw the fading edges and restore layers   
 *   6. Draw decorations (scrollbars for instance)
 *   其中最重要的是第三步和第四步
 *   第三步会去调用onDraw()绘制内容
 *   第四步会去调用dispatchDraw()绘制子View
 *   
 *   在这个从上而下的重绘过程中,ViewGroup和View的重绘流程差不多.
 *   最重要的差别在于View不会执行dispatchDraw()因为它没有子View.
 *   
 *   在重绘View树时ViewGroup和View时按理都会经过onMeasure()和onLayout()以及
 *   onDraw()方法.当然系统会判断这三个方法是否都必须执行,如果没有必要就不会调用.
 * 
 * 
 * 
 * 注意事项:
 * 1 该示例继承自XXXXLayout不要继承自ViewGroup
 *   假如继承自ViewGroup那么xml的cc.testviewstudy2.ViewGroupLayout中
 *   设置layout_width和layout_height不论是wrap_content还是fill_parent
 *   这个父视图总是充满屏幕的.
 *   现象是如此,原因暂不明.待以后调查
 *   
 * 2 在本LinearLayoutTest我们只放入一个ImageView作为测试
 * 
 * 3 在LinearLayoutTest中有个ImageView,所以其子View只有一个.
 *   注意:在onDraw()里面绘制出来的东西并不算作子View
 */
public class LinearLayoutTest extends LinearLayout {
    private Paint mPaint;
	public LinearLayoutTest(Context context) {
		super(context);
	}

	public LinearLayoutTest(Context context, AttributeSet attrs) {
		super(context, attrs);
	}
	
	
	
	/**
	 * 再次补充说明measureChild()方法,解释如下:
	 * 综合ViewGroup的widthMeasureSpec和heightMeasureSpec以及自身的布局
	 * 从而得到每个子View的widthMeasureSpec和heightMeasureSpec.
	 * 然后才去调用子view.measure()
	 * 
	 * 源码如下:
	 *  protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
	 *     final LayoutParams lp = child.getLayoutParams();
	 *     final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
	 *     mPaddingLeft + mPaddingRight, lp.width);
	 *     final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
	 *     mPaddingTop + mPaddingBottom, lp.height);
	 *     child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
	 *  }
	 *  
	 *  所以该方法不是用来修改子View经过测量所得的大小.
	 *  
	 *  可用measure()方法
	 *  childView.measure(MeasureSpec.EXACTLY+200, MeasureSpec.EXACTLY+200);
	 *  修改系统默认测量的子view的大小.
	 *  有的人会说:直接调用childView.setMeasuredDimension()就行了嘛,何必调用
	 *  childView.measure()呢?反正measure()最后悔调用setMeasuredDimension()的.
	 *  可是View的对象不能直接调用setMeasuredDimension()方法.
	 *  1 注意源码 该方法的定义
	 *    protected final void setMeasuredDimension(int measuredWidth, int measuredHeight)
	 *    它是protected修饰的
	 *  2 关于protected关键字可以参考
	 *    http://blog.csdn.net/cskgnt/article/details/7988486
	 *  
	 *  
	 */
	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		super.onMeasure(widthMeasureSpec, heightMeasureSpec);
		int widthSize=MeasureSpec.getSize(widthMeasureSpec);
		int widthMode=MeasureSpec.getMode(widthMeasureSpec);
		int heightSize=MeasureSpec.getSize(heightMeasureSpec);
		int heightMode=MeasureSpec.getMode(heightMeasureSpec);
		System.out.println("父视图给出的该ViewGroup的参考值:");
		System.out.println("widthSize="+widthSize+",widthMode="+widthMode);
		System.out.println("heightSize="+heightSize+",heightMode="+heightMode);
		System.out.println("            ");
		//我们可以不依照该参考值,而通过以下代码自己设置
		//设置了整个ViewGroup(即此处的LinearLayoutTest)的大小
		this.setMeasuredDimension(250, 250);
		if (getChildCount()>0) {
			View childView=getChildAt(0);
			
			//测量子View
			measureChild(childView, widthMeasureSpec, heightMeasureSpec);
			//不能简单粗暴地这写:
			//measureChild(childView, (MeasureSpec.EXACTLY+150), (MeasureSpec.EXACTLY+150));
			//以为这样就可以最终修改测量值.原因参见上面的解释
			
			//但我们可以这样来设置子View的测量大小
			//childView.measure(MeasureSpec.EXACTLY+200, MeasureSpec.EXACTLY+200);
			
		}
	}
	
	

	/**
	 *注意事项:
	 *1 layout(int l, int t, int r, int b)
	 *  该方法的值都是相对于父元素的左上角而言
	 *  
	 *2  若要想把子View的X,Y位置均平移50
	 *  错误写法--->
	 *  childView.layout(50, 50, childView.getMeasuredWidth(), childView.getMeasuredHeight());
	 *  导致图片显示出来很小
	 *  正确写法--->
	 *  childView.layout(50, 50, childView.getMeasuredWidth()+50, childView.getMeasuredHeight()+50);
	 *
	 */
	@Override
	protected void onLayout(boolean arg0, int left, int top, int right, int bottom) {
		if (getChildCount()>0) {
			View childView=getChildAt(0);
			System.out.println("子视图childView.getMeasuredWidth()="+childView.getMeasuredWidth()+
					           ",子视图childView.getMeasuredHeight()="+ childView.getMeasuredHeight());
			
			//摆放子View
			childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight());
		}

	}
	
	
	
	
	/**
	 *注意事项:
	 *在onDraw()里面draw出来的东西并不是一个子View
	 *参见MainActivity里的验证性代码
	 */
	@Override
	protected void onDraw(Canvas canvas) {
		super.onDraw(canvas);
		mPaint = new Paint();
		mPaint.setColor(Color.YELLOW);
		mPaint.setTextSize(20);
		String content = "Hello World";
		canvas.drawText(content, 150, 100, mPaint);
	}

}



main.xml如下:

<cc.testviewstudy2.LinearLayoutTest 
    android:id="@+id/linearLayoutTest"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:background="#ff0033"
     >

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/ic_launcher" 
    />

</cc.testviewstudy2.LinearLayoutTest>







相关文章
|
1月前
|
XML 前端开发 Android开发
Android:UI:Drawable:View/ImageView与Drawable
通过本文的介绍,我们详细探讨了Android中Drawable、View和ImageView的使用方法及其相互关系。Drawable作为图像和图形的抽象表示,提供了丰富的子类和自定义能力,使得开发者能够灵活地实现各种UI效果。View和ImageView则通过使用Drawable实现了各种图像和图形的显示需求。希望本文能为您在Android开发中使用Drawable提供有价值的参考和指导。
40 2
|
1月前
|
Android开发 开发者 UED
安卓开发中自定义View的实现与性能优化
【10月更文挑战第28天】在安卓开发领域,自定义View是提升应用界面独特性和用户体验的重要手段。本文将深入探讨如何高效地创建和管理自定义View,以及如何通过代码和性能调优来确保流畅的交互体验。我们将一起学习自定义View的生命周期、绘图基础和事件处理,进而探索内存和布局优化技巧,最终实现既美观又高效的安卓界面。
37 5
|
2月前
|
缓存 数据处理 Android开发
在 Android 中使用 RxJava 更新 View
【10月更文挑战第20天】使用 RxJava 来更新 View 可以提供更优雅、更高效的解决方案。通过合理地运用操作符和订阅机制,我们能够轻松地处理异步数据并在主线程中进行 View 的更新。在实际应用中,需要根据具体情况进行灵活运用,并注意相关的注意事项和性能优化,以确保应用的稳定性和流畅性。可以通过不断的实践和探索,进一步掌握在 Android 中使用 RxJava 更新 View 的技巧和方法,为开发高质量的 Android 应用提供有力支持。
|
2月前
|
缓存 调度 Android开发
Android 在子线程更新 View
【10月更文挑战第21天】在 Android 开发中,虽然不能直接在子线程更新 View,但通过使用 Handler、AsyncTask 或 RxJava 等方法,可以实现子线程操作并在主线程更新 View 的目的。在实际应用中,需要根据具体情况选择合适的方法,并注意相关的注意事项和性能优化,以确保应用的稳定性和流畅性。可以通过不断的实践和探索,进一步掌握在子线程更新 View 的技巧和方法,为开发高质量的 Android 应用提供支持。
39 2
|
2月前
|
XML 前端开发 Android开发
Android面试高频知识点(3) 详解Android View的绘制流程
Android面试高频知识点(3) 详解Android View的绘制流程
Android面试高频知识点(3) 详解Android View的绘制流程
|
2月前
|
XML 前端开发 Android开发
Android面试高频知识点(3) 详解Android View的绘制流程
Android面试高频知识点(3) 详解Android View的绘制流程
27 2
|
2月前
|
Web App开发 编解码 视频直播
视频直播技术干货(十二):从入门到放弃,快速学习Android端直播技术
本文详细介绍了Android端直播技术的全貌,涵盖了从实时音视频采集、编码、传输到解码与播放的各个环节。文章还探讨了直播中音视频同步、编解码器选择、传输协议以及直播延迟优化等关键问题。希望本文能为你提供有关Andriod端直播技术的深入理解和实践指导。
56 0
|
28天前
|
开发框架 前端开发 Android开发
安卓与iOS开发中的跨平台策略
在移动应用开发的战场上,安卓和iOS两大阵营各据一方。随着技术的演进,跨平台开发框架成为开发者的新宠,旨在实现一次编码、多平台部署的梦想。本文将探讨跨平台开发的优势与挑战,并分享实用的开发技巧,帮助开发者在安卓和iOS的世界中游刃有余。
|
15天前
|
搜索推荐 前端开发 API
探索安卓开发中的自定义视图:打造个性化用户界面
在安卓应用开发的广阔天地中,自定义视图是一块神奇的画布,让开发者能够突破标准控件的限制,绘制出独一无二的用户界面。本文将带你走进自定义视图的世界,从基础概念到实战技巧,逐步揭示如何在安卓平台上创建和运用自定义视图来提升用户体验。无论你是初学者还是有一定经验的开发者,这篇文章都将为你打开新的视野,让你的应用在众多同质化产品中脱颖而出。
40 19
|
16天前
|
JSON Java API
探索安卓开发:打造你的首个天气应用
在这篇技术指南中,我们将一起潜入安卓开发的海洋,学习如何从零开始构建一个简单的天气应用。通过这个实践项目,你将掌握安卓开发的核心概念、界面设计、网络编程以及数据解析等技能。无论你是初学者还是有一定基础的开发者,这篇文章都将为你提供一个清晰的路线图和实用的代码示例,帮助你在安卓开发的道路上迈出坚实的一步。让我们一起开始这段旅程,打造属于你自己的第一个安卓应用吧!
41 14