安卓编程初学者手册第三版-四-

55 阅读54分钟

安卓编程初学者手册第三版(四)

原文:zh.annas-archive.org/md5/ceefdd89e585c59c20db6a7760dc11f1

译者:飞龙

协议:CC BY-NC-SA 4.0

第十二章:栈、堆和垃圾收集器

在本章结束时,Java 和我们的 XML 布局之间的缺失链接将被完全揭示,让我们有能力像以前一样向我们的应用程序添加各种小部件,但这一次,我们将能够通过我们的 Java 代码来控制它们。

在本章中,我们将控制一些简单的 UI 元素,如ButtonTextView,在下一章中,我们将进一步操作一系列 UI 元素。

为了让我们理解发生了什么,我们需要更多地了解 Android 设备的内存以及其中的两个区域 -

在本章中,我们将学习以下内容:

  • Android UI 元素是类

  • 垃圾回收

  • 我们的 UI 在堆上?

  • 特殊类型的类,包括内部和匿名

回到那个新闻快讯。

技术要求

您可以在 GitHub 上找到本章的代码文件github.com/PacktPublishing/Android-Programming-for-Beginners-Third-Edition/tree/main/chapter%2012

所有的 Android UI 元素也是类

当我们的应用程序运行并且从onCreate方法中调用setContentView方法时,布局从 XML 中inflated,UI 类的实例被加载到内存中作为可用对象。它们存储在一个称为堆的内存部分中。堆由Android RuntimeART)系统管理。

重新介绍引用

但是所有这些 UI 对象/类在哪里?我们在代码中肯定看不到它们。我们怎么能得到它们?

每个 Android 设备内部的 ART 负责为我们的应用程序分配内存。此外,它将不同类型的变量存储在不同的位置。

我们在方法中声明和初始化的变量存储在被称为的内存区域。在谈论栈时,我们可以继续使用我们现有的仓库类比。我们已经知道如何使用简单的表达式在栈上操作变量。所以,让我们谈谈堆和那里存储的东西。

重要事实

所有类的对象都是引用类型变量,只是指向存储在堆上的实际对象的引用 - 它们不是实际的对象。

把堆想象成同一个仓库的另一个区域。堆有大量的地板空间用于奇形怪状的对象,用于较小对象的货架,有很多长排有较小尺寸的小隔间等等。这就是对象存储的地方。问题是我们无法直接访问堆。把它想象成仓库的受限区域。你实际上不能去那里,但你可以引用那里存储的东西。让我们看看引用变量到底是什么。

这是我们通过引用引用和使用的变量。引用可以宽松但有用地定义为地址或位置。对象的引用(地址或位置)在栈上。

因此,当我们使用点运算符时,我们要求 Android 在一个特定的位置执行任务,这个位置存储在引用中。

为什么我们要一个这样的系统?直接把我的对象放在栈上。原因如下。

快速休息一下,扔掉垃圾

这就是整个栈和堆的作用。

正如我们所知,ART 系统为我们跟踪所有的对象,并将它们存储在我们仓库的一个专门区域,称为堆。在我们的应用程序运行时,ART 会定期扫描栈,我们仓库的常规货架,并匹配指向堆上对象的引用。它发现没有匹配引用的对象,就会销毁它。或者用 Java 术语来说,它进行垃圾回收

想象一辆非常挑剔的垃圾车驶过我们堆的中间,扫描对象以匹配引用(在栈上)。没有引用?你现在是垃圾。

如果一个对象没有引用变量,我们无法做任何事情,因为我们无法访问它。垃圾收集系统通过释放未使用的内存帮助我们的应用程序更有效地运行。

如果这个任务留给我们,我们的应用程序将更加复杂。

因此,在方法中声明的变量是局部变量,位于堆栈上,并且仅在它们声明的方法内部可见。成员变量(在对象中)位于堆上,并且可以在任何有引用到它的地方引用它,并且访问规范(封装)允许。

关于堆栈和堆的七个事实

让我们快速看一下我们对堆栈和堆的了解:

  1. 局部变量和方法位于堆栈上,局部变量局限于它们声明的特定方法。

  2. 实例/类变量在堆上(与它们的对象一起),但对对象的引用(其地址)是堆栈上的局部变量。

  3. 我们控制堆栈上的内容。我们可以使用堆上的对象,但只能通过引用它们。

  4. 垃圾收集器通过清除和更新堆来保持堆的清晰。

  5. 您不会删除对象,但 ART 系统在认为适当时会发送垃圾收集器。当不再有有效引用时,对象将被垃圾收集。因此,当引用变量(局部或实例)从堆栈中移除时,其相关对象变得可供垃圾收集。当 ART 系统决定时机合适(通常非常迅速),它将释放 RAM 内存以避免耗尽。

  6. 如果我们尝试引用一个不存在的对象,我们将得到一个NullPointerException,应用程序将崩溃。

让我们继续看看这些信息对我们控制 UI 有什么好处。

那么这个堆的东西如何帮助我呢?

在 XML 布局中设置了id属性的任何 UI 元素都可以使用findViewById方法从堆中检索到其引用,该方法是 Activity/AppCompatActivity类的一部分。由于它是我们在所有应用程序中扩展的类的一部分,因此我们可以访问它,正如这段代码所示:

Button myButton = findViewById(R.id.myButton);

上述代码假设在 XML 布局中有一个Button小部件,其id属性设置为myButtonmyButton对象现在直接引用 XML 布局中id属性设置为myButton的小部件。

请注意,findViewById方法也是多态的,任何扩展View类的类都可以从 UI 中检索,并且碰巧UI面板中的所有内容都扩展了View

敏锐的读者可能会注意到,我们在检索从抽象Animal继承的Elephant实例时,不使用强制转换来确保我们得到一个Button对象(而不是TextView或其他View后代):

someElephant = (Elephant) feed(someElephant);

这是因为View类使用了 Java 的泛型自动类型推断。这是一个我们不会在本书中涵盖的高级主题,但它意味着强制转换是自动的,我们不需要编写更多的冗长代码,如下所示:

Button myButton = (Button) findViewById(R.id.myButton);

抓取 UI 中任何东西的引用的能力令人兴奋,因为这意味着我们可以开始使用这些对象具有的所有方法。以下是一些我们可以用于Button对象的方法的示例:

myButton.setText
myButton.setHeight
myButton.setOnCLickListener
myButton.setVisibility

重要提示

Button类本身有大约 50 个方法!

如果您认为在经过 11 章之后,我们终于要开始在 Android 上做一些有趣的事情,那么您是对的!

从我们的布局中使用按钮和 TextView 小部件

要跟随这个项目,创建一个新的 Android Studio 项目,称之为Java Meet UI,选择空活动模板,并将所有其他选项保持默认。通常情况下,您可以在第十二章/Java Meet UI文件夹中找到 Java 代码和 XML 布局代码。

首先,让我们按照以下步骤构建一个简单的 UI:

  1. 在 Android Studio 的编辑窗口中,切换到activity_main.xml,确保你在设计选项卡上。

  2. 删除自动生成的TextView,即那个写着“Hello world!”的。

  3. 在布局的顶部中心添加一个TextView小部件。

  4. 将其设置为040sp。注意id属性值的大小写。它有一个大写的V

  5. 现在在布局上拖放六个按钮,使其尽可能接近下一个屏幕截图。确切的布局并不重要:图 12.1 - 布局设置

图 12.1 - 布局设置

  1. 当布局设置好后,点击推断约束按钮来约束所有的 UI 项目。

  2. 依次编辑每个按钮的属性(从左到右,从上到下),并按照下表中所示设置textid属性。表后面的图片应该清楚地显示了哪个按钮有哪些值。

完成后,你的布局应该看起来像下一个屏幕截图:

图 12.2 - 最终布局

图 12.2 - 最终布局

按钮上的精确位置和文本并不是很重要,但是id属性的值必须与表中的值相同。原因是我们将使用这些 ID 来从我们的 Java 代码中获取对这个布局中的按钮和TextView的引用。

切换到编辑器中的MainActivity.java选项卡,我们将编写代码。

修改这一行:

