2023BaiduAndroidlec5-lec6

2023BaiduAndroidlec5-lec6

SYuan03 Lv4

lec5-Android四大组件

大作业要求:

Activity

用户与应用程序交互的界面,每一个Activity都会代表一个屏幕,负责展示屏幕并处理用户的操作。每个Activity都是独立的,它们之间可以进行跳转、数据传递等互动

  • 实现xml布局

  • 实现Activity类文件

    1
    2
    3
    4
    5
    6
    class MainActivity: Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main) // 给Activity设置布局
    }
    }
  • 给Activity设置布局

  • 在manifest中声明Activity

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <activity
    android:name=".MainActivity"
    android:exported="true">
    <intent-filter>
    <action android:name="android.intent.action.MAIN" />

    <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
    </activity>
    </application>

    Intent-filter 中的配置代表这个Activity是应用程序入口,点击桌面图标可进入

    exported 属性代表Activity是否应用外开放

Activity的生命周期

  • onCreate()

    必须实现,首次创建Activity触发

  • onStart()

    当 Activity 进入"已开始"状态时,系统会调用此回调

  • onStop()

    Activity 不再对用户可见,说明其已进入"已停止"状态

  • onRestart()

    当用户回到Activity(非首次)

  • onResume()

    当Activity 可见并进入可交互状态

  • onPause() :比如应用程序切换到后台就是pause+stop

    当Activity 可见并进入不可交互状态

  • onDestory()

    当Activity 销毁之前回调

Activity 显示调用

从一个Activity 启动另一个 Activity

startActivity 无返回值

1
2
val intent = Intent(this, SecondActivity::class.java)
startActivity(intent)

startActivityForResult 有返回值回调

1
2
3
4
5
6
7
startActivityForResult(Intent(this, SecondActivity::class.java), 1000)

override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
if(1000 == requestCode) {

}
}

RequestCode的含义

第三个参数 1000 是请求代码(Request Code),用于标识此次启动 Activity 的请求。它是一个整数值,用于标记这个请求,以便在返回结果时可以识别是从哪个请求返回的结果。

Activity 隐式调用

隐式调用是指应用程序中不需要明确指定Activity 名称的调用方式,需要使用Intent对象来指定要启动的Activity 的类型和动作。Intent 对象包含了要启动的Activity的category 和action 信息,当调用startActivity() 方法时, Android 系统会根据Intent 对象中的信息来查找匹配的Activity,并启动该Activity。隐式调用可能存在一些安全问题,需要增加校验,防止Activity 劫持

Activity 跳转时生命周期

ActivityA 跳转 ActivityB, A与B均不透明,生命周期变化

A:onPause

B:onCreate -> onStart -> onResume

A:onStop

ActivityB 回到 ActivityA,生命周期变化

B:onPause

A:onRestart -> onStart -> onResume

B:onStop -> onDestory

演示

启动第一个activity

依次是Create Start Resume

Activity的任务栈

遵守先进后出罢了

Activity 的四种启动模式

  • Standard(标准模式)

    标准模式是默认的启动模式。
    每次启动Activity时都会创建一个新的实例 ,并放置在任务栈的顶部。

  • SingleTop(单顶模式)

    当Activity位于任务栈的顶部时,再次启动该Activity时不会创建新的实例,而是会调用已存在实例的onNewIntent()方法。如果不在顶部,则会创建新的实例

  • SingleTask(单任务模式)

    单任务模式下,每个任务栈只能包含一个该Activity的实例。如果要启动的Activity已经存在于任务栈中,则会将该任务栈调到前台,触发该Activity的onNewlntent()方法,并将该Activity之上的Activity全部出栈。如果要启动的Activity不存在于任何任务栈中,则会创建新的任务栈并将该Activity放入其中

  • SingleInstance(单实例模式)

    单实例模式是最为独特的一种启动模式。在单实例模式下,该Activity会独占一个任务栈,并且该任务栈中只能有一个实例。无论从哪个应用程序中启动该Activity,都会共享同一个实例。如果要启动的Activity不存在于任何任务栈中,则会创建新的任务栈并将该Activity放入其中。

这些启动模式可以通过在AndroidManifest.xml文件中为Activityi设置android:launchMode属性来指定。

单实例使用场景例子 调用微信支付—>回退返回到自己的程序

