Android五大布局及自定义布局

本文详细介绍了Android的五大布局——LinearLayout、FrameLayout、RelativeLayout、AbsoluteLayout和TableLayout,以及如何创建自定义布局。自定义布局通过减少view使用、提升UI效率并实现复杂UI设计。文章讲解了自定义布局的十个步骤,从创建View类到保存状态,涵盖绘图、交互、测量、属性应用和状态管理等方面。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

概念

Android的界面是有布局和组件协同完成的,布局好比是建筑里的框架,而组件则相当于建筑里的砖瓦。组件按照布局的要求依次排列,就组成了用户所看见的界面。Android的五大布局分别是LinearLayout(线性布局)、FrameLayout(单帧布局)、RelativeLayout(相对布局)、AbsoluteLayout(绝对布局)和TableLayout(表格布局)。

LinearLayout

LinearLayout按照垂直或者水平的顺序依次排列子元素,可以通过属性orientation设定垂直vertical还是水平horizontal,每一个子元素都位于前一个元素之后。如果是垂直排列,那么将是一个N行单列的结构,每一行只会有一个元素,而不论这个元素的宽度为多少;如果是水平排列,那么将是一个单行N列的结构。如果搭建两行两列的结构,通常的方式是先垂直排列两个元素,每一个元素里再包含一个LinearLayout进行水平排列。
这里写图片描述 这里写图片描述
LinearLayout有个重要属性layout_weight(权重),它用于描述该子元素在剩余空间中占有的大小比例。例如我们可以对于上图水平排序的三个按钮分别添加
android:layout_weight=“1”,它表示,三个按钮的各占用父元素(也就是这一整行)一份权重,也就是各三分之一。当只有一个按钮添加了权重时,它表示第二第三个按钮不参与权重分隔,它们的大小是包裹内容的大小,父元素剩下的部分被第一个按钮全部占用(因为只有它)。
这里写图片描述 这里写图片描述

<Button
    android:id="@+id/btn1"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="第一个按钮"
    android:layout_weight="1"
    />
<Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:id="@+id/btn2"
    android:text="第二个按钮"
    />
<Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:id="@+id/btn3"
    android:text="第三个按钮"
    />

FrameLayout

FrameLayout是五大布局中最简单的一个布局,在这个布局中,整个界面被当成一个九宫格。如果控件放在同一个九宫格里面,那么后一个就会将前一个覆盖。
FrameLayout布局控件的位置通过属性android:layout_gravity=”“确定,显然它有九个值:left|top、top、right|top、left|center、center、right|center、left|bottom、bottom、right|bottom。
这里写图片描述 这里写图片描述

<Button
    android:layout_width="180dp"
    android:layout_height="140dp"
    android:text="第一个按钮"
    android:background="#ff0000"
    android:id="@+id/button1"
    android:layout_gravity="left|top" />

<Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="第二个按钮"
    android:background="#00ff00"
    android:id="@+id/button2"
    android:layout_gravity="left|top" />

RelativeLayout

顾名思义,相对布局RelativeLayout按照各子元素之间的相对位置关系完成布局。在此布局中的子元素里与位置相关的属性将生效。例如android:layout_below, android:layout_above等。子元素就通过这些属性和各自的ID配合指定位置关系。注意在指定位置关系时,引用的ID必须在引用之前,先被定义,否则将出现异常。
RelativeLayout里常用的位置属性如下:
android:layout_toLeftOf —— 该组件位于引用组件的左方
android:layout_toRightOf —— 该组件位于引用组件的右方
android:layout_above —— 该组件位于引用组件的上方
android:layout_below —— 该组件位于引用组件的下方
android:layout_alignParentLeft —— 该组件是否对齐父组件的左端
android:layout_alignParentRight —— 该组件是否对齐其父组件的右端
android:layout_alignParentTop —— 该组件是否对齐父组件的顶部
android:layout_alignParentBottom —— 该组件是否对齐父组件的底部
android:layout_centerInParent —— 该组件是否相对于父组件居中
android:layout_centerHorizontal —— 该组件是否横向居中
android:layout_centerVertical —— 该组件是否垂直居中
这里写图片描述

<Button
   android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="第一个按钮"
    android:id="@+id/button2"
    android:layout_alignParentTop="true"
    android:layout_alignParentLeft="true"
    android:layout_alignParentStart="true" />

<Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="第二个按钮"
    android:id="@+id/button3"
    android:layout_below="@+id/button2"
    android:layout_toRightOf="@+id/button2"
    android:layout_toEndOf="@+id/button2" />

<Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="第三个按钮"
    android:id="@+id/button5"
    android:layout_alignParentBottom="true"
    android:layout_centerHorizontal="true" />

