Final

本章为移动程序开发——Android开发的期末复习专题,提供了一些简单的例子和图像。

Foundation

这里是一些关于 Android 开发的基础知识。

线程与进程有什么关系

  1. 一个线程只能属于一个进程,一个进程可以有多个线程,但至少有一个线程。
  2. 资源是分配给进程的。同一个进程的所有线程共享该进程的所有资源。
  3. 处理器是分配给线程的,也就是说,线程真正运行在处理器上。
  4. 线程在执行过程中需要同步。不同进程的线程应使用消息通信来实现同步。

 

  1. A thread can only belong to one process, and a process can have multiple threads, but at least one thread.
  2. Resources are allocated to processes. All threads of the same process share all resources of the process.
  3. The processor is distributed to threads, that is, the thread is really running on the processor.
  4. Threads need to be synchronized during execution. The threads of different processes should use message communication to achieve synchronization.

 

Activity的生命周期回调函数的顺序是怎么样的?

生命周期流程可以如下表示:

  • Activity 首次启动: onCreate() -> onStart() -> onResume()
  • 另一个 Activity 启动并遮挡当前 Activity: onPause() -> onStop()
  • 用户返回到之前被遮挡的 Activity: onRestart() -> onStart() -> onResume()
  • 用户按下返回键或关闭 Activity: onPause() -> onStop() -> onDestroy()

 

  • onCreate() 方法只会在 Activity 第一次被创建时调用一次
  • onRestart() 方法只会在 Activity 从 onStop() 状态返回时调用。
  • onPause() 它在 Activity 离开前台时被调用,一般在此保存一些资源和状态操作
  • onStop() 表示 Activity 完全不可见时调用
  • onDestroy() 是 Activity 被销毁前的最后一次机会进行清理工作。

image-20241104173435712

 

AndroidManifest.xml 中,如何设置Activity的标题以及是否为首页?

AndroidManifest.xml 中,可以在AndroidManifest.xml中为Activity指定一个标签(label)

<activity android:name=".MainActivity"
    android:label="@string/app_name">
</activity>

要将某个Activity设置为应用的首页,需要在AndroidManifest.xml文件中为这个Activity添加<intent-filter>,并包含ACTION_MAINCATEGORY_LAUNCHER

<activity android:name=".MainActivity">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

 

在 Android 项目结构中,存在哪些关键的文件夹和文件,它们的作用是什么?

我们的 Android 项目中,主要的项目文件存放于:app/src/main,下面也以这个文件夹为根目录。

  1. java/: 存放Activity、Fragment、Service、BroadcastReceiver 等组件的 Java/Kotlin 代码。
  2. res/:这是存放应用程序各种资源文件的目录。
    • res/menu/: 存放菜单资源文件,如选项菜单、上下文菜单等。
    • res/drawable/存放可绘制资源,如图片、九宫格图像、颜色状态列表等。
    • res/values/存放各种值的资源文件,包括字符串、颜色、尺寸、样式、维度等。
      • res/values/colors.xml:定义颜色资源,便于在应用中统一管理和使用颜色。
      • res/values/strings.xml:定义字符串资源,便于本地化和国际化。
    • res/layout/:存放XML布局文件,定义应用的用户界面。
    • res/mipmap/: 存放应用的图标资源,不同密度的图标会放在相应的子文件夹中,如mipmap-mdpimipmap-hdpi等。

 