Fragment(注意并非四大组件之一

Fragment,在Activity 中负责用户界面部分,可以将多个Fragment 组合在一个Activity 中来创建多窗口UI,或者在Activity 中重复使用某个Fragment。

  • Fragment 是依赖于Activity的,不能独立存在的。
  • 一个Activity 里可以有多个Fragment。
  • 一个Fragment 可以被多个Activity 重用
  • Fragment 有自己的生命周期,并能接收输入事件。
  • 我们能在Activity 运行时动态地添加或删除Fragment。

Fragment 的生命周期

  • onAttach():当Fragment 与Activity 关联时调用。在这个阶段可以获取到与Fragment 关联的Activity 实例。
  • onCreateView():创建Fragment 的视图。在这个阶段,可以定义Fragment 的布局,并返回布局的根视图。
  • onActivityCreated():当与Fragment 关联的Activity 的onCreate()方法返回时调用。
  • onDestroyView():销毁Fragment 的视图。在这个阶段,应该清理与视图相关的资源。
  • onDetach(): 当Fragment 与Activity 解除关联时调用。

Fragment 的实现

  1. 创建一个新的类,这个类需要继承自Fragment 类

    1
    2
    3
    public class MyFragment: Fragment() {
    // ...
    }
  2. 重写onCreateView方法。这个方法是Fragment 创建它的用户界面时调用的。在这个方法中,返回一个包含Fragment 布局的View。

    1
    2
    3
    override fun View onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?):View? {
    return inflater.inflate(R.layout.fragment_my, container, false)
    }

Activity 添加Fragment

Fragment可以通过两种方式被添加到Activity 中

静态添加:在Activity 的布局中使用<fragment> 标签来声明Fragment。

动态添加:在运行时通过FragmentManager 和FragmentTransaction 来添加、替换或者移除Fragment

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">

<!-- 静态添加fragment -->
<fragment
android:name="com.baidu.androidlearn.lesson5.MyFragment"
android:id="@+id/myfragment"
android:layout_width="match_parent"
android:layout_height="match_parent"/>

<!-- 动态添加fragment -->
<FrameLayout
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent"/>

</LinearLayout>
1
2
3
4
5
val myFragment: Fragment = MyFragment()
val fragmentManager: FragmentManager = supportFragmentManager
val fragmentTransaction: FragmentTransaction = fragmentManager.beginTransaction()
fragmentTransaction.add(R.id.fragment_container, myFragment)
fragmentTransaction.commit()

R.id.fragment_container 是要添加Fragment的布局的id

FragmentTransaction

  • add():添加Fragment 到 Activity 界面中
  • remove():移除 Activity 中的指定 Fragment
  • replace()系列:通过内部调用 remove() 和add() 完成 Fragment 的修改
  • hide() 和show():隐藏和显示 Activity 中的 Fragment
  • commit():提交事务,对 Fragment 的改动都必须通过调用 commit() 方法完成提交
  • commitAllowingStateLoss():和commit()类似,但是允许在Activity状态保存后执行commit
  • addToBackStack():该方法用于将当前的事务添加到回退栈中 ,当用户点击后退键时,将会导航到上一个Fragment状态。

Activity 与 Fragment 通讯:使用Bundle

  1. 在Activity 中创建Bundle 并设置到Fragment 中

    1
    2
    3
    4
    val bundle = Bundle()
    bundle.putString("key", "value")
    val fragment = MyFragment()
    fragment.setArguments(bundle)
  2. 在Fragment 中获取参数

    1
    2
    3
    if(getArguments() != null) {
    val value = getArguments().getString("key")
    }

Fragment 与Activity 通讯

  1. 定义接口

    1
    2
    3
    interface FragmentListener {
    fun test()
    }
  2. Activity实现接口

    1
    2
    3
    4
    5
    class MyFragmentActivity: FragmentActivity(), FragmentListener {
    override fun test() {
    // ...
    }
    }
  3. Fragment调用接口

    1
    2
    3
    if (activity is FragmentListener) {
    (activity as FragmentListener).test()
    }

比如同一个activity下有多个fragment,那么两个fragment之间想要通信就可以通过都与activity通信来实现

BroadCast

没怎么仔细听

广播介绍

广播是一种跨应用程序的消息传递机制,允许应用程序发送和接收系统级或应用级的事件。广播的目的是为了实现不同组件之间的解耦和通信

符合设计模式中的 观察者模式,

也符合 发布订阅模式?

标准广播

一种异步进行的广播,当广播发出后,所有的接收器几乎同时接收到这个消息,这种广播的执行效率比较高,但是无法被截断。标准广播主要用于对性能要求较高的场景。

有序广播

是一种同步进行的广播,当广播发出后,系统会按照优先级顺序逐个传递给接收器。接收器在处理完广播后可以决定是否继续传递给下一个接收器,这样就实现了对广播的截断。有序广播主要用于对数据安全性和顺序要求较高的场景。

