安卓开发基础教程

第一章 UI布局

1.1线性布局LinearLayout

在Android开发中,LinearLayout 是一种非常常用的布局容器,它按照直线方式(水平或垂直)排列其子视图。LinearLayout 支持两个主要方向:水平方向(默认)和垂直方向。

基本属性

  • android:orientation:设置子视图的排列方向,可以是 horizontal(水平)或 vertical(垂直)。
  • android:gravity:设置子视图在布局中的位置,例如 centertopbottomleftright 等。
  • android:layout_widthandroid:layout_height:定义 LinearLayout 的大小,可以是 match_parentwrap_content 或具体的尺寸值。

子视图属性

  • android:layout_weight:当子视图的 layout_widthlayout_height 设置为 0dp 时,layout_weight 属性决定了子视图在剩余空间中所占的比例。
  • android:layout_gravity:设置子视图在其父容器中的位置,与 LinearLayoutgravity 属性类似。

示例代码

以下是一个简单的 LinearLayout 示例,它水平排列了两个按钮:

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:gravity="center_vertical">

    <Button
        android:id="@+id/button1"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:text="Button 1" />

    <Button
        android:id="@+id/button2"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:text="Button 2" />
</LinearLayout>

在这个例子中,两个按钮都设置了 layout_width0dplayout_weight1,这意味着它们将平分 LinearLayout 的宽度。

注意事项

  • LinearLayout 可以嵌套使用,但过度嵌套会导致布局复杂且性能下降,应尽量避免。
  • 当使用 LinearLayout 时,建议明确设置每个子视图的 layout_widthlayout_height,以避免不必要的布局重排。
  • 在设计响应式布局时,可以使用 weight 属性来分配空间,使得子视图能够根据屏幕大小动态调整大小。

1.2 相对布局RelativeLayout

RelativeLayout 是 Android 中一种灵活的布局方式,它允许开发者根据相对位置来定位子视图。与 LinearLayout 不同,RelativeLayout 不限制子视图的排列方向,子视图可以自由地放置在相对布局的任何位置。

基本属性

  • android:layout_widthandroid:layout_height:定义 RelativeLayout 的大小。
  • android:gravity:设置子视图在布局中的默认位置。

子视图属性

  • android:layout_alignParentTopandroid:layout_alignParentBottomandroid:layout_alignParentLeftandroid:layout_alignParentRight:使子视图与父布局的相应边缘对齐。
  • android:layout_centerInParent:使子视图在父布局中居中。
  • android:layout_aboveandroid:layout_below:使子视图放置在另一个视图的上方或下方。
  • android:layout_toLeftOfandroid:layout_toRightOf:使子视图放置在另一个视图的左边或右边。
  • android:layout_marginandroid:layout_marginLeftandroid:layout_marginRightandroid:layout_marginTopandroid:layout_marginBottom:设置子视图与父布局或兄弟视图之间的间距。

示例代码

以下是一个 RelativeLayout 的示例,展示了如何相对定位三个按钮:

<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    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:layout_alignParentTop="true"
        android:layout_centerHorizontal="true"
        android:text="Button 1" />

    <Button
        android:id="@+id/button2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/button1"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="20dp"
        android:text="Button 2" />

    <Button
        android:id="@+id/button3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/button2"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="20dp"
        android:text="Button 3" />
</RelativeLayout>

在这个例子中,button1 位于顶部中央,button2 位于 button1 的正下方,button3 位于 button2 的正下方。

注意事项

  • RelativeLayout 可以非常灵活地定位子视图,但过度使用或不当使用可能会导致布局结构复杂,影响性能。
  • 当布局变得复杂时,考虑使用 ConstraintLayout,它提供了更高级的布局功能,可以简化布局代码。
  • 确保合理使用边距和填充,以避免子视图之间的重叠和布局冲突。

RelativeLayout 是一个强大的布局工具,适用于需要精确控制子视图位置的场景。然而,随着 Android 布局系统的发展,ConstraintLayout 已经逐渐成为更受欢迎的选择,因为它提供了更简洁、更高效的布局解决方案。

1.3FrameLayout 布局

FrameLayout 是 Android 中最简单的布局容器之一,它按照文档顺序叠加子视图。FrameLayout 不提供任何特殊的对齐或分布功能,子视图只会按照它们在布局文件中出现的顺序堆叠在一起。

基本属性

  • android:layout_widthandroid:layout_height:定义 FrameLayout 的大小。
  • android:foregroundandroid:foregroundGravity:设置和控制布局的前景图像或重叠视图。
  • android:measureAllChildren:默认情况下,FrameLayout 只会测量第一个子视图。如果设置为 false,则会测量所有子视图。

子视图属性

  • android:layout_gravity:设置子视图在 FrameLayout 中的位置,例如 topbottomleftrightcenter 等。

示例代码

以下是一个 FrameLayout 的示例,它叠加了两个按钮:

<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    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_gravity="top|left" />

    <Button
        android:id="@+id/button2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Button 2"
        android:layout_gravity="bottom|right" />
</FrameLayout>

在这个例子中,button1 被放置在 FrameLayout 的左上角,而 button2 被放置在右下角。

注意事项

  • FrameLayout 通常用于叠加少量的视图,或者作为其他复杂布局的子视图容器。
  • 由于 FrameLayout 不提供自动的子视图对齐和分布功能,因此它在性能上通常优于 RelativeLayoutLinearLayout,特别是当布局只需要叠加少量视图时。
  • 在设计 UI 时,如果不需要子视图之间的对齐或分布,FrameLayout 是一个很好的选择,因为它的渲染速度快。
  • 由于 FrameLayout 会叠加子视图,因此需要确保子视图的 z-index(通过 android:elevation 属性设置)和 layout_gravity 属性正确设置,以避免视图重叠导致的可见性问题。

FrameLayout 是 Android 中最基础的布局之一,它的简单性使其在需要叠加视图或创建叠加层的场景中非常有用。

第二章 Material Design设计

Material Design(材料设计)是Google推出的一套设计语言,旨在为手机、平板电脑、网页和更多平台提供更一致、更广泛的设计体验。随着时间的推移,Material Design经历了几次更新,其中最新的是Material Design 3(MD3),它引入了更多个性化和动态的设计元素。

Material Design 3(MD3)的特点包括:

  1. 动态配色(Dynamic Color):MD3可以根据用户的壁纸颜色动态改变应用或小部件的主题,提供更加个性化的体验。这种动态配色是建立在ColorScheme基础上的,系统通过算法从当前壁纸中提取并更新ColorScheme。

  2. 新的配色方案:MD3引入了新的颜色槽,如PrimaryContainer和OnPrimaryContainer,它们的颜色更浅,用于不同重要度的组件上,以保持整体的协调性。

  3. 形状(Shape):在形状方面,MD3采用了新的分类方式,不再是按照组件尺寸分类,而是根据圆角的弧度进行分类,从NoneFull,为设计师提供了更多的表达方式。

  4. Material You:MD3也被称为Material You,它代表了Google对个性化设计的重视,旨在帮助开发者构建出色且富有表现力的应用。

  5. Jetpack Compose支持:MD3与Jetpack Compose紧密集成,使得开发者可以更轻松地在应用中实现Material Design 3的设计风格。

  6. 跨平台一致性:Material Design 3的设计原则和组件旨在跨多个平台提供一致的用户体验,包括Android、iOS、Web和Flutter。

使用案例:

  • 应用主题:开发者可以在应用中设置MD3主题,利用动态配色功能,使应用界面能够根据设备壁纸变化而自动调整配色方案。
  • 界面设计:利用MD3提供的新组件和指导原则,设计具有现代感和动感的界面。
  • 组件使用:在应用中使用MD3的组件,如按钮、卡片、对话框等,这些组件都遵循MD3的设计规范,确保一致性和美观性。

如何实现:

要在你的Android应用中实现Material Design 3,你可以使用Jetpack Compose或传统的XML布局方式。对于Jetpack Compose,你需要添加对应的依赖项,并使用新的MaterialTheme和其他Material组件来构建你的界面。

资源:

通过这些资源和工具,你可以为你的Android应用带来Material Design 3的现代和个性化设计。

MaterialButton案例

MaterialButton 是 Material Design 组件库中的一种按钮组件,它提供了丰富的视觉效果和交互反馈,符合 Material Design 的设计指南。MaterialButton 是在 Android Jetpack 的 Material Components 库中引入的,旨在提供更加一致和现代化的用户体验。

主要特点:

  1. 丰富的视觉效果MaterialButton 支持多种颜色和形状,包括填充色、边框色、阴影等,可以根据不同的使用场景和主题进行定制。
  2. 响应式反馈:当用户点击 MaterialButton 时,它会提供视觉反馈,如涟漪效果(ripple),增强用户的交互体验。
  3. 可定制性:可以通过 XML 属性或代码来定制按钮的大小、颜色、圆角、阴影等属性。
  4. 无障碍支持MaterialButton 内置了对无障碍功能的支持,如内容描述(content description)和无障碍服务的反馈。
  5. 主题一致性:在不同的主题(如浅色主题、深色主题)中,MaterialButton 能够自动适应,保持一致的视觉效果。

MaterialButton 属性:

  1. 背景颜色和主题

    • app:backgroundTint:设置按钮的背景颜色。
    • app:rippleColor:设置按下时水波纹效果的颜色。
  2. 形状和尺寸

    • app:cornerRadius:设置按钮的圆角半径。
    • app:strokeWidth:设置按钮边框的宽度(如果需要边框)。
    • app:strokeColor:设置按钮边框的颜色。
  3. 阴影和 elevation

    • app:elevation:设置按钮的阴影高度,影响立体感。
    • app:shapeAppearance:引用或设置一个 ShapeAppearance 资源,用于定义按钮的形状。
  4. 图标和文本

    • android:icon:设置按钮内的图标。
    • android:text:设置按钮内的文本。
    • android:textAppearance:设置文本的样式,如字体大小和颜色。
  5. 交互状态

    • app:iconGravity:设置图标相对于文本的位置。
    • app:iconPadding:设置图标和文本之间的间距。
  6. 无障碍性

    • android:contentDescription:设置按钮的无障碍内容描述。
  7. 其他行为

    • app:iconTint:设置图标的颜色。
    • app:iconSize:设置图标的大小。
  8. 特殊效果

    • app:backgroundTintMode:设置背景色混合模式。
    • app:rippleStyle:设置水波纹效果的样式。
  9. 大小和布局

    • android:layout_widthandroid:layout_height:设置按钮的宽度和高度。
    • android:minHeightandroid:minWidth:设置按钮的最小宽度和高度。
  10. 样式和主题

    • style:应用一个预定义的样式资源。

这些属性可以通过 XML 布局文件或在 Java/Kotlin 代码中动态设置。通过组合这些属性,你可以创建符合 Material Design 指南的丰富和一致的按钮。

使用MaterialButton

  1. 添加依赖:首先,确保你的项目中包含了 Material Components 库的依赖项。

    dependencies {
        implementation 'com.google.android.material:material:1.5.0' // 请使用最新版本
    }
    
  2. 在布局文件中添加 MaterialButton

    <com.google.android.material.button.MaterialButton
        android:id="@+id/materialButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Click Me"
        app:cornerRadius="20dp"
        app:backgroundTint="@color/colorPrimary" />
    
  3. 自定义样式

    <style name="Widget.MyApp.Button" parent="Widget.MaterialComponents.Button">
        <item name="backgroundTint">@color/colorPrimary</item>
        <item name="cornerRadius">20dp</item>
    </style>
    

    然后在布局文件中引用这个样式:

    <com.google.android.material.button.MaterialButton
        android:id="@+id/materialButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Click Me"
        style="@style/Widget.MyApp.Button" />
    
  4. 使用状态属性

    MaterialButton 支持状态属性,可以根据不同的交互状态(如按下、聚焦、激活等)改变按钮的外观。

注意事项:

  • 确保你的项目中使用了最新版本的 Material Components 库。
  • 在定制按钮样式时,注意保持与应用的整体设计风格一致。
  • 考虑到无障碍性,确保为按钮设置适当的内容描述。

MaterialButton 是 Material Design 实践中的重要组成部分,它不仅提供了美观的外观,还提供了良好的用户交互体验。

第三章 中级控件

.1图形定制

.1.1图形Drawable

.1.2形状图形