public class MainActivity extends AppCompatActivity{
to
public class MainActivity extends AppCompatActivity implements
   View.OnClickListener{

提示

你需要导入View类。确保在继续下一步之前做到这一点,否则你会得到混乱的结果。

import android.view.View;

注意我们刚刚修改的整行都被下划线标出了错误。现在,因为我们已经将MainActivity转换为OnClickListener,通过将其添加为一个接口,我们必须实现OnClickListener所需的抽象方法。这个方法叫做onClick。当我们添加onClick方法时,错误就会消失。

我们可以让 Android Studio 为我们添加它,方法是在带有错误的行的任何地方左键单击,然后使用键盘组合Alt + Enter。左键单击实现方法选项,如下一个屏幕截图所示。

图 12.3 - 实现方法

图 12.3 - 实现方法

现在,左键单击onClick方法。错误已经消失,我们可以继续添加代码。我们还有一个空的onClick方法,很快我们将看到我们将如何处理它。

现在我们将声明一个名为valueint类型变量,并将其初始化为0。我们还将声明六个Button对象和一个TextView对象。我们将给它们与我们 UI 布局中的id属性值完全相同的 Java 变量名。这种名称关联并不是必需的,但它对于跟踪我们的 Java 代码中的哪个Button将持有对我们的 XML UI 布局中的哪个Button的引用是有用的。

此外,我们将它们全部声明为private访问权限,因为我们知道它们在这个类之外不会被需要。

在继续输入代码之前,请注意所有这些变量都是MainActivity类的成员。这意味着我们在上一步修改的类声明之后立即输入所有下面显示的代码。

将所有这些变量都变成成员/字段意味着它们具有类范围,我们可以在MainActivity类的任何地方访问它们。这对这个项目非常重要,因为我们需要在onCreate方法和我们的新onClick方法中使用它们。

MainActivity类的开头大括号{后和onCreate方法前输入我们刚刚讨论过的下面的代码:

// An int variable to hold a value
private int value = 0;
// A bunch of Buttons and a TextView
private Button btnAdd;
private Button btnTake;
private TextView txtValue;
private Button btnGrow;
private Button btnShrink;
private Button btnReset;
private Button btnHide;

提示

记得使用ALT + Enter键盘组合来导入新的类。

import android.widget.Button;

import android.widget.TextView;

接下来,我们要准备好所有的变量以准备行动。这样做的最佳位置是onCreate方法,因为我们知道这将在应用程序显示给用户之前由 Android 调用。这段代码使用findViewById方法将我们的每个 Java 对象与 UI 布局中的一个项目关联起来。

它通过返回与堆上的 UI 小部件关联的对象的引用来实现。它“知道”我们要找的是哪一个,因为我们使用id属性值作为参数。例如,...(R.id.btnAdd)将返回我们在布局中创建的文本为ADDButton小部件。

onCreate方法中的setContentView调用后,输入以下代码:

// Get a reference to all the buttons in our UI
// Match them up to all our Button objects we declared earlier
btnAdd = findViewById(R.id.btnAdd);
btnTake = findViewById(R.id.btnTake);
txtValue = findViewById(R.id.txtValue);
btnGrow = findViewById(R.id.btnGrow);
btnShrink = findViewById(R.id.btnShrink);
btnReset = findViewById(R.id.btnReset);
btnHide = findViewById(R.id.btnHide);

现在我们已经有了对所有Button小部件和TextView小部件的引用,所以现在我们可以开始使用它们的方法。在接下来的代码中,我们使用setOnClickListener方法在每个Button引用上,使 Android 将用户的任何点击传递到我们的onClick方法。

这样做是因为当我们实现View.OnClickListener接口时,我们的MainActivity类实际上成为了一个OnClickListener

因此,我们只需依次在每个按钮上调用setOnClickListener。作为提醒,this参数是对MainActivity的引用。因此,方法调用表示:“嘿,Android,我想要一个OnClickListener,我希望它是MainActivity类。”

现在 Android 知道要在哪个类上调用onClick。如果我们没有先实现接口,下面的代码将无法工作。此外,我们必须在 Activity 启动之前设置这些监听器,这就是为什么我们在onCreate中这样做的原因。

很快,我们将添加代码到onClick方法中,以处理当按钮被点击时发生的情况,并且我们将看到如何区分所有不同的按钮。

在上一个代码之后,在onCreate方法内添加以下代码:

// Listen for all the button clicks
btnAdd.setOnClickListener(this);
btnTake.setOnClickListener(this);
txtValue.setOnClickListener(this);
btnGrow.setOnClickListener(this);
btnShrink.setOnClickListener(this);
btnReset.setOnClickListener(this);
btnHide.setOnClickListener(this);

现在滚动到 Android Studio 在我们实现OnClickListener接口后为我们添加的onClick方法。在其中添加float size;变量声明和一个空的switch块,使其看起来像下面的代码。要添加的新代码已经突出显示:

public void onClick(View view){
            // A local variable to use later
      float size;
      switch(view.getId()){

      }
}

记住,switch将检查是否有一个caseswitch语句内的条件匹配。

在上一个代码中,switch条件是view.getId()。让我们逐步解释一下。view变量是一个指向View类型对象的引用,它是由 Android 通过onClick方法传递的:

public void onClick(View view)

ViewButtonTextView等的父类。因此,或许正如我们所期望的那样,调用view.getId()将返回被点击并触发对onClick的调用的 UI 小部件的id属性。

然后我们只需要为我们想要响应的每个Button引用提供一个case语句(和适当的操作)。

接下来我们将看到的是前三个case语句。它们处理R.id.btnAddR.id.btnTakeR.id.btnReset情况:

  • R.id.btnAdd情况下的代码简单地增加了value变量,然后做了一些新的事情。

它调用txtValue对象上的setText方法。这是参数:(""+ value)。这个参数使用一个空字符串,并将存储在value中的任何值添加(连接)到其中。这会导致我们的TextView txtValue显示存储在value中的任何值。

  • R.id.btnTake)做的事情完全相同,只是从value中减去 1 而不是加 1。

  • 第三个case语句处理将value归零,并再次更新txtValuetext属性。

然后,在每个case的末尾,都有一个break语句。在这一点上,switch块被退出,onClick方法返回,生活恢复正常-直到用户的下一次点击。

在左花括号{后的switch块中输入我们刚讨论过的代码:

case R.id.btnAdd:
   value ++;
   txtValue.setText(""+ value);
   break;
case R.id.btnTake:
   value--;
   txtValue.setText(""+ value);
   break;
case R.id.btnReset:
   value = 0;
   txtValue.setText(""+ value);
   break;

下面的两个case语句处理R.id.btnGrowR.id.btnShrink。新的更有趣的是使用的两种新方法。

getTextScaleX方法返回其所用对象中文本的水平比例。我们可以看到它所用的对象是我们的TextView txtValue。代码行开头的size =将返回的值赋给我们的float变量size

在每个case语句的下一行代码改变了文本的水平比例,使用了setTextScaleX。当size + 1时,当size - 1时。

总体效果是允许这两个按钮通过每次点击来增大或缩小txtValue小部件中的文本比例 1。

输入我们刚讨论过的下面的下一个两个case语句:

case R.id.btnGrow:
   size = txtValue.getTextScaleX();
   txtValue.setTextScaleX(size + 1);
   break;
case R.id.btnShrink:
   size = txtValue.getTextScaleX();
   txtValue.setTextScaleX(size - 1);
   break;

在我们下面要编写的最后一个case语句中,我们有一个if-else块。条件需要稍微解释一下,所以让我们提前看一下:

if(txtValue.getVisibility() == View.VISIBLE)

要评估的条件是txtValue.getVisibility() == View.VISIBLE。在==运算符之前的条件的第一部分返回我们的txtValue TextViewvisibility属性。返回值将是View类中定义的三个可能的常量值之一。它们是View.VISIBLEView.INVISIBLEView.GONE

如果TextView在 UI 上对用户可见,则该方法返回View.VISIBLE,条件将被评估为true,并且if块将被执行。

if块内,我们在txtValue对象上使用setVisibility方法,并使用View.INVISIBLE参数使其对用户不可见。

除此之外,我们将btnHide小部件上的文本更改为setText方法。

if块执行完毕后,txtValue是不可见的,我们的 UI 上有一个按钮显示为if语句将为 false,else块将执行。在else块中,我们将情况反转。我们将txtValue设置回View.VISIBLE,并将btnHide上的text属性设置为HIDE

如果这有任何不清楚的地方,只需输入代码,运行应用程序,然后在看到它实际运行后再回顾这段代码和解释:

case R.id.btnHide:
   if(txtValue.getVisibility() == View.VISIBLE)
   {
         // Currently visible so hide it
         txtValue.setVisibility(View.INVISIBLE);
         // Change text on the button
         btnHide.setText("SHOW");
   }else{
         // Currently hidden so show it
         txtValue.setVisibility(View.VISIBLE);
         // Change text on the button
         btnHide.setText("HIDE");
   }
   break;

我们已经有了 UI 和代码,所以现在是时候运行应用程序,尝试所有的按钮了。

运行应用程序

以通常的方式运行应用程序。注意,value向任一方向增加或减少 1,然后在TextView小部件中显示结果。在下一个截图中,我点击了ADD按钮三次。

图 12.4  -  添加按钮示例

图 12.4 - 添加按钮示例

请注意,将value变量设置为 0,并在TextView小部件上显示它。在下一个截图中,我点击了GROW按钮八次。

图 12.5 - 增长按钮示例

图 12.5 - 增长按钮示例

最后,TextView小部件在再次点击时将其自身文本更改为TextView

提示

我不会打扰你展示一张隐藏的图片。一定要尝试这个应用程序。

请注意,在这个应用程序中不需要LogToast类,因为我们最终是使用我们的 Java 代码操纵 UI。让我们通过探索内部和匿名类来更深入地使用我们的代码操纵 UI。

内部和匿名类

在我们继续下一章并构建具有大量不同小部件的应用程序之前,这些小部件将实践和强化我们在本章学到的一切,我们将对匿名内部类进行非常简要的介绍。

当我们在*第十章**中实现了我们的基本类演示应用程序,面向对象编程时,我们在一个单独的文件中声明和实现了该类,该文件必须与我们的MainActivity类具有相同的名称。这是创建常规类的方法。

我们还可以在一个类中声明和实现其他类。除了如何我们这样做之外,当然,唯一剩下的问题是为什么我们要这样做?

当我们实现内部类时,内部类可以访问封闭类的成员变量,封闭类也可以访问内部类的成员。

这通常使我们的代码结构更加直观。因此,内部类有时是解决问题的方法。

此外,我们还可以在我们的类的方法中声明和实现一个完整的类。当我们这样做时,我们使用稍微不同的语法,并且不使用类名。这就是匿名类。

在本书的其余部分,我们将看到内部类和匿名类的实际应用,并在使用它们时进行彻底讨论。

常见问题

  1. 我并没有完全理解,实际上我现在有比章节开始时更多的问题。我该怎么办?

你已经了解了足够的面向对象编程知识,可以在 Android 和其他类型的 Java 编程中取得相当大的进步。如果你现在迫切想了解更多关于面向对象编程的知识,有很多书籍专门讨论面向对象编程。然而,实践和熟悉语法将有助于达到相同的效果,并且更有趣。这正是我们将在本书的其余部分做的事情。

总结

在本章中,我们终于让我们的代码和 UI 进行了一些真正的交互。事实证明,每当我们向我们的 UI 添加一个小部件时,我们都在添加一个我们可以在 Java 代码中引用的类的实例。所有这些对象都存储在内存的一个单独区域中,称为堆 - 以及我们自己的任何类。

现在我们可以学习并使用一些更有趣的小部件。我们将在下一章中看到大量的小部件,然后在本书的其余部分继续介绍更多新的小部件。

第十三章:匿名类-让 Android 小部件活起来

这一章本来可以被称为更多 OOP,因为匿名类仍然是这个主题的一部分。然而,正如您将看到的,匿名类为我们提供了如此多的灵活性,特别是在与用户界面UI)交互时,我认为它们值得有一章专门介绍它们及它们在 Android 中的关键用途。

现在我们已经对 Android 应用的布局和编码有了一个很好的概述,再加上我们新获得的对面向对象编程OOP)的见解,以及我们如何可以从 Java 代码中操作 UI,我们准备尝试使用调色板上的更多小部件以及匿名类。

面向对象编程有时是一个棘手的事情,匿名类对于初学者来说可能有点尴尬。然而,通过逐渐学习这些新概念,然后反复练习,随着时间的推移,它们将成为我们的朋友。

在本章中,我们将通过回到 Android Studio 调色板,查看半打我们根本没有看到过或者还没有完全使用过的小部件,来进行大量的多样化。

一旦我们做到了这一点,我们将把它们全部放入一个布局中,并用 Java 代码练习操作它们。

在本章中,我们将重点关注以下主题:

  • 声明和初始化布局小部件

  • 只使用 Java 代码创建小部件

  • EditTextImageViewRadioButton(和RadioGroup),SwitchCheckBoxTextClock小部件

  • 使用 WebView

  • 如何使用匿名类

  • 使用所有前述小部件和一些匿名类创建小部件演示迷你应用程序

让我们开始一个快速回顾。

技术要求

您可以在 GitHub 上找到本章的代码文件,网址为 github.com/PacktPublis…

声明和初始化对象

我们知道,当我们从onCreate方法调用setContentView方法时,Android 会将所有小部件和布局膨胀,并将它们转换为堆上的真正的 Java 对象。

此外,我们知道要使用堆中的小部件,我们必须首先声明正确类型的对象,然后使用它来通过其唯一的id属性获取对堆上的 UI 小部件对象的引用。

例如,我们通过id属性为txtTitleTextView小部件获取一个引用,并将其赋值给一个新的 Java 对象,称为myTextView,如下所示:

// Grab a reference to an object on the heap
TextView myTextView = findViewById(R.id.txtTitle);

现在,使用我们的myTextView实例变量,我们可以做任何TextView类设计的事情。例如,我们可以设置文本显示如下:

myTextView.setText("Hi there");

此外,我们可以按如下方式使其消失:

// Bye bye
myTextView.setVisibility(View.GONE)

我们可以再次更改它的文本并使其重新出现:

myTextView.setText("BOO!");
// Surprise
myTextView.setVisibility(View.VISIBLE)

值得一提的是,我们可以在 Java 中操纵在前几章中使用 XML 设置的任何属性。此外,我们已经暗示过-但实际上没有看到-使用纯 Java 代码从无到有地创建小部件。

在纯 Java 中创建 UI 小部件而不使用 XML

我们还可以从不是指向布局中对象的 Java 对象创建小部件。我们可以在代码中声明、实例化和设置小部件的属性,如下所示:

Button myButton = new Button();

上述代码通过使用new()关键字创建了一个新的Button。唯一的注意事项是Button必须是布局的一部分,才能被用户看到。因此,我们可以从 XML 布局中获取对布局元素的引用,或者在代码中创建一个新的布局。

如果我们假设我们的 XML 中有一个id属性等于linearLayout1LinearLayout,我们可以将我们之前代码行中的Button放入其中,如下所示:

// Get a reference to the LinearLayout
LinearLayout linearLayout = (LinearLayout)
   findViewById(R.id.linearLayout);
// Add our Button to it
linearLayout.addView(myButton);

我们甚至可以通过纯 Java 代码创建一个完整的布局,首先创建一个新布局,然后创建我们想要添加的所有小部件。最后,我们在具有我们小部件的布局上调用setContentView方法。

在下面的代码中,我们用纯 Java 创建了一个布局,尽管它非常简单,只有一个LinearLayout里面有一个Button

// Create a new LinearLayout
LinearLayout linearLayout = new LinearLayout();
// Create a new Button
Button myButton = new Button();
// Add myButton to the LinearLayout
linearLayout.addView(myButton);
// Make the LinearLayout the main view
setContentView(linearLayout);

显而易见的是,仅使用 Java 设计详细和细致的布局会更加麻烦,更难以可视化,并且通常不是通常的做法。然而,有时我们会发现以这种方式做事情是有用的。

我们现在已经相当高级了,涉及到布局和小部件。然而,很明显,调色板中还有许多其他小部件,我们尚未探索或与之交互。所以,让我们解决这个问题。

探索调色板 – 第一部分

让我们快速浏览一下调色板中以前未探索/未使用的一些项目。然后,我们可以将它们拖放到布局中,查看它们可能有用的一些方法。然后,我们可以实现一个项目来使用它们。

我们在上一章已经探索了ButtonTextView小部件。让我们更仔细地看看一些其他小部件。

EditText 小部件

EditText小部件就像其名称所示。如果我们向用户提供EditText小部件,他们确实可以在其中编辑 文本。我们在早期的章节中已经看过这个了;然而,我们实际上并没有做任何事情。我们没有探索的是如何捕获其中的信息,或者我们将在哪里输入这个捕获文本的代码。

以下代码块假设我们已经声明了一个EditText类型的对象,并使用它来获取我们 XML 布局中的EditText小部件的引用。例如,我们可能会为按钮点击编写类似以下代码的代码,例如,表单的提交按钮。但是,它可以放在我们认为对我们的应用程序必要的任何地方:

String editTextContents = editText.getText()
// editTextContents now contains whatever the user entered

我们将在下一个迷你应用程序中在实际环境中使用它。

ImageView 小部件

到目前为止,我们已经几次将图像放到我们的布局中,但以前从未从我们的 Java 代码中获取引用或对其进行任何操作。获取对ImageView小部件的引用的过程与任何其他小部件相同:

