50个Android开发技巧(03 自己定义ViewGroup)

问题:怎样创建一个例如以下图所看到的的布局?

图1

(原文地址:http://blog.csdn.net/vector_yi/article/details/24415537)

你可能会说,利用RelativeLayout和margins就能够实现。的确,例如以下XML代码能够简单地构建一个类似的布局:

<RelativeLayout xmlns:android = "http://schemas.android.com/apk/res/android"
    android:layout_width= "fill_parent"
    android:layout_height= "fill_parent" >

    <View
        android:layout_width ="100dp"
        android:layout_height ="150dp"
        android:background ="#FF0000" />

    <View
        android:layout_width ="100dp"
        android:layout_height ="150dp"
        android:layout_marginLeft ="30dp"
        android:layout_marginTop ="20dp"
        android:background ="#00FF00" />

    <View
        android:layout_width ="100dp"
        android:layout_height ="150dp"
        android:layout_marginLeft ="60dp"
        android:layout_marginTop ="40dp"
        android:background ="#0000FF" />

</RelativeLayout>

效果如图2:

图2

可是当遇到复杂、要求可变的类似布局时,利用margins可能就会显得操作非常繁杂。

在此,我们来看还有一种创建类似上图布局的方式---自己定义ViewGroup

优点有下面几点:

  • 当你将这个布局应用到不同Activity中时更加easy维护
  • 能够利用自己定义属性来自己定义ViewGroup中的每一个子View
  • 更加简洁可读的XML文件内容
  • 假设须要改变margin的时候。不须要手动的去计算每一个子View的margin

一、理解Android绘制一个View的步骤

关于绘制View的步骤。能够參见Android官方文档:http://developer.android.com/guide/topics/ui/how-android-draws.html

在此,我们重点来关注ViewGroup的绘制过程:

1.处理ViewGroup的width和height.

处理width及height的操作在onMeasure()方法中进行,在此方法内。ViewGroup会依据它的子View来计算自身所占用的布局空间。

2.布局到页面上

这点操作在onLayout()方法中进行,在此方法中,ViewGroup会依据从onMeasure()中得到的信息将其每个子View绘制出来。

二、构建CascadeLayout类

首先在XML布局文件里加入CascadeLayout:

<FrameLayout
    <!--自己定义命名空间,以便在下文中使用自己定义的属性-->
    xmlns:cascade ="http://schemas.android.com/apk/res/com.manning.androidhacks.hack003"
    xmlns:android= "http://schemas.android.com/apk/res/android"
    android:layout_width= "fill_parent"
    android:layout_height= "fill_parent" >

    <com.manning.androidhacks.hack003.view.CascadeLayout
        android:layout_width ="fill_parent"
        android:layout_height ="fill_parent"
        cascade:horizontal_spacing ="30dp"<!--由于前面加入了cascade命名空间,所以此处能够使用自己定义属性-->
        cascade:vertical_spacing ="20dp" >

        <View
            android:layout_width ="100dp"
            android:layout_height ="150dp"
            cascade:layout_vertical_spacing ="90dp"<!--为子View加入的自己定义属性,将在本文第三部分用到-->
            android:background ="#FF0000" />

        <View
            android:layout_width ="100dp"
            android:layout_height ="150dp"
            android:background ="#00FF00" />

        <View
            android:layout_width ="100dp"
            android:layout_height ="150dp"
            android:background ="#0000FF" />
    </com.manning.androidhacks.hack003.view.CascadeLayout>

</FrameLayout>

要使用这些自己定义的属性,我们必需要定义它。

在res/values目录下创建一个attrs.xml文件:

<? xml version ="1.0" encoding= "utf-8" ?>
<resources>
    <declare-styleable name= "CascadeLayout" >
        <attr name= "horizontal_spacing" format = "dimension" />
        <attr name= "vertical_spacing" format = "dimension" />
    </declare-styleable>
</resources>

然后。当我们在创建CascadeLayout且没有为其指定horizontal_spacing与vertical_spacing时,须要有一个默认值。

我们将这个默认值预先定义好并存放在res/values目录下的dimens.xml中:

<?

xml version ="1.0" encoding= "utf-8" ?

>
<resources>
    <dimen name= "cascade_horizontal_spacing" >10dp</dimen>
    <dimen name= "cascade_vertical_spacing" >10dp</dimen>
</resources>

最后,我们须要创建一个名为CascadeLayout的Java类。它继承了ViewGroup并重写了onMeasure()与OnLayout()方法。

1.CascadeLayout的构造函数

public CascadeLayout (Context context, AttributeSet attrs) {
    super( context, attrs);

    TypedArray a = context .obtainStyledAttributes (attrs ,
        R. styleable. CascadeLayout );

    try {
      mHorizontalSpacing = a. getDimensionPixelSize(
          R. styleable. CascadeLayout_horizontal_spacing ,
          getResources ().getDimensionPixelSize (
              R. dimen. cascade_horizontal_spacing ));

      mVerticalSpacing = a. getDimensionPixelSize(
          R. styleable. CascadeLayout_vertical_spacing , getResources ()
              .getDimensionPixelSize (R .dimen .cascade_vertical_spacing ));
    } finally {
      a .recycle ();
    }

2.构建自己定义的LayoutParams类

LayoutParams类将作为CascadeLayout的内部类存在,它将存储每一个子View的x。y坐标。定义例如以下:

public static class LayoutParams extends ViewGroup .LayoutParams {
    int x;
    int y;

    public LayoutParams( Context context , AttributeSet attrs) {
      super (context , attrs );
    }

    public LayoutParams( int w , int h ) {
      super (w , h );
    }

  }

3.重写onMeasure()方法

onMeasure()方法将是CascadeLayout类中最关键的部分,这种方法不仅计算整个ViewGroup所占用的布局空间。还将计算出每一个子View所占用的布局空间。

@Override
  protected void onMeasure (int widthMeasureSpec , int heightMeasureSpec ) {
    int width = 0;
    int height = getPaddingTop ();

    final int count = getChildCount ();
    for ( int i = 0; i < count; i++) {
      View child = getChildAt (i );
      measureChild (child , widthMeasureSpec , heightMeasureSpec );
       LayoutParams lp = (LayoutParams ) child .getLayoutParams ();
      width = getPaddingLeft () + mHorizontalSpacing * i;

      lp .x = width;
      lp .y = height;

      width += child .getMeasuredWidth ();
      height += mVerticalSpacing ;
    }

    width += getPaddingRight ();
    height += getChildAt (getChildCount () - 1). getMeasuredHeight ()
        + getPaddingBottom ();

    setMeasuredDimension ( resolveSize( width, widthMeasureSpec ),
        resolveSize( height, heightMeasureSpec ));
  }

4.最后一步,重写onLayout()方法

代码非常easy,就是让每一个子View都调用layout()方法。

@Override
  protected void onLayout (boolean changed, int l , int t , int r , int b ) {

    final int count = getChildCount ();
    for ( int i = 0; i < count; i++) {
      View child = getChildAt (i );
      LayoutParams lp = ( LayoutParams ) child .getLayoutParams ();

      child .layout (lp .x , lp .y , lp .x + child. getMeasuredWidth (), lp .y
          + child .getMeasuredHeight ());
    }
  }

至此,就利用自己定义的ViewGroup创建了一个和图2一样效果的布局页面。

三、为子View加入自己定义属性

既然费了这么大劲,怎么可能就和之前几行XML代码效果一样?

以下,我们就来为CascadeLayout中的子View加入自己定义属性:

首先,在之前创建的attrs.xml中加入例如以下代码:

<declare-styleable name="CascadeLayout_LayoutParams">
     <attr name="layout_vertical_spacing" format="dimension" />
</declare-styleable>

由于这个新加入的属性是以 layout_ 开头的。所以它会被加入到LayoutParams中去。

我们能够在之前自己定义的内部类LayoutParams中的构造函数中读取到这个属性,将第一个构造函数改为:

public LayoutParams (Context context, AttributeSet attrs) {
      super (context , attrs );

      TypedArray a = context .obtainStyledAttributes (attrs ,
          R. styleable. CascadeLayout_LayoutParams );
      try {
        verticalSpacing = a
            .getDimensionPixelSize (
                R .styleable .CascadeLayout_LayoutParams_layout_vertical_spacing ,
                -1 );
      } finally {
        a .recycle ();
      }
    }

既然加入了新的自己定义属性。就必须在onMeasure()方法中对其加以处理:

@Override
  protected void onMeasure (int widthMeasureSpec , int heightMeasureSpec ) {
    int width = getPaddingLeft ();
    int height = getPaddingTop ();
    int verticalSpacing ;

    final int count = getChildCount ();
    for ( int i = 0; i < count; i++) {
      verticalSpacing = mVerticalSpacing ;

      View child = getChildAt (i );
      measureChild (child , widthMeasureSpec , heightMeasureSpec );

      LayoutParams lp = ( LayoutParams ) child .getLayoutParams ();
      width = getPaddingLeft () + mHorizontalSpacing * i;

      lp .x = width;
      lp .y = height;

      if (lp .verticalSpacing >= 0 ) {
        verticalSpacing = lp .verticalSpacing ;
      }

      width += child .getMeasuredWidth ();
      height += verticalSpacing ;
    }

    width += getPaddingRight ();
    height += getChildAt (getChildCount () - 1). getMeasuredHeight ()
        + getPaddingBottom ();

    setMeasuredDimension ( resolveSize( width, widthMeasureSpec ),
        resolveSize( height, heightMeasureSpec ));
  }

最后附上完整的CascadeLayout代码:

package com.manning.androidhacks.hack003.view;

import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;

import com.manning.androidhacks.hack003.R;

public class CascadeLayout extends ViewGroup {

  private int mHorizontalSpacing;
  private int mVerticalSpacing;

  public CascadeLayout(Context context, AttributeSet attrs) {
    super(context, attrs);

    TypedArray a = context.obtainStyledAttributes(attrs,
        R.styleable.CascadeLayout);

    try {
      mHorizontalSpacing = a.getDimensionPixelSize(
          R.styleable.CascadeLayout_horizontal_spacing,
          getResources().getDimensionPixelSize(
              R.dimen.cascade_horizontal_spacing));

      mVerticalSpacing = a.getDimensionPixelSize(
          R.styleable.CascadeLayout_vertical_spacing, getResources()
              .getDimensionPixelSize(R.dimen.cascade_vertical_spacing));
    } finally {
      a.recycle();
    }

  }

  @Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int width = getPaddingLeft();
    int height = getPaddingTop();
    int verticalSpacing;

    final int count = getChildCount();
    for (int i = 0; i < count; i++) {
      verticalSpacing = mVerticalSpacing;

      View child = getChildAt(i);
      measureChild(child, widthMeasureSpec, heightMeasureSpec);

      LayoutParams lp = (LayoutParams) child.getLayoutParams();
      width = getPaddingLeft() + mHorizontalSpacing * i;

      lp.x = width;
      lp.y = height;

      if (lp.verticalSpacing >= 0) {
        verticalSpacing = lp.verticalSpacing;
      }

      width += child.getMeasuredWidth();
      height += verticalSpacing;
    }

    width += getPaddingRight();
    height += getChildAt(getChildCount() - 1).getMeasuredHeight()
        + getPaddingBottom();

    setMeasuredDimension(resolveSize(width, widthMeasureSpec),
        resolveSize(height, heightMeasureSpec));
  }

  @Override
  protected void onLayout(boolean changed, int l, int t, int r, int b) {

    final int count = getChildCount();
    for (int i = 0; i < count; i++) {
      View child = getChildAt(i);
      LayoutParams lp = (LayoutParams) child.getLayoutParams();

      child.layout(lp.x, lp.y, lp.x + child.getMeasuredWidth(), lp.y
          + child.getMeasuredHeight());
    }
  }

  @Override
  protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
    return p instanceof LayoutParams;
  }

  @Override
  protected LayoutParams generateDefaultLayoutParams() {
    return new LayoutParams(LayoutParams.WRAP_CONTENT,
        LayoutParams.WRAP_CONTENT);
  }

  @Override
  public LayoutParams generateLayoutParams(AttributeSet attrs) {
    return new LayoutParams(getContext(), attrs);
  }

  @Override
  protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
    return new LayoutParams(p.width, p.height);
  }

  public static class LayoutParams extends ViewGroup.LayoutParams {
    int x;
    int y;
    public int verticalSpacing;

    public LayoutParams(Context context, AttributeSet attrs) {
      super(context, attrs);

      TypedArray a = context.obtainStyledAttributes(attrs,
          R.styleable.CascadeLayout_LayoutParams);
      try {
        verticalSpacing = a
            .getDimensionPixelSize(
                R.styleable.CascadeLayout_LayoutParams_layout_vertical_spacing,
                -1);
      } finally {
        a.recycle();
      }
    }

    public LayoutParams(int w, int h) {
      super(w, h);
    }

  }
}

