第5 章 UI 组件与布局 第4章介绍了Activity,以及Android应用程序中与用户交互的基本单元。一个 Activity中通常包含各种UI控件,以完成不同的功能。这些控件按照一定的位置分布在 Activity的不同区域,这称为布局(layout)。第2 章和第4 章已经使用了Button、 TextView 等控件,以及最简单的布局———LinearLayout(线性布局)。本章将介绍 Android应用程序开发中常用的UI组件和布局。 5.1 常用控件 Android中有多种编写程序界面的方式可供选择。AndroidStudio和Eclipse中都 提供了可视化的界面编辑器。可视化编辑器允许使用拖曳控件的方式对布局进行编辑, 并能在视图上直接修改控件的属性,但是这种方式并不利于真正了解界面背后的原理和 技术。本节在讲解UI组件和布局时,大多数都采用编写XML代码的方式。 下面从Android系统中几种最常见的控件开始熟悉UI组件。 Android提供了大量的UI控件,合理使用这些控件能轻松地编写出美观的界面。本 节将选择几种最常用的控件,详细介绍其使用方法。 Android控件继承结构如图5.1所示。 从图5.1可以看出,View 是Android所有控件的基类,同时ViewGroup也继承自 View。知道View的层级关系有助于理解View。从图5.1可以发现常用的控件都继承 自View,如果掌握了View的知识体系,那么在界面编程时会更加得心应手。 5.1.1 View 类 可以看到,所有的UI控件(主要在包android.view 和包android.widget中)都是 View的子类。使用较早的Android版本进行应用程序开发时,每当用findViewById(R. id.xx)方法时,总要将其返回类型进行强制类型转换,因为该方法返回的是一个View 实 例。其中不得不提View的子类———ViewGroup。Android系统中的所有UI类都建立在 View和ViewGroup 类的基础上。所有View 的子类都称为Widget(小部件),所有 ViewGroup的子类都称为Layout(布局)。View和ViewGroup之间采用组合设计模式, 可以使得“部分-整体”同等对待。ViewGroup作为布局容器类的最上层,布局容器里又 可以有View和ViewGroup。通过这种方式,获得了UI的组合方式。 ViewGroup的子类用不同的方式管理容器中View 控件的摆放位置以及显示方式; 99 第 5 章 UI 组件与布局 但是,对于UI控件具体摆放到什么位置,以及大小等属性,则需要每个布局类的内部类 LayoutParams进行处理,该类是ViewGroup的内部类。LayoutParams类有多个子类实 现,用于指定不同的布局参数。 图5.1 Android控件继承结构 可以看到,Android中所有的UI控件都是View的子类,所以可以通过继承View类 实现自定义控件。注意,此时需要重载View的构造函数。View的构造方法有下面4 个,比较常用的是第一个和第二个。 .publicView(Contextcontext); .publicView(Contextcontext,AtributeSetatrs); .publicView(Contextcontext,AtributeSetatrs,intdefStyleAtr); .publicView(Contextcontext,AtributeSetatrs,intdefStyleAtr,intdefStyleRes); 第一个构造方法的参数context代表该View对象所属的Context对象。第二个构 造方法的参数atrs代表在布局文件中该View对象对应的元素相关的属性值的集合。 使用UI控件时,一般采用下面的步骤。 (1)在布局文件中添加该UI控件的元素(元素的名称与该UI控件的类名完全相 同),并定义该元素的一些属性的值(其中id属性非常重要,下一步将会使用id属性的值 引用该元素)。 (2)在其他资源文件中引用该UI控件;在源代码中定义一个该UI控件类型的变量 (成员变量、局部变量均可),使用findViewById()方法使该变量引用步骤(1)中添加的UI 控件。 1 00 移动应用开发技术 (3)定义处理UI控件某种事件的监听器,并调用步骤(2)中变量的相关方法,把监听 器与UI控件绑定,之后如果用户触发了该种UI事件,那么Android系统将会回调已经 绑定的监听器中的事件处理方法(例如,第4章中Button控件事件监听器的onClick()方 法,该方法由View.OnClickListener接口定义)。 上述步骤将会在后续各种控件的介绍中多次看到。 5.1.2 TextView 为展示Android系统中常见控件的使用方法,可在AndroidStudio中新建一个项目 UIWidgetDemo,使用自动创建的MainActivity以及布局文件activity_main.xml。 第2章已经使用TextView控件显示了HelloWorld! 的欢迎信息。 修改项目UIWidgetDemo中activity_main.xml的代码,具体如下所示。 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/tvHello" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="厉害了,我的国!" /> </LinearLayout> 图5.2 TextView 控件运行结果 观察上述布局文件中的<TextView>标签,使用android:id属性给当前控件定义了 一个唯一的标识符,然后使用android:layout_width 和 android:layout_height属性指定了控件的宽度和高度。 Android中所有的控件都具有这两个属性,可选值有3种: match_parent、fill_parent和wrap_content,其中match_ parent和fill_parent的意义相同,现在更推荐使用match_ parent。match_parent表示让当前控件的大小和父布局的 大小一样,也就是由父布局决定当前控件的大小。wrap_ content表示让当前控件的大小能够刚好包含控件的内容, 也就是由控件的内容决定当前控件的大小。根据上述规 则,上述布局文件的代码表示让TextView 的宽度和父布 局的宽度(也就是手机屏幕的宽度)一样,而TextView 的 高度足够包含其中的内容就可以了。当然,除了上述选项 外,也可以对控件的宽度和高度指定固定的值,但是这样 做有时会在多种手机屏幕(大小和分辨率不同)的适配方 面出现问题。 下面通过android:text属性指定了TextView 中显示 的文本内容,运行该程序,运行结果如图5.2所示。 第5 章 UI 组件与布局1 01 虽然指定的文本内容正常显示了,但是好像并没有看出TextView 控件的宽度和手 机屏幕宽度相同,其实这是由于TextView 中的文本默认居左显示,虽然TextView 控件 的宽度充满了整个屏幕,但是由于文本内容不够长,因此从效果上看不出TextView 控件 的宽度。修改布局文件中TextView控件的对齐方式,代码如下所示。 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/tvHello" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" android:text="厉害了,我的国!" /> </LinearLayout> 使用android:gravity属性指定文字的对齐方式,可选值有top、bottom、left、right、 center等,也可以使用|同时指定多个值。这里指定的center,效果上等价于center_ vertical|center_horizontal,表示文字在垂直和水平方向都居中对齐。重新运行该程序, 运行结果如图5.3所示。 这也说明了TextView的宽度的确和屏幕宽度一样。 另外,还可以对TextView中文字的大小和颜色进行修改,代码如下所示。 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/tvHello" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" android:textSize="36sp" android:textColor="#ff0000" android:text="厉害了,我的国!" /> </LinearLayout> 通过android:textSize属性可以指定文字的大小,通过android:textColor属性可以 指定文字的颜色。在Android中,字体的大小使用sp(scaledpixels)作为单位。重新运行 程序,运行结果如图5.4所示。 除上述属性之外,TextView还有很多其他的属性,此处不再一一列举。 1 02 移动应用开发技术 图5.3 TextView 控件运行结果(居中对齐) 图5.4 TextView 控件运行结果(修改字体大小) 5.1.3 Button Button是应用程序中和用户进行交互的一种常用的控件。第4章中多次使用到 Button。从图5.1 可以看出,Button 类的直接父类是TextView 类,因此5.1.2 节中 TextView的属性Button都会自动继承得到。在布局文件activity_main.xml中添加一 个Button,代码如下: <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <Button android:id="@+id/buttonTest" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="TestButton" /> </LinearLayout> 加入Button之后的界面如图5.5所示。 仔细观察会发现,在布局文件中设置的文本值是TestButton,但最终的显示结果却 是TESTBUTTON,这是因为Android系统会将Button中的所有英文字母自动显示为 第5 章 UI 组件与布局1 03 图5.5 加入Button之后的界面 大写状态。如果仍然想显示包含大小写的原有形式,可以设置android:textAllCaps属性 值为false,代码如下: <Button android:id="@+id/buttonTest" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="TestButton" android:textAllCaps="false" /> 接下来为该Button控件的单击事件绑定一个监听器,代码如下: public class MainActivity extends AppCompatActivity { Button button; TextView textView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); this.button=findViewById(R.id.buttonTest); this.textView=findViewById(R.id.tvHello); //创建监听器类的一个对象 ButtonListener btnListener=new ButtonListener(); 1 04 移动应用开发技术 //把按钮控件与监听器对象绑定,也称为注册 this.button.setOnClickListener(btnListener); } private class ButtonListener implements View.OnClickListener { @Override public void onClick(View v) { MainActivity.this.textView.setText("中国梦!"); } } } 这样,每当用户单击该按钮时,Android系统会执行监听器类中定义的onClick()方 法,只在该方法中编写相关的业务处理逻辑即可。对于初学者来说,上述编程方式比较清 晰明了,但是代码编写比较繁杂,其实,创建的监听器类的对象btnListener只在绑定监听 器时使用,因此使用监听器类的无名对象即可。上述创建对象和绑定监听器的两行代码 可以用下面的一行代码替换。 this.button.setOnClickListener(new ButtonListener()); 这种方法调用中的参数,一般称为创建了ButtonListener类的一个匿名对象(也称无 名对象)。更进一步,ButtonListener类的类名也只在上述绑定事件监听器中使用一次, 因此也可以使用实现了View.OnClickListener接口的一个匿名类的一个匿名对象作为 setOnClickListener()方法的参数,这样就省略了定义类ButtonListener,而只是实现 View.OnClickListener接口中的onClick()方法,在Mainactivity类的onCreate()方法的 最后添加如下代码: this.button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { MainActivity.this.textView.setText("中国梦!"); } }); 添加了Button按钮的程序,单击该按钮后TextView 控件中的内容会改为“中国 梦!”,运行结果如图5.6所示。 可以看出,在调用方法时使用实现了View.OnClickListener接口的匿名类的匿名对 象作为参数,代码非常简洁,只是写法看上去不是很直观,理解起来稍有难度。这样,如果 在MainActivity中有多个控件,就不需要为每个控件定义一个事件处理的监听器类了, 因为那种方式会导致MainActivity的代码结构过于繁杂,不利于代码的编写和维护。这 种方法调用时使用实现了某接口的匿名类的匿名对象的方式,在第6章的多线程程序设 计中还会多次出现,不同的是,接口变成了Runnable,需要实现的方法变成了publicvoid run()。 另外,还有一种实现上述功能的方式,那就是在定义类MainActivity的时候,除了从 第5 章 UI 组件与布局1 05 图5.6 单击Button后的运行结果 AppCompatActivity继承外,还实现View.OnClickListener接口,并在MainActivity类中 实现onClick()方法,代码如下: public class MainActivity extends AppCompatActivity implements View.OnClickListener { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); this.button=findViewById(R.id.buttonTest); this.textView=findViewById(R.id.tvHello); this.button.setOnClickListener(this); } @Override public void onClick(View v) { switch(v.getId()) { case R.id.buttonTest: this.textView.setText("中国梦!"); break; default: break; } } 1 06 移动应用开发技术 由于是MainActivity类本身实现了View.OnClickListener接口,实现了onClick() 方法,因此在绑定事件监听器的时候,方法调用的参数使用this 即可。如果在 MainActivity类有多个控件,这些控件都使用this对象作为事件的监听器,那么在实现 onClick()方法时,需要判断用户单击事件发生在哪个控件上,代码中使用v.getId()获得 发生单击事件的控件对象的id号,之后使用switch-case语句进行处理。由于这种方法 会改变MainActivity类的签名,因此不推荐使用。 另外,无论是TextView,还是Button控件,都可以使用setText()方法设置其中的文 字内容,该方法的定义如下: . publicfinalvoidsetText(CharSequencetext); . publicfinalvoidsetText(char[]text,intstart,intlen); . publicfinalvoidsetText(intresid); . publicfinalvoidsetText(intresid,BufferTypetype); 第一个方法的参数类型是CharSequence接口,String、StringBuffer、StringBuilder等 类都实现了该接口,因此实际参数使用上述类的对象均可。第三个方法的参数是指定的 字符串资源的id,如R.string.app_name,而不是需要显示的整数的内容。如果需要在控 件中显示数值类型的内容,须转换为String类型(最简单的办法是调用toString()方法) 后调用第一个方法,而不是直接调用第三个方法。 5.1.4 EditText EditText是应用程序用来和用户进行交互中使用非常广泛的一种控件,它和 TextView的区别在于,EditText允许用户在控件里输入和编辑内容,同时可以在程序中 对这些内容进行处理。EditText控件的使用场景非常广泛,在进行用户注册、用户登录、 搜索、发送短信、发送微信消息、发微博、聊QQ 等操作中,都会使用EditText控件。从图 5.1中可以看出,EditText和Button类一样,都继承自TextView 类,因此也会继承得到 TextView类的相关成员变量和方法,例如重载的多个setText()方法。 在activity_main.xml布局文件中添加一个EditText控件,代码如下: <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <EditText android:id="@+id/etStatement" android:layout_width="match_parent" android:layout_height="wrap_content"/> </LinearLayout> 通过上述代码可以看出使用UI控件的一般步骤:首先通过android:id属性给控件 定义一个唯一的id,然后指定该控件的宽度和高度,最后再添加一些该控件特有的属性。 第5 章 UI 组件与布局1 07 运行程序,运行结果如图5.7所示。 图5.7 EditText运行结果 如果具有比较丰富的使用Android手机的经验,会发现一些人性化的软件会在输入 框中显示一些提示性的文字,然后当用户输入任何内容之后,原有的提示性的文字就会消 失,这种提示功能在Android中很容易实现,通过android:hint属性(hint的意思是提示、 注意事项、暗示等)设置即可。 设置android:hint属性的代码如下: <EditText android:id="@+id/etStatement" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="请输入您的心情" /> 重新运行程序,界面如图5.7(b)所示,输入信息后如图5.7(c)所示。可以看到, EditText控件中显示了一段提示信息,当输入内容时,这段文本就会自动消失。 但是,随着输入内容的不断增加,EditText会被不断拉长,这是由于EditText的高度 指定的值是wrap_content,因此该控件总能包含用户输入的文本内容,但是当输入的文本 内容过多时,可能会破坏整个Activity的布局,导致这个界面很难看。为了解决这个问 题,可以使用android:maxLines属性修改布局文件,如下所示。 <EditText android:id="@+id/etStatement" android:layout_width="match_parent"