  • 声明一个对象。

  • 使用findViewById方法和有效的id属性获取引用,例如以下内容:

ImageView imageView = findViewById(R.id.imageView);

然后,我们可以使用类似以下的代码对我们的图像做一些相当不错的事情:

// Make the image 50% TRANSPARENT
imageView.setAlpha(.5f);

重要提示

看起来奇怪的f只是让编译器知道值是float类型,这是setAlpha方法所需的。

在上述代码中,我们在imageView上使用了setAlpha方法。setAlpha方法接受一个介于 0 和 1 之间的值。完全透明的图像为 0,而完全不透明的图像为 1。

提示

还有一个重载的setAlpha方法,它接受一个从 0(完全透明)到 255(不透明)的整数值。我们可以在需要时选择最合适的方法。如果您想了解方法重载的内容,请参考第九章学习 Java 方法

我们将在下一个应用程序中使用ImageView类的一些方法。

单选按钮和组

当用户需要选择两个或更多互斥的选项时,会使用RadioButton小部件。这意味着当选择一个选项时,其他选项就不会被选择,就像在老式收音机上一样。请看下面截图中带有几个RadioButton小部件的简单RadioGroup

图 13.1 – RadioButton 小部件

图 13.1 – RadioButton 小部件

当用户选择一个选项时,其他选项将自动取消选择。我们通过将RadioButton小部件放置在 UI 布局中的RadioGroup中来控制它们。当然,我们可以使用可视化设计工具简单地将一堆RadioButtons拖放到RadioGroup中。当我们在ConstraintLayout布局中这样做时,XML 将类似于以下内容:

<RadioGroup
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     tools:layout_editor_absoluteX="122dp"
     tools:layout_editor_absoluteY="222dp" >
     <RadioButton
          android:id="@+id/radioButton1"
          android:layout_width="match_parent"
          android:layout_height="wrap_content"
          android:text="Option 1" />
     <RadioButton
          android:id="@+id/radioButton2"
          android:layout_width="match_parent"
          android:layout_height="wrap_content"
          android:text="Option 2" />
     <RadioButton
          android:id="@+id/radioButton3"
          android:layout_width="match_parent"
          android:layout_height="wrap_content"
          android:text="Option 3" />
</RadioGroup>

请注意,正如前面的代码所强调的,每个RadioButton实例都设置了适当的id属性。然后,我们可以像这段代码所示的那样引用它们:

// Get a reference to all our widgets
RadioButton rb1 = findViewById(R.id.radioButton1);
RadioButton rb2 = findViewById(R.id.radioButton2);
RadioButton rb3 = findViewById(R.id.radioButton3);

然而,在实践中,正如你将看到的,我们几乎可以仅通过RadioGroup引用来管理几乎所有的事情。此外,你将了解到我们可以为RadioGroup小部件分配一个id属性以实现这个目的。

你可能会想,我们怎么知道它们被点击了呢?或者,你可能会想知道是否跟踪被选中的那个可能会很尴尬。我们需要一些来自 Android API 和 Java 的帮助,以匿名类的形式。

匿名类

第十二章**,堆栈、堆和垃圾收集器中,我们简要介绍了匿名类。在这里,我们将更详细地讨论它,并探讨它如何帮助我们。当RadioButton小部件是RadioGroup小部件的一部分时,它们的所有视觉外观都是为我们协调好的。我们所需要做的就是在任何给定的RadioButton小部件被按下时做出反应。当然,就像任何其他按钮一样,我们需要知道它们何时被点击。

RadioButton小部件的行为与常规的Button不同,仅仅监听onClick(在实现OnClickListener之后)的点击是行不通的,因为RadioButton并非设计为这样。

我们需要做的是使用另一个 Java 特性。我们需要实现一个类,也就是一个匿名类,唯一的目的是监听RadioGroup小部件上的点击。下面的代码块假设我们有一个名为radioGroupRadioGroup小部件的引用。这是代码:

radioGroup.setOnCheckedChangeListener(
   new RadioGroup.OnCheckedChangeListener() {

         @Override
         public void onCheckedChanged(RadioGroup group, 
                int checkedId) {

                // Handle clicks here
         }
   }
);

前面的代码,特别是从{}RadioGroup.OnCheckedChangedListener被称为匿名类。这是因为它没有名字。

如果我们将前面的代码放在onCreate方法中,那么令人惊讶的是,当调用onCreate时代码并不运行。它只是准备好新的匿名类,以便在radioGroup上处理任何点击。我们现在将更详细地讨论这一点。

这个类更正式地称为匿名内部类,因为它在另一个类内部。内部类可以是匿名的或者有名字的。我们将在第十六章中学习有名字的内部类,适配器和回收器

我记得第一次看到匿名类时,我想躲进橱柜里。然而,它并不像一开始看起来那么复杂。

我们正在为radioGroup添加一个监听器。这与我们在第十二章中实现View.OnClickListener的效果非常相似,堆栈、堆和垃圾收集器。然而,这一次,我们声明并实例化了一个监听器类,准备好监听radioGroup,同时重写了所需的方法,这种情况下是onCheckedChanged。这就像RadioGroup中的onClick等效。

让我们逐步了解这个过程:

  1. 首先,我们在radioGroup实例上调用setOnCheckedChangedListener方法:
radioGroup.setOnCheckedChangeListener(
  1. 然后,我们提供一个新的匿名类实现,将该类的重写方法的细节作为参数传递给setOnCheckedChangedListener方法:
new RadioGroup.OnCheckedChangeListener() {

      @Override
      public void onCheckedChanged(RadioGroup group, 
             int checkedId) {

             // Handle clicks here
      }
}
  1. 最后,我们有方法的闭合括号,当然,还有分号标记代码行的结束。我们将它呈现在多行上的唯一原因是为了使其更易读。就编译器而言,它可以全部合并在一起:
);

如果我们使用前面的代码来创建和实例化一个类,监听我们的RadioGroup的点击,也许在onCreate方法中,它将在整个 Activity 的生命周期内监听和响应。现在我们需要学习的是如何处理我们重写的onCheckedChanged方法中的点击。

请注意,onCheckedChanged方法的一个参数(在radioGroup被按下时传入)是int checkedId。这保存了当前选定的RadioButton小部件的id属性。这正是我们需要的 – 好吧,几乎是。

也许令人惊讶的是,checkedId是一个int。Android 将所有 ID 存储为int,即使我们使用字母数字字符声明它们,比如radioButton1radioGroup

当应用程序编译时,所有我们人性化的名称都会转换为整数。那么,我们如何知道哪个整数值指的是哪个id属性值,比如radioButton1radioButton2

我们需要做的是获取整数标识符的实际对象的引用。我们可以通过使用int CheckedId参数来实现,然后询问对象其人性化的id属性值。我们可以这样做:

RadioButton rb = group.findViewById(checkedId);

现在,我们可以使用getId方法检索当前选定的RadioButton小部件的熟悉的id属性值,我们现在已经将其存储在rb中:

rb.getId();

因此,我们可以通过使用switch块和case来处理任何RadioButton小部件的点击,对于每个可能被按下的RadioButton小部件,我们可以使用rb.getId()作为switch块的表达式。

以下代码显示了我们刚讨论的onCheckedChanged方法的全部内容:

// Get a reference to the RadioButton 
// that is currently checked
RadioButton rb = group.findViewById(checkedId);
// Switch based on the 'friendly' id from the XML layout
switch (rb.getId()) {
   case R.id.radioButton1:
          // Do something here
          break;
   case R.id.radioButton2:
          // Do something here
          break;
   case R.id.radioButton3:
          // Do something here
          break;
}
// End switch block

为了使这更清晰,我们将在下一个工作应用程序中实时查看它的运行情况,我们可以按下按钮。

让我们继续探索调色板。

探索调色板和更多匿名类 – 第二部分

现在我们已经了解了匿名类是如何工作的,特别是与RadioGroupRadioButton一起,我们可以继续探索调色板,并查看匿名类如何与更多 UI 小部件一起工作。

Switch

Switch(不要与小写的switch Java 关键字混淆)小部件就像Button小部件一样,只是它有两种可能的状态可以读取和响应。

Switch小部件的一个明显用途是显示或隐藏某些内容。请记住,在我们的 Java Meet UI 应用程序中,在第十二章中,堆栈、堆和垃圾收集器,我们使用了Button小部件来显示和隐藏TextView小部件。

每次我们隐藏或显示TextView小部件时,我们都会更改Button小部件上的text属性,以清楚地表明如果再次点击它会发生什么。对于用户来说,以及对我们作为程序员来说,更直观、更简单的做法可能是使用Switch小部件,如下所示:

图 13.2 – Switch 小部件

图 13.2 – Switch 小部件

以下代码假设我们已经有一个名为mySwitch的对象,它是布局中Switch对象的引用。我们可以像在我们的 Java Meet UI 应用程序中第十二章中那样显示和隐藏TextView小部件。

为了监听并响应点击,我们再次使用匿名类。但是,这一次,我们使用CompoundButton版本的OnCheckedChangedListener,而不是RadioGroup版本。

我们需要重写onCheckedChanged方法,该方法有一个名为isChecked的布尔参数。isChecked变量对于关闭来说只是false,对于打开来说只是true

以下是我们如何更直观地替换第十二章中的文本隐藏/显示代码,堆栈、堆和垃圾收集器

mySwitch.setOnCheckedChangeListener(
   new CompoundButton.OnCheckedChangeListener() {

         public void onCheckedChanged(
                CompoundButton buttonView, boolean 
                isChecked) {

                if(isChecked){
                      // Currently visible so hide it
                      txtValue.setVisibility(View.
                      INVISIBLE);

                }else{
                      // Currently hidden so show it
                      txtValue.setVisibility(View. 
                      VISIBLE);
                }
         }
   }
);

如果匿名类代码看起来有点奇怪,不要担心,因为随着您不断使用它,它会变得更加熟悉。现在我们将在查看CheckBox小部件时这样做。

复选框

这是一个CheckBox小部件。它可以是选中的,也可以是未选中的。在下面的屏幕截图中,它是选中的:

图 13.3 – CheckBox 小部件

图 13.3 - CheckBox 小部件

使用CheckBox小部件,我们可以简单地在特定时刻检测其状态(选中或未选中)-例如,在特定按钮被点击时。以下代码让我们可以看到这种情况是如何发生的,再次使用内部类作为监听器:

myCheckBox.setOnCheckedChangeListener(
   new CompoundButton.OnCheckedChangeListener() {

         public void onCheckedChanged(
                CompoundButton buttonView, boolean 
                isChecked) {
                if (myCheckBox.isChecked()) {
                      // It's checked so do something
                } else {
                      // It's not checked do something else
                }
         }
   }
);

在前面的代码中,我们假设myCheckBox已经被声明和初始化。然后,我们使用了与Switch相同类型的匿名类,以便检测和响应点击。

TextClock

在我们的下一个应用程序中,我们将使用TextClock小部件展示一些它的特性。我们需要直接添加 XML,因为这个小部件不能从调色板中拖放。TextClock小部件看起来类似于以下截图:

图 13.4 - TextClock 小部件

图 13.4 - TextClock 小部件

让我们看一个使用TextClock的例子。这是我们如何将其时间设置为与布鲁塞尔,欧洲的时间相同的方法:

tClock.setTimeZone("Europe/Brussels");

上面的代码假设tClock是布局中TextClock小部件的引用。

使用 WebView

WebView 是一个非常强大的小部件。它可以用来在应用的 UI 中显示网页。你甚至可以只用几行代码来实现一个基本的网页浏览器应用程序。

提示

通常情况下,你不会实现一个完整的网页浏览器;相反,你会使用用户首选的网页浏览器。

要简单地获取 XML 中存在的WebView小部件的引用并显示一个网站,你只需要两行代码。这段代码加载了我的网站- gamecodeschool.com - 假设布局中有一个id属性设置为webViewWebView小部件:

WebView webView = findViewById(R.id.webView);
webView.loadUrl("https://gamecodeschool.com");

有了所有这些额外的信息,让我们制作一个比我们迄今为止更广泛地使用 Android 小部件的应用程序。

小部件探索应用程序

到目前为止,我们已经讨论了七个小部件:EditTextImageViewRadioButton(和RadioGroup)、SwitchCheckBoxTextClockWebView。让我们制作一个可工作的应用程序,并对每个小部件做一些真实的事情。我们还将再次使用Button小部件和TextView小部件。

请记住,你可以在下载包中找到已完成的代码。这个应用程序可以在第十三章/小部件探索中找到。

设置小部件探索项目和 UI

首先,我们将设置一个新项目并准备 UI 布局。这些步骤将在屏幕上排列所有小部件并设置id属性,准备好引用它们。在开始之前,看一下目标布局是很有用的-当它正在运行时。看一下以下截图:

图 13.5 - 小部件探索布局

图 13.5 - 小部件探索布局

这个应用程序将演示这些小部件:

