Android自定义ViewGroup View的大小和坐标控制

  1. 除非你总是需要一个100×100像素的控件,否则,你必须要重写onMeasure。  
  2.   
  3. onMeasure方法在控件的父元素正要放置它的子控件时调用。它会问一个问题,“你想要用多大地方啊?”,然后传入两个参数——widthMeasureSpec和heightMeasureSpec。  
  4. 它们指明控件可获得的空间以及关于这个空间描述的元数据。  
  5.   
  6.  比返回一个结果要好的方法是你传递View的高度和宽度到setMeasuredDimension方法里。  
  7. 接下来的代码片段给出了如何重写onMeasure。注意,调用的本地空方法是来计算高度和宽度的。它们会译解widthHeightSpec和heightMeasureSpec值,并计算出合适的高度和宽度值。  
  8.   
  9.    
  10.   
  11. @Override  
  12.   
  13. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  14.   
  15. int measuredHeight = measureHeight(heightMeasureSpec);  
  16.   
  17. int measuredWidth = measureWidth(widthMeasureSpec);  
  18.   
  19. setMeasuredDimension(measuredHeight, measuredWidth);  
  20.   
  21. }  
  22.   
  23.    
  24.   
  25. private int measureHeight(int measureSpec) {  
  26.   
  27. // Return measured widget height.   
  28.   
  29. }  
  30.   
  31.    
  32.   
  33. private int measureWidth(int measureSpec) {  
  34.   
  35. // Return measured widget width.   
  36.   
  37. }  
  38.   
  39.    
  40.   
  41. 边界参数——widthMeasureSpec和heightMeasureSpec ,效率的原因以整数的方式传入。在它们使用之前,首先要做的是使用MeasureSpec类的静态方法getMode和getSize来译解,如下面的片段所示:  
  42.   
  43.    
  44.   
  45. int specMode = MeasureSpec.getMode(measureSpec);  
  46.   
  47. int specSize = MeasureSpec.getSize(measureSpec);  
  48.   
  49.    
  50.   
  51. 依据specMode的值,如果是AT_MOST,specSize 代表的是最大可获得的空间;如果是EXACTLY,specSize 代表的是精确的尺寸;如果是UNSPECIFIED,对于控件尺寸来说,没有任何参考意义。  
  52.   
  53. 当以EXACT方式标记测量尺寸,父元素会坚持在一个指定的精确尺寸区域放置View。在父元素问子元素要多大空间时,AT_MOST指示者会说给我最大的范围。在很多情况下,你得到的值都是相同的。  
  54.   
  55. 在两种情况下,你必须绝对的处理这些限制。在一些情况下,它可能会返回超出这些限制的尺寸,在这种情况下,你可以让父元素选择如何对待超出的View,使用裁剪还是滚动等技术。  
  56.   
  57.  接下来的框架代码给出了处理View测量的典型实现:  
  58.   
  59.    
  60.   
  61. @Override  
  62.   
  63. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  64.   
  65. int measuredHeight = measureHeight(heightMeasureSpec);  
  66.   
  67. int measuredWidth = measureWidth(widthMeasureSpec);  
  68.   
  69. setMeasuredDimension(measuredHeight, measuredWidth);  
  70.   
  71. }  
  72.   
  73.    
  74.   
  75. private int measureHeight(int measureSpec) {  
  76.   
  77. int specMode = MeasureSpec.getMode(measureSpec);  
  78.   
  79. int specSize = MeasureSpec.getSize(measureSpec);  
  80.   
  81.    
  82.   
  83. // Default size if no limits are specified.   
  84.   
  85. int result = 500;  
  86.   
  87. if (specMode == MeasureSpec.AT_MOST)   
  88.   
  89. {  
  90.   
  91. // Calculate the ideal size of your   
  92.   
  93. // control within this maximum size.   
  94.   
  95. // If your control fills the available   
  96.   
  97. // space return the outer bound.   
  98.   
  99. result = specSize;  
  100.   
  101. }   
  102.   
  103. else if (specMode == MeasureSpec.EXACTLY)   
  104.   
  105. {  
  106.   
  107. // If your control can fit within these bounds return that value.   
  108.   
  109. result = specSize;  
  110.   
  111. }  
  112.   
  113. return result;  
  114.   
  115. }  
  116.   
  117.    
  118.   
  119. private int measureWidth(int measureSpec) {  
  120.   
  121. int specMode = MeasureSpec.getMode(measureSpec);  
  122.   
  123. int specSize = MeasureSpec.getSize(measureSpec);  
  124.   
  125.    
  126.   
  127. // Default size if no limits are specified.   
  128.   
  129. int result = 500;  
  130.   
  131. if (specMode == MeasureSpec.AT_MOST)  
  132.   
  133. {  
  134.   
  135. // Calculate the ideal size of your control   
  136.   
  137. // within this maximum size.   
  138.   
  139. // If your control fills the available space   
  140.   
  141. // return the outer bound.   
  142.   
  143. result = specSize;  
  144.   
  145. }   
  146.   
  147. else if (specMode == MeasureSpec.EXACTLY)   
  148.   
  149. {  
  150.   
  151. // If your control can fit within these bounds return that value.   
  152.   
  153. result = specSize;  
  154.   
  155. }  
  156.   
  157. return result;  
  158.   
  159. }  
  160.   
  161.    