shape图形又称形状图形,用来描述常见的几何图形,包括矩形、圆用矩形、圆形、椭圆。用好图形
还可以节省美工不少工作量。
形状图形的定义文件放在drawable目录下,它是以shape标签为根节点的XML描述文件。根节点下定义了6个节点,分别是:size(尺寸)、stroke(描边)、corners(圆角)、solid(填充)、padding(间隔)、gradient(渐变),各节点的属性值主要是长宽、半径、角度以及颜色等。下面是形状图形各个节点及其属性的简要说明。

  1. shape(形状)

shape是形状图形文件的根节点,它描述了当前是哪种几何图形。下面是shape节点的常用属性说明。
•shape:字符串类型,表示图形的形状。形状类型的取值说明见表5-1。
表5-1

形状类型取值说明
rectangle矩形。默认值
oval椭圆。此时corners节点会失效
line直线。此时必须设置stroke节点,不然会报错
ring圆环
  1. size(尺寸)

size是shape的下级节点,它描述了形状图形的宽高尺寸。若无size节点,则表示宽高与宿主视图一样大小。下面是size节点的常用属性说
•height:像素类型,图形高度。

•width:像素类型,图形宽度。

  1. stroke(描边)

stroke是shape的下级节点,它描述了形状图形的描边规格。若无stroke节点,则表示不存在描边。下面是stroke节点的常用属性说明。

  • color:颜色类型,描边的颜色。

  • dashGap:像素类型,每段虚线之间的间隔

  • dashWidth:像素类型,每段虚线之间的宽度,若和dashGap中一个为0,则描边为实线

  • width:像素类型,描边宽度

  1. cornes(圆角)

cornes是shape的下级节点,它描述了形状图形的圆角。下面是常用属性

  • bottomLeftRadius:左下圆角半径
  • bottomRightRadius:左下圆角半径
  • radius:四个圆角的半径
  1. solid(填充)
  • color:内部填充颜色
  1. padding(与外部控件间隔)
  2. gradient(渐变)

gradient是shape的下级节点,它描述了形状图形的颜色渐变。若无gradient节点,则表示没有渐变效果。下面是gradient节点的常用属性

  • angel:渐变角度
  • type:渐变类型

.1.3 .9图片

.9.png 文件是一种特殊的图像资源格式,也称为九宫格图片或点九图。这种格式的图片允许开发者定义图像的可拉伸区域,使得图像可以在不同屏幕尺寸和分辨率的设备上适应性地缩放,而不会失真或变形。这对于创建能够适应多种屏幕尺寸的用户界面元素(如按钮、背景等)非常有用。

要制作.9.png文件,可以使用Android Studio内置的9-patch图像编辑器,或者使用第三方图像编辑软件如Photoshop。在Android Studio中,你可以通过以下步骤创建和编辑点九图:

  1. 将图片放入到res/drawable文件夹中。
  2. 将图片后缀改为.9.png,然后双击图片进入编辑视图。
  3. 使用鼠标在图片的边缘拖动就可以绘制黑点,这些黑点定义了可拉伸区域。
  4. 按住Shift键拖动可以擦除已经绘制的黑点。

.1.4状态列表选择图形

在Android开发中,状态列表选择图形(通常称为selector)是一种允许开发者为UI组件定义不同状态下的显示效果的Drawable资源。这种资源特别适用于需要根据用户交互改变外观的组件,如按钮、开关和复选框等。

状态列表选择图形通过XML文件定义,其中可以包含多个<item>元素,每个元素指定了一种状态下的Drawable资源。例如,一个按钮在被按下时可能显示一个凹陷的效果,而在正常状态下则显示为凸起。这些状态包括但不限于:

  • state_pressed:当组件被按下时。
  • state_focused:当组件获得焦点时。
  • state_selected:当组件被选中时。
  • state_checkable:当组件是可选的(通常用于切换按钮)。
  • state_checked:当组件被勾选时(例如复选框或单选按钮)。
  • state_enabled:当组件可用时。

一个简单的状态列表选择图形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_focused" android:state_focused="true" />
    <item android:drawable="@drawable/button_selected" android:state_selected="true" />
    <item android:drawable="@drawable/button_checked" android:state_checked="true" />
    <item android:drawable="@drawable/button_normal" />
</selector>

在这个例子中,button_pressedbutton_focusedbutton_selectedbutton_checked 是不同状态下的Drawable资源,而 button_normal 是默认状态下的Drawable资源。

状态列表选择图形不仅用于按钮控件,还可用于其他拥有多种状态的控件。通过使用状态列表选择图形,开发者可以创建出更加丰富和动态的用户界面。

.2选择控件

.2.1复选框CheckBox

.2.2开关按钮Switch

在Android开发中,Switch 控件是一种允许用户在两种状态之间切换的UI组件。它通常用于设置选项,如开启或关闭Wi-Fi、蓝牙等功能。Switch 控件在Android 4.0(API级别14)中被引入,并且可以通过XML布局文件和Java代码进行定制和使用。

常用属性

  • android:textOn:指定开关打开时显示的文字。
  • android:textOff:指定开关关闭时显示的文字。
  • android:thumb:设置滑块(开关部分)的图片。
  • android:track:设置滑轨(背景部分)的图片。
  • android:switchMinWidth:设置开关的最小宽度。
  • android:switchPadding:设置滑块与文字之间的间距。
  • android:switchTextAppearance:设置开关文字的样式。
  • android:checked:设置开关的初始状态(是否被选中)。
  • android:splitTrack:是否在滑块和滑轨之间设置间隙,此属性在API 21及以上版本有效。
  • android:showText:是否显示开关状态的文字,此属性在API 21及以上版本有效。

使用方法

在布局文件中添加Switch控件,并设置相应的属性。例如:

<Switch
    android:id="@+id/my_switch"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:textOn="On"
    android:textOff="Off"
    android:checked="false" />

在Java代码中,可以通过以下方式设置监听器来响应状态改变:

Switch mySwitch = (Switch) findViewById(R.id.my_switch);
mySwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
        if (isChecked) {
            // Switch is checked
        } else {
            // Switch is not checked
        }
    }
});

自定义样式

可以通过自定义thumbtrack的Drawable资源来改变Switch的样式。例如,创建thumb_selector.xmltrack_selector.xml文件来定义不同状态下的滑块和滑轨的样式。

监听状态变化

Switch控件提供了OnCheckedChangeListener接口,可以通过实现该接口来监听开关状态的变化。

小结

Switch控件是Android平台上用于状态切换的控件,通过设置不同的属性和监听器,可以满足不同的开发需求,提供良好的用户体验。

.2.3单选框RadioButton

在Android开发中,RadioButton是一种允许用户从一组选项中选择单个项目的控件。它是一种特殊的按钮,通常用于表示一组互斥的选项,其中用户可以选择一个选项,而其他选项会自动取消选择。

基本属性

  • android:id:每个RadioButton的唯一标识符。
  • android:layout_widthandroid:layout_height:定义RadioButton的大小。
  • android:text:显示在RadioButton旁边的文本。
  • android:checked:指定该RadioButton是否被选中。默认情况下,组中的RadioButton没有一个是选中的,除非你指定android:checked="true"
  • android:button:指定RadioButton的样式,可以是引用到一个drawable资源,用于自定义外观。

使用方法

在布局文件中定义RadioButton

<RadioButton
    android:id="@+id/radioButton1"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Option 1"
    android:checked="true" />

在Java代码中,你可以设置RadioButton的监听器来响应用户的选择:

RadioButton radioButton = (RadioButton) findViewById(R.id.radioButton1);
radioButton.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
        if (isChecked) {
            // RadioButton被选中
        } else {
            // RadioButton被取消选中
        }
    }
});

组管理

RadioButton通常用于创建一个组,在这个组内,只能选择一个RadioButton。这是通过将它们放在同一个父布局中实现的,或者通过编程方式将它们添加到同一个RadioGroup中。

在布局文件中使用RadioGroup

<RadioGroup
    android:id="@+id/radioGroup"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:orientation="vertical">

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

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

    <!-- 更多RadioButton -->
</RadioGroup>

在Java代码中获取选中的RadioButton

RadioGroup radioGroup = (RadioGroup) findViewById(R.id.radioGroup);
int selectedId = radioGroup.getCheckedRadioButtonId();
RadioButton selectedRadioButton = (RadioButton) findViewById(selectedId);

自定义样式

你可以通过自定义drawable资源来改变RadioButton的外观。例如,你可以创建一个自定义的selector XML文件来定义不同状态下的背景。

小结

RadioButton是Android中用于单选按钮的控件,通过将它们组织在RadioGroup中,可以确保用户只能选择一个选项。它们通常用于表单、设置页面或任何需要用户从有限选项中选择一个的场景。

.3输入控件EditText

在Android开发中,EditText 是一个用于输入文本的UI控件。用户可以通过软键盘在 EditText 中输入文本,它支持各种文本编辑功能,如文本选择、复制、粘贴等。EditText 继承自 TextView,因此它具有 TextView 的所有特性,并且还提供了一些额外的功能,如设置输入类型、输入限制等。

基本属性

  • android:idEditText 的唯一标识符。
  • android:layout_widthandroid:layout_height:定义 EditText 的大小。
  • android:hint:显示在 EditText 中的提示文本,当没有文本输入时显示。
  • android:textEditText 中的初始文本。
  • android:inputType:指定输入类型,如文本、数字、电话号码等。
  • android:maxLength:设置输入的最大长度。

使用方法

在布局文件中定义 EditText

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

在Java代码中,你可以获取用户输入的文本:

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

设置输入类型

通过 android:inputType 属性,你可以指定 EditText 接受的输入类型,这会影响软键盘的布局。例如:

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

常见的 inputType 值包括:

  • text
  • textPassword(密码,显示为点)
  • number
  • phone
  • datetime

文本监听器

你可以为 EditText 设置文本监听器来响应文本变化:

editText.addTextChangedListener(new TextWatcher() {
    @Override
    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
        // 文本改变之前的回调
    }

    @Override
    public void onTextChanged(CharSequence s, int start, int before, int count) {
        // 文本改变时的回调
    }

    @Override
    public void afterTextChanged(Editable s) {
        // 文本改变之后的回调
    }
});

自定义样式

你可以自定义 EditText 的样式,包括背景、边框、文本颜色等。这可以通过在布局文件中设置属性或者在Java代码中动态设置样式来实现。

小结

EditText 是Android中用于文本输入的基本控件,它支持多种输入类型和丰富的API,使得开发者可以根据应用的需求定制其行为和外观。通过监听器,开发者可以实时响应用户的输入,从而提供动态的交互体验。

.4对话框

.4.1基本类型

  • AlertDialog:最常见的对话框类型,用于显示简单的警告或消息,以及一到三个按钮。
  • ProgressDialog:显示一个进度指示器,表明应用正在处理某些任务,不能进行其他交互。
  • DatePickerDialog:允许用户选择日期。
  • TimePickerDialog:允许用户选择时间。
  • 自定义Dialog:继承自DialogAlertDialog,允许开发者创建具有自定义布局的对话框。

.4.2使用AlertDialog

以下是创建和显示一个简单AlertDialog的步骤:

  1. 创建AlertDialog实例
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle("Title");
builder.setMessage("Message");

// 设置"积极"按钮
builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
    public void onClick(DialogInterface dialog, int id) {
        // 用户点击了"OK"按钮的操作
    }
});

// 设置"否定"按钮
builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
    public void onClick(DialogInterface dialog, int id) {
        // 用户点击了"Cancel"按钮的操作
    }
});

// 创建AlertDialog
AlertDialog dialog = builder.create();
dialog.show();
  1. 显示AlertDialog

对话框可以通过调用show()方法显示。

.4.3进度条ProgressDialog

ProgressDialog用于显示一个圆形的进度条,表示某些操作正在进行中:

java复制

ProgressDialog progressDialog = new ProgressDialog(context);
progressDialog.setTitle("Loading");
progressDialog.setMessage("Please wait...");
progressDialog.setCancelable(false);
progressDialog.show();

.4.4时间日期对话框

DatePickerDialog和TimePickerDialog

这些对话框允许用户选择日期或时间:

Calendar calendar = Calendar.getInstance();
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH);
int day = calendar.get(Calendar.DAY_OF_MONTH);

DatePickerDialog datePickerDialog = new DatePickerDialog(context,
    new DatePickerDialog.OnDateSetListener() {
        @Override
        public void onDateSet(DatePicker view, int year, int month, int day) {
            // 用户选择日期后的操作
        }
    }, year, month, day);
datePickerDialog.show();

.4.5自定义Dialog

如果内置的对话框类型不满足需求,可以创建自定义对话框:

java复制

LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View layout = inflater.inflate(R.layout.custom_dialog, ( ViewGroup) null);
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setView(layout);
builder.setTitle("Custom Dialog");
builder.setPositiveButton("OK", null);
AlertDialog dialog = builder.create();
dialog.show();

第四章 高级控件RecyclerView

RecyclerView 是 Android 开发中用于展示大量数据集的一个高效组件。它是 ListViewGridView 的进化版,提供了更高的灵活性、自定义性和性能。RecyclerView 通过回收和重用视图(View)来优化滑动和性能,特别适合用于展示长列表或网格。

核心组件:

  1. RecyclerView:本身是一个 ViewGroup,用于承载所有的子项视图。

  2. LayoutManager:负责测量和定位子项视图,常见的布局管理器有 LinearLayoutManager(线性布局)、GridLayoutManager(网格布局)和 StaggeredGridLayoutManager(交错网格布局)。

  3. Adapter:负责提供数据给 RecyclerView,并绑定数据到视图上。Adapter 继承自 RecyclerView.Adapter 类,并实现数据的创建、绑定和通知数据变化。

  4. ViewHolder:用于缓存子项视图的引用,减少 findViewById 调用,提高性能。

基本用法:

  1. 定义布局:在布局文件中添加 RecyclerView

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
    
  2. 创建 Adapter:创建一个继承自 RecyclerView.Adapter 的类,并实现 onCreateViewHolderonBindViewHoldergetItemCount 方法。

    public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {
        private List<String> mData;
    
        public MyAdapter(List<String> data) {
            mData = data;
        }
    
        @NonNull
        @Override
        public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
            View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_layout, parent, false);
            return new ViewHolder(view);
        }
    
        @Override
        public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
            holder.textView.setText(mData.get(position));
        }
    
        @Override
        public int getItemCount() {
            return mData.size();
        }
    
        static class ViewHolder extends RecyclerView.ViewHolder {
            TextView textView;
    
            public ViewHolder(View itemView) {
                super(itemView);
                textView = itemView.findViewById(R.id.textView);
            }
        }
    }
    
  3. 设置 LayoutManager:在你的 ActivityFragment 中设置 RecyclerViewLayoutManager

    RecyclerView recyclerView = findViewById(R.id.recyclerView);
    recyclerView.setLayoutManager(new LinearLayoutManager(this));
    
  4. 绑定 Adapter:将 Adapter 绑定到 RecyclerView

    MyAdapter adapter = new MyAdapter(dataList);
    recyclerView.setAdapter(adapter);
    
  5. 处理数据变化:当数据发生变化时,调用 notifyDataSetChanged 或其他通知方法来更新 RecyclerView

    adapter.notifyDataSetChanged();
    

注意事项:

  • RecyclerView 需要正确的 LayoutManager 来确定子项的布局方式。
  • ViewHolder 的设计模式是关键,它帮助提高 RecyclerView 的性能。
  • 当数据集发生变化时,应该使用 Adapter 的通知方法来更新 UI。
  • RecyclerView 提供了多种滚动和动画效果,可以通过自定义 ItemAnimator 来实现。
  • RecyclerView 支持触摸事件和点击事件的监听,可以通过设置 OnItemClickListener 来处理。

RecyclerView 是 Android 中处理列表和网格数据的强大工具,它的灵活性和性能使其成为现代 Android 应用开发的首选组件。

第五章 网络编程

5.1 OKhttp

OkHttp 是一个高效的 HTTP 客户端库,用于发送和接收网络请求。它支持同步阻塞调用和异步调用,提供了丰富的功能,包括请求和接收 JSON、文件下载、请求压缩、HTTPS、线程池复用等。OkHttp 由 Square, Inc. 开发,是 Android 和 Java 应用中常用的网络库之一。

特点:

  1. 支持同步和异步请求:可以方便地发起同步请求并等待响应,也可以使用异步请求避免阻塞主线程。
  2. 支持 HTTPS:提供了对 HTTPS 的支持,包括证书的配置。
  3. 请求和响应压缩:支持请求和响应的 GZIP 压缩,可以减少网络传输数据。
  4. 连接池复用:通过连接池复用,减少了建立连接的开销,提高了效率。
  5. 拦截器:可以自定义拦截器来拦截请求和响应,进行日志记录、请求重试等操作。
  6. 文件下载和上传:支持文件的上传和下载,包括大文件的支持。

基本用法:

添加依赖

build.gradle 文件中添加 OkHttp 库的依赖:

dependencies {
    implementation 'com.squareup.okhttp3:okhttp:4.9.0' // 请使用最新版本
}
发起 GET 请求
OkHttpClient client = new OkHttpClient();

String url = "http://www.example.com";
Request request = new Request.Builder()
    .url(url)
    .build();

// 同步请求
try {
    Response response = client.newCall(request).execute();
    if (response.isSuccessful()) {
        String responseBody = response.body().string();
        // 处理响应内容
    }
} catch (IOException e) {
    // 处理异常
}

// 异步请求
client.newCall(request).enqueue(new Callback() {
    @Override
    public void onFailure(Call call, IOException e) {
        // 请求失败的处理
    }

    @Override
    public void onResponse(Call call, Response response) throws IOException {
        if (response.isSuccessful()) {
            String responseBody = response.body().string();
            // 处理响应内容
        }
    }
});
发起 POST 请求
RequestBody requestBody = new MultipartBody.Builder()
    .setType(MultipartBody.FORM)
    .addFormDataPart("key1", "value1")
    .addFormDataPart("key2", "value2")
    .build();

Request request = new Request.Builder()
    .url("http://www.example.com/post")
    .post(requestBody)
    .build();

// 同步或异步请求,与 GET 请求类似

使用拦截器

拦截器(Interceptor)是 OkHttp 的一个核心特性,允许你在请求和响应被处理的各个阶段进行拦截和转换。

拦截器的特点如下

  • 拦截器链是按顺序执行的,每个拦截器都可以决定是否将请求传递给链中的下一个拦截器。
  • 拦截器可以访问和修改请求和响应对象,这为自定义网络请求处理提供了极大的灵活性。
  • 在使用拦截器时,要注意线程安全和性能影响,因为拦截器可能会增加请求处理的时间。

拦截器可以用来实现多种功能,例如:

  1. 添加请求头:为所有请求添加认证信息或自定义头。
  2. 修改请求体:在发送请求之前修改请求的内容。
  3. 重写 URL:根据需要重定向请求到不同的 URL。
  4. 记录日志:记录请求和响应的详细信息,用于调试或监控。
  5. 处理重试:在遇到特定类型的失败时自动重试请求。
  6. 缓存响应:将响应存储在本地缓存中,以便后续请求可以直接使用。
  7. 添加响应头:为所有响应添加或修改头信息。
  8. 修改响应体:在返回给调用者之前修改响应的内容。
创建和使用拦截器

以下是创建和使用 OkHttp 拦截器的基本步骤:

  1. 创建拦截器类:实现 Interceptor 接口,并在 intercept 方法中定义拦截逻辑。
public class LoggingInterceptor implements Interceptor {
    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        long t1 = System.nanoTime();

        // 记录请求信息
        System.out.println(String.format("Sending request %s on %s%n%s",
            request.url(), chain.connection(), request.headers()));

        Response response = chain.proceed(request);

        long t2 = System.nanoTime();
        System.out.println(String.format("Received response for %s in %.1fms%n%s",
            response.request().url(), (t2 - t1) / 1e6d, response.headers()));

        return response;
    }
}
  1. 添加拦截器到 OkHttp 客户端:在创建 OkHttp 客户端时,将拦截器添加到客户端构建器中。
OkHttpClient client = new OkHttpClient.Builder()
    .addInterceptor(new LoggingInterceptor())
    .build();
  1. 发送请求:使用配置了拦截器的 OkHttp 客户端发送请求。
Request request = new Request.Builder()
    .url("http://www.example.com")
    .build();

Response response = client.newCall(request).execute();
内置拦截器

OkHttp 还提供了一些内置的拦截器,例如:

  • CacheInterceptor:处理 HTTP 响应缓存。
  • ConnectInterceptor:处理与服务器的连接。
  • CallServerInterceptor:处理服务器请求和响应。

注意事项:

  • 在 Android 9.0(API 级别 28)及以上版本,默认不允许明文传输(HTTP),需要配置网络安全配置文件(network_security_config.xml)或使用 HTTPS。
  • 从 Android 10(API 级别 29)开始,默认禁止了不安全请求,需要在 AndroidManifest.xml 中添加相应的权限或配置。
  • 确保处理所有可能的异常,包括网络错误和请求超时。
  • 在实际应用中,建议使用 RetrofitOkHttp 结合使用,Retrofit 提供了更高级别的抽象,使得网络请求更加简洁。

OkHttp 是一个强大且灵活的网络库,可以帮助开发者高效地进行网络通信。

5.2 gson解析字符串

Gson 是一个由 Google 开发的 Java 库,用于将 Java 对象序列化为 JSON 格式以及将 JSON 字符串反序列化为 Java 对象。Gson 使得在 Java 程序中处理 JSON 数据变得简单和直接。

以下是 Gson 的一些主要特性:

  1. 简单易用:Gson 提供了简单的 API 来序列化和反序列化 Java 对象。
  2. 灵活:可以处理复杂的对象,包括泛型和自定义类。
  3. 类型安全:Gson 能够处理 Java 的泛型类型,使得反序列化时类型安全。
  4. 可扩展:可以通过自定义序列化器和反序列化器来扩展 Gson 的功能。

如何使用 Gson

  1. 添加依赖:首先,确保你的项目中已经添加了 Gson 的依赖。在 build.gradle 文件中添加以下依赖(如果还没有的话):
dependencies {
    implementation 'com.google.code.gson:gson:2.8.6' // 请使用最新版本
}
  1. 序列化 Java 对象为 JSON
import com.google.gson.Gson;

// 创建一个 Gson 实例
Gson gson = new Gson();

// 定义一个 Java 对象
MyObject myObject = new MyObject("value1", "value2");

// 将对象转换为 JSON 字符串
String json = gson.toJson(myObject);
System.out.println(json);
  1. 反序列化 JSON 字符串为 Java 对象
// 假设我们有一个 JSON 字符串
String json = "{\"property1\":\"value1\",\"property2\":\"value2\"}";

// 使用 Gson 将 JSON 字符串转换为 Java 对象
MyObject myObject = gson.fromJson(json, MyObject.class);
System.out.println(myObject.property1);
  1. 处理泛型和复杂对象
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.util.List;

// 假设我们有一个 JSON 字符串,表示一个对象列表
String json = "[{\"property1\":\"value1\",\"property2\":\"value2\"}]";

// 获取 List 接口的类型
Type listType = new TypeToken<List<MyObject>>(){}.getType();

// 将 JSON 字符串转换为 MyObject 对象列表
List<MyObject> myObjectList = gson.fromJson(json, listType);
  1. 自定义序列化和反序列化
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonSerializer;
import com.google.gson.JsonDeserializer;

// 创建自定义序列化器
JsonSerializer<MyObject> serializer = new JsonSerializer<MyObject>() {
    @Override
    public JsonElement serialize(MyObject src, Type typeOfSrc, JsonSerializationContext context) {
        // 自定义序列化逻辑
    }
};

// 创建自定义反序列化器
JsonDeserializer<MyObject> deserializer = new JsonDeserializer<MyObject>() {
    @Override
    public MyObject deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) {
        // 自定义反序列化逻辑
    }
};

// 使用 GsonBuilder 添加自定义序列化器和反序列化器
Gson gson = new GsonBuilder()
        .registerTypeAdapter(MyObject.class, serializer)
        .registerTypeAdapter(MyObject.class, deserializer)
        .create();

Gson 是处理 JSON 数据的强大工具,它的灵活性和易用性使得它在 Android 开发中非常受欢迎。

Retrofit 是一个类型安全的 HTTP 客户端,由 Square 开发,用于 Android 和 Java 应用程序。它将 HTTP API 转换为 Java 接口。Retrofit 使得网络请求变得简单,并且易于维护。它支持同步阻塞调用和异步回调,同时也支持 RxJava 等响应式编程框架。

以下是 Retrofit 的一些主要特性:

  1. 类型安全的 REST 客户端:通过注解将 HTTP API 定义为 Java 接口。
  2. 数据转换:支持多种数据转换器,如 Gson、Jackson、Moshi 等,用于自动处理 JSON 数据。
  3. 同步和异步请求:支持同步调用和异步回调,以及 RxJava 等响应式编程。
  4. 请求构建器:强大的请求构建器,支持构建复杂的请求。
  5. 支持多种 HTTP 操作:GET、POST、PUT、DELETE、HEAD 等。
  6. 自定义客户端:可以自定义 OkHttp 客户端,用于配置拦截器、日志记录等。