  • 单选按钮允许用户将时钟显示的时间更改为三个时区中的一个选择。

  • TextView小部件(在右侧)显示EditText小部件(在左侧)中当前的内容。

  • 这三个CheckBox小部件将为 Android 机器人图像添加和删除视觉效果。

  • Switch小部件将打开和关闭TextView小部件,显示在EditText小部件中输入的信息,并在按下按钮时捕获。

  • WebView小部件将占据应用程序的整个宽度和下半部分。在添加小部件到布局时要记住这一点;尽量将它们全部放在上半部分。

确切的布局位置并不重要,但指定的id属性必须完全匹配。如果你只想查看/使用代码,你可以在下载包的第十三章/小部件探索文件夹中找到所有文件。

因此,让我们执行以下步骤来设置一个新项目并准备 UI 布局:

  1. 创建一个名为Widget Exploration的新项目。设置API 17:Android 4.2 (Jelly Bean)。然后,使用一个空活动,并保持所有其他设置为默认值。我们使用API 17是因为TextClock小部件的一个功能需要我们这样做。我们仍然可以支持超过 99%的所有 Android 设备。

  2. 切换到activity_main.xml布局文件,并确保您处于设计视图中。删除默认的TextView小部件。

  3. 使用显示在设计视图上方的下拉控件(如下面的屏幕截图所示),选择横向方向的平板电脑。我选择了Pixel C选项:图 13.6 - 选择方向选项

图 13.6 - 选择方向选项

重要提示

有关如何制作平板模拟器的提醒,请参阅第三章探索 Android Studio 和项目结构。有关如何操作模拟器方向的其他建议,请参阅第五章使用 CardView 和 ScrollView 创建美丽的布局

  1. 从调色板的按钮类别中拖动一个开关小部件到布局的右上角附近。然后,在这下面,添加一个TextView小部件。您的布局的右上角现在应该看起来类似于以下的屏幕截图:图 13.7 - 将开关小部件添加到布局

图 13.7 - 将开关小部件添加到布局

  1. 拖动三个sym_def_app_icon以使用 Android 图标作为ImageView的图像。布局的中间部分现在应该看起来类似于以下的屏幕截图。有关最终布局的更多上下文,请参考显示完成的应用程序的屏幕截图:图 13.8 - 复选框小部件

图 13.8 - 复选框小部件

  1. RadioGroup拖到布局的左上角。

  2. RadioGroup内添加三个RadioButton小部件。可以使用组件树窗口轻松完成此步骤。

  3. RadioGroup下面,从调色板的文本类别中拖动一个纯文本小部件。请记住,尽管它的名字是这样,但这是一个允许用户在其中输入一些文本的小部件。稍后,我们将学习如何捕获和使用输入的文本。

  4. 纯文本小部件下面添加一个按钮小部件。您的布局的左侧应该看起来类似于以下的屏幕截图:图 13.9 - 添加一个按钮小部件

图 13.9 - 添加一个按钮小部件

  1. 现在,为我们刚刚布置的小部件添加以下属性:

重要提示

请注意,一些属性可能已经默认正确。

  1. 倒数第二个小部件有点不同,所以我认为我们应该单独处理它。在左侧的按钮小部件下方再添加一个常规的TextView小部件,并将其id属性设置为textClock。请记住,与所有其他小部件一样,保持此小部件在垂直方向上大约中点以上。如果需要,重新调整其上方的一些小部件。

  2. 切换到代码视图,并找到我们正在处理的TextView小部件 - 其id属性值为textClock的那个。

  3. 观察 XML 代码的开头,如下面的代码片段所示,其中突出显示了一些关键部分:

<TextView to TextClock, and we have deleted the text property and replaced it with the format that we would like for our clock. 
  1. 切换到设计选项卡。

  2. 现在是最后一个小部件的时间了。从WebView小部件中拖动一个WebView小部件。实际上,如果你仔细观察,你会发现WebView小部件似乎不见了。事实上,如果你仔细观察,你会发现WebView小部件在布局的左上角有一个微小的指示。我们将稍微不同地配置WebView小部件的位置和大小。

  3. 确保在组件树窗口中选择了WebView小部件。

  4. id属性更改为webView(如果尚未是此值)。

  5. 为了使下一步正常工作,所有其他小部件都必须受到约束。因此,单击推断约束按钮以确保所有其他小部件。

  6. 目前,我们的WebView小部件没有受到任何约束,也无法抓取我们需要的约束手柄。现在,在属性窗口中找到布局部分,如下面的屏幕截图所示:图 13.10 – 添加约束

图 13.10 – 添加约束

  1. 单击上一个屏幕截图中突出显示的添加约束到底部按钮。现在,我们有一个约束,其中WebView小部件的底部受到布局底部的约束。这几乎是完美的,但默认边距设置得非常高。

  2. 在属性窗口中找到layout_margin_bottom属性,并将其更改为0dp

  3. 在属性窗口中将layout_height属性更改为400dp。请注意,当此项目完成时,如果您的WebView小部件太高或太矮,那么您可以回来调整此值。

