客户端开发 - Android Studio 笔记

一、基础介绍与环境配置

1. 课程前面的话

Android开发是指使用Android操作系统(基于Linux内核)进行应用程序(App)的开发。

Android开发通常使用Java或Kotlin作为编程语言,开发环境主要基于Android Studio。

随着技术的不断演进,Android开发已经从单一的智能手机应用扩展到平板、电视、可穿戴设备等多个平台。

我们将通过逐步学习Android的基本概念和开发流程,帮助大家掌握Android开发的核心技能,并能够独立开发Android应用。

2. Android 发展历程

Android由Google主导开发,是目前世界上使用最广泛的移动操作系统之一。

Android的发展历程可分为以下几个关键阶段:

  • 2003年:Android公司成立,最初是为了开发一种新型的数字相机操作系统。
  • 2005年:Google收购Android公司,Android项目正式转向移动操作系统开发。
  • 2008年:Android 1.0正式发布,并发布首款Android手机——T-Mobile G1。
  • 2010年:Android成为全球最大的移动操作系统。
  • 2014年:Android 5.0 Lollipop发布,支持64位架构,提升了图形性能和用户体验。
  • 2017年:Android 8.0 Oreo发布,加入了很多新的特性,如自适应图标、通知渠道等。
  • 2020年:Android 11发布,引入了更多隐私控制和通知管理功能。

3. Android 开发机器配置要求

Android开发环境的配置要求相对较高,以下是开发Android应用所需要的计算机配置建议:

  • 操作系统
    • Windows 10或更高版本,64位
    • macOS 10.14(Mojave)或更高版本
    • Ubuntu 18.04或更高版本(64位)
  • 处理器:现代Intel或AMD处理器,建议至少4核CPU。
  • 内存:至少8GB RAM(16GB或更高会更流畅)。
  • 存储:安装Android Studio及相关工具需要约8GB的可用硬盘空间。建议安装SSD,以加快开发速度。
  • 显卡:支持OpenGL 2.0的显卡,部分Android模拟器可能需要。

4. Android Studio与SDK下载安装

Android Studio是Google官方推荐的IDE(集成开发环境),用于开发Android应用。

以下是Android Studio与SDK的下载安装步骤:

  1. 下载Android Studio
  2. 安装Android Studio
    • 对于Windows用户,双击.exe文件进行安装。
    • 对于macOS用户,拖动Android Studio图标到应用程序文件夹中。
    • 对于Linux用户,解压后通过命令行进行配置。
  3. 下载并安装SDK
    • 安装完Android Studio后,打开并进入“SDK Manager”,在其中下载必要的SDK工具、平台和API包。
    • 确保至少安装一个API版本(通常选择最新的稳定版本)。
  4. 安装必要的工具
    • 在SDK Manager中,确保安装了以下工具:
      • Android SDK Platform-tools(命令行工具)
      • Android SDK Build-tools(构建工具)
      • Android Emulator(模拟器)
      • 系统映像(如Google APIs等)

5. 创建工程与创建模拟器

创建第一个Android工程的步骤如下:

  1. 新建项目
    • 打开Android Studio,点击“Start a new Android Studio project”。
    • 选择一个模板(如Empty Activity)。
    • 填写项目名、包名、保存位置等信息,选择编程语言(Java或Kotlin)。
    • 点击“Finish”完成项目创建。
  2. 创建模拟器
    • 在Android Studio中点击工具栏的“AVD Manager”图标(Android Virtual Device Manager)。
    • 点击“Create Virtual Device”,选择设备型号(如Pixel 4)。
    • 选择系统映像(可以选择与当前SDK版本匹配的版本)。
    • 完成后,点击“Finish”创建模拟器。
    • 在AVD Manager中选择模拟器并点击“Play”启动模拟器。

6. 观察App运行日志

Android开发中,我们常通过日志来查看应用的运行状态、调试信息和错误信息。

使用Android Studio中的Logcat工具来观察App运行日志。

  1. 打开Logcat

    • 在Android Studio的底部工具栏点击“Logcat”选项卡。
  2. 过滤日志

    • 可以通过日志级别(如Verbose、Debug、Info、Warn、Error)来过滤日志。
    • 可以通过包名过滤日志,只显示当前应用的日志信息。
    • 在代码中使用Log.d("TAG", "Debug message")等方法输出日志。
  3. 常用日志方法

    Log.v("TAG", "Verbose message"); // 最详细的日志
    Log.d("TAG", "Debug message"); // 调试信息
    Log.i("TAG", "Info message"); // 常规信息
    Log.w("TAG", "Warning message"); // 警告信息
    Log.e("TAG", "Error message"); // 错误信息
    

7. 环境安装可能会遇到的问题

在安装和配置Android开发环境时,可能会遇到以下常见问题:

  1. 无法启动模拟器
    • 确认CPU虚拟化(如Intel VT-x或AMD-V)在BIOS中已启用。
    • 确保系统支持硬件加速虚拟化。
  2. SDK无法下载或更新
    • 可能是因为网络问题,尝试更改SDK镜像源(例如使用国内镜像)。
  3. Gradle构建错误
    • 确认Gradle版本与Android Studio兼容。可以在gradle-wrapper.properties文件中修改Gradle版本。
    • 运行./gradlew clean命令清理缓存后重新构建项目。
  4. 模拟器启动缓慢或卡顿
    • 尝试使用更高性能的系统映像(如x86体系结构的映像),并确保启用了硬件加速。

二、界面设计与控件使用

在安卓应用开发中,界面设计与控件的使用是核心部分,涉及到如何有效地创建和管理用户界面(UI)以及如何与用户交互。

1. 界面显示与逻辑处理

在Android中,界面显示是通过ActivityFragment以及布局文件来实现的。逻辑处理则通过编写Java/Kotlin代码来与UI控件交互。

主要步骤

  • 布局文件:通常使用XML文件来描述界面。布局文件中定义了UI组件的结构及其属性。
  • Activity:每个界面通常对应一个Activity类,通过Activity来加载布局文件并处理用户输入。
  • 事件处理:通过在Activity或Fragment中编写事件处理逻辑(如按钮点击事件、文本框输入事件等)来响应用户的交互。
<!-- 示例布局文件 (res/layout/activity_main.xml) -->

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:orientation="vertical"
              android:padding="16dp">

    <TextView
              android:id="@+id/textView"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:text="Hello World!"
              android:textSize="20sp"
              android:textColor="#000000"/>

    <Button
            android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Click Me" />
</LinearLayout>
// Activity代码 (MainActivity.java)

public class MainActivity extends AppCompatActivity {

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

        TextView textView = findViewById(R.id.textView);
        Button button = findViewById(R.id.button);
        button.setOnClickListener(v -> textView.setText("Button clicked!"));
    }
}

2. Activity 创建与跳转

Activity是Android应用中的一个单一屏幕,用于处理界面显示与用户交互。不同的Activity之间通过Intent来实现跳转。

Activity 启动与结束

  1. 启动Activity:使用Intent对象来启动另一个Activity。可以通过显式Intent或隐式Intent来启动Activity。

    // 显式启动Activity
    Intent intent = new Intent(this, SecondActivity.class);
    startActivity(intent);
    
  2. 结束Activity:在当前Activity中使用finish()方法结束当前Activity,回到上一个Activity。

    finish();
    

Activity 生命周期

Activity的生命周期是指Activity从创建到销毁过程中的各个状态。生命周期方法包括:

  1. onCreate(): Activity被创建时调用,用于初始化。

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
    
  2. onStart(): Activity准备好与用户交互时调用。

    @Override
    protected void onStart() {
        super.onStart();
    }
    
  3. onResume(): Activity开始与用户交互时调用。

    @Override
    protected void onResume() {
        super.onResume();
    }
    
  4. onPause(): Activity即将进入后台时调用,通常用于保存数据或暂停动画等。

    @Override
    protected void onPause() {
        super.onPause();
    }
    
  5. onStop(): Activity不再可见时调用。

    @Override
    protected void onStop() {
        super.onStop();
    }
    
  6. onDestroy(): Activity被销毁时调用,用于释放资源。

    @Override
    protected void onDestroy() {
        super.onDestroy();
    }
    

Activity 启动模式

Android提供了四种Activity启动模式(任务栈模式):

  1. standard:每次启动Activity都会创建一个新的实例,默认模式。
  2. singleTop:如果目标Activity位于栈顶,不会创建新的实例。
  3. singleTask:如果任务栈中已有该Activity的实例,将把该实例置于栈顶并销毁其他Activity。
  4. singleInstance:与singleTask类似,但它会保证该Activity的任务栈中只包含该Activity。
<!-- 在AndroidManifest.xml中设置启动模式 -->

<activity android:name=".SecondActivity"
    android:launchMode="singleTop">
</activity>

3. 常用控件与事件处理

3.1 设置文本的内容

TextView是用于显示文本的常用控件。通过setText()方法可以动态地改变文本内容。

TextView textView = findViewById(R.id.textView);
textView.setText("New Text Content");

3.2 设置文本字体大小

通过setTextSize()方法可以设置文本的字体大小,单位为sp

TextView textView = findViewById(R.id.textView);
textView.setTextSize(24); // 设置字体大小为24sp

3.3 设置文本的颜色

可以通过setTextColor()方法来设置文本的颜色。颜色可以使用资源文件中的颜色值或直接使用Color类定义的颜色。

TextView textView = findViewById(R.id.textView);
textView.setTextColor(Color.parseColor("#FF5733")); // 设置字体颜色为橙色

3.4 设置视图的宽高

通过布局文件或代码来动态设置视图(如按钮、文本框等)的宽高。在代码中,ViewGroup.LayoutParams可以设置视图的宽高。

Button button = findViewById(R.id.button);
ViewGroup.LayoutParams params = button.getLayoutParams();
params.width = ViewGroup.LayoutParams.MATCH_PARENT;
params.height = ViewGroup.LayoutParams.WRAP_CONTENT;
button.setLayoutParams(params);

3.5 设置视图的间距

视图的间距可以通过layout_margin属性设置,单位为dp。例如,设置左边距为16dp。

<Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Click Me"
        android:layout_marginLeft="16dp"/>

或者在代码中动态设置:

