六、提醒 Lab:第二部分
本章介绍如何通过使用自定义对话框来捕获用户输入。我们还将继续演示适配器和 SQLite 数据库的使用。在本章中,我们将完成从第五章开始的实验。
添加/删除提醒
第五章中的例子让屏幕空着,没有任何提醒。要查看带有提醒列表的应用布局,在应用启动时添加一些示例提醒很有用。如果您试图对前一章的挑战提出一个解决方案,将您的代码与清单 6-1 中的变化进行比较。清单 6-1 中的代码检查实例是否有任何保存的状态,如果没有,它继续设置示例数据。为此,代码调用了DatabaseAdapter
上的一些方法;一个用于清除所有提醒,另一个用于插入一些提醒。
Listing 6-1. Add Some Example Reminders
public class RemindersActivity extends ActionBarActivity {
private ListView mListView;
private RemindersDbAdapter mDbAdapter;
private RemindersSimpleCursorAdapter mCursorAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_reminders);
mListView = (ListView) findViewById(R.id.reminders_list_view);
mListView.setDivider(null);
mDbAdapter = new RemindersDbAdapter(this);
mDbAdapter.open();
if (savedInstanceState == null) {
//Clear all data
mDbAdapter.deleteAllReminders();
//Add some data
mDbAdapter.createReminder("Buy Learn Android Studio", true);
mDbAdapter.createReminder("Send Dad birthday gift", false);
mDbAdapter.createReminder("Dinner at the Gage on Friday", false);
mDbAdapter.createReminder("String squash racket", false);
mDbAdapter.createReminder("Shovel and salt walkways", false);
mDbAdapter.createReminder("Prepare Advanced Android syllabus", true);
mDbAdapter.createReminder("Buy new office chair", false);
mDbAdapter.createReminder("Call Auto-body shop for quote", false);
mDbAdapter.createReminder("Renew membership to club", false);
mDbAdapter.createReminder("Buy new Galaxy Android phone", true);
mDbAdapter.createReminder("Sell old Android phone - auction", false);
mDbAdapter.createReminder("Buy new paddles for kayaks", false);
mDbAdapter.createReminder("Call accountant about tax returns", false);
mDbAdapter.createReminder("Buy 300,000 shares of Google", false);
mDbAdapter.createReminder("Call the Dalai Lama back", true);
}
//Removed remaining method code for brevity...
}
//Removed remaining method code for brevity...
}
有几个对createReminder()
方法的调用,每个调用都接受一个带有提醒文本的字符串值和一个将提醒标记为重要的布尔值。我们为true
设置了几个值,以提供良好的视觉效果。在所有的createReminder()
调用周围单击并拖动选择,然后按 Ctrl+Alt+M | Cmd+Alt+M,弹出提取方法对话框,如图 6-1 所示。这是通过“重构”菜单和快捷键组合可用的许多重构操作之一。输入 insertSomeReminders 作为新方法的名称,然后按 Enter 键。RemindersActivity
中的代码将被替换为对您在提取方法对话框中命名的新方法的调用,并且代码将被移动到该方法的主体中。
图 6-1。
Extract Method dialog box, create insertSomeReminders( ) method
运行应用,查看界面的外观和使用示例提醒的行为。你的应用应该看起来像图 6-2 中的截图。一些提醒应该用绿色的行标签显示,而那些标记为重要的会用橙色标签显示。使用消息“添加示例提醒”提交您的更改。
图 6-2。
Runtime with example reminders inserted
响应用户交互
任何应用都没有多大用处,除非它对输入做出响应。在本节中,您将添加逻辑来响应触摸事件,并最终允许用户编辑各个提醒。应用中的主要组件是 Android View
对象的子类ListView
。到目前为止,除了将View
对象放置在布局中,你还没有对它们做什么。android.view.View
对象是所有绘制到屏幕上的组件的超类。
将清单 6-2 中的代码添加到RemindersActivity
中onCreate()
方法的底部,就在右花括号之前,然后解析 imports。这是一个匿名的内部类实现OnItemClickListener
,它只有一个方法onItemClicked()
。当您与它所附加的ListView
组件交互时,运行时将使用这个对象。每当你点击ListView
时,匿名内部类的onCreate()
方法将被调用。我们定义的方法使用了Toast
,Android SDK 中的一个类。对Toast.makeText()
的调用会在屏幕上显示一个小弹出窗口,显示传递给该方法的任何文本。您可以使用Toast
作为方法被正确调用的快速指示器,如清单 6-2 所示。
Note
某些设备可能会隐藏信息。另一种方法是使用 Android logger 记录消息,这在第十二章中有详细介绍。
Listing 6-2. Set an OnItemClickListener with a Toast
//when we click an individual item in the listview
mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Toast.makeText(RemindersActivity.this, "clicked " + position,
Toast.LENGTH_SHORT).show();
}
});
单击列表中的第一项会调用onItemClick()
方法,该方法的位置值为 0,因为列表中的元素从 0 开始进行索引。然后逻辑弹出一个带有点击文本和位置的祝酒词,如图 6-3 所示。
图 6-3。
Toast message after tapping the first reminder
用户对话框
熟悉了触摸事件之后,您现在可以增强 click listener 来显示对话框。用清单 6-3 中的代码替换整个onItemClick()
方法。当你解析导入时,请使用android.support.v7.app.AlertDialog
类。
Listing 6-3. onItemClick( ) Modifications to Allow Edit/Delete
public void onItemClick(AdapterView<?> parent, View view, final int masterListPosition, long id) {
AlertDialog.Builder builder = new AlertDialog.Builder(RemindersActivity.this);
ListView modeListView = new ListView(RemindersActivity.this);
String[] modes = new String[] { "Edit Reminder", "Delete Reminder" };
ArrayAdapter<String> modeAdapter = new ArrayAdapter<>(RemindersActivity.this,
android.R.layout.simple_list_item_1, android.R.id.text1, modes);
modeListView.setAdapter(modeAdapter);
builder.setView(modeListView);
final Dialog dialog = builder.create();
dialog.show();
modeListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
//edit reminder
if (position == 0) {
Toast.makeText(RemindersActivity.this, "edit "
+ masterListPosition, Toast.LENGTH_SHORT).show();
//delete reminder
} else {
Toast.makeText(RemindersActivity.this, "delete "
+ masterListPosition, Toast.LENGTH_SHORT).show();
}
dialog.dismiss();
}
});
}
在前面的代码中,您看到了另一个正在工作的 Android 类,AlertDialog.Builder
。类Builder
是嵌套在AlertDialog
类中的静态类,它用于构建AlertDialog
。
到目前为止,这个 Lab 中的代码创建了一个ListView
和一个ArrayAdapter
来向一个ListView
提供项目。你可能还记得第五章中的这个模式。在传递给ListView
之前,该适配器由两个潜在选项组成的数组创建,编辑提醒和删除提醒,然后传递给AlertDialog.Builder
。然后使用生成器创建并显示一个带有选项列表的对话框。
请特别注意清单 6-3 中的最后一段代码。它类似于前面添加的OnItemClickListener()
代码;然而,我们在当前的OnItemClickListener
中创建的modeListView
上附加了一个点击监听器。您看到的是一个带有OnItemClickListener
的ListView
,它创建了另一个modeListView
和另一个嵌套的OnItemClickListener
来响应modeListView
的点击事件。
嵌套的 click 侦听器弹出一条 toast 消息,指示是否点击了编辑或删除项。它还将外层OnItemClickListener
的位置参数重命名为masterListPosition
,以区别于嵌套OnItemClickListener
中的position
参数。这个主位置在 toast 中用于指示哪个提醒可能被编辑或删除。最后,从点击监听器中调用dialog.dismiss()
方法,这将完全删除对话框。
通过在您的设备或仿真器上运行来测试图 6-4 中所示的新功能。点击提醒事项,然后在新弹出对话框中点击编辑提醒事项或删除提醒事项。如果 toast 中报告的提醒位置与您点击的提醒不匹配,请仔细检查您是否将masterListPosition
值附加到了 toast 中的文本,并且没有使用position
。按 Ctrl+K | Cmd+K 提交此逻辑,并使用消息为单个列表项添加一个 ListView 对话框。
图 6-4。
Simulating the deletion of a reminder
提供多选上下文菜单
随着应用开始成形,你现在将攻击一个允许在一次操作中编辑多个提醒的功能。此功能仅在运行 API 11 和更高版本的设备上可用。您将通过使用资源加载约定,使该特性在应用中有条件地可用。这一过程将在本章稍后解释,并在第八章中详细说明。您还需要在运行时进行检查,以决定是否启用该特性。
首先为提醒行项目创建一个备用布局。打开项目工具窗口,右键单击res
文件夹,弹出上下文菜单。从菜单中选择新建 Android 资源文件,在对话框中输入 reminders_row 作为名称,如图 6-5 所示。
图 6-5。
New resource file for reminders_row
选择 Layout 作为资源类型,这将自动将目录名更改为layout
。在可用限定词部分下选择版本,然后单击双 v 形(> >)按钮将版本添加到所选限定词列表中。输入 11 作为平台 API 级别,注意目录名已经更新,以反映所选的限定符。这些被称为资源限定符,它们在运行时被询问,以允许您为特定的设备和平台版本定制用户界面。按 Enter 键(或单击 OK)接受这个新的资源限定目录并继续。如果你打开项目工具窗口,并将其视图设置为 Android,如图 6-6 所示,你会看到两个reminders_row
布局文件被分组在layout
文件夹下。同样,项目窗口的 Android 视图将相关文件分组在一起,让您可以有效地管理它们。
图 6-6。
Grouped layouts
复制整个原始reminders_row
布局并粘贴到版本 11 新创建的布局中。现在使用下面的代码改变内部水平LinearLayout
的background
属性:
android:background="?android:attr/activatedBackgroundIndicator"
这个赋给背景属性的值以?android:attr/
为前缀,它指的是 Android SDK 中定义的一种样式。Android SDK 提供了许多这样的预定义属性,你可以在你的应用中使用它们。属性使用系统定义的颜色作为多选模式下激活的项目的背景。
针对早期的 SDK
现在您将学习如何引入一个平台相关的特性。打开项目工具窗口,并打开 Gradle 脚本部分下的app
模块的build.gradle
文件(这将是第二个条目)。这些 Gradle 文件包含编译和打包应用的构建逻辑。所有关于你的应用支持哪些平台的配置都位于这些特殊的文件中(第十三章深入探讨了 Gradle build 系统)。请注意,minSdkVersion 设置为 8,这允许您的应用在 99%以上的 Android 设备上运行。我们将要创建的特性需要最低版本为 11 的 SDK (aka API)。我们在本节中介绍的代码和特性将允许运行 SDK 11 或更高版本的用户利用一个称为上下文动作模式的特性。此外,那些运行 SDK 版本低于 11 的人将不会看到这个功能,但更重要的是,他们的应用不会崩溃。
添加上下文操作模式
下一个特性在多选模式下引入了一个上下文动作菜单,这是一个动作列表,可以应用于所有选中项目的上下文。通过右键单击 res/menu 目录并选择 New ➤菜单资源文件来添加一个新的菜单资源,并将其命名为cam_menu
。用下面的代码来修饰它:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="``http://schemas.android.com/apk/res/android
<item android:id="@+id/menu_item_delete_reminder"
android:icon="@android:drawable/ic_menu_delete"
android:title="delete" />
</menu>
这个资源文件为上下文菜单定义了一个单独的delete
动作项。这里您也使用了稍微不同的属性值。这些特殊值类似于您之前在background
属性中使用的值,因为它们允许您访问内置的 Android 默认值。然而,?android:attr/
前缀仅在引用样式属性时使用。这些属性中使用的语法遵循稍微不同的形式。使用 at 符号(@
)触发资源值的名称空间查找。您可以通过这种方式访问各种名称空间。android
名称空间是所有内置 Android 值所在的位置。在这个名称空间中有各种资源位置,例如drawable
、string
和layout
。当您使用特殊的@+id
前缀时,它会在项目的 R.java 文件中创建一个新的 ID,当您使用@id
前缀时,它会在 Android SDK 的 R.java 文件中查找一个现有的 ID。这个例子定义了一个新的 ID 名menu_item_delete_reminder
,它与菜单选项相关联。它还从名称空间android:drawable
中取出一个图标,用作它的图标。
使用新的上下文菜单和运行 API 11 或更高版本的设备的备用布局,您可以添加一个复选标记,以有条件地启用带有上下文操作菜单的多选模式。打开RemindersActivity
并在onCreate
方法的末尾添加以下if
块:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
}
Build
类是从android.os
包中导入的,它让您可以访问一组常数值,这些常数值可以用来匹配具有特定 API 级别的设备。在这种情况下,您希望 API 级别等于或高于包含整数值 11 的HONEYCOMB
。将清单 6-4 中的代码插入刚刚定义的 if 块中。if 块保护运行低于 Honeycomb 的操作系统的设备,如果没有 Honeycomb,应用将会崩溃。
Listing 6-4. MultiChoiceModeListener Example
mListView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL);
mListView.setMultiChoiceModeListener(new AbsListView.MultiChoiceModeListener() {
@Override
public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) { }
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
MenuInflater inflater = mode.getMenuInflater();
inflater.inflate(R.menu.cam_menu, menu);
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_item_delete_reminder:
for (int nC = mCursorAdapter.getCount() - 1; nC >= 0; nC--) {
if (mListView.isItemChecked(nC)) {
mDbAdapter.deleteReminderById(getIdFromPosition(nC));
}
}
mode.finish();
mCursorAdapter.changeCursor(mDbAdapter.fetchAllReminders());
return true;
}
return false;
}
@Override
public void onDestroyActionMode(ActionMode mode) { }
});
解决任何导入。您会注意到 getIdFromPositon()没有被定义,并且被标记为红色。将光标放在方法上,按 Alt+Enter 调用IntelliSense
并选择创建方法。选择 RemindersActivity 作为目标类。选择int
作为返回值。装饰方法如清单 6-5 所示。
Listing 6-5. getIdFromPosition() method
private int getIdFromPosition(int nC) {
return (int)mCursorAdapter.getItemId(nC);
}
前面的逻辑定义了一个MultiChoiceModeListener
并将它附加到ListView
上。每当您长按ListView
中的一个项目时,运行时就会调用MultiChoiceModeListener
上的onCreateActionMode()
方法。如果方法返回布尔值true
,则进入多选操作模式。在这种模式下,这里被重写的方法中的逻辑会展开一个显示在操作栏中的上下文菜单。使用多选操作模式的好处是可以选择多行。一次点击选择项目,随后的点击取消选择项目。当您点击上下文菜单中的每一项时,运行时将使用被点击的菜单项调用onActionItemClicked()
方法。
在这个方法中,通过比较添加到菜单项的删除元素的itemId
和id
来检查删除项是否被点击。(有关删除项 ID 的描述,请参见本节开头的 XML 清单。)如果项目被选中,您循环遍历每个列表项目,并请求mDbAdapter
删除它们。删除选中的项目后,逻辑调用ActionMode
对象上的finish()
,这将禁用多选操作模式,并将ListView
返回到正常状态。接下来,调用fetchAllReminders()
从数据库中重新加载所有提醒,并将调用返回的光标传递给mCursorAdapter
对象上的changeCursor
方法。最后,该方法返回true
来指示该动作已经被正确处理。在不处理逻辑的所有其他情况下,该方法返回false
,表明其他一些事件侦听器可以处理 tap 事件。
Android Studio 将突出显示几个错误语句,因为您使用的 API 在比 Honeycomb 更老的平台上不可用。这个错误是由 Lint 生成的,Lint 是一个内置于 Android SDK 中的静态分析工具,并完全集成到 Android Studio 中。您需要在@Override
注释的上方或下方向RemindersActivity.onCreate()
方法添加以下注释,并解析 TargetApi 的导入:
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
这个特殊的注释告诉 Lint,无论构建配置指定什么,都要将方法调用视为针对所提供的 API 级别。将您的更改提交给 Git,并显示消息“使用上下文操作菜单添加上下文操作模式”。图 6-7 描绘了当您构建并运行应用来测试新功能时可能会看到的情况。
图 6-7。
Multichoice mode enabled
实现添加、编辑和删除
到目前为止,您已经添加了从列表中删除提醒的逻辑。该逻辑仅在上下文动作模式下可用。您目前无法插入新的提醒或修改现有的提醒。但是,现在您将创建一个自定义对话框来添加提醒,并创建另一个对话框来编辑现有的提醒。最终,您会将这些对话框绑定到RemindersDbAdapter
。
在继续之前,您需要定义一些额外的颜色。将以下颜色定义添加到您的colors.xml
文件中:
<color name="light_grey">#bababa</color>
<color name="black">#000000</color>
<color name="blue">#ff1118ff</color>
Note
通常,你的应用会有一个整体的颜色主题,这将确保所有屏幕和对话框之间的一致性。然而,颜色主题超出了这个简单实验的范围。
规划自定义对话框
要养成的一个好习惯是,在实现 UI 之前,先用简单的工具画出草图。这样做可以让您在编写任何代码之前,直观地了解元素在屏幕上的位置。你可以使用跨平台的编辑器,比如 Inkscape,或者你可以使用像笔记本纸和铅笔这样简单的东西。在移动业务中,这些草图被称为线框。
图 6-8 是用 Inkscape 完成的自定义对话框的图示。线框是有意非正式的,强调组件的位置,而不是特定的外观和感觉。
图 6-8。
Wireframe sketch of the custom dialog box Note
本书中的一些自定义插图和线框是使用多平台矢量图形编辑器 Inkscape 创建的。可在 www.inkscape.org
免费获取。
有了线框,你可以开始计划如何在屏幕上排列组件。因为大多数组件从上到下流动,所以对最外层的容器使用垂直的LinearLayout
是一个明显的选择。但是,底部的两个按钮是并排的。对于这些你可以使用一个水平的LinearLayout
并将其嵌套在包含垂直的LinearLayout
中。图 6-9 向图纸添加注释并突出显示该嵌套组件。
图 6-9。
Wireframe sketch with widget labels
从计划到代码
有了这些线框之后,尝试使用可视化设计器来设计布局。首先右键单击项目工具窗口中的res
目录,选择创建新的 Android 资源文件选项,将资源文件命名为 dialog_custom,然后选择 Layout 作为资源类型。使用LinearLayout
作为根元素完成对话框。为了重现我们的线框,从面板拖放视图到舞台上。清单 6-6 包含了完整的布局 XML 定义,以及您将在 Java 代码中使用的 ID 值。
Listing 6-6. Completed dialog_custom.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="``http://schemas.android.com/apk/res/android
android:id="@+id/custom_root_layout"
android:layout_width="300dp"
android:layout_height="fill_parent"
android:background="@color/green"
android:orientation="vertical"
>
<TextView
android:id="@+id/custom_title"
android:layout_width="fill_parent"
android:layout_height="60dp"
android:gravity="center_vertical"
android:padding="10dp"
android:text="New Reminder:"
android:textColor="@color/white"
android:textSize="24sp" />
<EditText
android:id="@+id/custom_edit_reminder"
android:layout_width="fill_parent"
android:layout_height="100dp"
android:layout_margin="4dp"
android:background="@color/light_grey"
android:gravity="start"
android:textColor="@color/black">
<requestFocus />
</EditText>
<CheckBox
android:id="@+id/custom_check_box"
android:layout_width="fill_parent"
android:layout_height="30dp"
android:layout_margin="4dp"
android:background="@color/black"
android:paddingLeft="32dp"
android:text="Important"
android:textColor="@color/white" />
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="horizontal">
<Button
android:id="@+id/custom_button_cancel"
android:layout_width="0dp"
android:layout_height="60dp"
android:layout_weight="50"
android:text="Cancel"
android:textColor="@color/white"
/>
<Button
android:id="@+id/custom_button_commit"
android:layout_width="0dp"
android:layout_height="60dp"
android:layout_weight="50"
android:text="Commit"
android:textColor="@color/white"
/>
</LinearLayout>
</LinearLayout>
创建自定义对话框
您现在将使用RemindersActivity
中已完成的对话框布局。清单 6-7 是一个新fireCustomDialog()
方法的实现。将这段代码放在RemindersActivity.java
文件中,就在onCreateOptionsMenu()
方法的上面,并解析导入。
Listing 6-7. The fireCustomDialog( ) Method
private void fireCustomDialog(final Reminder reminder){
// custom dialog
final Dialog dialog = new Dialog(this);
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
dialog.setContentView(R.layout.dialog_custom);
TextView titleView = (TextView) dialog.findViewById(R.id.custom_title);
final EditText editCustom = (EditText) dialog.findViewById(R.id.custom_edit_reminder);
Button commitButton = (Button) dialog.findViewById(R.id.custom_button_commit);
final CheckBox checkBox = (CheckBox) dialog.findViewById(R.id.custom_check_box);
LinearLayout rootLayout = (LinearLayout) dialog.findViewById(R.id.custom_root_layout);
final boolean isEditOperation = (reminder != null);
//this is for an edit
if (isEditOperation){
titleView.setText("Edit Reminder");
checkBox.setChecked(reminder.getImportant() == 1);
editCustom.setText(reminder.getContent());
rootLayout.setBackgroundColor(getResources().getColor(R.color.blue));
}
commitButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String reminderText = editCustom.getText().toString();
if (isEditOperation) {
Reminder reminderEdited = new Reminder(reminder.getId(),
reminderText, checkBox.isChecked() ? 1 : 0);
mDbAdapter.updateReminder(reminderEdited);
//this is for new reminder
} else {
mDbAdapter.createReminder(reminderText, checkBox.isChecked());
}
mCursorAdapter.changeCursor(mDbAdapter.fetchAllReminders());
dialog.dismiss();
}
});
Button buttonCancel = (Button) dialog.findViewById(R.id.custom_button_cancel);
buttonCancel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
dialog.dismiss();
}
});
dialog.show();
}
fireCustomDialog()方法将用于插入和编辑,因为这两种操作之间几乎没有区别。该方法的前三行创建了一个没有标题的 Android 对话框,并扩展了清单 6-6 中的布局。然后,fireCustomDialog()方法从这个布局中找到所有重要的元素,并将它们存储在局部变量中。然后,该方法通过检查提醒参数是否为空来设置一个isEditOperation
布尔变量。如果有一个提醒被传入(或者如果值不为空),该方法假定这不是一个编辑操作,并且变量被设置为false
;否则,设置为true
。如果对 fireCustomDialog()的调用是一个编辑操作,则标题被设置为编辑提醒,而CheckBox
和EditText
则使用来自提醒参数的值进行设置。该方法还将最外层容器布局的背景设置为蓝色,以便在视觉上区分编辑对话框和插入对话框。
接下来的几行代码组成了一个代码块,它为提交按钮设置并定义了一个OnClickListener
。这个侦听器通过更新数据库来响应提交按钮上的 click 事件。再次检查isEditOperation()
,如果正在进行编辑操作,则使用来自提醒参数的 ID 和来自EditText
的值以及屏幕上的复选框值创建一个新的提醒。这个提醒通过使用updateReminder()
方法传递给mDbAdapter
。
如果没有进行编辑,逻辑会要求mDbAdapter
使用来自EditText
的值和屏幕复选框值在数据库中创建一个新的提醒。在更新或创建调用被调用后,通过使用mCursorAdapter.changeCursor()
方法重新加载提醒。这个逻辑类似于您之前在清单 6-5 中添加的逻辑。提醒重新加载后,点击监听器关闭对话框。
在配置了“提交”按钮的单击行为之后,该示例为“取消”按钮设置了另一个单击侦听器。这个监听器简单地关闭对话框。指定了这两个按钮的行为后,该示例以显示自定义对话框结束。
现在你可以在OnItemClickListener
中使用这个新方法来代替onCreate()
方法中的modeListView
。找到这个监听器的onItemClick()
方法,并用下面的代码替换整个方法:
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
//edit reminder
if (position == 0) {
int nId = getIdFromPosition(masterListPosition);
Reminder reminder = mDbAdapter.fetchReminderById(nId);
fireCustomDialog(reminder);
//delete reminder
} else {
mDbAdapter.deleteReminderById(getIdFromPosition(masterListPosition));
mCursorAdapter.changeCursor(mDbAdapter.fetchAllReminders());
}
dialog.dismiss();
}
要编辑一个提醒,您可以使用ListView
位置,用一个查找提醒的调用替换Toast.makeText()
调用。这个提醒然后被传递给fireCustomDialog()
方法来触发编辑行为。要删除提醒,您可以使用与在多选模式下添加到清单 6-5 中的逻辑相同的逻辑。同样,mDbAdapter.deleteReminderById()
用于删除提醒,changeCursor()
方法用于从mDbAdapter.fetchAllReminders()
调用返回的光标。
在RemindersActivity.java
文件的最底部找到onOptionsItemSelected()
方法,并修改它,看起来像清单 6-8 。
Listing 6-8. onOptionsItemSelected Definition
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_new:
//create new Reminder
fireCustomDialog(null);
return true;
case R.id.action_exit:
finish();
return true;
default:
return false;
}
}
这里,当选择的菜单项是action_new item
时,您只需添加对fireCustomDialog()
的调用。您将null
传递给该方法,因为前面的逻辑会检查 null 值并将isEditOperation
设置为false
,从而调用一个新的提醒对话框。运行应用并测试新功能。您应该能够看到新的自定义对话框。创建提醒时会看到绿色对话框,编辑提醒时会看到蓝色对话框,分别如图 6-10 和图 6-11 所示。测试菜单项以确保创建和删除操作正常运行。使用 Commit 消息将您的更改提交到 Git,该消息通过自定义对话框添加了数据库创建、读取、更新和删除支持。
图 6-11。
Edit Reminder dialog box
图 6-10。
New Reminder dialog box
添加自定义图标
有了所有的功能,您可以添加一个自定义图标作为点睛之笔。您可以使用任何图像编辑器来创建图标,或者,如果您不喜欢图形,可以在网上找到一些免版税的剪贴画。我们的示例用在 Inkscape 中创建的自定义图稿替换了ic_launcher
图标。打开项目工具窗口,右键单击 res/mipmap 目录。现在选择新的➤图像素材。你会看到如图 6-12 所示的对话框。单击位于Image file:
字段最右侧的省略号按钮,导航至您创建的图像素材的位置。保留其余的设置,如图 6-13 所示。现在单击下一步,并在随后的对话框中单击完成。
图 6-13。
Custom icon in action bar
图 6-12。
New Image Asset dialog box
有许多名为mipmap
的文件夹。这些文件夹都有指定为屏幕大小限定符的后缀。Android 运行时将从特定的文件夹中提取资源,这取决于运行该应用的设备的屏幕分辨率。资源文件夹及其后缀在第八章中有更详细的介绍。
将以下代码行插入 RemindersActivity 的 onCreate()方法中,在展开布局的代码行之后,setContentView
(R.layout.activity_reminders
);。这段代码在您的操作栏中显示一个自定义图标:
ActionBar actionBar = getSupportActionBar();
actionBar.setHomeButtonEnabled(true);
actionBar.setDisplayShowHomeEnabled(true);
actionBar.setIcon(R.mipmap.ic_launcher);
当您运行代码时,您将在操作栏中看到您的自定义图标。图 6-13 显示了使用自定义图标运行的应用示例。
按 Ctrl+K | Cmd+K 并提交您的更改,同时显示消息“添加自定义图标”。
摘要
恭喜你!您已经使用 Android Studio 实现了您的第一个 Android 应用。在此过程中,您学习了如何使用可视化设计器编辑 XML 布局。您还了解了如何使用文本模式编辑原始 XML。本章向您展示了如何在支持该特性的平台上有条件地实现上下文动作模式。最后,您看到了如何为各种屏幕密度添加自定义图标。
七、Git 简介
Git 版本控制系统(VCS)正在迅速成为事实上的标准,不仅在 Android 应用开发中,而且在一般的软件编程中。与需要使用中央服务器的早期版本控制系统不同,Git 是分布式的,这意味着存储库的每个副本都包含项目的整个历史,任何贡献者都没有特权。Git 是由 Linux fame 的 Linus Torvalds 开发的,目的是管理 Linux 操作系统的开发。就像开源运动本身一样,Git 是系统化的非层级结构,鼓励协作。
虽然 Git 从命令行提供了丰富的特性,但本章主要关注在 Android Studio 中使用 Git。多年来,支撑 Android Studio 的 IntelliJ 平台为包括 Git 在内的几个 VCS 系统提供了出色的支持。不同支持系统的一致性以一种使新手和专业人员都容易精通的方式呈现。然而,理解从 Android Studio 内部使用 Git 和从命令行使用 Git 之间的区别是很重要的。本章非常详细地解释了你开始使用 Git 所需要的一切。您将重用在前面章节中开始的 Reminders 应用来学习提交、分支、推送和获取等重要命令的基础知识。您将使用本地和远程 Git 存储库,并了解如何在协作环境中使用 Git 和 Android Studio。
打开你在第一章中创建的 HelloWorld 项目。如果您跳过了这一章,从头开始创建一个名为 HelloWorld 的新项目。在向导过程中,使用所有默认设置。您将简要地使用这个项目来理解 Git 设置的基础。
安装 Git
在开始使用 Git 之前,您需要安装它。将浏览器指向 http://git-scm.com/downloads
。点击您的操作系统的下载按钮,如图 7-1 所示。
图 7-1。
Git download page
我们建议将 Git 安装在 Windows 上的C:\java\ directory
目录中,或者 Mac 或 Linux 上的∼/java
目录中。无论您决定在哪里安装它,都要确保整个路径没有空格。例如,不要在C:\Program Files
目录中安装 Git,因为在Program
和Files
之间有一个空格。像 Git 这样的面向命令行的工具可能会遇到名称中有空格的目录的问题。一旦安装完成,您必须确保C:\java\git\bin\
目录是您的PATH
环境变量的一部分。关于如何给PATH
环境变量添加路径的详细说明,参见第一章。
通过单击 Git Bash 图标启动 Git Bash 终端。如果你运行的是 Mac 或 Linux,只需打开一个终端。您需要用您的姓名和电子邮件配置 Git,以便您的提交将有一个作者。从 Git Bash 发出以下命令,用您自己的名字和电子邮件地址替换 John Doe 的名字和电子邮件地址。图 7-2 显示了一个例子。
图 7-2。
Adding your name and e-mail to Git
$ git config --global user.name "John Doe"
$ git config --global user.email
johndoe@example.com
返回 Android Studio 继续设置 Git 与 Android Studio 的集成。导航到文件➤设置,然后在左窗格的版本控制部分找到 Git。单击省略号按钮并导航到刚刚安装的 Git 二进制文件。单击 Test 按钮以确保您的 Git 环境是可操作的。您应该看到一个弹出窗口,表明 Git 执行成功,以及您安装的 Git 版本。
导航到 VCS ➤导入到版本控制➤创建 Git 存储库。当对话框提示您选择将创建新 Git 存储库的目录时,确保您选择了项目根目录HelloWorld
。你可以选择在目录选择器对话框中点击小小的 Android Studio 图标。该图标将导航到项目的根目录,如图 7-3 所示。单击 OK 按钮,您的本地 Git 存储库将被创建。
图 7-3。
Selecting the directory for your Git repository
您会注意到项目工具窗口中的大多数文件名都变成了棕色。这意味着这些文件被 Git 在本地识别,但是没有被 Git 跟踪,也没有被计划添加。Git 以两阶段的方式管理提交(这不同于 Subversion 和 Perforce 等其他 VCS 工具使用的方式)。登台区是 Git 在提交之前组织更改的地方。进行中的变更、临时区域变更和提交的变更之间的差异是显著的,可能会使新用户不知所措。因此,Android Studio 不会暴露这些差异。取而代之的是一个简单的 changes 界面,允许您轻松地管理和提交修改后的文件。
忽略文件
当您创建本地存储库时,Android Studio 会生成特殊的.gitignore
文件,防止某些路径被跟踪。除非您另外指定,否则 Git 将继续跟踪这个目录及其子目录中的所有文件。然而,.gitignore
文件可以告诉 Git 忽略某些文件或整个目录。
通常,根目录有一个.gitingore
文件,每个项目有一个.gitignore
文件。在 HelloWorld 中,一个.gitignore
位于 HelloWorld 的根目录,一个.gitignore
位于 app 文件夹的根目录。打开位于 HelloWorld 根目录下的.gitignore
文件并检查其内容。图 7-4 展示了项目根目录下生成的.gitignore
文件。默认情况下,Android Studio 会将某些文件从您的 Git 存储库中排除。该列表包括由项目生成或特定于本地计算机的控制设置生成的文件。例如,/.idea/workspace.xml
文件控制 Android Studio 本地配置的设置。虽然可以在 Git 中跟踪这一点,但它不一定是您正在构建的项目的一部分,事实上可能会造成问题,因为该文件对于每个工作空间(例如计算机)都是唯一的。注意.gitignore
中的一个条目是/local.properties
。像 workspace.xml 一样,local.properties 对于每台计算机都是唯一的。
图 7-4。
The root .gitignore file contents
注意列表中的/build
条目。在第十三章中深入讨论的 Android Studio 构建系统 Gradle,在你编译和运行你的项目时,把它所有的输出放在这里。因为该文件夹将包含来自。类文件到.dex
文件再到最终的可安装 Android 包,而且因为它的内容是不断变化的,所以用 Git 跟踪它意义不大。在项目工具窗口中找到local.properties
文件。你会注意到它是黑色的,而其他文件是棕色的。
Android Studio 使用一种配色方案,允许您在工作时轻松识别您的版本控制系统将会看到什么。正如我们已经说过的,brown 表示 Git 在本地识别了 a 文件,但是 Git 没有跟踪它,也没有计划添加它。蓝色表示 Git 正在跟踪的文件已经被更改。绿色用于 Git 正在跟踪的全新文件。黑色表示文件未被更改或未被跟踪。Android Studio 不断跟踪添加到项目中的文件,并在必要时提示您将这些文件与 Git 保持同步。
添加文件
打开屏幕底部的更改视图。它包括两个部分:默认和未版本化的文件。默认部分最初为空,表示活动的变更列表。当您修改和创建文件时,它们将属于此部分,因为它保存了准备提交到您的 VCS 的文件。未版本化文件部分包含所有不被 VCS 跟踪的内容。
因为尚未跟踪所有项目文件,所以它们属于未版本化文件部分。您需要将这些添加到您的存储库中。“变更”视图的左侧是两列图标。在右栏中,单击顶部第三个图标(文件夹图标);参见图 7-5 中的圆圈图标。这是一种切换方式,使您可以按文件夹对文件进行分组,以便更好地了解它们在项目中的相对位置。右键单击未版本化文件部分标题,然后从上下文菜单中单击添加到 VCS,将这些文件添加到 Git 索引中。或者,您可以单击并拖动整个部分到粗体默认部分。
图 7-5。
Group files by folders
添加完所有文件后,单击带有向上绿色箭头的 VCS 图标。这将打开你在第五章中开始使用的熟悉的提交对话框。单击 Commit 记录您的更改,默认部分最终会清空。您也可以按 Ctrl+K | Cmd+K 来执行相同的操作。从现在开始,你在 Android Studio 中接触的每个文件都将在 Git 下被跟踪。
克隆参考应用:提醒
本节扩展了您在第 5 和 6 章节中创建的提醒应用。我们邀请您使用 Git 克隆这个项目,以便跟进,尽管您将使用从第 5 和 6 章中使用的资源库派生的新 Git 资源库重新创建这个项目。如果您的计算机上没有安装 Git,请参见第七章。在 Windows 中打开一个 Git-bash 会话(或者在 Mac 或 Linux 中打开一个终端)并导航到 C:\androidBook\reference(如果没有参考目录,请创建一个。在 Mac 上,导航到/your-labs-parent-dir/reference/)并发出以下 git 命令:git clonehttps://bitbucket.org/csgerber/reminders-git.git
reminders git。您将使用 Git 特性来修改项目,就像您在一个团队中工作一样。通过这个过程,您将学习如何派生和克隆一个项目,并在开发特性时设置和维护分支。在开始本练习之前,将您在第六章中完成的提醒项目重命名为 RemindersChapter6,因为您将很快重新创建该文件夹。在 windows 中,您可以右键单击资源管理器中的文件夹,然后选择重命名。在 Linux 或 Mac 上运行以下命令:mv∽/Android book/Reminders∽/Android book/Reminders chapter 6。
分叉和克隆
派生远程存储库包括在单个 web 托管服务上从一个远程帐户/分区克隆到另一个远程帐户/分区。Fork 不是 Git 命令;这是一个网站托管服务的操作,如 Bitbucket 或 GitHub。据我们所知,两个更受欢迎的网络托管服务,Bitbucket 和 GitHub,不允许它们的服务器之间有分叉。派生一个项目是将一个项目从它原来的远程存储库复制到您自己的远程 Git 存储库的过程,目的是为了改变它或者制作衍生作品。
从历史上看,分叉具有某种负面的含义,因为它通常是不同的最终目标或项目成员之间的分歧的结果。这些差异经常导致来自多个团队的看似相同的软件的替代版本,并且没有用户社区可以依赖的明确的官方版本。然而,现在由于 Git 的出现,分叉得到了强烈的鼓励。分叉现在是协作的自然组成部分。许多开源项目使用 forks 作为改进整个源代码库的手段。成员鼓励其他人派生并改进代码。这些改进通过拉请求的方式被拉回到原始项目中,或者通过个人的请求将 bug 修复或特性拉回到主线中。因为 Git 的合并和分支非常灵活,所以您可以将任何东西放入您的存储库,从单个提交到整个分支。
本章并没有涵盖拉请求和开源协作的全部,但是涵盖了推动这种强大协作形式的特性。登录您的 Bitbucket 帐户,在 Bitbucket 上查找案例研究。如果你还没有一个比特币账户,请在浏览器中导航到 Bitbucket 并注册。注册大约需要 30 秒。登录 Bitbucket 后,您可以使用 Bitbucket web 界面右上角的搜索框找到提醒存储库。在搜索框中,键入 csgerber/reminders。同样,不要将其与您之前作为参考克隆的 reminders-git 存储库混淆。要分叉这个项目,点击左边的分叉按钮,如图 7-6 所示。当随后的窗口提示时,接受默认设置并点击如图 7-7 所示的 Fork repository 按钮。
图 7-7。
Click the Fork repository button
图 7-6。
Click Fork in the Reminders repository left margin controls
现在,我们将克隆您刚刚分叉的存储库。在 Git 中,克隆是从另一个位置复制整个 Git 项目的过程,通常是从远程复制到本地。找到项目的分支并复制 URL。您可以通过在 Bitbucket web 界面的搜索框中键入 your-bit bucket-username/reminders 来完成此操作。在搜索框的正下方,沿着 Bitbucket web 界面的右上方,您会发现克隆框,其中会有一个类似于git@bitbucket.org:csgerber/reminders.git
或https://your-bitbucket-username@bitbucket.org/your-bitbucket-username/reminders.git
的 URL。如果你没有 http URL,那么点击 URL 旁边的按钮,它应该被标记为 SSH,如图 7-8 所示。这将显示一个下拉菜单,允许您选择一个 http URL。从版本控制> Git 导航到 VCS > Checkout。图 7-9 所示的对话框打开,提示您输入 VCS 存储库 URL、父目录和目录名。VCS 存储库 URL 是前面克隆框中的 URL,父目录和目录名的组合是您希望在本地计算机上放置副本的位置。默认情况下,目录名称中的项目名称是小写的。我们建议您用大写字母命名您的项目,因此请根据图 7-9 进行更改。
图 7-9。
Cloning the repository with the Git GUI
图 7-8。
The Bitbucket Share URL
点击克隆,源代码将被复制到本地。
使用 Git 日志
Git 日志是一个强大的特性,它让您能够探索项目的提交历史。通过单击工具按钮或按 Alt+9 | Cmd+9 打开“更改”工具窗口,然后选择“日志”选项卡以显示日志。图 7-10 说明了在第六章结束时通过最终提交的提醒项目的历史。这个视图显示了与存储库中各个分支相关联的时间线。
图 7-10。
Exploring the Git log
单击时间轴中的单个条目会在右侧显示更改列表中的文件;这些是在提交过程中更改的文件。单击来自任何特定提交的文件,然后按 Ctrl+D | Cmd+D(或者简单地双击它们)来获得可视文本 diff,这是一个并排的比较,突出显示对文件的更改。您可以使用更改列表上方的工具栏按钮来编辑源文件,打开文件的存储库版本,或者恢复选定的更改。您还可以使用日志下面的窗口来查看提交作者、日期、时间和哈希代码 ID。这些散列码是唯一的 id,在使用 Git 的一些更高级的特性时,可以用来识别单个提交。
分支
到目前为止,您已经在一个名为master
的分支上进行了所有的提交,这是默认的分支名称。但是,你不需要停留在master
上。Git 允许您创建任意多的分支,分支在 Git 中有多种用途。这里有一个可能的场景。假设您正在与一个开发团队一起工作,并且在一个开发周期中,您每个人都被分配了特定的任务。这些任务中有些是特性,有些是错误修复。处理这项工作的一个合乎逻辑的方法是将每个任务变成一个分支。开发人员都同意,当一个任务完成并测试后,开发人员会将该任务分支合并成一个名为dev
的分支,然后删除该任务分支。在开发周期结束时,dev
分支由 QA 团队测试,QA 团队要么拒绝变更并将项目踢回给开发团队,要么签署周期并将dev
合并到master
。这个过程被称为 Git 流,这是用 Git 在团队中开发软件的推荐方式。你可以在这里阅读更多关于 Git 流的内容:
https://guides.github.com/introduction/flow/index.html
Git Flow 非常适合大型团队,但是如果你是单独开发或者只和一两个其他开发人员一起工作,你可能需要同意一个不同的工作流程。无论您的工作流是什么样的,Git 中的分支功能都是灵活的,它将允许您调整您的工作流以适应 Git。在本节中,我们将假设您正在进行一个团队项目,并被分配了在提醒应用中添加一个功能的任务,该功能允许用户在一天中的特定时间安排提醒。
在树枝上生长
选择“文件”“➤导入项目”,打开之前克隆的 Reminders-Git 项目。右键单击项目视图中的 Reminders-Git 根文件夹,并选择 Git ➤存储库➤分支,以打开分支提示窗口。这个提示允许您浏览所有可用的分支。从提示中单击新建分支。将您的分支命名为 ScheduledReminders,如图 7-11 所示。
图 7-11。
Creating a new branch with Git
新的分支将被创建并签出,供您使用。打开 Changes 视图并点击绿色加号按钮来创建一个新的 changelist。将其命名为 ScheduledReminders,就像您的新分支一样,因为下一轮的更改将引入安排提醒的功能。确保选中激活该变更列表复选框,如图 7-12 所示。
图 7-12。
Creating a new changelist for the branch work
要开始您的新功能,您需要向对话框添加一个新选项,当单击提醒时会显示该选项。打开RemindersActivity.java
并转到附加到 mListViewvariable 的第一个OnItemClickListener
嵌套类中的onItemClick()
方法的顶部。在String
数组中添加日程提醒作为第三个条目,构建可点击选项,如图 7-13 的第 92 行所示。接下来,您需要允许用户设置当您的新选项被点击时的提醒时间。找到第二个嵌套的OnItemClickListener
,它附加到modeListView
上,当单个提醒被点击时,它会创建一个对话框。这将发生在 dialog.show()方法调用之后。查看其onItemClick()
方法内部,如第 101 行所示,并进行如图 7-13 所示的更改。您需要解决日期类的导入问题。
图 7-13。
Changes for scheduled reminders
这里,您将删除提醒的else
块更改为else if
块,它检查索引 1 处的位置。您添加了一个在第三个新选项被点击时运行的else
块。这个块创建一个新的代表今天的Date
,并用它来构建一个TimePickerDialog
。立即运行应用,测试新选项。图 7-14 显示了新功能的运行情况。
图 7-14。
Trying the Schedule Reminder option
现在,新功能的一部分已经开始工作,按 Ctrl+K | Cmd+K 确认消息添加新的计划时间选取器选项。回到 IDE,将找到提醒的两行移到position==0
条件之外。将reminder
变量标记为final
。示例见图 7-15 。
图 7-15。
Move the reminder variable outside the if block
接下来转到您刚刚添加的else
块,在那里您构造并显示时间选择器对话框。在显示与图 7-13 中第 113 行对应的对话框的行前添加以下代码:
final Date today = new Date();
TimePickerDialog.OnTimeSetListener listener = new TimePickerDialog.OnTimeSetListener() {
@Override
public void onTimeSet(TimePicker timePicker, int hour, int minute) {
Date alarm = new Date(today.getYear(), today.getMonth(), today.getDate(), hour, minute);
scheduleReminder(alarm.getTime(), reminder.getContent());
}
};
这将为时间选取器对话框创建一个侦听器。在这个监听器中,您使用今天的日期作为闹钟的基准时间。然后包括从对话框中选择的小时和分钟,为您的提醒创建闹钟日期变量。你在一个新的scheduleReminder()
方法中使用了闹钟时间和提醒的内容。Android Studio 会将 TimePicker 标记为未解析的类,并将 scheduleReminder()方法标记为未解析的方法。按 Alt+Enter 解决 TimePicker 类的导入问题。再次按 F2 和 Alt+Enter 打开智能感知对话框,然后按 Enter 让 Android Studio 为您生成方法,如图 7-16 所示。
图 7-16。
Generate method using IntelliSense
选择RemindersActivity
类,如图 7-17 所示。
图 7-17。
Selecting the RemindersActivity as the target class
将以下代码添加到新的方法体中:
AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
Intent alarmIntent = new Intent(this, ReminderAlarmReceiver.class);
alarmIntent.putExtra(ReminderAlarmReceiver.REMINDER_TEXT, content);
PendingIntent broadcast = PendingIntent.getBroadcast(this, 0, alarmIntent, 0);
alarmManager.set(AlarmManager.RTC_WAKEUP, time, broadcast);
同样,Android Studio 会为代码中缺失的导入标记一系列错误。按 F2,然后按 Alt+Enter 打开快速修复提示并修复每个错误。快速修复选项最终会提示您ReminderAlarmReceiver
不存在。按 Alt+Enter 并选择第一个选项来生成类。在第一个弹出对话框中按 Enter 键使用建议的包,然后在第二个弹出对话框中再次按 Enter 键将这个新的类文件添加到 Git。让类扩展BroadcastReceiver
并实现onReceive()
方法。您的ReminderReceiver.java
文件应该如下所示:
package com.apress.gerber.reminders;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
public class ReminderAlarmReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
}
}
Tip
重复按 F2(下一个突出显示的错误)和 Alt+Enter(快速修复)来让 Android Studio 修复许多从清单中复制代码时出现的错误,如图 7-16 。它将添加缺失的导入,并为未定义的方法、常量和类生成代码。
返回到RemindersActivity.java
文件。通过按 F2 然后 Alt+Enter 找到并修复最后一个错误,并选择第二个建议来编码生成一个String
常量,如图 7-18 所示。将此文本的值设置为“REMINDER_TEXT”。
图 7-18。
Generate a Constant field .
最后,打开你的 AndroidManifest.xml 文件,添加一个 receiver 标签来定义新的BroadcastReceiver
,如图 7-19 所示。
图 7-19。
BroadcastReceiver manifest entry
运行应用进行测试。您应该能够点击一个提醒,选择日程提醒,并设置它的触发时间。选择时间不会有任何作用,因为我们还没有涉及到BroadcastReceivers
的细节。现在按 Ctrl+K | Cmd+K 调用提交更改对话框。请花些时间在“提交更改”对话框中确认您到目前为止所做的更改。请注意,该对话框保留了您之前提交的消息,您应该更新该消息,如图 7-20 所示。
图 7-20。
Git’s Commit Changes dialog box
选择RemindersActivity
后,点击显示差异按钮(如图 7-20 所示)以显示所有变更的并排差异。点按左上角的向上和向下箭头,或者按下 F7 键,在文件的早期和晚期差异之间移动。这些控制出现在图 7-21 中。使用向下箭头移动到您的onItemClickListener
中有趣的变化。
图 7-21。
Visual text diff view
到目前为止,您已经设法包含了一个当前没有使用的OnTimeSetListener
。(listener
变量的浅灰色表示它不在代码中使用。)当您在这个视图中移动代码时,不仅会提醒您已经做出的更改,还会提醒您可能错过的更改,这给了您在提交之前修复问题的另一个机会。diff 视图也是一个具有一些语法感知特性的编辑器。如果您选择进行小的调整,您可以利用诸如自动完成之类的东西。
按 Escape 键关闭 diff 视图,并在提交更改之前更改提交消息。点击提交,让 Android Studio 有机会执行代码分析。您将看到另一个对话框,告诉您某些文件包含问题。对话框将提示代码中有警告。此时,您可以单击 Review 按钮取消提交,并生成所有潜在问题的列表。尽管忽略警告不是好的做法,但是您可以有意地暂时不理会这些警告,继续下一步。
Git 提交和分支
分支上提交的 Git 风格是相似的,但是如果您来自使用 Perforce 或 Subversion 等工具的传统 VCS 背景,可能会感觉与您所习惯的有些不同。您将希望理解 Git 如何管理提交和分支的细微差别。这些差异可能会让新手感到困惑,但它们是 Git 强大和灵活的核心。
Git 中的提交被视为项目历史中的一级实体,可以通过特殊的提交哈希代码来识别。虽然您不需要了解 Git 如何实现单个提交和版本控制的细节,但是将提交视为存在于代表存储库整个状态的历史时间线中的对象或实体是很重要的。提交作为一个原子工作单元存在,它发生在 Git 历史中的某一时刻,用描述工作的提交消息进行注释。每个提交都有一个父提交,它前面有一个或多个提交。您可以将分支视为历史中指向单个提交的标签。当您创建一个分支时,会在历史记录中的该点创建一个标签,当您对该分支进行提交时,标签会跟随提交的历史记录。下图从图 7-22 开始,展示了 Git 当前看到的 Reminders 项目历史。
图 7-22。
Git history showing ScheduledReminders branch Note
Android Studio 提交日志是自下而上的,而我们的图是自上而下的。
主分支由指向来自克隆项目的最后一个 commit A 的灰色箭头表示。(与 Git 日志视图相比,您将会注意到还有其他提交在进行,但是为了简洁起见,我们省略了它们。)ScheduledReminders 分支是绿色箭头,指向实现新特性的一系列提交 B 和 C 中的最新一个。为了简单起见,我们使用单个字母作为标签,但是 Git 使用提交哈希代码,其中包括更长的十六进制名称,例如c04ee425eb5068e95c1e5758f6b36c6bb96f6938
。您可以通过仅使用其散列的前几个字符来引用特定的提交,只要它们是唯一的或者不与任何其他散列的前几个字母相似。
Revert 在哪里?
当人们第一次尝试 Git 时,最大的障碍之一就是适应 Git reverts,因为他们不能像其他 VCS 客户那样工作。Git revert 是一个撤销早期提交的提交(工作单元)。理解的最好方法是看它的实际操作。让我们做一个修改,修复您在RemindersActivity.java
中的反对警告。引入Calendar
物体,移除Date
物体,如图 7-23 所示。
图 7-23。
Fix the deprecation warnings
构建并运行代码以验证它是否有效,然后提交此更改并显示消息 Fixes deprecation warnings。注意,对于未使用的变量,仍然会有一个警告,这将在后面的“重定基时解决冲突”一节中解决。Android Studio 中的 revert 命令与 Git revert 命令有很大不同。在这里,您将使用命令行 git revert 命令来理解其中的区别。找到“修复不推荐使用的警告”提交在“更改工具”窗口的 Git 历史记录中,右键单击它并选择“复制哈希”,将提交哈希代码复制到您的系统剪贴板。现在,通过单击底部空白处的终端窗口按钮打开终端,输入 git revert 并粘贴提交散列作为命令的最后一部分。你的命令应该如图 7-24 所示。按 enter 键,Git 将在您的终端中启动一个提交消息编辑会话,如图 7-25 所示。键入“:q”退出编辑会话,该会话保存默认提交消息并执行提交。
图 7-25。
Commit message edit. Exit by typing :q.
图 7-24。
Issuing the git revert command from the terminal
git 恢复会导致执行新的提交,从而取消之前的提交。切换回 Android Studio,看看有什么变化。所有的弃用警告都随着展开的更改返回。您的 Git 历史将反映提交。Git 应用前一次提交的所有更改的反转,并立即执行一次带有这些更改的提交,并显示一条与前一次提交相同的带有 Revert 前缀的消息。相比之下,其他工具会跟踪您对文件的本地修改,并允许您在提交之前撤消修改。尽管这种撤销更改的新风格有所不同,但 Android Studio 为您提供了一个与经典的、更熟悉的版本控制工具一致的恢复界面。在撰写本文时,还没有 IDE 命令或菜单动作触发 Git revert 的等效操作。但是,一个内置选项允许您在本地应用来自本地历史、Git 历史甚至补丁文件的反向更改。Git revert 自动化了应用反向更改和执行提交这两个步骤。图 7-26 展示了 Git 的历史,提交 D 引入了修改弃用的变更,提交 D 代表了恢复弃用的对Date
对象的调用的撤销变更。
图 7-26。
Git history after revert
撤销已提交更改的另一种方法是使用 reset 命令,它的工作方式类似于 revert,但有一个细微的区别。将图 7-23 中的更改添加回源代码,并再次提交。你的 Git 历史将在-D 后面有一个额外的 E 提交,如图 7-27 所示。这次选择风投➤ Git ➤重置头。在弹出的对话框中输入 HEAD 1,如图 7-28 所示,点击复位。
图 7-28。
The Git Reset Head dialog box
图 7-27。
Git history after reapplying the deprecation fix
Git 会在您最后一次提交之前将您的存储库与提交同步,这相当于对该提交的撤销——使您的历史看起来如图 7-26 所示。Android Studio 通过使用当前的更改列表重新应用您的更改来增强 Git 重置。这为您提供了第二次机会,以便在意外执行重置时收回提交。在大多数情况下,您会希望在重置后完全放弃更改。单击更改工具窗口中的恢复更改按钮以完全放弃更改。在图 7-29 中,恢复更改按钮被圈起来。
图 7-29。
Click the revert changes button
让我们更进一步地重置,以删除您在不赞成使用的方法调用上的所有工作痕迹。选择 VCS ➤ Git ➤重置头。然后在弹出的对话框中输入 HEAD 2,如图 7-28 所示,点击 Reset。记得之后点按“还原更改”按钮。这将成为你每次在 Android Studio 中使用 Git Reset 的习惯。你的历史将会反映出图 7-22 的历史。
Revert Vs. Reset
还原和重置之间的区别是微妙但重要的。revert 添加了一个提交,该提交反转了上次提交的更改,而 reset 取消了一个提交。重置本质上是通过给定数量的提交来备份您的分支标签。如果您不小心提交了某个东西,您通常会想要撤销或删除某个提交。在这种情况下使用重置是合理的,因为这是最简单的选择,并且不会增加您的历史记录。但是,在某些情况下,您可能希望您的历史记录能够反映撤销提交的工作—例如,如果您从项目中提取一个特性,并且希望向用户社区记录该特性的删除。revert 的另一个重要用途是远程存储库,我们将在本章后面讨论。因为您只能向远程存储库添加提交,所以删除或撤销远程存储库上的提交的唯一方法是使用 revert,它将反转的更改作为提交追加。
合并
合并是将两个独立分支的工作结合起来的一种方式。历史上,由于分支之间的冲突,合并需要额外的努力。由于 Git 实现了更改,合并变得不那么痛苦了。
您将从在主分支上为极端拖延者添加一个新特性开始。这个新功能会将所有提醒的默认值设置为重要,因为我们知道你会忽略除了最重要的提醒之外的任何事情。单击文件➤ VCS ➤ Git ➤分支机构调出分支机构列表。选择主分支,然后选择签出。请注意,基础源代码已经更改,支持新功能的所有更改都已删除,并且您的项目已恢复到您开始处理计划提醒之前的状态。创建一个名为的新变更列表,并将其设置为活动。出现提示时,删除空的 ScheduledReminders 更改列表。图 7-30 和 7-31 演示了该流程。
图 7-31。
A confirmation dialog box appears when deleting the old changelist
图 7-30。
New changelist dialog box
查看fireCustomDialog()
方法,找到从对话框布局中检索复选框的行。新增一行调用checkBox.setChecked(true)
,将设置新的默认值,如图 7-32 第 200 行所示。
图 7-32。
Set the check box default to checked
构建并运行应用以测试新功能,然后使用 Ctrl+K | Cmd+K 提交。Git 将看到图 7-33 中记录的历史,它代表您在分支的初始克隆之后的最新提交。
图 7-33。
Commit history after adding a feature to the master branch
在这里,你把你的头转向主,并作出了 D 的承诺。此最新提交遵循与 ScheduledReminders 功能的提交不同的历史路径,因为此提交不在同一分支上。
Note
如果您在 Git 日志视图中跟踪历史,您会注意到有另一个 origin/master 分支指向我们没有显示的提交。这是稍后讨论的远程分支。
您已经在 master 分支上做了一些工作,并提交了一些内容,以便在 ScheduledReminders 分支上添加一个新特性,所以现在您将把这些更改一起放到主线或 master 分支中,以便其他人可以看到它们。再次单击文件➤ VCS ➤ Git ➤分支机构,调出分支机构列表。选择 ScheduledReminders 分支,然后单击“合并”。该分支的所有更改和历史记录都将合并到主分支中。构建并运行应用来测试这两个特性。从“选项”菜单中单击“新建提醒”将打开一个选中“重要提示”复选框的“新建提醒”对话框,而单击列表中的任何提醒将提供在特定时间安排提醒的选项。图 7-34 展示了 Git 是如何管理你的变更的。
图 7-34。
Commit history after merging the ScheduledReminders feature
自动执行新的 E 提交,包括来自 C 和 D (E 的父代)的更改。还要注意,HEAD 指向包含最新提交的主分支的头。
Git 重置更改历史
如果你想把你的重要提醒功能作为一个分支呢?您从未为此功能创建分支。相反,你就在主分支的顶端发展。您可以强制您的主分支进行备份,并指向您的 D commit,所以我们现在就开始吧。单击文件➤ VCS ➤ Git,然后单击重置头。“提交”字段将被设置为“标题”。设置为 HEAD∾1 点击重置按钮如图 7-35 再次重置你的主分支,更像是一个标签。记得恢复从 Git 重置保存的更改。然后它将指向先前的提交。Git 现在将会看到如图 7-33 中之前的图表所示的存储库。
图 7-35。
Git reset dialog box
由于最后一次提交包含了合并的更改,重置使得合并从未发生,您现在位于提交之上,这引入了 ImportantReminders 特性。这让您可以自由地改变历史,使它看起来好像这个新特性是在一个分支上开发的。单击文件➤ VCS ➤ Git,然后单击分支打开分支对话框。单击新建分支。将该分支命名为 ImportantReminders,然后单击 OK 创建它。你现在有了图 7-36 中描述的历史。
图 7-36。
Git history showing the new branch
master 和 ImportantReminders 分支都指向同一个提交。使用“分支”对话框检查主分支,可以通过单击状态栏右上角的“分支”部分或选择“文件”“➤VCS”“➤git”“➤分支”来调用该对话框。再次重置该分支,使其指向最初从 Bitbucket 克隆项目的位置,然后签出 ImportantReminders 分支。历史现在反映了仍在开发中的两个实验性特性分支,而工作副本(您在 IDE 中看到的)反映了项目在您第一次克隆它时的存在。你可以在图 7-37 中看到这一点。
图 7-37。
Git history after resetting master to the beginning
现在,您想要进一步更改历史并重新排序您的特性提交,以便它们看起来像是连续开发的,并且在开发期间没有使用分支。在此之前,签出 master 分支,并将其与 ImportantReminders 分支合并。合并将导致一个特殊的快进操作:Git 只是在历史中将主分支向前移动到由 ImportantReminders 分支共享的同一个提交。这与前面的合并分支示例不同,因为一个分支是另一个分支的后代。如果您足够仔细地观察,您会注意到创建一个将来自 ImportantReminders 分支的更改合并到主服务器上的提交与这个分支已经指向的 D 提交是相同的。因此,Git 优化了操作,只是将主分支向前移动,这将您带回到类似于图 7-36 所示的历史。不同之处在于,您签出的是 master 而不是 ImportantReminders 分支。
现在你会让你的历史更有趣。您将在应用中添加一个“关于”对话框,以便您的用户了解更多关于开发者的信息。“关于”对话框也是放置所用技术和艺术作品属性的好地方。你的会相对简单。如果尚未删除 ImportantReminders 更改列表,请将其删除,并使用标题为 AboutScreen 的新更改列表。在app
➤ src
➤ main
➤ res
➤ layout
下新建一个名为 dialog_about.xml 的资源 XML 文件,并用清单 7-1 中的代码填充。
Listing 7-1. dialog_about.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="``http://schemas.android.com/apk/res/android
android:layout_width="match_parent" android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:text="Reminders!"
android:id="@+id/textView2"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:text="Version 1.0\nAll rights reserved\nDeveloped by yours truly!"
android:id="@+id/textView3"
android:layout_marginTop="34dp"
android:layout_below="@+id/imageView"
android:layout_centerHorizontal="true"
android:gravity="center" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/imageView"
android:layout_below="@+id/textView2"
android:layout_centerHorizontal="true"
android:src="@drawable/ic_launcher" />
</RelativeLayout>
此布局定义了一个“关于”对话框,其中包含标题的文本视图、正文的文本视图,并在两者之间放置提醒启动图标。您需要一个新的菜单项来触发对话框。打开menu_reminders.xml
并在第一个和第二个项目标签之间添加以下 XML 片段:
<item android:id="@+id/action_about"
android:title="About"
android:orderInCategory="200"
app:showAsAction="never" />
将退出菜单项的orderInCategory
从 200 更改为 300,以便它可以在新的“关于”项之后排序。
现在打开RemindersActivity.java
,为调用新fireAboutDialog
方法的新菜单项添加一个用例,如图 7-38 所示。
图 7-38。
Add an About screen
方法使用你的新布局构建一个对话框并显示它。构建并运行新特性来测试它。最后,按 Ctrl+K | Cmd+K 并提交消息“添加一个关于屏幕”。Git 历史现在在重要的提醒特性之后多了一个 commit,它现在由一个分支指向。图 7-39 中您最新的 E commit 包含了关于对话框功能。
图 7-39。
Git history after adding the About screen
吉特·福克斯
重设基础是一种基于另一个分支或一系列提交创建分支的方法。它类似于合并,因为它合并了分支之间的更改,但它以一种没有多个父级的方式创建提交历史。最好用现在的历史作为例子。单击文件➤ VCS ➤ Git ➤重设基础以打开重设基础分支对话框。告诉该对话框,您想通过从“到”下拉菜单中选择主分支来将主分支重置到 ScheduledReminders 分支,如图 7-40 所示。保持选择交互式选项,这样您可以更好地控制组合内容。
图 7-40。
The Git Rebase branch dialog box
这将带您进入交互式重设基础模式,显示图 7-41 中的对话框。交互式重定基础是 Git 更强大的特性之一。在这里,您可以删除和更改提交历史中的单个提交。“重设提交基准”对话框列出了所选分支历史记录中发生的所有提交,直到您“基于”的分支的第一个共同祖先。首先要注意的是每个提交的 Action 列下的选项。该对话框提供了拾取、编辑、跳过或挤压选项。然而,Android Studio 默认选择每个提交。
图 7-41。
The Git Rebase commits dialog box
假设您不再需要这个分支的 ImportantReminders 特性,但是您仍然对 About 屏幕感兴趣。选择 Skip 操作从列表中删除这个提交,当您完成 rebase 和合并分支时,这些更改都不会出现。单击开始重置基础选项以完成操作。你的 Git 历史现在看起来如图 7-42 所示。
图 7-42。
After rebasing and skipping the ImportantReminders branch
分离头
让我们假设当您最初克隆这个项目时,有另一个开发人员正在开发一个报警功能。再进一步说,你想最终融合在这个作品里。要模拟这种情况,您需要在历史中回到 A commit 并启动新特性。到目前为止,您一直在针对一个特定的分支进行工作和提交。这可以是自定义命名的分支,也可以是初始导入时创建的主分支。
我们现在将演示 Git 中的另一种工作方式,即分离头模式。如果您签出一个特定的提交而不是一个分支,那么这个头将从您正在处理的分支中分离出来并被暴露。首先,您需要检查 ImportantReminders 分支的父提交。为此,打开分支对话框,点击签出标签或修订,如图 7-43 所示。
图 7-43。
Checking out the change prior to the last change in the ImportantReminders branch
在结帐提示中输入 important reminders 1。您现在将处于分离模式,您的头分支和项目状态将反映您最初克隆项目时所做的最后一次提交,如图 7-44 所示。
图 7-44。
git_diagram8
请注意,Git 现在公开了一个新的 HEAD,它与开发过程中创建的任何分支都是分离的。负责人已经正式跟踪了你检查过的任何一个分支。当您提交时,签出的分支将随着头部移动到最近的提交。您输入的 important reminders 1 文本是您希望从何处开始结帐的相对参考。您可以给大多数需要分支或提交散列的操作一个相对引用。相对引用使用以下两种格式之一:
BranchOrCommitHash^
BranchOrCommitHash∼NumberOfStepsBack
单插入符号形式从左侧指定的分支或提交在历史中后退一步,而波浪号形式在历史中后退的步数等于波浪号右侧给出的步数。
相对引用
相对引用是 Git 表达式,用于引用 Git 历史中的特定点。他们使用一个起点或参考点,以及一个目标,该目标是从参考点开始的步数。虽然引用经常作为 HEAD 给出,但它也可以是分支的名称或特定提交的哈希代码(或缩写的哈希代码)。您可以使用相对引用来完成任务,比如将分支移动到 Git 历史中的任意位置,选择特定的提交,或者将头移动到历史中的特定点。在任何可以给出分支名或提交散列的地方,相对引用都可以作为参数给出。虽然我们已经看到了一些在 IDE 中使用它们的例子,但是它们最好在命令行中与 Git 一起使用。
创建一个新的分支来开始下一个特性,并将其命名为 SetAlarm。为新分支创建一个变更列表,并删除任何旧的空变更列表。在com.apress.gerber.reminders
包中添加一个名为 RemindersAlarmReceiver 的新类,并用下面的代码填充它:
public class ReminderAlarmReceiver extends BroadcastReceiver{
public static final String REMINDER_TEXT = "REMINDER TEXT";
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
@Override
public void onReceive(Context context, Intent intent) {
String reminderText = intent.getStringExtra(REMINDER_TEXT);
Intent intentAction = new Intent(context, RemindersActivity.class);
PendingIntent pi = PendingIntent.getActivity(context, 0, intentAction, 0);
Notification notification = new Notification.Builder(context)
.setSmallIcon(R.drawable.ic_launcher)
.setTicker("Reminder!")
.setWhen(new Date().getTime())
.setContentText(reminderText)
.setContentIntent(pi)
.build();
NotificationManager notificationManager = (NotificationManager)
context.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify(1, notification);
}
}
这里我们有一个BroadcastReceiver
,它期望REMINDER_TEXT
作为额外的意图给出。它使用该文本并创建一个动作意图,用它来构建一个通知以发布到通知栏中。接下来,在AndroidManifest.xml
中,在activity
标签之后,在结束的application
标签之前添加以下条目,以定义BroadcastReceiver
:
<receiver android:name="com.apress.gerber.reminders.ReminderAlarmReceiver"/>
按 Ctrl+K | Cmd+K 并提交带有消息“添加警报广播接收器”的设置警报更改列表。您的 Git 历史将类似于图 7-45 ,其中第三次提交在您的初始起点 a 处挂起。
图 7-45。
Git history after committing to the SetAlarm branch
这个特性本身不会做太多事情。它需要与 ScheduledReminders 特性合并,后者存在于它自己的分支上。为了完成您的工作,您需要将这两个特性结合起来,并将它们推送到您的远程 Bitbucket 主机,但是您希望以一种方式完成这项工作,使它看起来像是由一个人或一个团队在主分支上完成的,并清理您的所有其他分支。前面您已经看到了 Git 合并如何创建一个提交,其中包含来自合并所涉及的两个分支的两个父提交。您还了解了 Git rebase 通过单个父提交以线性方式组合了两个分支,这正是您所需要的。打开“分支”对话框并签出主分支。点击文件➤ VCS ➤ Git ➤ Rebase。选择 SetAlarm 作为您所基于的分支,并取消选择 Interactive 复选框,因为您现在想要包含来自主干的所有更改。单击开始重置基础。您应该会看到如图 7-46 所示的错误弹出窗口。
图 7-46。
Rebase conflict pop-up
重置基础时解决冲突
这个弹出窗口不应该让您惊慌,因为它指出 Git 已经发现了一些冲突。Git 用冲突状态标记它不能自动合并的文件。在重定基数可以继续之前,由您来解决这些冲突。传统上,解决冲突是许多合作努力的祸根。当你遇到错误或冲突时,感到不舒服是很自然的,尤其是在合并期间。然而,让自己熟悉不那么愉快的合作之路,让合并和冲突解决成为一种习惯,会增加你协调团队和个人之间变化的能力。此外,Android Studio 使解决这种冲突变得不那么痛苦。记住,作为 ScheduledReminders 特性的一部分,您在主分支中启动了BroadcastReceiver
。这种冲突是由包含相似或相同更改的两个分支中的代码引起的。通过查找红色突出显示的文件,在变更视图中找到冲突,如图 7-47 所示。
图 7-47。
Merge conflicts in the Changes view
右键单击并从上下文菜单中选择 Git ➤解决冲突,如图 7-48 所示。这将启动“合并有冲突的文件”对话框。传统上,解决冲突需要双方提供两种输入来源;您的本地更改或您的更改,以及他们的传入更改或他们的更改。
图 7-48。
Select the Resolve Conflicts option
图 7-49 所示的 Android Studio 文件合并冲突对话框是一个强大的合并工具,用于执行三向文件合并和解决文本冲突。它从传统的合并场景中借用了你的和他们的术语,引导你完成合并。合并工具将您正在重置的 SetAlarm 分支视为他们的分支,或者接收服务器发生了变化。您重新设置基础的主分支被认为是您的,或者是本地工作副本。“合并有冲突的文件”对话框首先出现一个对话框,允许您接受自己的文件、接受他们的文件或进行合并。“接受您的”选项完全忽略来自您正在重置基础的分支的传入服务器文件更新,而支持来自您正在重置基础的本地工作副本分支的更改,并将文件标记为已解决。“接受他们的”选项将当前分支的本地工作副本完全替换为来自分支的传入服务器文件更新,同时将文件标记为已解析。Merge 选项将带您进入三向合并编辑器,在这里您可以将来自传入服务器和工作副本的单个行更改拉入基本合并副本,同时仅自定义合并您需要的内容。基本合并副本是合并的输出或结果。
图 7-49。
Merge the ReminderAlarmReceiver
点按“合并”按钮来查看这是如何工作的。图 7-50 所示的合并编辑器打开。
图 7-50。
The Merge editor
合并编辑器将您的工作副本和传入副本排列在合并结果的两侧,这是屏幕的可编辑部分。它支持语法和导入,这意味着在编辑本地副本时,可以使用自动完成、快速修复和其他键盘快捷键。这为您提供了某些外部 VCS 合并工具所没有的优势。编辑器显示了本地工作副本和传入的更新,后者被标记为来自服务器的更改。这些是您要重置基础的 SetAlarm 分支的更改。沿着边栏,您会看到在更改的行旁边有小的双 v 形和 x。单击任一侧的双 v 形图标将在合并结果中包含该特定更改。单击 X 将忽略该特定更改。这些更改也用颜色编码,红色表示冲突,绿色表示附加行,蓝色表示更改的行。在这种情况下,文件的大部分是相互冲突的。
由于您在左侧的本地副本中只有一个类的存根,所以从右侧传入的更改中接受整个完整的实现更有意义。单击 Cancel 并对询问您是否要退出而不应用更改的提示回答 Yes。在“合并有冲突的文件”对话框中,单击“接受他们的”以接受所有传入的服务器更改。接下来,对话框将排列最主要的文件。如果您单击“合并”,您将看到本地工作副本与传入的服务器副本具有完全相同的修改,因此您可以选择您的或他们的。单击本地工作副本中的双 v 形符号接受您的更改,单击传入副本窗格中的 X 拒绝他们的更改。在弹出的提示中单击 Save and Finish 完成合并。对于 Git,这两个文件都将被标记为冲突已解决。如果您在更改工具窗口中查看,您会在默认的更改列表中看到您合并的文件。Git 在向 ScheduleAlarm 分支重放一系列更改的过程中暂停了,正在等待您继续。
进入主菜单,找到风险投资公司➤➤git continue rebase 选项,如图 7-51 所示。注意,您还可以选择中止重设基础或者在重设基础时跳过此提交。如果您正在进行一个复杂的合并,并意识到有些事情是灾难性的错误,您可以单击 Abort Rebasing 并将所有内容恢复到开始 rebase 之前的状态。如果您不小心包含了一个有几个冲突的提交,您还可以选择跳过。单击“继续重设基础”以完成重设基础。
图 7-51。
Click the Continue Rebasing menu option
重置将完成,并执行新的提交。Git 历史将反映时间线中 SetAlarm 提交之后主服务器的所有更改的副本。如图 7-52 所示。
图 7-52。
Git history after rebasing and fixing conflicts
主包含提交 B 和 C,支持 ScheduledReminders 提交 E,增加了关于屏幕;并从 SetAlarm 分支提交 F。您还决定不再需要之前的 rebase 中的 ImportantReminders 特性。
设置闹钟和实现实际的BroadcastReceiver
的任务是在一个单独的分支上完成的,但现在它看起来像是你的时间线中的一个标记或里程碑。为了完成您的特性,您需要将您的工作从 ScheduleReminders 分支绑定到 SetAlarm 分支中的实际BroadcastReceiver
。进行以下更改,将调用BroadcastReceiver
的监听器连接到TimePickerDialog
。您将在 else 块的末尾、我们用于编辑提醒的对话框之前插入以下代码片段。
new TimePickerDialog(RemindersActivity.this,listener,today.getHours(),today.getMinutes(),false).show();
在设备上运行您的项目,并验证该功能是否有效。现在,当您安排提醒时,您应该会收到设备通知,如图 7-53 所示。
图 7-53。
Notification from a reminder
现在,您可以将主分支推送到远程 Bitbucket 主机。从“文件”菜单中,选择“VCS ➤ Git ➤推送”。图 7-54 中的对话框打开,您可以将更改从本地主分支推送到 Bitbucket 存储库的远程主分支。单击推送按钮执行推送。
图 7-54。
Push your changes to Bitbucket
由于您已经完成了 ScheduledReminders 和 ImportantReminders 分支,因此可以删除它们。打开“分支”对话框,依次选择这两个分支;单击删除以删除它们。
Git Remotes
Git 远程只是存储在远程服务器上的 Git 存储库的副本,可以通过网络访问。虽然您可以像使用 Subversion 这样的传统客户端/服务器模型的 VCS 一样使用它们,但是最好将它们视为您的作品的可公开访问的副本。在 Git 工作流中,您没有提交到共享的中央服务器;相反,你通过拉请求来分享你的工作。
pull 请求是来自一个开发人员的请求,从该开发人员概要文件下的公共存储库中获取变更。其他人可以根据自己的判断自由地包含个人提交或您的整个工作。您通常会发现一个主分支,有一个或多个主要开发人员负责用最新的和最有价值的特性和提交来更新该分支。领导通过使用 Git 的整个特性集从不同的贡献者那里引入变更,Git 允许选择、删除、重新排序、合并、压缩和修改单个提交。
然而,拉请求是针对高级 Git 用户的。人们开始使用 Git 最常见的方式是从 Git 托管服务器克隆一个项目——下载 Git 存储库的完整副本以便在本地使用。您继续进行更改并在本地提交它们,然后最终将这些更改推回到远程存储库。您还可以获取和合并其他人上传到远程的更改。
另一种选择是从本地的一个空存储库开始,然后构建一个项目。然后,您将项目推送到一个 Git 托管服务(如 Bitbucket 或 GitHub ),并公布与他人共享,或者您可以将其设为私有,并自行邀请他人。开发像普通方法一样继续,本地提交推送到远程。最终,当您工作时,贡献者会分支并添加到他们的远程项目副本中,您将获取并合并这些变更。
拉模式与推模式
传统的 VCS 系统依赖于一种推送模式,在这种模式下,功能由几个开发人员开发,最终被推送到一个中央服务器。虽然这种模型已经工作了多年,但是它受到主分支的单个副本被破坏的限制,因为贡献者试图通过使用 diffs 和补丁文件来合并他们的更改。补丁文件是更改源文件所采取的单个操作的文本表示;例如,指示添加这些行、移除这些行或改变这些行。大多数遵循这种模型的 VCS 系统随着时间的推移,通过应用一系列差异来管理变更。
Git 遵循分布式拉模型,将项目视为共享实体。因为 Git 允许主分支的分布式副本,所以任何人都可以在任何时候提交和更新本地副本,这降低了贡献者之间合并工作的复杂性。Git 还提升了单个提交的重要性,将它们视为存储库随时间变化的快照。这使得该工具更擅长管理变更。它还增加了分别管理对单个源文件的多个更改的灵活性。合并更加精确和易于管理,合并工作的复杂性大大降低。例如,一个项目负责人可以将您在多个分支之间通过大约 10 次提交实现的功能提取出来,将它们全部压缩到一个分支中,修改消息,并在主分支中的其他提交之前将其组织到该负责人的个人历史中,最后在与项目相关联的远程设备上推送并公布它。
摘要
这涵盖了在 Android Studio 中使用 Git 的基础知识。在本章中,你已经看到了如何安装 Git 并使用它来跟踪变化。我们演示了如何将源代码添加到 Git 中,并使用 Git 日志特性来查看提交历史的摘要。您已经看到了分支如何像指向单个提交的标签一样工作的深入示例。可以通过使用相对引用在提交之间移动分支,甚至完全删除分支。我们已经展示了 Git history 如何修改并行提交的更改,并按顺序排列它们。我们展示了几个涉及多个分支同时成熟的协作场景。
八、设计布局
充分利用你的应用通常意味着给它合适的视觉吸引力来取悦你的目标受众。虽然 Android 使得启动和运行各种模板项目变得很简单,但有时您可能需要对应用的外观和感觉有更多的控制。也许你想调整一个单选按钮在另一个控件旁边的位置,或者你需要创建你自己的自定义控件。本章介绍了设计布局和组织控件的基础知识,以便它们能在各种 Android 设备上正确显示。
Android 布局设计基于三个核心 Android 类,Views
、ViewGroups
和Activities
。当画屏幕时,这些是你的基本构件。虽然用户界面包有更多的类,但大多数都是这些核心类的子类,利用这些核心类,或者是这些核心类的组件。另一个重要的组件 fragment 是在 Android 3.0 蜂巢(API 11)中引入的。片段解决了设计用户界面的模块化部分的关键需求,允许跨多种形式的重用,特别是平板电脑。本章从核心用户界面类开始,然后在后面的章节中继续讨论。
活动
Android activity 代表一个用户可以与之交互的屏幕。Activity
类本身不画任何东西;相反,它是根容器,负责编排绘制的每个组件。任何被绘制到屏幕上的组件都存在于活动的边界内。Activity
类也用于响应用户输入。当用户在屏幕之间导航时,一个活动可以转换到另一个活动。活动有一个众所周知的生命周期,详见表 8-1 。我们将在本章后面提到活动生命周期。
表 8-1。
Activity Life-Cycle Methods
| 方法 | 描述 | 之后杀死 | 然后 | | --- | --- | --- | --- | | `onCreate()` | 这在最初创建活动时被调用。它负责构造视图、将数据绑定到控件,以及管理或恢复给定包的状态。 | 不 | `onStart()` | | `onRestart()` | 该方法在活动停止后、再次启动前调用。这种情况发生在一些情况下,比如在打完电话后继续或者将应用带回前台。 | 不 | `onStart()` | | `onStart()` | 在屏幕上显示活动之前,立即调用此方法。如果活动被置于前台,则随后调用`onResume()`,如果活动被隐藏,则调用`onStop()`。 | 不 | `onResume()`或`onStop()` | | `onResume()` | 当活动被创建、启动并准备好接收用户输入时,触发`onResume()`方法。该活动将在此方法完成后运行。 | 不 | `onPause()` | | `onPause()` | 每当系统准备好恢复活动时,就会触发此方法。当系统准备转换到另一个活动时,可以在当前活动正在执行时调用它,或者当当前活动被中断并发送到后台时调用它。 | 是 | `onStop()`或`onResume()` | | `onStop()` | 当活动不可见时,调用此方法。 | 是 | `onRestart()``or` | | `onDestroy()` | 活动就在被销毁之前得到这个调用。这通常是从活动内部显式调用`finish()`的结果,或者是因为`WatchDog`需要终止活动来回收内存或者因为它变得没有响应。这是活动将收到的最后一个呼叫。 | 是 | 不适用的 |视图和视图组
尽管活动是根组件,但它通常包含几个View
和ViewGroup
对象的集合。视图是屏幕上所有可见元素的超类,包括view-group
。这些元素包括按钮、文本字段、文本输入控件、复选框等等。一个视图通常包含在一个或多个视图组中。视图组代表一个或多个视图对象的集合。一个视图组可以嵌套在 n 层的其他视图组中,以创建复杂的布局。视图组的主要职责是控制一个或多个嵌套的View
或ViewGroup
对象的布局。各种类型的专用视图组控制它们的子组件如何定位。这些是布局容器对象。每个布局对象的行为不同,并使用唯一的位置属性。LinearLayout
、RelativeLayout
、FrameLayout
、TableLayout
、GridLayout
为核心布局容器。
为了更好地理解各个布局是如何工作的,让我们来看几个例子。使用新建项目向导启动一个名为 SimpleLayouts 的新项目。选择至少符合 API 14 (IceCreamSandwich)标准的手机和平板电脑外形,并使用空白活动模板。保留默认的活动名称MainActivity
和布局名称字段的名称activity_main.xml
,然后继续创建项目。你应该进入主活动布局的编辑模式,如图 8-1 所示。
图 8-1。
Starting with the main activity’s layout
预览窗格
对于新项目,您将在文本编辑模式下开始主活动的布局 XML。如果您的项目不在此模式下,请按 Ctrl+Shift+N | Cmd+Shift+O 打开文件搜索对话框,并键入名称activity_main
以找到您的主布局。Android Studio 支持文本和设计两种模式来设计布局,你应该熟悉这两种模式。可以使用编辑器窗口左下角的选项卡来切换这些模式。默认情况下,文本模式允许您像编辑任何其他源文件一样直接编辑 XML 文件。
编辑器右侧的预览面板在您进行更改时,为您提供布局外观的实时预览。您还可以通过选择“配置渲染”菜单下的“预览所有屏幕尺寸”选项来预览您的布局在多个设备上的外观。虚拟设备下拉菜单中有一个相同的选项。这两个菜单都位于预览窗格的左上角。您可以打开和关闭预览选项,看看它是如何工作的。
预览窗格顶部有几个控件,允许您更改预览的呈现方式。您可以在定义了 AVD 的任何特定设备中渲染预览。您可以同时在多台设备上预览。您还可以更改用于渲染预览的 API 级别和主题。表 8-2 描述了图 8-2 中突出显示的预览窗格的注释部分。
表 8-2。
Description of the Preview Pane
| 部分 | 描述 | | --- | --- | | 答:预览切换 | 这是一个预览开关。它可以选择特定的 Android 版本或选择所有的屏幕尺寸。它可用于根据当前布局快速创建特定屏幕尺寸的布局。 | | B: AVD 渲染 | 此菜单允许您在特定设备上预览布局。它也可以用来切换所有屏幕尺寸作为优先菜单。 | | C: UI 模式 | 在这里,您可以找到在横向、纵向和各种 UI 模式之间切换预览器的选项,以及汽车、桌面和电视对接模式。它还包括电器模式和夜间模式。 | | 主题控制 | 主题切换允许您预览带有特定主题的布局。它默认为 AppTheme,但是您可以从 SDK 中的各种主题中选择,或者从您的项目中选择任何主题。 | | 活动协会 | 活动关联菜单允许您将当前布局与特定活动相关联。 | | f:本地控制 | 此菜单将预览设置为使用特定的翻译。 | | g:安卓版本 | API 菜单允许您将预览设置为特定的 API 级别。您可以使用它来查看您的布局如何响应各种 API 级别。 |图 8-2。
The Preview pane in detail
在文本模式下,选择RelativeLayout
标签并将其开始和结束标签更改为FrameLayout
。请注意预览窗格中没有任何变化,因为您只更改了根布局标记,还没有触及其中的任何内容。稍后您将了解到这些布局之间的更多区别。
选择嵌套的TextView
中的Hello World
文本,它会自动展开为@string/hello_world
,这是对外部strings.xml
文件中文本的引用。Android Studio 的代码折叠功能默认隐藏外部字符串引用。按 Ctrl± | Cmd±将属性收拢或折叠回其呈现形式,然后按 Ctrl+= | Cmd+=将其展开以查看实际属性值。在 Android 中,将字符串值硬编码到你的布局中被认为是不好的做法,因为它们作为字符串引用会更好地处理。在一个简单的例子中,比如我们在这里创建的例子,硬编码字符串并不重要,但一个商业应用可能需要以几种语言推出,外部化的字符串使这个过程变得非常简单。所以,养成字符串外化的习惯是个好主意。
引用是在资源文件中编码的特殊属性值,它引用在别处定义的实际值。在这种情况下,特殊字符串“@string/hello_world
”指的是在strings.xml
资源文件中定义的值。Ctrl+click | Cmd+click 文本导航到"Hello World"
字符串定义,如下所示:
<string name="hello_world">Hello world!</string>
将值更改为“Hello Android Studio!”按 Ctrl+Alt+左箭头键| Cmd+Alt+左箭头键导航回布局,并在预览窗格中查看更新的新值。现在将文本改为一个随机的硬编码值,比如“再见,拉斯维加斯!”,预览将再次更新,但在这种情况下,您已经直接覆盖了字符串。当您更改TextView
时,预览窗格将会更新。
宽度和高度
文本视图是您可以添加到布局中的许多视图之一。每个视图都有控制其大小的宽度和高度属性。您可以设置绝对像素值,如250px
或使用各种相对值之一,如250dp
。最好使用一个带有dp
后缀的相对值,因为这使得组件能够根据呈现它的设备的像素密度来调整大小。相对尺寸将在后面的“覆盖各种显示尺寸”一节中解释。将TextView
标签改为Button tag
,然后将android:layout_width
属性改为match_parent
。文本视图将变成一个按钮,横跨整个屏幕。将android:layout_height
属性更改为match_parent
。该按钮将占据整个屏幕。将android:layout_width
属性更改为wrap_content
,按钮宽度会变窄,但仍然占据屏幕的整个高度。match_parent
值是一个特殊的相对值,它根据父容器调整视图的大小。图 8-3 描述了使用match_parent
测量部件宽度和/或高度的可能变化。wrap_content
是另一个广泛使用的相对值,它以一种紧密围绕其内容的方式来调整视图的大小。将Button
标签改回TextView
标签,将其宽度和高度设置为match_parent
,并向我们的布局添加几个其他组件,如Button
和CheckBox
,如清单 8-1 中所定义。
图 8-3。
Variations of the match_parent size value Listing 8-1. Add More Components to the Layout
<FrameLayout xmlns:android="``http://schemas.android.com/apk/res/android
xmlns:tools="``http://schemas.android.com/tools
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context=".MainActivity">
<TextView
android:text="Goodbye, Las Vegas!"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<Button
android:text="Push Me"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<CheckBox
android:text="Click Me"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</FrameLayout>
请注意这些组件是如何一个接一个地绘制的。图 8-4 说明了这个问题。FrameLayout
的行为是按照定义的顺序堆叠组件。暂时删除额外堆叠的组件,以便您可以探索设计器模式并了解如何可视化地布局组件。
图 8-4。
Widgets are stacked on top of one another
让我们检查一下FrameLayout
容器标签。这个标签定义了两个属性,android:layout_width
和android:layout_height
,它们都指定了match_parent
。这意味着框架的宽度和高度将与其包含的父级的宽度和高度相匹配。因为FrameLayout
是最外层的元素,或者说根元素,所以它是所有其他组件的父元素。因此,它的宽度和高度将覆盖设备屏幕的整个可视区域。
设计师模式
点击编辑器左下方的设计选项卡(如图 8-5 所示)调出设计模式。在本节中,您将探索如何使用可视化设计器来定位控件。
图 8-5。
The designer and text view tabs
设计模式与文本模式具有相同的实时预览窗格,但添加了一个小部件调色板。在可视化设计布局时,您可以将组件从组件面板拖放到预览窗格中。可视化设计器为您生成 XML,同时允许您专注于布局的外观。设计模式还在右上角显示一个组件树窗格,在其下方显示一个属性窗格。组件树提供了当前布局中所有视图和视图组组件的分层视图。顶部是根组件,在我们的例子中是FrameLayout
。
框架布局
正如您所看到的,FrameLayout
按照定义的顺序堆叠组件。但是,它也将您的屏幕分成九个特殊部分。单击TextView in the component tree
并按 Delete 键将其删除。执行相同的操作来移除复选框和按钮小部件,并完全清除显示。在左侧面板中找到Button
小部件,并单击它。在预览窗格中移动鼠标,注意鼠标移动时显示的突出显示部分。屏幕被分成由每个特殊FrameLayout
部分指示的区域(参见图 8-6 )。单击左上部分以放下按钮。双击按钮并将其文本更改为左上角以指示其位置。继续拖放其他八个部分中的小部件,并相应地标记它们。当您拖放每个按钮时,在文本模式和设计模式之间来回切换,以查看 XML 是如何生成的。当你完成时,你应该有类似图 8-7 的东西。参见清单 8-2 中创建该布局的代码。
图 8-7。
Layout demonstrating FrameLayout
图 8-6。
Preview pane is divided into nine drop sections Listing 8-2. Code That Creates the Figure 8-7 Layout
<FrameLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
xmlns:android="``http://schemas.android.com/apk/res/android
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Center"
android:id="@+id/button"
android:layout_gravity="center" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Top Left"
android:id="@+id/button2"
android:layout_gravity="left|top" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Top Center"
android:id="@+id/button3"
android:layout_gravity="center_horizontal|top" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Top Right"
android:id="@+id/button4"
android:layout_gravity="right|top" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Center Left"
android:id="@+id/button5"
android:layout_gravity="center|left" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Center Right"
android:id="@+id/button6"
android:layout_gravity="center|right" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Bottom Left"
android:id="@+id/button7"
android:layout_gravity="bottom|left" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Bottom Center"
android:id="@+id/button8"
android:layout_gravity="bottom|center" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Bottom Right"
android:id="@+id/button9"
android:layout_gravity="bottom|right" />
</FrameLayout>
设计者生成这个 XML,它以一个FrameLayout
标签开始。它的宽度和高度被设置为占据屏幕的整个可视区域。每个嵌套的按钮都指定了一个layout_gravity
,它决定了按钮落入屏幕的哪个区域。
线性布局
LinearLayout
水平或垂直地组织相邻的子节点。打开左侧的项目窗格。在res
文件夹下找到layout
文件夹,右键打开右键菜单。单击“新建➤ XML ➤ XML 布局文件”创建一个新的布局资源文件,并将其命名为 three_button。单击并将三个按钮放置到预览中,每个按钮都位于“上一个”按钮的下方。你的布局应该看起来像图 8-8 的左侧。在预览的左上方,点按“转换方向”按钮(在第二行按钮中)。屏幕上的按钮会从垂直对齐切换到水平对齐,如图 8-8 右图所示。
图 8-8。
Vertical LinearLayout vs. a Horizontal LinearLayout
下面的 XML(如清单 8-3 所示)以一个LinearLayout
根标签开始,它指定了一个方向属性。方向可以设置为垂直或水平。嵌套在LinearLayout
中的Button
标签根据方向从上到下或从左到右排列。
Listing 8-3. A Three-Button LinearLayout Example
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="``http://schemas.android.com/apk/res/android
android:orientation="horizontal" android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="New Button"
android:id="@+id/button1" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="New Button"
android:id="@+id/button2" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="New Button"
android:id="@+id/button3" />
</LinearLayout>
相对布局
通过使用相对属性来组织它的子节点。使用这些类型的布局时,您可以创建更复杂的设计,因为您可以更好地控制每个子视图的放置位置。在本例中,您将假装创建一个个人资料视图,类似于您在社交网络应用中找到的内容。
创建一个名为 relative_example 的新布局 XML 文件,并将RelativeLayout
指定为根元素。将一个ImageView
拖放到预览的左上角。拖动时您会看到辅助线,它应该会吸附到左上角。不要惊慌,这个控件在放下时会消失,因为我们没有给它维度或内容。在屏幕右侧的属性窗格中找到src
属性,并单击省略号以打开资源对话框。(您可能需要滚动属性才能找到src
。)选择系统页签,然后选择名为sym_def_app_icon
的资源,如图 8-9 所示。
图 8-9。
Select the sym_def_app_icon
图标将呈现在添加到预览窗格的ImageView
中。从调色板中点击PlainTextView
,然后点击ImageView
的右上方,将PlainTextView
放置在相对于该组件的右侧,并与其父组件的顶部对齐。当您在图像的右边缘移动鼠标时,会出现一个工具提示,指示当前的放置位置。操纵直至刀尖同时提示toRightOf=imageView
和alignParentTop
,如图 8-10 所示。
图 8-10。
Tool tips show as you move around the view
将另外两个PlainTextView
组件拖到预览上,将每个组件排在前面组件的下方和ImageView
的右侧。使用指南来帮助你。双击顶部的TextView
,并更改其文本以包含一个名称。更改中间TextView
的文本,以包含一个著名的城市。最后,改变底部TextView
的文本以包含一个网站。当您在 designer 视图中工作时,来回切换到 text 视图以查看生成的 XML。你应该有类似图 8-11 的东西。请参见清单 8-4 了解该布局背后的代码。
图 8-11。
The relative layout for the profile Listing 8-4. The Code Behind the Layout in Figure 8-11
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="``http://schemas.android.com/apk/res/android
android:layout_width="match_parent" android:layout_height="match_parent">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/imageView"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:src="@android:drawable/sym_def_app_icon" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Clifton Craig"
android:id="@+id/textView1"
android:layout_alignParentTop="true"
android:layout_toRightOf="@+id/imageView" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="California"
android:id="@+id/textView2"
android:layout_below="@+id/textView1"
android:layout_toRightOf="@+id/imageView" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="``http://codeforfun.wordpress.com
android:id="@+id/textView3"
android:layout_below="@+id/textView2"
android:layout_toRightOf="@+id/imageView" />
</RelativeLayout>
生成的 XML 包含作为根元素的RelativeLayout
。这个布局包含一个具有两个属性的ImageView
,layout_alignParentTop
和layout_alignParentLeft
。这些属性使ImageView
固定在布局的左上方。layout_alignParentStart
属性用于支持从右到左的语言,不会产生歧义。ImageView
也指定了我们之前研究过的高度和宽度属性。最后,它定义了一个指向sym_def_app_icon
资源的src
属性,这是一个由 Android 运行时预定义的内置资源。
每个小部件都包含一个值以@+id/
开始的android:id
属性。这些 ID 属性是在运行时定位单个小部件的一种方式。对于RelativeLayouts
,它们变得尤其重要,因为它们被用来指定一个小部件相对于另一个小部件的位置。注意剩余的TextView
组件如何使用layout_below
和layout_toRightOf
属性中的这些值。它们各自指定了layout_toRightOf=@+id/imageView
,这将它们直接放置在图像视图的右边缘。最后两个TextView
小部件指定了一个layout_below
属性,该属性指向紧接在它前面的TextView
。
嵌套布局
布局可以相互嵌套,以创建复杂的设计。如果您想改进前面的概要视图,您可以利用在您的RelativeLayout
中嵌套一个LinearLayout
的优势。该布局可以包括在线状态标签和描述字段。
在调色板中单击垂直的LinearLayout
,并在预览窗格中单击ImageView
的正下方以放置它。确保刀尖指示alignParentLeft
和below=imageView
。在调色板中点击Plain TextView
,然后在新添加的LinearLayout
内点击放置该组件。这将是您的在线状态指示器。接下来找到Large Text
小部件;在组件面板中点击它,这一次在右边的组件树中找到另一个新的TextView
,试着在它下面点击来放置组件。将鼠标悬停在LinearLayout
中的TextView
下方,会出现一个粗下划线的拖放目标指示器,如图 8-12 所示。
图 8-12。
Mouse under the TextView to see a drop-target indicator, and click to add the widget
使用 properties 窗格,将第一个TextView
的 text 属性更改为 online,并向它下面的TextView
的 text 属性添加一个伪描述。接下来单击预览中的任意位置,并按 Ctrl+A | Cmd+A 选择所有组件。找到layout:margin
属性,展开,设置 all 为5dp
,给每个组件一个 5 像素的边距,如图 8-13 所示。
图 8-13。
Give all widgets a 5-pixel margin
边距控制组件边缘和任何相邻组件之间的间距。为组件提供边距是减少界面混乱的好方法。尽管我们在所有组件的所有边上都设置了相同的边距,但是您可以尝试在某些边上设置不同的边距。
layout:margin
分组包含四个边的设置:左、上、右和下。再次选择所有组件,展开layout:margin
设置,找到全部选项。删除5dp
值,改为将5dp
值设置到左侧设置。组件将紧密地组合在一起,但是左边距在水平边缘之间留出足够的空间。选择在线的TextView
,设置它的上边距为5dp
,让它和上面的图片之间有更多的空间。图 8-14 显示了此时的结果。清单 8-5 显示了这个布局背后的代码。
图 8-14。
The results of adding left and top margins Listing 8-5. The Code for relative_example.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="``http://schemas.android.com/apk/res/android
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/imageView"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:src="@android:drawable/sym_def_app_icon"
android:layout_marginLeft="5dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Clifton Craig"
android:id="@+id/textView1"
android:layout_alignParentTop="true"
android:layout_toRightOf="@+id/imageView"
android:layout_marginLeft="5dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="California"
android:id="@+id/textView2"
android:layout_below="@+id/textView1"
android:layout_toRightOf="@+id/imageView"
android:layout_marginLeft="5dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="``http://codeforfun.wordpress.com
android:id="@+id/textView3"
android:layout_below="@+id/textView2"
android:layout_toRightOf="@+id/imageView"
android:layout_marginLeft="5dp" />
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/imageView"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_marginLeft="5dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Online"
android:id="@+id/textView"
android:layout_marginLeft="5dp"
android:layout_marginTop="5dp" />
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/editText"
android:text="Likes biking, reads tech manuals and loves to code in Java"
android:layout_marginLeft="5dp" />
</LinearLayout>
</RelativeLayout>
嵌套布局的另一种方法是用 includes 间接引用它们。找到LinearLayout
,更改其属性,使其包含一个值为details
的id
属性,并确保其高度设置为wrap_content
。同时更改设置layout_below
属性,使其属于textView3
。这显示在以下代码中:
<LinearLayout
android:id="@+id/details"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/textView3"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_marginLeft="5dp">
接下来,在最后一个TextView
标签下,但就在结束LinearLayout
标签之前,添加下面的include
标签:
<include layout="@layout/three_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/details"/>
特殊的include
标签将任何预定义的布局添加到当前布局。在前面的示例中,您在当前布局中包含了我们之前的三按钮示例。您将宽度声明为match_parent
,它扩展了布局的整个宽度,并将高度设置为wrap_content
。您还将按钮布局设置在details
组件的下面,这是相对布局的名称。
按住 Ctrl 键并单击| Cmd 键并单击布局属性的值@layout/three_button
,导航到其定义。在定义中,您将更改每个按钮的文本,以反映社交网络应用中可用的典型操作。更改每个按钮的文本属性,依次添加好友、关注和消息。您可以在文本或设计模式下完成此操作。图 8-15 展示了这在设计模式下的样子。
图 8-15。
Add labels to the buttons
完成后,导航回relative_example.xml
查看集成按钮。图 8-16 显示了完成的结果。
图 8-16。
The relative_example.xml with integrated buttons
列表视图
ListView
小部件是一个容器控件,它提供了一个项目列表,每个项目都是可操作的。这些列表项被组织在一个位于可滚动视图中的布局中。单个列表项的内容由适配器以编程方式提供,适配器从数据源提取内容。适配器将数据映射到布局中的各个视图。在这个例子中,您将探索一个ListView
组件的简单用法。
在 res ➤布局文件夹下创建一个名为list_view
的新布局。指定FrameLayout
作为根元素。在FrameLayout
的中心添加一个ListView
。预览窗格将显示使用默认布局的ListView
,称为简单的 2 行列表项。切换到文本编辑模式,向根元素标签添加一个xmlns:tools
属性。将其值设置为 http://schemas.android.com/tools
。这使得 tools:前缀属性可用,其中一个属性将用于更改预览渲染的方式。向ListView
标签添加一个tools:listitem
属性,并将其值设置为"@android:layout/simple_list_item_1"
。如下面的代码片段所示:
<FrameLayout xmlns:android="``http://schemas.android.com/apk/res/android
android:layout_width="match_parent" android:layout_height="match_parent"
xmlns:tools="``http://schemas.android.com/tools
>
<ListView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:id="@+id/listView"
android:layout_gravity="center"
tools:listitem="@android:layout/simple_list_item_1"
/>
</FrameLayout>
在 Android Studio 的早期版本中,可以在设计模式下右键单击预览窗格中的ListView
,从菜单中选择预览列表内容➤简单列表项,如图 8-17 所示。1.0 版本中删除了此功能。
图 8-17。
List Preview Layout option feature from Android Studio 0.8 beta
打开MainActivity
类,将其改为扩展ListActivity
,然后在onCreate()
方法中输入以下内容:
public class MainActivity extends ListActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.list_view);
String[] listItems = new String[]{"Mary","Joseph","Leah", "Mark"};
setListAdapter(new ArrayAdapter<String>(this,
android.R.layout.simple_list_item_1,
listItems));
}
//...
}
ListActivity
是一个特殊的基类,旨在提供处理ListView
的通用功能。在我们的例子中,我们使用提供的setListAdapter
方法,它将一个适配器与列表视图关联起来。我们创建一个ArrayAdapter
,并给它一个上下文(当前正在执行的活动)、一个列表项布局和一个填充ListView
的项目数组。现在构建并运行应用,它会崩溃!这是因为ListActivity
的常见误用。这个特殊的活动寻找一个 id 为@android:id/list
的ListView
。这些是由系统定义的特殊的 Android ids,这个特殊的 id 让ListActivity
找到它的ListView
,并自动将其连接到给定的ListAdapter
。按如下方式更改list_view
布局中的ListView
标签:
<ListView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:id="@android:id/list"
android:layout_gravity="center"
tools:listitem="@android:layout/simple_list_item_1"
/>
构建并测试应用,您应该会看到如图 8-18 所示的名称列表。
图 8-18。
Screenshot of a simple ListView
通过为列表项提供自定义布局,可以进一步自定义列表视图的外观。要想知道最终结果会是什么样子,打开list_view.xml
。右键单击预览窗格中的ListView
,将其预览列表内容设置回简单的两行列表项。这种布局使用一个大文本视图和一个小文本视图来显示多个值。切换到文本视图查看生成的 XML,如清单 8-6 所示。
Listing 8-6. Custom Layout for list items
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="``http://schemas.android.com/apk/res/android
xmlns:tools="``http://schemas.android.com/tools
android:layout_width="match_parent" android:layout_height="match_parent">
<ListView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:id="@android:id/list"
android:layout_gravity="center"
tools:listitem="@android:layout/simple_list_item_2" />
</FrameLayout>
在ListView
元素中添加了一个特殊的tools:listitem
属性来控制预览窗格中的布局。这个属性是在 tools XML 名称空间中定义的,它被添加到了FrameLayout
根元素中。ctrl+click | Cmd+单击listitem
属性的值,导航到其定义。该布局包括两个子视图,其id
值为@android:id/text1
和@android:id/text2
。我们之前的例子包括一个数组适配器,它知道如何向simple_list_item_1
布局添加值。使用这种新布局,您需要定制逻辑来为这两个子视图设置值。回到MainActivity
类。在最顶层定义一个内部的Person
类,为列表中的每个人保存一个额外的 web 站点值,并更改onCreate()
方法,如清单 8-7 所示。
Listing 8-7. Create Person Class and Modify onCreate( )
public class MainActivity extends ListActivity {
class Person {
public String name;
public String website;
public Person(String name, String website) {
this.name = name;
this.website = website;
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.list_view);
Person[] listItems = new Person[]{
new Person("Mary", "
www.allmybuddies.com/mary
new Person("Joseph", "``www.allmybuddies.com/joeseph
new Person("Leah", "``www.allmybuddies.com/leah
new Person("Mark", "``www.allmybuddies.com/mark
};
setListAdapter(new PersonAdapter(this,
android.R.layout.simple_expandable_list_item_2,
listItems)
);
}
//...
}
在这些修订中,您创建了一个由Person
对象组成的数组,每个对象都在构造函数中接受名称和网站字符串值。这些值缓存在公共变量中。(尽管我们强烈主张在常规实践中使用 getters 和 setters 而不是 public 变量,但为了简洁起见,我们在我们设计的示例中使用了后者。)然后你将列表和相同的simple_expandable_list_item_2
布局传递给一个自定义的PersonAdapter
,我们还没有定义它。按 Alt+Enter 激活 IntelliSense,这将给你机会为PersonAdapter
创建一个存根内部类。见图 8-19 。
图 8-19。
Add PesonAdapter inside onCreate( ) method
选择 Create Inner Class 选项,将在当前类中为您生成一个类存根。使用 Tab 键浏览构造函数参数。随着您的推进,将每个构造函数参数更改为Context context
、int layout
和Person[] listItems respectively
。让这个类扩展BaseAdapter
而不是实现ListItem
,然后使用清单 8-8 中的代码完成它的定义。因为我们在 PersonAdapter 中使用了Person
类,所以需要将它移到 MainActivity 之外。将光标放在Person
类定义上,按下 F6 将其移动到更高的级别。您将看到如图 8-20 所示的对话框。单击“重构”移动该类。
图 8-20。
Add PesonAdapter inside onCreate( ) method Listing 8-8. PersonAdapter Class
public class PersonAdapter extends BaseAdapter {
private final Context context;
private final int layout;
private Person[] listItems;
public PersonAdapter(Context context, int layout, Person[] listItems) {
this.context = context;
this.layout = layout;
this.listItems = listItems;
}
@Override
public int getCount() {
return listItems.length;
}
@Override
public Object getItem(int i) {
return listItems[i];
}
@Override
public long getItemId(int i) {
return i;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View view = convertView;
if (view==null) {
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
view = inflater.inflate(layout, parent, false);
}
TextView text1 = (TextView) view.findViewById(android.R.id.text1);
TextView text2 = (TextView) view.findViewById(android.R.id.text2);
text1.setText(listItems[position].name);
text2.setText(listItems[position].website);
return view;
}
}
这个基本示例说明了适配器如何创建单个列表项进行显示。定义从缓存上下文、布局资源 ID 和列表项作为成员变量开始,这些变量稍后用于创建单独的列表项视图。扩展BaseAdapter
为您提供了适配器接口中一些方法的默认实现。否则,这些方法将被要求显式定义。然而,您有义务为getCount()
、getItem()
、getItemId()
和getView()
抽象方法提供一个实现。运行时调用getCount()
方法,以便知道需要呈现多少视图。对于需要在给定位置检索项目的调用者来说,getItem()
是必需的。getItemId()
必须返回给定位置的项目的唯一编号。在我们的示例中,您可以只返回作为参数给出的位置,因为它是唯一的。最后,getView()
包含了组装每个列表项视图的所有逻辑。它用一个位置、一个可能为空也可能不为空的convertView()
以及包含它的ViewGroup
父对象被重复调用。如果convertView()
为空,您必须使用从构造函数缓存的布局 ID 和父视图组作为其目的地,来扩展一个新视图以保存列表项细节。您使用LAYOUT_INFLATER_SERVICE
系统服务进行膨胀。展开视图后,找到text1
和text2
子视图,并分别用给定位置的人的姓名和 web 站点值填充它们。运行这个示例,看看每个 person 对象是如何映射到新的布局的。图 8-21 显示了您的屏幕外观。
图 8-21。
List showing new list item layout and use of PersonAdapter
布局设计指南
市场上有如此多的 Android 设备,每一个都有不同的屏幕尺寸和密度,布局设计可能具有挑战性。设计布局时,你需要注意几点。你也可以遵循一些规则来跟上快速发展的形势。一般来说,你要注意屏幕分辨率和像素密度。
屏幕分辨率是屏幕在水平和垂直方向上可以容纳的像素总数,以二维数字的形式给出。分辨率通常以标准 VGA 测量值给出。VGA 代表视频图形阵列,是 640×480 的台式机和笔记本电脑的标准。这意味着 640 像素宽和 480 像素高。这年头能找到半 VGA (HVGA)、480×320 等移动变种;四分之一 VGA (QVGA),320×240;宽 VGA (WVGA),800×480;扩展图形阵列(XGA);宽 XGA(WXGA);以及更多。这些仅仅是一些可能的解决方案。
像素密度表示在给定的测量单位内可以压缩的像素总数。这种测量与屏幕大小无关,尽管它会受到屏幕大小的影响。例如,想象一下,20 英寸显示器的分辨率为 1024×768 像素,而 5 英寸显示器的分辨率为 1024×768 像素。两种情况下使用的像素数量相同,但后一种屏幕将这些像素压缩到一个更小的区域,从而增加了它们的密度。像素密度以每英寸点数(dpi)来衡量,它表示 1 英寸区域中可以容纳的点数或像素。在 Android 屏幕上,密度通常以一种称为密度无关像素(dp)的单位来测量。它是基于 160dpi 屏幕上相当于 1 个像素的基线测量。使用差压作为测量单位可使您的布局在不同密度的设备上适当缩放。
Android 包括了另一种隔离不同屏幕尺寸的方法:资源限定符。在我们之前的例子中,我们将一个图像复制到了drawable
文件夹中,这是任何可绘制资源被提取的默认位置。可绘制资源通常是图像,但也可以包括定义形状定义、轮廓和边框的资源 XML 文件。为了定位可绘制资源,Android 运行时首先考虑当前设备的屏幕尺寸。如果它属于主要类别列表中的一个,运行时将在带有资源限定符后缀的drawable
文件夹下查找。这些是后缀,比如ldpi
、mdpi
、hdpi
和xhdpi
。ldpi
后缀是低密度屏幕,大约 120dpi(每英寸 120 点)。中密度屏幕,160dpi,使用mdpi
后缀。高密度屏幕,320dpi,使用hdpi
后缀。超高密度屏幕使用xhdpi
后缀。这不是一个详尽的列表,但它代表了更常见的后缀。当您在 Android Studio 中启动一个项目时,会在res
文件夹下创建无数特定于分辨率的子文件夹。在下一个例子中,您将研究如何以实用的方式使用这些文件夹。
覆盖各种显示器尺寸
在本练习中,您将找到一个 200×200 像素的配置文件图像,并将其交换到您已经构建的RelativeLayout
示例中。您可以选择使用本书源代码下载中的图片。这将是您在最高分辨率显示器上使用的图像。
将图像命名为 my_profile.png,并将其保存到硬盘上。打开项目窗口,展开res
文件夹。您的项目应该有带有mdpi
、hdpi
、xhdpi
和xxhdpi
后缀的drawable
文件夹。您需要为不同的屏幕尺寸创建原始图像的缩小版本。您将遵循 3:4:6:8 的缩放比例进行调整。您可以使用 Microsoft Paint 或任何其他工具来调整大小。(一个名为 Image Resizer for Windows 的开源项目可在imageresizer.codeplex.com
获得,它可以使这项任务变得简单,并与 Windows 资源管理器很好地集成。)参考表 8-3 了解如何按照我们的比例指南在单个文件夹中创建缩放尺寸。将图像的每个版本保存在表格所示的文件夹中,并对每个版本使用相同的my_profile.png
名称。
表 8-3。
Various Image Asset Sizes and Descriptions
| 文件夹 | 原始大小 | 比例 | 缩放尺寸 | 图像 | | --- | --- | --- | --- | --- | | `drawable-xxhdpi` | 200×200 | 不适用的 | 200×200 | ![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fgitee.com%2FOpenDocCN%2Fvkdoc-android-zh%2Fraw%2Fmaster%2Fdocs%2Flearn-as%2Fimg%2FA978-1-4302-6602-0_8_Figa_HTML.jpg&pos_id=img-YfcM5Yk3-1723516111501) | | `drawable-xhdpi` | 200×200 | 3:4 | 150×150 | ![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fgitee.com%2FOpenDocCN%2Fvkdoc-android-zh%2Fraw%2Fmaster%2Fdocs%2Flearn-as%2Fimg%2FA978-1-4302-6602-0_8_Figb_HTML.jpg&pos_id=img-wkieiuyt-1723516111502) | | `drawable-hdpi` | 150×150 | 4:6 | 100×100 | ![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fgitee.com%2FOpenDocCN%2Fvkdoc-android-zh%2Fraw%2Fmaster%2Fdocs%2Flearn-as%2Fimg%2FA978-1-4302-6602-0_8_Figc_HTML.jpg&pos_id=img-O8SkPtg1-1723516111502) | | `drawable-mdpi` | 100×100 | 6:8 | 75×75 | ![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fgitee.com%2FOpenDocCN%2Fvkdoc-android-zh%2Fraw%2Fmaster%2Fdocs%2Flearn-as%2Fimg%2FA978-1-4302-6602-0_8_Figd_HTML.jpg&pos_id=img-mR5NGfAw-1723516111502) |添加这些图像后,在设计器模式下打开relative_example.xml
布局并找到图像视图。点击该组件的src
属性旁边的省略号,在资源对话框中找到my_profile
图像,如图 8-22 所示。
图 8-22。
Resources dialog box with image of Clifton Craig
更新图片后,点击预览窗口中的 Android 虚拟设备按钮,尝试各种屏幕渲染选项,如图 8-23 所示。选择 Preview All Screen Sizes(预览所有屏幕尺寸),可以同时在多个设备上查看模拟轮廓,如图 8-24 所示。
图 8-24。
Layout previewed on various devices
图 8-23。
Preview All Screen Sizes from Design mode in Visual Designer
把这一切放在一起
现在,您将使用 Java 加载布局,并探索如何在运行时进行细微的更改。在开始之前,您需要向将要使用的组件添加描述性 id。在设计模式下打开relative_example.xml
布局,并将以下 id 添加到这些嵌套在LinearLayout
中的组件中:
imageView: profile_image
textView1: name
textView2: location
textView3: website
textView4: online_status
editText: description
通过单击每个小部件,然后在右窗格的属性编辑器中更改其id
属性来进行这些更改。当您进行更改时,您将看到一个弹出对话框,要求更新使用情况。见图 8-25 。
图 8-25。
Android Studio will update usages while you work
选中复选框并单击是,允许 Android Studio 在您工作时更新每个小部件的所有用法。切换到文本模式查看最终结果,如清单 8-9 所示。
Listing 8-9. New Layout with Components Placed Inside
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="``http://schemas.android.com/apk/res/android
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/profile_image"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:src="@drawable/my_profile"
android:layout_marginLeft="5dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Clifton Craig"
android:id="@+id/name"
android:layout_alignParentTop="true"
android:layout_toRightOf="@+id/profile_image"
android:layout_marginLeft="5dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="California"
android:id="@+id/location"
android:layout_below="@+id/name"
android:layout_toRightOf="@+id/profile_image"
android:layout_marginLeft="5dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="``http://codeforfun.wordpress.com
android:id="@+id/website"
android:layout_below="@+id/location"
android:layout_toRightOf="@+id/profile_image"
android:layout_marginLeft="5dp" />
<LinearLayout
android:id="@+id/details"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/profile_image"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_marginLeft="5dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Online"
android:id="@+id/online_status"
android:layout_marginLeft="5dp"
android:layout_marginTop="5dp" />
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/description"
android:text="Likes biking, reads tech manuals and loves to code in Java"
android:layout_marginLeft="5dp" />
</LinearLayout>
<include
android:id="@+id/buttons"
layout="@layout/three_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/details"/>
</RelativeLayout>
注意 Android Studio 不仅更新了 id 定义,还更新了每个 id 的每次使用,以保持组件彼此相邻对齐,就像以前一样。创建一个名为ProfileActivity
的新类,并将其修改为清单 8-10 。
Listing 8-10. ProfileActivity Class
public class ProfileActivity extends Activity {
private TextView name;
private TextView location;
private TextView website;
private TextView onlineStatus;
private EditText description;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.relative_example);
name = (TextView) findViewById(R.id.name);
location = (TextView) findViewById(R.id.location);
website = (TextView) findViewById(R.id.website);
onlineStatus = (TextView) findViewById(R.id.online_status);
description = (EditText) findViewById(R.id.description);
View parent = (View) name.getParent();
parent.setBackgroundColor(getResources().getColor(android.R.color.holo_blue_light));
name.setTextAppearance(this,android.R.style.TextAppearance_DeviceDefault_Large);
location.setTextAppearance(this, android.R.style.TextAppearance_DeviceDefault_Medium);
location.setTextAppearance(this, android.R.style.TextAppearance_DeviceDefault_Inverse);
website.setTextAppearance(this, android.R.style.TextAppearance_DeviceDefault_Inverse);
onlineStatus.setTextAppearance(this, android.R.style.TextAppearance_DeviceDefault_Inverse);
description.setEnabled(false);
description.setBackgroundColor(getResources().getColor(android.R.color.white));
description.setTextColor(getResources().getColor(android.R.color.black));
}
}
这里您已经为每个TextView
和EditText
组件添加了成员字段。onCreate()
方法首先找到每个视图组件,并将它们保存在单独的成员变量中。接下来,您找到名称标签的父标签,并将其背景颜色更改为浅蓝色。Android Studio 有一个独特的功能,用一个正方形装饰左边的槽,说明这一行引用的颜色。这些方块也出现在引用颜色资源的其他行上。然后更改每个TextView
的文本外观,使名称以大的外观突出出来。您正在使用来自android.R
类的预定义样式,该类包含对 Android SDK 中所有可用资源的引用。每个剩余的TextView
也被更新以使用中等或相反的外观。最后,禁用描述EditText
以防止修改其内容。您还可以将其背景设置为白色,同时将文本颜色更改为黑色。
要尝试我们新的ProfileActivity
和布局,你必须在AndroidManifest.xml
中定义它,并将其链接到MainActivity
。打开清单,在MainActivity
定义下为我们的ProfileActivity
添加一个标签:
<activity
android:name=".ProfileActivity"
android:label="@string/app_name" />
接下来返回到MainActivity
,用下面的代码覆盖onListItemClick()
方法,围绕ProfileActivity
类创建一个新的意图,并开始活动。运行该示例,并尝试单击任何列表项以显示其配置文件。参见图 8-26 。
图 8-26。
New layout with buttons and EditText
@Override
protected void onListItemClick(ListView l, View v, int position, long id) {
super.onListItemClick(l, v, position, id);
Intent intent = new Intent(this, ProfileActivity.class);
startActivity(intent);
}
现在您将学习如何将值从列表视图带入下一个活动。使用清单 8-11 中的代码更改MainActivity
类中的onCreate()
方法。
Listing 8-11. Modifications to MainActivity
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.list_view);
Person[] listItems = new Person[]{
new Person(R.drawable.mary, "Mary", "New York",
"``www.allmybuddies.com/mary
"Avid cook, writes poetry."),
new Person(R.drawable.joseph,"Joseph", "Virginia",
"``www.allmybuddies.com/joeseph
"Author of several novels."),
new Person(R.drawable.leah, "Leah", "North Carolina",
"``www.allmybuddies.com/leah
"Basketball superstar. Rock climber."),
new Person(R.drawable.mark, "Mark", "Denver",
"``www.allmybuddies.com/mark
"Established chemical scientist with several patents.")
};
setListAdapter(new PersonAdapter(this,
android.R.layout.simple_expandable_list_item_2,
listItems)
);
}
您正在向构造函数调用添加名称、位置和描述字段。现在使用清单 8-12 中的代码将 Person 类更改为接受并保存这些新值。
Listing 8-12. Modifications to the Person Class
class Person {
public int image;
public String name;
public String location;
public String website;
public String descr;
Person(int image, String name, String location, String website, String descr) {
this.image = image;
this.name = name;
this.location = location;
this.website = website;
this.descr = descr;
}
}
Next change the onListItemClick() as follows:
@Override
protected void onListItemClick(ListView l, View v, int position, long id) {
super.onListItemClick(l, v, position, id);
Person person = (Person) l.getItemAtPosition(position);
Intent intent = new Intent(this, ProfileActivity.class);
intent.putExtra(ProfileActivity.IMAGE, person.image);
intent.putExtra(ProfileActivity.NAME, person.name);
intent.putExtra(ProfileActivity.LOCATION, person.location);
intent.putExtra(ProfileActivity.WEBSITE, person.website);
intent.putExtra(ProfileActivity.DESCRIPTION, person.descr);
startActivity(intent);
}
在这里,您检索被单击的Person
对象,并将它的每个成员变量作为额外的值传递给下一个活动。这些额外的值被映射到ProfileActivity
常量,我们在ProfileActivity
类的顶部定义了这些常量:
public class ProfileActivity extends Activity {
public static final String IMAGE = "IMAGE";
public static final String NAME = "NAME";
public static final String LOCATION = "LOCATION";
public static final String WEBSITE = "WEBSITE";
public static final String DESCRIPTION = "DESCRIPTION";
//...
}
现在对清单 8-13 和ProfileActivity
进行如下修改,以定义一个profileImage ImageView
成员变量,并将所有额外的意图读入缓存的视图组件。
Listing 8-13. Modifications to the PersonActivity Class
private ImageView proflieImage;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.relative_example);
name = (TextView) findViewById(R.id.name);
location = (TextView) findViewById(R.id.location);
website = (TextView) findViewById(R.id.website);
onlineStatus = (TextView) findViewById(R.id.online_status);
description = (EditText) findViewById(R.id.description);
proflieImage = (ImageView) findViewById(R.id.profile_image);
int profileImageId = getIntent().getIntExtra(IMAGE, -1);
proflieImage.setImageDrawable(getResources().getDrawable(profileImageId));
name.setText(getIntent().getStringExtra(NAME));
location.setText(getIntent().getStringExtra(LOCATION));
website.setText(getIntent().getStringExtra(WEBSITE));
description.setText(getIntent().getStringExtra(DESCRIPTION));
运行应用,尝试点击列表视图中的项目,调出相应的配置文件。您可以点击后退键导航回列表视图并选择不同的项目。参见图 8-27 。
图 8-27。
Layout with ImageView
碎片
片段是您之前检查的活动和可包含文件之间的一个步骤。片段是可重用的 XML 片段,类似于包含布局。然而,像活动一样,它们有包含业务逻辑的额外好处。片段用于使你的用户界面适应不同的外形。考虑一下我们之前的例子,我们开发的时候考虑到了智能手机,在 10 英寸的平板电脑上会是什么样子。更大的显示屏所提供的额外空间会让一个简单列表视图的屏幕看起来很笨拙。使用片段,您可以智能地组合两个屏幕,这样您的显示就像当前在较小的屏幕上一样,但在较大的屏幕上包含列表和细节视图。要完成这项任务,您必须将所有的 UI 更新逻辑从活动中移出,放到新的片段类中。从MainActivity
中的ListView
逻辑开始,您需要将嵌套类作为外部顶级类取出。点击MainActivity
顶部的Person
类,然后按 F6。弹出的 Move Refactor 对话框询问您希望将类移动到哪个包和目录中。您可以在这里接受默认值。对底层的PersonAdapter
类做同样的事情。
创建一个名为BuddyListFragment
的新类,它扩展了ListFragment
并包含了MainActivity
中的ListView
的初始化,如清单 8-14 所示。
Listing 8-14. BuddyListFragment Class Which Extends ListFragment
import android.app.Activity;
import android.os.Bundle;
import android.support.v4.app.ListFragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ListView;
public class BuddyListFragment extends ListFragment {
private OnListItemSelectedListener onListItemSelectedListener;
public interface OnListItemSelectedListener {
void onListItemSelected(Person selectedPerson);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Person[] listItems = new Person[]{
new Person(R.drawable.mary, "Mary",
"``www.allmybuddies.com/mary
"New York","Avid cook, writes poetry."),
new Person(R.drawable.joseph, "Joseph",
"``www.allmybuddies.com/joeseph
"Virginia","Author of several novels"),
new Person(R.drawable.leah, "Leah",
"``www.allmybuddies.com/leah
"North Carolina",
"Basketball superstar. Rock climber."),
new Person(R.drawable.mark,"Mark",
"``www.allmybuddies.com/mark
"Denver",
"Established chemical scientist with several patents.")
};
setListAdapter(new PersonAdapter(getActivity(),
android.R.layout.simple_expandable_list_item_2,
listItems)
);
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
if(!(activity instanceof OnListItemSelectedListener)) {
throw new ClassCastException(
"Activity should implement OnListItemSelectedListener");
}
//Save the attached activity as an onListItemSelectedListener
this.onListItemSelectedListener = (OnListItemSelectedListener) activity;
}
@Override
public void onListItemClick(ListView l, View v, int position, long id) {
Person selectedPerson = (Person) l.getItemAtPosition(position);
this.onListItemSelectedListener.onListItemSelected(selectedPerson);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.list_view, container, false);
}
}
这个片段镜像了MainActivity
中的onCreate()
方法,但是增加了两个生命周期方法。onAttach()
方法捕获必须实现在类顶部声明的OnListItemSelectedListener()
的附加活动。ListFragment
超类定义了一个在这里被覆盖的onListItemClick()
回调方法。在我们的自定义版本中,您可以引用缓存的onListItemSelectedListener()
并将选择的人传递到它上面。最后,您覆盖了扩展我们的list_view
布局的onCreateView()
生命周期方法,并将其返回到运行时。
创建一个扩展Fragment
的BuddyDetailFragment
类,并用清单 8-15 中所示的代码填充它。
Listing 8-15. BuddyDetailFragment Code
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
public class BuddyDetailFragment extends Fragment {
public static final String IMAGE = "IMAGE";
public static final String NAME = "NAME";
public static final String LOCATION = "LOCATION";
public static final String WEBSITE = "WEBSITE";
public static final String DESCRIPTION = "DESCRIPTION";
private Person person;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
updatePersonDetail(bundle);
return inflater.inflate(R.layout.relative_example, container, false);
}
@Override
public void onStart() {
super.onStart();
updatePersonDetail(getArguments());
}
private void updatePersonDetail(Bundle bundle) {
//if bundle arguments were passed, we use them
if (bundle != null) {
this.person = new Person(
bundle.getInt(IMAGE),
bundle.getString(NAME),
bundle.getString(LOCATION),
bundle.getString(WEBSITE),
bundle.getString(DESCRIPTION)
);
}
//if we have a valid person from the bundle
//or from restored state then update the screen
if(this.person !=null){
updateDetailView(this.person);
}
}
public void updateDetailView(Person person) {
FragmentActivity activity = getActivity();
ImageView profileImage = (ImageView) activity.findViewById(R.id.profile_image);
TextView name = (TextView) activity.findViewById(R.id.name);
TextView location = (TextView) activity.findViewById(R.id.location);
TextView website = (TextView) activity.findViewById(R.id.website);
EditText description = (EditText) activity.findViewById(R.id.description);
profileImage.setImageDrawable(getResources().getDrawable(person.image));
name.setText(person.name);
location.setText(person.location);
website.setText(person.website);
description.setText(person.descr);
}
}
这个思路和之前创造的ProfileActivity
差不多。但是,现在您有了一个内部的Person
成员变量,用于保存包值。您这样做是因为您现在从两个地方读取包中的值,onCreate()
和onStart()
。您还创建了一个公共方法,允许外部调用者用给定的人更新片段。另一件要注意的事情是,您覆盖了onCreateView()
生命周期方法,并要求充气机通过使用资源 ID 来膨胀适当的视图。
我们的主屏幕将被更改以反映单个片段,它将像以前一样具有列表视图。简化activity_main
布局,由一个FrameLayout
组成:
<FrameLayout xmlns:android="``http://schemas.android.com/apk/res/android
android:id="@+id/empty_fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
>
</FrameLayout>
这个空布局有一个适当命名的 ID,值为empty_fragment_container
。稍后,您将向该布局动态添加一个片段。重新访问您的res
目录,使用特殊资源限定符large
创建另一个布局文件。右键单击res
文件夹,选择新建➤ Android 资源文件,添加新的资源文件。将名称设置为activity_main
,与之前使用的名称相同。将资源类型设置为布局。从可用限定符列表中选择大小;从屏幕尺寸下拉菜单中选择大。参见图 8-28 进行指导。这与我们前面的例子相似,在前面的例子中,您为不同的屏幕密度添加了图像。布局文件将位于layout-large
目录中。此文件夹中的布局将在分类为大型的设备上选择,例如 7 英寸及以上的平板电脑。
图 8-28。
Select layout-large from New Resource Directory
打开新创建的activity_main
布局,切换到文本模式并输入以下 XML:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="``http://schemas.android.com/apk/res/android
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment android:name="com.apress.gerber.simplelayouts.BuddyListFragment"
android:id="@+id/list_fragment"
android:layout_width="wrap_content"
android:layout_height="match_parent"
/>
<fragment android:name="com.apress.gerber.simplelayouts.BuddyDetailFragment"
android:id="@+id/detail_fragment"
android:layout_width="wrap_content"
android:layout_height="match_parent"
/>
</LinearLayout>
请注意,它在平板电脑 AVD 上呈现布局。预览窗格会抱怨在设计时没有足够的信息来呈现您的布局。它将建议几个布局与预览相关联,并给你一个选项来选择你的项目布局。使用“选择布局”超链接为您的项目选择布局。选择第一个片段的list_view
布局,第二个片段的relative_example
布局,如图 8-29 所示。
图 8-29。
Linear layout containing fragments
此时,列表视图将占据整个屏幕。您需要稍微调整宽度和重量,以便为查看两个片段留出空间。诀窍是将宽度设置为 0dp,并使用 weight 属性来适当调整小部件的大小。将两个片段的宽度都更改为 0dp。将BuddyListFragment
重量设置为 1,将BuddyDetailFragment
重量设置为 2。weight 属性允许您根据可用空间的比率来调整组件的大小。系统将布局中所有组件的重量相加,并用该总和除以可用空间。每个组件占据的空间部分相当于其重量。在我们的例子中,细节片段将占据屏幕的 2/3,而列表将占据 1/3。您的更改应该类似于清单 8-16 。
Listing 8-16. Linear Layout Containing Fragments with Changes
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="``http://schemas.android.com/apk/res/android
xmlns:tools="``http://schemas.android.com/tools
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment android:name="com.apress.gerber.simplelayouts.BuddyListFragment"
android:id="@+id/list_fragment"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="match_parent"
tools:layout="@layout/list_view" />
<fragment android:name="com.apress.gerber.simplelayouts.BuddyDetailFragment"
android:id="@+id/detail_fragment"
android:layout_weight="2"
android:layout_width="0dp"
android:layout_height="match_parent"
tools:layout="@layout/relative_example" />
</LinearLayout>
尝试预览窗格。使用工具栏中的控制将方向更改为横向或选取不同的 avd。图 8-30 展示了 Nexus 10 的横向布局。
图 8-30。
View the fragment in landscape on a Nexus 10
有了这两个片段,你就可以打开MainActivity
并简化它了。Make it extend FragmentActivity. FragmentActivity
是一个特殊的类,它允许您在视图层次结构中查找片段,并通过一个FragmentManager
类执行片段事务。您将在我们的示例中使用事务来添加和替换屏幕上的片段。在小屏幕设备上,运行时将使用empty_fragment_container
选择布局。您将使用FragmentManager
将我们的BuddyListFragment
添加到屏幕上。您还将在用一个片段替换另一个片段时创建一个事务,并将其添加到后台堆栈,以便用户可以通过单击 back 按钮来展开操作。
简化清单 8-17 中所示的onCreate()
方法。
Listing 8-17. Simplified onCreate( ) Method
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if(findViewById(R.id.empty_fragment_container)!=null) {
// We should return if we're being restored from a previous state
// to avoid overlapping fragments.
if (savedInstanceState != null) {
return;
}
BuddyListFragment buddyListFragment = new BuddyListFragment();
// Pass any Intent extras to the fragment as arguments
buddyListFragment.setArguments(getIntent().getExtras());
FragmentTransaction transaction =
getSupportFragmentManager().beginTransaction();
transaction.add(R.id.empty_fragment_container, buddyListFragment);
transaction.commit();
}
}
通过将内容视图设置为activity_main
布局来开始这个方法,这是您简化的。然后,检查如果应用正在从先前的状态恢复,是否应该提前返回。然后实例化BuddyListFragment
,并将任何额外的意图作为参数传递给它。接下来创建一个FragmentTransaction
,向其中添加片段并提交事务。您只有在找到empty_fragment_container
的情况下才执行此操作。
更改类声明,使其也实现BuddyListFragment.OnListItemSelectedListener
接口。它应该如下所示:
public class MainActivity extends FragmentActivity
implements BuddyListFragment.OnListItemSelectedListener {
IntelliSense 将标记错误的类,因为它没有定义所需的方法。按 Alt+Enter 并选择提示来生成方法存根。使用清单 8-18 中所示的示例进行填写。
Listing 8-18. Code Showing FragmentManager Transaction
@Override
public void onListItemSelected(Person selectedPerson) {
BuddyDetailFragment buddyDetailFragment = (BuddyDetailFragment)
getSupportFragmentManager().findFragmentById(R.id.detail_fragment);
if (buddyDetailFragment != null) {
buddyDetailFragment.updateDetailView(selectedPerson);
} else {
buddyDetailFragment = new BuddyDetailFragment();
Bundle args = new Bundle();
args.putInt(BuddyDetailFragment.IMAGE, selectedPerson.image);
args.putString(BuddyDetailFragment.NAME, selectedPerson.name);
args.putString(BuddyDetailFragment.LOCATION, selectedPerson.location);
args.putString(BuddyDetailFragment.WEBSITE, selectedPerson.website);
args.putString(BuddyDetailFragment.DESCRIPTION, selectedPerson.descr);
buddyDetailFragment.setArguments(args);
//Start a fragment transaction to record changes in the fragments.
FragmentTransaction transaction =
getSupportFragmentManager().beginTransaction();
// Replace whatever is in the fragment_container view with this fragment,
// and add the transaction to the back stack so the user can navigate back
transaction.replace(R.id.empty_fragment_container, buddyDetailFragment);
transaction.addToBackStack(null);
// Commit the transaction
transaction.commit();
}
}
移除onListItemSelected()
方法,因为这段代码替换了它。在这里,您检查buddyDetailFragment
是否已经在视图层次结构中。如果有,你找到它并更新它。否则,您可以重新创建它,并通过使用您在BuddyDetailFragment
中定义的键,将所选的人作为一个包中的单个值传递进来。最后,创建并提交一个片段事务,用细节片段替换列表片段,并将事务添加到后台堆栈。在平板电脑和智能手机上运行代码(分别为图 8-31 和图 8-32 ),查看不同的行为。您可以创建一个 Nexus 10 平板电脑 AVD,用于大屏幕测试。
图 8-32。
Layout rendered on a phone
图 8-31。
Layout rendered on a tablet
摘要
在本章中,你学习了在 Android Studio 中设计用户界面的基础知识。您使用了可视化设计器和文本编辑器来创建和修改布局。您了解了如何使用各种容器和属性来对齐用户界面中的元素,以及如何嵌套容器来创建复杂的界面。我们解释了如何针对不同的屏幕尺寸和设备类型调整布局中的元素,并举例说明了如何在多个设备上同时查看布局。我们谈到了碎片。每个主题都有更多的细节。Android 包括丰富的定制,允许您构建和调整用户界面来满足您的需求。参见 https://developer.android.com
了解更多可用的高级特性和 API。