  4. 调整您的布局,使其尽可能地类似于以下参考图。但是,如果您具有正确的 UI 类型和正确的id属性,即使布局不完全相同,代码仍将起作用。请记住,WebView小部件是不可见的,但是一旦我们进行了一些编码,它将占据屏幕的下半部分。

图 13.11 – 调整布局

图 13.11 – 调整布局

我们刚刚布置并设置了布局所需的属性。除了一些小部件类型对我们来说是新的,布局略微更加复杂之外,这里没有我们以前没有做过的事情。

现在我们可以开始在我们的 Java 代码中使用所有这些小部件了。

编写 Widget Exploration 应用程序的代码

此应用程序需要许多import语句。因此,让我们现在添加它们,以免每次都提到它们。添加以下import语句:

import android.graphics.Color;
import android.graphics.PorterDuff;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.AnalogClock;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.Switch;
import android.widget.TextClock;
import android.widget.TextView;

让我们在 Java 代码中获取我们将在 UI 中使用的所有部分的引用。

获取对 UI 的所有部分的引用

下一块代码看起来相当长而杂乱,但我们所做的只是获取布局中每个小部件的引用。当我们使用它们时,我们将更详细地讨论代码。

在下一块代码中唯一新的是,一些对象被声明为final。这是必要的,因为它们将在匿名类中使用。

但是final不是意味着对象不能被更改吗?

如果您回忆第十一章**,更多面向对象编程,我们学到声明为final的变量不能被更改,也就是说,它们是常量。那么,我们如何改变这些对象的属性呢?请记住,对象是引用类型变量。这意味着它们引用堆上的一个对象。它们不是对象本身。我们可以将它们视为持有对象的地址。地址是不会改变的。我们仍然可以使用地址引用堆上的对象,并随意更改实际对象。让我们进一步使用地址的类比。如果您住在特定地址,如果地址是最终的,那么您就不能搬到新房子。但是,您在该地址上可以做任何事情。例如,您仍然可以重新布置您的房子,也许重新粉刷客厅,并在厨房放浴缸,把沙发放在屋顶上。

onCreate方法中的setContentView方法调用之后立即输入以下代码:

// Get a reference to all our widgets
RadioGroup radioGroup =
findViewById(R.id.radioGroup);
final EditText editText = 
findViewById(R.id.editText);
final Button button = 
findViewById(R.id.button);
final TextClock tClock = 
findViewById(R.id.textClock);
final CheckBox cbTransparency = 
findViewById(R.id.checkBoxTransparency);
final CheckBox cbTint = 
findViewById(R.id.checkBoxTint);
final CheckBox cbReSize = 
findViewById(R.id.checkBoxReSize);
final ImageView imageView = 
findViewById(R.id.imageView);
Switch switch1 = (Switch) findViewById(R.id.switch1);
final TextView textView = 
findViewById(R.id.textView);
// Hide the TextView at the start of the app
textView.setVisibility(View.INVISIBLE);

我们现在在我们的 Java 代码中引用了我们需要操作的布局中的所有 UI 元素。

编写复选框的代码

现在,我们可以创建一个匿名类来监听和处理复选框的点击。接下来的三个代码块分别实现了每个复选框的匿名类。然而,每个以下三个代码块中的不同之处在于我们如何响应点击;我们将依次讨论每个。

更改透明度

第一个复选框标记为imageView中的setAlpha方法,以更改其透明度。setAlpha方法以 0 到 1 之间的浮点值作为参数。

0 是不可见的,1 表示完全不透明。因此,当选中此复选框时,我们将 alpha 设置为.1,这意味着图像几乎不可见。当取消选中时,我们将其设置为1,这意味着它完全可见,没有透明度。onCheckedChangedboolean isChecked参数包含truefalse,以显示复选框是否被选中。

onCreate方法中的上一个代码块之后添加以下代码:

/*
   Now we need to listen for clicks
   on the button, the CheckBoxes 
   and the RadioButtons
*/
// First the check boxes using an anonymous class
cbTransparency.setOnCheckedChangeListener(new 
CompoundButton.OnCheckedChangeListener(){
   public void onCheckedChanged(
   CompoundButton buttonView, boolean isChecked){
         if(cbTransparency.isChecked()){
                // Set some transparency
                imageView.setAlpha(.1f);
         }else{
                imageView.setAlpha(1f);
         }
   }
});

在下一个匿名类中,我们将处理标记为Tint的复选框。

更改颜色

onCheckedChanged方法中,我们使用imageView中的setColorFilter方法在图像上叠加一个颜色层。当isChecked为 true 时,我们叠加一个颜色,当isChecked为 false 时,我们移除它。

setColorFilter方法接受Color类中的argb颜色。argb方法的四个参数分别是 alpha、red、green 和 blue 的值。这四个值创建了一种颜色。在我们的情况下,值15025500创建了强烈的红色色调。另外,值0000则完全没有色调。

重要说明

要了解更多关于Color类的信息,请访问 Android 开发者网站developer.android.com/reference/android/graphics/Color.html。此外,要更详细地了解 RGB 颜色系统,请参考以下维基百科页面:en.wikipedia.org/wiki/RGB_color_model

onCreate方法中的上一个代码块之后添加以下代码:

// Now the next checkbox
cbTint.setOnCheckedChangeListener(new 
CompoundButton.OnCheckedChangeListener() {
   public void onCheckedChanged(CompoundButton 
   buttonView, boolean isChecked) {
         if (cbTint.isChecked()) {
                // Checked so set some tint
                imageView.setColorFilter(
                Color.argb(150, 255, 0, 0));
         } else {
                // No tint needed
                imageView.setColorFilter(Color.argb(0, 0, 
                0, 0));
         }
   }
});

现在我们将看看如何调整 UI 的比例。

更改大小

在处理标记为setScaleX方法的匿名类中,可以调整机器人图像的大小。当我们在imageView中调用setScaleX(2)setScaleY(2)时,我们将使图像的大小加倍,而setScaleX(1)setScaleY(1)将使其恢复正常。

onCreate方法中的上一个代码块之后添加以下代码:

// And the last check box
cbReSize.setOnCheckedChangeListener(
new CompoundButton.OnCheckedChangeListener() {
   public void onCheckedChanged(
   CompoundButton buttonView, boolean isChecked) {
         if (cbReSize.isChecked()) {
                // It's checked so make bigger
                imageView.setScaleX(2);
                imageView.setScaleY(2);
         } else {
                // It's not checked make regular size
                imageView.setScaleX(1);
                imageView.setScaleY(1);
         }
   }
});

现在我们将处理这三个单选按钮。

编写单选按钮

由于它们是RadioGroup小部件的一部分,我们可以处理它们的方式比处理CheckBox对象要简洁得多。我们是这样做的。

首先,我们确保它们一开始是清除的,通过在radioGroup中调用clearCheck()。然后,我们创建OnCheckedChangedListener类型的匿名类,并重写onCheckedChanged方法。

当点击RadioGroup中的任何RadioButton小部件时,将调用此方法。我们需要做的就是获取被点击的RadioButton小部件的id属性,并做出相应的响应。我们可以通过使用一个switch语句来实现这一点,有三种可能的情况,每种情况对应一个RadioButton

您会记得当我们第一次谈论RadioButton小部件时,我们提到onCheckedChanged方法的checkedId参数中提供的值是一个整数。这就是为什么我们必须首先从checkedId参数创建一个新的RadioButton实例的原因:

RadioButton rb = 
(RadioButton) group.findViewById(checkedId);

然后,我们可以在新的RadioButton实例上调用getId作为switch块的条件:

switch (rb.getId())

然后,在每个case选项中,我们使用带有适当 Android 时区代码的setTimeZone方法。

提示

您可以在gist.github.com/arpit/1035596查看所有 Android 时区代码。

看一下以下代码,它包含了我们刚刚讨论的所有内容。在先前输入的处理复选框的代码之后,将其添加到onCreate方法中:

// Now for the radio buttons
// Uncheck all buttons
radioGroup.clearCheck();
radioGroup.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(
RadioGroup group, int checkedId) {
         RadioButton rb =
         group.findViewById(checkedId);

                switch (rb.getId()) {
                      case R.id.radioButtonLondon:
                            tClock.setTimeZone(
                            "Europe/London");
                            break;
                      case R.id.radioButtonBeijing:
                            tClock.setTimeZone("Etc/GMT-
                            8");
                            break;
                      case R.id.radioButtonNewYork:

                            tClock.setTimeZone(
                            "America/New_York");
                            break; 
                }// End switch block
   }
});

现在来点新鲜的东西。

使用匿名类处理普通按钮

在下一段代码中,我们将编写并使用一个匿名类来处理普通Button的点击。我们调用button.setOnclickListener,就像以前一样。但是,这一次,我们不是像以前那样将this作为参数传递,而是创建一个全新的View.OnClickListener类型的类,并覆盖onClick方法作为参数 - 就像我们以前的其他匿名类一样。

提示

在这种情况下,这种方法是可取的,因为只有一个按钮。如果我们有很多按钮,那么让MainActivity实现View.OnClickListener,然后覆盖onClick方法以处理所有点击的方法可能更可取,就像我们以前做过的那样。

onClick方法中,我们使用setText方法在textView上设置text属性,并使用editTextgetText方法获取EditText小部件中当前的文本。

onCreate方法中的先前代码块之后添加以下代码:

/*
   Let's listen for clicks on our "Capture" Button.
   We can do this with an anonymous class as well.
   An interface seems a bit much for one button.
*/
button.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View v) {
          // We only handle one button
          // So, no switching required

          // Change the text on the TextView 
          // to whatever is currently in the EditText
          textView.setText(editText.getText());
   }
});

编写 Switch 小部件

接下来,我们将创建另一个匿名类来监听和处理Switch小部件的更改。

isChecked变量为true时,我们显示textView;当它为false时,我们隐藏它。

onCreate方法中的先前代码块之后添加以下代码:

// Show or hide the TextView
switch1.setOnCheckedChangeListener(
   new CompoundButton.OnCheckedChangeListener() {

   public void onCheckedChanged(
         CompoundButton buttonView, boolean isChecked) {

         if(isChecked){
                textView.setVisibility(View.VISIBLE);
         }else{
                textView.setVisibility(View.INVISIBLE);
         }
   }
});

现在我们可以继续进行WebView小部件。

使用 WebView

您的清单必须包括INTERNET权限。这是我们添加它的方式。

打开AndroidManifest.xml文件,并添加以下突出显示的代码行,显示了一些上下文:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.gamecodeschool.widgetexploration">
    <uses-permission android:name="android
      .permission.INTERNET" />
    <application
     …
     …

最后,我们将添加另外两行代码,以便获取对WebView小部件的引用并加载网站。修改代码以加载您喜欢的任何网站应该是一个相对简单的过程。在onCreate方法的末尾添加以下代码行:

WebView webView = (WebView) findViewById(R.id.webView);
webView.loadUrl("https://gamecodeschool.com");

现在我们可以运行我们的应用程序并尝试所有功能。

运行 Widget Exploration 应用程序

以通常的方式在平板模拟器上运行应用程序。

提示

Android 模拟器可以通过在 PC 上按下Ctrl + F11键组合或在 Mac 上按下Ctrl + fn+ F11来旋转为横向模式。

这是整个应用程序,包括现在可见的WebView小部件:

图 13.12 - 最终应用程序布局

图 13.12 - 最终应用程序布局

尝试选中单选按钮,看看时区在时钟上的变化。在下图中,我将一些裁剪的屏幕截图合在一起,以显示选择新时区时时间的变化:

图 13.13 - 时区

图 13.13 - 时区

要测试CAPTURE按钮、可编辑文本和开关,请按照以下步骤进行(我们也在相邻的屏幕截图中列出了它们):

  1. EditText小部件(位于左侧)中输入不同的值。

  2. 点击CAPTURE按钮。

  3. 确保Switch小部件是打开的。

  4. 查看消息:

图 13.14 - 测试 CAPTURE 按钮

图 13.14 - 测试 CAPTURE 按钮

您可以通过不同的选中和未选中复选框的组合来更改前面的图表外观,并且您可以使用上面的开关来隐藏和显示TextView小部件。以下屏幕截图显示了当您选择TintRe-size选项时ImageView小部件会发生什么:

图 13.15 - 测试 ImageView 小部件

图 13.15 - 测试 ImageView 小部件

糟糕!图标的大小增加得太多,以至于它与Re-size复选框重叠。

提示

透明度在印刷书籍中并不清晰,所以我没有展示“透明度”框被选中的视觉示例。一定要在模拟器或真实设备上尝试一下。

总结

在本章中,我们学到了很多,并且探索了大量的小部件。我们学会了如何在 Java 代码中实现小部件而不需要任何 XML,并且我们使用了我们的第一个匿名类来处理小部件上的点击,并将我们所有新的小部件技能应用到一个工作中的应用程序中。

现在,让我们继续看看另一种显著增强我们 UI 的方式。

在下一章中,我们将看到一个全新的 UI 元素,我们不能简单地从调色板中拖放,但我们仍然会得到来自 Android API 的大量帮助。接下来是对话框窗口。此外,我们还将开始制作迄今为止最重要的应用程序,即备忘录、待办事项列表和个人笔记的“自我备忘录”应用程序。

第十四章:Android 对话框窗口

在本章中,我们将学习如何向用户呈现弹出对话框窗口。然后,我们可以将我们所知道的全部内容放入我们的第一个应用程序Note to Self的第一阶段。接下来的四章(直到第十八章**, 本地化)中,我们将探索最新的 Android 和 Java 功能,并利用我们新获得的知识来增强 Note to Self 应用程序的每个阶段。

每一章还将构建一系列与此主要应用程序分开的较小的应用程序。那么,这一章对您有什么意义呢?好吧,我们将涵盖以下主题:

  • 使用弹出对话框框架创建一个简单的应用程序

  • 使用DialogFragment类开始 Note to Self 应用程序

  • 在我们的项目中添加字符串资源,而不是在布局中硬编码文本

  • 首次使用 Android 命名约定,使我们的代码更易读

  • 实现更复杂的对话框以捕获用户输入

让我们开始吧。

技术要求

您可以在 GitHub 上找到本章中的代码文件,网址为github.com/PacktPublishing/Android-Programming-for-Beginners-Third-Edition/tree/main/chapter%2014

对话框窗口

在我们的应用程序中,我们经常需要在弹出窗口中向用户显示一些信息,甚至要求确认某个操作。这就是所谓的对话框窗口。如果您快速扫描 Android Studio 中的调色板,您可能会惊讶地发现根本没有提到对话框。

在 Android 中,对话框比简单的小部件甚至整个布局更高级。它们是可以拥有自己的布局和其他用户界面UI)元素的类。

在 Android 中创建对话框窗口的最佳方式是使用FragmentDialog类。

注意

片段是 Android 中一个广泛而重要的主题,我们将在本书的后半部分花费大量时间来探索和使用它们。

为用户创建一个整洁的弹出对话框(使用FragmentDialog)是对片段的一个很好的介绍,而且一点也不复杂。

创建对话框演示项目

我们之前提到,在 Android 中创建对话框的最佳方式是通过FragmentDialog类。

为此,在 Android Studio 中使用空活动模板创建一个新项目,并将其命名为Dialog Demo。正如您所期望的那样,该项目的完整代码位于下载包的第十四章/Dialog Demo文件夹中。

编写一个 DialogFragment 类

通过右键单击包含MainActivity.java文件的包名称(与MainActivity.java文件相同的包名称)在 Android Studio 中创建一个新类。选择MyDialog。按下Enter键创建类。

