第8章 Android数据存储与I/O
第8章 Android数据存储与I/O
本章要点
-
SharedPreferences的概念和作用
- 使用SharedPreferences保存程序的参数、选项
- 读写其他应用的SharedPreferences
-
Android的文件I/O
- 读写SD卡上的文件
-
了解SQLite数据库
- 使用Android的API操作SQLite数据库
- 使用sqlite3工具管理SQLite数据库
- SQLiteOpenHelper类的功能与用法
-
Android的手势支持
- 手势检测
- 向手势库中添加手势
- 识别用户手势
-
让应用说话(TTS)
所有应用程序都必然涉及数据的输入和输出,Android应用也不例外。应用程序的参数设置、程序运行状态数据等,都需要保存到外部存储器上,这样在系统关机之后数据才不会丢失。Android应用是使用Java或Kotlin语言来开发的,因此开发者在Java I/O中的编程经验大部分都可以“移植”到Android应用开发中。Android系统还提供了一些专门的I/O API,通过这些API可以更有效地进行输入和输出。
如果应用程序只有少量数据需要保存,那么使用普通文件就可以了;但如果应用程序有大量数据需要存储和访问,就需要借助于数据库了。Android系统内置了SQLite数据库,SQLite数据库是一个真正轻量级的数据库,它没有后台进程,整个数据库就对应于一个文件,这样可以非常方便地在不同设备之间移植。Android不仅内置了SQLite数据库,而且为访问SQLite数据库提供了大量便捷的API。本章将会详细介绍如何在Android应用中使用SQLite数据库。
8.1 使用 SharedPreferences
有些时候,应用程序有少量的数据需要保存,而且这些数据的格式很简单,都是普通的字符串、标量类型的值等,比如应用程序的各种配置信息(如是否打开音效、是否使用振动效果等)、小游戏的玩家积分(如扫雷英雄榜之类的)等。对于这种数据,Android 提供了 SharedPreferences 进行保存。
8.1.1 SharedPreferences 与 Editor 简介
SharedPreferences 保存的数据主要是类似于配置信息格式的数据,因此它保存的数据主要是简单类型的 key-value 对。
SharedPreferences 接口主要负责读取应用程序的 Preferences 数据,它提供了如下常用方法来访问 SharedPreferences 中的 key-value 对:
boolean contains(String key)
: 判断 SharedPreferences 是否包含特定 key 的数据。Map<String, ?> getAll()
: 获取 SharedPreferences 数据里全部的 key-value 对。getXxx(String key, xxx defValue)
: 获取 SharedPreferences 数据里指定 key 对应的 value。如果该 key 不存在,则返回默认值 defValue。其中 xxx 可以是 boolean、float、int、long、String 等各种基本类型的值。
SharedPreferences 接口并没有提供写入数据的能力,通过 SharedPreferences.Editor 才允许写入。SharedPreferences 调用 edit()
方法即可获取它所对应的 Editor 对象。Editor 提供了如下方法来向 SharedPreferences 写入数据:
SharedPreferences.Editor clear()
: 清空 SharedPreferences 里所有数据。SharedPreferences.Editor putXxx(String key, xxx value)
: 向 SharedPreferences 存入指定 key 对应的数据。其中 xxx 可以是 boolean、float、int、long、String 等各种基本类型的值。SharedPreferences.Editor remove(String key)
: 删除 SharedPreferences 里指定 key 对应的数据项。boolean apply()
: 当 Editor 编辑完成后,调用该方法提交修改。与apply()
功能类似的方法还有commit()
,区别是commit()
会立即提交修改;而apply()
在后台提交修改,不会阻塞前台线程,因此推荐使用apply()
方法。
提示:从用法角度来看,SharedPreferences 和 SharedPreferences.Editor 组合起来非常像 Map,其中 SharedPreferences 负责根据 key 读取数据,而 SharedPreferences.Editor 则用于写入数据。
SharedPreferences 本身是一个接口,程序无法直接创建 SharedPreferences 实例,只能通过 Context 提供的 getSharedPreferences(String name, int mode)
方法来获取 SharedPreferences 实例,该方法的第二个参数支持如下几个值:
Context.MODE_PRIVATE
: 指定该 SharedPreferences 数据只能被本应用程序读写。Context.MODE_WORLD_READABLE
: 指定该 SharedPreferences 数据能被其他应用程序读,但不能写。Context.MODE_WORLD_WRITEABLE
: 指定该 SharedPreferences 数据能被其他应用程序读写。
提示:从 Android 4.2 开始,Android 不再推荐使用 MODE_WORLD_READABLE、MODE_WORLD_WRITEABLE 这两种模式,因为这两种模式允许其他应用程序来读或写本应用创建的数据,因此容易导致安全漏洞。如果应用程序确实需要把内部数据暴露出来供其他应用访问,则应该使用本书后面介绍的 ContentProvider。
8.1.2 SharedPreferences 的存储位置和格式
下面的程序示范了如何向 SharedPreferences 中写入、读取数据。该程序的界面很简单,它只是提供了两个按钮,其中一个用于写入数据;另一个用于读取数据,故此处不再给出界面布局文件。程序代码如下。
程序清单: codes\08\8.1\SharedPreferencesTest\qs\src\main\java\org\crazyit\io\MainActivity.java
public class MainActivity extends Activity {private SharedPreferences preferences;private SharedPreferences.Editor editor;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);// 获取只能被本应用程序读写的 SharedPreferences 对象preferences = getSharedPreferences("crazyit", Context.MODE_PRIVATE);editor = preferences.edit();Button read = findViewById(R.id.read);Button write = findViewById(R.id.write);read.setOnClickListener(view -> {// 读取字符串数据String time = preferences.getString("time", null);// 读取 int 类型的数据int randNum = preferences.getInt("random", 0);String result = time == null ? "您暂时还未写入数据" : "写入时间为: " + time + "\n上次生成的随机数为: " + randNum;// 使用 Toast 提示信息Toast.makeText(MainActivity.this, result, Toast.LENGTH_SHORT).show();});write.setOnClickListener(view -> {SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 hh:mm:ss");// 存入当前时间editor.putString("time", sdf.format(new Date()));// 存入一个随机数editor.putInt("random", (int) (Math.random() * 100));// 提交所有存入的数据editor.apply();});}
}
上面程序中的第一段粗体字代码用于读取 SharedPreferences 数据,当程序所读取的 SharedPreferences 数据文件根本不存在时,程序也返回默认值,并不会抛出异常;第二段粗体字代码用于写入 SharedPreferences 数据,由于 SharedPreferences 并不支持写入 Date 类型的值,故程序使用了 SimpleDateFormat 将 Date 格式化成字符串后写入。
运行上面的程序,单击程序界面上的“写入数据”按钮,程序将完成 SharedPreferences 的写入,写入完成后打开 Android Studio 的 File Explorer 面板,然后展开文件浏览树,将看到如图 8.1 所示的窗口。
从图 8.1 可以看出,SharedPreferences 数据总是保存在 /data/data/<package name>/shared_prefs
目录下,SharedPreferences 数据总是以 XML 格式保存。通过 File Explorer 面板的导出文件按钮导出该 XML 文档,打开该 XML 文档可以看到如下文件内容:
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map><int name="random" value="40" /><string name="time">2018年10月30日 03:44:10</string>
</map>
从上面的文件不难看出,SharedPreferences 数据文件的根元素是 <map.../>
元素,该元素里每个子元素代表一个 key-value 对,当 value 是整数类型时,使用 <int.../>
子元素;当 value 是字符串类型时,使用 <string.../>
子元素……依此类推。
单击程序界面上的“读取数据”按钮,程序弹出一个 Toast 对话框显示上次写入的数据。
实例: 记录应用程序的使用次数
这个简单的实例可以记录应用程序的使用次数:当用户第一次启动该应用程序时,系统创建 SharedPreferences 来记录使用次数。用户以后启动应用程序时,系统先读取 SharedPreferences 中记录的使用次数,然后将使用次数加 1。
本实例与程序界面无关,直接使用 Android Studio 自动生成的界面布局文件即可,因此此处不再给出界面布局文件。本实例程序的代码如下。
程序清单: codes\08\8.1\SharedPreferencesTest\usecount\app\src\main\java\org\crazyit\io\MainActivity.java
public class MainActivity extends Activity {private SharedPreferences preferences;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);preferences = getSharedPreferences("count", Context.MODE_PRIVATE);setContentView(R.layout.activity_main);// 读取 SharedPreferences 里的 count 数据int count = preferences.getInt("count", 0);// 显示程序以前使用的次数Toast.makeText(this, "程序以前被使用了 " + count + " 次。", Toast.LENGTH_LONG).show();SharedPreferences.Editor editor = preferences.edit();// 存入数据editor.putInt("count", ++count);// 提交修改editor.apply();}
}
上面程序中的第一行粗体字代码用于读取 SharedPreferences 中记录的使用次数;第二行粗体字代码将使用次数增加 1,并再次将使用次数写入 SharedPreferences 中。
8.2 File 存储
在学习 Java SE 的过程中,您可能已经了解了 Java 提供的一套完整的 IO 流体系,包括 FileInputStream
、FileOutputStream
等。通过这些 IO 流可以非常方便地访问磁盘上的文件内容。Android 同样支持以这种方式来访问手机存储器上的文件。
8.2.1 openFileOutput
和 openFileInput
Context
提供了如下两个方法来打开应用程序的数据文件夹里的文件 IO 流。
FileInputStream openFileInput(String name)
: 打开应用程序的数据文件夹下的 name 文件对应的输入流。FileOutputStream openFileOutput(String name, int mode)
: 打开应用程序的数据文件夹下的 name 文件对应的输出流。
上面两个方法分别用于打开文件输入流和输出流,其中第二个方法的第二个参数指定打开文件的模式,该模式支持如下值:
MODE_PRIVATE
: 该文件只能被当前程序读写。MODE_APPEND
: 以追加方式打开该文件,应用程序可以向该文件中追加内容。MODE_WORLD_READABLE
: 该文件的内容可以被其他程序读取。MODE_WORLD_WRITEABLE
: 该文件的内容可由其他程序读写。
注意: 从 Android 4.2 开始,Android 不推荐使用 MODE_WORLD_READABLE
和 MODE_WORLD_WRITEABLE
两种模式。
除此之外,Context
还提供了如下几个方法来访问应用程序的数据文件夹:
getDir(String name, int mode)
: 在应用程序的数据文件夹下获取或创建 name 对应的子目录。File getFilesDir()
: 获取应用程序的数据文件夹的绝对路径。String[] fileList()
: 返回应用程序的数据文件夹下的全部文件。boolean deleteFile(String name)
: 删除应用程序的数据文件夹下的指定文件。
下面的程序简单示范了如何读写应用程序数据文件夹内的文件。该程序的界面布局同样很简单,只包含两个文本框和两个按钮,其中第一组文本框和按钮用于处理写入,文本框用于接受用户输入,当用户单击“写入”按钮时,程序将会把数据写入文件;第二组文本框和按钮用于处理读取,当用户单击“读取”按钮时,该文本框显示文件中的数据。
程序清单: codes\08\8.2\FileTest\filetest\src\main\java\org\crazyit\io\MainActivity.java
public class MainActivity extends Activity {public static final String FILE_NAME = "crazyit.bin";@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);// 获取两个按钮Button read = findViewById(R.id.read);Button write = findViewById(R.id.write);// 获取两个文本框EditText edit1 = findViewById(R.id.edit1);TextView edit2 = findViewById(R.id.edit2);// 为 write 按钮绑定事件监听器write.setOnClickListener(view -> {// 将 edit1 中的内容写入文件中write(edit1.getText().toString());edit1.setText("");});read.setOnClickListener(view ->edit2.setText(read()) /* 读取指定文件中的内容,并显示出来 */);}private String read() {try (FileInputStream fis = openFileInput(FILE_NAME)) {byte[] buff = new byte[1024];int hasRead;StringBuilder sb = new StringBuilder();// 读取文件内容while ((hasRead = fis.read(buff)) > 0) {sb.append(new String(buff, 0, hasRead));}return sb.toString();} catch (Exception e) {e.printStackTrace();}return null;}private void write(String content) {try (FileOutputStream fos = openFileOutput(FILE_NAME, Context.MODE_APPEND);PrintStream ps = new PrintStream(fos)) {// 输出文件内容ps.println(content);} catch (Exception e) {e.printStackTrace();}}
}
上面程序中的第一段粗体字代码用于读取应用程序的数据文件,第二段粗体字代码用于向应用程序的数据文件中追加内容。从上面的代码可以看出,当 Android 系统调用 Context
的 openFileInput()
、openFileOutput()
打开文件输入流或输出流之后,接下来 IO 流的用法与 Java SE 中 IO 流的用法完全一样:直接用节点流读写也行,用包装流包装之后再处理也没问题。
运行上面的程序,当单击程序界面中的“写入”按钮时,用户在第一个文本框中输入的内容将会被保存到应用程序的数据文件中。打开 Android Studio 的 File Explorer 面板,可以看到应用程序的数据文件默认保存在 /data/data/<package name>/files
目录下。
8.2.2 读写 SD 卡上的文件
当程序通过 Context
的 openFileInput()
或 openFileOutput()
来打开文件输入流、输出流时,程序所打开的都是应用程序的数据文件夹里的文件,这样所存储的文件大小可能比较有限,因为手机内置的存储空间是有限的。
为了更好地存取应用程序的大文件数据,应用程序需要读写 SD 卡上的文件。SD 卡大大扩充了手机的存储能力。
读写 SD 卡上的文件请按如下步骤进行:
-
请求动态获取读写 SD 卡的权限,只有当用户授权读写 SD 卡时才执行读写。例如使用如下代码:
requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0x123);
-
调用
Environment
的getExternalStorageDirectory()
方法来获取外部存储器,也就是 SD 卡的目录。 -
使用
FileInputStream
、FileOutputStream
、FileReader
或FileWriter
读写 SD 卡里的文件。
注意点:
- 手机上应该已插入 SD 卡。对于模拟器来说,可通过
mksdcard
命令来创建虚拟存储卡。关于虚拟存储卡的管理请参考第 1 章。 - 为了读写 SD 卡上的数据,必须在应用程序的清单文件 (
AndroidManifest.xml
) 中添加读写 SD 卡的权限。例如如下配置:<!-- 向 SD 卡写入数据的权限 --> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
下面的程序示范了如何读写 SD 卡上的文件。该程序的主界面与上一个程序的界面完全相同,只是该程序数据读写是基于 SD 卡的。
程序清单: codes\08\8.2\FileTest\sdtest\src\main\java\org\crazyit\io\MainActivity.java
public class MainActivity extends Activity {private static final String FILE_NAME = "/crazyit.bin";private EditText edit1;private TextView edit2;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);// 获取两个按钮Button read = findViewById(R.id.read);Button write = findViewById(R.id.write);// 获取两个文本框edit1 = findViewById(R.id.edit1);edit2 = findViewById(R.id.edit2);// 为 write 按钮绑定事件监听器write.setOnClickListener(view -> {// 运行时请求获取写入 SD 卡的权限requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0x123); // ①});read.setOnClickListener(view ->requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0x456));}private String read() {// 如果手机插入了 SD 卡,而且应用程序具有访问 SD 卡的权限if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {File sdCardDir = Environment.getExternalStorageDirectory();try (FileInputStream fis = new FileInputStream(sdCardDir.getCanonicalPath() + FILE_NAME);BufferedReader br = new BufferedReader(new InputStreamReader(fis))) {StringBuilder sb = new StringBuilder();String line;// 循环读取文件内容while ((line = br.readLine()) != null) {sb.append(line);}return sb.toString();} catch (IOException e) {e.printStackTrace();}}return null;}private void write(String content) {// 获取 SD 卡的目录File sdCardDir = Environment.getExternalStorageDirectory();try {File targetFile = new File(sdCardDir.getCanonicalPath() + FILE_NAME);try (RandomAccessFile raf = new RandomAccessFile(targetFile, "rw")) {// 将文件记录指针移动到最后raf.seek(targetFile.length());// 输出文件内容raf.write(content.getBytes());}} catch (IOException e) {e.printStackTrace();}}@Overridepublic void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {if (requestCode == 0x123) {// 如果用户同意授权访问if (grantResults != null && grantResults[0] == PackageManager.PERMISSION_GRANTED) {write(edit1.getText().toString());edit1.setText("");} else {// 提示用户必须允许写入 SD 卡的权限Toast.makeText(this, R.string.writesd_tip, Toast.LENGTH_LONG).show();}}if (requestCode == 0x456) {// 如果用户同意授权访问if (grantResults != null && grantResults[0] == PackageManager.PERMISSION_GRANTED) {// 读取指定文件中的内容,并显示出来edit2.setText(read());} else {// 提示用户必须允许读取 SD 卡的权限Toast.makeText(this, R.string.writesd_tip, Toast.LENGTH_LONG).show();}}}
}
由于向 SD 卡中写入数据需要在运行时动态获取权限,因此该程序中的 ①
号代码用于动态获取 WRITE_EXTERNAL_STORAGE
权限,当用户授权完成后会触发 onRequestPermissionsResult
方法,程序会在该方法中根据用户授权来决定是否向 SD 卡中写入数据。
上面程序中的第一段粗体字代码用于读取 SD 卡中指定文件的内容;第二段粗体字代码则使用 RandomAccessFile
向 SD 卡中的指定文件追加内容——如果使用 FileOutputStream
向指定文件写入数据,FileOutputStream
会把原有的文件内容清空,那就不是追加文件内容了。由此可见,当程序直接使用 FileOutputStream
进行输出时,比使用 Context
的 openFileOutput()
方便。
运行上面的程序,在第一个文本框内输入一些字符串,然后单击“写入”按钮,即可将数据写入底层 SD 卡中。
实例:SD卡文件浏览器
在本例中,我们将利用 File
类开发一个 SD 卡文件浏览器。该程序首先获取访问系统 SD 卡目录的权限,然后通过 File
的 listFiles()
方法来获取指定目录下的全部文件和文件夹。
当程序启动时,系统会显示 SD 卡根目录下的全部文件和文件夹,并使用 RecyclerView
将它们列出来。当用户点击 RecyclerView
中的某个列表项时,系统会显示该列表项对应文件夹下的全部文件和文件夹。
界面布局文件
以下是该程序的界面布局文件,用于展示当前路径及目录下的文件和文件夹:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayoutxmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"><!-- 显示当前路径的文本框 --><TextViewandroid:id="@+id/path"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_gravity="center_horizontal"android:layout_marginStart="8dp"android:layout_marginEnd="8dp"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent" /><!-- 列出当前路径下所有文件的 RecyclerView --><android.support.v7.widget.RecyclerViewandroid:id="@+id/recycler"android:layout_width="match_parent"android:layout_height="0dp"android:layout_marginStart="8dp"android:layout_marginTop="4dp"android:layout_marginEnd="8dp"android:layout_marginBottom="2dp"app:layout_constraintBottom_toTopOf="@+id/parent"app:layout_constraintTop_toBottomOf="@+id/path" /><!-- 返回上一级目录的按钮 --><Buttonandroid:id="@+id/parent"android:layout_width="38dp"android:layout_height="34dp"android:layout_marginBottom="4dp"android:background="@drawable/home"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent" />
</android.support.constraint.ConstraintLayout>
主程序代码
程序的主要逻辑是利用 File
类的 listFiles()
方法来列出指定目录下的全部文件和文件夹。以下是主程序的代码:
public class MainActivity extends Activity {private RecyclerView recyclerView;private TextView textView;// 记录当前的父文件夹private File currentParent;// 记录当前路径下的所有文件的文件数组private File[] currentFiles;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);// 获取列出全部文件的 RecyclerViewrecyclerView = findViewById(R.id.recycler);LinearLayoutManager layoutManager = new LinearLayoutManager(this);// 设置 RecyclerView 的滚动方向layoutManager.setOrientation(LinearLayoutManager.VERTICAL);// 为 RecyclerView 设置布局管理器recyclerView.setLayoutManager(layoutManager);textView = findViewById(R.id.path);// 运行时请求获取写入 SD 卡的权限requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0x123);}@Overridepublic void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {if (requestCode == 0x123) {// 如果用户同意授权访问if (grantResults != null && grantResults[0] == PackageManager.PERMISSION_GRANTED) {// 获取系统的 SD 卡的目录File root = new File(Environment.getExternalStorageDirectory().getPath());// 如果 SD 卡存在if (root.exists()) {currentParent = root;currentFiles = root.listFiles();// 使用当前目录下的全部文件、文件夹来填充 RecyclerViewinflateRecyclerView(currentFiles);}} else {// 提示用户必须允许写入 SD 卡的权限Toast.makeText(this, R.string.writesd_tip, Toast.LENGTH_LONG).show();}}}private void inflateRecyclerView(File[] files) {RecyclerView.Adapter adapter = new RecyclerView.Adapter<LineViewHolder>() {@NonNull@Overridepublic LineViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {LinearLayout line = (LinearLayout) getLayoutInflater().inflate(R.layout.line, null);return new LineViewHolder(line);}@Overridepublic void onBindViewHolder(@NonNull LineViewHolder viewHolder, int i) {viewHolder.nameView.setText(files[i].getName());// 如果当前 File 是文件夹,则使用 folder 图标; 否则使用 file 图标if (files[i].isDirectory())viewHolder.iconView.setImageResource(R.drawable.folder);elseviewHolder.iconView.setImageResource(R.drawable.file);}@Overridepublic int getItemCount() {return files.length;}};// 为 RecyclerView 设置 AdapterrecyclerView.setAdapter(adapter);textView.setText(getResources().getString(R.string.cur_tip, currentParent.getPath()));}class LineViewHolder extends RecyclerView.ViewHolder {ImageView iconView;TextView nameView;public LineViewHolder(LinearLayout itemView) {super(itemView);itemView.setOnClickListener(view -> {int position = getAdapterPosition();// 用户单击了文件,直接返回,不做任何处理if (currentFiles[position].isFile()) return;// 获取用户单击的文件夹下的所有文件File[] tmp = currentFiles[position].listFiles();if (tmp == null || tmp.length == 0) {Toast.makeText(MainActivity.this, "当前路径不可访问或该路径下没有文件", Toast.LENGTH_SHORT).show();} else {// 获取用户单击的列表项对应的文件夹,设为当前的父文件夹currentParent = currentFiles[position];// 保存当前父文件夹内的全部文件和文件夹currentFiles = tmp;// 再次更新 RecyclerViewinflateRecyclerView(currentFiles);}});this.nameView = itemView.findViewById(R.id.file_name);this.iconView = itemView.findViewById(R.id.icon);}}
}
在上面的代码中:
- 通过
inflateRecyclerView()
方法填充RecyclerView
,根据文件夹或文件类型来设置不同的图标。 - 当用户点击
RecyclerView
中的某个项目时,程序会显示用户点击的文件夹中的所有文件。
运行效果
运行上述程序,可以看到如图 8.4 所示的界面:
该程序就像一个 SD 卡资源管理器,可以非常方便地浏览 SD 卡中包含的所有文件和文件夹。
提示:
- 从图 8.4 所示界面中可以看到
/storage/emulator/0/abc
目录下包含了haha
、hehe
两个目录和color.jpg
文件。这就要求开发者的模拟器的 SD 卡里带有这些目录和文件。为了在 SD 卡里创建目录,可先运行adb shell
命令来启动 Android 的 Shell 窗口——由于 Android 的内核就是 Linux,因此可在该 Shell 窗口里执行ls
、mkdir
等常见的 Linux 命令。
8.3 SQLite 数据库
Android 系统集成了一个轻量级的数据库:SQLite。SQLite 是一个嵌入式数据库引擎,适用于资源有限的设备,如手机和 PDA。虽然 SQLite 支持大部分 SQL92 语法,并允许开发者使用 SQL 语句操作数据库中的数据,但 SQLite 数据库只是一个文件,不需要安装或启动服务器进程。
提示
从本质上看,SQLite 的操作方式是一种更为便捷的文件操作。当应用程序创建或打开一个 SQLite 数据库时,实际上是打开一个文件进行读写。因此,有人认为 SQLite 类似于 Microsoft 的 Access(实际上 SQLite 功能更强大)。对于实际项目中有大量数据需要读写,且需要面对大量用户的并发存储的情况,本身就不应该将数据存放在手机的 SQLite 数据库中。毕竟手机的存储能力和计算能力不足以充当服务器的角色。
8.3.1 SQLiteDatabase 简介
Android 提供了 SQLiteDatabase
类来代表一个数据库(底层就是一个数据库文件)。一旦应用程序获得了代表指定数据库的 SQLiteDatabase
对象,就可以通过该对象来管理和操作数据库。
SQLiteDatabase
提供了如下静态方法来打开一个文件对应的数据库:
static SQLiteDatabase openDatabase(String path, SQLiteDatabase.CursorFactory factory, int flags)
: 打开 path 文件所代表的 SQLite 数据库。static SQLiteDatabase openOrCreateDatabase(File file, SQLiteDatabase.CursorFactory factory)
: 打开或创建(如果不存在)file 文件所代表的 SQLite 数据库。static SQLiteDatabase openOrCreateDatabase(String path, SQLiteDatabase.CursorFactory factory)
: 打开或创建(如果不存在)path 文件所代表的 SQLite 数据库。
在程序中获取 SQLiteDatabase
对象之后,可以调用如下方法来操作数据库:
execSQL(String sql, Object[] bindArgs)
: 执行带占位符的 SQL 语句。execSQL(String sql)
: 执行 SQL 语句。insert(String table, String nullColumnHack, ContentValues values)
: 向指定表中插入数据。update(String table, ContentValues values, String whereClause, String[] whereArgs)
: 更新指定表中的特定数据。delete(String table, String whereClause, String[] whereArgs)
: 删除指定表中的特定数据。Cursor query(...)
: 对指定数据表执行查询(支持多种参数组合)。rawQuery(String sql, String[] selectionArgs)
: 执行带占位符的 SQL 查询。beginTransaction()
: 开始事务。endTransaction()
: 结束事务。
这些方法的作用类似于 JDBC 的 Connection
接口,但 SQLiteDatabase
提供了更多的方法,比如 insert
、update
、delete
和 query
等方法。Android 提供这些方法是为了帮助不熟悉 SQL 语法的开发者更简单地操作数据表中的数据。
Cursor 类
上述查询方法返回一个 Cursor
对象,Cursor
类似于 JDBC 的 ResultSet
。Cursor
提供了如下方法来移动查询结果的记录指针:
move(int offset)
: 将记录指针向上或向下移动指定的行数。offset 为正数时向下移动,为负数时向上移动。boolean moveToFirst()
: 将记录指针移动到第一行,如果移动成功则返回true
。boolean moveToLast()
: 将记录指针移动到最后一行,如果移动成功则返回true
。boolean moveToNext()
: 将记录指针移动到下一行,如果移动成功则返回true
。boolean moveToPosition(int position)
: 将记录指针移动到指定行,如果移动成功则返回true
。boolean moveToPrevious()
: 将记录指针移动到上一行,如果移动成功则返回true
。
一旦将记录指针移动到指定行之后,就可以调用 Cursor
的 getXxx()
方法获取该行的指定列的数据。
提示
如果你有 JDBC 编程的经验,可以将 SQLiteDatabase
当作 JDBC 中 Connection
和 Statement
的混合体,因为 SQLiteDatabase
既代表了与数据库的连接,也可以直接用于执行 SQL 操作。Android 中的 Cursor
可以看作是 ResultSet
,并且提供了更多便捷的方法来操作结果集。
对于一个 Java 程序员来说,JDBC 几乎是必备的编程基础。如果你需要详细学习 JDBC 的相关知识,可以参考《疯狂 Java 讲义》。
8.3.2 创建数据库和表
使用 SQLiteDatabase
的 openOrCreateDatabase()
静态方法可以打开或创建数据库。例如,下面的代码用于打开或创建一个 SQLite 数据库:
SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase("/mnt/sdcard/temp.db3", null);
如果 /mnt/sdcard/
目录下的 temp.db3
文件存在,程序将打开该数据库;如果文件不存在,上述代码将在该目录下创建 temp.db3
文件(即对应的数据库)。
创建数据库之后,可以使用 SQLiteDatabase
对象的 execSQL()
方法执行任意 SQL 语句,来创建表:
// 定义建表语句
String sql = "CREATE TABLE user_inf (user_id INTEGER PRIMARY KEY, user_name VARCHAR(255), user_pass VARCHAR(255))";
// 执行 SQL 语句
db.execSQL(sql);
8.3.3 SQLiteOpenHelper 类
在实际项目中,很少直接使用 SQLiteDatabase
的 openOrCreateDatabase()
静态方法打开数据库,通常会继承 SQLiteOpenHelper
类,并通过其子类的 getReadableDatabase()
或 getWritableDatabase()
方法来打开数据库。
SQLiteOpenHelper
是 Android 提供的一个管理数据库的工具类,用于管理数据库的创建和版本更新。通常的用法是创建 SQLiteOpenHelper
的子类,并扩展其 onCreate(SQLiteDatabase db)
和 onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)
方法。
常用方法
synchronized SQLiteDatabase getReadableDatabase()
: 以读写方式打开数据库的SQLiteDatabase
对象。synchronized SQLiteDatabase getWritableDatabase()
: 以写的方式打开数据库的SQLiteDatabase
对象。abstract void onCreate(SQLiteDatabase db)
: 当第一次创建数据库时回调此方法。abstract void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)
: 当数据库版本更新时回调此方法。synchronized void close()
: 关闭所有打开的SQLiteDatabase
对象。
使用 SQLiteOpenHelper
类的步骤
- 获取数据库对象:通过
SQLiteOpenHelper
的子类获取SQLiteDatabase
对象,它代表了与数据库的连接。 - 执行 SQL 语句:调用
SQLiteDatabase
的方法来执行 SQL 语句。 - 操作结果:对 SQL 语句的执行结果进行操作,例如用
CursorRecyclerViewAdapter
封装Cursor
。 - 关闭数据库:在适当的地方关闭
SQLiteDatabase
,回收资源,避免资源泄漏。
8.3.4 使用 SQL 语句操作 SQLite 数据库
SQLiteDatabase
的 execSQL()
方法可以执行任意 SQL 语句,包括带占位符的 SQL 语句,但由于该方法没有返回值,通常用于执行 DDL 语句或 DML 语句。如果需要执行查询语句,则可以调用 SQLiteDatabase
的 rawQuery(String sql, String[] selectionArgs)
方法。
例如,以下代码用于执行插入语句:
db.execSQL("INSERT INTO news_inf VALUES (null, ?, ?)", new String[]{title, content});
示例程序
下面的示例程序展示了如何在 Android 应用中操作 SQLite 数据库。该程序提供了两个文本框,用户可以输入内容。当用户点击“插入”按钮时,文本框中的内容将插入数据库。
public class MainActivity extends Activity {private RecyclerView recyclerView;private MyDatabaseHelper dbHelper;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);recyclerView = findViewById(R.id.recycler);LinearLayoutManager layoutManager = new LinearLayoutManager(this);layoutManager.setOrientation(LinearLayoutManager.VERTICAL);recyclerView.setLayoutManager(layoutManager);dbHelper = new MyDatabaseHelper(this, this.getFilesDir().toString() + "/my.db3", 1);loadData();Button bn = findViewById(R.id.ok);EditText titleEt = findViewById(R.id.title);EditText contentEt = findViewById(R.id.content);bn.setOnClickListener(view -> {String title = titleEt.getText().toString().trim();String content = contentEt.getText().toString().trim();insertData(title, content);loadData();});}private void loadData() {Cursor cursor = dbHelper.getReadableDatabase().rawQuery("SELECT * FROM news_inf", null);inflateRecycler(cursor);}private void insertData(String title, String content) {dbHelper.getReadableDatabase().execSQL("INSERT INTO news_inf VALUES (null, ?, ?)", new String[]{title, content});}private void inflateRecycler(Cursor cursor) {// 用于展示数据库内容的适配器}
}
8.3.5 使用sqlite3工具
在Android SDK的platform-tools
目录下提供了一个名为sqlite3.exe
的文件,这是一个简单的SQLite数据库管理工具,类似于MySQL提供的命令行工具。开发者可以使用该工具来查询和管理数据库。
例如,如果你将应用程序生成的my.db3
数据库文件导出到本地计算机的F:\
盘根目录下,可以使用以下命令来启动SQLite数据库:
sqlite3 f:/my.db3
运行上面的命令后,将进入sqlite3工具的命令行界面。通过这个界面,开发者可以使用一些常用的命令来查看和操作数据库。以下是一些常用的命令:
.databases
: 查看当前数据库。.tables
: 查看当前数据库中的数据表。.help
: 查看sqlite3支持的所有命令。
通过在命令行界面中输入.help
,sqlite3工具将列出所有支持的命令。此外,SQLite数据库还支持大部分常用的SQL语句,开发者可以在这个界面中运行各种DDL(数据定义语言)、DML(数据操作语言)和查询语句来测试和操作数据库。
提示
SQLite数据库支持的SQL语句与MySQL非常相似,开发者可以将已有的MySQL经验迁移到SQLite上。如果需要了解更多关于SQL语法和MySQL数据库的知识,可以参考疯狂Java体系的《疯狂 Java讲义》。当Android应用提示某条SQL语句有语法错误时,建议首先利用sqlite3工具测试该语句,以确保语法正确。
需要注意的是,SQLite内部只支持以下5种数据类型:
- NULL: 表示空值。
- INTEGER: 有符号的整型数值。
- REAL: 浮点数。
- TEXT: 文本字符串。
- BLOB: 二进制大对象。
尽管SQLite仅支持上述5种数据类型,但它可以接受如varchar(n)
、char(n)
、decimal(p,s)
等数据类型。SQLite会在运算或保存时将这些数据类型转换为对应的内部数据类型。
此外,SQLite还有一个特点:它允许在任何类型的字段中保存各种类型的数据,开发者可以不必关心字段声明的数据类型。例如,程序可以将字符串类型的值存入INTEGER
类型的字段,也可以将数值类型的值存入布尔类型的字段。然而,有一种例外情况:定义为INTEGER PRIMARY KEY
的字段只能存储64位整数,向此类字段中保存除整数以外的其他类型的数据时,SQLite会产生错误。
由于SQLite允许在存入数据时忽略底层数据列的实际数据类型,因此在编写建表语句时可以省略数据列的类型声明。例如,以下SQL语句在SQLite中也是正确的:
CREATE TABLE my_test (_id INTEGER PRIMARY KEY AUTOINCREMENT,name,pass,gender
);
这种灵活的设计使得SQLite非常适合资源有限的设备(如手机、PDA等)上适量数据的存取。
8.3.6 使用特定方法操作SQLite数据库
如果开发者对SQL语法不熟悉,甚至从未使用过任何数据库,Android的SQLiteDatabase
提供了insert
、update
、delete
和query
等方法来操作数据库。虽然这些方法提供了便捷的操作,但对于熟悉SQL语法的开发者来说,直接使用SQL语句更为高效。
以下是这些特定方法的简单介绍:
1. 使用 insert
方法插入记录
SQLiteDatabase
的insert
方法签名为:
long insert(String table, String nullColumnHack, ContentValues values)
参数说明:
table
: 要插入数据的表名。nullColumnHack
: 用于指定如果values
为空或不包含任何数据时,允许插入null
值的列名。values
: 要插入的一行记录的数据,以ContentValues
形式存储。
例如,下面的代码演示了如何插入一条记录:
ContentValues values = new ContentValues();
values.put("name", "孙悟空");
values.put("age", 500);
// 插入数据,返回新添记录的行号,出错时返回-1
long rowid = db.insert("person_inf", null, values);
不管values
是否包含数据,insert()
方法总会插入一条记录。如果values
为空,除了主键之外,其他字段值都会是null
。
2. 使用 update
方法更新记录
SQLiteDatabase
的update
方法签名为:
int update(String table, ContentValues values, String whereClause, String[] whereArgs)
参数说明:
table
: 要更新数据的表名。values
: 要更新的数据,以ContentValues
形式存储。whereClause
: 指定满足该条件的记录将被更新。whereArgs
: 为whereClause
中的占位符提供参数。
例如,更新person_inf
表中主键大于20的人的姓名:
ContentValues values = new ContentValues();
values.put("name", "新人名");
int result = db.update("person_inf", values, "_id>?", new String[]{"20"});
3. 使用 delete
方法删除记录
SQLiteDatabase
的delete
方法签名为:
int delete(String table, String whereClause, String[] whereArgs)
参数说明:
table
: 要删除数据的表名。whereClause
: 指定满足该条件的记录将被删除。whereArgs
: 为whereClause
中的占位符提供参数。
例如,删除person_inf
表中所有姓名以“孙”开头的记录:
int result = db.delete("person_inf", "person_name LIKE ?", new String[]{"孙%"});
4. 使用 query
方法查询记录
SQLiteDatabase
的query
方法签名为:
Cursor query(boolean distinct, String table, String[] columns, String whereClause, String[] selectionArgs, String groupBy, String having, String orderBy, String limit)
参数说明:
distinct
: 是否去除重复记录。table
: 执行查询数据的表名。columns
: 要查询的列名,相当于SELECT
语句中的字段部分。whereClause
: 查询条件子句,相当于WHERE
语句的条件部分。selectionArgs
: 为whereClause
中的占位符提供参数。groupBy
: 分组依据,相当于GROUP BY
语句的部分。having
: 用于对分组进行过滤,相当于HAVING
语句的部分。orderBy
: 排序依据,相当于ORDER BY
语句的部分。limit
: 限制查询结果的数量,相当于LIMIT
语句的部分。
例如,查询person_inf
表中人名以“孙”开头的记录:
Cursor cursor = db.query("person_inf", new String[]{"_id", "name", "age"},"name LIKE ?", new String[]{"孙%"}, null, null, "personid DESC", "5,10");
// 处理结果集
cursor.close();
尽管这些方法可以简化操作,但如果需要动态构建查询条件时,这些方法尤其有用,可以避免手动拼接SQL语句。
8.3.7 事务
在SQLiteDatabase
中,可以使用事务来确保一组数据库操作要么全部执行成功,要么全部回滚。SQLite数据库提供以下方法来控制事务:
beginTransaction()
: 开始一个新的事务。endTransaction()
: 结束当前事务。inTransaction()
: 判断当前上下文是否处于事务环境中,若处于事务中则返回true
,否则返回false
。
在执行endTransaction()
方法时,事务的处理结果取决于是否调用了setTransactionSuccessful()
方法。如果调用了setTransactionSuccessful()
,则事务提交;如果没有调用这个方法,事务将回滚。
示例代码:
// 开始事务
db.beginTransaction();
try {// 执行DML语句 (如插入、更新或删除操作)// 调用该方法设置事务成功;否则endTransaction()方法将回滚事务db.setTransactionSuccessful();
} finally {// 根据事务的标志决定是提交事务还是回滚事务db.endTransaction();
}
8.3.8 SQLite数据库最佳实践建议
为了更好地使用SQLite数据库,以下是一些最佳实践建议:
-
关于打开数据库的方式
SQLite数据库有两种方式可以打开:
- 直接通过
SQLiteDatabase
的静态方法来打开数据库。 - 通过
SQLiteOpenHelper
类的子类来打开数据库。
推荐使用第二种方式,通过
SQLiteOpenHelper
类来管理数据库。这种方式可以避免开发者手动判断数据库是否存在,或是否需要更新等繁琐操作。 - 直接通过
-
关于SQLite的用途
不要把SQLite数据库当成真正的服务器级数据库来使用。Android应用运行在手机环境中,存储和计算能力有限,因此不建议在SQLite中存储大量数据。如果有大量数据需要存储,建议使用后台服务器数据库。
SQLite的典型用途是用来缓存部分服务端数据,减少网络通信流量,并允许用户在离线状态下使用应用。它也可以用于存储一些不重要的小量数据。
-
关于操作数据库的方式
有两种方式可以操作SQLite数据库:
- 使用
execSQL()
、rawQuery()
方法执行原生SQL语句。 - 使用
insert()
、update()
、delete()
、query()
等方法进行CRUD操作。
对于有SQL编程经验的开发者,推荐使用第一种方式,这样操作更直接、更高效。
如果应用中频繁需要操作SQLite数据库,建议使用ORM工具(如OrmLite、GreenDao、LitePal等)。这些工具类似于Hibernate,可以简化数据库操作,使开发更加便捷。
- 使用
8.4 手势(Gesture)
在Android中,手势是指用户在触摸屏上的连续触摸行为,例如从左至右的滑动或在屏幕上画一个圆圈。Android支持两种类型的手势行为:手势检测和手势识别。
8.4.1 手势检测
Android提供了GestureDetector
类来处理手势检测。GestureDetector
实例用于检测用户的手势,并通过一个GestureDetector.OnGestureListener
接口来响应这些手势。
GestureDetector.OnGestureListener
接口
该接口包含以下方法,用于处理各种手势事件:
-
boolean onDown(MotionEvent e)
: 当触摸事件按下时触发该方法。此方法必须返回true
,否则后续的手势事件不会被处理。 -
boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)
: 当用户在屏幕上拖动时触发。distanceX
和distanceY
表示在X轴和Y轴上的移动距离。 -
void onShowPress(MotionEvent e)
: 当用户在屏幕上按下且未移动和松开时触发。 -
boolean onSingleTapUp(MotionEvent e)
: 当用户在屏幕上轻击时触发。 -
void onLongPress(MotionEvent e)
: 当用户手指在屏幕上长按时触发。 -
boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)
: 当用户在屏幕上快速滑动时触发。velocityX
和velocityY
表示滑动的速度。
手势检测的实现步骤
- 创建
GestureDetector
对象: 需要实现GestureDetector.OnGestureListener
接口。 - 处理触摸事件: 将触摸事件传递给
GestureDetector
对象进行处理。
示例代码:
public class MainActivity extends Activity implements GestureDetector.OnGestureListener {// 定义手势检测器变量private GestureDetector detector;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);// 创建手势检测器detector = new GestureDetector(this, this);}@Overridepublic boolean onTouchEvent(MotionEvent me) {// 将触摸事件交给GestureDetector处理return detector.onTouchEvent(me);}@Overridepublic boolean onDown(MotionEvent e) {Toast.makeText(this, "onDown", Toast.LENGTH_SHORT).show();return false;}@Overridepublic void onShowPress(MotionEvent e) {Toast.makeText(this, "onShowPress", Toast.LENGTH_SHORT).show();}@Overridepublic boolean onSingleTapUp(MotionEvent e) {Toast.makeText(this, "onSingleTapUp", Toast.LENGTH_SHORT).show();return false;}@Overridepublic boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {Toast.makeText(this, "onScroll", Toast.LENGTH_SHORT).show();return false;}@Overridepublic void onLongPress(MotionEvent e) {Toast.makeText(this, "onLongPress", Toast.LENGTH_SHORT).show();}@Overridepublic boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {Toast.makeText(this, "onFling", Toast.LENGTH_SHORT).show();return false;}
}
在上述代码中:
detector
是GestureDetector
对象。- 在
onTouchEvent()
方法中,将触摸事件交给GestureDetector
处理。 - 各个手势事件的方法会根据用户的操作显示不同的
Toast
消息。
结论
通过使用GestureDetector
和GestureDetector.OnGestureListener
接口,Android提供了一个灵活的方式来处理用户的各种手势事件。了解和掌握这些手势事件,可以帮助你在应用中实现更流畅和自然的用户交互体验。
实例:通过手势缩放图片
在这个实例中,我们将展示如何通过手势来缩放图片。用户可以通过在屏幕上左右挥动手指来调整图片的缩放比例。具体来说,手指从左向右挥动时图片会放大,从右向左挥动时图片会缩小,挥动速度越快,缩放比越大。
程序步骤
- 创建
GestureDetector
对象: 用于检测用户的手势。 - 实现手势监听器: 通过继承
GestureDetector.SimpleOnGestureListener
类,重写onFling
方法,根据手势的速度来计算缩放比例。 - 处理图片缩放: 使用
Matrix
对象来缩放图片,并更新ImageView
显示的图片。
程序代码
public class MainActivity extends Activity {// 定义手势检测器变量private GestureDetector detector;private ImageView imageView;// 初始的图片资源private Bitmap bitmap;// 定义图片的宽、高private int width;private int height;// 记录当前的缩放比private float currentScale = 1.0f;// 控制图片缩放的Matrix对象private Matrix matrix;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);// 创建手势检测器detector = new GestureDetector(this, new GestureDetector.SimpleOnGestureListener() {@Overridepublic boolean onFling(MotionEvent event1, MotionEvent event2, float velocityX, float velocityY) {// 限制velocityX的最大值和最小值float vx = velocityX > 4000 ? 4000f : (velocityX < -4000 ? -4000f : velocityX);// 根据手势的速度来计算缩放比currentScale *= vx / 4000.0f;// 保证currentScale不会等于0currentScale = currentScale > 0.01 ? currentScale : 0.01f;// 重置Matrixmatrix.reset();// 缩放Matrixmatrix.setScale(currentScale, currentScale, width / 2, height / 2);BitmapDrawable tmp = (BitmapDrawable) imageView.getDrawable();// 如果图片还未回收,先强制回收该图片if (!tmp.getBitmap().isRecycled()) {tmp.getBitmap().recycle();}// 根据原始位图和Matrix创建新图片Bitmap bitmap2 = Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, true);// 显示新的位图imageView.setImageBitmap(bitmap2);return true;}});imageView = findViewById(R.id.show);matrix = new Matrix();// 获取被缩放的源图片bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.flower);// 获得位图宽和高width = bitmap.getWidth();height = bitmap.getHeight();// 设置ImageView初始化时显示的图片imageView.setImageBitmap(bitmap);}@Overridepublic boolean onTouchEvent(MotionEvent me) {// 将该Activity上的触摸事件交给GestureDetector处理return detector.onTouchEvent(me);}
}
解释
-
GestureDetector
对象的创建: 在onCreate
方法中创建了一个GestureDetector
对象,并传入了一个GestureDetector.SimpleOnGestureListener
实例。这个类继承自GestureDetector.OnGestureListener
,只实现了onFling
方法。 -
onFling
方法: 这个方法会在用户快速滑动时触发。我们使用velocityX
(横向速度)来计算缩放比例。如果velocityX
大于 4000,则限制为 4000;如果小于 -4000,则限制为 -4000。根据这个值调整currentScale
,然后用Matrix
对象来更新图片的缩放比例。 -
Matrix
对象: 使用Matrix
对象来缩放图片,通过matrix.setScale
方法设置缩放比例。width / 2
和height / 2
用于指定缩放的中心点为图片的中心。 -
更新图片: 通过
Bitmap.createBitmap
方法根据新的Matrix
创建缩放后的图片,并更新到ImageView
中显示。
总结
这个示例展示了如何通过手势来动态调整图片的缩放比例。使用 GestureDetector
结合手势监听器,可以根据用户的手势快速、直观地处理图像缩放,提升了用户的交互体验。
实例:通过多点触碰缩放 TextView
在这个实例中,我们将展示如何通过多点触碰来缩放 TextView
的字号。用户可以通过捏合手势(即两个手指的距离变化)来控制 TextView
中的文字大小。实现这个功能的关键在于处理多点触碰事件,并根据两个手指之间的距离来计算缩放比。
程序步骤
- 创建
TouchZoomView
类: 继承自TextView
,重写onTouchEvent
方法来处理多点触碰事件。 - 处理多点触碰事件: 使用
MotionEvent
的getPointerCount
和getActionMasked
方法来判断触碰点的数量和触碰事件的类型。 - 计算手指间的距离: 根据两个手指之间的距离来计算缩放比例,并更新
TextView
的字号。
程序代码
public class TouchZoomView extends TextView {// 保存 TextView 当前的字号大小private float textSize;// 保存两个手指前一次的距离private float prevDist;public TouchZoomView(Context context, AttributeSet attrs, int defstyle) {super(context, attrs, defstyle);}public TouchZoomView(Context context, AttributeSet attrs) {super(context, attrs);}public TouchZoomView(Context context) {super(context);}@Overridepublic boolean onTouchEvent(MotionEvent event) {// 只处理触碰点大于或等于2(必须是多点触碰)的情形if (event.getPointerCount() >= 2) {// 获取该TextView默认的字号大小if (textSize == 0) {textSize = this.getTextSize();}// 对于多点触碰事件,需要使用getActionMasked来获取触摸事件类型switch (event.getActionMasked()) {// 处理手指按下的事件case MotionEvent.ACTION_POINTER_DOWN:// 计算两个手指之间的距离prevDist = calSpace(event);break;// 处理手指移动的事件case MotionEvent.ACTION_MOVE:// 实时计算两个手指之间的距离float curVDist = calSpace(event);// 根据两个手指之间的距离计算缩放比zoom(curVDist / prevDist);// 为下一次移动的缩放做准备prevDist = curVDist;break;}return true;}return super.onTouchEvent(event);}// 缩放字号private void zoom(float f) {textSize *= f;this.setTextSize(px2sp(getContext(), textSize));}// 将px值转换为sp值,保证文字大小不变public static int px2sp(Context context, float pxValue) {float fontScale = context.getResources().getDisplayMetrics().scaledDensity;return (int) (pxValue / fontScale + 0.5f);}// 计算两个手指之间的距离private float calSpace(MotionEvent event) {// 获取两个点之间X坐标的差值float x = event.getX(0) - event.getX(1);// 获取两个点之间Y坐标的差值float y = event.getY(0) - event.getY(1);// 计算两点距离return (float) Math.sqrt(x * x + y * y);}
}
解释
-
onTouchEvent
方法:- 通过
event.getPointerCount()
判断当前触碰点的数量,只处理多点触碰(即两个或更多触摸点)的情况。 - 使用
event.getActionMasked()
来处理触碰事件类型。MotionEvent.ACTION_POINTER_DOWN
用于处理新手指的按下事件,MotionEvent.ACTION_MOVE
用于处理手指的移动事件。
- 通过
-
计算缩放比例:
calSpace
方法计算两个手指之间的距离。zoom
方法根据手指之间的距离变化来调整TextView
的字号。px2sp
方法将像素值转换为 sp(scaled pixels),以保证字号的转换不受屏幕密度的影响。
-
初始化和更新:
- 在
onTouchEvent
中,处理手指按下时初始化prevDist
,然后在移动时实时更新字号。
- 在
总结
这个示例展示了如何通过多点触碰来缩放 TextView
的字号。用户可以通过捏合手势直观地调整文字的大小。处理多点触碰事件涉及到手指的距离计算和缩放比例的调整,掌握这些技能可以帮助你创建更加互动和友好的用户界面。
实例:通过多点触碰缩放图片
在这个示例中,我们将实现一个自定义的 View
子类,用于显示图片,并允许用户通过手指捏合来缩放图片,通过手指拖动来移动图片。这个示例将涉及到多点触碰事件的处理,以及 GestureDetector
的使用来处理拖动手势。
程序步骤
- 定义
TouchZoomImageView
类: 继承自View
,重写onTouchEvent
方法来处理触摸事件。 - 处理多点触碰事件: 使用
MotionEvent
的getPointerCount
和getActionMasked
方法来判断触摸点的数量和触摸事件的类型。 - 处理单点触摸事件: 使用
GestureDetector
处理拖动手势。 - 绘制图片: 使用
Matrix
对象处理图片的缩放和移动,并在onDraw
方法中绘制图片。
程序代码
public class TouchZoomImageView extends View {// 定义手势检测器变量private GestureDetector detector;// 保存该组件所显示的位图的变量private Bitmap mBitmap;// 定义对位图进行变换的 Matrixprivate Matrix matrix = new Matrix();private float prevDist;private float totalScaleRadio = 1.0f;private float totalTranslateX = 0.0f;private float totalTranslateY = 0.0f;// 为 TouchZoomImageView 定义不同场景下的构造器public TouchZoomImageView(Context context, AttributeSet attrs, int defstyle) {super(context, attrs, defstyle);initDetector();}public TouchZoomImageView(Context context, AttributeSet attrs) {super(context, attrs);initDetector();}public TouchZoomImageView(Context context) {super(context);initDetector();}// 初始化手势检测器public void initDetector() {detector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() {@Overridepublic boolean onFling(MotionEvent event1, MotionEvent event2, float velocityX, float velocityY) {// 根据手势的滑动距离来计算图片的位移float translateX = event2.getX() - event1.getX();float translateY = event2.getY() - event1.getY();totalTranslateX += translateX;totalTranslateY += translateY;postInvalidate();return false;}});}// 根据传入的位图资源ID来设置位图public void setImage(int resourceId) {// 根据位图资源ID来解析图片Bitmap bm = BitmapFactory.decodeResource(getResources(), resourceId);setImage(bm);}public void setImage(Bitmap bm) {this.mBitmap = bm;// 当传递过来位图之后,对位图进行初始化操作postInvalidate();}@Overridepublic boolean onTouchEvent(MotionEvent event) {// 获取当前触摸点的数量,如果触碰点的数量大于或等于2,则说明是缩放行为if (event.getPointerCount() >= 2) {// 使用 getActionMasked 来处理多点触碰事件switch (event.getActionMasked()) {// 处理手指按下的事件case MotionEvent.ACTION_POINTER_DOWN:// 计算两个手指之间的距离prevDist = calSpace(event);break;// 处理手指移动的事件case MotionEvent.ACTION_MOVE:float curDist = calSpace(event);// 计算出当前的缩放值float scaleRatio = curDist / prevDist;totalScaleRadio *= scaleRatio;// 调用 onDraw 方法,重新绘制界面postInvalidate();// 准备处理下一次缩放行为prevDist = curDist;break;}} else {// 对于单点触碰,将触碰事件交给 GestureDetector 处理detector.onTouchEvent(event);}return true;}@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);matrix.reset();// 处理缩放matrix.postScale(totalScaleRadio, totalScaleRadio);// 处理位移matrix.postTranslate(totalTranslateX, totalTranslateY);// 绘制图片if (mBitmap != null) {canvas.drawBitmap(mBitmap, matrix, null);}}// 计算两个手指之间的距离private float calSpace(MotionEvent event) {// 获取两个点之间 X 坐标的差值float x = event.getX(0) - event.getX(1);// 获取两个点之间 Y 坐标的差值float y = event.getY(0) - event.getY(1);// 计算两点距离return (float) Math.sqrt(x * x + y * y);}
}
解释
-
onTouchEvent
方法:- 当触摸点数量大于或等于 2 时,程序计算两个手指之间的距离,以此来计算缩放比例,并更新
totalScaleRadio
。 - 当触摸点数量为 1 时,将触摸事件交给
GestureDetector
处理,用于处理拖动手势。
- 当触摸点数量大于或等于 2 时,程序计算两个手指之间的距离,以此来计算缩放比例,并更新
-
initDetector
方法:- 初始化
GestureDetector
并重写onFling
方法来处理拖动手势,通过计算手势的滑动距离来更新图片的位置。
- 初始化
-
onDraw
方法:- 使用
Matrix
对象来处理图片的缩放和移动。Matrix
的postScale
和postTranslate
方法分别处理缩放和位移。
- 使用
-
calSpace
方法:- 计算两个手指之间的距离,用于计算缩放比例。
总结
这个示例展示了如何通过多点触碰来缩放图片,并通过手势拖动来移动图片。通过重写 onTouchEvent
方法来处理多点触碰事件,并使用 GestureDetector
来处理单点触摸事件,可以实现对图片的缩放和移动功能。这种交互方式在许多应用中非常实用,特别是在需要用户对图像进行操作的场景中。
实例:通过手势实现翻页效果
这个示例展示了如何通过手势来控制 ViewFlipper
实现翻页效果。ViewFlipper
是一个可以在多个视图之间进行切换的组件,结合手势检测器(GestureDetector
),我们可以实现用户通过滑动手势来切换视图的效果。
界面布局代码
布局文件 activity_main.xml
定义了一个 ViewFlipper
组件,用于在视图之间切换:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"><!-- 定义 ViewFlipper 组件 --><ViewFlipperandroid:id="@+id/flipper"android:layout_width="match_parent"android:layout_height="match_parent" />
</LinearLayout>
Activity 代码
MainActivity
实现了手势检测并控制 ViewFlipper
的视图切换:
public class MainActivity extends Activity {// ViewFlipper 实例private ViewFlipper flipper;// 定义手势检测器变量private GestureDetector detector;// 定义一个动画数组,用于为 ViewFlipper 指定切换动画效果private Animation[] animations = new Animation[4];// 定义手势动作两点之间的最小距离private float flipDistance = 0f;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);// 获取屏幕上 ViewFlipper 实例flipper = findViewById(R.id.flipper);// 为 ViewFlipper 添加 6 个 ImageView 组件flipper.addView(addImageView(R.drawable.javaee));flipper.addView(addImageView(R.drawable.java));flipper.addView(addImageView(R.drawable.ajax));flipper.addView(addImageView(R.drawable.android));flipper.addView(addImageView(R.drawable.html));flipper.addView(addImageView(R.drawable.swift));// 初始化 Animation 数组animations[0] = AnimationUtils.loadAnimation(this, R.anim.left_in);animations[1] = AnimationUtils.loadAnimation(this, R.anim.left_out);animations[2] = AnimationUtils.loadAnimation(this, R.anim.right_in);animations[3] = AnimationUtils.loadAnimation(this, R.anim.right_out);// 创建手势检测器detector = new GestureDetector(this, new GestureDetector.SimpleOnGestureListener() {@Overridepublic boolean onFling(MotionEvent event1, MotionEvent event2, float velocityX, float velocityY) {if (event1.getX() - event2.getX() > flipDistance) {// 手势从右向左滑动flipper.setInAnimation(animations[0]);flipper.setOutAnimation(animations[1]);flipper.showPrevious();return true;} else if (event2.getX() - event1.getX() > flipDistance) {// 手势从左向右滑动flipper.setInAnimation(animations[2]);flipper.setOutAnimation(animations[3]);flipper.showNext();return true;}return false;}});// 设置手势动作两点之间的最小距离flipDistance = getResources().getDimension(R.dimen.flip_distance);}// 定义添加 ImageView 的工具方法private View addImageView(int resId) {ImageView imageView = new ImageView(this);imageView.setImageResource(resId);imageView.setScaleType(ImageView.ScaleType.CENTER);return imageView;}@Overridepublic boolean onTouchEvent(MotionEvent event) {// 将该 Activity 上的触碰事件交给 GestureDetector 处理return detector.onTouchEvent(event);}
}
解释
-
布局文件:
ViewFlipper
组件用于在多个视图之间切换,布局文件中只定义了这个组件的宽高。
-
Activity 代码:
flipper
实例用于添加多个ImageView
,这些ImageView
将在ViewFlipper
中进行切换。- 手势检测器(
GestureDetector
)用于处理用户的滑动手势,并根据滑动方向来控制ViewFlipper
的切换。 - 在
onFling
方法中,通过比较两个触摸点的 X 坐标差值来判断滑动方向。如果差值大于flipDistance
,则认为是翻页手势。 - 根据手势方向设置
ViewFlipper
的动画效果,并调用showPrevious()
或showNext()
方法来切换视图。
-
动画效果:
AnimationUtils.loadAnimation()
方法加载动画资源,用于指定视图切换时的动画效果。flipper.setInAnimation()
和flipper.setOutAnimation()
方法分别设置视图进入和退出的动画效果。
-
手势处理:
- 在
onTouchEvent
方法中,将触摸事件交给GestureDetector
处理,实现对滑动手势的检测和响应。
- 在
运行效果
运行程序后,用户可以通过左右滑动手势来控制 ViewFlipper
中的视图切换,获得流畅的翻页效果。这个功能在电子书阅读器或图片查看器应用中非常常见,能够提升用户体验。
扩展:手势库和手势编辑
除了简单的手势处理,Android 还允许应用程序创建和管理手势库,使用 GestureLibrary
和 GestureOverlayView
来识别和处理自定义手势。有关手势库的创建、加载和管理,请参阅相关文档或教程以了解更多细节。
8.4.3 识别用户手势
在本节中,我们将介绍如何利用 GestureLibrary
来识别用户绘制的手势。通过使用 GestureLibrary
的 recognize(Gesture gesture)
方法,可以检测用户输入的手势是否与预定义的手势匹配,并获取匹配的结果。
程序代码
以下是一个示例程序,它利用前一个示例中创建的手势库来识别用户输入的手势:
public class MainActivity extends Activity {// 定义手势编辑组件private GestureOverlayView gestureView;// 记录手机上已有的手势库private GestureLibrary gestureLibrary;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);// 请求读写SD卡权限requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0x123);}@Overridepublic void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {// 如果用户授权访问SD卡if (requestCode == 0x123 && grantResults != null && grantResults[0] == 0) {// 读取上一个程序所创建的手势库gestureLibrary = GestureLibraries.fromFile(Environment.getExternalStorageDirectory().getPath() + "/mygestures");if (gestureLibrary.load()) {Toast.makeText(MainActivity.this, "手势文件装载成功!", Toast.LENGTH_SHORT).show();} else {Toast.makeText(MainActivity.this, "手势文件装载失败!", Toast.LENGTH_SHORT).show();}} else {Toast.makeText(MainActivity.this, "无法获取权限!", Toast.LENGTH_SHORT).show();}// 获取手势编辑组件gestureView = findViewById(R.id.gesture);// 为手势编辑组件绑定事件监听器gestureView.addOnGesturePerformedListener((source, gesture) -> {// 识别用户刚刚所绘制的手势List<Prediction> predictions = gestureLibrary.recognize(gesture);List<String> result = new ArrayList<>();// 遍历所有找到的 Prediction 对象for (Prediction pred : predictions) {// 只有相似度大于 2.0 的手势才会被输出if (pred.score > 2.0) {result.add("与手势【" + pred.name + "】相似度为" + pred.score);}}if (result.size() > 0) {ArrayAdapter<String> adapter = new ArrayAdapter<>(MainActivity.this,android.R.layout.simple_dropdown_item_1line, result);// 使用一个带 List 的对话框来显示所有匹配的手势new AlertDialog.Builder(MainActivity.this).setAdapter(adapter, null).setPositiveButton("确定", null).show();} else {Toast.makeText(MainActivity.this, "无法找到能匹配的手势!", Toast.LENGTH_SHORT).show();}});}
}
代码解释
-
权限请求:
- 在
onCreate
方法中,首先请求对 SD 卡的读写权限,以便访问存储的手势库。
- 在
-
权限结果处理:
- 在
onRequestPermissionsResult
方法中,检查权限请求是否被允许。如果权限被授予,加载手势库文件;如果加载成功,显示成功的 Toast 消息,否则显示失败的消息。
- 在
-
手势识别:
- 通过
GestureOverlayView
组件,用户可以在屏幕上绘制手势。 addOnGesturePerformedListener
方法用于监听手势绘制完成事件,并调用手势库的recognize
方法来识别手势。- 将识别结果(
Prediction
对象)根据相似度筛选,并将匹配结果显示在对话框中。
- 通过
-
匹配结果显示:
- 如果找到匹配的手势,使用
AlertDialog
显示匹配结果的列表。 - 如果没有匹配结果,则显示一个 Toast 提示用户无法找到匹配的手势。
- 如果找到匹配的手势,使用
注意事项
- SD 卡权限:程序需要访问 SD 卡上的手势文件,因此需要授予读写 SD 卡的权限。
- 手势相似度:程序只显示相似度大于 2.0 的手势,这个阈值可以根据实际需求进行调整。
通过这个示例程序,用户可以在设备上绘制手势并与预定义的手势进行匹配,识别结果会展示在对话框中。这个功能可以用于实现自定义手势识别的应用场景。
8.5 让应用说话 (TTS)
Android 提供了文本到语音(TextToSpeech, TTS)功能,可以将文本内容转换为语音输出。通过 TTS,应用程序可以实现动态的音频输出,增强用户体验。以下是如何使用 TTS 在 Android 应用中实现文本朗读和音频录制的步骤。
TTS 基础
-
创建 TextToSpeech 对象:
- 使用
TextToSpeech
类构造器创建对象,并传入TextToSpeech.OnInitListener
监听器来监测初始化结果。
TextToSpeech tts = new TextToSpeech(context, status -> {if (status == TextToSpeech.SUCCESS) {// 初始化成功} });
- 使用
-
设置语言和国家选项:
- 使用
setLanguage(Locale loc)
方法设置语言和国家选项。
int result = tts.setLanguage(Locale.US); if (result != TextToSpeech.LANG_COUNTRY_AVAILABLE && result != TextToSpeech.LANG_AVAILABLE) {// 语言不支持 }
- 使用
-
朗读文本:
- 使用
speak(CharSequence text, int queueMode, Bundle params, String utteranceId)
方法进行朗读。
tts.speak("Hello World", TextToSpeech.QUEUE_ADD, null, "utteranceId");
- 使用
-
将文本合成到文件:
- 使用
synthesizeToFile(CharSequence text, Bundle params, File file, String utteranceId)
方法将文本合成到音频文件。
tts.synthesizeToFile("Hello World", null, new File("path/to/file.wav"), "utteranceId");
- 使用
-
关闭 TTS:
- 在
Activity
的onDestroy
方法中调用shutdown()
来释放资源。
@Override public void onDestroy() {super.onDestroy();tts.shutdown(); }
- 在
示例程序
以下是一个示例程序,演示了如何使用 TTS 实现文本朗读和音频录制:
public class MainActivity extends Activity {private TextToSpeech tts;private EditText editText;private Button speech;private Button record;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);// 初始化 TextToSpeech 对象tts = new TextToSpeech(this, status -> {if (status == TextToSpeech.SUCCESS) {// 设置使用美式英语朗读int result = tts.setLanguage(Locale.US);if (result != TextToSpeech.LANG_COUNTRY_AVAILABLE && result != TextToSpeech.LANG_AVAILABLE) {Toast.makeText(MainActivity.this, "TTS暂时不支持这种语言的朗读。", Toast.LENGTH_SHORT).show();} else {editText = findViewById(R.id.txt);speech = findViewById(R.id.speech);record = findViewById(R.id.record);speech.setOnClickListener(view -> {// 执行朗读tts.speak(editText.getText().toString(), TextToSpeech.QUEUE_ADD, null, "speech");});record.setOnClickListener(view -> {// 将朗读文本的音频记录到指定文件中tts.synthesizeToFile(editText.getText().toString(), null, new File(getFilesDir() + "/sound.wav"), "record");Toast.makeText(MainActivity.this, "声音记录成功!", Toast.LENGTH_SHORT).show();});}}});}@Overridepublic void onDestroy() {super.onDestroy();// 关闭 TextToSpeech 对象tts.shutdown();}
}
代码说明
-
初始化 TTS:
- 创建
TextToSpeech
对象并设置语言。 - 如果语言支持不成功,显示提示消息。
- 创建
-
设置按钮点击事件:
- 朗读按钮:点击后调用
speak()
方法朗读文本框中的内容。 - 记录按钮:点击后调用
synthesizeToFile()
方法将文本朗读音频保存到指定文件。
- 朗读按钮:点击后调用
-
资源释放:
- 在
onDestroy
方法中调用tts.shutdown()
释放 TTS 资源。
- 在
注意事项
- 权限:确保应用有访问存储的权限(如果需要保存音频文件)。
- 语言支持:不同的设备和 TTS 引擎对语言的支持程度不同,可能需要使用特定的 TTS 引擎来获得更好的语言支持,例如科大讯飞的 TTS 引擎对于中文支持较好。
通过以上步骤和示例代码,你可以在 Android 应用中实现文本到语音的功能,提升用户体验。
8.6 本章小结
本章主要介绍了 Android 系统中输入和输出的各种支持方式,具体包括以下几个方面:
-
文件操作:
- Android 提供了便捷的文件 I/O 方法
openFileOutput
和openFileInput
,使得文件的读写变得简单方便。与 Java I/O 类似,但 Android 特有的方法可以更好地适应移动设备的存储机制。
- Android 提供了便捷的文件 I/O 方法
-
SharedPreferences:
- 用于存储应用程序的轻量级参数和设置。通过
SharedPreferences
,可以轻松地读写键值对,适用于存储用户偏好、应用状态等简单数据。
- 用于存储应用程序的轻量级参数和设置。通过
-
SQLite 数据库:
- Android 内置了 SQLite 数据库,为应用程序提供了一个轻量级的关系型数据库。通过 SQLite 的 API,可以进行数据库的创建、更新和查询操作。掌握 SQLite 的使用对于开发需要数据持久化功能的应用程序至关重要。
-
手势支持:
- Android 提供了手势检测和手势识别功能。手势检测用于识别用户在屏幕上的触摸动作,属于事件处理的范畴。手势识别则用于将手势与已定义的手势进行匹配,属于系统 I/O 方面的支持。这些功能可以用来实现更自然和直观的用户交互。
-
自动朗读 (TTS):
- TextToSpeech(TTS)功能可以将文本转换为语音,并进行播放。TTS 还支持将文本合成的语音保存为音频文件。通过 TTS,可以在应用程序中添加语音输出,增强用户体验。
总结
掌握本章内容,能够使你在 Android 开发中更加得心应手,尤其是在处理文件 I/O、数据库管理、手势交互和语音输出方面。利用这些功能可以提升应用程序的交互性和用户体验。