AbsoluteLayout

AbsoluteLayout布局时,整个平面任何位置都可以随意放置控件,在此布局中的子元素的android:layout_x和android:layout_y属性将生效,用于描述该子元素的坐标位置。屏幕左上角为坐标原点(0,0),第一个0代表横坐标,向右移动此值增大,第二个0代表纵坐标,向下移动,此值增大。在此布局中的子元素可以相互重叠。在实际开发中,通常不采用此布局格式,因为它的界面代码过于刚性,以至于有可能不能很好的适配各种终端。
这里写图片描述

<Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="第一个按钮"
    android:id="@+id/button2"
    android:layout_x="59dp"
    android:layout_y="63dp" />

<Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="第二个按钮"
    android:id="@+id/button3"
    android:layout_x="157dp"
    android:layout_y="303dp" />

TableLayout

顾名思义,TableLayout为表格布局,适用于N行N列的布局格式。一个TableLayout由许多TableRow组成,一个TableRow就代表TableLayout中的一行。
  TableRow是LinearLayout的子类,它的android:orientation属性值恒为horizontal,并且它的android:layout_width和android:layout_height属性值恒为MATCH_PARENT和WRAP_CONTENT。所以它的子元素都是横向排列,并且宽高一致的。这样的设计使得每个TableRow里的子元素都相当于表格中的单元格一样。在TableRow中,单元格可以为空,但是不能跨列。
这里写图片描述

 <TableRow
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:text="第一个按钮"
        android:id="@+id/button2"
        android:layout_column="0" />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="第二个按钮"
        android:id="@+id/button3"
        android:layout_column="1" />
</TableRow>

<TableRow
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:text="第三个按钮"
        android:id="@+id/button4"
        android:layout_column="1" />
</TableRow>

自定义布局

在实际开发当中,我们肯定见到过各式各样复杂的布局,如植物大战僵尸、愤怒的小鸟等。显然,这种精致的布局是不可能通过上述五大布局来实现的,这就需要我们来自定义布局。
我们主要是通过以下五个方面创建一个自定义View
1,绘图,通过重写onDraw方法控制View在屏幕上的渲染效果
2,交互,通过重写onTouchEvent方法或者使用手势来控制用户的交互
3,测量,通过重写onMeasure方法来对控件进行测量
4,属性,可以通过xml自定义控件的属性,然后通过TypedArray来进行使用
5,状态的保存,为了避免配置改变时丢失View状态,通过重写onSaveInstanceState,onRestoreInstanceState方法来保存和恢复状态

总结起来,自定义布局有两大优点:

1.通过减少view的使用和更快地遍历布局元素让你的UI显示更加有效率;
2.可以构建那些无法由已有的view实现的UI。

自定义布局的流程

下面我们以一个简单的绘图实例来演示自定义布局的流程

一、自定义View类继承View

为了创建点击可切换的形状的自定义View,我们继承View,编写构造方法。实现三个构造方法,最终调用三个参数的构造方法。

public class CustomView extends View {

   public CustomView(Context context) {
       this(context, null);
   }

   public CustomView(Context context, AttributeSet attrs) {
       this(context, attrs, 0);
   }

   public CustomView(Context context, AttributeSet attrs, int defStyle) {
       super(context, attrs, defStyle);
   }
}
二、将自定义view加入到layout中

注意,自定义view的名称必须包含全路径,即所有的包名。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.example.administrator.myapplication.CustomView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/customview"
        android:layout_centerInParent="true"
        />

</RelativeLayout>
三、给自定义View添加自定义属性

一个良好的自定义控件应该是能通过xml进行控制的,所以我们需要考虑一下我们的自定义View的哪些属性需要被提取到xml中,比如,我们应该可以让用户选择图形的颜色,是否显示图形的名称等。我们可以通过下面的代码在xml中进行配置。

<com.example.administrator.myapplication.CustomView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/customview"
        android:layout_centerInParent="true"
        xmlns:app="http://schemas.android.com/apk/res/
                  com.example.administrator.myapplication"
        app:displayShapeName="true"
        app:shapeColor="#7f0000"
        />

为了能够使用图形的颜色和图形显示的名字的属性,我们应该新建res/values/attrs.xml文件,在里面定义这些属性。

<?xml version="1.0" encoding="utf-8"?>
<resources>
   <declare-styleable name="CustomView">
       <attr name="shapeColor" format="color" />
       <attr name="displayShapeName" format="boolean" />
   </declare-styleable>
</resources>

注意上述代码,我们为每一个attr节点都写了name属性和format属性,format是属性的数据结构,合法的值包括string, color, dimension, boolean, integer, float, enum等