除非你总是需要一个100×100像素的控件,否则,你必须要重写onMeasure。

onMeasure方法在控件的父元素正要放置它的子控件时调用。它会问一个问题,“你想要用多大地方啊?”,然后传入两个参数——widthMeasureSpec和heightMeasureSpec。
它们指明控件可获得的空间以及关于这个空间描述的元数据。

 比返回一个结果要好的方法是你传递View的高度和宽度到setMeasuredDimension方法里。
接下来的代码片段给出了如何重写onMeasure。注意,调用的本地空方法是来计算高度和宽度的。它们会译解widthHeightSpec和heightMeasureSpec值,并计算出合适的高度和宽度值。

 

@Override

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

int measuredHeight = measureHeight(heightMeasureSpec);

int measuredWidth = measureWidth(widthMeasureSpec);

setMeasuredDimension(measuredHeight, measuredWidth);

}

 

private int measureHeight(int measureSpec) {

// Return measured widget height.

}

 

private int measureWidth(int measureSpec) {

// Return measured widget width.

}

 

边界参数——widthMeasureSpec和heightMeasureSpec ,效率的原因以整数的方式传入。在它们使用之前,首先要做的是使用MeasureSpec类的静态方法getMode和getSize来译解,如下面的片段所示:

 

int specMode = MeasureSpec.getMode(measureSpec);

int specSize = MeasureSpec.getSize(measureSpec);

 

依据specMode的值,如果是AT_MOST,specSize 代表的是最大可获得的空间;如果是EXACTLY,specSize 代表的是精确的尺寸;如果是UNSPECIFIED,对于控件尺寸来说,没有任何参考意义。

当以EXACT方式标记测量尺寸,父元素会坚持在一个指定的精确尺寸区域放置View。在父元素问子元素要多大空间时,AT_MOST指示者会说给我最大的范围。在很多情况下,你得到的值都是相同的。

在两种情况下,你必须绝对的处理这些限制。在一些情况下,它可能会返回超出这些限制的尺寸,在这种情况下,你可以让父元素选择如何对待超出的View,使用裁剪还是滚动等技术。

 接下来的框架代码给出了处理View测量的典型实现:

 

@Override

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

int measuredHeight = measureHeight(heightMeasureSpec);

int measuredWidth = measureWidth(widthMeasureSpec);

setMeasuredDimension(measuredHeight, measuredWidth);

}

 