5.3Retrofit

如何使用 Retrofit

  1. 添加依赖:首先,确保你的项目中已经添加了 Retrofit 和转换器(如 Gson)的依赖。在 build.gradle 文件中添加以下依赖(如果还没有的话):
dependencies {
    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
}
  1. 定义 HTTP 接口
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Path;

public interface MyApiService {
    @GET("users/{user}/repos")
    Call<List<Repo>> listRepos(@Path("user") String user);
}
  1. 创建 Retrofit 实例
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;

Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://api.github.com/")
    .addConverterFactory(GsonConverterFactory.create())
    .build();
  1. 创建服务实例并发起请求
MyApiService service = retrofit.create(MyApiService.class);

Call<List<Repo>> repos = service.listRepos("octocat");
repos.enqueue(new Callback<List<Repo>>() {
    @Override
    public void onResponse(Call<List<Repo>> call, Response<List<Repo>> response) {
        if (response.isSuccessful()) {
            // 处理成功的响应
            List<Repo> repos = response.body();
            // ...
        }
    }

    @Override
    public void onFailure(Call<List<Repo>> call, Throwable t) {
        // 处理请求失败
    }
});
  1. 同步请求
try {
    Response<List<Repo>> response = service.listRepos("octocat").execute();
    if (response.isSuccessful()) {
        // 处理成功的响应
        List<Repo> repos = response.body();
        // ...
    } else {
        // 处理错误响应
    }
} catch (IOException e) {
    // 处理网络错误
}
  1. 自定义请求
public interface MyApiService {
    @GET("users/{user}/repos")
    Call<List<Repo>> listRepos(@Path("user") String user,
                                @Query("sort") String sort,
                                @Query("page") int page);
}
  1. 使用 RxJava
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.schedulers.Schedulers;

Observable<List<Repo>> reposObservable = service.listRepos("octocat")
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread());

reposObservable.subscribe(repos -> {
    // 在主线程中处理成功的响应
}, throwable -> {
    // 处理错误
});

Retrofit 提供了灵活的方式来处理网络请求,使得代码更加简洁和易于理解。它与 Android 生态系统中的其他库(如 OkHttp 和 RxJava)集成良好,提供了强大的网络请求解决方案。

自定义请求

Retrofit 允许你通过自定义请求来实现一些特殊的需求,比如请求头的添加、请求体的自定义、请求参数的加密等。这些自定义操作通常是通过以下几种方式实现的:

  1. 自定义转换器(Converter)
    Retrofit 使用转换器(Converter)来序列化和反序列化请求和响应体。你可以创建自定义的转换器来处理特殊的数据格式或加密解密操作。

    public class CustomGsonConverterFactory extends Converter.Factory {
        private final Gson gson;
    
        public CustomGsonConverterFactory(Gson gson) {
            this.gson = gson;
        }
    
        @Override
        public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
            // 返回一个用于解析响应体的转换器
            return new CustomGsonResponseBodyConverter<>(gson, type);
        }
    
        @Override
        public Converter<?, RequestBody> requestBodyConverter(Type type, Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {
            // 返回一个用于序列化请求体的转换器
            return new CustomGsonRequestBodyConverter<>(gson);
        }
    }
    
  2. 自定义拦截器(Interceptor)
    你可以添加自定义的拦截器来拦截请求和响应,进行一些特定的操作,比如添加公共请求头、日志打印、请求重试等。

    public class CustomInterceptor implements Interceptor {
        @Override
        public Response intercept(Chain chain) throws IOException {
            Request originalRequest = chain.request();
            // 修改请求,比如添加请求头
            Request customRequest = originalRequest.newBuilder()
                    .addHeader("Authorization", "Bearer token")
                    .build();
            // 继续执行请求
            return chain.proceed(customRequest);
        }
    }
    
  3. 自定义请求方法注解
    Retrofit 允许你通过注解来定义请求方法。你可以使用内置的注解,如 @GET@POST 等,也可以创建自定义的注解。

    @POST("custom_endpoint")
    Call<CustomResponse> customMethod(@Body CustomRequest customRequest);
    
  4. 动态 URL 和 路径参数
    你可以在接口方法中使用动态 URL 和路径参数来构建请求。

    public interface ApiInterface {
        @GET("users/{user}/repos")
        Call<List<Repo>> listRepos(@Path("user") String user);
    }
    
  5. 请求体和响应体的自定义
    你可以自定义请求体和响应体的格式,比如使用自定义的 JSON 格式或 XML 格式。

    public class CustomRequest {
        // 定义请求体的字段
    }
    
    public class CustomResponse {
        // 定义响应体的字段
    }
    
  6. 使用 RxJava 或其他响应式编程库
    如果你使用 RxJava 或其他响应式编程库,你可以自定义 CallAdapter 来转换 Retrofit 的 Call 类型为响应式类型,如 ObservableFlowable 等。

    public class RxJava2CallAdapterFactory extends CallAdapter.Factory {
        @Override
        public CallAdapter<?, ?> get(Type returnType, Annotation[] annotations, Retrofit retrofit) {
            if (getRawType(returnType) != Call.class) {
                return null;
            }
            Type responseType = Utils.getCallResponseType(returnType);
            return new CallAdapter<Object, Observable<?>>() {
                @Override
                public Type responseType() {
                    return responseType;
                }
    
                @Override
                public <R> Observable<R> adapt(Call<R> call, Type responseType) {
                    return new Observable<R>() {
                        @Override
                        protected void subscribeActual(Observer<? super R> observer) {
                            // 订阅逻辑
                        }
                    };
                }
            };
        }
    }
    
  7. 自定义缓存逻辑
    你可以使用自定义缓存逻辑来缓存请求结果,减少网络请求次数。

    public class CustomCacheInterceptor implements Interceptor {
        @Override
        public Response intercept(Chain chain) throws IOException {
            Request request = chain.request();
            // 检查缓存逻辑
            Response response = /* 从缓存中获取响应 */;
            if (response != null) {
                return response;
            }
            // 没有缓存则正常请求
            response = chain.proceed(request);
            // 将响应缓存起来
            /* 缓存响应 */;
            return response;
        }
    }
    

通过上述方法,你可以根据自己的需求对 Retrofit 进行灵活的自定义,以满足特定的网络请求场景。在实际开发中,你可能需要根据项目的具体要求来选择合适的自定义方式。

第六章 存储

6.1SharedPreference

SharedPreferences 是Android提供的一种轻量级的存储机制,主要用于保存少量的配置信息。它允许你保存和获取各种类型的数据,如字符串(String)、整型(int)、布尔型(boolean)、浮点数(float)、长整型(long)以及字符串集合(StringSet)。

