本章介绍App开发常见的几类简单控件的用法,主要包括:显示文字的文本视图、容纳视图的常用布局、响应点击的按钮控件、显示图片的图像视图等。然后结合本章所学的知识,演示一个实战项目“简单计算器”的设计与实现。
1.1 文本显示
本节介绍如何在文本视图TextView上显示规定的文本,包括:怎样在XML文件和java代码中设置文本内容,尺寸的大小有哪些单位、又该怎么设置文本的大小,颜色的色值是如何表达的、又该怎么设置文本的颜色。
1.1.1 设置文本的内容
设置文本内容的两种方式,一种是在XML文件中通过属性android:text设置文本,比如下面这样:
<TextView
android:id="@+id/tv_hello"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="你好,世界!" />
另一种实在Java代码中调用文本视图对象的setText方法设置文本,比如下面这样:
// 获取名叫tv_hello的文本视图
TextView tv_hello = findViewById(R.id.tv_hello);
tv_hello.setText("你好,世界"); // 设置tv_hello的文字内容
在XML文件中设置文本的话,把鼠标移到“你好,世界”上方时,Android Studio会弹出如图所示的提示框。
提示内容为“Hardcoded string "你好,世界",should use @string resouce ”,意思是说这几个字是硬编码的字符串,建议使用来自@string的资源。Android Studio不推荐在XML布局文件里直接写字符串,因为可能有好几个页面都显示“你好,世界”,若想把这句话换成“你吃饭了吗?”,就得一个一个XML文件改过去,无疑费时费力。故而Android Studio推荐把字符串放到专门的地方进行管理,这个名为@string的地方位于res/values目录下的strings.xml,打开该文件发现它的初始内容如下:
<resources>
<string name="app_name">My Application</string>
</resources>
strings.xml定义了一个名为“app_name”的字符串常量,其值为“My Application”。在此添加新的字符串定义,字符串名为“hello”,字符串值为“你好,世界”,添加之后的strings.xml内容如下:
<resources>
<string name="app_name">My Application</string>
<string name="hello">你好,世界</string>
</resources>
添加完新的字符串定义,回到XML布局文件,将android:text属性值改为“@string/字符串名”这般,也就是“@string/hello”,修改之后的TextView标签示例如下:
<TextView
android:id="@+id/tv_hello"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/hello" />
然后把鼠标移到“你好,世界”上方,此时Android Studio不再弹出任何提示了。
若要在Java代码中引用字符串资源,则需要在调用setText方法时填写形如“R.string.字符串名”的参数,就本例而言填入“R.string.hello”,修改之后的Java代码示例如下:
//获取名为tv_hello的文本视图
TextView tv_hello = findViewById(R.id.tv_hello);
tv_hello.setText(R.string.hello); //设置tv_hello的文字资源
至此不管是XML文件还是Java代码都从strings.xml引用字符串资源,以后想把“你好,世界”改为其他文字的话,只需改动string.xml一个地方即可。
1.1.2 设置文本的大小
TextView允许设置文本内容,也允许设置文本大小,在Java代码中调用setTextSize方法,即可指定文本大小,就像以下代码这样:
//从布局文件中获取名为tv_sp的文本视图
TextView tv_sp = findViewById(R.id.tv_sp);
tv_sp.setTextSize(30); //设置tv_sp的文本大小
这里的大小数值越大,则看到的文本也越大;大小数值越小,则看到的文本也越小。在XML文件中则通过属性android:textSize指定文本大小,可是如果给TextView标签添加"android:textSize="30"",数字马上变成红色,鼠标移过去还会提示错误“Cannot resolve symbol '30'”,意思是无法解析“30”这个符号,如图所示。
原来文本大小存在不同的字号单位,XML文件要求在字号数字后面写明单位类型,常见的字号单位主要有px、dp、sp 3种,分别介绍如下。
1. px
px是手机屏幕的最小显示单位,它与设备的显示屏有关。一般来说,同样尺寸的屏幕(比如6英寸手机),如果看起来越清晰,则表示像素密度越高,以px计量的分辨率也越大。
2. dp
dp有时也写作dip,指的是与设备无关的显示单位,它只与屏幕的尺寸有关。一般来说,同样尺寸的屏幕以dp计量的分辨率是相同的,比如同样是6英寸手机,无论它由哪个厂家生产,其分辨率换算成dp单位都是一个大小。
3. sp
sp的原理跟dp差不多,但它专门用来设置字体大小,也是Android推荐的字号单位。手机在系统设置里可以调整字体的大小(小、标准、大、超大)。设置普通字体时,同数值dp和sp的文字看起来一样大;如果设置为大字体,用dp设置的文字没有变化,用sp设置的文字就变大了。
字体大小采用不同单位的话,显示的文字大小各不相同。例如,30px、30dp、30sp这3个字号,在不同手机上的显示大小有所差异。有的手机像素密度较低,1个dp相当于2个px,此时30px等同于15dp;有的手机像素密度较高,1个dp相当于3个px,此时30px等同于10dp。假设某个APP的内部文本使用字号30px,则该App安装到前一部手机的字体大小为15dp,安装到后一部手机的字体大小为10dp,显然后一部手机显示的文本会更小。
至于dp与sp之间的区别,可通过以下实验加以观察。首先创建测试活动页面,该页面的XML文件分别声明30px、30dp、30sp这3个字号的TextView控件,布局内容如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="你好,世界!(px大小)"
android:textSize="30px" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="你好,世界!(dp大小)"
android:textSize="30dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="你好,世界!(sp大小)"
android:textSize="30sp" />
</LinearLayout>
1.1.3 设置文本的颜色
除了文字大小外,文字的颜色也经常需要修改,毕竟Android默认的灰色文字不够醒目。在Java代码中调用setTextColor方法即可设置文本颜色,具体在Color类中定义了12种颜色,详细的取值说明见表。
表 颜色类型的取值说明
Color类中的颜色类型 | 说明 | Color类中的颜色类型 | 说明 |
BLACK | 黑色 | GREEN | 绿色 |
DKGRAY | 深灰 | BLUE | 蓝色 |
GRAY | 灰色 | YELLOW | 黄色 |
LTGRAY | 浅灰 | CYAN | 青色 |
WHITE | 白色 | MAGENTA | 玫红 |
RED | 红色 | TRANSPARENT | 透明 |
比如以下代码便将文本试图的文字颜色改成了绿色:
//从布局文件中获取名为tv_code_system的文本试图
TextView tv_code_system = findViewById(R.id.tv_code_system);
//将tv_code_system的文字颜色设置为系统自带的颜色
tv_code_system.setTextColor(Color.GREEN);
由于XML文件无法引用Color类的颜色常量,为此Android制定了一套规范的编码标准,将色值交由透明度alpha和RGB三原色(红色red、绿色green、蓝色blue)联合定义。该标准又有8位十六进制数与6位十六进制数两种表达方式,例如8位编码FFEEDDCC中,FF表示透明度,EE表示红色的浓度,DD表示绿色的浓度,CC表示蓝色的浓度。透明度为FF表示完全不透明,为00表示完全透明。RGB三色的数值越大,表示颜色越浓,也就越暗;数值越小,表示颜色越淡,也就越亮。RGB亮到极致就是白色,暗到极致就是黑色。
至于6位十六进制编码,则有两种情况:它在XML文件中默认不透明(等价于透明度为FF),在代码中默认透明(等价于透明度为00)。以下代码给两个文本视图分别设置6位色值与8位色值,注意添加0x前缀表示十六进制数:
//从布局文件中获取名为tv_code_six的文本视图
TextView tv_code_six = findViewById(R.id.tv_code_six);
//将tv_code_six的文字颜色设置为透明的绿色,透明就是看不到
tv_code_six.setTextColor(0x00ff00);
//从布局文件中获取名为tv_code_eight的文本视图
TextView tv_code_eight = findViewById(R.id.tv_code_eight);
//将tv_code_eight的文字颜色设置为不透明的绿色,即正常的绿色
tv_code_eight.setTextColor(0xff00ff00);
运行测试App,发现tv_code_six控件的文本不见了(其实是变透明了),而tv_code_eight控件的文本显示正常的绿色。
在XML文件中可通过属性android:textColor设置文本颜色,但要给色值添加井号前缀(#),设定文本颜色的TextView标签示例如下:
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="布局文件设置6位文字颜色"
android:textColor="#00ff00" />
就像字符串资源那样,Android把颜色也当做一种资源,打开res/values目录下的colors.xml,发现里面已经定义了2种颜色:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>
那么先在resources节点内部补充如下的绿色常量定义:
<color name="green">#00ff00</color>
然后回到XML布局文件,把android:textColor的属性值改为“@color/颜色名称”,也就是android:textColor="@color/green",修改之后的TextView标签如下:
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="布局文件设置6位文字颜色"
android:textColor="@color/green" />
不仅文本颜色,还有背景颜色也会用到上述的色值定义,在XML文件中通过属性android:background设置控件的背景颜色。Java代码则有两种方式设置背景颜色:倘若色值来自Color类或十六进制数,则调用setBackgroundColor方法设置背景颜色;倘若色值来自colors.xml中的颜色资源,则调用setBackgroundResource方法,以“R.color.颜色名称”的格式设置背景颜色。下面是两种方式的背景颜色设定代码的例子:
//从布局文件中获取名为tv_code_background的文本视图
TextView tv_code_background = findViewById(R.id.tv_code_background);
//将tv_code_background的背景颜色设置为绿色
tv_code_background.setBackgroundColor(Color.GREEN); //在代码中定义的色值
tv_code_background.setBackgroundResource(R.color.green); //颜色来自资源文件
注意属性android:background和setBackgroundResource方法,它俩用来设置控件的背景,不单单是背景颜色,还包括背景图片。在设置背景图片之前,先将图片文件放到res/drawable***目录(以drawable开头的目录,不仅仅是drawable目录),然后把android:background的属性值改为“@drawable/不含扩展名的图片名称”,或者调用setBackgroundResource方法填入“R.drawable.不含扩展名的图片名称”。
1.2 视图基础
本节介绍视图的几个基本概念及其用法,包括:如何设置视图的宽度和高度,如何设置视图的外部间距和内部间距,如何设置视图的外部对齐方式和内部对齐方式,等等。
1.2.1 设置视图的宽和高
手机屏幕是块长方形区域,较短的那条边叫做宽,较长的那条边叫做高。App控件通常也是长方形形状,控件宽度通过属性android:layout_width表达,控件高度通过属性android:layout_height表达,宽和高的取值主要有下列3种:
(1)match_parent:表示与上级视图保持一致。上级视图的尺寸有多大,当前视图的尺寸就有多大。
(2)wrap_content:表示与内容自适应。对于文本视图来说,内部文字需要多大的显示空间,当前视图就要占据多大的尺寸。但最宽不能超过上级视图的宽度,一旦超过就要换行;最高不能超过上级视图的高度,一旦超过就会被隐藏。
(3)以dp为单位的具体尺寸,比如300dp,表示宽度或者高度就是这么大。
在XML文件中采用以上任一方式均可设置视图的宽和高,但在Java代码中设置宽和高就有点复杂了,首先确保XML中的宽和高属性值为wrap_content,这样才允许在代码中修改宽和高。接着打开该页面对应的Java代码,依序执行以下3个步骤:
01 调用控件对象的setLayoutParams方法获取布局参数,参数类型为ViewGroup.LayoutParams.
02 布局参数的width属性表示宽度,height属性表示高度,修改这两个属性值,即可调整控件的 宽和高。
03 调用控件对象的setLayoutParams方法,填入修改后的布局参数使之生效。
不过布局参数的width和height两个数值默认是px单位,需要将dp单位的数值转换为px单位的数值,然后才能赋值给width属性和height属性。下面是把dp大小转为px大小的方法代码:
package util;
import android.content.Context;
public class Utils {
//根据手机的分辨率从dp的单位转成为px(像素)
public static int dip2px(Context context, float dpValue){
//获取当前手机的像素密度(1个dp对应几个px)
float scale = context.getResources().getDisplayMetrics().density;
return (int) (dpValue*scale+0.5f); //四舍五入取整
}
}
有了上面定义的公共方法dip2px,就能将某个dp数值转换成px数值,比如准备把文本视图的宽度改为300dp,那么调整宽度的Java代码示例如下:
package com.example.myapplication;
import android.annotation.SuppressLint;
import android.os.Bundle;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import util.Utils;
@SuppressLint("MissingInflatedId")
public class MainActivity extends AppCompatActivity {
public TextView tv_code;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_main);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
//获取名为tv_code的文本视图
tv_code = findViewById(R.id.tv_code);
//获取tv_code的布局参数(含宽度和高度)
ViewGroup.LayoutParams params = tv_code.getLayoutParams();
//修改布局参数中的宽度数值,注意默认是px单位,需要把dp数值转成px数值
params.width = Utils.dip2px(this, 300);
tv_code.setLayoutParams(params); //设置tv_code的布局参数
}
}
接下来通过演示页面并观察几种尺寸设置方式的界面效果,主要通过背景色区分当前视图的宽高范围,详细的XML文件内容如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activity_view_border"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:background="#00ffff"
android:text="视图宽度采用wrap_content定义"/>
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="5dp"
android:background="#00ffff"
android:text="视图宽度采用match_parent定义"/>
<TextView
android:layout_width="300dp"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:background="#00ffff"
android:text="视图宽度采用固定大小"/>
<TextView
android:id="@+id/tv_code"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:background="#00ffff"
android:text="通过代码指定视图宽度"/>
</LinearLayout>
运行测试App,打开的演示界面如图所示,依据背景色判断文本视图的边界,可见wrap_content方式刚好包住了文本内容,match_parent方式扩展了与屏幕等宽,而300dp的宽度介于前两者之间(安卓手机的屏幕宽度基本为360dp)。
1.2.2 设置视图的间距
在上一小节末尾的XML文件中,每个TextView标签都携带新的属性android:layout_marginTop="5dp",该属性的作用是让当前视图与上方间隔一段距离。同理,android:layout_marginLeft让当前视图与左边间隔一段距离,android:layout_marginRight让当前视图与右边间隔一段距离,android:layout_marginBottom让当前视图与下方间隔一段距离。如果上下左右都间隔同样的距离,还能使用android:layout_margin一次性设置四周的间距。
layout_margin不单单用于文本视图,还可用于所有视图,包括各类布局和各类控件。因为不管布局还是控件,它们统统由视图基类View派生而来,而layout_margin正是View的一个通用属性,所以View的子子孙孙都能使用layout_margin。在View的大家族中,视图组ViewGroup尤为特殊,它既是View的子类,又是各类布局的基类。布局下面能容纳其他视图,而控件却不行,这正源自ViewGroup的组装特性。View、ViewGroup、控件、布局四者的继承关系如图所示。
除了layout_margin之外,padding也是View的一个通用属性,它用来设置视图的内部间距,并且padding也提供了paddingTop、paddingBottom、paddingLeft、paddingRight四个方向的距离属性。同样是设置间距,layout_margin指的是当前视图与外部视图(包括上级视图和平级视图)之间的距离,而padding指的是当前视图与内部视图(包括下级视图和内部文本)之间的距离。为了观察外部间距和内部间距的差异,接下来做个实验,看看layout_margin与padding究竟有什么区别。
首先创建新的活动页面,并给该页面的XML文件填入以下的布局内容:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="300dp"
tools:context=".activity_view_margin"
android:orientation="vertical"
android:background="#00aaff">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="20dp"
android:background="#ffff99"
android:padding="60dp">
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ff0000"/>
</LinearLayout>
</LinearLayout>
上面的XML文件有两层视图嵌套,第一层是蓝色背景布局里面放黄色背景布局,第二层是黄色背景布局里面放红色背景视图。中间层的黄色背景布局,同时设置了20dp的layout_margin,以及60dp的padding,其中padding是layout_margin的三倍宽(60/20=3)。接着运行测试App,看到的演示界面如图所示。
从效果图可见,外面一圈间隔较窄,里面一圈间隔较宽,表示20dp的layout_margin位于外圈,而60dp的padding位于内圈。这种情况印证了:layout_margin指的是当前图层与外部图层的距离,而padding指的是当前图层与内部图层的距离。
1.2.3 设置视图的对齐方式
App界面上的视图排列,默认靠左朝上对齐,这也符合日常的书写格式。然而页面的排版不是一成不变的,有时出于美观或者其他原因,要将视图排列改为朝下或靠右对齐,为此需要另外指定视图的对齐方式。在XML文件中通过属性android:layout_gravity可以指定当前视图的对齐方向,当属性值为top时表示视图朝上对齐,为bottom时表示视图朝下对齐,为left时表示视图靠左对齐,为right时表示视图靠右对齐。如果希望视图既朝上又靠左,则用竖线连接top与left,此时属性标记为android:layout_gravity="top|left";如果希望视图既朝下又靠右,则用竖线连接bottom与right,此时属性标记为android:layout_gravity="bottom|right"。
注意layout_gravity规定的对齐方式,指的是当前视图往上级视图的那个方向对齐,并非当前视图的内部对齐。若想设置内部视图的对齐方向,则需由当前视图的属性android:gravity指定,该属性一样拥有top、bottom、left、right 4种取值及其组合。它与layout_gravity的不同之处在于:layout_gravity设定了当前视图相对于上级视图的对齐方式,而gravity设定了下级视图相对于当前视图的对齐方式;前者决定了当前视图的位置,而后者决定了下级视图的位置。
为了进一步分辨layout_gravity与gravity的区别,接下来做个实验,对某个布局视图同时设置android:layout_gravity和android:gravity属性,再观察内外视图的对齐情况。下面便是实验用的XML文件例子:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="400dp"
tools:context=".activity_view_gravity"
android:background="#ffff99"
android:padding="5dp">
<LinearLayout
android:layout_width="0dp"
android:layout_height="200dp"
android:layout_weight="1"
android:layout_gravity="bottom"
android:gravity="left"
android:background="#ff0000"
android:layout_margin="10dp"
android:padding="10dp">
<View
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#00ffff"/>
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="200dp"
android:layout_weight="1"
android:layout_gravity="top"
android:gravity="right"
android:background="#ff0000"
android:layout_margin="10dp"
android:padding="10dp">
<View
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#00ffff"/>
</LinearLayout>
</LinearLayout>
运行测试App,打开演示界面如图所示。
由效果图可见,第一个子布局朝下,并且它的内部视图靠左;而第二个子布局朝上,并且它的内部视图靠右。对比XML文件中的layout_gravity和gravity取值,证明了二者的对齐情况正如之前所言:layout_grativy决定当前视图位于上级视图的哪个方位,而gravity决定了下级视图位于当前视图的哪个方位。
1.3 常用布局
本节介绍常见的几种布局用法,包括:在某个方向上顺序排列的线性布局,参照其他视图的位置相对排列的相对布局,像表格那样分行分列显示的网格布局,以及支持通过滑动操作拉出更多内容的滚动视图。
1.3.1 线性布局 LinearLayout
前几个小节的例程中,XML文件用到了LinearLayout布局,它的学名为线性布局。顾名思义,线性布局像是用一根线把它的内部视图串起来,故而内部视图之间的排列顺序是固定的,要么从左到右排列,要么从上到下排列。在XML文件中,LinearLayout通过属性android:orientation区分两种方向,其中从左到右排列叫做水平方向,属性值为horizontal;从上到下排列叫做垂直方向,属性值为vertical。如果LinearLayout标签不指定具体方向,则系统默认该布局为水平方向排列,也就是默认android:orientation="horizontal"。
下面做个实验,让XML文件的根节点挂着两个线性布局,第一个线性布局采取水平方向。第二个线性布局采取垂直方向。然后每个线性布局内部各有两个文本视图,通过观察这些文本视图的排列情况,从而检验线性布局的显示效果。详细的XML文件内容如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activity_linear_layout"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="横排第一个"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="横排第二个"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="竖排第一个"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="竖排第二个"/>
</LinearLayout>
</LinearLayout>
运行测试App,进入如图所示的演示页面,可见horizontal为横向排列,vertical为纵向排列,说明android:orientation的方向属性确实奏效了。
除了方向之外,线性布局还有一个权重概念。所谓权重,指的是线性布局的下级视图各自拥有多大比例的宽和高。比如一块蛋糕分给两个人吃,可能两个人平均分,也可能甲分三分之一,乙分三分之二。两人平均分的话,先把蛋糕平均切成两半,然后甲分到一半,乙分到另一半,此时甲、乙的权重比为1:1.甲分三分之一、乙分三分之二的话,先把蛋糕平均切成三块,然后甲分到一块,乙分到两块,此时甲、乙的权重比为1:2.就线性布局而言,它自身的尺寸相当于一整块蛋糕,它的下级视图们一起来分这一整块蛋糕,有的视图分得多,有的视图分得少。分多分少全凭每个视图分到了多大的权重而定,这个权重在XML文件中通过属性android:layout_weight来表达。
把线性布局看作蛋糕的话,分蛋糕的甲、乙两人就相当于线性布局的下级视图。假设线性布局平均分为左、右两块,则甲视图和乙视图的权重比为1:1,意味着两个下级视图的layout_weight属性都是1。不过视图有宽、高两个方向,系统怎知layout_weight表示哪个方向的权重呢?所以这里有个规定,一旦设置了layout_weight属性值,便要求layout_width填0dp或者layout_height填0dp。如果layout_width填0dp,则layout_weight表示水平方向的权重,下级视图会从左往右分割线性布局;如果layout_height填0dp,则layout_weight表示垂直方向的权重,下级视图会从上往下分割线性布局。
按照左右均分的话,线性布局设置水平方向horizontal,且甲、乙两视图的layout_width都填0dp,layout_weight都填1,此时横排的XML片段示例如下:
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#ff0000"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:text="横排第一个"/>
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:text="横排第二个"/>
</LinearLayout>
按照上下 均分的话,线性布局设置垂直方向vertical,且甲乙两视图对的layout_height都填0dp,layout_weight都填1,此时竖排的XML片段示例如下:
<LinearLayout
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="#00ffff"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:gravity="center"
android:text="竖排第一个"/>
<TextView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:gravity="center"
android:text="竖排第二个"/>
</LinearLayout>
把上面两个片段放到新页面的XML文件中,其中第一个是横排区域,采用红色背景(色值为ff0000),第二个是竖排区域,采用青色背景(色值为00ffff)。重新运行测试App,打开的演示界面如图所示,可见横排区域平均分为左、右两块,竖排区域平均分为上、下两块。
1.3.2 相对布局 RelativeLayout
线性布局的下级视图是顺序排列着的,另一种相对布局的下级视图位置则由其他视图决定。相对布局名为RelativeLayout,因为下级视图的位置是相对位置,所以得有具体的参照物才能确定最终位置。如果不设定下级视图的参照物,那么下级视图默认显示在RelativeLayout内部的左上角。
用于确定下级视图位置的参照物分两种:一种是与该视图自身平级的视图;另一种是该视图的上级视图(也就是它归属的RelativeLayout)。综合两种参照物,相对位置在XML文件中的属性取值说明见表。
相对位置的属性取值说明
相对位置的属性取值 | 相对位置说明 |
layout_toLeftOf | 当前视图在指定视图的左边 |
layout_toRightOf | 当前视图在指定视图的右边 |
layout_above | 当前视图在指定视图的上方 |
layout_below | 当前视图在指定视图的下方 |
layout_alignLeft | 当前视图与指定视图的左侧对齐 |
layout_alignRight | 当前视图与指定视图的右侧对齐 |
layout_alignTop | 当前视图与指定视图的顶部对齐 |
layout_alignBottom | 当前视图与指定视图的底部对齐 |
layout_centerInParent | 当前视图在上级视图中间 |
layout_centerHorizontal | 当前视图在上级视图的水平方向居中 |
layout_centerVertical | 当前视图在上级视图的垂直方向居中 |
layout_alignParentLeft | 当前视图与上级视图的左侧对齐 |
layout_alignParentRight | 当前视图与上级视图的右侧对齐 |
layout_alignParentTop | 当前视图与上级视图的顶部对齐 |
layout_alignParentBottom | 当前视图与上级视图的底部对齐 |
为了更好的理解上述相对属性的含义,接下来使用RelativeLayout及其下级视图进行布局来看看实际效果。下面是演示相对布局的XML文件例子:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="150dp"
tools:context=".activity_relative_layout">
<TextView
android:id="@+id/tv_center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:background="#eeeeee"
android:text="我在中间"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:background="#eeeeee"
android:text="我在水平中间"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:background="#eeeeee"
android:text="我在垂直中间"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:background="#eeeeee"
android:text="我跟上级左边对齐"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:background="#eeeeee"
android:text="我跟上级右边对齐"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:background="#eeeeee"
android:text="我跟上级顶部对齐"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:background="#eeeeee"
android:text="我跟上级底部对齐"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toLeftOf="@+id/tv_center"
android:layout_alignTop="@+id/tv_center"
android:background="#eeeeee"
android:text="我在中间左边"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@+id/tv_center"
android:layout_alignBottom="@+id/tv_center"
android:background="#eeeeee"
android:text="我在中间右边"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@+id/tv_center"
android:layout_alignLeft="@+id/tv_center"
android:background="#eeeeee"
android:text="我在中间上面"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/tv_center"
android:layout_alignRight="@+id/tv_center"
android:background="#eeeeee"
android:text="我在中间下面"/>
</RelativeLayout>
上述XML文件的布局效果如图所示,RelativeLayout下级视图都是文本视图,控件上的文字说明了所处的相对位置,具体的控件显示方位正如XML属性中描述的那样。
1.3.3 网格布局 GridLayout
虽然线性布局既能在水平方向排列,也能在垂直方向排列,但它不支持多行多列的布局方式,只支持单行(水平排列)或单列(垂直排列)的布局方式。若要实现类似表格那样的多行多列形式,可采用网格布局GridLayout。
网格布局默认从左到右、从上到下排列,它先从第一行从左往右放置下级视图,塞满之后另起一行放置其余的下级视图,如此循环往复直至所有下级视图都放置完毕。为了判断能够容纳几行几列,网格布局新增了android:columnCount与android:rowCount两个属性,其中columnCount指定了网格的列数,即每行能放多少个视图;rowCount指定了网格的行数,即每列能放多少个视图。
下面是运用网格布局的XML布局样例,它规定了一个两行两列的网格布局,且内部容纳四个文本视图。XML文件内容如下:
<?xml version="1.0" encoding="utf-8"?>
<GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activity_grid_layout"
android:columnCount="2"
android:rowCount="2">
<TextView
android:layout_width="180dp"
android:layout_height="60dp"
android:gravity="center"
android:background="#ffcccc"
android:text="浅红色" />
<TextView
android:layout_width="180dp"
android:layout_height="60dp"
android:gravity="center"
android:background="#ffaa00"
android:text="橙色" />
<TextView
android:layout_width="180dp"
android:layout_height="60dp"
android:gravity="center"
android:background="#00ff00"
android:text="绿色" />
<TextView
android:layout_width="180dp"
android:layout_height="60dp"
android:gravity="center"
android:background="#660066"
android:text="深紫色" />
</GridLayout>
在一个新建的活动页面加载上述布局,运行App观察到的界面如图所示。
由图可见,App界面的第一行分布着浅红色背景与橙色背景的文本视图,第二行分布着绿色背景与深紫色背景的文本视图,说明利用网格布局实现了多行多列的效果。
1.3.4 滚动视图 ScrollView
手机屏幕的显示空间有限,常常需要上下滑动或左右滑动才能拉出其余页面内容,可惜一般的布局节点都不支持自行滚动,这时就要借助滚动视图了。与线性布局类似,滚动视图也分为垂直方向和水平方向两类,其中垂直滚动视图名为ScrollView,水平滚动视图名为HorizontalScrollView。这两个滚动视图的使用并不复杂,主要注意以下3点:
(1)在垂直方向滚动时,layout_width属性值设置为match_parent,layout_height属性值设置为wrap_content。
(2)在水平方向滚动时,layout_width属性值设置为wrap_content,layout_height属性值设置为match_parent。
(3)滚动视图节点下面必须且只能挂着一个子布局节点,否则会在运行时报错Caused by:java.lang.IllegalStateException:ScrollView can host only one direct child。
下面是ScrollView和HorizontalScrollView的XML例子:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activity_scroll_view"
android:orientation="vertical">
<HorizontalScrollView
android:layout_width="wrap_content"
android:layout_height="200dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="horizontal">
<View
android:layout_width="300dp"
android:layout_height="match_parent"
android:background="#aaffff" />
<View
android:layout_width="300dp"
android:layout_height="match_parent"
android:background="#ffff00" />
</LinearLayout>
</HorizontalScrollView>
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<View
android:layout_width="match_parent"
android:layout_height="400dp"
android:background="#00ff00" />
<View
android:layout_width="match_parent"
android:layout_height="400dp"
android:background="#ffffaa" />
</LinearLayout>
</ScrollView>
</LinearLayout>
运行测试App,可知ScrollView在纵向滚动,而HorizontalScrollView在横向滚动。
有时ScrollView的实际内容不够,又想让它充满屏幕,怎么办呢?如果把layout_height属性赋值为match_parent,结果还是不会充满,正确的做法是再增加一行属性android:fillViewport(该属性值为true表示允许填满视图窗口),属性片段举例如下:
android:layout_height="match_parent"
android:fillViewport="true"
1.4 按钮触控
本节介绍按钮控件的常见用法,包括:如何设置大小写属性与点击属性,如何响应按钮的点击事件和长按事件,如何禁用按钮又该如何启用按钮,等等。
1.4.1 按钮控件 Button
除了文本视图之外,按钮也是一种基础控件。因为Button是由TextView派生而来,所以文本视图拥有的属性和方法,包括文本内容、文本大小、文本颜色等,按钮控件均能使用。不同的是,Button拥有默认的按钮背景,而TextView默认无背景;Button的内部文本默认居中对齐,而TextView的内部文本默认靠左对齐。此外,按钮还要额外注意textAllCaps与onClick两个属性,这两个属性分别介绍如下:
1. textAllCaps属性
对于TextView来说,text属性设置了什么文本,文本视图就显示什么文本。但对于Button来说,不管text属性设置的是大写字母还是小写字母,按钮控件都默认转成大写字母显示。比如在XML文件中加入下面的Button标签:
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Hello World" />
此功能已取消,不用写了
2. onClick属性
按钮之所以成为按钮,是因为它会响应按下动作,就手机而言,按下动作等同于点击操作,即手指轻触屏幕然后马上松开。每当点击按钮之时,就表示用户确认了某个事项,接下来轮到App进行处理了。onClick属性便用来接管用户的点击动作,该属性的值是个方法名,也就是当前页面的Java代码存在这么一个方法:当用户点击按钮时,就会自动调用该方法。
例如下面的Button标签指定了onClick属性值为doClick,表示点击该按钮会触发Java代码中的doClick方法:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activity_button_style"
android:orientation="vertical">
<Button
android:id="@+id/btn_click_xml"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="直接指定点击方法"
android:onClick="doClick"
android:textColor="#000000"
android:textSize="17sp" />
<TextView
android:id="@+id/tv_result"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="这里查看按钮的点击结果"
android:textColor="#000000"
android:textSize="17sp" />
</LinearLayout>
与之相对应,页面所在的Java代码需要增加doClick方法,方法代码如下:
package com.example.myapplication;
import android.annotation.SuppressLint;
import java.text.SimpleDateFormat;
import java.util.Date;
@SuppressLint("SimpleDateFormat")
public class DateUtil {
// 获取当前的日期时间
public static String getNowDateTime() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
return sdf.format(new Date());
}
// 获取当前的时间
public static String getNowTime() {
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
return sdf.format(new Date());
}
}
package com.example.myapplication;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
public class activity_button_style extends AppCompatActivity {
private TextView tv_result; //声明一个文本视图实例
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_button_style);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
tv_result = findViewById(R.id.tv_result); //获取名叫tv_result的文本视图
}
public void doClick(View view) {
String desc = String.format("%s 您点击了按钮:%s",DateUtil.getNowTime(),((Button) view).getText());
tv_result.setText(desc);
}
}
然后编译运行,并在App界面上点击新加的按钮,点击前、后的界面分别如以下两图所示。
比较两图的文字差异,可见点击按钮之后确实调用了doClick方法。
1.4.2 点击事件和长按事件
虽然按钮控件能够在XML文件中通过onClick属性指定点击方法,但是方法的名称可以随便叫,既能叫doClick也能叫doTouch,甚至叫它doA或doB都没问题,这样很不利于规范化代码,倘若以后换了别人接手,就不知道doA或doB是干什么用的。因此在实际开发中,不推荐使用Button标签的onClick属性,而是在代码中给按钮对象注册点击监听器。
所谓监听器,意思是专门监听控件的动作行为,它平时无事可做,只有控件发生了指定的动作,监听器才会触发开关去执行对应的代码逻辑。点击监听器需要实现接口View.OnClickListener,并重写onClick方法补充点击事件的处理代码,再由按钮调用setOnClickListener方法设置监听器对象。比如下面的代码给按钮控件btn_click_single设置了一个点击监听器:
// 从布局文件中获取名叫btn_click_single的按钮控件
Button btn_click_single = findViewById(R.id.btn_click_single);
// 设置点击监听器,一旦用户点击按钮,就触发监听器的onClick方法
btn_click_single.setOnClickListener(new MyOnClickListener());
上面的点击监听器名为MyOnClickListener,它的定义代码示例如下:
// 定义一个点击监听器,它实现了接口View.OnClickListener
class MyOnClickListener implements View.OnClickListener {
@Override
public void onClick(View v) { // 点击事件的处理方法
String desc = String.format("%s 您点击了按钮:%s",
DateUtil.getNowTime(), ((Button) v).getText());
tv_result.setText(desc); // 设置文本视图的文本内容
}
}
接着运行App,点击按钮之后的界面如图所示,可见点击动作的确触发了监听器的onClick方法。
如果一个页面只有一个按钮,单独定义新的监听器倒也无妨,可是如果存在许多按钮,给每个按钮都定义自己的监听器,那就劳民伤财了。对于同时监听多个按钮的情况,更好的办法是注册统一的监听器,也就是让当前页面实现接口View.OnClickListener,如此一来,onClick方法便写在了页面代码之内。因为是统一的监听器,所以onClick内部需要判断是哪个按钮被点击了,也就是利用视图对象的getId方法检查控件编号,完整的onClick代码举例如下:
@Override
public void onClick(View v) {
if (v.getId() == R.id.btn_click_public) { // 来自按钮btn_click_public
String desc = String.format("%s 您点击了按钮:%s",
DateUtil.getNowTime(), ((Button) v).getText());
tv_result.setText(desc); // 设置文本视图的文本内容
}
}
当然在该页面的onCreate内部别忘了调用按钮对象的setOnClickListener方法,把按钮的点击监听器设置成当前页面,设置代码如下:
// 从布局文件中获取名叫btn_click_public的按钮控件
Button btn_click_public = findViewById(R.id.btn_click_public);
// 设置点击监听器,一旦用户点击按钮,就触发监听器的onClick方法
btn_click_public.setOnClickListener(this);
重新运行App,点击第二个按钮之后的界面如图所示,可见当前页面的onClick方法也正确执行了。
完整代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activity_button_click"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/btn_click_single"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="指定单独的点击监听器"
android:textColor="#000000"
android:textSize="15sp" />
<Button
android:id="@+id/btn_click_public"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="指定公共的点击监听器"
android:textColor="#000000"
android:textSize="15sp" />
</LinearLayout>
<TextView
android:id="@+id/tv_result"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="5dp"
android:text="这里查看按钮的点击结果"
android:textColor="#000000"
android:textSize="15sp" />
</LinearLayout>
@SuppressLint("SimpleDateFormat")
public class DateUtil {
// 获取当前的日期时间
public static String getNowDateTime() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
return sdf.format(new Date());
}
// 获取当前的时间
public static String getNowTime() {
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
return sdf.format(new Date());
}
}
package com.example.myapplication;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
public class activity_button_click extends AppCompatActivity implements View.OnClickListener {
private TextView tv_result; //声明一个文本视图实例
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_button_click);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
tv_result = findViewById(R.id.tv_result); // 获取名叫tv_result的文本视图
// 从布局文件中获取名叫btn_click_single的按钮控件
Button btn_click_single = findViewById(R.id.btn_click_single);
// 设置点击监听器,一旦用户点击按钮,就触发监听器的onClick方法
btn_click_single.setOnClickListener(new MyOnClickListener());
// 从布局文件中获取名叫btn_click_public的按钮控件
Button btn_click_public = findViewById(R.id.btn_click_public);
// 设置点击监听器,一旦用户点击按钮,就触发监听器的onClick方法
btn_click_public.setOnClickListener(this);
}
// 定义一个点击监听器,它实现了接口View.OnClickListener
class MyOnClickListener implements View.OnClickListener {
@Override
public void onClick(View v) { // 点击事件的处理方法
String desc = String.format("%s 您点击了按钮:%s",
DateUtil.getNowTime(), ((Button) v).getText());
tv_result.setText(desc); // 设置文本视图的文本内容
}
}
@Override
public void onClick(View v) {
if (v.getId() == R.id.btn_click_public) { // 来自按钮btn_click_public
String desc = String.format("%s 您点击了按钮:%s",
DateUtil.getNowTime(), ((Button) v).getText());
tv_result.setText(desc); // 设置文本视图的文本内容
}
}
}
除了点击事件外,Android还设计了另外一种长按事件,每当控件被按住超过500毫秒之后,就会触发该控件的长按事件。若要捕捉按钮的长按事件,可调用按钮对象的setOnLongClickListener方法设置长按监听器。具体的设置代码示例如下:
// 从布局文件中获取名叫btn_click_public的按钮控件
Button btn_longclick_public = findViewById(R.id.btn_longclick_public);
// 设置长按监听器,一旦用户长按按钮,就触发监听器的onLongClick方法
btn_longclick_public.setOnLongClickListener(this);
以上代码把长按监听器设置到当前页面,意味着该页面需要实现对应的长按接口View.OnLongClickListener,并重写长按方法onLongClick,下面便是重写后的onLongClick代码例子:
@Override
public boolean onLongClick(View v) {
if (v.getId() == R.id.btn_longclick_public) { // 来自按钮btn_longclick_public
String desc = String.format("%s 您长按了按钮:%s",
DateUtil.getNowTime(), ((Button) v).getText());
tv_result.setText(desc); // 设置文本视图的文本内容
}
return true;
}
再次运行App,长按按钮之后的界面如图所示,说明长按事件果然触发了onLongClick方法。
值得注意的是,点击监听器和长按监听器不局限于按钮控件,其实它们都来自视图基类View,凡是从View派生而来的各类控件,均可注册点击监听器和长按监听器。譬如文本视图TextView,其对象也能调用setOnClickListener方法与setOnLongClickListener方法,此时TextView控件就会响应点击动作和长按动作。因为按钮存在按下和松开两种背景,便于提示用户该控件允许点击,但文本视图默认没有按压背景,不方便判断是否被点击,所以一般不会让文本视图处理点击事件和长按事件。
完整代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activity_button_longclick"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/btn_longclick_single"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="指定单独的长按监听器"
android:textColor="#000000"
android:textSize="15sp" />
<Button
android:id="@+id/btn_longclick_public"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="指定公共的长按监听器"
android:textColor="#000000"
android:textSize="15sp" />
</LinearLayout>
<TextView
android:id="@+id/tv_result"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="5dp"
android:text="这里查看按钮的长按结果"
android:textColor="#000000"
android:textSize="15sp" />
</LinearLayout>
package com.example.myapplication;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
public class activity_button_longclick extends AppCompatActivity implements View.OnLongClickListener {
private TextView tv_result; // 声明一个文本视图实例
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_button_longclick);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
tv_result = findViewById(R.id.tv_result); // 获取名叫tv_result的文本视图
// 从布局文件中获取名叫btn_click_single的按钮控件
Button btn_longclick_single = findViewById(R.id.btn_longclick_single);
// 设置长按监听器,一旦用户长按按钮,就触发监听器的onLongClick方法
btn_longclick_single.setOnLongClickListener(new MyOnLongClickListener());
// 从布局文件中获取名叫btn_click_public的按钮控件
Button btn_longclick_public = findViewById(R.id.btn_longclick_public);
// 设置长按监听器,一旦用户长按按钮,就触发监听器的onLongClick方法
btn_longclick_public.setOnLongClickListener(this);
}
// 定义一个长按监听器,它实现了接口View.OnLongClickListener
class MyOnLongClickListener implements View.OnLongClickListener {
@Override
public boolean onLongClick(View v) { // 长按事件的处理方法
String desc = String.format("%s 您长按了按钮:%s",
DateUtil.getNowTime(), ((Button) v).getText());
tv_result.setText(desc); // 设置文本视图的文本内容
return true;
}
}
@Override
public boolean onLongClick(View v) {
if (v.getId() == R.id.btn_longclick_public) { // 来自按钮btn_longclick_public
String desc = String.format("%s 您长按了按钮:%s",
DateUtil.getNowTime(), ((Button) v).getText());
tv_result.setText(desc); // 设置文本视图的文本内容
}
return true;
}
}
1.4.3 禁用与恢复按钮
尽管按钮控件生来就是给人点击的,可是某些情况仍然希望暂时禁止点击操作,譬如用户在注册的时候,有的网站要求用户必须同意指定条款,而且至少浏览10秒之后才能点击注册按钮。那么在10秒之前,注册按钮应当置灰且不能点击,等过了10秒之后,注册按钮才恢复正常。在这样的业务场景中,按钮先后拥有两种状态,即不可用状态与可用状态,它们在外观和功能上的区别如下:
(1)不可用按钮:按钮不允许点击,即使点击也没反应,同时按钮文字为灰色。
(2)可用按钮:按钮允许点击,点击按钮会触发点击事件,同时按钮文字为正常的黑色。
从上述的区别说明可知,不可用与可用状态主要有两点差异:其一,是否允许点击;其二,按钮文字的颜色。就文字颜色而言,可在布局文件中使用textColor属性设置颜色,也可在Java代码中调用setTextColor方法设置颜色。至于是否允许点击,则需引入新属性android:enabled,该属性值为true时表示启用按钮,即允许点击按钮;该属性值为false时表示禁用按钮,即不允许点击按钮。在Java代码中,则可通过setEnabled方法设置按钮的可用状态(true表示启用,false表示禁用)。
接下来通过一个例子演示按钮的启用和禁用操作。为了改变测试按钮的可用状态,需要额外添加两个控制按钮,分别是“启用测试按钮”和“禁用测试按钮”,加起来一共3个按钮控件,注意“测试按钮”默认是灰色文本。“测试按钮”尚未启用时的界面效果如图所示。
与图对应的布局文件内容如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".activity_button_enable">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/btn_enable"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="启用测试按钮" />
<Button
android:id="@+id/btn_disable"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="禁用测试按钮" />
</LinearLayout>
<Button
android:id="@+id/btn_test"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:enabled="false"
android:text="测试按钮" />
<TextView
android:id="@+id/tv_result"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="5dp"
android:text="这里查看测试按钮的点击结果" />
</LinearLayout>
然后再Java代码中给3个按钮分别注册点击监听器,注册代码如下:
// 因为按钮控件的setOnClickListener方法来自View基类,所以也可对findViewById得到的视图直接设置点击监听器
findViewById(R.id.btn_enable).setOnClickListener(this);
findViewById(R.id.btn_disable).setOnClickListener(this);
btn_test = findViewById(R.id.btn_test); // 获取名叫btn_test的按钮控件
btn_test.setOnClickListener(this); // 设置btn_test的点击监听器
同时重写页面的onClick方法,分别处理3个按钮的点击事件,修改之后的onClick代码示例如下:
@Override
public void onClick(View v) {
// 由于多个控件都把点击监听器设置到了当前页面,因此公共的onClick方法内部需要区分来自哪个按钮
if (v.getId() == R.id.btn_enable){// 点击了按钮“启用测试按钮”
btn_test.setTextColor(Color.BLACK);// 设置按钮的文字颜色
btn_test.setEnabled(true);// 启用当前控件
} else if (v.getId() == R.id.btn_disable) {// 点击了按钮“禁用测试按钮”
btn_test.setTextColor(Color.GRAY); // 设置按钮的文字颜色
btn_test.setEnabled(false);// 禁用当前控件
} else if (v.getId() == R.id.btn_test) {
String desc = String.format("%s 您点击了按钮:%s",
DateUtil.getNowTime(), ((Button) v).getText());
tv_result.setText(desc);// 设置文本视图的文本内容
}
}
完整代码如下:
@SuppressLint("SimpleDateFormat")
public class DateUtil {
// 获取当前的日期时间
public static String getNowDateTime() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
return sdf.format(new Date());
}
// 获取当前的时间
public static String getNowTime() {
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
return sdf.format(new Date());
}
}
package com.example.myapplication;
import android.graphics.Color;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
public class activity_button_enable extends AppCompatActivity implements View.OnClickListener {
private TextView tv_result; //声明一个文本视图实例
private Button btn_test; //声明一个按钮控件实例
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_button_enable);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
tv_result = findViewById(R.id.tv_result); //获取名叫tv_result的文本视图
// 因为按钮控件的setOnClickListener方法来自View基类,所以也可对findViewById得到的视图直接设置点击监听器
findViewById(R.id.btn_enable).setOnClickListener(this);
findViewById(R.id.btn_disable).setOnClickListener(this);
btn_test = findViewById(R.id.btn_test); // 获取名叫btn_test的按钮控件
btn_test.setOnClickListener(this); // 设置btn_test的点击监听器
}
@Override
public void onClick(View v) {
// 由于多个控件都把点击监听器设置到了当前页面,因此公共的onClick方法内部需要区分来自哪个按钮
if (v.getId() == R.id.btn_enable){// 点击了按钮“启用测试按钮”
btn_test.setTextColor(Color.BLACK);// 设置按钮的文字颜色
btn_test.setEnabled(true);// 启用当前控件
} else if (v.getId() == R.id.btn_disable) {// 点击了按钮“禁用测试按钮”
btn_test.setTextColor(Color.GRAY); // 设置按钮的文字颜色
btn_test.setEnabled(false);// 禁用当前控件
} else if (v.getId() == R.id.btn_test) {
String desc = String.format("%s 您点击了按钮:%s",
DateUtil.getNowTime(), ((Button) v).getText());
tv_result.setText(desc);// 设置文本视图的文本内容
}
}
}
最后编译运行App,点击了“启用测试按钮”之后,原本置灰的测试按钮btn_test恢复正常的黑色文本,点击该按钮发现界面有了反应,具体效果如图所示。
对比两图,观察按钮启用前后的外观及其是否响应点击动作,即可知晓禁用按钮和启用按钮两种模式的差别。
1.5 图像显示
本节介绍与图像显示有关的几种控件及其用法,包括:专门用于显示图片的图像视图以及若干缩放类型的效果,支持显示图片的按钮控件--图像按钮,如何在按钮控件上同时显示文本和图标等。
1.5.1 图像视图 ImageView
显示文本用到了文本视图TextView,显示图像则用到了图像视图ImageView。由于图像通常保存为单独的图片文件,因此需要先把图片放到res/drawable目录中,然后再去引用该图片的资源名称。比如先在有张名为apple.png的苹果图片,那么XML文件通过属性android:src设置图片资源,属性值格式形如“@drawabl/不含扩展名的图片名称”。添加了src属性的ImageView标签示例如下:
<ImageView
android:id="@+id/iv_scale"
android:layout_width="match_parent"
android:layout_height="220dp"
android:layout_marginTop="5dp"
android:src="@drawable/apple" />
若想在Java代码中设置图像视图的图片资源,可调用ImageView控件的setImageResource方法,方法参数格式形如“R.drawable.不含扩展名的图片名称”。仍以上述的苹果图片为例,给图像视图设置图片资源的代码例子如下:
// 从布局文件中获取名为iv_scale的图像视图
ImageView iv_scale = findViewById(R.id.iv_scale);
iv_scale.setImageResource(R.drawable.apple); // 设置图像视图的图片资源
运行测试App,展示图片的界面效果如图所示。
观察效果图发现苹果图片居中显示,而非像文本视图里的文字那样默认靠左显示,这时怎么回事?原来ImageView本身默认图片居中显示,不管图片有多大抑或有多小,图像视图都会自动缩放图片,使之刚好够着ImageView的边界,并且缩放后的图片保持原始的宽高比例,看起来图片很完美地占据了视图中央。这种缩放类型在XML文件中通过属性android:scaleType定义,即使图像视图未明确指定该属性,系统也会默认其值为fitCenter,表示让图像缩放后居中显示。添加了缩放属性的ImageView标签如下:
<ImageView
android:id="@+id/iv_scale"
android:layout_width="match_parent"
android:layout_height="220dp"
android:layout_marginTop="5dp"
android:src="@drawable/apple"
android:scaleType="fitCenter" />
在Java代码中可调用setScaleType方法设置图像视图的缩放类型,其中fitCenter对应的类型为ScaleType.FIT_CENTER,设置代码示例如下:
// 将缩放类型设置为“保持宽高比例,缩放图片使其位于视图中间”
iv_scale.setScaleType(ImageView.ScaleType.FIT_CENTER);
除了居中显示,图像视图还提供了其他缩放类型,详细的缩放类型取值说明见表。
缩放类型的取值说明
XML中的缩放类型 | ScaleType类型中的缩放类型 | 说明 |
fitCenter | FIT_CENTER | 保持宽高比例,缩放图片使其位于视图中间 |
centerCrop | CENTER_CROP | 缩放图片使其充满视图(超出部分会被裁剪),并位于视图中间 |
centerInside | CENTER_INSIDE | 保持宽高比例,缩小图片使之位于视图中间(只缩小不放大) |
center | CENTER | 保持图片原始尺寸,并使其位于视图中间 |
fitXY | FIT_XY | 缩放图片使其正好填满视图(图片可能被拉伸变形) |
fitStart | FIT_START | 保持宽高比例,缩放图片使其位于视图上方或左侧 |
fitEnd | FIT_END | 保持宽高比例,缩放图片使其位于视图下方或右侧 |
注意,fitCenter是默认的缩放类型 ,它的图像效果如之前的图所示。其余缩放类型的图像显示效果分别不同,具体完整代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".activity_image_scale">
<ImageView
android:id="@+id/iv_scale"
android:layout_width="match_parent"
android:layout_height="220dp"
android:layout_marginTop="5dp"
android:src="@drawable/apple" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/btn_fitCenter"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="fitCenter"
android:textColor="#000000"
android:textSize="14sp" />
<Button
android:id="@+id/btn_centerCrop"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="centerCrop"
android:textColor="#000000"
android:textSize="14sp" />
<Button
android:id="@+id/btn_centerInside"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="centerInside"
android:textColor="#000000"
android:textSize="14sp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:orientation="horizontal">
<Button
android:id="@+id/btn_center"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="center"
android:textColor="#000000"
android:textSize="14sp" />
<Button
android:id="@+id/btn_fitXY"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="fitXY"
android:textColor="#000000"
android:textSize="14sp" />
<Button
android:id="@+id/btn_fitStart"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="fitStart"
android:textColor="#000000"
android:textSize="14sp" />
<Button
android:id="@+id/btn_fitEnd"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="fitEnd"
android:textColor="#000000"
android:textSize="14sp" />
</LinearLayout>
</LinearLayout>
package com.example.myapplication;
import android.os.Bundle;
import android.view.View;
import android.widget.ImageView;
import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
public class activity_image_scale extends AppCompatActivity implements View.OnClickListener {
private ImageView iv_scale; // 声明一个图像视图的对象
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_image_scale);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
// 从布局文件中获取名叫iv_scale的图像视图
iv_scale = findViewById(R.id.iv_scale);
// 下面通过七个按钮,分别演示不同缩放类型的图片缩放效果
findViewById(R.id.btn_center).setOnClickListener(this);
findViewById(R.id.btn_fitCenter).setOnClickListener(this);
findViewById(R.id.btn_centerCrop).setOnClickListener(this);
findViewById(R.id.btn_centerInside).setOnClickListener(this);
findViewById(R.id.btn_fitXY).setOnClickListener(this);
findViewById(R.id.btn_fitStart).setOnClickListener(this);
findViewById(R.id.btn_fitEnd).setOnClickListener(this);
}
@Override
public void onClick(View v) {
if (v.getId() == R.id.btn_center) {
// 将缩放类型设置为“按照原尺寸居中显示”
iv_scale.setScaleType(ImageView.ScaleType.CENTER);
} else if (v.getId() == R.id.btn_fitCenter) {
// 将缩放类型设置为“保持宽高比例,缩放图片使其位于视图中间”
iv_scale.setScaleType(ImageView.ScaleType.FIT_CENTER);
} else if (v.getId() == R.id.btn_centerCrop) {
// 将缩放类型设置为“缩放图片使其充满视图,并位于视图中间”
iv_scale.setScaleType(ImageView.ScaleType.CENTER_CROP);
} else if (v.getId() == R.id.btn_centerInside) {
// 将缩放类型设置为“保持宽高比例,缩小图片使之位于视图中间(只缩小不放大)”
iv_scale.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
} else if (v.getId() == R.id.btn_fitXY) {
// 将缩放类型设置为“缩放图片使其正好填满视图(图片可能被缩放变形)”
iv_scale.setScaleType(ImageView.ScaleType.FIT_XY);
} else if (v.getId() == R.id.btn_fitStart) {
// 将缩放类型设置为“保持宽高比例,缩放图片使其位于视图上方或左侧”
iv_scale.setScaleType(ImageView.ScaleType.FIT_START);
} else if (v.getId() == R.id.btn_fitEnd) {
// 将缩放类型设置为“保持宽高比例,缩放图片使其位于视图下方或右侧”
iv_scale.setScaleType(ImageView.ScaleType.FIT_END);
}
}
}
1.5.2 图像按钮 ImageButton
常见的按钮控件Button其实是文本按钮,因为按钮上面只能显示文字,不能显示图片,ImageButton才是显示图片的图像按钮。虽然ImageButton号称图像按钮,但它并非继承自Button,而是继承了ImageView。所以凡是ImageView拥有的属性和方法,ImageButton统统拿了过来,区别在于ImageButton有个按钮背景。
尽管ImageButton源自ImageView,但它毕竟是个按钮,按钮家族常用的点击事件和长按事件,ImageButton全都没落下。不过ImageButton和Button之间除了名称不同外,还有下列差异:
● Button既可显示文本也可显示图片(通过setBackgroundResource方法设置背景图片),而ImageButton只能显示图片不能显示文本。
● ImageButton上的图像可按比例缩放,而Button通过背景设置的图像会拉伸变形,因为背景图采取fitXY方式,无法按比例缩放。
● Button只能靠背景显示一张图片,而ImageButton可分别在前景和背景显示图片,从而实现两张图片叠加的效果。
从上面可以看出,Button与ImageButton各有千秋,通常情况使用Button就够用了。但在某些场合,比如输入法打不出来的字符,以及特殊字体显示的字符串,就适合先切图再放到ImageButton。举个例子,数学常见的开方运算,由输入法打出来的运算符为“√”,但该符号缺少右上角的一横,正确的开方符号是带横线的√▔,此时便需要通过ImageButton显示这个开方图片。
不过使用ImageButton得注意,图像按钮默认的缩放类型为center(保持原始尺寸不缩放图片),而非图像视图默认的fitCenter,倘若图片尺寸较大,那么图像按钮将无法显示整个图片。为避免显示不完整的情况,XML文件中的ImageButton标签必须指定fitCenter的缩放类型,详细的标签内容示例如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".activity_image_button">
<ImageView
android:layout_width="match_parent"
android:layout_height="80dp"
android:src="@drawable/sqrt"
android:scaleType="fitCenter" />
</LinearLayout>
运行测试App,打开的演示界面如图所示,可见图像按钮正确展示了开方符号。
1.5.3 同时展示文本与图像
现在有了Button可在按钮上显示文字,又有ImageButton可在按钮上显示图像,照理说绝大多数场合都够用了。然而现实项目中的需求往往捉摸不定,例如客户要求在按钮文字的左边加一个图标,这样按钮内部既有文字又有图片,乍看之下Button和ImageButton都没法直接使用。若用LinearLayout对ImageView和TextView组合布局,虽然可行,但XML文件却变得冗长许多。
其实有个既简单又灵活的办法,要想在文字周围放置图片,使用Button就能实现。Button提供了几个与图标有关的属性,通过这些属性即可指定文字旁边的图标。相关的图标属性说明如下:
● drawableTop:指定文字上方的图片。
● drawableBottom:指定文字下方的图片。
● drawableLeft:指定文字左边的图片。
● drawableRight:指定文字右边的图片。
● drawablePadding:指定图片与文字的间距。
譬如下面是个既有文字又有图标的Button标签例子:
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_gravity="center"
android:drawableTop="@drawable/ic_about"
android:drawablePadding="5dp"
android:text="图标在上"
android:textColor="#000000"
android:textSize="17sp" />
以上的Button标签通过属性android:drawableTop设置了文字上边的图标,若想变更图标所处的位置,只要把drawableTop换成对应方向的属性即可。各方向的图文混排按钮效果分别如图所示。
1.6 实战项目:计算器
本章虽然只学了一些Android的简单控件,但是只要活学善用这些布局和控件,也能够做出实用的App。接下来让我们尝试设计并实现一个简单计算器。
1.6.1 需求描述
计算器是人们日常生活中最常用的工具之一,无论是在计算机上还是在手机上,都少不了计算器的身影。以Windows系统自带的计算器为例,它的界面简洁且十分实用,如图所示。
计算器的界面分为两大部分:第一部分是上方的计算表达式,既包括用户的按键输入,也包括计算结果数字;第二部分是下方的各个按键,包括从0到9的数字按键、加减乘除与等号、正负号按键、小数点按键、求倒数按键、平方按键、开方按键,以及退格、清空、取消等控制按键。通过这些按键操作,能够实现整数和小数的四则运算,以及求倒数、求平方、求开方等简单运算。
1.6.2 界面设计
上一小节介绍的Windows计算器,它主要由上半部分的计算结果与下半部分的计算按键两块区域组成,据此可创建一个界面相似的计算器App,同样由计算结果和计算按键两部分组成,如图所示。
按照计算器App的效果图,大致分布着下列Android控件:
● 线性布局LinearLayout:因为计算器界面整体从上往下布局,所以需要垂直方向的LinearLayout。
● 网格布局GridLayout:计算器下半部分的几排按键,正好成五行四列表格分布,适合采用GridLayout。
● 滚动视图ScrollView:虽然计算器界面不宽也不高,但是以防万一,最好还是加个垂直方向的ScrollView。
● 文本视图TextView:很明显顶部标题 “简单计算器” 就是TextView,且文字居中显示;标题下面的计算结果也需要使用TextView,且文字靠右靠下显示。
● 按钮Button:几乎所有的数字与运算符按键都采用了Button控件。
● 图像按钮ImageButton:开方的运算符 “√” 虽然能够打出来,但是右上角少了数学课上的一横,所以该按钮要显示一张标准的开方符号图片,需要用到ImageButton。
1.6.3 关键代码
App在同用户交互的过程中,时常要向用户反馈一些信息,例如:点错了按钮、输入了非法字符,诸如此类。对于这些一句话的提示,Android设计了Toast控件,用于展示短暂的提示文字。Toast的用法很简单,只需以下一行代码即可弹出提示窗口:
Toast.makeText(MainActivity.this,"提示文字",Toast.LENGTH_SHORT).show();
上面代码用到了两个方法,分别是makeText和show,其中show方法用来展示提示窗口,而makeText方法用来构建提示文字的模版。makeText的第一个参数为当前页面的实例,倘若当前页面名为MainActivity的话,这里就填MainActivity.this,当然如果不引发歧义的话,直接填this也可以;第二个参数为准备显示的提示文本;第三个参数规定了提示窗的驻留时长,为Toast.LENGTH_SHORT表示停留2秒后消失,为Toast.LENGTH_LONG表示停留3.5秒后消失。
对于计算器来说,有好几种情况需要提示用户,比如“除数不能为零” “开方的数值不能小于零” “不能对零求倒数” 等,这时就能通过Toast控件弹窗提醒用户。Toast弹窗的展示效果如图所示,此时App发现了除数为零的情况。
对于 简单计算来说,每次运算至少需要两个操作数,比如加减乘除四则运算就要求有两个操作数,求倒数、求平方、求开方只要求一个操作数;并且每次运算过程有且仅有一个运算符(等号不计在内),故而计算器App得事先声明下列几个字符串变量:
private String operator = ""; //运算符
private String firstNum = ""; //第一个操作数
private String secondNum = ""; //第二个操作数
private String result = ""; //当前的计算结果
用户在计算器界面每输入 一个按键,App都要进行下列两项操作:
1. 输入按键的合法性校验
在开展计算之前,务必检查用户输入的按键是否合法,因为非法按键将导致不能正常运算。合法的按键输入包括但不限于下列情况:
(1)除数不能为零。
(2)开方的数值不能小于零。
(3)不能对零求倒数。
(4)一个数字不能有两个小数点。
(5)如果没输入运算符,就不能点击等号按钮。
(6)如果没输入操作符,也不能点击等号按钮。
比如点击等号按钮之时,App的逻辑校验代码示例如下:
if (v.getId() == R.id.btn_equal) { // 点击了等号按钮
if (operator.equals("")) { // 无运算符
Toast.makeText(this, "请输入运算符", Toast.LENGTH_SHORT).show();
return false;
}
if (firstNum.equals("") || secondNum.equals("")) { // 无操作数
Toast.makeText(this, "请输入数字", Toast.LENGTH_SHORT).show();
return false;
}
if (operator.equals("÷") && Double.parseDouble(secondNum) == 0) { // 除数为零
Toast.makeText(this, "除数不能为零", Toast.LENGTH_SHORT).show();
return false;
}
}
2. 执行运算并显示计算结果
合法性校验通过,方能继续接下来的业务逻辑,倘若用户本次未输入与计算有关的按钮(例如等号、求倒数、求平方、求开方),则计算器只需拼接操作数或者运算符;倘若用户本次输入了与计算有关的按钮(例如等号、求倒数、求平方、求开方),则计算器立即执行运算操作并显示计算结果。以加减乘除四则运算为例,它们的计算代码例子如下:
// 加减乘除四则运算,返回计算结果
private double calculateFour() {
double calculate_result = 0;
if (operator.equals("+")) { // 当前是相加运算
calculate_result = Double.parseDouble(firstNum) + Double.parseDouble(secondNum);
} else if (operator.equals("-")) { // 当前是相减运算
calculate_result = Double.parseDouble(firstNum) - Double.parseDouble(secondNum);
} else if (operator.equals("×")) { // 当前是相乘运算
calculate_result = Double.parseDouble(firstNum) * Double.parseDouble(secondNum);
} else if (operator.equals("÷")) { // 当前是相除运算
calculate_result = Double.parseDouble(firstNum) / Double.parseDouble(secondNum);
}
Log.d(TAG, "calculate_result=" + calculate_result); // 把运算结果打印到日志中
return calculate_result;
}
完成合法性校验与运算处理之后,计算器App的编码基本结束了。运算计算器App,执行各种运算的界面效果如两图所示。其中图一为执行乘法运算8*9=?的计算器界面,图二为先对8做开方再给开方结果加上60的计算器界面。
完整代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#eeeeee"
android:padding="5dp"
tools:context=".activity_calculator">
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="360dp"
android:layout_height="wrap_content"
android:gravity="center"
android:text="简单计算器"
android:textColor="#000000"
android:textSize="20sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/tv_result"
android:layout_width="360dp"
android:layout_height="wrap_content"
android:background="#ffffff"
android:gravity="right|bottom"
android:lines="3"
android:maxLines="3"
android:scrollbars="vertical"
android:text="0"
android:textColor="#000000"
android:textSize="25sp" />
</LinearLayout>
<GridLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:columnCount="4">
<Button
android:id="@+id/btn_cancel"
android:width="90dp"
android:height="75dp"
android:gravity="center"
android:text="CE"
android:textColor="@color/black"
android:textSize="30sp" />
<Button
android:id="@+id/btn_divide"
android:layout_width="90dp"
android:layout_height="75dp"
android:gravity="center"
android:text="÷"
android:textColor="@color/black"
android:textSize="30sp" />
<Button
android:id="@+id/btn_multiply"
android:layout_width="90dp"
android:layout_height="75dp"
android:gravity="center"
android:text="×"
android:textColor="@color/black"
android:textSize="30sp" />
<Button
android:id="@+id/btn_clear"
android:layout_width="90dp"
android:layout_height="75dp"
android:gravity="center"
android:text="C"
android:textColor="@color/black"
android:textSize="30sp" />
<Button
android:id="@+id/btn_seven"
android:layout_width="90dp"
android:layout_height="75dp"
android:gravity="center"
android:text="7"
android:textColor="@color/black"
android:textSize="30sp" />
<Button
android:id="@+id/btn_eight"
android:layout_width="90dp"
android:layout_height="75dp"
android:gravity="center"
android:text="8"
android:textColor="@color/black"
android:textSize="30sp" />
<Button
android:id="@+id/btn_nine"
android:layout_width="90dp"
android:layout_height="75dp"
android:gravity="center"
android:text="9"
android:textColor="@color/black"
android:textSize="30sp" />
<Button
android:id="@+id/btn_plus"
android:layout_width="90dp"
android:layout_height="75dp"
android:gravity="center"
android:text="+"
android:textColor="@color/black"
android:textSize="30sp" />
<Button
android:id="@+id/btn_four"
android:layout_width="90dp"
android:layout_height="75dp"
android:gravity="center"
android:text="4"
android:textColor="@color/black"
android:textSize="30sp" />
<Button
android:id="@+id/btn_five"
android:layout_width="90dp"
android:layout_height="75dp"
android:gravity="center"
android:text="5"
android:textColor="@color/black"
android:textSize="30sp" />
<Button
android:id="@+id/btn_six"
android:layout_width="90dp"
android:layout_height="75dp"
android:gravity="center"
android:text="6"
android:textColor="@color/black"
android:textSize="30sp" />
<Button
android:id="@+id/btn_minus"
android:layout_width="90dp"
android:layout_height="75dp"
android:gravity="center"
android:text="-"
android:textColor="@color/black"
android:textSize="30sp" />
<Button
android:id="@+id/btn_one"
android:layout_width="90dp"
android:layout_height="75dp"
android:gravity="center"
android:text="1"
android:textColor="@color/black"
android:textSize="30sp" />
<Button
android:id="@+id/btn_two"
android:layout_width="90dp"
android:layout_height="75dp"
android:gravity="center"
android:text="2"
android:textColor="@color/black"
android:textSize="30sp" />
<Button
android:id="@+id/btn_three"
android:layout_width="90dp"
android:layout_height="75dp"
android:gravity="center"
android:text="3"
android:textColor="@color/black"
android:textSize="30sp" />
<ImageButton
android:id="@+id/ib_sqrt"
android:layout_width="90dp"
android:layout_height="75dp"
android:scaleType="centerInside"
android:src="@drawable/sqrt" />
<Button
android:id="@+id/btn_reciprocal"
android:layout_width="90dp"
android:layout_height="75dp"
android:gravity="center"
android:text="1/x"
android:textColor="@color/black"
android:textSize="30sp" />
<Button
android:id="@+id/btn_zero"
android:layout_width="90dp"
android:layout_height="75dp"
android:gravity="center"
android:text="0"
android:textColor="@color/black"
android:textSize="30sp" />
<Button
android:id="@+id/btn_dot"
android:layout_width="90dp"
android:layout_height="75dp"
android:gravity="center"
android:text="."
android:textColor="@color/black"
android:textSize="30sp" />
<Button
android:id="@+id/btn_equal"
android:layout_width="90dp"
android:layout_height="75dp"
android:gravity="center"
android:text="="
android:textColor="@color/black"
android:textSize="30sp" />
</GridLayout>
</LinearLayout>
</ScrollView>
</LinearLayout>
package com.example.myapplication;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
public class activity_calculator extends AppCompatActivity implements View.OnClickListener {
private final static String TAG = "CalculatorActivity";
private TextView tv_result; // 声明一个文本视图对象
private String operator = ""; // 运算符
private String firstNum = ""; // 第一个操作数
private String secondNum = ""; // 第二个操作数
private String result = ""; // 当前的计算结果
private String showText = ""; // 显示的文本内容
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_calculator);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
// 从布局文件中获取名叫tv_result的文本视图
tv_result = findViewById(R.id.tv_result);
// 下面给每个按钮控件都注册了点击监听器
findViewById(R.id.btn_cancel).setOnClickListener(this); // “取消”按钮
findViewById(R.id.btn_divide).setOnClickListener(this); // “除法”按钮
findViewById(R.id.btn_multiply).setOnClickListener(this); // “乘法”按钮
findViewById(R.id.btn_clear).setOnClickListener(this); // “清除”按钮
findViewById(R.id.btn_seven).setOnClickListener(this); // 数字7
findViewById(R.id.btn_eight).setOnClickListener(this); // 数字8
findViewById(R.id.btn_nine).setOnClickListener(this); // 数字9
findViewById(R.id.btn_plus).setOnClickListener(this); // “加法”按钮
findViewById(R.id.btn_four).setOnClickListener(this); // 数字4
findViewById(R.id.btn_five).setOnClickListener(this); // 数字5
findViewById(R.id.btn_six).setOnClickListener(this); // 数字6
findViewById(R.id.btn_minus).setOnClickListener(this); // “减法”按钮
findViewById(R.id.btn_one).setOnClickListener(this); // 数字1
findViewById(R.id.btn_two).setOnClickListener(this); // 数字2
findViewById(R.id.btn_three).setOnClickListener(this); // 数字3
findViewById(R.id.btn_reciprocal).setOnClickListener(this); // 求倒数按钮
findViewById(R.id.btn_zero).setOnClickListener(this); // 数字0
findViewById(R.id.btn_dot).setOnClickListener(this); // “小数点”按钮
findViewById(R.id.btn_equal).setOnClickListener(this); // “等号”按钮
findViewById(R.id.ib_sqrt).setOnClickListener(this); // “开平方”按钮
}
private boolean verify(View v) {
if (v.getId() == R.id.btn_cancel) { // 点击了取消按钮
if (operator.equals("") && (firstNum.equals("") || firstNum.equals("0"))) { // 无运算符,则表示逐位取消第一个操作数
Toast.makeText(this, "没有可取消的数字了", Toast.LENGTH_SHORT).show();
return false;
}
if (!operator.equals("") && secondNum.equals("")) { // 有运算符,则表示逐位取消第二个操作数
Toast.makeText(this, "没有可取消的数字了", Toast.LENGTH_SHORT).show();
return false;
}
} else if (v.getId() == R.id.btn_equal) { // 点击了等号按钮
if (operator.equals("")) { // 无运算符
Toast.makeText(this, "请输入运算符", Toast.LENGTH_SHORT).show();
return false;
}
if (firstNum.equals("") || secondNum.equals("")) { // 无操作数
Toast.makeText(this, "请输入数字", Toast.LENGTH_SHORT).show();
return false;
}
if (operator.equals("÷") && Double.parseDouble(secondNum) == 0) { // 除数为零
Toast.makeText(this, "除数不能为零", Toast.LENGTH_SHORT).show();
return false;
}
} else if (v.getId() == R.id.btn_plus || v.getId() == R.id.btn_minus // 点击了加、减、乘、除按钮
|| v.getId() == R.id.btn_multiply || v.getId() == R.id.btn_divide) {
if (firstNum.equals("")) { // 缺少第一个操作数
Toast.makeText(this, "请输入数字", Toast.LENGTH_SHORT).show();
return false;
}
if (!operator.equals("")) { // 已有运算符
Toast.makeText(this, "请输入数字", Toast.LENGTH_SHORT).show();
return false;
}
} else if (v.getId() == R.id.ib_sqrt) { // 点击了开根号按钮
if (firstNum.equals("")) { // 缺少底数
Toast.makeText(this, "请输入数字", Toast.LENGTH_SHORT).show();
return false;
}
if (Double.parseDouble(firstNum) < 0) { // 不能对负数开平方
Toast.makeText(this, "开根号的数值不能小于零", Toast.LENGTH_SHORT).show();
return false;
}
} else if (v.getId() == R.id.btn_reciprocal) { // 点击了求倒数按钮
if (firstNum.equals("")) { // 缺少底数
Toast.makeText(this, "请输入数字", Toast.LENGTH_SHORT).show();
return false;
}
if (Double.parseDouble(firstNum) == 0) { // 不能对零求倒数
Toast.makeText(this, "不能对零求倒数", Toast.LENGTH_SHORT).show();
return false;
}
} else if (v.getId() == R.id.btn_dot) { // 点击了小数点
if (operator.equals("") && firstNum.contains(".")) { // 无运算符,则检查第一个操作数是否已有小数点
Toast.makeText(this, "一个数字不能有两个小数点", Toast.LENGTH_SHORT).show();
return false;
}
if (!operator.equals("") && secondNum.contains(".")) { // 有运算符,则检查第二个操作数是否已有小数点
Toast.makeText(this, "一个数字不能有两个小数点", Toast.LENGTH_SHORT).show();
return false;
}
}
return true;
}
@Override
public void onClick(View v) {
if (!verify(v)) { // 未通过合法性校验,直接返回
return;
}
String inputText;
if (v.getId() == R.id.ib_sqrt) { // 如果是开根号按钮
inputText = "√";
} else { // 除了开根号之外的其他按钮
inputText = ((TextView) v).getText().toString();
}
Log.d(TAG, "inputText=" + inputText);
if (v.getId() == R.id.btn_clear) { // 点击了清除按钮
clear();
} else if (v.getId() == R.id.btn_cancel) { // 点击了取消按钮
if (operator.equals("")) { // 无运算符,则表示逐位取消第一个操作数
if (firstNum.length() == 1) {
firstNum = "0";
} else if (firstNum.length() > 1) {
firstNum = firstNum.substring(0, firstNum.length() - 1);
}
refreshText(firstNum);
} else { // 有运算符,则表示逐位取消第二个操作数
if (secondNum.length() == 1) {
secondNum = "";
} else if (secondNum.length() > 1) {
secondNum = secondNum.substring(0, secondNum.length() - 1);
}
refreshText(showText.substring(0, showText.length() - 1));
}
} else if (v.getId() == R.id.btn_plus || v.getId() == R.id.btn_minus // 点击了加、减、乘、除按钮
|| v.getId() == R.id.btn_multiply || v.getId() == R.id.btn_divide) {
operator = inputText; // 运算符
refreshText(showText + operator);
} else if (v.getId() == R.id.btn_equal) { // 点击了等号按钮
double calculate_result = calculateFour(); // 加减乘除四则运算
refreshOperate(String.valueOf(calculate_result));
refreshText(showText + "=" + result);
} else if (v.getId() == R.id.ib_sqrt) { // 点击了开根号按钮
double calculate_result = Math.sqrt(Double.parseDouble(firstNum)); // 开平方运算
refreshOperate(String.valueOf(calculate_result));
refreshText(showText + "√=" + result);
} else if (v.getId() == R.id.btn_reciprocal) { // 点击了求倒数按钮
double calculate_result = 1.0 / Double.parseDouble(firstNum); // 求倒数运算
refreshOperate(String.valueOf(calculate_result));
refreshText(showText + "/=" + result);
} else { // 点击了其他按钮,包括数字和小数点
if (result.length() > 0 && operator.equals("")) { // 上次的运算结果已经出来了
clear();
}
if (operator.equals("")) { // 无运算符,则继续拼接第一个操作数
firstNum = firstNum+inputText;
} else { // 有运算符,则继续拼接第二个操作数
secondNum = secondNum + inputText;
}
if (showText.equals("0") && !inputText.equals(".")) { // 整数不需要前面的0
refreshText(inputText);
} else {
refreshText(showText + inputText);
}
}
}
// 刷新运算结果
private void refreshOperate(String new_result) {
result = new_result;
firstNum = result;
secondNum = "";
operator = "";
}
// 刷新文本显示
private void refreshText(String text) {
showText = text;
tv_result.setText(showText);
}
// 清空并初始化
private void clear() {
refreshOperate("");
refreshText("");
}
// 加减乘除四则运算,返回计算结果
private double calculateFour() {
double calculate_result = 0;
if (operator.equals("+")) { // 当前是相加运算
calculate_result = Double.parseDouble(firstNum) + Double.parseDouble(secondNum);
} else if (operator.equals("-")) { // 当前是相减运算
calculate_result = Double.parseDouble(firstNum) - Double.parseDouble(secondNum);
} else if (operator.equals("×")) { // 当前是相乘运算
calculate_result = Double.parseDouble(firstNum) * Double.parseDouble(secondNum);
} else if (operator.equals("÷")) { // 当前是相除运算
calculate_result = Double.parseDouble(firstNum) / Double.parseDouble(secondNum);
}
Log.d(TAG, "calculate_result=" + calculate_result); // 把运算结果打印到日志中
return calculate_result;
}
}