广播的类型(根据来源和作用范围划分

  1. 系统广播 ( System Broadcast )
    系统广播是由Android系统发出的广播,用于通知应用程序系统状态的变化,例如电池电量变化、网络状态变化、屏幕关闭等。应用程序可以通过注册广播接收器( BroadcastReceiver)来监听和处理这些系统广播
  2. 自定义广播( Custom Broadcast )
    自定义广播是由应用程序自己发出的广播,用于向其他应用程序或应用程序内的不同组件传递消息。自定义广播可以是标准广播也可以是有序广播,开发者可以根据实际需求选择合适的广播类型。
  3. 本地广播( Local Broadcast )
    本地广播是一种仅限于应用程序内部使用的广播,它使用Android Support Library中的LocalBroadcastManager 进行管理。本地广播相比于全局广播更加安全,因为它不会泄露应用程序内部的信息给其他应用程序

应用内部尽量使用本地广播,更安全

常见系统广播

自定义广播:自定义receiver继承BroadcastReceiver()

直接看demo6的NetStateChangeReceiver

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class NetStateChangeReceiver : BroadcastReceiver() {

override fun onReceive(context: Context?, intent: Intent?) {
if ((ConnectivityManager.CONNECTIVITY_ACTION == intent?.action)) {
// ...
}
}

fun registerReceiver(context: Context?) {
val intentFilter: IntentFilter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)
context?.registerReceiver(this, intentFilter)
}

fun unRegisterReceiver(context: Context?) {
context?.unregisterReceiver(this)
}
}

静态广播和动态广播

Service

Service是Android中的一个核心组件,它是一个在后台运行的组件,用于在不与用户交互的情况下执行一些长时间运行的操作。Service中的操作可以在用户不使用应用程序时运行,从而使我们可以在后台执行任务,如下载文件、播放音乐等。Service不具备用户界面,因此它不会对用户界面产生影响。

Service是在主线程运行的

适合service的场景:

  • 音乐播放器
  • 后台定位服务
  • 网络同步
  • 长时间运行的后台任务
  • 跨进程通信 (IPC)

Service的类型

Service 的生命周期:有两种

启动服务

当使用startService() 方法启动服务时,服务会在后台无限期运行,直到它自行停止或被其他组件(如Activity)调用stopService() 方法停止

绑定服务

当使用bindService() 方法鄉定服务时,服务与调用它的应用组件(如Activity)建立了一个持续的连接。组件可以与服务进行双向通信

创建service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class MyService : Service() {

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// 在此处处理服务的任务,例如启动一个线程来执行长时间运行的操作
Log.i(TAG, "onStartCommand")
return super.onStartCommand(intent, flags, startId)
}

override fun onBind(intent: Intent?): IBinder? {
// 如果不允许绑定服务,可以返回null
Log.i(TAG, "onBind")
return myBinder
}


override fun onDestroy() {
// 当服务被停止时,可以在这里执行一些清理工作
Log.i(TAG, "onDestroy")
super.onDestroy()
}
}

注意service也需要在manifest中注册

另外还需要再Manifest 中注册<service android:name=“.MyService”>

启动service

启动服务(startService):

val intent = Intent(this, MyService::class.java)
startService(intent)

停止服务(stopService):

val intent = Intent(this, MyService::class.java)
stopService(intent)

绑定服务 (bindService):

val intent = Intent(this, MyService::class.java)
bindService(intent, serviceConnection, Context. BIND_AUTO_CREATE)

取消绑定服务 (unbindService):

unbindService(serviceConnection)

ContentProvider

简介

ContentProvider URI

ContentProvider使用URI ( Uniform Resource Identifier,统一资源标识符)来唯一标识和访问不同的数据资源。ContentProvider URI通常具有以下结构:

  • content:// :这是一个固定的前级,表示这是一个ContentProvider的URI。
  • content://authority/path/id
  • authority:用于标识ContentProvider的唯一字符串,通常采用应用的包名加上一个自定义的字符串。在AndroidManifest.xml文件中注册ContentProvider时 ,需要指定它的aandroid:authorities属性。
  • path:表示数据资源的类型或类别,可以通过在URI中指定表名作为路径来访问特定表的数据
  • id:这是一个可选部分,用于表示特定数据项的ID。在URI中加入ID可以让您访问特定记录的数据。

定义ContentProvider

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class MyContentProvider : ContentProvider() {
override fun onCreate(): Boolean {
// 初始化ContentProvider,例如创建数据库等
return false
}
override fun query(uri: Uri, projection: Array<out String>?, selection: String?,
selectionArgs: Array<out String>?, sortOrder: String?): Cursor? {
// 根据URI查询数据
return null
}
override fun getType(uri: Uri): String? {
// 返回当前URI对应的数据的MIME类型
return null
}
override fun insert(uri: Uri, values: ContentValues?): Uri? {
// 根据URI插入数据
return null
}
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
// 根据URI删除数据
return 0
}
override fun update(uri: Uri, values: ContentValues?, selection: String?,
selectionArgs: Array<out String>?): Int {
// 根据URI更新数据
return 0
}

}

