search

Home  >  Q&A  >  body text

php - Android新人求教问:如何自定义ViewGroup,望大神潜入、不吝赐教。

3张扑克牌叠在一起显示效果如下:
这个布局效果可以用该RelativeLayout或FrameLayout,然后为每一个扑克牌设置margin就能实现,不过我觉得这种方式有点low,谁可以告知高级一点的实现方式啊,求告知~

ringa_leeringa_lee2897 days ago558

reply all(2)I'll reply

  • 阿神

    阿神2017-04-10 15:00:47

    除了你说的那种,我们还可以用ViewGroup实现。不过在定制ViewGroup之前,我们需要先理解一些定义。
    Android绘制视图的方式。“绘制布局由两个遍历过程组成:测量过程和布局过程。测量过程由measure(int, int)方法完成,该方法从上到下遍历视图树。在递归遍历过程中,每个视图都会向下层传递尺寸和规格。当measure方法遍历结束,每个视图都保存了各自的尺寸信息。第二个过程由 layout(int,int,int,int)方法完成,该方法也是由上而下遍历视图树,在遍历过程中,每个父视图通过测量过程的结果定位所有子视图的位置信息。”
    简而言之,第一步是测量ViewGroup的宽度和高度,在onMeasure()方法中完成,ViewGroup遍历所有子视图计算出它的大小。第二步是根据第一步获取的尺寸去布局所有子视图,在onLayout()中完成。

    创建CascadeLayout
    终于到了定制ViewGroup的阶段了。假设我们已经定制了一个CascadeLayout的容器,我们会这样使用它。
    1.<FrameLayout xmlns:cascade="http://schemas.android.com/apk/res/com.manoel.custom"  
    2.      
    3.    xmlns:android="http://schemas.android.com/apk/res/android"  
    4.    android:layout_width="fill_parent"  
    5.    android:layout_height="fill_parent" >  
    6.  
    7.    <com.manoel.view.CascadeLayout  
    8.        android:layout_width="fill_parent"  
    9.        android:layout_height="fill_parent"  
    10.          
    11.        cascade:horizontal_spacing="30dp"  
    12.        cascade:vertical_spacing="20dp" >  
    13.  
    14.        <View  
    15.            android:layout_width="100dp"  
    16.            android:layout_height="150dp"  
    17.            android:background="#FF0000" />  
    18.  
    19.        <View  
    20.            android:layout_width="100dp"  
    21.            android:layout_height="150dp"  
    22.            android:background="#00FF00" />  
    23.  
    24.        <View  
    25.            android:layout_width="100dp"  
    26.            android:layout_height="150dp"  
    27.            android:background="#0000FF" />  
    28.    </com.manoel.view.CascadeLayout>  
    29.  
    30.  

    首先,定义属性。在values文件夹下面创建attrs.xml,代码如下:
    1.  
    2.    <declare-styleable name="CascadeLayout">  
    3.        <attr name="horizontal_spacing" format="dimension" />  
    4.        <attr name="vertical_spacing" format="dimension" />  
    5.      
    6.
      
    同时,为了严谨一些,定义一些默认的垂直距离和水平距离,以防在布局中没有提供这些属性。
    在dimens.xml中添加如下代码:
    1.  
    2.    <dimen name="cascade_horizontal_spacing">10dp  
    3.    <dimen name="cascade_vertical_spacing">10dp  
    4.
      
    准备工作已经做好了,接下来看一下CascadeLayout的源码,略微有点长,后面帮助大家分析一下。

    1.public class CascadeLayout extends ViewGroup {  
    2.  
    3.  private int mHorizontalSpacing;  
    4.  private int mVerticalSpacing;  
    5.  
    6.  public CascadeLayout(Context context, AttributeSet attrs) {  
    7.    super(context, attrs);  
    8.  
    9.    TypedArray a = context.obtainStyledAttributes(attrs,  
    10.        R.styleable.CascadeLayout);  
    11.  
    12.    try {  
    13.      mHorizontalSpacing = a.getDimensionPixelSize(  
    14.          R.styleable.CascadeLayout_horizontal_spacing,  
    15.          getResources().getDimensionPixelSize(  
    16.              R.dimen.cascade_horizontal_spacing));  
    17.  
    18.      mVerticalSpacing = a.getDimensionPixelSize(  
    19.          R.styleable.CascadeLayout_vertical_spacing, getResources()  
    20.              .getDimensionPixelSize(R.dimen.cascade_vertical_spacing));  
    21.    } finally {  
    22.      a.recycle();  
    23.    }  
    24.  
    25.  }  
    26.  
    27.  @Override  
    28.  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
    29.    int width = getPaddingLeft();  
    30.    int height = getPaddingTop();  
    31.    int verticalSpacing;  
    32.  
    33.    final int count = getChildCount();  
    34.    for (int i = 0; i < count; i++) {  
    35.      verticalSpacing = mVerticalSpacing;  
    36.  
    37.      View child = getChildAt(i);  
    38.      measureChild(child, widthMeasureSpec, heightMeasureSpec);  
    39.  
    40.      LayoutParams lp = (LayoutParams) child.getLayoutParams();  
    41.      width = getPaddingLeft() + mHorizontalSpacing * i;  
    42.  
    43.      lp.x = width;  
    44.      lp.y = height;  
    45.  
    46.      if (lp.verticalSpacing >= 0) {  
    47.        verticalSpacing = lp.verticalSpacing;  
    48.      }  
    49.  
    50.      width += child.getMeasuredWidth();  
    51.      height += verticalSpacing;  
    52.    }  
    53.  
    54.    width += getPaddingRight();  
    55.    height += getChildAt(getChildCount() - 1).getMeasuredHeight()  
    56.        + getPaddingBottom();  
    57.  
    58.    setMeasuredDimension(resolveSize(width, widthMeasureSpec),  
    59.        resolveSize(height, heightMeasureSpec));  
    60.  }  
    61.  
    62.  @Override  
    63.  protected void onLayout(boolean changed, int l, int t, int r, int b) {  
    64.  
    65.    final int count = getChildCount();  
    66.    for (int i = 0; i < count; i++) {  
    67.      View child = getChildAt(i);  
    68.      LayoutParams lp = (LayoutParams) child.getLayoutParams();  
    69.  
    70.      child.layout(lp.x, lp.y, lp.x + child.getMeasuredWidth(), lp.y  
    71.          + child.getMeasuredHeight());  
    72.    }  
    73.  }  
    74.  
    75.  @Override  
    76.  protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {  
    77.    return p instanceof LayoutParams;  
    78.  }  
    79.  
    80.  @Override  
    81.  protected LayoutParams generateDefaultLayoutParams() {  
    82.    return new LayoutParams(LayoutParams.WRAP_CONTENT,  
    83.        LayoutParams.WRAP_CONTENT);  
    84.  }  
    85.  
    86.  @Override  
    87.  public LayoutParams generateLayoutParams(AttributeSet attrs) {  
    88.    return new LayoutParams(getContext(), attrs);  
    89.  }  
    90.  
    91.  @Override  
    92.  protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {  
    93.    return new LayoutParams(p.width, p.height);  
    94.  }  
    95.  
    96.  public static class LayoutParams extends ViewGroup.LayoutParams {  
    97.    int x;  
    98.    int y;  
    99.    public int verticalSpacing;  
    100.  
    101.    public LayoutParams(Context context, AttributeSet attrs) {  
    102.      super(context, attrs);  
    103.    }  
    104.  
    105.    public LayoutParams(int w, int h) {  
    106.      super(w, h);  
    107.    }  
    108.  
    109.  }  
    110.}  

    首先,分析构造函数。

    1.public CascadeLayout(Context context, AttributeSet attrs) {  
    2.    super(context, attrs);  
    3.  
    4.    TypedArray a = context.obtainStyledAttributes(attrs,  
    5.        R.styleable.CascadeLayout);  
    6.  
    7.    try {  
    8.      mHorizontalSpacing = a.getDimensionPixelSize(  
    9.          R.styleable.CascadeLayout_horizontal_spacing,  
    10.          getResources().getDimensionPixelSize(  
    11.              R.dimen.cascade_horizontal_spacing));  
    12.  
    13.      mVerticalSpacing = a.getDimensionPixelSize(  
    14.          R.styleable.CascadeLayout_vertical_spacing, getResources()  
    15.              .getDimensionPixelSize(R.dimen.cascade_vertical_spacing));  
    16.    } finally {  
    17.      a.recycle();  
    18.    }  
    19.  
    20.  }  
    如果在布局中使用CasecadeLayout,系统就会调用这个构造函数,这个大家都应该知道的吧。这里不解释why,有兴趣的可以去看源码,重点看系统是如何解析xml布局的。

    构造函数很简单,就是通过布局文件中的属性,获取水平距离和垂直距离。

    然后,分析自定义LayoutParams。
    这个类的用途就是保存每个子视图的x,y轴位置。这里把它定义为静态内部类。ps:提到静态内部类,我又想起来关于多线程内存泄露的问题了,如果有时间再给大家解释一下多线程造成内存泄露的问题。

    1.public static class LayoutParams extends ViewGroup.LayoutParams {  
    2.    int x;  
    3.    int y;  
    4.    public int verticalSpacing;  
    5.  
    6.    public LayoutParams(Context context, AttributeSet attrs) {  
    7.      super(context, attrs);  
    8.    }  
    9.  
    10.    public LayoutParams(int w, int h) {  
    11.      super(w, h);  
    12.    }  
    13.  
    14.  }  
    除此之外,还需要重写一些方法,checkLayoutParams()、generateDefaultLayoutParams()等,这个方法在不同ViewGroup之间往往是相同的。

    接下来,分析onMeasure()方法。
    1.@Override  
    2.protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
    3.  int width = getPaddingLeft();  
    4.  int height = getPaddingTop();  
    5.  int verticalSpacing;  
    6.  
    7.  final int count = getChildCount();  
    8.  for (int i = 0; i < count; i++) {  
    9.    verticalSpacing = mVerticalSpacing;  
    10.  
    11.    View child = getChildAt(i);  
    12.    measureChild(child, widthMeasureSpec, heightMeasureSpec); // 令每个子视图测量自身  
    13.  
    14.    LayoutParams lp = (LayoutParams) child.getLayoutParams();  
    15.    width = getPaddingLeft() + mHorizontalSpacing * i;  
    16.    // 保存每个子视图的x,y轴坐标  
    17.    lp.x = width;  
    18.    lp.y = height;  
    19.  
    20.    if (lp.verticalSpacing >= 0) {  
    21.      verticalSpacing = lp.verticalSpacing;  
    22.    }  
    23.  
    24.    width += child.getMeasuredWidth();  
    25.    height += verticalSpacing;  
    26.  }  
    27.  
    28.  width += getPaddingRight();  
    29.  height += getChildAt(getChildCount() - 1).getMeasuredHeight()  
    30.      + getPaddingBottom();  
    31.  // 使用计算所得的宽和高设置整个布局的测量尺寸  
    32.  setMeasuredDimension(resolveSize(width, widthMeasureSpec),  
    33.      resolveSize(height, heightMeasureSpec));  
    34.}  
    最后,分析onLayout()方法。
    1.@Override  
    2.protected void onLayout(boolean changed, int l, int t, int r, int b) {  
    3.  
    4.  final int count = getChildCount();  
    5.  for (int i = 0; i < count; i++) {  
    6.    View child = getChildAt(i);  
    7.    LayoutParams lp = (LayoutParams) child.getLayoutParams();  
    8.  
    9.    child.layout(lp.x, lp.y, lp.x + child.getMeasuredWidth(), lp.y  
    10.        + child.getMeasuredHeight());  
    11.  }  
    12.}  
    逻辑很简单,用onMeasure()方法计算出的值为参数循环调用子View的layout()方法。

    为子视图添加自定义属性
    作为示例,下面将添加子视图重写垂直间距的方法。
    第一步是向attrs.xml中添加一个新的属性。

    1.<declare-styleable name="CascadeLayout_LayoutParams">  
    2.    <attr name="layout_vertical_spacing" format="dimension" />  
    3.  
    这里的属性名是layout_vertical_spacing,因为该属性名前缀是layout_,同时,又不是View固有的属性,所以该属性会被添加到LayoutParams的属性表中。在CascadeLayout类的构造函数中读取这个新属性。
    1.public static class LayoutParams extends ViewGroup.LayoutParams {  
    2.    int x;  
    3.    int y;  
    4.    public int verticalSpacing;  
    5.  
    6.    public LayoutParams(Context context, AttributeSet attrs) {  
    7.      super(context, attrs);  
    8.  
    9.      TypedArray a = context.obtainStyledAttributes(attrs,  
    10.          R.styleable.CascadeLayout_LayoutParams);  
    11.      try {  
    12.        verticalSpacing = a  
    13.            .getDimensionPixelSize(  
    14.                R.styleable.CascadeLayout_LayoutParams_layout_vertical_spacing,  
    15.                -1);  
    16.      } finally {  
    17.        a.recycle();  
    18.      }  
    19.    }  
    20.  
    21.    public LayoutParams(int w, int h) {  
    22.      super(w, h);  
    23.    }  
    24.  
    25.  }  

    那怎么使用这个属性呢?so easy!

    1.<com.manoel.view.CascadeLayout  
    2.    android:layout_width="fill_parent"  
    3.    android:layout_height="fill_parent"  
    4.    cascade:horizontal_spacing="30dp"  
    5.    cascade:vertical_spacing="20dp" >  
    6.  
    7.      
    8.    <View  
    9.        android:layout_width="100dp"  
    10.        android:layout_height="150dp"  
    11.        cascade:layout_vertical_spacing="90dp"  
    12.        android:background="#FF0000" />  
    13.  
    14.    <View  
    15.        android:layout_width="100dp"  
    16.        android:layout_height="150dp"  
    17.        android:background="#00FF00" />  
    18.  
    19.    <View  
    20.        android:layout_width="100dp"  
    21.        android:layout_height="150dp"  
    22.        android:background="#0000FF" />  
    23.</com.manoel.view.CascadeLayout>  

    reply
    0
  • 巴扎黑

    巴扎黑2017-04-10 15:00:47

    其实你只需要搜索“创建定制的ViewGroup”就能找到正确的答案了。
    right answer

    reply
    0
  • Cancelreply