基本使用方法

  1. 获取SharedPreferences对象
    使用Context类的getSharedPreferences方法获取SharedPreferences对象。你可以为这个对象指定一个名称,如果指定的名称已经存在,则会获取到现有的SharedPreferences文件;如果不存在,则会创建一个新的。

    SharedPreferences sharedPreferences = context.getSharedPreferences("filename", Context.MODE_PRIVATE);
    
  2. 编辑SharedPreferences文件
    通过SharedPreferences对象的edit方法获取SharedPreferences.Editor对象,然后使用put方法保存数据。

    SharedPreferences.Editor editor = sharedPreferences.edit();
    editor.putString("key", "value");
    editor.putInt("key", 1);
    editor.putBoolean("key", true);
    // ...
    editor.apply(); // 异步提交
    editor.commit(); // 同步提交
    
    1. 读取数据
   
   通过`SharedPreferences`对象的`get`方法读取数据,需要提供键(key)和默认值(defaultValue)。
   ```java
   String value = sharedPreferences.getString("key", "default value");
   int intValue = sharedPreferences.getInt("key", 0);
   boolean boolValue = sharedPreferences.getBoolean("key", false);
  1. 移除和清除数据
    使用remove方法移除特定的键值对,使用clear方法清除所有的数据。

    editor.remove("key").commit();
    editor.clear().commit();
    
  2. 监听变化
    可以通过注册OnSharedPreferenceChangeListener来监听SharedPreferences文件的变化。

    sharedPreferences.registerOnSharedPreferenceChangeListener(new SharedPreferences.OnSharedPreferenceChangeListener() {
        @Override
        public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
            // 处理变化
        }
    });
    

性能注意事项

  • SharedPreferences是单例对象,第一次获取后,后续获取速度很快。
  • 数据存储在XML文件中,因此不适合存储大量数据。
  • apply方法异步提交,适用于不需要立即获取提交结果的场景;commit方法同步提交,适用于需要立即获取提交结果的场景。

封装工具类

为了简化操作,可以将SharedPreferences的操作封装成工具类,提供简洁的API进行数据的存取。

以上信息综合了搜索结果中的相关内容,提供了SharedPreferences的基本介绍和使用方法。

6.2SQLLITE

在Android开发中,SQLite是一个轻量级的嵌入式关系型数据库,它被广泛用于移动应用和嵌入式系统中。SQLite数据库通常由一个或多个表组成,每个表包含行和列,可以通过SQL语言进行数据的增删改查操作。

创建和使用SQLite数据库

在Android中,通常通过继承SQLiteOpenHelper类来创建和管理数据库。这个类提供了onCreate()onUpgrade()两个方法,分别用于创建数据库和更新数据库结构。创建SQLiteOpenHelper的子类时,需要实现以下方法:

  • onCreate(SQLiteDatabase db):在数据库首次创建时调用,用于创建表和初始化数据。
  • onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion):当数据库需要升级时调用,用于处理数据库结构变更。

基本操作

  • 添加数据:使用insert()方法或直接执行INSERT INTO SQL语句。
  • 查询数据:使用query()方法或直接执行SELECT SQL语句,返回一个Cursor对象,可以遍历查询结果。
  • 更新数据:使用update()方法或直接执行UPDATE SQL语句。
  • 删除数据:使用delete()方法或直接执行DELETE FROM SQL语句。

性能优化

  • 使用索引:为经常查询的列创建索引,可以提高查询效率。
  • 使用事务:在执行批量插入或更新操作时,使用事务可以提高性能并保证数据一致性。
  • 避免在主线程中进行数据库操作:数据库操作可能会耗时,应避免在主线程中直接操作数据库,以免造成界面卡顿。

最佳实践

  • 使用参数化查询:为了防止SQL注入攻击,应使用参数化查询代替字符串拼接。
  • 定期备份数据库:定期备份数据库文件,以防数据丢失。
  • 合理选择数据类型:根据数据的特点选择合适的数据类型,以节省存储空间并提高查询效率。

6.2.5在Android中使用sqllite

当然,以下是一个简单的Android SQLite数据库使用示例,包括创建数据库、添加数据、查询数据、更新数据和删除数据的基本操作。

1. 创建数据库帮助类

首先,创建一个继承自SQLiteOpenHelper的类,用于管理数据库的创建和版本管理。

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

public class DatabaseHelper extends SQLiteOpenHelper {

    // 数据库版本
    private static final int DATABASE_VERSION = 1;
    // 数据库名称
    private static final String DATABASE_NAME = "MyDatabase.db";

    // 表名
    private static final String TABLE_NAME = "users";
    // 列名
    private static final String COLUMN_ID = "id";
    private static final String COLUMN_NAME = "name";
    private static final String COLUMN_EMAIL = "email";

    // 创建表的SQL语句
    private static final String CREATE_TABLE_USERS = "CREATE TABLE "
            + TABLE_NAME + " (" + COLUMN_ID
            + " INTEGER PRIMARY KEY AUTOINCREMENT, " + COLUMN_NAME
            + " TEXT, " + COLUMN_EMAIL + " 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_NAME);
        onCreate(db);
    }
}
2. 插入数据

接下来,创建一个方法来添加数据到数据库。

public void addUser(String name, String email) {
    SQLiteDatabase db = this.getWritableDatabase();

    ContentValues values = new ContentValues();
    values.put(COLUMN_NAME, name);
    values.put(COLUMN_EMAIL, email);

    db.insert(TABLE_NAME, null, values);
    db.close();
}
3. 查询数据

创建一个方法来查询数据库中的数据。

public Cursor getAllUsers() {
    SQLiteDatabase db = this.getReadableDatabase();
    return db.rawQuery("SELECT * FROM " + TABLE_NAME, null);
}
4. 更新数据

创建一个方法来更新数据库中的数据。

public int updateUser(int id, String name, String email) {
    SQLiteDatabase db = this.getWritableDatabase();

    ContentValues values = new ContentValues();
    values.put(COLUMN_NAME, name);
    values.put(COLUMN_EMAIL, email);

    return db.update(TABLE_NAME, values, COLUMN_ID + " = ?",
            new String[] { String.valueOf(id) });
}
5. 删除数据

创建一个方法来删除数据库中的数据。

public int deleteUser(int id) {
    SQLiteDatabase db = this.getWritableDatabase();
    return db.delete(TABLE_NAME, COLUMN_ID + " = ?",
            new String[] { String.valueOf(id) });
}
6. 使用数据库

最后,在你的Activity或其他组件中,你可以这样使用上面定义的DatabaseHelper类:

DatabaseHelper db = new DatabaseHelper(context);

// 添加用户
db.addUser("John Doe", "john.doe@example.com");

// 获取所有用户
Cursor cursor = db.getAllUsers();
if (cursor.moveToFirst()) {
    do {
        String name = cursor.getString(cursor.getColumnIndex("name"));
        String email = cursor.getString(cursor.getColumnIndex("email"));
        // 处理用户数据
    } while (cursor.moveToNext());
    cursor.close();
}

// 更新用户
db.updateUser(1, "Jane Doe", "jane.doe@example.com");

// 删除用户
db.deleteUser(1);

这个例子展示了如何在Android应用中使用SQLite数据库进行基本的CRUD操作。在实际应用中,你可能需要处理更复杂的数据结构和逻辑,但基本的操作流程是类似的。

6.3文件存储

在Android中,文件存储可以通过内部存储和外部存储两种方式进行。内部存储是设备内部的私有存储空间,而外部存储通常指的是SD卡或外部存储设备。

6.3.1内部存储

内部存储是应用的私有存储空间,其他应用无法访问。你可以使用以下方法来存储和访问内部存储中的文件:

  1. 使用内部存储写入文件

    FileOutputStream fos = openFileOutput("filename.txt", MODE_PRIVATE);
    fos.write("Hello World".getBytes());
    fos.close();
    
  2. 使用内部存储读取文件

    FileInputStream fis = openFileInput("filename.txt");
    InputStreamReader isr = new InputStreamReader(fis);
    BufferedReader br = new BufferedReader(isr);
    StringBuilder sb = new StringBuilder();
    String line;
    while ((line = br.readLine()) != null) {
        sb.append(line).append("\n");
    }
    String data = sb.toString();
    fis.close();
    

6.3.2外部存储

外部存储是设备上的共享存储空间,可以被多个应用访问。从Android 6.0(API级别23)开始,访问外部存储需要动态请求存储权限。

  1. 请求外部存储权限(Android 6.0及以上):

    <!-- 在AndroidManifest.xml中添加 -->
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    

    动态请求权限:

    if (ContextCompat.checkSelfPermission(thisActivity, Manifest.permission.WRITE_EXTERNAL_STORAGE)
            != PackageManager.PERMISSION_GRANTED) {
        ActivityCompat.requestPermissions(thisActivity,
                new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
                MY_PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE);
    }
    
  2. 使用外部存储写入文件

    File file = new File(Environment.getExternalStorageDirectory(), "filename.txt");
    FileOutputStream fos = new FileOutputStream(file);
    fos.write("Hello World".getBytes());
    fos.close();
    
  3. 使用外部存储读取文件

    File file = new File(Environment.getExternalStorageDirectory(), "filename.txt");
    FileInputStream fis = new FileInputStream(file);
    InputStreamReader isr = new InputStreamReader(fis);
    BufferedReader br = new BufferedReader(isr);
    StringBuilder sb = new StringBuilder();
    String line;
    while ((line = br.readLine()) != null) {
        sb.append(line).append("\n");
    }
    String data = sb.toString();
    fis.close();
    

6.3.3文件访问权限控制

  • 从Android 10(API级别29)开始,默认情况下,应用无法直接访问外部存储中的所有文件。你需要使用MediaStore API或Storage Access Framework来访问用户选择的文件。
  • 从Android 11(API级别30)开始,应用默认被授予其专属目录的访问权限,而不是整个外部存储空间。你可以使用getExternalFilesDir()方法来获取应用专属的外部存储目录。
  • 使用外部存储时,要考虑设备没有SD卡或用户禁用外部存储的情况。
获取专属外部存储目录
File externalAppDir = new File(context.getExternalFilesDir(null), "myFolder");
if (!externalAppDir.exists()) {
    externalAppDir.mkdirs();
}
File file = new File(externalAppDir, "filename.txt");

这个目录是应用私有的,其他应用无法访问,但它仍然位于外部存储上,因此当用户卸载应用时,这些文件会被删除。

访问外部公共存储空间

在Android中,访问外部存储的公共空间通常涉及到对外部存储权限的处理。以下是一些关键点和步骤,用于在Android应用中访问外部存储的公共空间:

  1. 权限声明:在AndroidManifest.xml中声明所需的权限。对于Android 6.0及以上版本,需要动态请求权限。

  2. 检查和请求权限:在代码中检查是否已经获取了READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE权限。如果没有,需要在运行时请求用户授权。

  3. 访问公共目录:使用Environment.getExternalStoragePublicDirectory(String type)方法可以访问外部存储的公共目录,如Environment.DIRECTORY_DOWNLOADSEnvironment.DIRECTORY_PICTURES等。

  4. 适配Android 10及以上版本:从Android 10(API级别29)开始,引入了作用域存储(Scoped Storage),应用默认只能访问自己的外部存储目录。如果需要访问公共目录,可以继续使用上述方法。

  5. 适配Android 11及以上版本:从Android 11(API级别30)开始,可以申请MANAGE_EXTERNAL_STORAGE权限来访问更广泛的外部存储空间,但这通常仅限于特定类型的应用,如文件管理器或备份应用。

  6. 使用MediaStore API:对于媒体文件(如图片、视频、音频),可以使用MediaStore API来访问和存储,这些文件可以被其他应用访问。

  7. 存储访问框架:对于非媒体文件,可以使用存储访问框架(Storage Access Framework)来允许用户选择文件。

  8. 处理Android 10的分区存储:如果你的应用目标是Android 10及以上版本,你需要适配分区存储。可以通过在AndroidManifest.xml中添加android:requestLegacyExternalStorage="true"属性来请求传统的外部存储访问,但这不推荐长期使用。

  9. 清理缓存:及时清理不再需要的缓存文件,避免占用过多空间。

  10. 异步操作:进行文件操作时,考虑使用异步任务或后台线程,避免阻塞主线程影响用户体验。

请注意,访问外部存储的权限和方法可能会随着Android版本的更新而变化,因此建议查看最新的官方文档以获取最新信息。

第七章 活动

在 Android 应用开发中,Activity 是一个非常重要的概念。它代表了应用中的一个用户界面屏幕,用户可以与它进行交互。以下是关于 Activity 的一些基本知识点:

  1. 生命周期
    Activity 拥有自己的生命周期,它定义了 Activity 在创建、运行、暂停、停止和销毁时的各种状态。主要的生命周期方法包括:

    • onCreate(Bundle savedInstanceState):当 Activity 第一次创建时调用,用于初始化界面和成员变量。
    • onStart():当 Activity 变得可见时调用。
    • onResume():当 Activity 准备与用户交互时调用,是生命周期中的“活跃”状态。
    • onPause():当 Activity 失去焦点但仍然可见时调用,通常用于保存状态或停止动画。
    • onStop():当 Activity 不再可见时调用。
    • onDestroy():当 Activity 被销毁时调用,用于清理资源。
    • onRestart():当 Activity 从停止状态回到启动状态时调用。
  2. 配置变化
    当设备发生配置变化(如屏幕旋转)时,Android 系统可能会重新创建 Activity。你可以通过在 onSaveInstanceState(Bundle outState) 方法中保存状态,然后在 onCreate()onRestoreInstanceState(Bundle savedInstanceState) 中恢复状态来处理这些变化。

  3. Intent
    Activity 通过 Intent 来启动。Intent 是一种消息传递对象,它描述了要执行的操作和执行操作所需的信息。你可以通过显式 Intent(指定特定组件)或隐式 Intent(只指定动作和数据类型,系统决定哪个组件来处理)来启动 Activity。

  4. 任务和回栈
    Activity 被组织在任务(Task)中,任务是 Activity 的栈结构,每个任务有一个回栈(Back Stack),用于管理 Activity 的切换和返回操作。

  5. UI 更新
    Activity 通常包含一个或多个 Views,它们组成了用户界面。你可以在 Activity 中定义 Views,并在 onCreate() 方法中设置布局参数和事件监听器。

  6. 启动模式
    Activity 在 AndroidManifest.xml 文件中定义,并且可以设置不同的启动模式,如 standardsingleTopsingleTasksingleInstance 等,这些模式决定了新启动的 Activity 如何与现有任务和回栈交互。

  7. 上下文(Context)
    Activity 是 Context 的一个子类,提供了应用组件的全局信息,如资源和类加载器,以及访问应用范围内的应用程序偏好设置和其他应用程序级服务的方法。

  8. Fragment
    Activity 可以包含一个或多个 Fragment,Fragment 是 Activity 的一个更小的、可重用的部分,它有自己的布局和生命周期。

  9. 主题和样式
    Activity 可以有自己的主题和样式,通过在 manifest 文件中指定或在 onCreate() 方法中设置 setTheme() 来应用。

  10. 结果处理
    Activity 可以启动另一个 Activity 来获取结果,通过 startActivityForResult() 方法启动,并在 onActivityResult() 方法中接收结果。

理解 Activity 的这些基本知识对于开发 Android 应用至关重要,因为它们是构建用户界面和处理用户交互的基础。

生命周期

Android 中的 Activity 拥有一个明确定义的生命周期,它包含了一系列的回调方法,这些方法在 Activity 的不同状态下被调用。了解和正确管理这些生命周期方法对于开发稳定、高效的 Android 应用至关重要。以下是 Activity 生命周期的主要阶段和相应的回调方法:

  1. 创建(Creating):

    • onCreate(Bundle savedInstanceState):
      这是第一个被调用的方法,用于初始化 Activity。在这里,你可以设置布局、初始化数据和成员变量。如果 Activity 被系统销毁并重新创建(例如屏幕旋转),savedInstanceState 会传递之前保存的状态。
  2. 启动(Starting):

    • onStart():
      Activity 从不可见到可见时调用。此时,Activity 已经开始,但还没有获得焦点。
  3. 恢复(Resuming):

    • onResume():
      Activity 准备与用户交互时调用。这是 Activity 生命周期中“活跃”的开始,是执行动画、获取用户输入和启动定时器的好时机。
  4. 暂停(Pausing):

    • onPause():
      Activity 失去焦点并即将停止与用户交互时调用。这是一个保存状态、停止动画和释放资源的好时机。需要注意的是,这个方法并不保证一定会被调用,例如当设备突然断电时。
  5. 停止(Stopping):

    • onStop():
      Activity 不再可见时调用。此时,Activity 仍然在内存中,但用户无法看到它。你应该在这里释放不需要的资源,如网络连接和数据库连接。
  6. 重建(Recreating):

    • onSaveInstanceState(Bundle outState):
      Activity 需要被销毁并重新创建时(如屏幕旋转),系统会调用此方法。你应该在这里保存 Activity 的状态,以便在 onCreate()onRestoreInstanceState() 中恢复。
    • onRestoreInstanceState(Bundle savedInstanceState):
      如果 Activity 被重建,此方法会被调用,你可以在这里恢复之前保存的状态。
  7. 销毁(Destroying):

    • onDestroy():
      Activity 即将被销毁时调用。这是一个清理资源的好时机,如停止服务、广播接收器和注册的 LifecycleObserver。一旦这个方法被调用,Activity 的生命周期就结束了。

除了这些主要的生命周期方法,还有一些其他方法,如 onRestart(),它在 Activity 从停止状态回到启动状态时调用。

正确地管理 Activity 的生命周期对于确保应用的稳定性和响应性至关重要。你需要在适当的生命周期方法中保存和恢复状态,以及在不再需要时释放资源。

活动启动与数据传递

在 Android 中,Activity 的启动通常通过 Intent 来实现,而数据传递也是通过 Intent 进行的。以下是关于 Activity 启动和数据传递的基本知识:

启动 Activity

  1. 显式 Intent
    显式 Intent 直接指定要启动的组件(Activity、Service 或 Broadcast Receiver)。这种方式需要明确知道目标组件的类名。

    Intent intent = new Intent(CurrentActivity.this, TargetActivity.class);
    startActivity(intent);
    
  2. 隐式 Intent
    隐式 Intent 不直接指定组件,而是通过 Action、Data 和 Category 来描述要执行的操作。系统会根据这些信息找到合适的组件来处理这个 Intent。

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

数据传递

  1. 通过 Intent 传递数据
    可以使用 putExtra() 方法向 Intent 中添加额外的数据,然后在目标 Activity 中通过 getIntent().getStringExtra() 方法获取。

    // 在发送方 Activity 中
    Intent intent = new Intent(CurrentActivity.this, TargetActivity.class);
    intent.putExtra("key", "value");
    startActivity(intent);
    
    // 在接收方 Activity 中
    Bundle extras = getIntent().getExtras();
    if (extras != null) {
        String value = extras.getString("key");
    }
    
  2. 使用 Parcelable 和 Serializable
    如果需要传递复杂的对象,可以将对象实现为 ParcelableSerializable 接口,然后通过 Intent 传递。

    // 假设 MyData 类实现了 Parcelable 接口
    Intent intent = new Intent(CurrentActivity.this, TargetActivity.class);
    intent.putExtra("myData", myDataObject);
    startActivity(intent);
    
    // 在接收方 Activity 中
    Intent intent = getIntent();
    MyData myDataObject = intent.getParcelableExtra("myData");
    
  3. 使用 Bundle
    Bundle 类似于一个键值对集合,可以用来存储和传递数据。

    Bundle bundle = new Bundle();
    bundle.putString("key", "value");
    Intent intent = new Intent(CurrentActivity.this, TargetActivity.class);
    intent.putExtras(bundle);
    startActivity(intent);
    
  4. 启动模式
    在 AndroidManifest.xml 中定义 Activity 时,可以设置不同的启动模式,如 standardsingleTopsingleTasksingleInstance。这些启动模式决定了新启动的 Activity 如何与现有任务和回栈交互。

结果返回

如果需要从目标 Activity 返回数据到启动它的 Activity,可以使用 startActivityForResult()setResult() 方法。

  1. 启动目标 Activity

    // 在启动方 Activity 中
    Intent intent = new Intent(CurrentActivity.this, TargetActivity.class);
    startActivityForResult(intent, REQUEST_CODE);
    
  2. 在目标 Activity 中设置结果

    // 在目标 Activity 中
    Intent returnIntent = new Intent();
    returnIntent.putExtra("resultKey", "resultValue");
    setResult(Activity.RESULT_OK, returnIntent);
    finish();
    
  3. 处理返回的结果

    // 在启动方 Activity 中
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == REQUEST_CODE && resultCode == Activity.RESULT_OK) {
            String result = data.getStringExtra("resultKey");
        }
    }
    

正确地管理和传递数据对于构建功能丰富、用户体验良好的 Android 应用至关重要。

新版接受返回结果

在新的版本中startActivityForResult方法被标为弃用,如果想要获取activity的返回结果需要使用ActivityResultLauncher

步骤如下:

registerForActivityResult() 方法是 Android Jetpack activity-ktx 库提供的一个用于处理 Activity 结果的 API。它允许你以更简洁和更现代的方式来替代过时的 startActivityForResult() 方法。以下是如何使用 registerForActivityResult() 方法的步骤:

  1. 添加依赖:确保你的项目中添加了 activity-ktx 库的依赖。
dependencies {
    implementation 'androidx.activity:activity-ktx:1.2.0' // 请使用最新版本
}
  1. 注册 ActivityResultLauncher:在你的 Activity 或 Fragment 中注册一个 ActivityResultLauncher
public class MyActivity extends AppCompatActivity {
    private ActivityResultLauncher<Intent> mGetContentIntent;

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

        // 注册 ActivityResultLauncher
        mGetContentIntent = registerForActivityResult(
            new ActivityResultContracts.StartActivityForResult(),
            result -> {
                if (result.getResultCode() == Activity.RESULT_OK) {
                    Intent data = result.getData();
                    // 处理返回的数据,例如更新 ImageView
                    ImageView imageView = findViewById(R.id.imageView);
                    if (data != null && data.getData() != null) {
                        Uri imageUri = data.getData();
                        // 假设你已经有了一个方法来处理 Uri 更新 ImageView
                        updateImageView(imageView, imageUri);
                    }
                }
            }
        );

        Button button = findViewById(R.id.buttonChooseImage);
        button.setOnClickListener(v -> {
            // 启动图片选择器
            Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
            mGetContentIntent.launch(intent);
        });
    }

    private void updateImageView(ImageView imageView, Uri imageUri) {
        // 使用 Glide 或其他库来加载图片
        Glide.with(this).load(imageUri).into(imageView);
    }
}
  1. 启动 Activity:使用 launch() 方法启动 Activity。
// 已经在上面的例子中展示了如何启动
Button button = findViewById(R.id.buttonChooseImage);
        button.setOnClickListener(v -> {
            // 启动图片选择器
            Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
            mGetContentIntent.launch(intent);
        });
  1. 处理结果:在 registerForActivityResult() 的回调中处理返回的结果。
// 已经在上面的例子中展示了如何处理结果
result -> {
                if (result.getResultCode() == Activity.RESULT_OK) {
                    Intent data = result.getData();
                    // 处理返回的数据,例如更新 ImageView
                    ImageView imageView = findViewById(R.id.imageView);
                    if (data != null && data.getData() != null) {
                        Uri imageUri = data.getData();
                        // 假设你已经有了一个方法来处理 Uri 更新 ImageView
                        updateImageView(imageView, imageUri);
                    }

在这个例子中,我们注册了一个 ActivityResultLauncher 来处理从图片选择器返回的结果。当用户选择了一张图片后,ActivityResultLauncher 的回调会被触发,我们在回调中获取返回的数据并更新 ImageView

请注意,ActivityResultContracts.StartActivityForResult()ActivityResultContracts 类提供的一个标准契约,用于处理启动 Activity 并获取结果的场景。

使用 registerForActivityResult() 方法可以简化权限请求和 Activity 结果处理的代码,使其更加清晰和易于维护。

活动的启动模式

在 Android 中,Activity 的启动模式决定了新实例的行为以及它们如何与任务栈(Task Stack)中的其他 Activity 交互。这些启动模式通过 AndroidManifest.xml 文件中的 android:launchMode 属性来设置。以下是四种标准的 Activity 启动模式:

  1. standard(标准模式)

    • 这是默认的启动模式。
    • 每次启动 Activity 时,都会创建一个新的实例,并且将其添加到任务栈的顶部。
    • 如果已经在任务栈中存在该 Activity 的实例,系统会重新使用这个实例(不会创建新的实例),并将其带到前台。
  2. singleTop(栈顶复用模式)

    • 如果新启动的 Activity 已经位于任务栈的顶部,则不会创建新的实例,而是调用已有实例的 onNewIntent(Intent) 方法,并将新的 Intent 传递给它。
    • 如果该 Activity 不在栈顶,那么系统会创建一个新的实例,并将其放置在栈顶。
  3. singleTask(栈内复用模式)

    • 这种模式确保了整个任务栈中只有一个该 Activity 的实例。
    • 如果已经存在该 Activity 的实例,系统会将其带到前台,并且不会创建新的实例。
    • 同时,系统还会将该 Activity 之上的所有 Activity 都关闭,即从任务栈中移除。
  4. singleInstance(单独任务模式)

    • 这种模式确保了整个系统中只有一个该 Activity 的实例。
    • 每次启动该 Activity 时,都会创建一个新的任务栈,并将该 Activity 放入这个新的任务栈中。
    • 这种模式下,该 Activity 总是单独运行在一个任务栈中,不会与其他任何 Activity 共享任务栈。

除了这四种标准启动模式,还有一些与任务栈相关的其他配置:

  • clearTaskOnLaunch

    • 当设置为 true 时,每次启动 Activity 都会清除当前任务栈中所有在该 Activity 之上的 Activity。
  • alwaysRetainTaskState

    • 当设置为 true 时,系统不会在内存不足时销毁该 Activity,即使它位于任务栈的底部。
  • allowTaskReparenting

    • 当设置为 true 时,系统允许将 Activity 移动到另一个任务栈中。
  • finishOnTaskLaunch

    • 当设置为 true 时,如果用户通过快捷方式或启动器启动了该 Activity,并且该 Activity 已经在任务栈中存在,那么系统会销毁当前实例,并创建一个新的实例。

正确选择启动模式对于管理 Activity 的行为和用户体验至关重要。开发者需要根据应用的具体需求来选择合适的启动模式。

碎片Fragment

​ Fragment 是 Android 应用中的一个构建块,它代表了一个可以独立于 Activity 存在的用户界面部分。Fragment 可以被看作是 Activity 的一个子集,它有自己的生命周期、输入事件和可以包含自己的用户界面。

Fragment 的生命周期

Fragment 的生命周期与 Activity 紧密相关,但它有自己的一组回调方法。Fragment 生命周期的关键方法包括:

  • onAttach(Context context):当 Fragment 首次附加到 Activity 时调用。
  • onCreate(Bundle savedInstanceState):用于 Fragment 的初始化,类似于 Activity 的 onCreate
  • onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState):加载 Fragment 的布局,返回一个 View 对象。
  • onViewCreated(View view, Bundle savedInstanceState):在 Fragment 的视图创建后调用,但还没有被添加到 Activity 中。
  • onActivityCreated(Bundle savedInstanceState):当 Fragment 所在的 Activity 创建完毕后调用。
  • onStart():Fragment 变为可见时调用。
  • onResume():Fragment 准备与用户交互时调用。
  • onPause():Fragment 失去焦点时调用。
  • onStop():Fragment 不再可见时调用。
  • onDestroyView():Fragment 的视图被销毁前调用。
  • onDestroy():Fragment 被销毁时调用。
  • onDetach():Fragment 从 Activity 分离时调用。

Fragment 的使用

Fragment 可以被动态地添加到 Activity 中,或者在 Activity 的布局文件中静态定义。动态添加通常使用 FragmentManagerFragmentTransaction 来完成。

  • 动态添加
    Activity 可以通过 getFragmentManager() 获取 FragmentManager 实例,然后开始一个 Fragment 事务来添加、移除或替换 Fragment。
  • 静态定义
    在 Activity 的布局文件中,可以使用 <fragment> 标签直接嵌入 Fragment。

当然,下面我将通过代码示例展示如何在 Android 中动态添加和静态定义 Fragment。

动态添加 Fragment

首先,创建一个简单的 Fragment:

public class MyFragment extends Fragment {

    public MyFragment() {
        // Required empty public constructor
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_example, container, false);
    }
}

在 Activity 中动态添加这个 Fragment:

public class MainActivity extends AppCompatActivity {

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

        // 检查 savedInstanceState,以便在配置更改时恢复状态
        if (savedInstanceState == null) {
            // 动态添加 Fragment
            MyFragment firstFragment = new MyFragment();

            // 获取 FragmentManager 并开始一个事务
            FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();

            // 添加 Fragment,并给它一个标签,以便以后引用
            transaction.add(R.id.fragment_container, firstFragment, "MyFragment");

            // 提交事务
            transaction.commit();
        }
    }
}

activity_main.xml 布局文件中,定义一个容器来放置 Fragment:

<FrameLayout
    android:id="@+id/fragment_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />
静态定义 Fragment

在 Activity 的布局文件中静态定义 Fragment,你需要使用 <fragment> 标签:

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

    <fragment
        android:name="com.example.myapp.MyFragment"
        android:id="@+id/fragment_example"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>

在 Activity 中获取静态定义的 Fragment:

public class MainActivity extends AppCompatActivity {

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

        // 获取静态定义的 Fragment 实例
        MyFragment myFragment = (MyFragment) getSupportFragmentManager().findFragmentById(R.id.fragment_example);

        // 现在你可以与 exampleFragment 进行交互
    }
}

请注意,静态定义的 Fragment 需要在编译时就知道其类名,因此它们通常是当前包内的 Fragment。如果你需要引用不同包中的 Fragment,或者希望在运行时动态决定使用哪个 Fragment,那么你应该使用动态添加的方式。

这两种方法各有优势,静态定义的 Fragment 使得布局更加直观,而动态添加的 Fragment 提供了更大的灵活性。在实际开发中,你可以根据需求选择适合的方法。

Fragment 的通信

Fragment 可以通过几种方式与 Activity 通信:

  • 回调接口:Activity 可以定义一个回调接口,并将自身的实例作为该接口的引用传递给 Fragment。
  • 直接调用:如果 Fragment 与 Activity 紧密相关,Fragment 可以直接调用 Activity 的公共方法。
回调接口

使用回调接口是 Fragment 与 Activity 通信的一种常见方式。这种方式特别适用于 Fragment 需要将某些操作的结果或事件通知给 Activity 的情况。

首先,定义一个回调接口:

public interface OnFragmentInteractionListener {
    void onFragmentInteraction(String data);
}

在 Fragment 中,定义一个接口的成员变量,并在 onAttach() 方法中获取 Activity 的实例:

public class MyFragment extends Fragment {
    private OnFragmentInteractionListener mListener;

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        if (context instanceof OnFragmentInteractionListener) {
            mListener = (OnFragmentInteractionListener) context;
        } else {
            throw new RuntimeException(context.toString()
                + " must implement OnFragmentInteractionListener");
        }
    }

    @Override
    public void onDetach() {
        super.onDetach();
        mListener = null;
    }

    public void someMethod() {
        // 调用回调方法
        if (mListener != null) {
            mListener.onFragmentInteraction("Hello from Fragment");
        }
    }
}

在 Activity 中实现这个接口,并定义回调方法:

public class MainActivity extends AppCompatActivity implements OnFragmentInteractionListener {

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

        MyFragment myFragment = new MyFragment();
        myFragment.setOnFragmentInteractionListener(this); // 设置监听器
        getSupportFragmentManager().beginTransaction()
                .add(R.id.fragment_container, myFragment).commit();
    }

    @Override
    public void onFragmentInteraction(String data) {
        // 处理 Fragment 发送的数据
        Toast.makeText(this, data, Toast.LENGTH_SHORT).show();
    }
}
直接调用

如果 Fragment 与 Activity 紧密相关,或者 Activity 已经知道它将与之交互的 Fragment,Fragment 可以直接调用 Activity 的公共方法。

首先,在 Activity 中定义一个公共方法:

public class MainActivity extends AppCompatActivity {

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

    // 公共方法供 Fragment 调用
    public void updateData(String data) {
        Toast.makeText(this, data, Toast.LENGTH_SHORT).show();
    }
}

在 Fragment 中,直接调用 Activity 的公共方法:

public class MyFragment extends Fragment {

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_my, container, false);

        // 直接调用 Activity 的公共方法
        getActivity().runOnUiThread(new Runnable() {
            @Override
            public void run() {
                ((MainActivity) getActivity()).updateData("Hello from Fragment");
            }
        });

        return view;
    }
}

在这个例子中,MyFragment 直接调用了 MainActivityupdateData 方法。这种方式简单直接,但需要确保 Activity 在 Fragment 生命周期内始终可用,以避免 NullPointerException

两种通信方式各有适用场景,回调接口更加灵活,适用于解耦合的组件通信;直接调用则更简单直接,适用于紧密相关的组件之间。在实际开发中,应根据具体需求选择合适的通信方式。

Fragment 的管理

事务管理的步骤

//开启事务管理
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
//transaction.replace(R.id.fragment_container, new MyFragment());//一些操作
transaction.commit();//提交

事务管理

管理 Fragment 主要涉及以下几个方面:

  • 事务管理:使用 FragmentTransaction添加移除替换 Fragment。

    • 添加:transaction.add(R.id.fragment_container, new MyFragment());
    • 移除:transaction.remove(myFragment);
    • 替换:transaction.replace(R.id.fragment_container, new MyFragment());
  • BackStack:BackStack 是一个特殊的数据结构,用于维护 Fragment 事务的历史记录。当用户按下返回键时,系统会检查 BackStack,并根据记录的事务回退到之前的 Fragment 状态。

    1. 将事务添加到BackStack中

      transaction.addToBackStack(null); // 可以传递一个字符串作为事务的名字

    2. 回退到之前的 Fragment:用户按下返回键时,系统会自动处理 BackStack。如果需要程序控制回退,可以使用:getSupportFragmentManager().popBackStack();

  • 嵌套 Fragment:一个 Fragment 可以包含其他 Fragment,这允许创建更复杂的 UI 结构。

嵌套 Fragment

嵌套 Fragment 是指在一个 Fragment 内部包含另一个 Fragment。这允许你构建更复杂的 UI 结构,每个 Fragment 可以独立管理自己的子 Fragment。

在 Fragment 中添加另一个 Fragment

首先,在你的 Fragment 的布局文件中定义一个容器:

xml

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

然后,在 Fragment 中添加另一个 Fragment:

java

public class MyFragment extends Fragment {

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_my, container, false);
    }

    @Override
    public void onViewCreated(View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);

        // 获取容器的 ID,并添加另一个 Fragment
        FrameLayout container = view.findViewById(R.id.nested_fragment_container);
        NestedFragment nestedFragment = new NestedFragment();
        getChildFragmentManager().beginTransaction()
                .add(R.id.nested_fragment_container, nestedFragment)
                .commit();
    }
}

注意,我们使用 getChildFragmentManager() 而不是 getSupportFragmentManager(),因为嵌套的 Fragment 属于当前 Fragment 的子 Fragment。

Fragment 的兼容性

在 Android 3.0(API 级别 11)及以上版本中,可以直接使用 android.app.Fragment。对于更低版本的 Android,可以使用 Android 支持库中的 android.support.v4.app.Fragment 来实现类似的功能。

Fragment 的最佳实践

  • 生命周期意识:确保在正确的生命周期方法中进行 UI 更新和资源管理。
  • 状态保存:在 onSaveInstanceState 中保存 Fragment 状态,以便在 Activity 重建时恢复。
  • 数据加载:使用 Loader 或其他机制来管理 Fragment 的数据加载。

Fragment 提供了一种灵活的方式来构建动态和响应式的用户界面,使得应用能够更好地适应不同的屏幕尺寸和方向,同时提高了代码的可重用性。

第八章 广播接收BroadcastReceiver

广播机制简介

在 Android 中,广播(Broadcast)是一种系统级的通信机制,允许应用组件(如 Activity、Service、BroadcastReceiver)之间进行松耦合的通信。通过广播,应用可以监听或发送各种事件,如系统启动、屏幕关闭、电池低、设备连接等。

接收系统广播

要接收系统广播,你需要创建一个 BroadcastReceiver 并在其 onReceive() 方法中处理广播事件。然后,你需要在应用的 AndroidManifest.xml 文件中注册 BroadcastReceiver 以及它所监听的广播类型。

示例代码

public class MyBroadcastReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        // 处理接收到的广播
        String action = intent.getAction();
        if (Intent.ACTION_BATTERY_CHANGED.equals(action)) {
            int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
            int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
            float batteryPct = level / (float) scale;
            // 处理电池电量变化
        }
    }
}

// AndroidManifest.xml
<receiver android:name=".MyBroadcastReceiver">
    <intent-filter>
        <action android:name="android.intent.action.BATTERY_CHANGED" />
    </intent-filter>
</receiver>

发送自定义广播

发送自定义广播通常涉及到 sendBroadcast()sendOrderedBroadcast() 方法。自定义广播可以是应用内部的,也可以是跨应用的(需要添加权限)。

示例代码

// 发送自定义广播
Intent intent = new Intent("com.example.myapp.CUSTOM_ACTION");
sendBroadcast(intent);

使用本地广播

本地广播是一种安全的广播通信方式,它不会离开应用的边界,因此不需要声明权限。从 Android 7.0(Nougat)开始,推荐使用 LocalBroadcastManager 来发送和接收本地广播。

示例代码

// 发送本地广播
Intent intent = new Intent("com.example.myapp.LOCAL_CUSTOM_ACTION");
LocalBroadcastManager.getInstance(this).sendBroadcast(intent);

// 接收本地广播
private final BroadcastReceiver localReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        // 处理接收到的本地广播
    }
};

// 在 Activity 中注册和注销本地广播
@Override
protected void onStart() {
    super.onStart();
    LocalBroadcastManager.getInstance(this).registerReceiver(localReceiver, new IntentFilter("com.example.myapp.LOCAL_CUSTOM_ACTION"));
}

@Override
protected void onStop() {
    super.onStop();
    LocalBroadcastManager.getInstance(this).unregisterReceiver(localReceiver);
}

注意事项

  • 接收系统广播通常需要声明权限,除非是应用内部的广播。
  • 广播接收器在 onReceive() 方法执行完毕后,系统会立即停止它。因此,不应该在 onReceive() 中执行耗时操作。
  • 从 Android 8.0(API 级别 26)开始,对于静态注册的隐式广播接收器,应用需要在 AndroidManifest.xml 中声明 android:directBootAwareandroid:exported 属性。
  • 使用本地广播时,要注意注册和注销 BroadcastReceiver 的时机,以避免内存泄漏。

广播机制是 Android 中一种灵活的通信方式,适用于各种场景,从系统事件监听到组件间的消息传递。

第九章 内容提供者

Android 中的 ContentProvider 是一种特殊的服务,它允许不同的应用程序共享数据。它作为数据存储和检索的中介,提供了一种访问数据的标准方式,这种方式类似于数据库,但更加灵活和安全。

主要特点:

  1. 数据共享:允许多个应用程序访问和共享数据。
  2. 封装性:数据的存储细节被封装起来,外部应用通过统一的接口访问数据。
  3. 安全性:可以控制对数据的访问权限,如读取或写入。
  4. 基于URI:使用URI来唯一标识和访问数据。
  5. 支持CRUD操作:提供查询(query)、插入(insert)、更新(update)和删除(delete)数据的方法。

核心组件:

  • ContentResolver:用于发起数据请求,处理来自 ContentProvider 的数据。
  • ContentProvider:继承自Android框架的抽象类,需要实现数据的增删改查等操作。
  • UriMatcher:用于匹配请求的URI,以便 ContentProvider 知道如何处理请求。
  • ContentValues:用于封装要插入或更新的数据。

工作流程:

  1. 定义URI:为 ContentProvider 定义一个唯一的URI,通常包含authority(唯一标识)和path(数据路径)。
  2. 实现 ContentProvider:继承 ContentProvider 类并实现必要的方法,如 onCreate(), query(), insert(), delete(), update()getType()
  3. 注册 ContentProvider:在AndroidManifest.xml中注册 ContentProvider,指定authority和其他必要信息。
  4. 使用 ContentResolver:在应用程序中使用 ContentResolver 来访问 ContentProvider 提供的数据。

示例代码:

public class MyContentProvider extends ContentProvider {
    private static final int CODE_ALL_ITEMS = 1;
    private static final int CODE_SINGLE_ITEM = 2;
    private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);

    static {
        uriMatcher.addURI("com.example.myprovider", "items", CODE_ALL_ITEMS);
        uriMatcher.addURI("com.example.myprovider", "items/#", CODE_SINGLE_ITEM);
    }

    @Override
    public boolean onCreate() {
        // 初始化代码,如数据库连接
        return true;
    }

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

    // 其他方法实现...
}

在AndroidManifest.xml中注册:

<provider
    android:name=".MyContentProvider"
    android:authorities="com.example.myprovider"
    android:exported="true" />

注意事项:

  • 从Android 6.0(API级别23)开始,对于访问外部存储的 ContentProvider,需要动态请求存储权限。
  • 确保 ContentProviderauthorities 在所有使用它的应用程序中保持一致。
  • 考虑到数据的安全性,合理设置访问权限和验证机制。

ContentProvider 是Android中实现数据共享的重要组件,通过它可以实现应用程序之间的数据交互和共享。

案例

让我们通过一个简单的案例来演示如何使用 ContentProvider。假设我们正在开发一个应用程序,该应用程序允许用户记录书籍信息,并希望其他应用程序能够访问这些信息。以下是实现这一功能的基本步骤:

1. 定义数据模型

首先,我们定义一个简单的书籍数据模型,包括书籍的ID、标题和作者。

// 在书籍数据库帮助类中定义
public static final class BookColumns {
    public static final String ID = BaseColumns._ID;
    public static final String TITLE = "title";
    public static final String AUTHOR = "author";
}

2. 创建 ContentProvider

接下来,我们创建一个继承自 ContentProvider 的类,并实现必要的方法。

public class BookProvider extends ContentProvider {
    private static final int BOOKS = 1;
    private static final int BOOK_ID = 2;

    private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    static {
        uriMatcher.addURI("com.example.books", "books", BOOKS);
        uriMatcher.addURI("com.example.books", "books/#", BOOK_ID);
    }

    private BookDbHelper dbHelper;

    @Override
    public boolean onCreate() {
        dbHelper = new BookDbHelper(getContext());
        return true;
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
        SQLiteDatabase db = dbHelper.getReadableDatabase();
        Cursor cursor;
        
        int match = uriMatcher.match(uri);
        switch (match) {
            case BOOKS:
                cursor = db.query(BookColumns.TABLE_NAME, projection, selection, selectionArgs, null, null, sortOrder);
                break;
            case BOOK_ID:
                selection = BookColumns.ID + "=?";
                selectionArgs = new String[]{uri.getLastPathSegment()};
                cursor = db.query(BookColumns.TABLE_NAME, projection, selection, selectionArgs, null, null, sortOrder);
                break;
            default:
                throw new IllegalArgumentException("Unknown URI: " + uri);
        }
        cursor.setNotificationUri(getContext().getContentResolver(), uri);
        return cursor;
    }

    // 实现其他方法(insert、delete、update、getType)...
}

3. 在 AndroidManifest.xml 中注册 ContentProvider

<provider
    android:name=".BookProvider"
    android:authorities="com.example.books"
    android:exported="true" />

4. 使用 ContentResolver 查询数据

在其他应用中,你可以使用 ContentResolver 来查询书籍信息。

public class MainActivity extends AppCompatActivity {
    private static final String AUTHORITY = "com.example.books";
    private static final Uri BOOKS_URI = Uri.parse("content://" + AUTHORITY + "/books");

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

        Cursor cursor = getContentResolver().query(BOOKS_URI, null, null, null, null);
        if (cursor != null && cursor.moveToFirst()) {
            do {
                String title = cursor.getString(cursor.getColumnIndexOrThrow("title"));
                String author = cursor.getString(cursor.getColumnIndexOrThrow("author"));
                // 处理书籍数据
            } while (cursor.moveToNext());
            cursor.close();
        }
    }
}

5. 处理权限和安全性

在实际应用中,你可能需要处理权限请求,确保只有合适的应用程序能够访问你的 ContentProvider。你可以通过在 AndroidManifest.xml 中设置 android:permission 来控制访问权限。

这个案例展示了如何创建一个简单的 ContentProvider 来共享书籍数据,并如何在其他应用中访问这些数据。在实际开发中,你可能还需要实现 insert()delete()update()getType() 方法,并处理更复杂的数据模型和权限管理。

6.运行时权限申请

在 Android 中进行运行时权限申请,特别是访问相册这样的操作,需要遵循以下步骤:

  1. 检查权限:首先,你需要检查应用是否已经获得了所需的权限。这可以通过 ContextCompat.checkSelfPermission() 方法来完成。

  2. 申请权限:如果应用尚未获得权限,你需要使用 ActivityCompat.requestPermissions() 方法来请求用户授权。这通常在一个权限请求码(一个整数值,用于识别请求)中完成。

  3. 处理权限请求结果:用户对权限请求的响应会在 ActivityonRequestPermissionsResult() 方法中返回。你需要在这个回调方法中检查用户是否授予了权限。

以下是一个简单的代码示例,展示了如何请求访问相册的权限:

private static final int REQUEST_CODE = 100; // 权限请求码
private void requestStoragePermission() {
    String[] permissions = {Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE};
    if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED ||
        ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
        ActivityCompat.requestPermissions(this, permissions, REQUEST_CODE);
    } else {
        // 已经获取权限,可以进行相册访问操作
       openImageChooser();
    }
}

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    if (requestCode == REQUEST_CODE) {
        if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            // 权限被授予,可以进行相册访问操作
            openImageChooser();
        } else {
            // 权限被拒绝,提示用户
            Toast.makeText(this, "需要相册权限", Toast.LENGTH_SHORT).show();
        }
    }
}

private void openImageChooser() {
    // 打开相册的操作
    Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
    startActivityForResult(intent, REQUEST_CODE_CHOOSE_IMAGE);
}
//处理用户选择的图像
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == REQUEST_CODE_CHOOSE_IMAGE && resultCode == RESULT_OK && data != null) {
        Uri selectedImageUri = data.getData();
        ImageView imageView = findViewById(R.id.imageView);
        imageView.setImageURI(selectedImageUri);
    }
}

AndroidManifest.xml 文件中,你需要声明所需的权限:

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

从 Android 6.0(API 级别 23)开始,对于危险权限,如访问相册,需要在运行时请求用户授权。如果你的应用目标 API 级别是 23 或更高,那么在安装时不会自动获得这些权限,必须在应用运行时请求用户授权。

如果你的应用目标 API 级别低于 23,但运行在 Android 6.0 或更高版本的设备上,系统会默认授予在清单文件中声明的权限,但用户仍然可以在设置中随时关闭这些权限。

对于 Android 8.0(API 级别 26)及以上版本,对于某些权限,如存储权限,系统引入了更多的限制。例如,如果你的应用目标 API 级别是 26 或更高,那么在请求存储权限时,用户授予一个子权限(如 READ_EXTERNAL_STORAGE)后,系统不会自动授予该权限组中的其他子权限(如 WRITE_EXTERNAL_STORAGE),你需要显式地请求每个所需的权限。

第十章服务

8.1服务

在 Android 中,服务(Service)是一种在后台执行长时间运行操作而没有用户界面的应用组件。服务可以在后台完成多种任务,如播放音乐、处理网络事务、执行文件I/O等。

8.2安卓多线程

8.2.1在子线程中更新UI

在 Android 开发中,UI 更新必须在主线程(也称为 UI 线程)上执行。如果你在子线程中更新 UI,可能会抛出异常或导致应用程序崩溃。要在子线程中更新 UI,你需要使用一些方法将操作切回到主线程。以下是几种在子线程更新 UI 的常用方法:

  1. Handler
    你可以在主线程中创建一个 Handler,然后在子线程中使用这个 Handler 发送消息或 Runnable 来更新 UI。

    public class MainActivity extends AppCompatActivity {
        private Handler mainHandler = new Handler(Looper.getMainLooper());
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            Button button = findViewById(R.id.button);
            button.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    new Thread(new Runnable() {
                        @Override
                        public void run() {
                            // 子线程中执行任务
                            ...
                            // 然后使用 Handler 更新 UI
                            mainHandler.post(new Runnable() {
                                @Override
                                public void run() {
                                    // 在这里更新 UI
                                    TextView textView = findViewById(R.id.textView);
                                    textView.setText("更新后的文本");
                                }
                            });
                        }
                    }).start();
                }
            });
        }
    }
    
  2. Activity.runOnUiThread()
    如果你在 Activity 中,可以直接使用 runOnUiThread() 方法来执行 Runnable。

    public class MainActivity extends AppCompatActivity {
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            Button button = findViewById(R.id.button);
            button.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    new Thread(new Runnable() {
                        @Override
                        public void run() {
                            // 子线程中执行任务
                            ...
                            // 然后使用 runOnUiThread 更新 UI
                            runOnUiThread(new Runnable() {
                                @Override
                                public void run() {
                                    // 在这里更新 UI
                                    TextView textView = findViewById(R.id.textView);
                                    textView.setText("更新后的文本");
                                }
                            });
                        }
                    }).start();
                }
            });
        }
    }
    
  3. View.post()
    任何继承自 View 的组件都有 post() 方法,可以用来在主线程中执行 Runnable。

    public class MainActivity extends AppCompatActivity {
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            Button button = findViewById(R.id.button);
            button.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    new Thread(new Runnable() {
                        @Override
                        public void run() {
                            // 子线程中执行任务
                            ...
                            // 然后使用 view.post 更新 UI
                            button.post(new Runnable() {
                                @Override
                                public void run() {
                                    // 在这里更新 UI
                                    TextView textView = findViewById(R.id.textView);
                                    textView.setText("更新后的文本");
                                }
                            });
                        }
                    }).start();
                }
            });
        }
    }
    
  4. AsyncTask
    AsyncTask 是 Android 提供的一个辅助类,用于在后台线程中执行任务并在主线程中发布结果。不过,从 Android 11 开始,AsyncTask 被标记为弃用。

    public class MainActivity extends AppCompatActivity {
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            Button button = findViewById(R.id.button);
            button.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    new MyAsyncTask().execute();
                }
            });
        }
    
        private class MyAsyncTask extends AsyncTask<Void, Void, Void> {
            @Override
            protected Void doInBackground(Void... voids) {
                // 子线程中执行任务
                ...
                return null;
            }
    
            @Override
            protected void onPostExecute(Void aVoid) {
                // 在这里更新 UI
                TextView textView = findViewById(R.id.textView);
                textView.setText("更新后的文本");
            }
        }
    }
    
  5. LiveData 和 ViewModel
    在使用 Jetpack 架构组件时,你可以使用 LiveDataViewModel 来观察数据变化并更新 UI。

    public class MyViewModel extends ViewModel {
        private MutableLiveData<String> data = new MutableLiveData<>();
    
        public void setData(String data) {
            this.data.setValue(data);
        }
    
        public LiveData<String> getData() {
            return data;
        }
    }
    
    public class MainActivity extends AppCompatActivity {
        private MyViewModel viewModel;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            viewModel = new ViewModelProvider(this).get(MyViewModel.class);
            final TextView textView = findViewById(R.id.textView);
            viewModel.getData().observe(this, new Observer<String>() {
                @Override
                public void onChanged(String s) {
                    textView.setText(s);
                }
            });
    
            Button button = findViewById(R.id.button);
            button.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    new Thread(new Runnable() {
                        @Override
                        public void run() {
                            // 子线程中执行任务
                            ...
                            // 更新 LiveData
                            viewModel.setData("更新后的文本");
                        }
                    }).start();
                }
            });
        }
    }
    

在实际开发中,推荐使用 HandlerrunOnUiThread()View.post() 或 Jetpack 架构组件中的方法来在子线程中更新 UI。这些方法都是线程安全的,并且遵循 Android 的最佳实践。

8.3服务使用

1. 定义服务

创建一个服务类,继承自 Service 类,并实现必要的方法:

java

public class MyService extends Service {
    private final IBinder binder = new LocalBinder();

    @Override
    public IBinder onBind(Intent intent) {
        return binder;
    }

    public class LocalBinder extends Binder {
        MyService getService() {
            // 返回当前的服务实例,允许客户端与服务交互
            return MyService.this;
        }
    }

    @Override
    public void onCreate() {
        super.onCreate();
        // 在服务创建时执行初始化操作
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        // 当服务通过 startService() 方法启动时调用
        return START_STICKY;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        // 在服务销毁时执行清理操作
    }

    // 可以添加其他方法来执行服务的工作
}

2. 服务启动

服务启动有两种方式,单独启动和与activity绑定使用

  1. 直接启动服务

在 Activity 或其他组件中,使用 Intent 启动服务:

这种方式启动服务,声明周期和应用程序一样长 ,适用于创建长时间任务

Intent intent = new Intent(this, MyService.class);
startService(intent);
//当你完成服务的工作后,可以停止服务:
stopService(new Intent(this, MyService.class));
  1. 绑定服务

要与activity进行交互,你可以绑定到服务:

Intent intent = new Intent(this, MyService.class);
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);

private ServiceConnection serviceConnection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName className, IBinder service) {
        MyService.LocalBinder binder = (MyService.LocalBinder) service;
        MyService myService = binder.getService();
        // 现在你可以与服务进行交互
    }

    @Override
    public void onServiceDisconnected(ComponentName arg0) {
        // 服务连接丢失,可以在这里进行处理
    }
};

绑定了服务,可以这样停止:

unbindService(serviceConnection);
stopService(new Intent(this, MyService.class));

通过这种方式启动服务``onStartCommand`函数不会被触发

3. 服务的启动模式

服务可以有不同的启动模式,通过 onStartCommand() 方法的返回值指定:

  • START_STICKY:服务被停止后,系统会尝试重新创建它。
  • START_NOT_STICKY:服务被停止后,系统不会尝试重新创建它。
  • START_REDELIVER_INTENT:如果服务崩溃,系统会重新传递最后一次 Intent

8.4. 服务的生命周期

服务有三种主要的生命周期回调方法:

  • onCreate():当服务首次创建时调用。
  • onStartCommand(Intent intent, int flags, int startId):每次服务通过 startService() 方法启动时调用。
  • onDestroy():当服务销毁时调用。

注意事项

  • 服务运行在主线程中,不应该在服务中执行耗时操作。如果需要,应该使用 AsyncTaskIntentServiceJobIntentService
  • 服务会消耗电池,因此应该尽可能高效地使用服务,并在不需要时停止服务。
  • 从 Android 8.0(API 级别 26)开始,后台服务的使用受到限制。对于需要长时间运行的操作,可以考虑使用 JobSchedulerWorkManager

服务是 Android 应用中执行后台任务的重要组件,合理使用服务可以提高应用的性能和用户体验。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值