在manifest中定义contentprovider

1
2
3
4
<provider
android:name=".lesson5.MyContentProvider"
android:authorities="com.baidu.myapp.provider"
android:exported="false" />

在需要访问数据的地方,通过ContentResolver与ContentProvider进行交互

1
2
3
4
5
6
// 获取ContentResolver对象
val contentResolver: ContentResolver = getContentResolver()
// 构造ContentProvider的URI
val uri: Uri = Uri.parse("content://com.baidu.myapp.provider/your_table_name")
// 使用ContentResolver进行数据查询
val cursor: Cursor? = contentResolver.query(uri, null, null, null, null)

FileProvider

name是对外的

path是自己的真实路径

作业

视频的页面可以先留空

实现能选就行了,存到哪以后再说

点击首页的加号进入新闻发布页

lec6-Android图形图像和多媒体实现

图片

图片格式介绍

图片内存大小

对于Bitmap对象,有几种常见色彩格式

  • Bitmap.Config.ALPHA_8:每个像素占1个字节
  • Bitmap.Config.RGB_565:每个像素占2个字节
  • Bitmap.Config.ARGB_4444:每个像素占2个字节
  • Bitmap.Config.ARGB_8888:每个像素占2个字节

1.使用ImageView加载图片

如Kotlin API

1
2
3
4
5
6
7
8
9
10
11
// 找到ImageView控件
ImageView imageView = findViewById(R.id.imageView);

// 设置图像资源
imageView.setImageResource(R.drawable.sample_image);

// 设置透明度(0为完全透明,1为完全不透明)
imageView.setAlpha(0.5f);

// 设置缩放类型
imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);

ImageView组件:xml

1
2
3
4
5
6
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/plane"
android:scaleType="center"
/>

scaleType的值可以修改为上图所列

dp与sp

sp做单位会受缩放影响

2.使用android内置API加载图片

1
2
val bitmap: BitMap = BitmapFactory.decodeResource(getResources(), R.drawable.example_image)
imageView.setImageBitmap(bitmap)

基本不怎么用了,网络图片加载还要先异步请求

3.第三方图片加载库

推荐使用Glide或者Fresco

1
2
3
4
5
6
Glide.with(this)
.load(imageUrl)
.placeholder(R.drawable.placeholder) // 设置占位图
.error(R.drawable.error) // 设置加载错误时显示的图片
.thumbnail(0.5f) //设置缩略图(加载原图的一部分)
.into(imageView);// 放入imageView中

图片缓存

三级缓存策略

三级缓存减少不必要的网络请求和内存占用

  • 内存缓存
  • 本地缓存
  • 网络缓存

图片处理

压缩、裁剪、缩放、滤镜、拼接、合成、旋转

音频

MediaPlayer的使用方式

  1. 创建MediaPlayer实例

    1
    val mediaPlayer: MediaPlayer = MediaPlayer()
  2. 设置音频数据源

    1
    2
    3
    4
    // 设置本地音频数据源
    mediaPlayer.setDataSource("/path/to/your/audio/file")
    // 设置网络音频数据源
    mediaPlayer.setDataSource("http://example.com/your/audio/file.mp3")
  3. 准备播放

    1
    2
    3
    4
    // 同步准备
    mediaPlayer.prepare()
    // 异步准备
    mediaPlayer.prepareAsync()
  4. 设置监听器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 设置准备完成监听器:
    mediaPlayer.setOnPreparedListener(object: MediPlayer.OnPreparedListener{
    override fun onPrepared(mp: MediaPlayer?) {
    // 异步准备完成回调
    }
    })
    // 设置播放完成监听器:
    mediaPlayer.setOnCompletionListener(object: MediaPlayer.OnCompletionListener{
    override fun onConpletion(mp: MediaPlayer?) {
    // 播放完成后,可以进行相应的操作
    }
    })
  5. 控制播放

    1
    2
    3
    4
    5
    6
    7
    8
    // 播放
    mediaPlayer.start()
    // 暂停
    mediaPlayer.pause()
    // 停止
    mediaPlayer.stop()
    // 释放资源
    mediaPlayer.release()

prepare()的调用可能需要很长时间,所以切勿从应用的界面线程中调用它,界面中任何响应时间超过十分之一秒的操作都会导致明显的暂停,并让用户觉得应用运行缓慢。