四、应用自定义属性

现在我们已经通过xml设定了自定义属性shapeColor和displayShapeName,我们需要在构造方法中提取到这些属性。为了提取属性,我们使用TypedArray类和obtainStyledAttributes方法。

public class CustomView extends View {
    private int shapeColor;
    private boolean displayShapeName;

    public CustomView(Context context) {
        this(context, null);
    }

    public CustomView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CustomView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        setupAttributes(attrs);
    }

    private void setupAttributes(AttributeSet attrs) {
        // 提取自定义属性到TypedArray对象中
        TypedArray a = getContext().getTheme().obtainStyledAttributes(attrs,
                R.styleable.CustomView, 0, 0);
        // 将属性赋值给成员变量
        try {
            shapeColor = a.getColor(R.styleable.CustomView_shapeColor,
                    Color.BLACK);
            displayShapeName = a.getBoolean(
                    R.styleable.CustomView_displayShapeName, false);
        } finally {
            // TypedArray对象是共享的必须被重复利用。
            a.recycle();
        }
    }
}
五、增加属性的getter和setter方法
public boolean isDisplayingShapeName() {
    return displayShapeName;
  }

  public void setDisplayingShapeName(boolean state) {
    this.displayShapeName = state;
    invalidate();//重绘
    requestLayout();
  }

  public int getShapeColor() {
    return shapeColor;
  }

  public void setShapeColor(int color) {
    this.shapeColor = color;
    invalidate();
    requestLayout();
  }

注意以上代码,当View的属性发生改变时我们需要进行重绘和重新布局,为了保证正常进行,请确保调用了invalidate和requestLayout方法。

六、绘制图形

接下来,让我们开始真正使用自定义属性(颜色,是否显示图形名)进行图形的绘制。所有的View的绘制发生在onDraw方法里,我们使用其参数Canvas将图形绘制到View上,现在我们绘制一个正方形。

public class CustomView extends View {

    private int shapeWidth = 100;
    private int shapeHeight = 100;
    private int textXOffset = 0;
    private int textYOffset = 30;
    private Paint paintShape;


    private int currentShapeIndex = 0;

    public CustomView(Context context) {
        this(context, null);
    }

    public CustomView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CustomView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        setupAttributes(attrs);
        setupPaint();
    }
    private void setupPaint() {
        paintShape = new Paint();
        paintShape.setStyle(Style.FILL);
        paintShape.setColor(shapeColor);
        paintShape.setTextSize(30);
    }
}

以上代码会绘制我们定义的颜色的图形,如果显示图形名,其图形名也会被显示,效果图就跟上面的gif图片里的正方形一样。

七、计算尺寸

为了按照用户定义的宽度高度进行绘制,我们需要重写onMeasure方法进行View的测量,该方法决定了View的宽度和高度。我们定义的View的宽度和高度由我们的形状和形状名字共同决定。我们先看下onMeasure的代码。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 简单定义文本边距
        int textPadding = 10;
        int contentWidth = shapeWidth;
        // 使用测量模式获得宽度
        int minw = contentWidth + getPaddingLeft() + getPaddingRight();
        int w = resolveSizeAndState(minw, widthMeasureSpec, 0);
        // 同宽度
        int minh = shapeHeight + getPaddingBottom() + getPaddingTop();
        //如果现实图形名,则加上文字高度
        if (displayShapeName) {
            minh += textYOffset + textPadding;
        }
        int h = resolveSizeAndState(minh, heightMeasureSpec, 0);
        // 测量完成后必须调用setMeasuredDimension方法
        // 之后可以通过getMeasuredWidth 和 getMeasuredHeight 方法取出高度和宽度
        setMeasuredDimension(w, h);
    }

注意以上计算要将View的内边距计算进去然后再计算整个宽度高度,并且最后必须调用setMeasuredDimension方法设置宽度和高度,resolveSizeAndState() 方法将返回一个合适的尺寸,只要将测量模式和我们计算的宽度高度传进去即可。该方法在API11开始出现,低于该版本将无法使用该方法,这里我抽取android的源码供参考。