private int measureHeight(int measureSpec) {

int specMode = MeasureSpec.getMode(measureSpec);

int specSize = MeasureSpec.getSize(measureSpec);

 

// Default size if no limits are specified.

int result = 500;

if (specMode == MeasureSpec.AT_MOST) 

{

// Calculate the ideal size of your

// control within this maximum size.

// If your control fills the available

// space return the outer bound.

result = specSize;

} 

else if (specMode == MeasureSpec.EXACTLY) 

{

// If your control can fit within these bounds return that value.

result = specSize;

}

return result;

}

 

private int measureWidth(int measureSpec) {

int specMode = MeasureSpec.getMode(measureSpec);

int specSize = MeasureSpec.getSize(measureSpec);

 

// Default size if no limits are specified.

int result = 500;

if (specMode == MeasureSpec.AT_MOST)

{

// Calculate the ideal size of your control

// within this maximum size.

// If your control fills the available space

// return the outer bound.

result = specSize;

} 

else if (specMode == MeasureSpec.EXACTLY) 

{

// If your control can fit within these bounds return that value.

result = specSize;

}

return result;

}

 

 

getWidth得到是某个view的实际尺寸.

getMeasuredWidth是得到某view想要在parent view里面占的大小.

 

  1. getWidth在OnCreat的时候得到的是0. 当一个view对象创建时,android并不知道其大小,所以getWidth()和   getHeight()返回的结果是0,真正大小是在计算布局时才会计算.

    2.  getMeasuredWidth必须在parent view或者它自己调用measure()函数之后才能得到. measure函数就是计算该函数需要占用的空间大小.

 

    3.onMeasure会在onLayout之前调用

     4.如果ViewGroup中的View通过动画移动了位置 需要调用requestLayout()重新定位View的位置;
 
 
     viewgroup简单说就是可以装view的view.今天遇到一个问题,就是需要一个可以自动根据一行中view的宽度自动换行的布局,网上找了下,没有相关的例子,但是找到了思路:自定义一个viewgroup,然后在onlayout文件里面自动检测view的右边缘的横坐标值,和你的view的parent view的况度判断是否换行显示view就可以了。因为代码比较简单,就不多说了:

  

复制代码
 1 public class MyViewGroup extends ViewGroup {
 2     private final static String TAG = "MyViewGroup";
 3     
 4     private final static int VIEW_MARGIN=2;
 5 
 6     public MyViewGroup(Context context) {
 7         super(context);
 8     }
 9     @Override
10     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
11         Log.d(TAG, "widthMeasureSpec = "+widthMeasureSpec+" heightMeasureSpec"+heightMeasureSpec);
12         
13         for (int index = 0; index < getChildCount(); index++) {
14             final View child = getChildAt(index);
15             // measure16             child.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
17         }
18 
19         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
20     }
21 
22     @Override
23     protected void onLayout(boolean arg0, int arg1, int arg2, int arg3, int arg4) {
24         Log.d(TAG, "changed = "+arg0+" left = "+arg1+" top = "+arg2+" right = "+arg3+" botom = "+arg4);
25         final int count = getChildCount();
26         int row=0;// which row lay you view relative to parent27         int lengthX=arg1;    // right position of child relative to parent28         int lengthY=arg2;    // bottom position of child relative to parent29         for(int i=0;i<count;i++){
30             
31             final View child = this.getChildAt(i);
32             int width = child.getMeasuredWidth();
33             int height = child.getMeasuredHeight();
34             lengthX+=width+VIEW_MARGIN;
35             lengthY=row*(height+VIEW_MARGIN)+VIEW_MARGIN+height+arg2;
36             //if it can't drawing on a same line , skip to next line37             if(lengthX>arg3){
38                 lengthX=width+VIEW_MARGIN+arg1;
39                 row++;
40                 lengthY=row*(height+VIEW_MARGIN)+VIEW_MARGIN+height+arg2;
41                 
42             }
43             
44             child.layout(lengthX-width, lengthY-height, lengthX, lengthY);
45         }
46 
47     }
48 
49 }
复制代码

  这里有个地方要注意,那就要明白ViewGroup的绘图流程:ViewGroup绘制包括两个步骤:1.measure 2.layout

  在两个步骤中分别调用回调函数:1.onMeasure()   2.onLayout()

  1.onMeasure() 在这个函数中,ViewGroup会接受childView的请求的大小,然后通过childView的 measure(newWidthMeasureSpec, heightMeasureSpec)函数存储到childView中,以便childView的getMeasuredWidth() andgetMeasuredHeight() 的值可以被后续工作得到。

  2.onLayout() 在这个函数中,ViewGroup会拿到childView的getMeasuredWidth() andgetMeasuredHeight(),用来布局所有的childView。

  3.View.MeasureSpec 与 LayoutParams 这两个类,是ViewGroup与childView协商大小用的。其中,View.MeasureSpec是ViewGroup用来部署 childView用的, LayoutParams是childView告诉ViewGroup 我需要多大的地方。

  4.在View 的onMeasure的最后要调用setMeasuredDimension()这个方法存储View的大小,这个方法决定了当前View的大小。

  

  效果图:

                                 

 

 