推荐使用prepareAsync() 在后台准备媒体,准备就绪后系统会调用通过 setOnPreparedListener() 配置的 MediaPlayer.OnPreparedListeneronPrepared() 方法。

参考:MediaPlayer官方手册

在Service中使用MediaPlayer

SoundPool的使用

AudioManager控制音频焦点

视频

播放视频的方式

VideoView

1
2
3
4
5
<VideoView
android:id="@+id/video_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
1
2
3
4
5
6
7
8
9
10
11
12
13
// 加载视频并播放
val videoView = findViewById<VideoView>(R.id.video_view).apply {
setVideoURI(Uri.parse("https://globalimg.sucai999.com/uploadfile/20211210/267440/132836000096069452.mp4"))

}

// MediaController提供播放、暂停、快进、后退等按钮
videoView.setMediaController(MediaController(this).apply{
// 锚点视图,点击依附视图可以显示或隐藏控制按钮
setAnchorView(videoView)
// 播放视频
start()
})

SurfaceView介绍

TextureView介绍

SurfaceViewTextureView都是Android中用于显示图像、视频或动画的视图组件,但它们在实现和使用上有一些区别。

  1. SurfaceView: SurfaceView是一个特殊的视图,它允许你在一个独立的绘图表面上绘制内容。SurfaceView适用于需要频繁更新的图像、视频播放、动画等场景,因为它允许在后台线程中进行绘制,避免了在主线程中的UI阻塞。主要特点和用途包括:
    • 可以在后台线程(非UI线程)中绘制内容,避免主线程阻塞。
    • 使用双缓冲技术,提高绘制性能,避免闪烁现象。
    • 适合实现实时视频播放、相机预览等需要频繁刷新的场景。
    • 对于自定义绘图、动画或游戏等复杂场景,更为灵活和强大。
  2. TextureView: TextureView是一个可以显示视频或动画的视图组件。它与SurfaceView不同之处在于,它在视图层级中是一个普通的View,可以与其他视图组件一起进行布局,并在Android的硬件加速支持下提供更高的灵活性。主要特点和用途包括:
    • 可以在布局中与其他视图组件一起使用,更容易实现复杂的布局。
    • 支持硬件加速,提供更高的绘制性能。
    • 适用于视频播放、动画等场景,可以通过MediaPlayerSurfaceTexture来实现视频的播放。
    • 可以与动画效果、透明度等属性结合使用,实现更丰富的视觉效果。

选择使用SurfaceView还是TextureView取决于你的应用需求。如果你需要频繁更新、复杂的绘图逻辑或更好的性能,那么SurfaceView可能是更好的选择。如果你希望在布局中更灵活地使用,或者需要与其他视图组件结合使用,那么TextureView可能更适合你。

MeidaPlayer+SurfaceView播放视频

MeidaPlayer+TextureView播放视频

ExoPlayer

更加灵活,可拓展性比MediaPlayer强

基本教程:Hello World

ExoPlayer is deprecated,原有的ExoPlayer已经被Media3 ExoPlayer取代

build.gradle中不使用com.google.android.exoplayer:exoplayer-core:2.12.0了,新增如下内容

1
2
3
implementation "androidx.media3:media3-exoplayer:1.1.0"
implementation "androidx.media3:media3-exoplayer-dash:1.1.0"
implementation "androidx.media3:media3-ui:1.1.0"

这一段不知道是啥,别人的笔记

如何使用MediaPlayer访问本地文件,并请求权限

我们在/storage/emulated/0/Music目录上传了guitar.mp3音乐文件,并希望播放此音乐

错误做法:直接设置mediaPlayer.setDataSource(/storage/emulated/0/Music/guitar.mp3)mediaPlayer.setDataSource(Environment.getExternalStorageDirectory().path + "/Music/guitar.mp3")都会报错java.io.FileNotFoundException: /storage/emulated/0/Music/guitar.mp3: open failed: EACCES (Permission denied),意思没有访问的权限。

探索过程:

  1. 无效:设置android:requestLegacyExternalStorage="true"。This solution is temporary and won’t work on Android 11
  2. 有效:实现运行时权限获取 ,顺便参考storage access in android 11 ,个人感觉allowPermissionForFile()那个函数不能直接同步请求,应该放在异步任务中完成

解决方法:

  1. 在settings->Apps->Your_app->Permissions手动打开存储权限,不过这只适用于我们本地开发这么做,实际情况下需要引导用户打开权限

  2. 请求用户打开权限

  • ActivityManifest.xml 适用于API<=32的情景,若API>32,会提醒you should instead use one or more new storage permissions: READ_MEDIA_IMAGES, READ_MEDIA_VIDEO or READ_MEDIA_AUDIO
1
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
  • MediaPlayerActivity.kt