Button button = findViewById(R.id.button);
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) button.getLayoutParams();
params.leftMargin = 16;  // 设置左边距
button.setLayoutParams(params);

3.6 设置视图的对齐方式

对齐方式通常通过android:gravity(控制内容的对齐)和android:layout_gravity(控制视图的对齐)来设置。

  1. gravity:控制内容在视图内的对齐方式。

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        android:gravity="center" /> <!-- 文本居中对齐 -->
    
  2. layout_gravity:控制视图本身在其父视图中的对齐方式。

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Click Me"
        android:layout_gravity="center" /> <!-- 按钮居中对齐 -->
    

4. 布局控件

在 Android 中,布局控件是用于管理界面元素排列的容器。不同的布局控件提供不同的排列方式。

4.1 LinearLayout

LinearLayout 是最常见的布局之一,它将子控件按水平或垂直方向线性排列。

  • 属性
    • orientation: 设置布局的方向(verticalhorizontal)。
    • gravity: 控制子控件的对齐方式(如 center, left, right)。
    • layout_widthlayout_height: 控制 LinearLayout 自身的尺寸。
  • 代码示例
<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp">
  
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Button 1" />
        
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Button 2" />
</LinearLayout>

4.2 RelativeLayout

RelativeLayout 是一种更灵活的布局方式,它允许子控件相对于其他控件或者相对于父控件的位置来进行布局。

  • 属性
    • layout_alignParentTop, layout_alignParentLeft: 将控件与父布局对齐。
    • layout_toRightOf, layout_below: 将控件相对于另一个控件进行定位。
  • 代码示例
<RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:id="@+id/button1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Button 1"
        android:layout_alignParentTop="true"
        android:layout_alignParentLeft="true"/>

    <Button
        android:id="@+id/button2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Button 2"
        android:layout_below="@id/button1"
        android:layout_alignParentLeft="true"/>
</RelativeLayout>

4.3 GridLayout

GridLayout 是一种将子控件放置在网格中的布局方式,允许按行列排列控件。

  • 属性
    • rowCount: 定义网格的行数。
    • columnCount: 定义网格的列数。
  • 代码示例
<GridLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:rowCount="3"
    android:columnCount="3">

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="1"
        android:layout_row="0"
        android:layout_column="0" />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="2"
        android:layout_row="0"
        android:layout_column="1" />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="3"
        android:layout_row="0"
        android:layout_column="2" />
</GridLayout>

4.4 ScrollView

ScrollView 是一个可以让其子控件进行滚动的布局容器。通常用来在内容超过屏幕时提供滚动功能。

  • 属性
    • android:scrollbars: 设置滚动条的显示方式(如 vertical, horizontal)。
  • 代码示例
<ScrollView
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        
        <!-- 这里放置多个控件 -->
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Some long text that needs to be scrollable..." />
    </LinearLayout>
</ScrollView>

5. 按钮与点击事件

5.1 Button

Button 控件是 Android 中常见的控件之一,用于响应用户点击。

  • 属性
    • android:text: 设置按钮文本。
    • android:onClick: 绑定按钮的点击事件。
  • 代码示例
<Button
    android:id="@+id/button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Click Me" />

5.2 点击事件

可以通过 setOnClickListener 来设置按钮的点击事件。

  • 代码示例
Button button = findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Toast.makeText(MainActivity.this, "Button clicked!", Toast.LENGTH_SHORT).show();
    }
});

5.3 长按点击事件

通过 setOnLongClickListener 来监听长按事件。

  • 代码示例
Button button = findViewById(R.id.button);
button.setOnLongClickListener(new View.OnLongClickListener() {
    @Override
    public boolean onLongClick(View v) {
        Toast.makeText(MainActivity.this, "Button long pressed!", Toast.LENGTH_SHORT).show();
        return true; // 返回true表示事件已处理
    }
});

5.4 禁用与恢复按钮

可以使用 setEnabled(false) 来禁用按钮,使用 setEnabled(true) 来恢复按钮。

  • 代码示例
Button button = findViewById(R.id.button);
button.setEnabled(false);  // 禁用按钮
button.setEnabled(true);   // 恢复按钮

6. 图像控件

6.1 ImageView

ImageView 控件用于显示图像,可以设置静态图片或动态资源。

  • 属性
    • android:src: 设置图片源。
  • 代码示例
<ImageView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:src="@drawable/sample_image" />

6.2 ImageButton

ImageButton 是一个带有图像的按钮,可以响应点击事件。

  • 代码示例
<ImageButton
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:src="@drawable/sample_image"
    android:contentDescription="Image Button"
    android:onClick="onImageButtonClick" />

6.3 同时展示文本与图像

可以使用 ImageSpanTextView 中同时显示文本和图片。

  • 代码示例
TextView textView = findViewById(R.id.textView);
SpannableString spanString = new SpannableString("This is a button: ");
Drawable drawable = getResources().getDrawable(R.drawable.sample_image);
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());

ImageSpan imageSpan = new ImageSpan(drawable);
spanString.setSpan(imageSpan, 17, 18, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
textView.setText(spanString);

7. 案例:计算器

7.1 界面编码

使用 GridLayout 来实现简单的计算器界面。

  • 代码示例
<GridLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:rowCount="5"
    android:columnCount="4">

    <TextView
        android:id="@+id/result"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="0"
        android:gravity="end"
        android:textSize="32sp"
        android:layout_columnSpan="4" />

    <!-- 数字按钮 -->
    <Button android:text="1" ... />
    <Button android:text="2" ... />
    <Button android:text="3" ... />
    <Button android:text="+" ... />
    <!-- 其他按钮 -->
</GridLayout>

7.2 逻辑处理编码

在计算器的实现中,我们需要给每个按钮设置点击事件,根据用户的输入来进行简单的数学计算。

这里我们以数字按钮、运算符按钮为例,展示如何实现点击事件和计算的逻辑。

  1. 按钮点击事件设置

    首先,我们为每个按钮设置 OnClickListener,将点击的数字或运算符显示在 TextView(例如 resultText)中,并进行计算。

  2. 逻辑处理:在计算过程中,我们需要保持当前输入的数字和运算符。

    可以使用一个 StringBuilder 来保存输入的数字字符串,并通过按钮点击事件动态更新显示结果。

  • 代码示例
public class CalculatorActivity extends AppCompatActivity {

    private TextView resultText;
    private StringBuilder input = new StringBuilder(); // 用于保存用户输入的表达式
    private boolean isResult = false; // 用来判断是否显示计算结果

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

        resultText = findViewById(R.id.result);

        // 数字按钮的点击事件
        findViewById(R.id.button1).setOnClickListener(view -> appendNumber("1"));
        findViewById(R.id.button2).setOnClickListener(view -> appendNumber("2"));
        findViewById(R.id.button3).setOnClickListener(view -> appendNumber("3"));
        findViewById(R.id.button4).setOnClickListener(view -> appendNumber("4"));
        findViewById(R.id.button5).setOnClickListener(view -> appendNumber("5"));
        findViewById(R.id.button6).setOnClickListener(view -> appendNumber("6"));
        findViewById(R.id.button7).setOnClickListener(view -> appendNumber("7"));
        findViewById(R.id.button8).setOnClickListener(view -> appendNumber("8"));
        findViewById(R.id.button9).setOnClickListener(view -> appendNumber("9"));
        findViewById(R.id.button0).setOnClickListener(view -> appendNumber("0"));

        // 运算符按钮的点击事件
        findViewById(R.id.buttonAdd).setOnClickListener(view -> appendOperator("+"));
        findViewById(R.id.buttonSubtract).setOnClickListener(view -> appendOperator("-"));
        findViewById(R.id.buttonMultiply).setOnClickListener(view -> appendOperator("*"));
        findViewById(R.id.buttonDivide).setOnClickListener(view -> appendOperator("/"));

        // 清除按钮的点击事件
        findViewById(R.id.buttonClear).setOnClickListener(view -> clearInput());

        // 等号按钮的点击事件
        findViewById(R.id.buttonEqual).setOnClickListener(view -> calculateResult());
    }

    // 在屏幕上显示数字
    private void appendNumber(String number) {
        if (isResult) {
            input.setLength(0); // 如果是计算结果状态,清空输入
            isResult = false;
        }
        input.append(number);
        resultText.setText(input.toString());
    }

    // 在屏幕上显示运算符
    private void appendOperator(String operator) {
        if (isResult) {
            isResult = false;
        }
        input.append(operator);
        resultText.setText(input.toString());
    }

    // 清除输入
    private void clearInput() {
        input.setLength(0);
        resultText.setText("0");
        isResult = false;
    }

    // 计算结果
    private void calculateResult() {
        try {
            String expression = input.toString();
            double result = evaluateExpression(expression); // 使用自定义的表达式解析方法
            resultText.setText(String.valueOf(result));
            input.setLength(0); // 清空输入
            input.append(result); // 显示结果
            isResult = true; // 设置为计算结果状态
        } catch (Exception e) {
            resultText.setText("Error");
        }
    }

    // 自定义方法:简单的表达式解析方法(这里只做加减乘除的处理)
    private double evaluateExpression(String expression) {
        // 使用简单的 Java 脚本引擎来进行运算(这里可扩展为更复杂的运算)
        ScriptEngine engine = new ScriptEngineManager().getEngineByName("rhino");
        try {
            return (double) engine.eval(expression);
        } catch (ScriptException e) {
            e.printStackTrace();
            return 0;
        }
    }
}

三、Intent与资源管理

在 Android 应用开发中,Intent资源管理 是非常重要的组成部分,

它们涉及到应用间的通信、页面间数据传递、配置文件的管理、以及资源的动态加载和调整。

1. Intent与Activity通信

Intent 是 Android 中用于不同组件(如 ActivityServiceBroadcastReceiver)之间传递数据和启动操作的机制。

通过 Intent,你可以启动不同的组件并传递数据。在开发过程中,我们通常会遇到两种类型的 Intent:显式 Intent 和隐式 Intent。