我们都知道在onCreate()里面获取控件的高度是0,这是为什么呢?我们来看一下示例:
首先我们自己写一个控件,这个控件非常简单:
[java]
public class MyImageView extends ImageView { 
 
    public MyImageView(Context context, AttributeSet attrs) { 
        super(context, attrs); 
    } 
    public MyImageView(Context context) { 
        super(context); 
    } 
     
    @Override 
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 
        super.onMeasure(widthMeasureSpec, heightMeasureSpec); 
        System.out.println("onMeasure 我被调用了"+System.currentTimeMillis()); 
    } 
     
    @Override 
    protected void onDraw(Canvas canvas) { 
        super.onDraw(canvas); 
        System.out.println("onDraw 我被调用了"+System.currentTimeMillis()); 
    } 
 

布局文件:
[java] 
<com.test.MyImageView 
    android:id="@+id/imageview" 
    android:layout_width="wrap_content" 
    android:layout_height="wrap_content" 
    android:src="@drawable/test" /> 

测试的Activity的onCreate():
[java] 
@Override 
public void onCreate(Bundle savedInstanceState) { 
    super.onCreate(savedInstanceState); 
    setContentView(R.layout.main);         
    System.out.println("执行完毕.."+System.currentTimeMillis()); 

现在我们现在来看一下结果:

说明等onCreate方法执行完了,我们定义的控件才会被度量(measure),所以我们在onCreate方法里面通过view.getHeight()获取控件的高度或者宽度肯定是0,因为它自己还没有被度量,也就是说他自己都不知道自己有多高,而你这时候去获取它的尺寸,肯定是不行的.

现在碰到这个问题我们不能不解决,在网上找到了如下办法:
[java] 
//------------------------------------------------方法一 
int w = View.MeasureSpec.makeMeasureSpec(0,View.MeasureSpec.UNSPECIFIED); 
int h = View.MeasureSpec.makeMeasureSpec(0,View.MeasureSpec.UNSPECIFIED); 
imageView.measure(w, h); 
int height =imageView.getMeasuredHeight(); 
int width =imageView.getMeasuredWidth(); 
textView.append("\n"+height+","+width); 
 
 
 
 
//-----------------------------------------------方法二 
ViewTreeObserver vto = imageView.getViewTreeObserver(); 
vto.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 
    public boolean onPreDraw() { 
        int height = imageView.getMeasuredHeight(); 
        int width = imageView.getMeasuredWidth(); 
        textView.append("\n"+height+","+width); 
        return true; 
    } 
}); 
//-----------------------------------------------方法三    
ViewTreeObserver vto2 = imageView.getViewTreeObserver();   
vto2.addOnGlobalLayoutListener(new OnGlobalLayoutListener() { 
    @Override   
    public void onGlobalLayout() { 
        imageView.getViewTreeObserver().removeGlobalOnLayoutListener(this);   
        textView.append("\n\n"+imageView.getHeight()+","+imageView.getWidth()); 
    }   
});   