假如在用户界面同步请求权限,会导致应用程序卡主、长时间无响应甚至crash,所以我们应当在后台线程或异步任务重进行权限请求,避免阻塞主线程。这里借助

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// generated by chatgpt

/** 要执行的任务 */
private fun playMusic() {
mediaPlayer.setDataSource(Environment.getExternalStorageDirectory().path + "/Music/guitar.mp3")
mediaPlayer.setAudioAttributes(
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build()
)
mediaPlayer.prepareAsync()
// 准备好即播放
mediaPlayer.setOnPreparedListener { mp -> mp?.start() }
}

private val PERMISSION_REQUEST_CODE = 1

// 检查是否取得权限,若没有对应权限则在coroutine环境下请求权限
private fun checkAndRequestPermissionsAsync() {
GlobalScope.launch(Dispatchers.Main) {
val permissionGranted = checkPermissions()
if (!permissionGranted) {
// 请求权限
requestPermissionsAsync()
} else {
// 拥有权限,可以读取文件
playMusic()
}
}
}

private suspend fun checkPermissions(): Boolean = withContext(Dispatchers.IO) {
ContextCompat.checkSelfPermission(
this@MediaPlayerDemoActivity,
Manifest.permission.READ_EXTERNAL_STORAGE
) == PERMISSION_GRANTED
}

private suspend fun requestPermissionsAsync() = withContext(Dispatchers.Main) {
// 要求mini api >= 23,这个可以在build.gradle(Module:app)里改
requestPermissions(
arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE),
PERMISSION_REQUEST_CODE
)
}

override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == PERMISSION_REQUEST_CODE) {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
playMusic()
} else {
// 用户拒绝了权限,根据需要处理权限未授予的情况
Toast.makeText(this, "You need to allow reading audio permission", Toast.LENGTH_SHORT).show()
}
}
}

上述方法是获取本地存储的资源,简单起见可以将文件放在raw文件夹下,然后通过setDataResource的方法获取(上述已给出实例)

如果要实现后台播放音乐,就需要用到Service的知识,暂不详述(其实不会x

随堂作业

不用能上下滑动,右边没有收藏分享啥的也可以

lec7-Android持久化

什么是持久化

持久化是将数据保存到可永久保存的存储设备中的过程。

持久化是将程序数据在 持久状态瞬间状态 见转换的机制

三种数据持久化方式

Shared Preferences

  • Android系统 提供的一种轻量级的存储方式。

  • 常用于存储简单的 应用设置、配置数据 或 状态信息。

  • 使用 键值对 的方式来存储数据。

  • 支持 多种不同数据类型 (如字符串、整数、浮点数、布尔值和长整型)

  • 相关数据存放在 xml 中。 xml文件的存放路径为:

    /data/data/packageName/shared_prefs/目录下

获取方式

  1. 通过Context对象获取: Context 的 getSharedPreferences() 方法,需要传入2个参数: 文件名模式 ( 目前只有MODE_PRIVATE, 表示只有当前应用可以访问这个Shared Preferences)

    1
    2
    // 代码示例
    val sharedPreferences = context.getSharedPreferences("my_prefs", Context.MODE_PRIVATE)
  2. 通过Activity对象获取:Activity 中的 getPreferences() 方法, 它默认将当前活动的类名作为文件名,只需要传入操作模式作为参数

    1
    val sharedPreferences = getPreferences(MODE_PRIVATE);
  3. 通过PreferenceManager类获取:PreferenceManagergetDefaultSharedPreferences(), 它接收一个 Context参数

    1
    val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)

如何写入数据

使用Editor对象来写入数据。Editor对象可以通过SharedPreferences.edit()方法获取

1
2
3
4
5
6
7
8
9
10
11
12
// 1. 创建一个SharedPreferences对象
val sharedPreferences = getPreferences(Context.MODE_PRIVATE)
// 2. 实例化SharedPreferences.Editor对象
val editor =sharedPreferences.edit()
// 3. 将获取过来的值放入文件
editor.putString("userName", "野比")
editor.putInt("age", 17)
// 4. 提交(异步提交)
editor.apply()

// 4. 提交(同步写入)
editor.commit()