1.1 显式Intent和隐式Intent

  • 显式Intent: 显式 Intent 是指明确指定目标组件(如 ActivityService 等)的 Intent

    通常用于同一个应用内的不同 Activity 之间的通信,或者在特定应用间进行组件交互。

    代码示例

    Intent intent = new Intent(CurrentActivity.this, TargetActivity.class);
    startActivity(intent);
    

    CurrentActivity.this 表示当前的 ActivityTargetActivity.class 是目标 Activity,显式地指定了启动的组件。

  • 隐式Intent: 隐式 Intent 没有明确指定目标组件,通过 Intent 中的 ActionCategory 来请求系统或其他应用处理某项任务。

    Android 系统会根据意图过滤并查找匹配的组件。

    代码示例

    Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.example.com"));
    startActivity(intent);
    

    ACTION_VIEW 是一个隐式的动作,指示 Android 系统启动一个能够处理该 URL 的应用。

    系统会自动选择适合的浏览器或其他应用来打开该网页。

1.2 向下一个Activity发送数据

通过 Intent,你可以在启动下一个 Activity 时传递数据。常见的方法是使用 putExtra() 将数据添加到 Intent 中。

代码示例

// 创建 Intent
Intent intent = new Intent(CurrentActivity.this, TargetActivity.class);

// 添加数据
intent.putExtra("key", "Hello from CurrentActivity!");

// 启动 TargetActivity
startActivity(intent);

TargetActivity 中接收数据的方式是通过 getIntent() 获取当前 Intent

然后使用 getStringExtra() 或其他 getExtra() 方法来获取传递的数据。

代码示例(接收数据)

Intent intent = getIntent();
String message = intent.getStringExtra("key");
Log.d("Received Data", message);  // 打印接收到的数据

1.3 向上一个Activity返回数据

如果你希望在当前 Activity 完成任务后,将数据返回给前一个 Activity,可以使用 setResult()finish() 方法。

通常通过 startActivityForResult() 启动下一个 Activity,然后在下一个 Activity 完成后,使用 setResult() 返回数据。

代码示例(启动并返回结果)

// 启动下一个 Activity 并等待结果
Intent intent = new Intent(CurrentActivity.this, ResultActivity.class);
startActivityForResult(intent, 1);

ResultActivity 完成后返回数据:

Intent resultIntent = new Intent();
resultIntent.putExtra("result", "This is the result!");
setResult(RESULT_OK, resultIntent);  // 设置返回结果
finish();  // 结束当前 Activity

在原来的 Activity 中接收结果:

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == 1 && resultCode == RESULT_OK) {
        String result = data.getStringExtra("result");
        Log.d("Result", result);
    }
}

2. 资源与配置信息

Android 的资源管理功能帮助你为不同设备配置和管理资源,如字符串、图像、布局等。

你可以使用资源文件来管理不同语言、屏幕尺寸和配置的资源。

2.1 利用资源文件配置字符串

在 Android 中,字符串资源存储在 res/values/strings.xml 文件中。

你可以通过资源 ID 来访问字符串,而不需要直接在代码中硬编码字符串。

字符串资源文件(strings.xml)

<resources>
    <string name="app_name">MyApp</string>
    <string name="hello_message">Hello, welcome to MyApp!</string>
</resources>

代码中使用字符串资源

String appName = getString(R.string.app_name);
String welcomeMessage = getString(R.string.hello_message);
Log.d("String Resource", appName + ": " + welcomeMessage);

通过这种方式,你可以轻松地进行多语言支持和屏幕适配,只需要修改资源文件而不需要修改代码。

2.2 利用元数据传递配置信息

元数据是一种以 <meta-data> 标签形式定义的配置信息,可以在 AndroidManifest.xml 文件中指定,也可以通过代码访问。

元数据通常用于定义一些静态的配置信息。

AndroidManifest.xml 示例

<application
    android:label="@string/app_name"
    android:icon="@drawable/ic_launcher">
    <meta-data
        android:name="com.example.app.API_KEY"
        android:value="your_api_key_here"/>
</application>

在代码中,你可以通过 getPackageManager() 获取元数据:

try {
    Bundle metaData = getPackageManager().getApplicationInfo(getPackageName(), PackageManager.GET_META_DATA).metaData;
    String apiKey = metaData.getString("com.example.app.API_KEY");
    Log.d("API Key", apiKey);
} catch (PackageManager.NameNotFoundException e) {
    e.printStackTrace();
}

2.3 给应用页面注册快捷方式

Android 允许你为应用创建快捷方式,用户可以通过快捷方式快速启动特定的页面。

在 Android 8.0 及以上版本,推荐使用 ShortcutManager 来管理快捷方式。

创建快捷方式

ShortcutManager shortcutManager = getSystemService(ShortcutManager.class);
if (shortcutManager != null) {
    ShortcutInfo shortcut = new ShortcutInfo.Builder(this, "id_1")
        .setShortLabel("My App")
        .setLongLabel("Open MyApp")
        .setIcon(Icon.createWithResource(this, R.drawable.ic_shortcut))
        .setIntent(new Intent(Intent.ACTION_VIEW, Uri.parse("https://www.example.com")))
        .build();
    shortcutManager.setDynamicShortcuts(Arrays.asList(shortcut));
}

为应用页面添加静态快捷方式(AndroidManifest.xml)

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

<shortcut>
    <shortcut id="id_1">
        <intent android:action="android.intent.action.VIEW" android:targetClass="com.example.app.MainActivity"/>
        <label>My App</label>
        <icon android:resource="@drawable/ic_launcher"/>
    </shortcut>
</shortcut>

3. 练习题

  1. 显式 Intent 和隐式 Intent:编写一个示例,使用显式 Intent 启动另一个 Activity,并通过隐式 Intent 打开一个网页。
  2. 数据传递:通过 Intent 向下一个 Activity 发送多个数据(如字符串、整数和布尔值),并在目标 Activity 中接收并显示。
  3. 快捷方式:创建一个应用快捷方式,指向一个特定的 Activity 或 URL,并测试其功能。
  4. 多语言支持:修改应用中的字符串资源,使其支持英文和中文两种语言。然后,在不同语言的设备上测试。

四、图形与UI组件

在Android开发中,图形与UI组件是用户交互的核心部分。通过合理的使用图形和控件,能够有效提升应用的交互体验。

1. Drawable与图形

Drawable是Android中用于定义图形对象的类,可以通过它显示不同类型的图像、形状、颜色等。

Drawable是一种可绘制的对象,可以应用于ImageViewButton等控件的背景或者源图像。

1.1 图形Drawable

Drawable是Android绘制图形的基类。可以使用它的子类来创建不同类型的图形,例如颜色、图片、形状等。

示例:使用Drawable设置背景颜色

<LinearLayout
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:background="@drawable/background_color">
</LinearLayout>

这里的@drawable/background_color是一个资源文件,可以定义为颜色背景或渐变等。

1.2 形状图形

ShapeDrawableDrawable的一个子类,允许我们绘制简单的形状(如矩形、圆形、圆角矩形等)。

示例:创建矩形形状

<LinearLayout
              android:layout_width="match_parent"
              android:layout_height="wrap_content">
    <TextView
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:text="Hello World"
              android:background="@drawable/rectangle_shape" />
</LinearLayout>

rectangle_shape.xml

<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="#FF5722"/>
    <corners android:radius="8dp"/>
</shape>

该代码定义了一个圆角矩形背景。

1.3 点9图片

点9图片(9-patch image)是指具有可伸缩区域的PNG图片,

通过设置边缘区域保持不变,其他区域可以拉伸,适用于按钮、背景等需要拉伸的元素。

  1. 使用.9.png后缀命名图片(例如button_background.9.png)。
  2. 在Android Studio中编辑图片的拉伸区域,修改图片的边框区域。

点9图片的使用通常通过ImageViewButton等控件来实现。

1.4 状态列表图形

状态列表图形(State List Drawable)可以根据控件的状态(如按下、选中、焦点等)显示不同的图像。

通常用于按钮的不同状态(如按下时的背景颜色)或者ImageView的图像切换。

示例:创建状态列表图形

button_background.xml

<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/button_pressed" android:state_pressed="true"/>
    <item android:drawable="@drawable/button_normal"/>
</selector>

在上面的代码中,当按钮被按下时,背景图像会显示为button_pressed,否则显示button_normal

2. 常用UI控件

2.1 复选框CheckBox

CheckBox是一个可以选择和取消选择的控件,通常用于多选的场景。

示例:使用CheckBox

<CheckBox
    android:id="@+id/checkBox"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Accept Terms and Conditions"/>

可以通过代码监听其选中状态:

CheckBox checkBox = findViewById(R.id.checkBox);
checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> {
    if (isChecked) {
        // 处理选中状态
    } else {
        // 处理未选中状态
    }
});

2.2 开关按钮Switch

Switch控件是一个用于显示和切换状态的控件,类似于iOS中的滑动开关。

示例:使用Switch

<Switch
    android:id="@+id/switchButton"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Enable Notifications"/>

监听Switch的状态变化:

Switch switchButton = findViewById(R.id.switchButton);
switchButton.setOnCheckedChangeListener((buttonView, isChecked) -> {
    if (isChecked) {
        // 开启通知
    } else {
        // 关闭通知
    }
});

2.3 单选按钮RadioButton

RadioButton是单选控件,通常用于一组互斥选项中,只能选择一个。

示例:使用RadioButton

<RadioGroup
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">
    <RadioButton
        android:id="@+id/radioButton1"
        android:text="Option 1"/>
    <RadioButton
        android:id="@+id/radioButton2"
        android:text="Option 2"/>
</RadioGroup>

可以通过RadioGroup来管理一组RadioButton

RadioGroup radioGroup = findViewById(R.id.radioGroup);
radioGroup.setOnCheckedChangeListener((group, checkedId) -> {
    switch (checkedId) {
        case R.id.radioButton1:
            // 选中Option 1
            break;
        case R.id.radioButton2:
            // 选中Option 2
            break;
    }
});

2.4 编辑框EditText

EditText是用于接收用户输入文本的控件。

示例:使用EditText

<EditText
    android:id="@+id/editText"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:hint="Enter text"/>

监听文本变化:

EditText editText = findViewById(R.id.editText);
editText.addTextChangedListener(new TextWatcher() {
    @Override
    public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) {}

    @Override
    public void onTextChanged(CharSequence charSequence, int start, int before, int count) {}

    @Override
    public void afterTextChanged(Editable editable) {
        // 文本变化后进行处理
    }
});

3. 事件监听器与对话框