/**
    * Utility to reconcile a desired size and state, with constraints imposed
    * by a MeasureSpec.  Will take the desired size, unless a different size
    * is imposed by the constraints.  The returned value is a compound integer,
    * with the resolved size in the {@link #MEASURED_SIZE_MASK} bits and
    * optionally the bit {@link #MEASURED_STATE_TOO_SMALL} set if the resulting
    * size is smaller than the size the view wants to be.
    *
    * @param size How big the view wants to be
    * @param measureSpec Constraints imposed by the parent
    * @return Size information bit mask as defined by
    * {@link #MEASURED_SIZE_MASK} and {@link #MEASURED_STATE_TOO_SMALL}.
    */
   public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
       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:
           if (specSize < size) {
               result = specSize | MEASURED_STATE_TOO_SMALL;
           } else {
               result = size;
           }
           break;
       case MeasureSpec.EXACTLY:
           result = specSize;
           break;
       }
       return result | (childMeasuredState&MEASURED_STATE_MASK);
   }

该方法里设计到了两处位运算,暂时还没搞懂这两处位运算有什么作用,如果有清除的还请帮忙解释下作用。

八、在不同图形之间进行切换

现在我们已经绘制了正方形,但是我们想让view在我们点击它的时候切换图形,现在我们给它加入事件,我们重写onTouchEvent方法即可

private String[] shapeValues = { "square", "circle", "triangle" };
  private int currentShapeIndex = 0;
  @Override
  public boolean onTouchEvent(MotionEvent event) {
    boolean result = super.onTouchEvent(event);
    if (event.getAction() == MotionEvent.ACTION_DOWN) {
      currentShapeIndex ++;
      if (currentShapeIndex > (shapeValues.length - 1)) {
        currentShapeIndex = 0;
      }
      postInvalidate();
      return true;
    }
    return result;
  }

现在无论什么时候点击view,都会选中对应的形状,当postInvalidate 方法被调用后就会进行重绘,现在我们更新onDraw代码,绘制不同的图形。

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    String shapeSelected = shapeValues[currentShapeIndex];
    if (shapeSelected.equals("square")) {
      canvas.drawRect(0, 0, shapeWidth, shapeHeight, paintShape);
      textXOffset = 0;
    } else if (shapeSelected.equals("circle")) {
      canvas.drawCircle(shapeWidth / 2, shapeHeight / 2, shapeWidth / 2, paintShape);
      textXOffset = 12;
    } else if (shapeSelected.equals("triangle")) {
      canvas.drawPath(getTrianglePath(), paintShape);
      textXOffset = 0;
    }
    if (displayShapeName) {
      canvas.drawText(shapeSelected, 0 + textXOffset, shapeHeight + textYOffset, paintShape);
    }
  }

  protected Path getTrianglePath() {
    Point p1 = new Point(0, shapeHeight), p2 = null, p3 = null;
    p2 = new Point(p1.x + shapeWidth, p1.y);
    p3 = new Point(p1.x + (shapeWidth / 2), p1.y - shapeHeight);
    Path path = new Path();
    path.moveTo(p1.x, p1.y);
    path.lineTo(p2.x, p2.y);
    path.lineTo(p3.x, p3.y);
    return path;
  }

现在我们点击view,每点击一次图形就会进行切换,其效果图就跟最初贴的gif图片一样。

九、完善控件

增加getter方法获得图形名

public String getSelectedShape() {
    return shapeValues[currentShapeIndex];
}

现在在activity中,我们就可以通过getSelectedShape可以获取到图形名了。

十、状态的保存

当配置改变时,比如手机屏幕发生旋转,我们必须保存一些数据供从容保证view的状态不会发生改变。我们通过重写onSaveInstanceState和onRestoreInstanceState方法来保存和恢复数据。比如,在我们的view中,我们需啊哟保存的数据是当前是什么图形,可以通过保存数组的下标currentShapeIndex来实现。

@Override
  public Parcelable onSaveInstanceState() {
    // 新建一个Bundle
    Bundle bundle = new Bundle();
    // 保存view基本的状态,调用父类方法即可
    bundle.putParcelable("instanceState", super.onSaveInstanceState());
    // 保存我们自己的数据
    bundle.putInt("currentShapeIndex", this.currentShapeIndex);
    // 当然还可以继续保存其他数据
    // 返回bundle对象
    return bundle;
  }

  @Override
  public void onRestoreInstanceState(Parcelable state) {
    // 判断该对象是否是我们保存的
    if (state instanceof Bundle) {
      Bundle bundle = (Bundle) state;
      // 把我们自己的数据恢复
      this.currentShapeIndex = bundle.getInt("currentShapeIndex");
      // 可以继续恢复之前的其他数据
      // 恢复view的基本状态
      state = bundle.getParcelable("instanceState");
    }
    // 如果不是我们保存的对象,则直接调用父类的方法进行恢复
    super.onRestoreInstanceState(state);
  }

一旦我们定义这些保存和恢复的方法,我们就能够在配置发生改变时保存我们必要的数据。
这里写图片描述 这里写图片描述 这里写图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值