同步写入会阻塞当前线程,直到数据写入完成。如果写入数据较多,会导致界面卡顿。因此建议写入大量数据时使用apply()方法异步写入(意思就是不会立即生效

其他操作

1
2
3
4
5
6
7
8
9
10
11
12
// 创建(获取)一个SharedPreferences对象
val sharedPreferences = getPreferences(Context.MODE_PRIVATE)
// 读取数据
val name = sharedPreferences.getString("userName", ""); // ""是默认值,当键"userName"不存在或者没有对应的值时,getString()方法会返回这个默认值
val age = sharedPreferences.getInt("age", 0)
// 删除数据
val editor = sharedPreferences.edit()
editor.remove("userName")
editor.apply()
// 清空数据,一般不建议使用
editor.clear()
editor.apply()

感觉可以用在某个activity的临时数据上,但要注意每个activity要有其独立的sp,名字要区分开,否则会导致写到相同的,清除了同一个文件,当然这也需要从代码上进行物理空间的解耦

优点

缺点

容量有限、不支持复杂的数据结构、难以维护、安全性较低

跨进程不安全、全量写入、加载缓慢

卡顿

KV存储(简单了解,不是android原生的,SP的衍生

常用的有MMKV,Booster,PreferceDataStore, UniKV, 代理方式优化写入性能

UniKV:百度在用的

代理方式:字节在用

  • MMKV
    • 内存准备
    • 数据组织
    • 写入优化

文件存储

Android的文件存储方式,适用于存储本地的二进制数据。可以通过读写FileInputStream 和 FileOutputStream来操作文件

区别于SharedPreference,更适合用来存储html数据、下载文件、日志打印、照片、视频等。

存储空间

  • /data/data
    • 用户应用 的安装目录,如百度地图的安装路径是 /data/data/com.baidu.com, 该目录需要root权限
  • /system
    • 系统应用 的安装目录
  • /storage
    • 内置存储卡外置SD卡 的挂载点
    • 外置SD卡挂载节点:/storage/sdcard1
    • 内置存储卡挂载节点:/storage/emulated/0
    • 不同的设备挂载节点不同,有的可能在 /mnt下

内部存储空间

  • 应用专属空间:创建其他应用不需要访问或不应访问的文件。
    • getFilesDir(), data/data/包名/files/,用于持久文件
    • getCacheDir(), data/data/包名/cache/,用于暂存文件

外部存储空间

  • getExternalFilesDir(), /mnt/sdcard/Android/data/包名/files/,用于持久文件
  • getExternalCacheDir(), /mnt/sdcard/Android/data/包名/cache/,用于暂存文件

具体代码应用

Context提供两种方法来打开数据文件里的文件IO流

  • openFileInput(String name), 读文件
  • openFileOutput(String name, int mode), 写文件,其中第二个参数指定打开文件的模式
    • MODE_PRIVATE, 默认操作模式,当指定同样文件名的时候会覆盖原有文件内容。
    • MODE_APPEND, 如果文件不存在就创建文件,如果存在就往后追加。

读取文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 使用openFileinput() 以信息流的形式读取文件
try {
// 创建一个FileInputstream 对象
val fis: FileInputStream = openFileInput("filename.txt")
// 读取文件内容
val isr = InputStreamReader(fis)
val bufferedReader = BufferedReader(isr)
var line: String?
while (bufferedReader.readLine().also {line = it } != null) { Log.d(TAG, line!!) }
// 关闭文件流
bufferedReader.close()
isr.close()
fis.close()
} catch (e: IOException) {
e.printStackTrace)
}

写入文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 使用 openFileOutput()写入文件
try {
// 创建一个FileOutputStream 对象
val fos: FileOutputStream = openFileOutput("filename. txt", MODE_PRIVATE)
// 写入文件内容
val data = "Hello, world!"
val bytes = data.toByteArray()
fos.write(bytes)

// 关闭文件流
fos.close()
} catch (e: IOException) {
e.printStackTrace)
}

外部存储空间

读取外部存储中的文件之前,需要在AndroidManifest.xml文件中添加相应的权限声明

1
<user-permission adnroid:name="android.permission.READ_EXTERNAL_STORAGE" />

之后跟内部存储方式的区别只是获取file的路径不同

1
2
3
4
5
// 指定外部存储的路径和文件名
val file = File(Environment.getExternalStorageDirectory(), "filename.txt")

// 创建文件输入流
val fis = FileInputStream(file)

数据库存储

SQLite数据库

  • SQLite是一款轻量级关系型数据库,它的运行速度快占用资源少,通常只需要几百k的内存即可。
  • 因而特别适合移动设备上使用
  • SQLite不仅支持标准的SQL语法,还遵循数据库事务管理
  • 只要以前用过其他关系型数据库就可以很快上手

创建数据库

  • 为了更方便地管理数据库,提供了一个帮助类 SQLiteOpenHelper
  • 新建一个类MyDatabaseHelper,继承SQLiteOpenHelper,实现三个方法:构造函数,onCreate,onUpgrade
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MyDBHelper(context: Context): SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
override fun onCreate(db: SQLiteDatabase) {

}

override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, nerVersion: Int) {

}