3.1 焦点变更监听器

FocusChangeListener用于监听控件的焦点变化。

EditText editText = findViewById(R.id.editText);
editText.setOnFocusChangeListener((v, hasFocus) -> {
    if (hasFocus) {
        // 获得焦点
    } else {
        // 失去焦点
    }
});

3.2 文本变化监听器

TextWatcher是监听EditText文本变化的接口,可以实时捕获文本的变化。

editText.addTextChangedListener(new TextWatcher() {
    @Override
    public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) {}

    @Override
    public void onTextChanged(CharSequence charSequence, int start, int before, int count) {}

    @Override
    public void afterTextChanged(Editable editable) {
        // 文本变化后执行操作
    }
});

3.3 提醒对话框AlertDialog

AlertDialog是Android中最常用的对话框之一,用于显示提示、警告等信息。

示例:创建一个简单的提示对话框

new AlertDialog.Builder(this)
    .setTitle("Alert")
    .setMessage("Are you sure you want to proceed?")
    .setPositiveButton("Yes", (dialog, which) -> {
        // 确认操作
    })
    .setNegativeButton("No", (dialog, which) -> {
        // 取消操作
    })
    .show();

3.4 日期对话框DatePickerDialog

DatePickerDialog用于选择日期。

示例:使用DatePickerDialog

Calendar calendar = Calendar.getInstance();
DatePickerDialog datePickerDialog = new DatePickerDialog(
    this,
    (view, year, monthOfYear, dayOfMonth) -> {
        // 获取日期
    },
    calendar.get(Calendar.YEAR),
    calendar.get(Calendar.MONTH),
    calendar.get(Calendar.DAY_OF_MONTH)
);
datePickerDialog.show();

3.5 时间对话框 TimePickerDialog

TimePickerDialog 是 Android 提供的一个对话框,用于让用户选择时间(小时和分钟)。

它非常适用于需要获取用户时间输入的场景,比如设置提醒、闹钟等。

示例:使用 TimePickerDialog

TimePickerDialog timePickerDialog = new TimePickerDialog(
    this, 
    (view, hourOfDay, minute) -> {
        // 这里可以获取用户选择的时间
        Log.d("TimePicker", "Selected time: " + hourOfDay + ":" + minute);
    },
    12, 0, true // 设置初始时间为12:00,true表示24小时制
);

// 显示时间选择对话框
timePickerDialog.show();

在上面的示例中,TimePickerDialog 的构造函数中有以下几个参数:

  • this:当前上下文(通常是Activity或Fragment)。

  • (view, hourOfDay, minute):一个 TimePickerDialog.OnTimeSetListener,用于监听用户选择的时间。

    通过 hourOfDayminute 可以获取用户选择的小时和分钟。

  • 12, 0:指定初始的时间(12点 0 分)。

  • true:是否使用 24 小时制。如果是 false,则为 12 小时制(AM/PM)。

4. 案例:找回密码

为了更好地理解如何运用图形、UI组件和事件监听器,这里将展示一个简单的找回密码的案例。

此案例将涉及到创建一个登录界面、实现登录逻辑以及找回密码的流程。

4.1 登录界面