首先要做的是将类声明更改为扩展DialogFragment。完成后,您的新类应该类似于以下代码块:

public class MyDialog extends DialogFragment {    
}

现在,让我们一点一点地向这个类添加代码,并解释每一步发生了什么。现在,我们需要导入DialogFragment类。您可以通过按住Alt键然后点击Enter,或者在MyDialog.java文件顶部的包声明之后添加以下突出显示的代码行来实现这一点:

package com.gamecodeschool.dialogdemo;
import androidx.fragment.app.DialogFragment;
public class MyDialog extends DialogFragment {
}

就像在 Android API 中的许多类一样,DialogFragment为我们提供了可以重写以与该类发生的不同事件交互的方法。

添加以下突出显示的代码以重写onCreateDialog方法。仔细研究它,我们将在下一步中检查发生了什么:

public class MyDialog extends DialogFragment {
   @Override
   public Dialog onCreateDialog(Bundle savedInstanceState) {
// Use the Builder class because this dialog 
   // has a simple UI
AlertDialog.Builder builder = 
          new AlertDialog.Builder(getActivity());
   }
}

您需要按照通常的方式导入DialogBundleAlertDialog类,或者通过手动添加以下突出显示的代码来导入:

import android.app.Dialog;
import android.os.Bundle;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;

注意

代码中仍然有一个错误,因为我们忘记了 onCreateDialog 方法的 return 语句。当我们完成了方法的其余部分编码后,我们将在稍后添加这个 return 语句。

在前面的代码中,我们首先添加了重写的 onCreateDialog 方法。当我们稍后通过 MainActivity 类中的代码向用户显示对话框时,Android 将调用此方法。

然后,在 onCreateDialog 方法内部,我们得到了一个新的类。我们声明并初始化了一个 AlertDialog.Builder 类型的对象,它需要 MainActivity 的引用传递给它的构造函数。这就是为什么我们使用 getActivity()方法作为参数。

getActivity 方法是 Fragment 类(因此也是 DialogFragment)的一部分,它返回一个对 Activity 的引用,该 Activity 将创建 DialogFragment。在这种情况下,就是我们的 MainActivity 类。

现在我们已经声明并初始化了 builder,让我们看看我们可以用 builder 做些什么。

使用链接配置 DialogFragment

现在我们可以使用 builder 对象来完成其余的工作。在下面的代码片段中有一些奇怪的地方。如果您快速扫描下面的三个代码块,您会注意到缺少分号;。事实上,这三个代码块对于编译器来说只是一行。

我们以前见过这种情况,但情况没有那么明显;也就是说,当我们创建一个 Toast 消息并在其末尾添加.show()方法时。这被称为链接。这是指我们在同一个对象上按顺序调用多个方法。这相当于编写多行代码;只是这种方式更简洁。

在我们添加的先前代码之后,在 onCreateDialog 方法中添加以下代码(使用链接)。检查新代码,我们将在下面讨论它:

// Dialog will have "Make a selection" as the title
builder.setMessage("Make a selection")

// An OK button that does nothing
.setPositiveButton("OK", new DialogInterface.OnClickListener() {
   public void onClick(DialogInterface dialog, int id) {
          // Nothing happening here
   }
})
// A "Cancel" button that does nothing 
.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {

   public void onClick(DialogInterface dialog, int id) {
          // Nothing happening here either

   }

});

此时,您需要导入 DialogInterface 类。使用 Alt | Enter 技术,或者在其他 import 语句中添加以下代码行:

import android.content.DialogInterface;

以下是我们刚刚添加的代码的每个部分的解释:

  1. 在三个使用链接的代码块中的第一个代码块中,我们调用 builder.setMessage。这将设置用户在对话框中看到的主要消息。另外,请注意,可以在链接方法调用的各个部分之间添加注释,因为编译器会忽略这些注释。

  2. 然后,我们使用.setPositiveButton 方法向对话框添加一个按钮,该方法的第一个参数将按钮的文本设置为“确定”。第二个参数是一个匿名类,称为 DialogInterface.OnClickListener,它处理按钮的任何点击。请注意,我们不打算向 onClick 方法添加任何代码。在这里,我们只是想看到这个简单的对话框;我们将在下一个项目中进一步进行。

  3. 接下来,我们在同一个 builder 对象上调用另一个方法。这次是 setNegativeButton 方法。同样,两个参数将“取消”设置为按钮的文本,并添加一个匿名类来监听点击。同样,出于演示目的,我们不会在重写的 onClick 方法中采取任何行动。在调用 setNegativeButton 方法之后,我们最终看到一个分号标记着代码行的结束。

最后,我们将编写 return 语句以完成该方法,并消除我们一开始就有的错误。在 onCreateDialog 方法的最后(但在最终大括号内部)添加 return 语句,如下面的代码片段所示:

// Create the object and return it
return builder.create();
}// End of onCreateDialog

这行代码的最后效果是将我们新的、完全配置好的对话框窗口返回给 MainActivity 类(它首先调用 onCreateDialog 方法)。我们将稍后检查并添加这个调用代码。

现在,我们有了扩展 FragmentDialog 的 MyDialog 类。我们所需要做的就是声明一个 MyDialog 的实例,实例化它,并调用它重写的 createDialog 方法。

使用 DialogFragment 类

在转向代码之前,让我们向布局中添加一个按钮。执行以下步骤:

  1. 切换到activity_main.xml标签,然后切换到设计标签。

  2. 拖动id属性设置为button

  3. 点击MyDialog类是目前的关键课程。

现在切换到MainActivity.java标签,我们将使用匿名类处理对这个新按钮的点击,就像我们在第十三章中的匿名类 - 让 Android 小部件活跃中的 Widget Exploration 应用程序中所做的那样。我们这样做是因为布局中只有一个按钮,这似乎比实现更复杂的OnClickListener接口替代方案更合理和更紧凑(就像我们在第十二章中为 Java Meet UI 演示应用程序所做的那样,堆栈、堆和垃圾收集器)。

请注意,在以下代码块中,匿名类与我们先前为其实现了接口的类型完全相同。将此代码添加到onCreate方法中:

/*
   Let's listen for clicks on our regular Button.
   We can do this with an anonymous class.
*/
Button button = (Button) findViewById(R.id.button);
button.setOnClickListener(
   new View.OnClickListener() {

         @Override
         public void onClick(View v) {
                // We only handle one button
                // Therefor no switching required
                MyDialog myDialog = new MyDialog();
                myDialog.show(getSupportFragmentManager(), 
                "123");
                // This calls onCreateDialog
                // Don't worry about the strange looking 
                   123
                // We will find out about this in chapter 
                   18
         }
   }
);

注意

以下import语句是此代码所需的:

import android.view.View;

import android.widget.Button;

请注意,代码中唯一发生的事情是onClick方法创建了MyDialog的一个新实例,并调用了它的show方法。毫不奇怪,这将显示我们的对话框窗口,就像我们在MyDialog类中配置的那样。

show方法需要一个对FragmentManager的引用,我们可以使用getSupportFragmentManager来获取。这是跟踪和控制活动的所有Fragment实例的类。我们还传入一个标识符:"123"

当我们更仔细地查看片段时,将会透露有关FragmentManager的更多细节。我们将在第二十章Android 片段部分进行。绘图图形

注意

我们使用getSupportFragmentManager方法的原因是,我们通过扩展AppCompatActivity来支持旧设备。如果我们简单地扩展Activity,那么我们可以使用getFragmentManager类。缺点是应用程序将无法在许多设备上运行。

现在我们可以运行应用程序,并欣赏我们的新对话框窗口,当我们点击布局中的按钮时它会出现。请注意,单击对话框窗口中的任一按钮都将关闭它。这是默认行为。以下截图显示了我们的对话框在 Pixel C 平板模拟器上的运行情况:

图 14.01 - 对话框在运行中

图 14.01 - 对话框在运行中

接下来,我们将制作另外两个实现对话框的类,作为我们多章节的Note to Self应用的第一阶段。我们将了解到对话框窗口几乎可以有我们选择的任何布局,而不必依赖于Dialog.Builder类给我们的简单布局。

Note to Self应用程序

欢迎来到我们将在本书中实施的三个主要应用程序中的第一个。当我们执行这些项目时,我们将比较专业地执行它们。我们将使用 Android 命名约定、字符串资源和适当的封装。

有时,当您试图学习新的 Android/Java 主题时,这些东西可能会有些过度。然而,它们是有用的,尽快在真实项目中开始使用它们是很重要的。最终,它们会变得自然而然,我们的应用程序质量会受益。

使用命名约定和字符串资源

第三章中,探索 Android Studio 和项目结构,我们讨论了在布局文件中使用字符串资源而不是硬编码文本。这样做有一些好处,但也相对冗长。

由于这是我们的第一个真实项目,现在是一个很好的时机以正确的方式做事,这样我们就可以获得这样做的经验。如果您想快速了解字符串资源的好处,请返回第三章探索 Android Studio 和项目结构

命名约定是我们代码中用于命名变量、方法和类的约定或规则。在本书中,我们已经宽松地应用了 Android 的命名约定。由于这是我们的第一个真实应用程序,我们将在应用这些命名约定时稍微严格一些。

特别要注意的是,当变量是类的成员时,我们将使用小写的m作为前缀。

注意

您可以在 source.android.com/source/code… 找到有关 Android 命名约定和代码风格的更多信息。

如何获取“Note to Self”应用程序的代码文件

完整的应用程序,包括所有代码和资源,可以在第十八章/Note to self文件夹中找到下载包中。由于我们将在接下来的五章中实现此应用程序,因此在每章结束时查看部分完成的可运行应用程序可能会有所帮助。部分完成的可运行应用程序及其所有相关代码和资源可以在各自的文件夹中找到:

第十四章/Note to self

第十六章/Note to self

第十七章/Note to self

第十八章/Note to self

注意

第十五章中没有“Note to Self”代码,数组映射和随机数。这是因为,即使我们将学习“Note to Self”中使用的主题,我们也不会在第十六章中进行任何更改,适配器和回收器

如果您正在跟着做,并打算从头到尾构建“Note to Self”应用程序,我们将构建一个名为Note to self的项目。但是,这并不妨碍您随时查看每章项目的代码文件,进行一些复制和粘贴。只是要注意,在说明的各个时间点,您将被要求删除或替换前一章的偶尔一行代码。

因此,即使您复制和粘贴的次数多于编写代码的次数,请务必全文阅读说明,并参考书中的代码,以获取可能有用的额外注释。

在每一章中,代码将被呈现为如果您已经完全完成了上一章,将显示来自早期章节的代码,必要时作为我们新代码的有用上下文。

每一章都不会完全致力于“Note to Self”应用程序 - 我们将学习其他通常相关的事物,并构建一些更小/更简单的应用程序。因此,当我们开始“Note to Self”实现时,理论上我们将为此做好准备。

完成的应用程序

以下屏幕截图来自已完成的应用程序。当然,在开发的各个阶段,它看起来会略有不同。必要时,我们将参考更多屏幕截图,作为提醒或查看开发过程中的差异。

完成的应用程序将允许用户点击应用程序右下角的浮动操作按钮,以打开对话框窗口添加新的便签。以下屏幕截图显示了此功能的突出显示:

图 14.02 - 浮动操作按钮

图 14.02 - 浮动操作按钮

在左侧,您可以查看要点击的按钮,在右侧,您可以查看用户可以添加新便签的对话框窗口。

最终,随着用户添加更多的笔记,他们将在应用程序的主屏幕上拥有所有已添加的笔记的列表,如下屏幕截图所示。用户可以选择笔记是重要想法和/或待办事项

图 14.03 –主屏幕上的笔记

图 14.03 –主屏幕上的笔记