companion object {
const val DATABASE_VERSION = 1
const val DATABASE_NAME = "Book.db"
}
}
1
2
3
4
5
6
override fun onCreate(db: SQLiteDatabase) {
// Book表的加标语句, id是整型主键自增长
// text表示文本类型
val createBook = "create table Book(id integer primary key autoincrement, author text, name text)"
db.execSQL(createBook)
}

添加数据

  • Android提供了一系列的辅佐性方法,使得在Android中,可以不去写SQL语句也能完成增删查改操作。

  • 调用SQLiteOpenHelper的**getReadableDatabase()getWritableDatabase()**方法可以用于创建和升级数据库

  • 这两个方法会返回一个SQLiteDatabase对象,借助这个对象就可以对数据库进行增删查改操作

  • SQLiteDatabase提供了一个insert() 方法,这个方法就是专门用于添加数据,它接收3个参数:

    • 第一个参数:表名,要操作的表
    • 第二个参数:null,在未指定添加数据的情况下给某些可以为空的列自动复制NULL
    • 第三个参数:要添加的数据,它是一个ContentValues对象,提供一些列的put()方法重载来传入数据
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // 访问数据座,先实例化 MVDBHelper 的子类
    val dbhelper = MyDBHelper(this)

    // 以写入模式获取数据存储库
    val db: SQLiteDatabase = dbhelper.getWritableDatabase()
    val values = ContentValues()

    // 插入第一行数据
    values.put("author""郭霖")
    values.put('name'"第一行代码")
    db.insert("Book", null, values)

    // 插入其它数据前先清空
    values.clear()

    // 插入第二条数据
    values.put("author" "张三")
    values.put("name", "I/FARI")
    db.insert("Book", null, values)

查询数据

  • SQLiteDatabase提供了query() 方法对数据进行查询,根据需要指定7个参数

  • 调用query()方法后会返回一个Cursor对象,查询到得所有数据都将从这个对象中取出。

    query() 参数 对应SQL部分
    table from table_name
    columns select column1, column2
    selection where column = value
    selectionArgs (where中占位符的值)
    groupBy group by coulmn
    having having column = value
    orderBy order by column
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
val dbhelper = MyDBHelper(this)
// 以读取模式获取数据存储库
val db: SQLiteDatabase = dbhelper.getReadableDatabase()
val cursor = db.query("Book", null, null, null, null, null, null)
val data = StringBuilder()
var i = 1
with(cursor) {
while (moveToNext()) {
val author = getString(getColumnindexOrThrow("author")
val name = getString(getColumnIndexOrThrow("name"))
data.append("第"+ i +"行数据,author :" + author + ", name : " + name + "\n\n")
i++
}
}
cursor.close()

更新数据

  • SQLiteDatabase提供了update()方法对数据进行更新

  • 这个方法接收4个参数:

    • table:String ,要更新的表名
    • values : ContentValues,要更新的数据
    • whereClause:String ,约束要更新哪行的数据
    • whereArgs:String[],上一个参数中占位符的值
    1
    2
    3
    4
    5
    6
    7
    8
    9
    val dbhelper = MyDBHelper(this)
    // 以写入模式获取数据存储库
    val db: SQLiteDatabase = dbhelper.getWritableDatabase()
    val values = ContentValues().apply{
    put("name", "第二行代码")
    }
    // ?是一个占位符
    // 第四个参数提供一个字符串数组,为第三个参数中的每个占位符指定相应的内容
    db.update("Book", values, "author = ?", arrayOf("郭霖"))

删除数据

  • SQLiteDatabase中提供了delete()方法删除数据

  • 这个方法接收了个参数:

    • table :String,要更新的表名
    • whereClause :String,约束要删除哪行的数据
    • whereArgs :String[],上一个参数中占位符的值
    1
    2
    3
    4
    5
    // 以写入模式获取数据存储库
    val db: SQLiteDatabase = dbhelper.getWritableDatabase0)
    // ?是一个占位符
    // 第四个参数提供一个字符串数组,为第三个参数中的每个占位符指定相应的内容
    db.delete("Book","author = ?",arrayOf("郭霖"))

其他的存储方式

作业

引申:

用户数据如何在不同设备之间共享?

其实是有个云服务器存储数据的

lec8-网络编程

lec9-

  • 标题: 2023BaiduAndroidlec5-lec6
  • 作者: SYuan03
  • 创建于 : 2023-07-23 22:33:10
  • 更新于 : 2024-03-10 20:03:05
  • 链接: https://bblog.031105.xyz/posts/2023-Summer-Courses-百度移动端/2023baiduandroidlec5-lec6.html
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论
此页目录
2023BaiduAndroidlec5-lec6