首先,我们需要创建一个简单的登录界面,包含用户名、密码输入框以及“找回密码”的链接。

  • 登录界面布局(activity_login.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp">

    <EditText
        android:id="@+id/usernameEditText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Username"
        android:inputType="text" />

    <EditText
        android:id="@+id/passwordEditText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Password"
        android:inputType="textPassword" />

    <Button
        android:id="@+id/loginButton"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Login" />

    <TextView
        android:id="@+id/forgotPasswordText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Forgot Password?"
        android:textColor="@android:color/holo_blue_dark"
        android:gravity="center"
        android:paddingTop="10dp" />

</LinearLayout>

在这个布局中,我们使用了:

  • EditText 控件来输入用户名和密码。
  • Button 控件用于用户点击登录。
  • TextView 用于显示“找回密码”链接,用户点击它后可以跳转到找回密码页面。

4.2 登录逻辑

接下来,在 LoginActivity 中实现登录逻辑。当用户点击登录按钮时,我们将验证用户名和密码是否正确。

为了简化演示,假设用户名为 admin,密码为 12345

  • 登录逻辑(LoginActivity.java
public class LoginActivity extends AppCompatActivity {

    private EditText usernameEditText;
    private EditText passwordEditText;
    private Button loginButton;
    private TextView forgotPasswordText;

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

        usernameEditText = findViewById(R.id.usernameEditText);
        passwordEditText = findViewById(R.id.passwordEditText);
        loginButton = findViewById(R.id.loginButton);
        forgotPasswordText = findViewById(R.id.forgotPasswordText);

        loginButton.setOnClickListener(v -> login());
        forgotPasswordText.setOnClickListener(v -> navigateToForgotPassword());
    }

    private void login() {
        String username = usernameEditText.getText().toString();
        String password = passwordEditText.getText().toString();

        if ("admin".equals(username) && "12345".equals(password)) {
            // 登录成功,跳转到主页
            Toast.makeText(this, "Login Successful", Toast.LENGTH_SHORT).show();
            // 在这里可以进行跳转,例如:
            // startActivity(new Intent(this, MainActivity.class));
        } else {
            // 登录失败
            Toast.makeText(this, "Invalid Username or Password", Toast.LENGTH_SHORT).show();
        }
    }

    private void navigateToForgotPassword() {
        // 跳转到找回密码页面
        Intent intent = new Intent(this, ForgotPasswordActivity.class);
        startActivity(intent);
    }
}

在上面的代码中:

  • loginButton.setOnClickListener():为登录按钮设置点击事件,当用户点击登录时,会执行 login() 方法。
  • forgotPasswordText.setOnClickListener():为找回密码链接设置点击事件,当用户点击时,会跳转到找回密码页面。

4.3 找回密码

找回密码页面通常需要用户输入电子邮件或用户名,以便发送重置密码的链接。在此案例中,我们将通过输入电子邮件地址来重置密码。

找回密码界面布局(activity_forgot_password.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp">

    <EditText
        android:id="@+id/emailEditText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Enter your email"
        android:inputType="textEmailAddress" />

    <Button
        android:id="@+id/resetPasswordButton"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Reset Password" />

</LinearLayout>
  • 找回密码逻辑(ForgotPasswordActivity.java
public class ForgotPasswordActivity extends AppCompatActivity {

    private EditText emailEditText;
    private Button resetPasswordButton;

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

        emailEditText = findViewById(R.id.emailEditText);
        resetPasswordButton = findViewById(R.id.resetPasswordButton);

        resetPasswordButton.setOnClickListener(v -> resetPassword());
    }

    private void resetPassword() {
        String email = emailEditText.getText().toString();

        if (isValidEmail(email)) {
            // 这里可以执行重置密码的操作,例如向服务器发送请求
            Toast.makeText(this, "Password reset link sent to " + email, Toast.LENGTH_LONG).show();
            finish();  // 返回登录页面
        } else {
            Toast.makeText(this, "Invalid email address", Toast.LENGTH_SHORT).show();
        }
    }

    private boolean isValidEmail(String email) {
        // 简单的邮箱验证
        return email != null && email.contains("@");
    }
}

ForgotPasswordActivity 中:

  • 用户输入邮箱并点击“Reset Password”按钮时,会验证邮箱地址格式是否正确。

    如果正确,则会发送重置密码的请求(在本示例中,直接显示提示信息,实际开发中可以调用服务器接口)。

  • 使用 Toast 提示用户操作结果。

五、数据存储与管理

1. SharedPreferences

1.1 SharedPreferences使用场景

SharedPreferences 是一种轻量级的数据存储方式,适用于存储一些简单的键值对数据,

常用于保存应用的配置信息、用户的登录状态、偏好设置等。这些数据存储在设备的磁盘中,可以跨会话持久化。

常见的使用场景:

  • 存储用户登录的状态(是否自动登录)
  • 存储用户设置的配置信息(如主题、语言)
  • 存储轻量级的数据,如单个用户的偏好、计数器等

1.2 SharedPreferences用法

使用 SharedPreferences 时,主要通过 SharedPreferences 实例来读取和写入数据。操作步骤通常为:

  1. 获取 SharedPreferences 实例
  2. 通过 SharedPreferences.Editor 对象进行数据存储
  3. 使用 apply()commit() 方法保存数据
// 获取 SharedPreferences 实例
SharedPreferences sharedPreferences = getSharedPreferences("user_preferences", MODE_PRIVATE);

// 获取编辑器进行写入操作
SharedPreferences.Editor editor = sharedPreferences.edit();

// 写入数据
editor.putString("username", "JohnDoe");
editor.putBoolean("isLoggedIn", true);

// 保存数据
editor.apply();

1.3 记住密码

记住密码功能可以通过 SharedPreferences 来实现,通常会存储用户名和密码(注意不建议明文存储密码,实际中最好加密存储)。

// 存储用户名和密码
SharedPreferences sharedPreferences = getSharedPreferences("login_info", MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString("username", "user123");
editor.putString("password", "password123");
editor.apply();

// 获取存储的用户名和密码
String username = sharedPreferences.getString("username", "");
String password = sharedPreferences.getString("password", "");

2. SQLite数据库操作

2.1 SQL基本语法

SQL(Structured Query Language)是与数据库交互的标准语言。常见的 SQL 语法包括:

  • SELECT:查询数据
  • INSERT:插入数据
  • UPDATE:更新数据
  • DELETE:删除数据

例如,查询用户表中的所有数据:

SELECT * FROM users;

2.2 SQLiteDatabase

SQLiteDatabase 类是操作 SQLite 数据库的主要接口,提供了执行 SQL 操作的方法,如执行查询、插入、更新和删除等。

常用方法:

  • insert(): 插入数据
  • update(): 更新数据
  • delete(): 删除数据
  • query(): 查询数据
SQLiteDatabase db = dbHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put("name", "John");
values.put("age", 25);

// 插入数据
db.insert("user_table", null, values);

2.3 SQLiteOpenHelper

SQLiteOpenHelper 是一个辅助类,帮助管理数据库的创建和版本管理。它在数据库版本变更时提供了 onUpgrade() 方法。

public class DBHelper extends SQLiteOpenHelper {

    public DBHelper(Context context) {
        super(context, "my_database", null, 1);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        String createTable = "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)";
        db.execSQL(createTable);
    }

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

2.4 增删改查

插入数据

ContentValues values = new ContentValues();
values.put("name", "John");
values.put("age", 25);
db.insert("users", null, values);

查询数据

Cursor cursor = db.query("users", new String[]{"id", "name", "age"}, null, null, null, null, null);
while (cursor.moveToNext()) {
    String name = cursor.getString(cursor.getColumnIndex("name"));
    int age = cursor.getInt(cursor.getColumnIndex("age"));
}
cursor.close();

更新数据

ContentValues values = new ContentValues();
values.put("age", 26);
db.update("users", values, "name = ?", new String[]{"John"});

删除数据

db.delete("users", "name = ?", new String[]{"John"});

2.5 事务管理

事务用于确保一组数据库操作的原子性。若某操作失败,事务可以回滚到操作前的状态。

db.beginTransaction();
try {
    db.insert("users", null, values);
    db.setTransactionSuccessful();
} finally {
    db.endTransaction();
}

2.6 数据库版本升级

当数据库结构发生变化时(如添加/删除表或修改表结构),需要通过 onUpgrade() 方法来处理数据库的版本升级。

@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    if (oldVersion < 2) {
        // 在这里添加版本2的升级操作
    }
}

3. 优化与存储

3.1 优化记住密码

记住密码时可以进行加密处理,以避免明文存储。例如,可以使用 AES 加密存储密码。

3.2 外部存储空间

外部存储(如 SD 卡)用于存储大量数据或文件。例如,存储用户下载的图片、音频文件等。

获取外部存储路径:

File externalStorage = Environment.getExternalStorageDirectory();

3.3 存储卡上读写图片文件

在外部存储中读写图片文件:

File file = new File(Environment.getExternalStorageDirectory(), "image.jpg");
Bitmap bitmap = BitmapFactory.decodeFile(file.getAbsolutePath());

4. Jetpack Room

4.1 Jetpack Room

Room 是 Android Jetpack 提供的数据库持久化库,用于简化 SQLite 操作。

它通过抽象层来简化数据库操作,并且提供了类型安全的查询。使用步骤:

  1. 创建实体类(Entity)
  2. 创建 DAO(Data Access Object)
  3. 创建数据库类(Database)

4.2 Room增删改查

定义实体类

@Entity(tableName = "users")
public class User {
    @PrimaryKey
    public int id;
    public String name;
    public int age;
}

定义 DAO 接口

@Dao
public interface UserDao {
    @Insert
    void insert(User user);

    @Query("SELECT * FROM users WHERE id = :userId")
    User getUserById(int userId);
}

创建数据库类

@Database(entities = {User.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
    public abstract UserDao userDao();
}

六、内容提供者与权限管理

1. 内容提供者

1.1 什么是内容提供者

内容提供者(ContentProvider)是 Android 系统的一种组件,用于应用之间共享数据。

通过内容提供者,应用可以访问其他应用的数据,如联系人、媒体文件等。

1.2 Server 端代码编写

内容提供者的实现需要继承 ContentProvider 类并实现 onCreate(), query(), insert(), update(), delete() 等方法。

public class MyContentProvider extends ContentProvider {
    @Override
    public boolean onCreate() {
        // 初始化数据库等
        return true;
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
        // 查询操作
        return null;
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        // 插入操作
        return null;
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        // 更新操作
        return 0;
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        // 删除操作
        return 0;
    }

    @Override
    public String getType(Uri uri) {
        return null;
    }
}

1.3 Client 端代码编写

客户端通过 ContentResolver 来访问内容提供者提供的数据。

ContentResolver resolver = getContentResolver();
Cursor cursor = resolver.query(Uri.parse("content://com.example.provider/users"), null, null, null, null);

1.4 数据删除

ContentResolver resolver = getContentResolver();
int rowsDeleted = resolver.delete(Uri.parse("content://com.example.provider/users"), "id=?", new String[]{"1"});

2. 运行时动态权限申请

2.1 Lazy模式

Lazy模式指的是只在实际需要权限的时候才请求权限,而不是一开始就请求。这样可以避免过早地请求用户权限,提升用户体验。

示例: 比如我们在应用启动时并不立即请求存储权限,而是等到用户点击“上传文件”按钮时,再请求存储权限。

// 检查存储权限是否已获得
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) 
        != PackageManager.PERMISSION_GRANTED) {
    
    // 权限未授予,申请权限
    ActivityCompat.requestPermissions(this, 
            new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 
            REQUEST_CODE_STORAGE);
} else {
    // 权限已授予,进行文件读取操作
    readFile();
}

2.2 Hungry模式

与 Lazy 模式相反,Hungry 模式是指在应用启动时就请求所有需要的权限,无论用户是否真正需要。

虽然这种方式能确保所有权限在应用启动时都得到授权,但可能会引起用户的不适,因为他们会被迫在应用启动时看到多个权限请求。

示例:

// 一开始就请求所有需要的权限
ActivityCompat.requestPermissions(this,
        new String[]{
                Manifest.permission.READ_EXTERNAL_STORAGE,
                Manifest.permission.ACCESS_FINE_LOCATION
        },
        REQUEST_CODE_ALL_PERMISSIONS);

3. 联系人与短信

3.1 添加联系人

在 Android 中,可以通过 ContentResolver 来操作联系人数据库,添加新的联系人。

示例:

ContentValues values = new ContentValues();
values.put(ContactsContract.RawContacts.ACCOUNT_TYPE, null);
values.put(ContactsContract.RawContacts.ACCOUNT_NAME, null);
Uri rawContactUri = getContentResolver().insert(ContactsContract.RawContacts.CONTENT_URI, values);

long rawContactId = ContentUris.parseId(rawContactUri);

// 添加电话号码
values.clear();
values.put(ContactsContract.Data.RAW_CONTACT_ID, rawContactId);
values.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE);
values.put(ContactsContract.CommonDataKinds.Phone.NUMBER, "1234567890");
values.put(ContactsContract.CommonDataKinds.Phone.TYPE, ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE);
getContentResolver().insert(ContactsContract.Data.CONTENT_URI, values);

3.2 批处理添加联系人

批量处理可以通过迭代多次插入联系人数据来实现。需要注意的是,批处理操作通常在后台线程中执行,以避免主线程阻塞。

3.3 查询联系人

查询联系人可以通过 ContentResolver 访问联系人数据库,并使用 Cursor 来处理查询结果。

示例:

Cursor cursor = getContentResolver().query(
        ContactsContract.CommonDataKinds.Phone.CONTENT_URI, 
        new String[]{ContactsContract.CommonDataKinds.Phone.NUMBER, 
                     ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME},
        null, null, null);

while (cursor.moveToNext()) {
    String name = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
    String number = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
    Log.d("Contact", "Name: " + name + ", Phone: " + number);
}
cursor.close();

3.4 监听短信内容

可以使用 BroadcastReceiver 监听并接收新的短信,确保用户接收到短信时应用能够做出响应。

示例:

BroadcastReceiver smsReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        if (Telephony.Sms.Intents.SMS_RECEIVED_ACTION.equals(action)) {
            // 获取短信内容
            Bundle bundle = intent.getExtras();
            Object[] pdus = (Object[]) bundle.get("pdus");
            for (Object pdu : pdus) {
                SmsMessage message = SmsMessage.createFromPdu((byte[]) pdu);
                String messageBody = message.getMessageBody();
                String sender = message.getOriginatingAddress();
                Log.d("SMS", "Sender: " + sender + " Message: " + messageBody);
            }
        }
    }
};
IntentFilter filter = new IntentFilter(Telephony.Sms.Intents.SMS_RECEIVED_ACTION);
registerReceiver(smsReceiver, filter);

4. 其他功能实现

4.1 跳转到相册选择图片

可以通过 Intent 启动相册应用,并获取用户选择的图片。

示例:

Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
startActivityForResult(intent, REQUEST_CODE_PICK_IMAGE);

onActivityResult() 方法中获取用户选择的图片:

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == REQUEST_CODE_PICK_IMAGE && resultCode == RESULT_OK) {
        Uri selectedImageUri = data.getData();
        // 使用 selectedImageUri 来加载图片
    }
}

4.2 发送彩信

发送彩信通常通过 Intent 来启动系统的彩信应用。

Intent intent = new Intent(Intent.ACTION_SEND);
intent.putExtra(Intent.EXTRA_STREAM, Uri.parse("file://path_to_image"));
intent.setType("image/jpeg");
startActivity(intent);

4.3 通过MediaStore查询图片

MediaStore 提供了一个接口来访问存储在设备上的多媒体文件,如图片、音频和视频。

示例:

Cursor cursor = getContentResolver().query(
        MediaStore.Images.Media.EXTERNAL_CONTENT_URI, 
        new String[]{MediaStore.Images.Media._ID, MediaStore.Images.Media.DATA},
        null, null, null);
while (cursor.moveToNext()) {
    String path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
    Log.d("ImagePath", "Path: " + path);
}
cursor.close();

4.4 FileProvider

FileProvider 是 Android 提供的一个工具类,用于为应用共享文件提供安全的方式。

它可以将应用私有的文件提供给其他应用使用,避免了直接暴露文件路径的安全问题。

配置 FileProviderAndroidManifest.xml 中配置 FileProvider

<provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="com.example.myapp.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
</provider>

res/xml/file_paths.xml 中配置文件路径:

<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path
        name="external_files"
        path="." />
</paths>

通过 FileProvider 获取文件的 URI:

File file = new File(getExternalFilesDir(null), "image.jpg");
Uri fileUri = FileProvider.getUriForFile(this, "com.example.myapp.fileprovider", file);

4.5 应用安装

通过 Intent 启动应用安装流程,用户可以选择是否安装 APK 文件。

Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.parse("file://path_to_apk"), "application/vnd.android.package-archive");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);

七、列表与视图管理

1. 列表视图

1.1 下拉列表

下拉列表通常是通过 Spinner 控件实现的,它允许用户从预定义的选项中选择一个项。

Spinner 是一个非常常见的 UI 控件,类似于 HTML 中的 <select> 元素。

使用示例:

// 获取 Spinner 控件
Spinner spinner = findViewById(R.id.spinner);