您将能够滚动列表并点击一个笔记,以在专门用于该笔记的另一个对话框窗口中查看它。以下屏幕截图显示了显示笔记的对话框窗口:

图 14.04 –显示所选笔记

图 14.04 –显示所选笔记

还将有一个简单(即非常简单)的设置屏幕,可以从菜单中访问。它将允许用户配置笔记列表是否以分隔线格式化。以下是设置菜单选项的操作:

图 14.05 –设置菜单选项

图 14.05 –设置菜单选项

现在我们确切地知道我们要构建什么,我们可以继续并开始实施它。

构建项目

现在让我们创建我们的新项目。使用基本活动模板。正如我们在第三章中讨论的那样,探索 Android Studio 和项目结构,此模板将生成一个简单的菜单和一个浮动操作按钮,这两者都在此项目中使用。将项目命名为Note to Self

准备字符串资源

在这里,我们将创建所有的字符串资源,我们将从布局文件中引用它们,而不是硬编码 UI 小部件的text属性,这是我们一直在做的。

要开始,请在项目资源管理器中的res/values文件夹中打开strings.xml文件。您将看到自动生成的资源。添加以下突出显示的字符串资源,我们将在项目的其余部分中在应用程序中使用它们。在闭合的</resources>标签之前添加以下代码:

...
<resources>
    <string name="app_name">Note to Self</string>
    <string name="action_settings">Settings</string>
    <!-- Strings used for fragments for navigation -->
    <string name="first_fragment_label">First 
               Fragment</string>
    <string name="second_fragment_label">Second 
               Fragment</string>
    <string name="next">Next</string>
    <string name="previous">Previous</string>
    <string name="hello_first_fragment">Hello first 
               fragment</string>
    <string name="hello_second_fragment">Hello second 
               fragment. Arg: %1$s</string>
    <string name="action_add">add</string>
    <string name="title_hint">Title</string>
    <string name="description_hint">Description</string>
    <string name="idea_text">Idea</string>
    <string name="important_text">Important</string>
    <string name="todo_text">To do</string>
    <string name="cancel_button">Cancel</string>
    <string name="ok_button">OK</string>

    <string name="settings_title">Settings</string>
    <string name="theme_title">Theme</string>
    <string name="theme_light">Light</string>
    <string name="theme_dark">Dark</string>
</resources>

在上述代码中,请注意每个字符串资源都有一个唯一的name属性,它使其与所有其他字符串资源区分开,并提供一个有意义的、有希望的、可记住的线索,以表示它所代表的实际字符串值。正是这些名称值,我们将在布局文件中引用。

我们将不需要再访问这个文件了。

编写 Note 类

这是应用程序的基本数据结构。这是一个我们将从头开始编写的类,它具有我们需要表示用户笔记之一的所有成员变量。在第十五章中,数组映射和随机数,我们将学习一些新的 Java,以便了解如何让用户拥有数十、数百甚至数千条笔记。

通过右键单击与您的包名称相同的文件夹来创建一个新类。预期的是,它也包含MainActivity.java文件。选择Note。按下Enter键创建类。

将以下突出显示的代码添加到新的Note类中:

public class Note {
    private String mTitle;
    private String mDescription;
    private boolean mIdea;
    private boolean mTodo;
    private boolean mImportant;
}

请注意,根据 Android 约定,我们的成员变量名都以m为前缀。另外,我们不希望任何其他类直接访问这些变量,所以它们都声明为private

因此,我们将需要为我们的每个成员添加一个 getter 和一个 setter 方法。将以下 getter 和 setter 方法添加到Note类中:

public String getTitle() {
   return mTitle;
}
public void setTitle(String mTitle) {
   this.mTitle = mTitle;
}
public String getDescription() {
   return mDescription;
}
public void setDescription(String mDescription) {
   this.mDescription = mDescription;
}
public boolean isIdea() {
   return mIdea;
}
public void setIdea(boolean mIdea) {
   this.mIdea = mIdea;
}
public boolean isTodo() {
   return mTodo;
}
public void setTodo(boolean mTodo) {
   this.mTodo = mTodo;
}
public boolean isImportant() {
   return mImportant;
}
public void setImportant(boolean mImportant) {
   this.mImportant = mImportant;
}

在上述列表中有相当多的代码,但没有什么复杂的。每个方法都指定了public访问权限,因此可以被任何具有对Note类型对象的引用的其他类使用。此外,对于每个变量,都有一个名为get...的方法和一个名为set...的方法。布尔类型变量的 getter 命名为is...。如果你仔细想想,这是一个合乎逻辑的名字,因为返回的答案要么是 true,要么是 false。

每个 getter 都只返回相关变量的值。每个 setter 都将相关变量的值设置为传递给方法的任何值。

注意

实际上,我们应该稍微增强我们的 setter,以确保传入的值在合理范围内。例如,我们可能希望检查并强制执行String mTtileString mDescription的最大或最小长度。这留作读者的练习回来完成。

让我们设计这两个对话框窗口的布局。

实施对话框设计

现在,我们将做一些我们以前做过很多次的事情,但这次是出于新的原因。正如我们所知,我们将有两个对话框窗口:一个用于用户输入新的笔记,另一个用于查看他们选择的笔记。

我们可以以与我们设计所有先前布局相同的方式设计这两个对话框窗口的布局。当我们开始为FragmentDialog类创建 Java 代码时,我们将学习如何将这些布局结合起来。

首先,让我们为“新笔记”对话框添加一个布局。执行以下步骤:

  1. 右键单击dialog_new_note以选择**文件名:**字段。

  2. 默认情况下,左键单击ConstraintLayout类型,作为其根元素。

  3. 在按照以下说明进行操作时,请参考目标设计图。我已经使用 Photoshop 完成了布局,包括我们即将自动生成的约束条件,以及隐藏约束条件以增加清晰度的布局:图 14.06 – 新笔记的完成布局

图 14.06 – 新笔记的完成布局

  1. 文本类别中拖放一个纯文本小部件到布局的左上角。然后,在其下方添加另一个纯文本小部件。暂时不用担心任何属性。

  2. 按钮类别中拖放三个复选框小部件,一个放在另一个下面。查看之前的参考图以获得指导。现在,暂时不用担心任何属性。

  3. 将两个按钮拖放到布局中。第一个将直接放在上一步中最后一个复选框小部件的下方;第二个将水平放置,与第一个按钮对齐,但在布局的右侧。

  4. 整理布局,使其尽可能接近参考设计图。然后,点击推断约束条件按钮来修复您选择的位置。

  5. 现在,我们可以设置所有的textidhint属性。您可以使用以下表格中的值来完成这一步。请记住,我们正在使用字符串资源来为texthint属性赋值:

注意

当您编辑第一个id属性(接下来我们将要做的)时,可能会弹出一个窗口询问您是否确认更改。勾选本次会话中不再询问框,然后点击继续:

图 14.07 – 确认更改

图 14.07 – 确认更改

我们现在有一个有组织的布局,准备好供我们的 Java 代码显示。确保您记住不同小部件的id属性值。当我们编写 Java 代码时,我们将看到它们的作用。重要的是,我们的布局看起来很好,并且每个相关项目都有一个id属性值,这样我们就可以引用它。

让我们创建“显示笔记”对话框的布局:

  1. 右键单击dialog_show_note以选择**文件名:**字段。

  2. 默认情况下,左键单击ConstraintLayout类型以选择其根元素。

  3. 在按照以下说明进行操作时,请参考目标设计图。我已经使用 Photoshop 完成了布局,包括我们即将自动生成的约束条件,以及隐藏约束条件以增加清晰度的布局:图 14.08 – 显示笔记对话框的完成布局

图 14.08 - 显示笔记对话框的完成布局

  1. 首先,拖放三个TextView小部件,使它们在布局顶部垂直对齐。

  2. 接下来,拖放另一个TextView小部件。

  3. 在上一个小部件的下方但靠左添加另一个TextView小部件。

  4. 现在在布局的中心水平放置一个Button,但靠近底部。

  5. 整理布局,使其尽可能接近参考图表,然后点击Infer Constraints按钮来修复你选择的位置。

  6. 配置以下表中的属性:

注意

你可能想要通过稍微拖动它们来调整一些 UI 元素的最终位置,因为我们调整了它们的大小和内容。首先,点击btnOK到按钮 ID,可能会弹出一个对话框说这个 ID 已经存在,点击Continue来忽略弹出窗口。

现在我们有一个布局,可以用来向用户显示一个笔记。请注意,我们可以重复使用一些字符串资源。我们的应用程序变得越来越大,以这种方式做事情就越有益处。

编写对话框

现在我们已经为我们的两个对话框窗口(“显示笔记”和“新笔记”)设计好了,我们可以利用我们对FragmentDialog类的了解来实现一个类来代表用户可以交互的每个对话框窗口。

我们将从“新笔记”屏幕开始。

编写 DialogNewNote 类

通过右键单击包含所有.java文件的项目文件夹,选择DialogNewNote来创建一个新类。

首先,更改类声明并扩展DialogFragment。然后,重写onCreateDialog方法,这是这个类中所有其余代码的位置。为了实现这一点,确保你的代码与以下代码片段相同:

public class DialogNewNote extends DialogFragment { 
   @Override
   public Dialog onCreateDialog(Bundle savedInstanceState) {

         // All the rest of the code goes here

   }
}

注意

你还需要添加这些新的导入:

import androidx.fragment.app.DialogFragment;

import android.app.Dialog;

import android.os.Bundle;

我们暂时在新类中有一个错误,因为我们需要一个return语句;然而,我们马上就会解决这个问题。

在下一段代码中,首先,我们声明并初始化一个AlertDialog.Builder对象,方式与我们之前创建对话框窗口时一样。然而,这一次,我们将比以前更少地依赖这个对象。

接下来,我们初始化一个LayoutInflater对象,我们将使用它来填充我们的 XML 布局。通过“填充”,我们只是指的是如何将我们的 XML 布局转换为 Java 对象。一旦完成了这一步,我们就可以以通常的方式访问所有的小部件。我们可以将inflater.inflate方法视为替换对话框的setContentView方法。然后,在第二行,我们使用inflate方法做到了这一点。

添加我们刚刚讨论的三行代码:

AlertDialog.Builder builder = 
   new AlertDialog.Builder(getActivity());
LayoutInflater inflater = 
   getActivity().getLayoutInflater();

View dialogView = 
   inflater.inflate(R.layout.dialog_new_note, null);

注意

为了使用前面三行代码中的新类,你需要添加以下import语句:

import androidx.appcompat.app.AlertDialog;

import android.view.View;

import android.view.LayoutInflater;

现在我们有一个名为dialogViewView对象,它包含了我们的dialog_new_note.xml布局文件中的所有 UI 元素。

在上一段代码之后,我们将添加下面的代码块,接下来我们将解释。

这段代码将以通常的方式获取每个 UI 小部件的引用。以下代码中的许多对象被声明为final,因为它们将被用于匿名类,正如我们之前学到的那样,这是必要的。请记住,它是引用是final的(也就是说,它不能改变);我们仍然可以改变它们指向的堆上的对象。

在上一段代码之后添加以下代码:

final EditText editTitle = 
dialogView.findViewById(R.id.editTitle);
final EditText editDescription = 
dialogView.findViewById(R.id.editDescription);
final CheckBox checkBoxIdea =
dialogView.findViewById(R.id.checkBoxIdea);
final CheckBox checkBoxTodo =
dialogView.findViewById(R.id.checkBoxTodo);
final CheckBox checkBoxImportant = 
dialogView.findViewById(R.id.checkBoxImportant);
Button btnCancel = 
dialogView.findViewById(R.id.btnCancel);
Button btnOK = 
dialogView.findViewById(R.id.btnOK);

注意

添加以下import代码语句,使你刚刚添加的代码无错误:

import android.widget.Button;

import android.widget.CheckBox;

import android.widget.EditText;