下面是一些常见组件,及其解释。

  • app/src/main/AndroidManifest.xml: 是每个Android应用都必须包含的一个文件,它位于应用的根目录下。这个文件向Android系统描述了应用的基本信息,包括应用的组件(如Activity、Service、BroadcastReceiver和ContentProvider)、权限需求、使用的API级别等。
    This manifest file records the configuration of the application, such as components, libraries, and other declarations.
  • Activity.java: 是Android应用中的一个关键组件,它负责与用户进行交互,通常表现为屏幕上的一个界面。
    Activities describe what your app does, and how the application interacts with the user.
  • activity_layout.xml: 样式文件,定义应用的用户界面。
    Layouts describe what your app looks like.
  • value/*.xml: 存放各种值的资源文件,包括字符串、颜色等。
    Variable Resource File definition, where you can find the set text value.

 

image-20241227151100965

 

startActivity.java 和 startActivity.class 之间有什么区别?

startActivity.javastartActivity.class 代表的是不同的阶段和不同类型的文件

特性 startActivity.java startActivity.class
类型 Java 源代码文件 Java 字节码文件
内容 人类可读的 Java 代码 JVM 可执行的字节码
生成方式 开发者编写 通过 Java 编译器编译 .java 文件生成
用途 编写程序逻辑 JVM 执行程序
在 Android 中 存放 Activity、Service 等组件的源代码 Android 运行时加载和执行的类文件

可以把 .java 文件比作菜谱,里面写着做菜的步骤(源代码)。而 .class 文件就像按照菜谱做出来的菜肴,可以直接食用(被 JVM 执行)。.java是编译前的文件,.class是编译后的文件。

 

Intent 是什么?

Intent 在 Android 中是一个消息传递对象,你可以使用它来请求执行某个操作。可以将 Intent 看作是不同组件之间进行通信的信使。通过 Intent,你可以启动 Activity、启动 Service、传递广播消息等。

Intent 可以分为两种类型:

  • 显式 Intent (Explicit Intent): 明确指定了要启动的目标组件的类名。系统会直接启动指定的组件。
Intent intent = new Intent(this, TargetActivity.class);
startActivity(intent);
  • 隐式 Intent (Implicit Intent): 没有明确指定要启动的目标组件,而是声明了一个 Action 和可选的 Data、Category。系统会根据这些信息匹配能够处理该 Intent 的组件。
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("https://www.example.com"));
startActivity(intent);

 

Adapter 是什么? 常见的 Adapter 有哪些,它们都有什么功能?

Adapter 在 Android 中是一个连接数据源和 AdapterView 的桥梁。它可以从数据源(例如数组、List、数据库查询结果等)中获取要显示的数据。

以下是一些常见的 Android Adapter 及其功能:

  • ArrayAdapter: 用于将一个数组或 List 中的数据展示在 AdapterView 上。
  • SimpleAdapter: 用于将一个 List 类型的、由 Map 对象组成的列表展示在 AdapterView 上。每个 Map 对象代表一行数据,键对应数据项的字段名,值对应字段值。 比 ArrayAdapter 更灵活,可以展示包含多个字段的数据。
  • CursorAdapter: 用于将 Cursor 对象中的数据展示在 AdapterView 上。Cursor 通常是数据库查询的结果集。
  • RecyclerView.Adapter: 用于 RecyclerView 的 Adapter。RecyclerView 是一个更加灵活和高效的 AdapterView,用于展示大量数据。

 

有哪些值得注意的Layout设计问题?

在 Android 开发中,Layout 是用于组织和排列屏幕上 UI 元素的容器。

以下是一些常见的布局类型:

  • LinearLayout (线性布局): 以水平或垂直方向排列子 View。是最基本的布局之一。
  • RelativeLayout (相对布局): 允许你相对于父容器或其他 View 的位置来定位子 View。
  • FrameLayout (帧布局): 最简单的布局,将所有子 View 堆叠在左上角。后添加的 View 会叠加在之前的 View 上。通常用于显示单个 View 或作为其他复杂布局的容器。
  • ConstraintLayout (约束布局): 一个功能强大且灵活的布局,允许你通过定义 View 之间的约束关系来创建复杂的布局,并且能有效减少布局的嵌套层级,提高性能。
  • GridLayout (网格布局): 以网格的形式排列子 View,可以将屏幕划分为行和列。
  • CoordinatorLayout (协调布局): 用于协调其子 View 之间的行为,常用于实现复杂的交互效果,例如带有折叠 Toolbar 的界面。

LinearLayout中,你至少需要设置其宽度和高度以及排列方向

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal/vertical">

</LinearLayout>

LinearLayout 的默认排列方向是 horizontal

wrap_contentmatch_parentView 的两个非常重要的布局参数,用于控制 View 的宽度和高度。

  • wrap_content: 表示 View 的大小会根据其内容的大小来调整。例如,如果一个 TextView 的文本内容很短,那么它的宽度就会很窄;如果文本内容很长,它的宽度就会相应地变宽。
  • match_parent: 表示 View 的大小会填充其父容器的剩余空间。对于宽度来说,它会填充父容器的宽度;对于高度来说,它会填充父容器的高度。

 

在 Android 中,id 是用于唯一标识一个 View 的标识符。你可以在 XML 布局文件中使用 android:id 属性来设置 View 的 ID。

android:id="@+id/your_view_id"

在 Java 代码中使用这个 ID 来获取 View 的实例,例如:

TextView userNameTextView = findViewById(R.id.user_name_text);

在EditText组件中,我们经常使用hint用于提示用户的输入:

android:hint="默认提示文本"

在CheckBox控件中,可以设计默认选中:

android:checked="true"

CheckBox 提供了几个方法和事件用于设置或者获取自身是否选中状态:

if (box.isChecked()){
    // 选中
}
box.setChecked(true);  // 设置选中

 

为什么需要用SQLite数据库,它有什么优点?

在 Android 应用开发中,我们经常需要持久化存储数据。这意味着即使应用被关闭或者设备重启,数据仍然能够被保存下来,并在下次应用启动时可以被访问。SQLite 是一个轻量级的、基于文件的、开源的关系型数据库管理系统。它不需要独立的服务器进程,整个数据库都存储在一个单独的文件中,这使得它非常适合嵌入式系统

SQLite具有如下优点:

  1. 轻量级 (Lightweight): SQLite 的代码库非常小巧,对系统资源的占用很低。这对于资源有限的移动设备来说非常重要。
  2. 事务性 (Transactional): SQLite 支持 ACID 事务属性(原子性、一致性、隔离性、持久性),这意味着你可以确保数据操作的完整性和可靠性。即使在应用崩溃或设备断电的情况下,数据也不会丢失或损坏。
  3. 内置支持 (Built-in Android Support): Android 系统内置了对 SQLite 的支持,提供了方便的 API (例如 SQLiteDatabase, SQLiteOpenHelper) 来进行数据库操作。这使得在 Android 应用中使用 SQLite 非常方便。

 


Click Button

在activity中,如何向 TextView 显示所选 Spinner 的值?

Show the value of choosen Spinner to TextView

image-20241104161733726

public void onClickFindBeer(View view) {
    // Get a reference to the TextView
    TextView brands = (TextView) findViewById(R.id.brands);
    
    // Get a reference to the Spinner
    Spinner color = (Spinner) findViewById(R.id.color);
    
    // Get the selected item in the Spinner
    String beerType = String.valueOf(color.getSelectedItem());
    
    // Display the selected item
    brand.setText(beerType);
}

Intent

在当前 Activity 中,如何跳转到 ReceiveMessageActivity,并携带一些信息?

Start the other activity (ReceiveMessageActivity) and put some message in the intent.

image-20241104163047069

import android.content.Intent
    ...
public void onSendMessage(View view) {
    // Get a reference to the EditView
    EditView messageView = (EditView) findViewById(R.id.message);
    
    // Get the value from EditView
    String message = messageView.getText().toString();
    
    // Define Intent
    Intent intent = new Intent(this, ReceiveMessageActivity.class);
    
    // Put infomation
    intent.putExtra(ReceiveMessageActivity.EXTRA_MESSAGE, message);
    
    // Start Intent
    startActivity(intent);
}

putExtra("var", value); "var" 需要引号,表示hash table中的一个名称,是String类的。

image-20241104171000355

  1. 用户点击按钮后,onSendMessage()回应点击。它创建了一个Intent,并向ReceiveMessageActivity发送了信息,并打开它。
  2. ReceiveMessageActivity接收Intent的信息,并且展示出来。

在 ReceiveMessageActivity 中如何获取传递的信息?

Get infomation from intent

import android.content.Intent
    
public class ReceiveMessageActivity ... {
    // Define a constant to pass the value in the Intent
    public static final String EXTRA_MESSAGE = "message";
    
    @Override
    protected void onCreate(...) {
        ...
        
        // Get the Intent
        Intent intent = getIntent();
        
        // Get the value from Intent
        String messageText = intent.getStringExtra(EXTRA_MESSAGE);
        
        // Set to a TextView
        TextView textView = (TextView) findViewById (R.id.text);
        textView.setText(messageText);
    }
    
}

EXTRA_MESSAGE 用于 Intent 传递数据时的键(key),通常定义在接收 Intent 的 Activity 中。

 

如何使用 Intent 传递 Action?

Create an intent that specifies an action. (Call other APP)

import android.content.Intent
    ...
public void onSendMessage(View view) {
    // Get a reference to the EditView and Get the value from EditView
    EditView messageView = (EditView) findViewById(R.id.message);
    String message = messageView.getText().toString();
    
    // Create an Intent with Action
    Intent intent = new Intent(Intent.ACTION_SEND);
    
    // Set Extra infomation format and message
    intent.setType("text/plain");
    intent.putExtra(Intent.EXTRA_TEXT, message);
    
    // Use a chooser and set title
    String chooserText = getString (R.string.chooser);
    Intent chooserIntent = Intent.createChooser(intent, chooserText);
    
    // Start Intent
    startActivity(intent);
}

image-20241104171251776

image-20241104171259545

image-20241104171303252

image-20241104171307798


Lifecycle

image-20241104172424451

在 Android 的生命周期中,onSaveInstanceState(),可以保存任何需要保留的值,以免丢失。

Save the current state

@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
    super.onSaveInstanceState(savedInstanceState);
    
    // Save values
    savedInstanceState.putInt("sec", 1);
    savedInstanceState.putString("message", "test");
    savedInstanceState.putBoolean("running", true);
}

使用 savedInstanceState.put{type}(“var”, value);"var"中存储值。

Get stored state

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout...);
    
    // Get values from savedInstanceState
    if (savedInstanceState != null) {
        int sec = savedInstanceState.getInt("sec");
        String message = savedInstanceState.getString("message");
        Boolean running = savedInstanceState.getBoolean("running");
    }
}

下面是完整的项目生命周期回调函数的顺序:

image-20241104173435712

  1. 当activity不再显示在前台时,即不在被聚焦时,调用onPause()
  2. 当activity重新恢复到前台时,即重新被聚焦时,调用onResume()
  3. 当activity在用户页面不可视,即被切换或关闭时,调用onStop().
  4. 当activity重新在用户页面可视时,即重新被切换到时,调用onRestart().
  • OnCreate: Called when an activity is created for the first time;
  • onStart: Called after the onCreate method, or when the Activity transitions from stopped state to Actived state;
  • OnStop: Called when the activity changes from the Active state to the Stopped state;
  • OnPause: Called when the activity changes from Active state to Paused state;

Layout

在 Android 布局中,Margin(外边距)和 Padding(内边距)是两个非常重要的概念,它们都用于控制 View 周围的空白空间,但作用的位置和影响的对象不同。

Margin 是指一个 View 自身边界之外空白区域,用于控制 View 与其相邻的 View 或父容器之间的距离。Margin 影响的是 当前 View 本身 在其父容器中的位置,以及与其他兄弟 View 之间的间隔。

Margin is to add extra space to the top/bottom/left/right of the its own view. 仅能对自己当前view进行距离添加调整。

image-20241104174546761

Padding 是指一个 View 自身边界之内 的空白区域,用于控制 View 的 内容 与其 自身边缘 之间的距离。Padding 影响的是 View 的内容 在其自身内部的布局,使其内容不会紧贴 View 的边缘。

Padding specifies the distance between its contents and the boundary of the parent view. 注重于与父视图之间的距离。

image-20241104175113116

特性 Margin (外边距) Padding (内边距)
位置 View 边界之外 View 边界之内
控制对象 View 与相邻 View 或父容器的距离 View 的内容与其自身边缘的距离
影响 View 的外部位置和与其他 View 的间隔 View 内部内容的布局和显示效果
属性前缀 layout_margin... padding...

 

android:gravity 属性作用于 View 自身,用于控制 View 内部内容 在 View 自身边界内的对齐方式。

Gravity controls the alignment (对齐方式) of the content inside the view. 仅视图内部。

image-20241104175308205

image-20241104175326226

  • top:内容靠顶部对齐。
  • bottom:内容靠底部对齐。
  • left/start:内容靠左侧对齐。
  • right/end:内容靠右侧对齐。
  • center_vertical:内容在垂直方向居中对齐。
  • center_horizontal:内容在水平方向居中对齐。
  • center:内容在水平和垂直方向都居中对齐。
  • 可以将多个值用 | 组合使用,例如 center|right 表示垂直居中并靠右对齐。

 

与它很相似的是layout_gravityandroid:layout_gravity 属性作用于 View 本身,但它是用来告诉 父容器 如何在父容器内 定位这个 View。它决定了 View 自身在其父容器所提供的空间内的对齐方式

 

例如:

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:gravity="center"  <!-- 内容居中于整个页面 -->
    android:text="Hello World!" />

<Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="right"  <!-- 整个按钮右对齐 -->
    android:text="Click Me" />
特性 android:gravity android:layout_gravity
作用对象 View 自身 View 自身 (但作用于其父容器)
控制目标 View 内部内容 在自身边界内的对齐方式 View 自身 在其父容器提供的空间内的对齐方式
影响范围 View 内部 View 在父容器中的位置
常用场景 控制 TextView 等文本的对齐,ImageView 图片的对齐 LinearLayout 等布局中控制子 View 的位置

 


Listener

当我们需要响应用户点击事件时,需要使用一个监听器(Listener) - OnClickListener(),用于处理复杂的用户点击和响应场景。

例如在ListView中需要响应用户对列表项的点击:

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ListView listView = findViewById(R.id.list_view);
        
        // Define Listener
        AdapterView.OnItemClickListener itemOnclickListener = new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> listView, View view, int position, long id) {
                // Handle event
                if (position == 0) {
                    Intent intent = new Intent(this, DrinkCategoryActivity.class);
                    startActivity(intent;
                }
            }
        }
        
        // Set OnItemClickListener
        listView.setOnItemClickListener(itemOnclickListener);
    }
}

onItemClick()的参数如下:

  • listView: AdapterView<?> listView 是触发事件的父视图,即ListView`。
  • view: View view 是被点击的具体项的视图。
  • position: int position 是被点击项在适配器中的位置。
  • id: long id 是被点击项的行ID。

image-20241104210646680

例如这是一个应用了Listener的例子。

  1. TopLevelActivity的ListView上设置了一个onItemClickListener
  2. 当列表中某一项被点击时,触发Listener的处理机制,将打开新的activity。

 


Adapter

Adapter (适配器) 是用于连接后端数据和前端显示的适配器接口,是数据data和UI(View)之间一个重要的纽带。View需要通过Adapter加载数据,Adapter就像是转接头一下,它们的关系如下:

image-20241104212032949

Adapter通过将数据项转换为视图(View)来工作。每当需要显示一个新的数据项时,Adapter会创建或重用一个视图,并将相应的数据绑定到该视图上。其中最常用的Adapter是ArrayAdapter,用于将数组或列表中的数据绑定到视图(一般用于ListView)。

// Define an Adapter
ArrayAdapter<String> adapter = new ArrayAdapter<>(
    this, 
    android.R.layout.simple_list_item_1, 
    data
);

// Set the Adapter for ListView
ListView listView = findViewById(R.id.my_list_view);
listView.setAdapter(adapter);
  • ArrayAdapter<{data_type}> adapter: 需要转换数据项的类型。
  • this: 表示当前的activity。
  • android.R.simple_list_item_1: 单行显示的内置样式。
  • data: 一个数组,表述数据。

我们有这样的例子:

image-20241104213114750

image-20241104213118583

  1. DrinkCategoryActivity的layout有一个ListView;
  2. 这个ListView在DrinkCategoryActivity中定义并设置了一个Adapter,是自己定义的Drink类;
  3. 这个Adapter配合Listener,在点击列表的时候从Drink类中获取信息。

我们还有这样的例子:

image-20241104213546214

很容易得到如下答案:

String[] colors = new String[] {"Red", "Blue", "Green"};
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    Spinner spinner = (Spinner) findViewById(R.id.spinner);
    ArrayAdapter<String> adapter = new ArrayAdapter<> (
    	this,
        android.R.layout.simple_spinner_item,
        colors
    );
    spinner.setAdapter(adapter);
}

Fragment

Fragment 可以被视为一个 Activity 的子部分,它具有自己的生命周期和事件处理机制。Fragment 使得在同一 Activity 中组合多个 UI 组件成为可能,从而实现模块化设计。

我们有如下Fragment的例子:

  1. MainActivity 由 WorkoutListFragment 组成,并可以启动DetailActivity
  2. DetailActivity 由 WorkoutDetailFragment 组成,两个Fragment共同使用一个Java数据类

image-20241105032844559

新的Fragment类继承自 Fragment 类,并重写 onCreateView() 方法以加载布局。

public class ExampleFragment extends Fragment {
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_example, container, false);
    }
}

如果是ListFragment的话,则需要继承ListFrament类,它必须包含ListView,同时通过一些ArrayAdapter处理点击响应:

public class MyListFragment extends ListFragment {
    private String[] cities = {"Shenzhen", "Beijing", "Shanghai"};

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        // 使用 ArrayAdapter 绑定数据
        setListAdapter(new ArrayAdapter<>(getActivity(), android.R.layout.simple_list_item_1, cities));
    }

    @Override
    public void onListItemClick(ListView l, View v, int position, long id) {
        // 处理点击事件
        String selectedCity = cities[position];
        Toast.makeText(getActivity(), "You selected: " + selectedCity, Toast.LENGTH_SHORT).show();
    }
}

我们创建好Fragment后,还需要将Fragment在需要使用的Activiy中载入,通常可以通过 FragmentManager 来实现:

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        if (savedInstanceState == null) {
            MyListFragment fragment = new MyListFragment();
            FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
            ft.replace(R.id.fragment_container, fragment);
            ft.addToBackStack(null);
            ft.commit();
        }
    }
}

这里定义的fragment是MyListFragment类的,需要确保其实fragment定义的主类。加载的过程需要使用transaction用于reference to the activity’s fragment manager.

  • add(int containerViewId, Fragment fragment): 将一个新的 Fragment 添加到指定的容器中。
  • replace(int containerViewId, Fragment fragment): 替换指定容器中的现有 Fragment。
  • remove(Fragment fragment): 从 UI 中移除指定的 Fragment。
  • addToBackStack(String name): 将当前事务添加到回退栈,以便用户可以通过后退按钮返回到之前的状态。可以提供一个名称以便于后续管理。
  • commit(): 提交事务,开始执行所有操作。

此时还需要在activity对应的layout中,确保在 Activity 的布局文件中有一个容器来放置fragment

<!-- activity_main.xml -->
<FrameLayout
    android:id="@+id/fragment_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

此外,当一个 Fragment 作为另一个 Fragment 的子 Fragment 时,使用 ChildFragmentManager 。例如在父Fragment中:

@Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        
        // 使用 ChildFragmentManager 添加子 Fragment
        if (savedInstanceState == null) {
            ChildFragment childFragment = new ChildFragment();
            getChildFragmentManager().beginTransaction()
                .replace(R.id.child_fragment_container, childFragment)
                .commit();
        }
    }

此时是在Fragment中嵌套了一个Fragment,使用ChildFragmentManager

 


AppBars

AppBar是位于 Android 应用界面顶部的工具栏区域。下面是最常见的两种AppBar。

  • ActionBar 是早期版本的应用栏实现,通常由系统自动提供。
  • Toolbar 是更现代、更灵活的应用栏实现,你需要手动添加到布局中并设置为 SupportActionBar

我们可以通过修改 AndroidManifest.xml 去开启或关闭AppBar:

<activity android:name=".YourActivity"
    android:theme="@style/Theme.AppCompat.Light">
    </activity>

如果需要使用Toolbar, 为了避免冲突,你需要确保你的应用主题禁用了默认的 ActionBar

<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
    <!-- Customize your theme here. -->
</style>

下面是Toolbar的工作流程:

  1. 在布局文件中添加 Toolbar 在你的 ActivityFragment 的布局 XML 文件中添加 <androidx.appcompat.widget.Toolbar> 元素。此外,我们也可以通过include引入自定义的样式。
    <androidx.appcompat.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="?attr/colorPrimary"
        android:elevation="4dp"
        android:theme="@style/ThemeOverlay.AppCompat.ActionBar"
        app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>
    
    
  2. Activity 中将其设置为 SupportActionBar 在你的 ActivityonCreate() 方法中,找到 Toolbar 的实例,并使用 setSupportActionBar() 方法将其设置为 Activity 的支持操作栏。
    public class MyActivity extends AppCompatActivity {
        private Toolbar toolbar;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_my);
    
            toolbar = findViewById(R.id.toolbar);
            setSupportActionBar(toolbar);
        }
    }
    
  3. 如果我们需要添加一些图标等ACTION,我们可以在 res/menu 目录下创建一个 XML 文件(例如 main_menu.xml),定义你的菜单项。
    <menu xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto">
        <item
            android:id="@+id/action_settings"
            android:orderInCategory="100"
            android:title="设置"
            app:showAsAction="never" />
        <item
            android:id="@+id/action_search"
            android:icon="@drawable/ic_search"
            android:orderInCategory="50"
            android:title="搜索"
            app:showAsAction="ifRoom" />
    </menu>
    
  4. 接下来就是加载你的菜单资源文件,并为自定义的按钮添加点击事件。
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.main_menu, menu);
        return true;
    }
    
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case R.id.action_settings:
                // 处理设置按钮的点击事件
                Toast.makeText(this, "点击了设置", Toast.LENGTH_SHORT).show();
                return true;
            case R.id.action_search:
                // 处理搜索按钮的点击事件
                Toast.makeText(this, "点击了搜索", Toast.LENGTH_SHORT).show();
                return true;
            default:
                return super.onOptionsItemSelected(item);
        }
    }
    
    

通过这样的设计,我们可以得到一个带有ACTION的Toolbar。

 

此外,我们还可以使用Fragment来添加标签(Tabs),以便它们可以随意折叠或滚动。

这是一个简单的 Fragment 示例,用于显示每个标签页的内容。

import ...

public class TabFragment extends Fragment {

    private String content;

    public TabFragment(String content) {
        this.content = content;
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(android.R.layout.simple_textview, container, false);
        TextView textView = view.findViewById(android.R.id.text1);
        textView.setText(content);
        return view;
    }
}

我们需要在activity_main.xml中设计:

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.google.android.material.appbar.AppBarLayout
        android:id="@+id/appBarLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">

        <com.google.android.material.appbar.MaterialToolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            app:layout_scrollFlags="scroll|enterAlways|snap"
            app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
            android:text="我的应用" />

        <com.google.android.material.tabs.TabLayout
            android:id="@+id/tabLayout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:tabMode="fixed"
            app:tabGravity="fill" />

    </com.google.android.material.appbar.AppBarLayout>

    <androidx.viewpager.widget.ViewPager
        android:id="@+id/viewPager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>

  • app:layout_scrollFlags="scroll|enterAlways": 定义了 Toolbar 在滚动时的行为 scroll。 scroll表示这个 Toolbar 应该跟随滚动事件一起滚动出屏幕。enterAlways表示当内容向上滚动时,这个 Toolbar 会立即显示出来,即使内容还没有完全滚动到顶部。
  • app:layout_behavior="@string/appbar_scrolling_view_behavior": 告诉 ViewPager 它的滚动应该与 AppBarLayout 协调。当 ViewPager 的内容向上滚动时,会触发 AppBarLayout 的折叠或滚动。

 

MainActivity.java中,

private Toolbar toolbar;
private TabLayout tabLayout;
private ViewPager viewPager;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    toolbar = findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);

    tabLayout = findViewById(R.id.tabLayout);
    viewPager = findViewById(R.id.viewPager);

    // 创建 Fragment 列表
    List<Fragment> fragmentList = new ArrayList<>();
    fragmentList.add(new TabFragment("标签页 1 的内容"));
    fragmentList.add(new TabFragment("标签页 2 的内容"));
    fragmentList.add(new TabFragment("标签页 3 的内容"));

    // 创建 FragmentPagerAdapter 的适配器
    PagerAdapter adapter = new PagerAdapter(getSupportFragmentManager(), fragmentList);
    viewPager.setAdapter(adapter);

    // 将 TabLayout 和 ViewPager 关联起来
    tabLayout.setupWithViewPager(viewPager);
}

// FragmentPagerAdapter 的适配器
private static class PagerAdapter extends FragmentPagerAdapter {
    private final List<Fragment> fragmentList;

    public PagerAdapter(FragmentManager fm, List<Fragment> fragmentList) {
        super(fm);
        this.fragmentList = fragmentList;
    }

    @NonNull
    @Override
    public Fragment getItem(int position) {
        return fragmentList.get(position);
    }

    @Override
    public int getCount() {
        return fragmentList.size();
    }

    @Nullable
    @Override
    public CharSequence getPageTitle(int position) {
        switch (position) {
            case 0:
                return "标签一";
            case 1:
                return "标签二";
            case 2:
                return "标签三";
            default:
                return null;
        }
    }
}

我们创建了一个继承自 FragmentPagerAdapter 的内部类 PagerAdapter

  • FragmentPagerAdapter 的构造函数需要 FragmentManager。 我们通常传递 getSupportFragmentManager()
  • getItem(int position): 返回指定位置的 Fragment 实例。
  • getCount(): 返回标签页的总数。
  • getPageTitle(int position): 对于 FragmentPagerAdapter,你需要重写 getPageTitle() 方法来提供每个标签页的标题。

image-20241227124123941

 


Snackbar

Snackbar 是 Android 中一种用于向用户提供操作反馈的控件,它会短暂地显示在屏幕底部,并且可以包含一个可选的操作按钮。与 Toast 类似,Snackbar 不会阻塞用户界面。 Snackbar 的一个主要优点是可以提供一个操作项,允许用户撤销或执行与先前操作相关的操作。

image-20241227124950431

假设我们有一个场景,用户刚刚删除了一条数据。 我们希望显示一个 Snackbar,提示用户数据已删除,并提供一个 "UNDO" 按钮,如果用户点击该按钮,则显示一个 Toast 来模拟撤销操作。

创建一个Snackbar,并将其显示:

int duration = Snackbar.LENGTH_SHORT
Snackbar snackbar = Snackbar.make(findViewById(R.id.coordinator), "数据已删除", duration);
snackbar.setAction("UNDO", new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        Toast.makeText(this, "UNDO", Toast.LENGTH_SHORT).show();
    }
});
snackbar.show();

image-20241227130102383

创建 Snackbar: 使用 Snackbar.make(View view, CharSequence text, int duration) 方法创建一个 Snackbar 实例。

  • View view: Snackbar 需要一个 View 来找到其父布局,并用于定位 Snackbar。 通常,你可以传递触发 Snackbar 显示的 View (例如这里的 findViewById(R.id.coordinator)),或者任何当前 Activity 布局中的 View
  • CharSequence text: Snackbar 上要显示的消息文本,这里是 "数据已删除"。
  • int duration: Snackbar 的显示时长。 常用的值有:
    • Snackbar.LENGTH_SHORT: 短暂显示。
    • Snackbar.LENGTH_LONG: 较长时间显示。
    • Snackbar.LENGTH_INDEFINITE: 无限期显示,直到用户执行操作或手动关闭。 注意:如果使用 LENGTH_INDEFINITE,通常需要设置一个 Action,否则用户无法关闭 Snackbar
  • 设置 Action (UNDO 按钮):使用 snackbar.setAction(CharSequence text, View.OnClickListener listener) 方法添加一个操作按钮。
    • CharSequence text: 操作按钮上显示的文本,这里是 "UNDO"。
    • View.OnClickListener listener: 一个 OnClickListener 接口的实现,定义了当操作按钮被点击时要执行的操作。 在这个例子中,我们创建了一个匿名内部类来实现 OnClickListener,并在其 onClick() 方法中调用了 undoDelete() 方法。
  • 在这个简单的例子中,我们只是弹出一个 Toast 显示 "UNDO"。 在实际应用中,这里会执行撤销删除的逻辑,例如将删除的数据恢复。
  • snackbar.show(): 最后调用 show() 方法来显示 Snackbar

 


RecyclerView

RecyclerView 是一个更加强大和灵活的滚动控件,专门用于高效地展示大型数据集。可以说是一个增强版的ListView。

它的主要功能是:视图回收 (View Recycling): RecyclerView 不会为每一个列表项都创建新的视图。当列表项滚动出屏幕时,RecyclerView 会回收这些视图,并将它们重新用于即将进入屏幕的新列表项。这样就避免了频繁创建和销毁视图带来的性能损耗。

RecyclerView 的工作流程可以大致分为以下几个步骤,我们通过Pizza的卡片列表展示为例。

我们的第一步就是:创建Pizza数据类。

//Pizza.java

public class Pizza {
    private String name;
    private String imageUrl;

    public Pizza(String name, String imageUrl) {
        this.name = name;
        this.imageUrl = imageUrl;
    }

    public String getName() {
        return name;
    }

    public String getImageUrl() {
        return imageUrl;
    }
}

接下来我们尝试创建ViewHolder,用于存放我们的每一个卡片对象。(这里假设了CardView中的id)

// PizzaViewHolder.java

import ...

public class PizzaViewHolder extends RecyclerView.ViewHolder {
    ImageView pizzaImage;
    TextView pizzaName;

    public PizzaViewHolder(@NonNull View itemView) {
        super(itemView);
        pizzaImage = itemView.findViewById(R.id.pizza_image);
        pizzaName = itemView.findViewById(R.id.pizza_name);
    }
}

下一步是创建 Adapter,这个过程从你的数据源(Pizza)中获取数据,并将这些数据转换成 RecyclerView 可以显示的视图。

// PizzaAdapter.java

import ...

public class PizzaAdapter extends RecyclerView.Adapter<PizzaViewHolder> {

    private List<Pizza> pizzaList;
    private Context context;

    public PizzaAdapter(Context context, List<Pizza> pizzaList) {
        this.pizzaList = pizzaList;
        this.context = context;
    }

    @NonNull
    @Override
    public PizzaViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_pizza, parent, false);  // item_pizza是存放CardView的样式的
        return new PizzaViewHolder(itemView);
    }

    @Override
    public void onBindViewHolder(@NonNull PizzaViewHolder holder, int position) {
        Pizza currentPizza = pizzaList.get(position);
        holder.pizzaName.setText(currentPizza.getName());  // 载入到每个ViewHolder的文本中
        Picasso.get().load(currentPizza.getImageUrl()).into(holder.pizzaImage);  // 载入到每个ViewHolder的图片中
    }

    @Override
    public int getItemCount() {
        return pizzaList.size();
    }
}

  • onCreateViewHolder() 方法: 这个方法负责创建 ViewHolderViewHolder 是一个用来保存列表中单个条目视图的引用的对象。你可以把它想象成一个“盒子”,ViewHolder是一个列表中的项的最小单位,用来存放一个 Pizza 的图片和文字描述的 ImageViewTextViewRecyclerView 需要显示一个新的条目时,它会调用这个方法来创建一个新的 ViewHolder
  • onBindViewHolder() 方法: 这个方法负责将数据绑定到 ViewHolder 的视图上。 当需要显示特定位置的 Pizza 信息时,RecyclerView 会调用这个方法,并传入对应的 ViewHolder 和数据的位置。你需要在 onBindViewHolder() 中从你的数据源中取出对应位置的 Pizza 对象,然后将其图片和文字信息设置到 ViewHolderImageViewTextView 上。

 

最后是在PizzaFragment 中初始化我们的RecyclerView 并设置 GridLayoutManager

// PizzaFragment.java

import ...

public class PizzaFragment extends Fragment {

    private RecyclerView recyclerView;
    private PizzaAdapter adapter;
    private List<Pizza> pizzaList;

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_pizza, container, false);
        recyclerView = view.findViewById(R.id.pizza_recycler_view);

        // 初始化数据
        pizzaList = new ArrayList<>();
        pizzaList.add(new Pizza("Pepperoni Pizza", "https://example.com/pepperoni.jpg"));
        pizzaList.add(new Pizza("Margherita Pizza", "https://example.com/margherita.jpg"));
        pizzaList.add(new Pizza("Vegetarian Pizza", "https://example.com/vegetarian.jpg"));
        // 添加更多 Pizza ...

        // 创建 GridLayoutManager,设置列数为 2
        GridLayoutManager layoutManager = new GridLayoutManager(getContext(), 2);
        recyclerView.setLayoutManager(layoutManager);

        // 创建 Adapter 实例
        adapter = new PizzaAdapter(getContext(), pizzaList);
        recyclerView.setAdapter(adapter);

        return view;
    }
}

总结一下我们的过程:

  1. Fragment 的创建和布局加载:
    • PizzaFragment 被创建时,onCreateView 方法会被调用。
    • LayoutInflater 会加载 fragment_pizza.xml 布局文件,这个布局文件中包含了 RecyclerView
  2. RecyclerView 的初始化和 LayoutManager 的设置:
    • onCreateView 中,我们找到 RecyclerView 的实例。
    • 我们创建了一个 GridLayoutManager 的实例,并指定了网格的列数为 2。GridLayoutManager 负责以网格的形式排列 RecyclerView 的列表项。
    • 通过 recyclerView.setLayoutManager(layoutManager),我们将 GridLayoutManager 设置给 RecyclerView,告诉它使用网格布局。
  3. Adapter 的创建和设置:
    • 我们创建了 PizzaAdapter 的实例,并将 Pizza 数据列表传递给它。
    • 通过 recyclerView.setAdapter(adapter),我们将 Adapter 设置给 RecyclerViewRecyclerView 会使用这个 Adapter 来获取需要展示的视图。
  4. ViewHolder 的创建 (onCreateViewHolder):
    • RecyclerView 需要显示新的列表项时(最初显示在屏幕上或滚动进入屏幕时),它会调用 AdapteronCreateViewHolder 方法。
    • onCreateViewHolder 中,我们使用 LayoutInflater 加载 item_pizza.xml 布局文件。由于 item_pizza.xml 的根布局是 CardView,所以这里创建的 ViewHolder 持有的 itemView 就是一个 CardView
  5. 数据的绑定 (onBindViewHolder):
    • 对于每一个需要显示的列表项,RecyclerView 会调用 AdapteronBindViewHolder 方法,并传入对应的 ViewHolder 实例和数据的位置。
    • onBindViewHolder 中,我们从 pizzaList 中取出对应位置的 Pizza 对象,然后将其 nameimageUrl 设置到 ViewHolderCardView 内部的 TextViewImageView 上。
  6. 显示:
    • RecyclerView 使用 GridLayoutManager 来确定每个列表项(即 CardView)在屏幕上的位置和大小,从而实现两列的网格布局。
    • Adapter 负责提供每个位置上需要显示的 CardView,并将数据填充到 CardView 内部的视图中。

 

image-20241226165915477

image-20241226165919385

image-20241226165923719

image-20241226165927792

 

下面是另一个例子,为PastaFragment应用RecyclerView。

image-20241226170139388

根据之前学到的RecyclerView的工作过程,我们可以得到如下的过程:

  1. 创建RecyclerView,它的格式如下:
    View view = inflater.inflate(R.layout.fragment_pasta, container, false);
    recyclerView = view.findViewById(R.id.pizza_recycler_view);
    

    LayoutInflater 是一个builder,负责根据 XML 蓝图创建实际的 UI 组件。

    inflate() 方法就像是“建造”的过程。

    你需要告诉它:

    • 蓝图是什么? (R.layout.xxx)
    • 将来要放在哪里? (parentcontainer)
    • 现在就放进去吗? (attachToRoot 参数)

    当你通过 inflate() 获得了 View 对象后,你就可以使用 findViewById() 方法在这个 View 对象中查找特定的子视图。例如,在 Fragment 中,view.findViewById(R.id.pizza_recycler_view) 会在 fragment_pizza.xml 布局中查找 idpizza_recycler_viewRecyclerView

    那看到题目这里只有一句,推测Layout文件中根元素就是<androidx.recyclerview.widget.RecyclerView>

    最后得到的是RecyclerView,那我们这里就是填入PasteFragment的蓝图,所以这里是:

    RecyclerView pasteRecycler = (RecyclerView) inflater.inflate(R.layout.fragment_paste, container, false);
    
  2. 接下来的两个循环用于取出数据,然后就是创建Adapter示例,并将 Adapter 设置给 RecyclerView,于是我们可以回答:
    CapionedImageAdapter adapter = new CapionedImageAdapter(pastaNames, pastaImages);
    pasteRecycler.setAdapter(adapter);
    

    这里的CapionedImageAdapter是题目定义的Adapter,而它的构造函数中,设计了对数组数据的处理。

  3. 我们观察到后续构造函数存在形如网格布局的配置,我们可以使用网格布局去管理 RecyclerView ,并将其应用到 RecyclerView 上。
    GridLayoutManager layoutManager = new GridLayoutManager(getActivity(), 2);
    pasteRecycler.setLayoutManager(layoutManager);
    

Database & Cursor

Android使用SQLite数据库来持久化数据。(Android uses SQLite databases to persist data)

原因有三:

  1. It's lightweight: SOLite数据库只是一个文件,不会占用任何处理器时间。
  2. It's optimized for a single user: 该数据只为我们的应用设计,只有我们的应用程序才可以与其沟通。
  3. It's stable and fast: 它可以处理数据库事务,并且使用了C编码,速度快且占用低。

 

在 Android 中,SQLite 是一个轻量级的、嵌入式的关系型数据库。这意味着你的应用程序可以直接在设备上存储和管理结构化的数据,而无需单独的数据库服务器。

我们一般使用 SQLiteOpenHelper 类来辅助创建和管理数据库。 SQLiteOpenHelper 提供了一种标准的方式来处理数据库的创建和版本升级。

我们需要创建一个继承自 SQLiteOpenHelper 的子类,并重写以下两个重要的方法:

  • onCreate(SQLiteDatabase db): 当数据库第一次被创建时调用。你可以在这个方法中执行创建表结构的 SQL 语句。
  • onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion): 当数据库版本升级时调用。你可以在这个方法中执行修改表结构、迁移数据等的 SQL 语句。

以如下作为例子我们来了解Android中如何使用数据库。

假设我们要创建一个名为 "mydatabase.db" 的数据库,并在其中创建一个名为 "users" 的表,包含 "id" (整型,主键,自增长) 和 "name" (文本类型) 两个字段。

import ...

public class DatabaseHelper extends SQLiteOpenHelper {

    private static final String DATABASE_NAME = "mydatabase.db";
    private static final int DATABASE_VERSION = 1;

    public static final String TABLE_USERS = "users";
    public static final String COLUMN_ID = "id";
    public static final String COLUMN_NAME = "name";

    private static final String CREATE_TABLE_USERS = "CREATE TABLE IF NOT EXISTS" + TABLE_USERS + "("
            + COLUMN_ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
            + COLUMN_NAME + " TEXT"
            + ")";

    public DatabaseHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(CREATE_TABLE_USERS);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        db.execSQL("DROP TABLE IF EXISTS " + TABLE_USERS);
        onCreate(db);
    }
}

在创建数据库的时候,使用SQL创建了数据表,而在更新版本的时候将表清除。

 

在数据库中,使用 SQLiteDatabaseupdate() 方法更新已有的数据行。

ContentValues updateValues = new ContentValues();
updateValues.put(DatabaseHelper.COLUMN_NAME, "Charlie");

String whereClause = DatabaseHelper.COLUMN_ID + " = ?";
String[] whereArgs = {"1"}; // 更新 ID 为 1 的行

db.update(DatabaseHelper.TABLE_USERS, updateValues, whereClause, whereArgs);

要操作数据库,需要获取 SQLiteDatabase 的实例。 可以通过 DatabaseHelpergetWritableDatabase() (用于写入数据) 或 getReadableDatabase() (用于读取数据) 方法来获取。

DatabaseHelper dbHelper = new DatabaseHelper(this);

SQLiteDatabase db = dbHelper.getWritableDatabase(); // 获取可写数据库实例
SQLiteDatabase readableDb = dbHelper.getReadableDatabase(); // 获取只读数据库实例

要从数据库中查询数据,可以使用 SQLiteDatabasequery() 方法。 query() 方法提供了多种参数来指定查询条件。

Cursor cursor = db.query("mydatabase.db",
                         new String[]{"id", "name"},  // 要查询的列
                         selection,  // WHERE 子句
                         selectionArgs,  // WHERE 子句的参数
                         groupBy,  // GROUP BY 子句
                         having,  // HAVING 子句
                         orderBy);  // ORDER BY 子句

Cursor 是一个接口,它提供了访问数据库查询结果集的能力。你可以将 Cursor 想象成一个指向查询结果集的指针,你可以移动这个指针来逐行访问数据

你需要使用 Cursor 的方法来移动到每一行并获取数据。

if (cursor != null && cursor.moveToFirst()) {
    do {
        int id = cursor.getInt(cursor.getColumnIndex(DatabaseHelper.COLUMN_ID));
        String name = cursor.getString(cursor.getColumnIndex(DatabaseHelper.COLUMN_NAME));
        // 处理获取到的数据,例如打印到日志
        Log.d("Database", "ID: " + id + ", Name: " + name);
    } while (cursor.moveToNext()); // 移动到下一行
}

// 始终记得关闭 Cursor 和数据库
if (cursor != null) {
    cursor.close();
}
db.close();
  • cursor.moveToFirst():Cursor 移动到结果集的第一行。如果结果集为空,则返回 false
  • cursor.moveToNext():Cursor 移动到结果集的下一行。如果已经到达末尾,则返回 false
  • cursor.getColumnIndex(String columnName): 获取指定列名在结果集中的索引位置。
  • cursor.getInt(int columnIndex): 获取指定索引位置的整型数据。
  • cursor.getString(int columnIndex): 获取指定索引位置的字符串数据。
  • cursor.close(): 释放 Cursor 占用的资源.

 

假设我们已经有了一个从数据库查询用户数据的 Cursor,我们想要将其直接显示在一个 ListView 中。此时我们就需要使用一个 CursorAdapterCursor 直接绑定到 ListViewSpinner 等 AdapterView 上。

import ...

public class UserListActivity extends AppCompatActivity {

    private DatabaseHelper dbHelper;
    private Cursor userCursor;
    private SimpleCursorAdapter userAdapter;
    private ListView userListView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_user_list);

        userListView = findViewById(R.id.user_list_view);
        dbHelper = new DatabaseHelper(this);
    }

    @Override
    protected void onResume() {
        super.onResume();
        populateListView();
    }

    private void populateListView() {
        SQLiteDatabase db = dbHelper.getReadableDatabase();
        String[] projection = {
                DatabaseHelper.COLUMN_ID,
                DatabaseHelper.COLUMN_NAME
        };
        userCursor = db.query(
                DatabaseHelper.TABLE_USERS,
                projection,
                null,
                null,
                null,
                null,
                null
        );

        // 定义 Cursor 中要读取的列和 ListView 中要显示这些列的 TextView 的 ID
        String[] fromColumns = {DatabaseHelper.COLUMN_NAME};
        int[] toViews = {android.R.id.text1}; // 使用 Android 提供的简单 TextView

        // 创建 SimpleCursorAdapter
        userAdapter = new SimpleCursorAdapter(
                this,
                android.R.layout.simple_list_item_1, // 使用 Android 提供的简单列表项布局
                userCursor,
                fromColumns,
                toViews,
                0 // flag (通常为 0)
        );

        // 将 Adapter 设置到 ListView
        userListView.setAdapter(userAdapter);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (userCursor != null) {
            userCursor.close();
        }
        if (dbHelper != null) {
            dbHelper.close();
        }
    }
}

  1. 我们首先从数据库中查询了用户数据,得到了一个 Cursor 对象 userCursor
  2. 然后,我们创建了一个 SimpleCursorAdapterSimpleCursorAdapterCursorAdapter 的一个简单实现,适用于将 Cursor 中的数据直接映射到 TextView 上。
  3. SimpleCursorAdapter 的构造函数需要以下参数:
    • context: 上下文对象。
    • layout: 用于显示每一行的布局文件的 ID。 android.R.layout.simple_list_item_1 是 Android 提供的包含一个 TextView 的简单布局。
    • cursor: 要绑定的 Cursor 对象。
    • fromColumns: 一个字符串数组,包含 Cursor 中要读取的列名。
    • toViews: 一个整型数组,包含布局文件中用于显示这些列数据的 TextView 的 ID。 android.R.id.text1simple_list_item_1 布局中 TextView 的 ID。
    • flags: 指定适配器行为的标志,通常为 0。
  4. 最后,我们将创建的 userAdapter 设置到 ListView 上。

当然如果我们先将数据库查询结果转换为一个 List<String> (假设我们只显示用户名),那么我们就不能使用 CursorAdapter 了。

import ...

public class UserListActivity extends AppCompatActivity {

    private DatabaseHelper dbHelper;
    private ListView userListView;
    private ArrayAdapter<String> userAdapter;
    private List<String> userNames;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_user_list);

        userListView = findViewById(R.id.user_list_view);
        dbHelper = new DatabaseHelper(this);
    }

    @Override
    protected void onResume() {
        super.onResume();
        populateListView();
    }

    private void populateListView() {
        SQLiteDatabase db = dbHelper.getReadableDatabase();
        Cursor cursor = db.query(
                DatabaseHelper.TABLE_USERS,
                new String[]{DatabaseHelper.COLUMN_NAME}, // 只查询用户名
                null,
                null,
                null,
                null,
                null
        );

        userNames = new ArrayList<>();
        if (cursor != null && cursor.moveToFirst()) {
            do {
                String name = cursor.getString(cursor.getColumnIndex(DatabaseHelper.COLUMN_NAME));
                userNames.add(name);
            } while (cursor.moveToNext());
            cursor.close();
        }

        // 创建 ArrayAdapter
        userAdapter = new ArrayAdapter<>(
                this,
                android.R.layout.simple_list_item_1,
                userNames
        );

        // 将 Adapter 设置到 ListView
        userListView.setAdapter(userAdapter);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (dbHelper != null) {
            dbHelper.close();
        }
    }
}

总的来说:

  • 当你直接从数据库查询结果得到 Cursor 时,并且想要直接将 Cursor 的数据展示在 AdapterView 上,CursorAdapter (或其子类如 SimpleCursorAdapter) 是一个非常方便且高效的选择。 CursorAdapter 可以高效地处理 Cursor 的数据,并且当 Cursor 的数据发生变化时,它可以自动更新 AdapterView 的显示。
  • 当你需要对从数据库查询到的数据进行额外的处理,或者你的数据来源不仅仅是数据库时,将数据转换为 List,然后使用 ArrayAdapterBaseAdapterRecyclerView.Adapter 会更加灵活。 这种方式下,你需要手动管理数据的更新。

 

在我们实际应用中,经常遇到我们更新了数据库后,之前显示的数据需要重新获取的问题。

Cursor 可以被看作是一个指向数据库查询结果集的指针。 当你执行数据库查询时,Cursor保存查询结果的一个快照。 即使数据库中的数据发生了改变,原有的 Cursor 对象仍然指向的是旧的数据集。 因此,你需要执行一个新的查询来获取最新的数据,并用新的 Cursor 来更新适配器。

以下是替换 Cursor 的一般步骤:

  1. 执行数据库更新操作: 首先,你需要执行插入、更新或删除等数据库操作,修改数据库中的数据。
  2. 创建并获取新的 Cursor 在数据库更新操作完成后,你需要执行一个新的查询来获取最新的数据。 这个查询应该与之前用于生成旧 Cursor 的查询条件相同,以获取更新后的结果集。
// 假设 dbHelper 是你的 DatabaseHelper 实例
SQLiteDatabase db = dbHelper.getReadableDatabase();
Cursor newCursor = db.query(
        DatabaseHelper.TABLE_USERS, // 表名
        null,                       // 返回所有列
        null,                       // WHERE 子句,这里假设没有条件
        null,                       // WHERE 子句的参数
        null,                       // GROUP BY 子句
        null,                       // HAVING 子句
        null                        // ORDER BY 子句
);

  1. 更新 CursorAdapter 对于 CursorAdapter (包括 SimpleCursorAdapter),你可以使用 swapCursor() 方法来替换底层的 CursorswapCursor() 方法会关闭旧的 Cursor (如果存在),并通知观察者数据已经改变,从而触发 ListViewRecyclerView 的更新。我们还可以使用changeCursor()方法,但是需要自己关闭旧的 Cursor
// 假设 userAdapter 是你的 SimpleCursorAdapter 实例
Cursor oldCursor = userAdapter.swapCursor(newCursor);

// 使用changeCursor,并手动关闭旧的 Cursor
userAdapter.changeCursor(newCursor);
if (oldCursor != null && oldCursor != newCursor) {
    oldCursor.close();
}

 


Services

服务是一种在后台执行长时间运行操作而不提供用户界面的应用程序组件。

Android 中的服务主要分为两种类型:

  • 启动服务 (Started Services): 当应用程序组件(例如 Activity)调用 startService() 方法时,服务将被启动。启动后,服务可以在后台无限期地运行,即使启动它的组件已被销毁。通常,启动服务会执行一个单一的操作,并在完成后自行停止。
  • 绑定服务 (Bound Services): 当应用程序组件通过调用 bindService() 方法绑定到服务时,服务是绑定的。绑定服务提供了一个客户端-服务器接口,允许组件与服务进行交互、发送请求和接收结果,甚至是跨进程的。绑定服务只有在至少有一个组件绑定到它时才会运行,当所有客户端都取消绑定后,服务就会被销毁。

一个服务可以同时是启动的和绑定的。

以下是服务生命周期中的主要回调方法:

  1. onCreate(): 这是服务第一次创建时调用的。
  2. onStartCommand(Intent intent, int flags, int startId): 当另一个组件(如 Activity)通过调用 startService() 请求启动服务时,系统会调用此方法。这是启动服务特有的回调。
  3. onBind(Intent intent): 当另一个组件想通过调用 bindService() 与服务绑定时,系统会调用此方法。这是绑定服务特有的回调。这个接口通常是一个 IBinder 对象。
  4. onUnbind(Intent intent): 当所有客户端都与服务取消绑定时调用。返回值布尔值表示服务在有新的连接到达时是否希望调用 onRebind()
  5. onDestroy(): 当服务不再被使用且即将被销毁时,系统会调用此方法。这是服务生命周期的最后一个回调。你应该在这里清理所有资源,例如取消注册监听器、释放网络连接等。

 

简单来说,对于启动服务onCreate() -> onStartCommand() -> onDestroy()

对于绑定服务onCreate() -> onBind() -> onUnbind() -> onDestroy()

 

让我们创建一个使用 GPS 服务的示例,该服务将在后台获取位置更新并在有新的位置信息时通知我们。

首先,在 AndroidManifest.xml 文件中添加访问 GPS 的权限,并声明 Service(IDE自动):

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<service android:name=".GPSService" />

接下来是在创建好的server里面使用GPS位置信息:

import ...

public class GPSService extends Service {

    private static final String TAG = "GPSService";
    private LocationManager locationManager;
    private LocationListener locationListener;

    @Override
    public void onCreate() {
        super.onCreate();
        Log.d(TAG, "onCreate");
        locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
        locationListener = new MyLocationListener();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.d(TAG, "onStartCommand");
        startLocationUpdates();
    }

    private void startLocationUpdates() {
        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
            Log.e(TAG, "Location permission not granted");
            return;
        }
        locationManager.requestLocationUpdates(
                LocationManager.GPS_PROVIDER,
                5000, // 最小时间间隔 (毫秒)
                10,   // 最小距离间隔 (米)
                locationListener
        );
    }

    private void stopLocationUpdates() {
        locationManager.removeUpdates(locationListener);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.d(TAG, "onDestroy");
        stopLocationUpdates();
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null; // 这个服务不提供绑定
    }

    private class MyLocationListener implements LocationListener {
        @Override
        public void onLocationChanged(Location location) {
            Log.d(TAG, "Location changed: " + location.getLatitude() + ", " + location.getLongitude());
            // 在这里处理新的位置信息,例如发送广播或更新 UI
        }

        @Override
        public void onStatusChanged(String provider, int status, Bundle extras) {
            Log.d(TAG, "Status changed: " + provider + " - " + status);
        }

        @Override
        public void onProviderEnabled(String provider) {
            Log.d(TAG, "Provider enabled: " + provider);
        }

        @Override
        public void onProviderDisabled(String provider) {
            Log.d(TAG, "Provider disabled: " + provider);
        }
    }
}

MainActivity.java 中,可以使用 startService()stopService() 方法来启动和停止 GPSService

import ...

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button startButton = findViewById(R.id.startButton);
        Button stopButton = findViewById(R.id.stopButton);

        startButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startGPSService();
            }
        });

        stopButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                stopGPSService();
            }
        });
    }

    private void startGPSService() {
        Intent serviceIntent = new Intent(this, GPSService.class);
        startService(serviceIntent);
    }

    private void stopGPSService() {
        Intent serviceIntent = new Intent(this, GPSService.class);
        stopService(serviceIntent);
    }
}

  1. 系统调用 GPSServiceonCreate() 方法(如果服务尚未创建)。
  2. 系统调用 GPSServiceonStartCommand() 方法。
  3. GPSService 开始请求位置更新。
  4. 当 GPS 位置发生变化时,MyLocationListeneronLocationChanged() 方法会被调用,你可以在这里处理新的位置信息。
  5. MainActivity 中点击 "停止服务" 按钮。
  6. 系统调用 GPSServiceonDestroy() 方法,停止位置更新。

image-20241227132947548

 


AsyncTask

AsyncTask 允许Android在后台执行耗时的操作,同时不会阻塞主线程(UI 线程),并在后台任务完成后更新 UI。

AsyncTask 提供了一种简单的实现异步操作的方式。 它将异步任务的执行过程分解为几个步骤,并在不同的线程上执行这些步骤。 下面是按照顺序执行的几种常见方法:

  1. onPreExecute() (UI 线程执行):
    • 在后台任务开始执行之前被调用,运行在 UI 线程
    • 通常用于进行一些准备工作,例如显示一个进度条。
  2. doInBackground(Params... params) (后台线程执行):
    • onPreExecute() 执行完毕后立即被调用,运行在 后台线程
    • 这是执行耗时操作的地方,例如网络请求、数据库操作、文件读写等。
    • 不允许 在这个方法中直接更新 UI 元素。
    • 可以通过 publishProgress(Progress...) 方法来发布进度更新。
    • 这个方法的返回值将会传递给 onPostExecute() 方法。
  3. onProgressUpdate(Progress... values) (UI 线程执行):
    • 在调用 publishProgress() 后被调用,运行在 UI 线程
    • 用于更新 UI 上的进度显示,例如更新 ProgressBar 的进度。
  4. onPostExecute(Result result) (UI 线程执行):
    • doInBackground() 方法执行完成后被调用,运行在 UI 线程
    • 接收 doInBackground() 方法的返回值作为参数。
    • 通常用于将后台任务的结果更新到 UI 上,例如显示下载完成的图片或数据。

image-20241226212932238

假设我们想要模拟从网络下载一张图片并在 ImageView 中显示。

import ...

public class DownloadImageActivity extends AppCompatActivity {

    private ImageView imageView;
    private ProgressBar progressBar;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_download_image);

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

        // 启动异步任务下载图片
        new DownloadImageTask().execute("sample.gif");
    }

    private class DownloadImageTask extends AsyncTask<String, Void, Bitmap> {

        @Override
        protected void onPreExecute() {
            progressBar.setVisibility(View.VISIBLE); // 显示进度条
        }

        @Override
        protected Bitmap doInBackground(String... urls) {
            String imageUrl = urls[0];
            Bitmap bitmap = null;
            try {
                URL url = new URL(imageUrl);
                HttpURLConnection connection = (HttpURLConnection) url.openConnection();
                connection.setDoInput(true);
                connection.connect();
                InputStream input = connection.getInputStream();
                bitmap = BitmapFactory.decodeStream(input);
            } catch (IOException e) {
                e.printStackTrace();
            }
            return bitmap;
        }

        @Override
        protected void onPostExecute(Bitmap result) {
            progressBar.setVisibility(View.GONE); // 隐藏进度条
            if (result != null) {
                imageView.setImageBitmap(result); // 将下载的图片设置到 ImageView
            } else {
                Toast.makeText(DownloadImageActivity.this, "下载图片失败", Toast.LENGTH_SHORT).show();
            }
        }
    }
}

  1. onPreExecute(): 在后台任务开始前,我们将 ProgressBar 设置为可见。
  2. doInBackground(): 在后台下载并解码为 Bitmap 对象并返回。这里是在后台线程执行,不能直接操作 ImageView
  3. onPostExecute(): 接收 doInBackground() 返回的 Bitmap 对象。首先隐藏 ProgressBar。如果 Bitmap 不为空,则将其设置到 ImageView 上,从而更新 UI。如果下载失败,则显示一个 Toast 消息。

 

Note: 需要注意的是,AsyncTask 已经被标记为 deprecated(不推荐使用),Google 推荐使用更现代的并发工具,如 CoroutinesRxJava

 


Navigation Drawer(导航抽屉)是一个从屏幕边缘滑出的面板,其中包含应用程序的主要导航选项。

除去各种Fragment和样式,我们来看如何应用一个Drawer到Activity中:

import ...

public class MainActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener {

    private DrawerLayout drawerLayout;
    private NavigationView navigationView;
    private Toolbar toolbar;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        toolbar = findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        drawerLayout = findViewById(R.id.drawer_layout);
        navigationView = findViewById(R.id.nav_view);
        navigationView.setNavigationItemSelectedListener(this);

        ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(this, drawerLayout, toolbar,
                R.string.navigation_drawer_open, R.string.navigation_drawer_close);
        drawerLayout.addDrawerListener(toggle);
        toggle.syncState();

        // 设置默认选中项 (可选)
        navigationView.setCheckedItem(R.id.nav_item1);
        
        // 初始化Fragment (省略)
    }

    @Override
    public void onBackPressed() {
        if (drawerLayout.isDrawerOpen(GravityCompat.START)) {
            drawerLayout.closeDrawer(GravityCompat.START);
        } else {
            super.onBackPressed();
        }
    }

    @Override
    public boolean onNavigationItemSelected(@NonNull MenuItem item) {
        int id = item.getItemId();
		FragmentManager fragmentManager = getSupportFragmentManager();
        FragmentTransaction transaction = fragmentManager.beginTransaction();
        
        if (id == R.id.nav_item1) {
            transaction.replace(R.id.fragment_container, new ItemOneFragment());
            // 处理选项一的点击事件,转换Fragment
        } else if (id == R.id.nav_item2) {
            startActivity(new Intent(this, ItemTwoFragment.class));
            // 处理选项二的点击事件,跳转
        }
		
        transaction.commit();
        drawerLayout.closeDrawer(GravityCompat.START);
        return true;
    }
}

  1. 获取布局元素:onCreate() 方法中,我们获取了 DrawerLayoutNavigationViewToolbar 的实例。
  2. 设置 Toolbar: 我们使用 setSupportActionBar(toolbar)Toolbar 设置为 Activity 的 ActionBar。
  3. 设置 NavigationView 监听器: 我们实现了 NavigationView.OnNavigationItemSelectedListener 接口,并将其设置为 NavigationView 的监听器,以便在菜单项被点击时接收回调。
  4. 创建 ActionBarDrawerToggle:
    • ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(...) 创建了一个 ActionBarDrawerToggle 的实例,它需要以下参数:
      • Activity: 当前的 Activity。
      • DrawerLayout: 我们的 DrawerLayout 实例。
      • Toolbar: 我们的 Toolbar 实例。
      • openDrawerContentDescRes: 用于辅助功能的字符串资源,描述打开抽屉的操作。
      • closeDrawerContentDescRes: 用于辅助功能的字符串资源,描述关闭抽屉的操作。
    • drawerLayout.addDrawerListener(toggle): 将 toggle 添加为 DrawerLayout 的监听器,以便它可以监听抽屉的状态变化。
    • toggle.syncState(): 同步抽屉的状态和菜单图标。这会在首次创建 Activity 或配置更改后更新菜单图标的状态。
  5. 处理后退按钮: 重写 onBackPressed() 方法,以便在抽屉打开时先关闭抽屉,而不是直接退出 Activity。
  6. 处理菜单项点击事件: onNavigationItemSelected(@NonNull MenuItem item) 方法在导航抽屉中的菜单项被点击时调用。
    • 我们使用 item.getItemId() 获取被点击菜单项的 ID。
    • 使用 if-else if 语句来判断点击了哪个菜单项,并执行相应的操作(这里只是简单地显示一个 Toast)。
    • drawerLayout.closeDrawer(GravityCompat.START): 在处理完点击事件后关闭导航抽屉。
    • 返回 true 表示我们已经处理了点击事件。

 

在处理中,我们采用了两种方法:

  • transaction.replace(R.id.fragment_container, new YourFragment()); 方法来替换 fragment_container 中的内容。最后,我们调用 transaction.commit(); 来提交事务,使 Fragment 的替换操作生效。
  • startActivity(new Intent(this, YourFragment.class));方法使用Intent跳转到下一页面。
这里的一切都有始有终,却能容纳所有的不期而遇和久别重逢。
最后更新于 2024-12-27