// 创建一个简单的适配器来为 Spinner 提供数据
ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, items);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);

// 设置适配器
spinner.setAdapter(adapter);

// 设置 Spinner 的监听器
spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
    @Override
    public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
        // 处理选中项
    }

    @Override
    public void onNothingSelected(AdapterView<?> parent) {
        // 处理未选中任何项的情况
    }
});

在这个例子中,ArrayAdapter 是用于填充下拉列表的数据适配器。setOnItemSelectedListener 用于监听用户的选择。

1.2 SimpleAdapter

SimpleAdapter 是一个适配器,用于将一个 ListMap 数据源绑定到 ListViewGridView 中。它支持数据的展示和简化。

使用示例:

// 准备数据
List<Map<String, String>> data = new ArrayList<>();
Map<String, String> map = new HashMap<>();
map.put("name", "John");
map.put("age", "25");
data.add(map);

// 使用 SimpleAdapter 来展示数据
SimpleAdapter adapter = new SimpleAdapter(
        this,
        data,
        R.layout.item_layout,  // 视图布局
        new String[]{"name", "age"},  // 显示数据的字段
        new int[]{R.id.name, R.id.age}  // 视图组件对应的ID
);
ListView listView = findViewById(R.id.listView);
listView.setAdapter(adapter);

这里 SimpleAdapter 允许从一个 List<Map<String, Object>> 数据源中获取数据,并将其映射到对应的视图中。

1.3 BaseAdapter

BaseAdapter 是 Android 中一个自定义适配器的基类,用来处理更复杂的列表或网格数据。

BaseAdapter 提供了更多自定义方法,如 getView() 方法,允许开发者自定义每个列表项的布局。

使用示例:

public class MyAdapter extends BaseAdapter {

    private Context context;
    private List<String> data;

    public MyAdapter(Context context, List<String> data) {
        this.context = context;
        this.data = data;
    }

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

    @Override
    public Object getItem(int position) {
        return data.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        if (convertView == null) {
            // 如果 convertView 为 null,则创建新的视图
            convertView = LayoutInflater.from(context).inflate(R.layout.item_layout, parent, false);
        }

        TextView textView = convertView.findViewById(R.id.textView);
        textView.setText(data.get(position));

        return convertView;
    }
}

BaseAdaptergetView() 方法是自定义视图和数据绑定的地方,开发者可以根据自己的需求灵活定制。

1.4 convertView 复用

ListViewGridView 中,当你滚动列表时,系统会不断地复用之前加载的 View,这就叫做视图复用。

convertView 就是指系统缓存的视图对象,通过它可以避免重复创建视图,从而提高性能。

示例:

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    if (convertView == null) {
        convertView = LayoutInflater.from(context).inflate(R.layout.item_layout, parent, false);
    }

    TextView textView = convertView.findViewById(R.id.textView);
    textView.setText(data.get(position));

    return convertView;
}

convertViewnull 时,说明这是一个新的视图,需要重新创建。

如果不是 null,则说明这是一个已经复用的视图,直接使用它,从而避免了不必要的资源开销。

1.5 列表视图 ListView

ListView 是 Android 中最常用的控件之一,用于显示大量数据的列表。

它支持垂直滚动,可以与不同的适配器配合使用(如 ArrayAdapterSimpleAdapterBaseAdapter)。

ListView 通过滑动和视图复用机制来优化性能,避免了一次性加载大量数据。

使用示例:

ListView listView = findViewById(R.id.listView);
BaseAdapter adapter = new MyAdapter(this, data);
listView.setAdapter(adapter);

通过设置适配器,ListView 就可以展示一个数据列表。用户滑动时,ListView 会复用视图来提高性能。

1.6 ListView 条目事件冲突

ListView 的条目点击事件和滚动事件可能会发生冲突,尤其是在需要处理子控件点击事件时。

例如,在 ListView 中的 ImageViewButton 点击时,可能会导致列表滚动。

解决方法:

可以使用 setOnTouchListenersetOnItemClickListener 来区分点击区域和滑动区域,避免事件冲突。

listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        // 处理条目点击事件
    }
});

此外,可以通过监听 MotionEvent 来拦截触摸事件,从而实现更精细的控制。

1.7 使用 ListView 对购物车升级

假设你正在开发一个电商应用,需要展示购物车中的商品,ListView 可以用来展示每个商品条目,

并且可以为每个商品条目设置点击事件进行商品的删除或修改数量。

示例:

public class CartAdapter extends BaseAdapter {
    private List<CartItem> items;

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

    @Override
    public Object getItem(int position) {
        return items.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        if (convertView == null) {
            convertView = LayoutInflater.from(context).inflate(R.layout.cart_item, parent, false);
        }

        TextView name = convertView.findViewById(R.id.productName);
        Button deleteButton = convertView.findViewById(R.id.deleteButton);

        CartItem item = items.get(position);
        name.setText(item.getName());

        deleteButton.setOnClickListener(v -> {
            // 删除购物车中的商品
            items.remove(position);
            notifyDataSetChanged();  // 更新视图
        });

        return convertView;
    }
}

在这个例子中,我们为每个商品设置了一个删除按钮,用户可以通过点击按钮从购物车中移除商品,

并通过 notifyDataSetChanged() 更新 ListView

2. 网格视图与翻页视图

2.1 网格视图 GridView

GridView 是一种用于显示网格状布局的视图,可以用于展示图片、商品等。它和 ListView 类似,但支持多列布局。

使用示例:

GridView gridView = findViewById(R.id.gridView);
BaseAdapter adapter = new MyGridAdapter(this, data);
gridView.setAdapter(adapter);

通过设置适配器,GridView 会根据数据源展示网格。通常,GridView 用于显示图片、图标等具有相同尺寸的项。

2.2 使用 GridView 对购物车升级

类似于 ListViewGridView 也可以用来展示购物车商品,但它适合以网格方式显示商品的缩略图。

2.3 翻页视图 ViewPager

ViewPager 是一个非常有用的控件,用于在多个页面之间进行切换,常用于实现图片轮播、引导页、新闻资讯等功能。

ViewPager 允许水平滑动,可以动态加载不同的页面内容。

基本使用示例:

ViewPager viewPager = findViewById(R.id.viewPager);
MyPagerAdapter adapter = new MyPagerAdapter(getSupportFragmentManager());
viewPager.setAdapter(adapter);

这里我们创建了一个 ViewPager,并设置了一个适配器 MyPagerAdapter 来管理不同的页面。

通常,适配器会使用 FragmentPagerAdapterFragmentStatePagerAdapter 来提供每个页面的内容。

实现细节:

  • FragmentPagerAdapter 用于持有少量页面,当页面不再显示时,ViewPager 会自动销毁它们。
  • FragmentStatePagerAdapter 用于持有大量页面,当页面不再显示时,ViewPager 会销毁页面,并在需要时重新创建。
public class MyPagerAdapter extends FragmentStatePagerAdapter {
    public MyPagerAdapter(FragmentManager fm) {
        super(fm);
    }

    @Override
    public Fragment getItem(int position) {
        // 根据不同的位置返回不同的Fragment
        return new MyFragment();
    }

    @Override
    public int getCount() {
        return 5;  // 页面数量
    }
}

这里,getItem() 方法返回当前页面的 FragmentgetCount() 方法返回页面的总数。你可以根据需要填充页面内容。

2.4 翻页标签栏 PagerTabStrip

PagerTabStrip 是一个视图标签栏,它与 ViewPager 配合使用,提供页面之间的滑动指示器。

它常用于页面之间切换时,显示当前页和总页数的导航条。

基本使用示例:

ViewPager viewPager = findViewById(R.id.viewPager);
PagerTabStrip pagerTabStrip = findViewById(R.id.pagerTabStrip);
MyPagerAdapter adapter = new MyPagerAdapter(getSupportFragmentManager());

viewPager.setAdapter(adapter);
pagerTabStrip.setViewPager(viewPager);

PagerTabStrip 会自动与 ViewPager 配合,显示当前页、总页数等信息,并且支持滑动指示器的效果。

你也可以通过修改 PagerTabStrip 的样式来定制标签栏的外观。

2.5 ViewPager 实现启动引导页

在很多应用程序中,启动时常常显示一个引导页,通过 ViewPager 可以实现这一效果。

每个页面展示一个引导图或介绍,用户滑动页面完成引导。步骤:

  1. 创建一个布局文件来展示每个引导页面。
  2. 使用 ViewPager 展示这些页面。
  3. 在最后一个页面设置跳转或结束引导的操作。

代码示例:

public class GuideActivity extends AppCompatActivity {

    private ViewPager viewPager;
    private GuidePagerAdapter adapter;

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

        viewPager = findViewById(R.id.viewPager);
        adapter = new GuidePagerAdapter(getSupportFragmentManager());
        viewPager.setAdapter(adapter);
    }
}

class GuidePagerAdapter extends FragmentPagerAdapter {

    public GuidePagerAdapter(FragmentManager fm) {
        super(fm);
    }

    @Override
    public Fragment getItem(int position) {
        // 返回不同的引导页Fragment
        return GuideFragment.newInstance(position);
    }

    @Override
    public int getCount() {
        return 3;  // 设置引导页的数量
    }
}

在这个示例中,我们通过 ViewPager 加载不同的 Fragment 来显示每一页的引导内容。

用户滑动页面时可以浏览不同的引导页,最后一页可以设置跳转到主页面。

3. Fragment 使用

3.1 Fragment 静态注册

静态注册是指在 AndroidManifest.xml 中声明一个 Fragment,使其在运行时能自动被加载和初始化。

这通常是在一些固定场景中使用,例如固定的启动页或静态的页面结构。

静态注册并不需要手动管理 FragmentTransaction,而是直接通过 FragmentActivity 的相关方法来加载。

示例:

<activity android:name=".MainActivity">
    <fragment
        android:name="com.example.MyFragment"
        android:id="@+id/myFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</activity>

在这种情况下,Fragment 会被自动加载和管理。

3.2 Fragment 生命周期