在下一个代码块中,我们将使用builder(我们的构建器实例)设置对话框的消息。然后,我们将编写一个匿名类来处理对btnCancel按钮的点击。在重写的onClick方法中,我们将简单地调用dismiss(),这是DialogFragment的一个公共方法,用于关闭对话框窗口。如果用户单击取消,这正是我们需要的。

添加我们刚讨论过的以下代码:

builder.setView(dialogView).setMessage("Add a new note");
// Handle the cancel button
btnCancel.setOnClickListener( new View.OnClickListener() {
   @Override
   public void onClick(View v) {
         dismiss();
   }
});

现在,我们将添加一个匿名类来处理用户单击btnOK时发生的情况。

首先,我们创建了一个名为newNote的新Note实例。然后,我们将newNote的每个成员变量设置为表单的适当内容。

在此之后,我们做了一些新的事情。我们使用getActivity方法创建对MainActivity类的引用。然后,我们使用该引用调用MainActivity中的createNewNote方法。

注意

请注意,我们还没有编写createNewNote方法,直到本章后面我们才会这样做。

发送到此方法的参数是我们新初始化的newNote对象。这将使用户的新笔记发送回MainActivity。我们将在本章后面学习如何处理这个。

最后,我们调用dismiss来关闭对话框窗口。

在前面的代码之后添加我们刚讨论过的以下代码:

btnOK.setOnClickListener(new View.OnClickListener() {

   @Override
   public void onClick(View v) {

         // Create a new note
         Note newNote = new Note();
         // Set its variables to match the 
         // user's entries on the form
         newNote.setTitle(editTitle.
                getText().toString());

         newNote.setDescription(editDescription.
                getText().toString());

         newNote.setIdea(checkBoxIdea.isChecked());
         newNote.setTodo(checkBoxTodo.isChecked());
         newNote.setImportant(checkBoxImportant.
                isChecked());
         // Get a reference to MainActivity
         MainActivity callingActivity = 
                (MainActivity) getActivity();

         // Pass newNote back to MainActivity
         callingActivity.createNewNote(newNote);
         // Quit the dialog
         dismiss();
   }
});
return builder.create();

这是我们的第一个对话框。我们还没有将其连接到MainActivity中,我们还需要实现createNewNote方法。我们将在创建下一个对话框后立即执行此操作。

编写 DialogShowNote 类

通过右键单击包含所有.java文件的项目文件夹并选择DialogShowNote来创建一个新类。

首先,更改类声明并扩展DialogFragment。然后,重写onCreateDialog方法。由于该类的大部分代码都在onCreateDialog方法中,因此实现签名和空主体,如下面的代码片段所示,我们稍后会重新访问它。

请注意,我们声明了一个Note类型的成员变量mNote。添加sendNoteSelected方法和初始化mNote的一行代码。此方法将由MainActivity调用,并将用户单击的Note对象传递给它。

添加我们刚讨论过的代码。然后,我们可以查看并编写onCreateDialog的细节:

public class DialogShowNote extends DialogFragment {
    private Note mNote;
    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        // All the other code goes here
    }
    // Receive a note from the MainActivity
    public void sendNoteSelected(Note noteSelected) {
        mNote = noteSelected;
    }
}

注意

此时,您需要导入以下类:

import android.app.Dialog;

import android.os.Bundle;

import androidx.fragment.app.DialogFragment;

接下来,我们声明并初始化了一个AlertDialog.Builder的实例。另外,就像我们对DialogNewNote类所做的那样,我们声明并初始化了一个LayoutInflater实例,然后使用它来创建一个具有对话框布局的View对象。在这种情况下,它是来自dialog_show_note.xml的布局。

最后,在以下代码中,我们获取对每个 UI 小部件的引用,并将text属性设置为mNote的适当成员变量的txtTitletextDescription,该成员变量在sendNoteSelected方法中初始化。

将我们刚讨论过的代码添加到onCreateDialog方法中:

// All the other code goes here
AlertDialog.Builder builder = 
      new AlertDialog.Builder(getActivity());
LayoutInflater inflater = 
      getActivity().getLayoutInflater();

View dialogView = 
      inflater.inflate(R.layout.dialog_show_note, null);
TextView txtTitle = 
       dialogView.findViewById(R.id.txtTitle);
TextView txtDescription = 
       dialogView.findViewById(R.id.txtDescription);
txtTitle.setText(mNote.getTitle());
txtDescription.setText(mNote.getDescription());
TextView txtImportant = 
       dialogView.findViewById(R.id.textViewImportant);
TextView txtTodo = 
       dialogView.findViewById(R.id.textViewTodo);
TextView txtIdea = 
       dialogView.findViewById(R.id.textViewIdea);

注意

添加以下import语句,以便前面代码中的所有类都可用:

import android.view.LayoutInflater;

import android.view.View;

import android.widget.TextView;

import androidx.appcompat.app.AlertDialog;

我们将添加的以下代码也在onCreateDialog方法中。它检查所显示的笔记是否“重要”,然后相应地显示或隐藏txtImportant TextView。然后我们对txtTodotxtIdea做同样的事情。

在仍然在onCreateDialog方法中时,在前一个代码块之后添加此代码:

if (!mNote.isImportant()){
   txtImportant.setVisibility(View.GONE);
}
if (!mNote.isTodo()){
   txtTodo.setVisibility(View.GONE);
}
if (!mNote.isIdea()){
   txtIdea.setVisibility(View.GONE);
}

现在,当用户单击onClick方法时,我们只需要dismiss(关闭)对话框窗口,该方法简单地调用dismiss方法,关闭对话框窗口。

在上一个代码块之后,将此代码添加到onCreateDialog方法中:

Button btnOK = (Button) dialogView.findViewById(R.id.btnOK);
builder.setView(dialogView).setMessage("Your Note");
btnOK.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View v) {
         dismiss();
   }
});
return builder.create();

注意

使用这行代码导入Button类:

import android.widget.Button;

我们现在有两个准备就绪的对话框窗口。我们只需要向MainActivity类添加一些代码来完成工作。首先,让我们进行一些项目清理。

删除不需要的自动生成片段

现在我们将整理项目的文件和结构。请记住,我们用于生成此项目的基本活动模板具有许多功能。有些是我们需要的,而有些则不需要。我们希望浮动操作按钮位于布局的右下角,并且希望主菜单与content_main.xml文件一起,我们将删除对片段的引用以及其所有导航选项。

打开content_main.xml布局文件。在content_main.xml文件中,找到nav_host_fragment元素。选择它,然后按键盘上的Delete键。现在,我们有一个更干净的 UI,可以用于未来的开发。

显示我们的新对话框

打开MainActivity.java文件。在MainActivity类声明之后添加一个新的临时成员变量。这不会出现在最终的应用程序中;这样我们就可以尽快测试我们的对话框窗口:

// Temporary code
Note mTempNote = new Note();

现在,将此方法添加到MainActivity类中,以便我们可以从DialogNewNote类接收一个新的笔记:

public void createNewNote(Note n){
   // Temporary code
   mTempNote = n;
}

要将笔记发送到DialogShowNote方法,我们需要在content_main.xml布局文件中添加一个 ID 为button的按钮。打开content_main.xml布局文件。

为了清楚起见,我们将把这个按钮的text属性更改为Show Note,如下所示:

  • 将一个按钮拖放到content_main.xml布局中,并将id属性配置为buttontext属性配置为Show Note

  • 单击推断约束按钮,使按钮停留在您放置的位置。此时此刻,此按钮的确切位置并不重要。

注意

只是为了澄清,这是一个用于测试目的的临时按钮,不会出现在最终的应用程序中。在开发结束时,我们将能够从滚动列表中点击笔记的标题。

onCreate方法中,我们将设置一个匿名类来处理我们临时按钮的点击。onClick方法中的代码将执行以下操作:

  • 创建一个名为dialog的新DialogShowNote实例。

  • dialog上调用sendNoteSelected方法,将mTempNote作为参数传入,这是我们的Note对象。

  • 最后,它将调用show,为我们的新对话框注入生命并展示给用户。

将我们刚刚描述的代码添加到onCreate中:

// Temporary code
Button button = findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View v) {
         // Create a new DialogShowNote called dialog
         DialogShowNote dialog = new DialogShowNote();

         // Send the note via the sendNoteSelected method
         dialog.sendNoteSelected(mTempNote);

         // Create the dialog
         dialog.show(getSupportFragmentManager(), "123");
   }
});

注意

使用这行代码添加Button类:

import android.widget.Button;

现在我们可以在点击按钮时召唤我们的DialogShowNote对话框窗口。运行应用程序,并点击具有dialog_show_note.xml布局的DialogShowNote对话框:

图 14.09 – DialogShowNote 对话框

图 14.09 – DialogShowNote 对话框

承认,考虑到我们在本章中所做的大量编码,这并不是什么了不起的东西。然而,当我们让DialogNewNote类工作时,我们将能够看到MainActivity类如何在这两个对话框之间交互和共享数据。

接下来,让DialogNewNote对话框可用。

编写浮动操作按钮

这将很容易。浮动操作按钮已经在布局中为我们提供。提醒一下,浮动操作按钮是圆形图标,上面有一个信封图像,就像前面截图的右下角所示。

它在activity_main.xml文件中。这是定位和定义其外观的 XML 代码。请注意,在浮动操作按钮的代码之前,有一行代码(已突出显示)包括content_main.xml文件。这目前包含我们的Show Note按钮,并最终将包含我们复杂的滚动列表:

…
…
<include layout="@layout/content_main" />
<com.google.android.material
     .floatingactionbutton.FloatingActionButton

     android:id="@+id/fab"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:layout_gravity="bottom|end"
     android:layout_margin="@dimen/fab_margin"
     app:srcCompat="@android:drawable/ic_dialog_email" />
…
…

Android Studio 甚至提供了一个匿名类来处理浮动操作按钮上的任何点击。我们只需要向这个已提供的类的onClick方法添加一些代码,就可以使用DialogNewNote类。

浮动操作按钮通常用于应用程序中的核心操作。例如,在电子邮件应用程序中,它可能用于启动新的电子邮件。或者,在笔记应用程序中,它可能用于添加新的笔记。所以,现在让我们这样做。

MainActivity.java文件中,找到 Android Studio 在MainActivity类的onCreate方法中提供的自动生成代码。以下是完整的代码:

fab.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View view) {
            Snackbar.make(view, "Replace with your own 
action", 
                          Snackbar.LENGTH_LONG)
.setAction("Action", 
                          null).show();

   }
});

在上述代码片段中,请注意突出显示的行并将其删除。现在,在删除的代码位置添加以下代码:

DialogNewNote dialog = new DialogNewNote();
dialog.show(getSupportFragmentManager(), "");

新代码创建了一个DialogNewNote类型的新对话框窗口,然后显示给用户。

现在我们可以运行应用程序。点击浮动操作按钮,并添加一个类似于以下截图的笔记:

图 14.10 – 添加新笔记

图 14.10 – 添加新笔记

接下来,我们可以点击显示笔记按钮,以在对话框中查看它,如下所示:

图 14.11 – 显示笔记对话框窗口

图 14.11 – 显示笔记对话框窗口

请注意,如果您添加第二个笔记,它将覆盖第一个,因为我们只有一个Note对象。我们需要学习更多的 Java 知识来解决这个问题。

总结

在本章中,我们讨论并实现了使用DialogFragment类的对话框窗口的常见 UI 设计。

然后,我们更进一步,通过实现更复杂的对话框来启动 Note to Self 应用程序,这些对话框可以从用户那里获取信息。我们看到DialogFragment类使我们能够在对话框中设计任何 UI。

在下一章中,我们将处理一个明显的问题,即用户只能有一个笔记。我们将通过探索 Java 数组及其近亲ArrayList,以及另一个与数据相关的 Java 类Map来解决这个问题。