这三个方法是哪里找到现在已经忘了.

现在要讨论的是当我们需要时候使用哪个方法呢?
现在把测试的Activity改成如下:
[java] 
@Override 
  public void onCreate(Bundle savedInstanceState) { 
      super.onCreate(savedInstanceState); 
      setContentView(R.layout.main); 
      final ImageView imageView = (ImageView) findViewById(R.id.imageview);       
       
      //------------------------------------------------方法一 
      int w = View.MeasureSpec.makeMeasureSpec(0,View.MeasureSpec.UNSPECIFIED); 
      int h = View.MeasureSpec.makeMeasureSpec(0,View.MeasureSpec.UNSPECIFIED); 
      imageView.measure(w, h); 
      int height =imageView.getMeasuredHeight(); 
      int width =imageView.getMeasuredWidth(); 
      textView.append("\n"+height+","+width); 
       
      System.out.println("执行完毕.."+System.currentTimeMillis()); 
  } 

 

接着来看下面几种方式输出结果:
把测试Activity改成如下:
[java] 
@Override 
public void onCreate(Bundle savedInstanceState) { 
    super.onCreate(savedInstanceState); 
    setContentView(R.layout.main); 
    final ImageView imageView = (ImageView) findViewById(R.id.imageview); 
-----------------------------------------------方法二 
    ViewTreeObserver vto = imageView.getViewTreeObserver(); 
    vto.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 
        public boolean onPreDraw() { 
            int height = imageView.getMeasuredHeight(); 
            int width = imageView.getMeasuredWidth(); 
            textView.append("\n"+height+","+width); 
            return true; 
        } 
    }); 

结果如下:


方法三就不再测试了同方法二!!!

那么方法而和方法三在执行上有什么区别呢?
我们在布局文件中加入一个TextView来记录这个控件的宽高.
[java] 
<ScrollView 
    android:layout_width="wrap_content" 
    android:layout_height="wrap_content" > 
 
    <TextView 
        android:id="@+id/text" 
        android:layout_width="wrap_content" 
        android:layout_height="wrap_content" /> 
</ScrollView> 

先来测试方法而:
[java] 
@Override 
public void onCreate(Bundle savedInstanceState) { 
    super.onCreate(savedInstanceState); 
    setContentView(R.layout.main); 
    final ImageView imageView = (ImageView) findViewById(R.id.imageview); 
-----------------------------------------------方法二 
    ViewTreeObserver vto = imageView.getViewTreeObserver(); 
    vto.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 
        public boolean onPreDraw() { 
            int height = imageView.getMeasuredHeight(); 
            int width = imageView.getMeasuredWidth(); 
            textView.append("\n"+height+","+width); 
            return true; 
        } 
    }); 

结果如下:


我们再来测试方法三
[java] 
@Override 
public void onCreate(Bundle savedInstanceState) { 
    super.onCreate(savedInstanceState); 
    setContentView(R.layout.main); 
    final ImageView imageView = (ImageView) findViewById(R.id.imageview); 
    //-----------------------------------------------方法三    
    ViewTreeObserver vto2 = imageView.getViewTreeObserver();   
    vto2.addOnGlobalLayoutListener(new OnGlobalLayoutListener() { 
        @Override   
        public void onGlobalLayout() { 
            imageView.getViewTreeObserver().removeGlobalOnLayoutListener(this);   
            textView.append("\n\n"+imageView.getHeight()+","+imageView.getWidth()); 
        }   
    });   

输出结果如下:

我想这方法二和方法三之间的区别就不用说了吧.  总结:那么需要获取控件的宽高该用那个方法呢?方法一: 比其他的两个方法多了一次计算,也就是多调用了一次onMeasure()方法,该方法虽然看上去简单,但是如果要目标控件计算耗时比较大的话,不见时使用,如listView等.方法二,它的回调方法会调用很多次,并且滑动TextView的时候任然会调用,所以不建议使用.方法三,比较合适.当然,实际应用的时候需要根据实际情况而定.