Fragment 有自己的生命周期,通常与它所在的 Activity 一起存在和销毁。Fragment 生命周期包括以下几个重要阶段:

  • onAttach(Context context): FragmentActivity 关联时调用。
  • onCreate(Bundle savedInstanceState): Fragment 被创建时调用。
  • onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState): Fragment 视图创建时调用。
  • onActivityCreated(Bundle savedInstanceState): ActivityonCreate() 方法调用完毕后调用。
  • onStart()、onResume(): Fragment 可见时调用。
  • onPause()、onStop(): Fragment 不可见时调用。
  • onDestroyView(): Fragment 的视图被销毁时调用。
  • onDetach(): FragmentActivity 断开关联时调用。

理解这些生命周期方法可以帮助你更好地管理 Fragment 中的资源和操作。

3.3 Fragment 动态注册

动态注册指的是通过代码来动态添加、移除或替换 Fragment

这是 Android 中更常见的做法,可以根据用户的行为或者应用的状态动态地加载不同的 Fragment

示例:

FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
transaction.replace(R.id.fragment_container, new MyFragment());
transaction.addToBackStack(null);  // 可以选择是否将事务添加到返回栈
transaction.commit();

通过这种方式,我们可以在 Activity 中的任意时刻替换当前显示的 Fragment

3.4 使用 Fragment 改进启动引导页

通过使用 Fragment,你可以将每一页引导内容分成多个 Fragment

然后使用 ViewPagerFragmentTransaction 来切换这些 Fragment,实现更加灵活的引导页展示。

示例:

public class GuideFragment extends Fragment {
    private int position;

    public static GuideFragment newInstance(int position) {
        GuideFragment fragment = new GuideFragment();
        Bundle args = new Bundle();
        args.putInt("position", position);
        fragment.setArguments(args);
        return fragment;
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        position = getArguments().getInt("position");
        // 根据 position 加载不同的引导内容
        return inflater.inflate(R.layout.fragment_guide, container, false);
    }
}

在这个例子中,每个引导页面都是一个 Fragment,根据 position 参数动态展示不同的引导内容。

3.5 Fragment 传递数据

在 Android 中,Fragment 之间的数据传递是一个常见的问题。

有两种主要的方式来实现这一点:通过 Bundle 或者使用共享的 ViewModel

3.5.1 使用 Bundle 传递数据

Fragment 可以通过 Bundle 来传递数据。

Bundle 是一种轻量级的对象,可以在 Fragment 之间传递基本数据类型(如字符串、整数、布尔值等)。

代码示例:

// 传递数据到Fragment
Bundle bundle = new Bundle();
bundle.putString("key", "value");  // 通过Bundle传递数据
Fragment fragment = new MyFragment();
fragment.setArguments(bundle);

// 在Fragment中获取数据
String value = getArguments().getString("key");

在这个例子中,我们通过 Bundle 将一个字符串传递到 Fragment

可以通过 getArguments() 方法在目标 Fragment 中访问这些数据。

3.5.2 使用 ViewModel 传递数据

在 Android 架构组件中,ViewModel 是用于管理界面相关数据的类,它可以在多个 FragmentActivity 之间共享数据。ViewModel 的优势在于数据能够在配置变化(如旋转屏幕)后保持。

代码示例:

// 在一个Fragment中创建ViewModel
public class MyFragment extends Fragment {
    private MyViewModel viewModel;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        viewModel = new ViewModelProvider(requireActivity()).get(MyViewModel.class);
    }

    // 在Fragment中获取ViewModel的数据
    public void setData(String data) {
        viewModel.setData(data);
    }
}

// 创建一个共享的ViewModel
public class MyViewModel extends ViewModel {
    private MutableLiveData<String> data = new MutableLiveData<>();

    public LiveData<String> getData() {
        return data;
    }

    public void setData(String newData) {
        data.setValue(newData);
    }
}

在这个示例中,MyViewModel 被多个 Fragment 共享。

Fragment 可以通过 ViewModelProvider 获取 ViewModel 的实例并通过它共享数据。

4. RecyclerView 和适配器模式

4.1 RecyclerView 基本使用

RecyclerView 是 Android 提供的一个强大的控件,用于高效显示大量的数据。

它比 ListView 更加灵活,并且支持多种布局管理器,如线性布局、网格布局等。

RecyclerViewAdapterViewHolder 配合使用,通过适配器模式将数据绑定到视图上。

代码示例:

RecyclerView recyclerView = findViewById(R.id.recyclerView);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
MyAdapter adapter = new MyAdapter(myData);
recyclerView.setAdapter(adapter);

在这个示例中,我们创建了一个 RecyclerView,并通过 LinearLayoutManager 设置了线性布局。

MyAdapter 是自定义的适配器,用于将数据绑定到视图。

4.2 自定义 RecyclerView 适配器

为了展示数据,RecyclerView 需要一个适配器来将数据与视图绑定。

适配器的核心是 onCreateViewHolder()onBindViewHolder()getItemCount() 方法。

代码示例:

public class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyViewHolder> {

    private List<String> dataList;

    public MyAdapter(List<String> dataList) {
        this.dataList = dataList;
    }

    @Override
    public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_view, parent, false);
        return new MyViewHolder(view);
    }

    @Override
    public void onBindViewHolder(MyViewHolder holder, int position) {
        holder.textView.setText(dataList.get(position));
    }

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

    public static class MyViewHolder extends RecyclerView.ViewHolder {
        TextView textView;

        public MyViewHolder(View itemView) {
            super(itemView);
            textView = itemView.findViewById(R.id.textView);
        }
    }
}

我们通过 MyAdapter 类绑定了一个字符串列表,并通过 onBindViewHolder() 方法将每个项的数据绑定到对应的视图上。

4.3 RecyclerView 支持不同的布局

RecyclerView 具有灵活的布局管理器,可以显示不同的布局,如线性布局、网格布局和瀑布流布局。

代码示例:

// 设置为网格布局
recyclerView.setLayoutManager(new GridLayoutManager(this, 2));  // 2列

// 设置为瀑布流布局
recyclerView.setLayoutManager(new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL));

在这里,我们通过 GridLayoutManager 设置了网格布局,显示两列;

通过 StaggeredGridLayoutManager 设置了瀑布流布局,用于展示不规则的内容。


5. 常用的 UI 控件及其使用方法

5.1 Button 和点击事件

在 Android 中,Button 是最常用的交互控件之一。你可以为 Button 设置点击事件来响应用户的操作。

代码示例:

Button button = findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Toast.makeText(MainActivity.this, "Button clicked!", Toast.LENGTH_SHORT).show();
    }
});

在这里,我们通过 setOnClickListener()Button 设置了点击事件,点击时会弹出一个 Toast 提示。

5.2 TextView 和样式

TextView 用于显示文本。你可以设置其内容、样式(如字体大小、颜色、行间距等),并通过 setText() 方法修改其文本。

代码示例:

TextView textView = findViewById(R.id.textView);
textView.setText("Hello, World!");
textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 18);
textView.setTextColor(Color.RED);

这里,我们为 TextView 设置了文本内容、字体大小和文本颜色。

5.3 EditText 和获取用户输入

EditText 是一个用于接收用户输入的控件,通常用于实现用户登录、搜索框等功能。你可以通过 getText() 方法获取输入的内容。

代码示例:

EditText editText = findViewById(R.id.editText);
String input = editText.getText().toString();

在这个代码中,我们通过 getText() 获取了 EditText 中的用户输入。

6. 动态权限请求(Runtime Permissions)

从 Android 6.0 (API 23) 开始,Android 引入了动态权限请求的机制。

这意味着在应用运行时,你必须请求用户授权才能访问某些权限(如读取存储、访问摄像头等)。

6.1 请求权限

在 Android 中,你可以使用 ActivityCompat.requestPermissions() 来请求权限。

代码示例:

if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
        != PackageManager.PERMISSION_GRANTED) {
    ActivityCompat.requestPermissions(this,
            new String[]{Manifest.permission.CAMERA}, 1);
}

在这个代码中,我们首先检查是否已经获得了 CAMERA 权限,如果没有,使用 requestPermissions() 请求权限。

6.2 处理权限结果

当用户做出响应时,系统会调用 onRequestPermissionsResult() 方法。

代码示例:

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
                                       @NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    if (requestCode == 1) {
        if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            // 权限请求成功
            Toast.makeText(this, "Permission granted!", Toast.LENGTH_SHORT).show();
        } else {
            // 权限请求失败
            Toast.makeText(this, "Permission denied!", Toast.LENGTH_SHORT).show();
        }
    }
}

通过 onRequestPermissionsResult() 方法,你可以根据权限请求的结果执行不同的操作。

八、广播与定时器

1. 广播机制

广播是一种信息传递机制,允许一个应用程序发送消息给其他应用程序或系统组件。

广播是一种消息传递的机制,允许应用程序向系统或其他应用程序发送信息,而其他应用程序可以接收并处理这些消息。

1.1 什么是广播

广播是 Android 中一种基于事件的通信方式,

它可以让不同的应用程序或同一应用的不同组件(如 Activity、Service 等)在不直接关联的情况下交换信息。

广播的发送者称为“广播源”,接收者称为“广播接收器”。广播有两种主要类型:标准广播有序广播

广播的常见用途包括:

  • 系统事件(如网络状态变化、时间变化等)的通知。
  • 应用程序之间的消息传递。
  • 内部事件(如音乐播放器播放状态变化等)的通知。

1.2 标准广播

标准广播是最简单的广播类型,它是异步的,广播会被同时发送到所有注册的接收器,接收器的顺序是不确定的。

代码示例

发送广播:

Intent intent = new Intent("com.example.broadcast.MY_NOTIFICATION");
intent.putExtra("message", "Hello, this is a standard broadcast!");
sendBroadcast(intent);

注册广播接收器:

BroadcastReceiver receiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        String message = intent.getStringExtra("message");
        Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
    }
};

IntentFilter filter = new IntentFilter("com.example.broadcast.MY_NOTIFICATION");
registerReceiver(receiver, filter);

在这个示例中,广播发送方通过 sendBroadcast 发送了一条消息,而接收方通过 BroadcastReceiver 注册并接收广播,显示消息。

1.3 有序广播

有序广播允许广播按照指定的顺序发送。

接收者可以在接收到广播后,决定是否中止广播的传播,或者将广播传递给下一个接收者。

有序广播适用于需要逐步处理广播并且允许控制处理流程的场景。