project文件夹结构:

(原文地址:http://blog.csdn.net/vector_yi/article/details/24415537)

时间: 2024-12-20 01:20:21

50个Android开发技巧(03 自己定义ViewGroup)的相关文章

50个Android开发技巧(10 为TextView加入样式)

首先来看一个控件的例子: (原文地址:http://blog.csdn.net/vector_yi/article/details/24428085) 手机上类似这种场景你一定已经见过非常多次了,但有没有考虑过它是如何实现的? 或许你会觉得这是一个略微复杂的自己定义的View,但,没错,这实际上仅仅是一个原生的TextView而已. TextView是一个简单而奇妙的Widget,你能够利用它以不同风格不同格式来展现文字. 举两个简单的样例: 1.在TextView中加入一个超链接 事实上非常e

50个Android开发技巧(24 处理ListView数据为空的情况)

在移动平台上为用户展示数据的一个经常用法是将数据填充进一个List内,而此时须要注意的一点就是: 原文地址:(http://blog.csdn.net/vector_yi/article/details/24936163) 怎样处理须要填充的数据为空的情况? ListView及其它继承自AdapterView的类都有一个简便的处理这样的情况的方法:setEmptyView(View). 当ListView的Adapter为空或者Adapter的isEmpty()方法返回true的时候,它将会把设

50个Android开发技巧(20 使用MVP模式)

??在提供了2个链接,其中一个是原文的链接?? http://yeungeek.com/2014/06/17/mvp-android/ http://blog.csdn.net/vector_yi/article/details/24719873 一.MVP介绍 随着UI创建技术的功能日益增强,UI层也履行着越来越多的职责.为了更好地细分视图(View)与模型(Model)的功能,让View专注于处理数据的可视化以及与用户的交互,同时让Model只关系数据的处理,基于MVC概念的MVP(Mode

50一个Android开发技巧(01 利用好layout_weight属性)

问题:如何将一个Button放置在布局的中间,并设置其宽度parent的50%? 分析:问题想要达到的效果应该是这样: (原文地址:http://blog.csdn.net/vector_yi/article/details/24397733) 这看起来不难,但非常多开发人员并不知道达到这样效果的最佳方法. 解决:在此我们将weightSum属性与layout_weight属性一起利用. <LinearLayout xmlns:android = "http://schemas.andro

50个Android开发技巧(11 为文字加入特效)

问题:怎样构建一个模拟LED数字时钟的页面?效果例如以下图所看到的: (原文地址:http://blog.csdn.net/vector_yi/article/details/24460227) 分析:我们能够利用两个TextView来显示,第一个TextView显示LED屏上默认不发光的88:88:88.还有一个显示实时的时间并加入发光及阴影效果. 可是我们还须要解决显示的字体问题,让它看起来更像是一个真实的LED数字时钟. 解决步骤:(1)自己定义一个LedTextView类,继承自Text

50个Android开发技巧(09 避免用EditText对日期进行验证)

我们都知道,在表单中对数据进行验证不但无聊并且easy出错. (原文地址:http://blog.csdn.net/vector_yi/article/details/24424713) 想象一下,一个表单内有若干个须要日期类型的输入,而你又不想对这些输入信息进行日期类型的验证,你会怎么做? 这里有一种解决的方法是,让用户觉得他们是在一个EditText中操作,但实际上是一个Button,点击这个Button时会弹出一个DatePicker. 为了让这个方法可行,我们须要改变这个Button的背

50个Android开发技巧(12 为控件加入圆角边框)

控件的圆角边框能够使你的App看起来更美观,事实上实现起来也非常easy. (原文地址:http://blog.csdn.net/vector_yi/article/details/24463025) 以创建一个灰色的带圆角边框的Button为例: 一.创建一个ShapeDrawable作为背景 在drawable文件夹下创建一个button_rounded_background.xml文件: <shape xmlns:android = "http://schemas.android.c

Android开发技巧——自定义控件之自定义属性

Android开发技巧--自定义控件之自定义属性 掌握自定义控件是很重要的,因为通过自定义控件,能够:解决UI问题,优化布局性能,简化布局代码. 上一篇讲了如何通过xml把几个控件组织起来,并继承某个ViewGroup子类,把它们封装起来使用.这是我们接触到的最简单的一种自定制控件了.但许多时候,我们还需要在布局文件中使用它们的时候,能通过属性传入一些值,来影响最终的显示结果. 我们在做项目中经常会遇到的一个情况:一张图片加一个文本的组合.比如充值账户成功之后显示的一个界面,上面是一个表示成功的

Android开发技巧——大图裁剪

本篇内容是接上篇<Android开发技巧--定制仿微信图片裁剪控件> 的,先简单介绍对上篇所封装的裁剪控件的使用,再详细说明如何使用它进行大图裁剪,包括对旋转图片的裁剪. 裁剪控件的简单使用 XML代码 使用如普通控件一样,首先在布局文件里包含该控件: <com.githang.clipimage.ClipImageView xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+i