代码示例

发送有序广播:

Intent intent = new Intent("com.example.broadcast.ORDERED_NOTIFICATION");
intent.putExtra("message", "This is an ordered broadcast.");
sendOrderedBroadcast(intent, null);

接收有序广播:

BroadcastReceiver orderedReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        String message = intent.getStringExtra("message");
        Toast.makeText(context, "Received ordered broadcast: " + message, Toast.LENGTH_SHORT).show();
        
        // 你可以选择中止广播的传递
        abortBroadcast();
    }
};

IntentFilter filter = new IntentFilter("com.example.broadcast.ORDERED_NOTIFICATION");
registerReceiver(orderedReceiver, filter);

通过 abortBroadcast(),接收器可以停止广播的进一步传播。

1.4 广播的静态注册

广播接收器可以通过在 AndroidManifest.xml 文件中静态注册。

静态注册广播接收器在应用启动时即加载,因此能够接收到应用在启动前就发生的广播(例如,系统广播)。

代码示例

AndroidManifest.xml 中注册广播接收器:

<receiver android:name=".MyBroadcastReceiver">
    <intent-filter>
        <action android:name="com.example.broadcast.MY_NOTIFICATION" />
    </intent-filter>
</receiver>

然后,在 MyBroadcastReceiver 中处理接收到的广播:

public class MyBroadcastReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        String message = intent.getStringExtra("message");
        Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
    }
}

这种方式适用于系统广播或应用间的长期监听(如电池状态、网络变化等)。

1.5 系统分钟到达广播

Android 系统每分钟都会发送一次广播,通知所有应用当前的时间已经到达。这是一个特殊的系统广播,常用于定时任务。

代码示例

<receiver android:name=".MinuteReceiver">
    <intent-filter>
        <action android:name="android.intent.action.TIME_TICK" />
    </intent-filter>
</receiver>

TIME_TICK 是一个系统广播,表示每分钟的时间到达。

public class MinuteReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        // 执行定时任务
        Log.d("MinuteReceiver", "A new minute has passed.");
    }
}

这种广播用于接收每分钟触发的事件,可以用于计时、自动同步等操作。

1.6 系统网络变更广播

Android 系统会在网络状态发生变化时发送一个广播,应用可以通过监听这个广播来实时获取网络状态。

代码示例

<receiver android:name=".NetworkChangeReceiver">
    <intent-filter>
        <action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
    </intent-filter>
</receiver>

然后在 NetworkChangeReceiver 中处理广播:

public class NetworkChangeReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
        boolean isConnected = activeNetwork != null && activeNetwork.isConnectedOrConnecting();
        if (isConnected) {
            Log.d("NetworkChangeReceiver", "Network connected");
        } else {
            Log.d("NetworkChangeReceiver", "Network disconnected");
        }
    }
}

这种广播用于监听网络变化,如连接与断开,可以在网络状态变化时执行一些必要的操作(如自动重连或通知用户)。

2. 定时器与任务管理

在 Android 中,定时器(如 TimerScheduledExecutorService)是执行定时任务的重要工具。

通过它们可以在指定的时间间隔后执行任务。

2.1 使用 Timer 执行定时任务

Timer 类是一个简单的定时器,可以用来执行周期性任务或延时任务。

你可以通过 schedule()scheduleAtFixedRate() 方法来指定任务的执行时间和周期。

代码示例

Timer timer = new Timer();
timer.schedule(new TimerTask() {
    @Override
    public void run() {
        // 执行定时任务
        Log.d("TimerTask", "Task executed at: " + System.currentTimeMillis());
    }
}, 1000, 5000); // 延时1秒执行,然后每5秒执行一次

在这个例子中,Timer 每隔 5 秒执行一次任务。

2.2 使用 ScheduledExecutorService 执行定时任务

ScheduledExecutorService 是一个更强大的定时任务执行工具,支持更灵活的任务调度和并发执行。

代码示例

ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(new Runnable() {
    @Override
    public void run() {
        Log.d("ScheduledExecutor", "Task executed at: " + System.currentTimeMillis());
    }
}, 1, 5, TimeUnit.SECONDS);

这种方式比 Timer 更加高效,特别是在需要高并发执行多个定时任务时。

3. 竖屏与横屏切换

竖屏与横屏切换主要涉及Android设备屏幕的方向管理、布局调整以及对用户操作的响应。

在不同的屏幕方向下,Android应用的界面布局可能会有所变化,因此理解如何处理竖屏与横屏的切换非常重要。

3.1 自动旋转与屏幕方向管理

Android设备通常会根据物理传感器(如重力感应器)来自动判断屏幕方向并进行旋转。

默认情况下,应用会随着设备的旋转自动切换竖屏和横屏。

1. 检测屏幕方向

你可以通过以下方法获取当前的屏幕方向:

// 获取屏幕方向
int orientation = getResources().getConfiguration().orientation;

if (orientation == Configuration.ORIENTATION_PORTRAIT) {
    // 竖屏模式
} else if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
    // 横屏模式
}
2. 强制设置屏幕方向

如果你想强制某个Activity始终保持在竖屏或横屏模式,可以通过android:screenOrientation属性进行设置:

  • 在AndroidManifest.xml中强制设置为竖屏
<activity android:name=".MainActivity"
    android:screenOrientation="portrait">
</activity>
  • 在AndroidManifest.xml中强制设置为横屏
<activity android:name=".MainActivity"
    android:screenOrientation="landscape">
</activity>

你还可以在代码中动态设置屏幕方向:

// 强制设置当前Activity为横屏
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);

// 强制设置当前Activity为竖屏
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
3. 处理方向变化时的资源配置

当屏幕方向变化时,应用的布局可能会发生变化。为了适应不同的屏幕方向,可以使用不同的布局资源文件。

  • 竖屏布局:res/layout/activity_main.xml
  • 横屏布局:res/layout-land/activity_main.xml

Android会根据设备的方向自动加载适合的布局。

4. 屏幕方向变化时保存和恢复数据

当设备方向发生变化时,Activity 会被销毁并重新创建。

为了防止丢失数据,可以重写 onSaveInstanceStateonRestoreInstanceState 方法来保存和恢复数据。

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putString("data_key", "保存的数据");
}

@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
    super.onRestoreInstanceState(savedInstanceState);
    String restoredData = savedInstanceState.getString("data_key");
}

3.2 横屏模式下的特殊需求

横屏模式通常用于需要更大视野的应用场景,如视频播放、游戏等。你可能需要特别处理横屏模式下的资源加载与布局,例如:

  • 调整视图大小:在横屏模式下,视图元素可能需要更大的显示区域,布局的调整可以通过不同的资源文件来实现。

  • 使用ConstraintLayout来适配不同方向ConstraintLayout是一个强大的布局容器,

    可以帮助你在竖屏与横屏模式下灵活地控制视图元素的位置和大小。

4. 回到桌面与任务切换

在Android中,用户经常会在多个应用和任务之间进行切换。理解如何处理回到桌面任务切换可以帮助开发者优化应用的用户体验。

4.1 回到桌面

回到桌面是指用户退出当前应用,回到Android主屏幕。

Android提供了多种方式来控制应用是否允许返回桌面,以及如何控制应用的生命周期。

1. 用户通过Home按钮回到桌面

通常,用户通过按下设备的Home键或者使用导航手势来返回主屏幕。

这个操作会将当前应用置于后台。为了优化应用的生命周期,开发者可以通过以下方法处理应用的启动和关闭。

  • 退出应用时保存状态:你可以重写onPause()onStop()onDestroy()方法,保存应用状态,以便恢复时更好地提供用户体验。
@Override
protected void onPause() {
    super.onPause();
    // 在此保存数据或做一些必要的资源释放
}

@Override
protected void onStop() {
    super.onStop();
    // 释放一些较重的资源,避免不必要的内存消耗
}

@Override
protected void onDestroy() {
    super.onDestroy();
    // 清理资源
}
2. 使用Intent跳转到桌面

如果你想通过代码直接返回桌面,可以使用以下代码:

Intent intent = new Intent(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_HOME);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);

这段代码会让应用跳转到Android主屏幕。

4.2 任务切换

Android的任务管理是基于栈(Stack)的,用户通过任务切换可以快速在多个任务间切换。

每个应用对应一个或多个任务(Task),任务栈的管理和操作通常是由Android系统自动完成的。

1. 查看任务切换视图

Android通过任务切换视图展示所有正在运行的应用。用户可以通过任务切换按钮或手势(如从屏幕底部向上滑动)来查看和切换任务。

2. 使用TaskStackBuilder管理任务

TaskStackBuilder是一种管理任务栈的工具,它允许开发者在启动Activity时加入任务栈的上下文,使得任务切换更加灵活。

TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);
stackBuilder.addParentStack(NextActivity.class);  // 添加父任务
stackBuilder.addNextIntent(new Intent(this, NextActivity.class));  // 添加当前任务
stackBuilder.startActivities();

这样,当你启动新的Activity时,它会自动将新任务压入栈顶,同时保留其他任务的上下文。

3. 后台应用的生命周期

当用户切换到另一个应用时,当前应用可能会进入后台状态。

Android系统会根据应用的优先级来管理后台应用的资源,例如停止一些不必要的服务或活动。

为了提高性能和节省资源,你可以通过onPause()onStop()等方法来处理后台应用的行为。

@Override
protected void onPause() {
    super.onPause();
    // 执行暂停或保存数据的操作
}

@Override
protected void onStop() {
    super.onStop();
    // 执行后台清理操作或暂停服务等
}

4.3 应用在后台时的行为

在后台,应用会继续执行其服务、广播接收器和其他后台任务。

如果应用的任务是周期性的或需要定时执行,开发者可以使用定时器(如HandlerTimer)来定期执行任务。

例如,使用Handler来创建一个定时器:

Handler handler = new Handler();
Runnable runnable = new Runnable() {
    @Override
    public void run() {
        // 执行定时任务
    }
};

// 每隔1秒执行一次
handler.postDelayed(runnable, 1000);
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,417评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,921评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,850评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,945评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,069评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,188评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,239评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,994评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,409评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,735评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,898评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,578评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,205评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,916评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,156评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,722评论 2 363
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,781评论 2 351

推荐阅读更多精彩内容