服务的启动和停止
服务是在后台默默运行着的Android组件
- onCreate:创建服务。
- onStartCommand:开始服务,Android 2.0及以上版本使用。
| 返回值类型 | 返回值说明 |
|---|---|
| START_STICKY | 黏性的服务。如果服务进程被杀掉,就保留服务的状态为开始状态,但不保留传送的 Intent 对象。随后系统尝试重新创建服务,由于服务状态为开始状态,因此创建服务后一定会调用onStartCommand方法如果在此期间没有任何启动命令传送给服务,参数 Intent 就为空值 |
| START_NOT_STICKY | 非黏性的服务。使用这个返回值时,如果服务被异常杀掉,系统就不会自动重启该服务 |
| START_REDELIVER_INTENT | 重传 Intent 的服务。使用这个返回值时,如果服务被异常杀掉,系统就会自动重启该服务,并传入 Intent 的原值 |
| START_STICKY_COMPATIBILITY | START_STICKY 的兼容版本,但不保证服务被杀后一定能重启 |
- onDestroy:销毁服务。
- onBind:绑定服务。
- onUnbind:解除绑定。返回值为true表示允许再次绑定,之后再绑定服务时不会调用onBind方法而是调用onRebind方法;返回值为false表示只能绑定一次,不能再次绑定。
- onRebind:重新绑定。只有上次的onUnbind方法返回true时,再次绑定服务才会调用onRebind方法。
创建一个 Service
public class MyService extends Service {
public MyService() {
}
@Override
public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
throw new UnsupportedOperationException("Not yet implemented");
}
}
并自动在 AndroidManifest.xml 中创建了一个 Service 标签
<application>
<service
android:name=".MyService"
android:enabled="true"
android:exported="true"></service>
启动服务 / 销毁服务
public class MyService extends Service {
public MyService() {
}
// 创建服务
@Override
public void onCreate() {
super.onCreate();
Log.e("222", "创建服务");
}
// 启动服务
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.e("222", "启动服务");
return super.onStartCommand(intent, flags, startId);
}
// 销毁服务
@Override
public void onDestroy() {
super.onDestroy();
Log.e("222", "销毁服务");
}
// 绑定服务,普通服务不存在绑定和解绑流程
@Override
public IBinder onBind(Intent intent) {
Log.e("222", "绑定服务");
throw new UnsupportedOperationException("Not yet implemented");
}
// 解绑服务---返回 true 表示允许多次已绑定,返回 false 表示只允许绑定一次
@Override
public boolean onUnbind(Intent intent) {
Log.e("222", "解绑服务");
return super.onUnbind(intent);
}
}
业务层启动
流程是--创建服务--启动服务--销毁服务
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private Intent intent; // 用于存储服务的 Intent 对象,以便后续停止服务使用
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 为开始和结束按钮设置点击事件监听器
findViewById(R.id.btn_start).setOnClickListener(this);
findViewById(R.id.btn_end).setOnClickListener(this);
}
@Override
public void onClick(View v) {
// 判断点击的是哪个按钮
if (v.getId() == R.id.btn_start) {
// 创建一个 Intent 对象,指定要启动的服务的类,这里是 MyService
intent = new Intent(this, MyService.class);
// 使用 startService 方法启动服务,使服务开始运行
startService(intent);
} else {
// 停止服务
stopService(intent);
}
}
}
绑定服务 / 解绑服务
点击绑定进行创建服务--绑定服务,点击解绑-解绑服务--销毁服务
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
// 文本视图对象
private static TextView textView;
// 服务对象
private MyService myService;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView = findViewById(R.id.text);
// 绑定按钮
findViewById(R.id.btn_start).setOnClickListener(this);
// 解绑按钮
findViewById(R.id.btn_end).setOnClickListener(this);
}
@Override
public void onClick(View v) {
// 绑定服务
if (v.getId() == R.id.btn_start) {
// 创建通往立即绑定服务的意图-- 意图对象
Intent intent = new Intent(this, MyService.class);
// 绑定服务,Context.BIND_AUTO_CREATE 表示服务不存在,系统自动创建
boolean service = bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
Log.e("221", service ? "true" : "false");
} else {
// 解绑服务,如果先前服务立即绑定,此时解绑之后服务自动停止
if (myService != null) {
unbindService(serviceConnection);
myService = null;
}
}
}
/**
* 链接服务接口
*/
private final ServiceConnection serviceConnection = new ServiceConnection() {
// 获取服务对象时的操作,成功连接
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
myService = ((MyService.LocalBinder) service).getService();
}
// 无法获取到服务对象时的操作,连接失败 / 终止链接
@Override
public void onServiceDisconnected(ComponentName name) {
myService = null;
}
};
}
活动与服务之间的交互
Service 加这个
// 定义当前服务的粘合剂,用于将该服务黏合到活动页面的进程中
public class LocalBinder extends Binder {
public MyService getService() {
return MyService.this;
}
public String getNumber(int number) {
return "收到了数字:" + number;
}
}
根据 IBinder 获取定义的通信方法
/**
* 链接服务接口
*/
private final ServiceConnection serviceConnection = new ServiceConnection() {
// 获取服务对象时的操作,成功连接
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
myService = ((MyService.LocalBinder) service).getService();
// 通过黏合剂 与服务通信
String number = ((MyService.LocalBinder) service).getNumber(99);
Log.e("xixi ", number);
}
图形定制
图形Drawable
Drawable类型表达了各种各样的图形,包括图片、色块、画板、背景等。
包含图片在内的图形文件放在res目录的各个drawable目录下,其中drawable目录一般保存描述性的XML文件,而图片文件一般放在具体分辨率的drawable目录下。
-
drawable-ldpi:
- 存放低分辨率的图片,例如 240x320 像素。
- 适用于较老的低分辨率设备,目前在新设备中已经较少使用。
-
drawable-mdpi:
- 存放中等分辨率的图片,例如 320x480 像素。
- 适用于一些中等分辨率的设备,已经较少使用。
-
drawable-hdpi:
- 存放高分辨率的图片,例如 480x800 像素。
- 适用于一些较小屏幕但高分辨率的设备,通常在 4 英寸到 4.5 英寸的手机上使用。
-
drawable-xhdpi:
- 存放加高分辨率的图片,例如 720x1280 像素。
- 适用于一些中等尺寸屏幕,通常在 5 英寸到 5.5 英寸的手机上使用。
-
drawable-xxhdpi:
- 存放超高分辨率的图片,例如 1080x1920 像素。
- 适用于一些大屏幕设备,通常在 6 英寸到 6.5 英寸的手机上使用。
-
drawable-xxxhdpi:
- 存放超超高分辨率的图片,例如 1440x2560 像素。
- 适用于一些大屏幕设备,通常在 7 英寸以上的平板计算机上使用。
形状图形
Shape图形又称形状图形,它用来描述常见的几何形状,包括矩形、圆角矩形、圆形、椭圆等等。
形状图形的定义文件是以shape标签为根节点的XML描述文件,它支持四种类型的形状(shape):
- rectangle:矩形。默认值
- oval:椭圆。此时corners节点会失效
- line:直线。此时必须设置stroke节点,不然会报错
- ring:圆环
除了根节点shape标签,形状图形还拥有下列规格标签:
- size(尺寸),它描述了形状图形的宽高尺寸。
- stroke(描边),它描述了形状图形的描边规格。
- corners(圆角),它描述了形状图形的圆角大小。
- solid(填充),它描述了形状图形的填充色彩。
- padding(间隔),它描述了形状图形与周围边界的间隔。
- gradient(渐变),它描述了形状图形的颜色渐变。
绘制
<!-- 定义一个形状,可以是矩形或圆形 -->
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 设置形状的填充色为 #ffdd66(浅黄色) -->
<solid android:color="#ffdd66" />
<!-- 设置形状的描边 -->
<stroke
android:width="10dp" <!-- 描边的宽度为10dp -->
android:color="#aaaaaa" /> <!-- 描边的颜色为 #aaaaaa(浅灰色) -->
<!-- 设置形状的圆角效果 -->
<corners android:radius="10dp" /> <!-- 圆角的半径为10dp -->
</shape>
// 获取刚刚的定义的图形
View view = findViewById(R.id.content);
// 将图形设置为背景色
view.setBackgroundResource(R.drawable.x2);
九宫格图片
点九图片的扩展名是png,文件名后面常带有“.9”字样。因为该图片划分了3×3的九宫格区域,所以得名点九图片,也叫九宫格图片。
在拉伸点九图片时,只拉伸内部区域,不拉伸边缘线条。
在Android Studio中右击某张图片,并在右键菜单中选择“Create 9-Patch files”,接着单击OK按钮即可自动生成点九图片。
状态列表图形
Button按钮的背景在正常情况下是凸起的,在按下时是凹陷的,从按下到弹起的过程,用户便能知道点击了这个按钮。
在项目中创建状态图形的XML文件,则需右击drawable目录,然后在右键菜单中依次选择New→Drawable resource file,即可自动生成一个空的XML文件。
定义图形
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true"
android:drawable="@drawable/button_normal"/>
<item android:drawable="@drawable/button_pressed_orig"/>
</selector>
使用图形
android:background="@drawable/status_draw"
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/status_draw"
android:text="按钮"
android:layout_margin="10dp"
/>
| 属性名称 | 说明 | 适用的控件 |
|---|---|---|
state_pressed | 是否按下 | 按钮 (Button) |
state_checked | 是否勾选 | 复选框 (CheckBox), 单选按钮 (RadioButton) |
state_focused | 是否获取焦点 | 文本编辑框 (EditText) |
state_selected | 是否选中 | 各控件通用 |
选择按钮
CompoundButton类是抽象的复合按钮,由它派生而来的子类包括:复选框CheckBox、单选按钮RadioButton以及开关按钮Switch。
CompoundButton 基本用法
CompoundButton在XML文件中主要使用下面两个属性。
- checked:指定按钮的勾选状态,true表示勾选,false表示未勾选。默认未勾选。
- button:指定左侧勾选图标的图形资源。如果不指定就使用系统的默认图标。
CompoundButton在Java代码中主要使用下列4种方法。
- setChecked:设置按钮的勾选状态。
- setButtonDrawable:设置左侧勾选图标的图形资源。
- setOnCheckedChangeListener:设置勾选状态变化的监听器。
- isChecked:判断按钮是否勾选。
复选框
<!--
android:checked="true" 是否选中
-->
<CheckBox
android:id="@+id/check_id"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
android:text="系统 box" />
java 代码
// 获取复选框
CheckBox checkBox = findViewById(R.id.check_id);
// 点击
checkBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
Log.e("选中状态:", isChecked ? "已选中" : "未选中");
}
});
// 判断选中状态
// checkBox.isChecked()
开关按钮
Switch 是开关按钮,它在选中与取消选中时可展现的界面元素比复选框丰富。
Switch控件新添加的XML属性说明如下。
- textOn:设置右侧开启时的文本。
- textOff:设置左侧关闭时的文本。
- track:设置开关轨道的背景。
- thumb:设置开关标识的图标。
可以直接用 Switch
也可以设计图形,仿 IOS 开关
形状 @drawable/swich
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checked="true"
android:drawable="@drawable/switch_on"
/>
<item android:drawable="@drawable/switch_off"/>
</selector>
布局
<!--
android:checked="true" 是否选中
-->
<CheckBox
android:id="@+id/checK_id"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/swich"
android:button="null" />
radio 单选
单选按钮要在一组按钮中选择其中一项,并且不能多选,这要求有个容器确定这组按钮的范围,这个容器便是单选组RadioGroup
RadioGroup实质上是个布局,同一组RadioButton都要放在同一个RadioGroup节点下。除了RadioButton,也允许放置其他控件
单选组与线性布局相比,它们主要有以下两个区别:
- 单选组多了管理单选按钮的功能,而线性布局不具备该功能;
- 如果不指定orientation属性,那么单选组默认垂直排列,而线性布局默认水平排列;
判断选中了哪个单选按钮,通常不是监听某个单选按钮,而是监听单选组的选中事件。 下面是RadioGroup常用的3个方法。
- check:选中指定资源编号的单选按钮。
- getCheckedRadioButtonId:获取选中状态单选按钮的资源编号。
- setOnCheckedChangeListener:设置单选按钮勾选变化的监听器。
布局标签
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="请选择额你的性别:"/>
<RadioGroup
android:id="@+id/rg_sx"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<RadioButton
android:id="@+id/men"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="男"/>
<RadioButton
android:id="@+id/women"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="女"/>
</RadioGroup>
<TextView
android:id="@+id/text_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
java 代码
textView = findViewById(R.id.text_2);
// 选中组
RadioGroup radioGroup = findViewById(R.id.rg_sx);
radioGroup.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(RadioGroup group, int checkedId) {
if (checkedId == R.id.men)
textView.setText("哇!你是一个帅气的男孩!");
else if (checkedId == R.id.women)
textView.setText("哇!你是一个可爱的女孩!");
}
});
文本输入
EditText 文本编辑框
用户可在此输入文本等信息。
EditText的常用属性说明如下。
- inputType:指定输入的文本类型。若同时使用多种文本类型,则可使用竖线“|”把多种文本类型拼接起来。
- maxLength:指定文本允许输入的最大长度。
- hint:指定提示文本的内容,提示文字
- textColorHint:指定提示文本的颜色。
| 输入类型 | 说明 |
|---|---|
| text | 文本 |
| textPassword | 文本密码。显示时用圆点“·”代替 |
| number | 整型数 |
| numberSigned | 带符号的数字。允许在开头带负号“-” |
| numberDecimal | 带小数点的数字 |
| numberPassword | 数字密码。显示时用圆点“·”代替 |
| datetime | 时间日期格式。除了数字外,还允许输入横线、斜杆、空格、冒号 |
| date | 日期格式。除了数字外,还允许输入横线“-”和斜杆“/” |
| time | 时间格式。除了数字外,还允许输入冒号“:” |
可以配置 形状 + select选择 美化边框
xml
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="下面是登录信息:" />
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/edit_text_selectot"
android:hint="请输入用户名"
android:inputType="text"
android:maxLength="10" />
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/edit_text_selectot"
android:hint="请输入密码"
android:inputType="textPassword"
android:maxLength="8" />
/>
焦点变更监听器
编辑框点击两次后才会触发点击事件,因为第一次点击只触发焦点变更事件,第二次点击才触发点击事件。
若要判断是否切换编辑框输入,应当监听焦点变更事件,而非监听点击事件。
调用编辑框对象的setOnFocusChangeListener方法,即可在光标切换之时(获得光标和失去光标)触发焦点变更事件。
EditText editText = findViewById(R.id.edit_text);
// 监听焦点变化
editText.setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
// hasFocus true 获取焦点,false 失去焦点
Editable text = editText.getText();
String phone = text.toString();
if (phone.isBlank() || phone.length() != 11) {
// 获取焦点
editText.requestFocus();
// 短时的提示信息
Toast.makeText(MainActivity.this, "请输入11位手机号!",
Toast.LENGTH_SHORT).show();
}
}
});
文本变化的监听器
调用编辑框对象的addTextChangedListener方法即可注册文本监听器。
文本监听器的接口名称为TextWatcher,该接口提供了3个监控方法,具体说明如下。
- beforeTextChanged:在文本改变之前触发。
- onTextChanged:在文本改变过程中触发。
- afterTextChanged:在文本改变之后触发。
案例:实现输入到11位自动关闭软键盘
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 获取对应的 EditText 控件
EditText editText = findViewById(R.id.edit_text);
// 创建一个自定义的 TextWatcher,用于监听文本变化并在达到指定长度时隐藏软键盘
HideTextWatcher hideTextWatcher = new HideTextWatcher(editText, 11);
// 将 TextWatcher 添加到 EditText 控件上
editText.addTextChangedListener(hideTextWatcher);
}
// 定义编辑框监听器
private class HideTextWatcher implements TextWatcher {
// 编辑框对象
private EditText editText;
// 最大长度变量
private int mMaxLength;
public HideTextWatcher(EditText editText, int mMaxLength) {
super();
this.editText = editText;
this.mMaxLength = mMaxLength;
}
// 在编辑框输入文本变化前触发
@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) {
String string = s.toString();
// 输入达到11位自动关闭键盘
if (mMaxLength == string.length()) {
// 获取输入法管理器
InputMethodManager inputMethodManager = getSystemService(InputMethodManager.class);
// 获取 EditText 控件的 windowToken,用于指定要关闭软键盘的目标控件
IBinder windowToken = editText.getWindowToken();
// 关闭软键盘,参数 0 表示不考虑任何附加选项
inputMethodManager.hideSoftInputFromWindow(windowToken, 0);
}
}
}
}
对话框
提醒对话框
AlertDialog可以完成常见的交互操作,例如提示、确认、选择等功能。AlertDialog借助建造器AlertDialog.Builder才能完成参数设置,AlertDialog.Builder的常用方法说明如下。
- setIcon:设置对话框的标题图标。
- setTitle:设置对话框的标题文本。
- setMessage:设置对话框的内容文本。
- setPositiveButton:设置肯定按钮的信息,包括按钮文本和点击监听器。
- setNegativeButton:设置否定按钮的信息,包括按钮文本和点击监听器。
- setNeutralButton:设置中性按钮的信息,包括按钮文本和点击监听器。
调用建造器的create方法生成对话框实例,再调用对话框实例的show方法,在页面上弹出提醒对话框。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView view = findViewById(R.id.text);
findViewById(R.id.xie).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 创建提醒对话框的建造器
AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
builder.setTitle("尊敬的用户");
builder.setMessage("你真的要卸载我吗? ");
// 设置确定按钮
builder.setPositiveButton("残忍卸载", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
view.setText("虽然依依不舍,但是只能离开了");
}
});
// 设置否定按钮
builder.setNegativeButton("我再想想", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
view.setText("让我再陪你365天");
}
});
// 根据建造器构建提醒对话框对象
AlertDialog alertDialog = builder.create();
// 显示
alertDialog.show();
}
});
}
}
日期对话框
日期选择器DatePicker可以让用户选择具体的年月日。
DatePickerDialog相当于在AlertDialog上装载了DatePicker,日期选择事件则由监听器OnDateSetListener负责响应,在该监听器的onDateSet方法中,开发者获取用户选择的具体日期,再做后续处理。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 文本框
TextView textView = findViewById(R.id.text);
// 弹出对话框
findViewById(R.id.xie).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 获取日历对象
Calendar instance = Calendar.getInstance();
// 构建日期对话框, 该对话框已经集成了日期选择器,参数2: 返回选中的结果
DatePickerDialog datePickerDialog = new DatePickerDialog(MainActivity.this, new DatePickerDialog.OnDateSetListener() {
@Override
public void onDateSet(DatePicker view, int year, int month, int dayOfMonth) {
instance.set(year, month, dayOfMonth);
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
Date time = instance.getTime();
String format1 = format.format(time);
textView.setText(format1);
}
// 下面是配置年份
}, instance.get(Calendar.YEAR), instance.get(Calendar.MONTH),
instance.get(Calendar.DAY_OF_MONTH));
// 显示
datePickerDialog.show();
}
});
}
}
时间对话框
时间选择器TimePicker可以让用户选择具体的小时和分钟
TimePickerDialog的用法类似DatePickerDialog,不同之处有两个:
- 构造方法传的是当前的小时与分钟,最后一个参数表示是否采取二十四小时制,一般传true,表示小时的数值范围为0~23;若为false则表示采取十二小时制。
- 时间选择监听器为OnTimeSetListener,对应需要实现onTimeSet方法,在该方法中可获得用户选择的小时和分钟。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 文本框
TextView textView = findViewById(R.id.text);
// 弹出对话框
findViewById(R.id.xie).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 获取日历对象
Calendar instance = Calendar.getInstance();
// 构建时间对话框, 该对话框集成了时间监听器
TimePickerDialog pickerDialog = new TimePickerDialog(MainActivity.this, new TimePickerDialog.OnTimeSetListener() {
@Override
public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
textView.setText("您选择的是:" + hourOfDay + "时" + minute + "分钟");
}
}, instance.get(Calendar.HOUR_OF_DAY), instance.get(Calendar.MINUTE), true);
// 显示
pickerDialog.show();
}
});
}
}
数据库
键值对
SharedPreferences是Android的一个轻量级存储工具,采用的存储结构是Key-Value的键值对方式。
共享参数的存储介质是符合XML规范的配置文件。保存路径是:/data/data/应用包名/shared_prefs/文件名.xml
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<int name="age" value="30" />
</map>
共享参数主要适用于如下场合:
- 简单且孤立的数据。若是复杂且相互间有关的数据,则要保存在数据库中。
- 文本形式的数据。若是二进制数据,则要保存在文件中。
- 需要持久化存储的数据。在App退出后再次启动时,之前保存的数据仍然有效。
实际开发中,共享参数经常存储的数据有App的个性化配置信息、用户使用App的行为信息、临时需要保存的片段信息等。
写入数据
// 获取共享参数实例
// MODE_PRIVATE 私有模式
SharedPreferences sharedPreferences = getSharedPreferences("share", MODE_PRIVATE);
// 获取共享参数编辑器
SharedPreferences.Editor edit = sharedPreferences.edit();
// put 数据
edit.putInt("age", 30);
edit.putString("name", "Mr Lee");
edit.putBoolean("married", true);
edit.putFloat("weight", 100f);
// 提交修改
boolean commit = edit.commit();
// 异步提交,会先将数据写入内存,再存入磁盘
// edit.apply();
读入数据
// 获取共享参数实例
// MODE_PRIVATE 私有模式
SharedPreferences sharedPreferences = getSharedPreferences("share", MODE_PRIVATE);
// 参数一是key, 参数二是默认值
int age = sharedPreferences.getInt("age", 0);
boolean age1 = sharedPreferences.getBoolean("married", false);
float weight = sharedPreferences.getFloat("weight", 0);
String name = sharedPreferences.getString("name", "");
更安全的数据仓库
Android官方推出了数据仓库DataStore,并将其作为Jetpack库的基础组件。
DataStore提供了两种实现方式,分别是Preferences DataStore 和Proto DataStore,前者采用键值对存储数据,后者采用自定义类型存储数据。
其中Preferences DataStore可以直接替代SharedPreferences。
添加依赖
implementation "androidx.datastore:datastore-preferences:1.0.0"
implementation "androidx.datastore:datastore-preferences-rxjava2:1.0.0"
设置 数据存储工具类
// 数据存储工具类,用于简化使用 DataStore 进行数据存储和检索的操作
public class DataStoreUtil {
// 声明一个数据仓库工具的实例
private static DataStoreUtil instance;
// 声明一个数据仓库实例(注意:使用 androidx.datastore.preferences.core.Preferences;)
private RxDataStore<Preferences> rxDataStore;
// 私有构造方法,用于初始化 DataStore 实例
private DataStoreUtil(Context context) {
rxDataStore = new RxPreferenceDataStoreBuilder
(context.getApplicationContext(), "dataStore").build();
}
// 获取数据仓库工具实例的方法,采用单例模式
public static DataStoreUtil getInstance(Context context) {
if (instance == null) {
instance = new DataStoreUtil(context);
}
return instance;
}
// 根据 key 获取数据的方法
public String getStringValue(String key) {
// 创建一个 String 类型的 Preferences.Key 对象,用于标识和访问字符串类型的数据。
Preferences.Key<String> stringKey = PreferencesKeys.stringKey(key);
// 使用 RxDataStore 获取 Flowable 对象,以便观察 Preferences 数据的变化。
Flowable<String> flowable = rxDataStore.data().map(preferences -> preferences.get(stringKey));
// 阻塞并获取 Flowable 中的第一个元素,即字符串数据。
return flowable.blockingFirst();
}
// 设置指定名称的字符串值的方法
public void setStringValue(String key, String value) {
// 创建一个 String 类型的 Preferences.Key 对象,用于标识和访问字符串类型的数据。
Preferences.Key<String> stringKey = PreferencesKeys.stringKey(key);
// 使用 RxDataStore 提供的 updateDataAsync 方法,在异步任务中更新 Preferences 数据。
Single<Preferences> preferencesSingle = rxDataStore.updateDataAsync(preferences -> {
// 将 Preferences 转换为 MutablePreferences,以便进行可变操作。
MutablePreferences mutablePreferences = preferences.toMutablePreferences();
// 设置指定键的字符串值。
mutablePreferences.set(stringKey, value);
// 返回更新后的 MutablePreferences 对象。
return Single.just(mutablePreferences);
});
}
// 获取指定 key 的整型数据
public Integer getIntValue(String key) {
// 创建 Int 类型的存储,用于存储和检索数据
Preferences.Key<Integer> keyId = PreferencesKeys.intKey(key);
// 在数据仓库中获取指定数据
Flowable<Integer> flow = rxDataStore.data().map(prefs -> prefs.get(keyId));
return flow.blockingFirst();
}
// 设置指定名称的整型数
public void setIntValue(String key, Integer value) {
Preferences.Key<Integer> keyId = PreferencesKeys.intKey(key);
Single<Preferences> result = rxDataStore.updateDataAsync(prefs -> {
MutablePreferences mutablePrefs = prefs.toMutablePreferences();
//Integer oldValue = prefs.get(keyId);
mutablePrefs.set(keyId, value);
return Single.just(mutablePrefs);
});
}
// 获取指定名称的双精度数
public Double getDoubleValue(String key) {
Preferences.Key<Double> keyId = PreferencesKeys.doubleKey(key);
Flowable<Double> flow = rxDataStore.data().map(prefs -> prefs.get(keyId));
try {
return flow.blockingFirst();
} catch (Exception e) {
return 0.0;
}
}
// 设置指定名称的双精度数
public void setDoubleValue(String key, Double value) {
Preferences.Key<Double> keyId = PreferencesKeys.doubleKey(key);
Single<Preferences> result = rxDataStore.updateDataAsync(prefs -> {
MutablePreferences mutablePrefs = prefs.toMutablePreferences();
//Double oldValue = prefs.get(keyId);
mutablePrefs.set(keyId, value);
return Single.just(mutablePrefs);
});
}
// 获取指定名称的布尔值
public Boolean getBooleanValue(String key) {
Preferences.Key<Boolean> keyId = PreferencesKeys.booleanKey(key);
Flowable<Boolean> flow = rxDataStore.data().map(prefs -> prefs.get(keyId));
try {
return flow.blockingFirst();
} catch (Exception e) {
return false;
}
}
// 设置指定名称的布尔值
public void setBooleanValue(String key, Boolean value) {
Preferences.Key<Boolean> keyId = PreferencesKeys.booleanKey(key);
Single<Preferences> result = rxDataStore.updateDataAsync(prefs -> {
MutablePreferences mutablePrefs = prefs.toMutablePreferences();
//Boolean oldValue = prefs.get(keyId);
mutablePrefs.set(keyId, value);
return Single.just(mutablePrefs);
});
}
}
SQLite
是一种小巧嵌入式数据库, 机构化查询
标准的SQL语句分为三类:数据定义、数据操纵和数据控制,但不同的数据库往往有自己的实现。
SQLite是一种小巧的嵌入式数据库,由于它属于轻型数据库,不涉及复杂的数据控制操作,因此App开发只用到数据定义和数据操纵两类SQL。
SQLite的SQL语法与通用的SQL语法略有不同。
创建表格
-
大小写敏感性: SQL语句在SQLite中不区分大小写,包括关键词、表格名称、字段名称。唯一区分大小写的是被单引号括起来的字符串值。
-
防止重复建表: 为避免重复建表,建议在CREATE TABLE语句中加上
IF NOT EXISTS关键词,例如CREATE TABLE IF NOT EXISTS 表格名称...。 -
数据类型支持: SQLite支持整型(
INTEGER)、长整型(LONG)、字符串(VARCHAR)、浮点数(FLOAT)等数据类型,但不支持布尔类型。布尔类型的数据应使用整型保存,在入库时SQLite会自动转为0或1,其中0表示false,1表示true。 -
唯一标识字段: 在建表时,通常需要一个唯一标识字段,一般命名为
id。创建新表时,务必加上该字段的定义,例如id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL。
-- 创建名为 user_info 的表,如果不存在则创建
CREATE TABLE IF NOT EXISTS user_info
(
-- 主键,自增长,整数类型,非空
_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
-- 用户名,字符串类型,非空
name VARCHAR NOT NULL,
-- 年龄,整数类型,非空
age INTEGER NOT NULL,
-- 身高,长整数类型,非空
height LONG NOT NULL,
-- 体重,浮点数类型,非空
weight FLOAT NOT NULL,
-- 婚姻状态,整数类型,非空
married INTEGER NOT NULL,
-- 更新时间,字符串类型,非空
update_time VARCHAR NOT NULL
);
删除表格
drop table if exists user_info
添加字段
格式为 ALTER TABLE 表格名称 修改操作
SQLite只支持增加字段,不支持修改字段,也不支持删除字段。
alter table user_info
add column 字段 类型
添加数据
insert into user_info (name, age, height, weight, married, update_time)
values ('张三', 20, 170, 50, 0, '20200504')
删除数据
DELETE FROM user_info
WHERE age = 30;
修改数据
UPDATE user_info
SET age = 25
WHERE name = 'John';
查询数据并排序
SELECT *
FROM user_info
WHERE age >= 25
ORDER BY age DESC;
数据库管理器SQLiteDatabase
SQLiteDatabase是SQLite的数据库管理类,它提供了若干操作数据表的API,常用的方法有3类:
- 管理类,用于数据库层面的操作
- openDatabase:打开指定路径的数据库
- isOpen:判断数据库是否已打开
- close:关闭数据库
- getVersion:获取数据库的版本号
- setVersion:设置数据库的版本号
- 事务类,用于事务层面的操作
- beginTransaction:开始事务
- setTransactionSuccessful:设置事务的成功标志
- endTransaction:结束事务
- 数据处理类,用于数据表层面的操作
- execSQL:执行拼接好的SQL控制语句
- delete:删除符合条件的记录
- update:更新符合条件的记录
- insert:插入一条记录
- query:执行查询操作,返回结果集的游标
- rawQuery:执行拼接好的SQL查询语句,返回结果集的游标
创建数据库 / 删除数据库
// 获取应用程序的内部文件目录,用于存储数据库文件
File filesDir = getFilesDir();
// 打开或创建名为 "test.db" 的数据库文件,采用私有模式(Context.MODE_PRIVATE)
SQLiteDatabase db = openOrCreateDatabase(filesDir + "/test.db", Context.MODE_PRIVATE, null);
// 获取文本视图对象
TextView viewById = findViewById(R.id.text);
// 设置文本内容,显示数据库创建成功的路径
viewById.setText("数据库创建在" + db.getPath() + "成功");
// 删除数据库
deleteDatabase(getFilesDir() + "/test.db");
数据库帮助器SQLiteOpenHelper
由于SQLiteDatabase存在局限性,一不小心就会重复打开数据库,处理数据库的升级也不方便,因此Android提供了数据库帮助器SQLiteOpenHelper,帮助开发者合理使用SQLite。
- 新建一个继承自SQLiteOpenHelper的数据库操作类,提示重写onCreate和onUpgrade两个方法。
- 封装保证数据库安全的必要方法,包括以下三种。
- 获取单例对象:确保App运行时数据库只被打开一次,避免重复打开引起错误。
- 打开数据库连接:读连接可调用SQLiteOpenHelper的getReadableDatabase方法获得,写连接可调用getWritableDatabase获得。
- 关闭数据库连接:数据库操作完了,调用SQLiteDatabase对象的close方法关闭连接。
- 提供对表记录进行增加、删除、修改、查询的操作方法。
游标
调用SQLiteDatabase的query和rawQuery方法时,返回的都是Cursor对象,因此获取查询结果要根据游标的指示一条一条遍历结果集合。
Cursor的常用方法可分为3类:
- 游标控制类方法,用于指定游标的状态。
- close:关闭游标。
- isClosed:判断游标是否关闭。
- isFirst:判断游标是否在开头。
- isLast:判断游标是否在末尾。
- 游标移动类方法,把游标移动到指定位置。
- moveToFirst:移动游标到开头。
- moveToLast:移动游标到末尾。
- moveToNext:移动游标到下一条记录。
- moveToPrevious:移动游标到上一条记录。
- move:往后移动游标若干条记录。
- moveToPosition:移动游标到指定位置的记录。
- 获取记录类方法,可获取记录的数量、类型以及取值。
- getCount:获取结果记录的数量。
- getInt:获取指定字段的整型值。
- getLong:获取指定字段的长整型值。
- getFloat:获取指定字段的浮点数值。
- getString:获取指定字段的字符串值。
- getType:获取指定字段的字段类型。
实战
配置工具类
/**
* TotalUtils 是一个工具类,提供了用于显示 Toast 的静态方法。
*/
public class TotalUtils {
/**
* 显示短时长的 Toast 消息。
*
* @param context 上下文对象,用于显示 Toast。
* @param desc 要显示的消息内容。
*/
public static void show(Context context, String desc) {
Toast.makeText(context, desc, Toast.LENGTH_SHORT).show();
}
}
工具类
/**
* 数据库操作工具类
*/
public class UserDBUtils extends SQLiteOpenHelper {
// 单例模式实例类
private static UserDBUtils userDBUtils;
// 数据库版本
private static final int DB_VERSION = 1;
// 链接的数据库
private static final String DB_DATABASE = "user.db";
// 表名
private static final String DB_TABLE = "user_info";
// 数据库实例
private SQLiteDatabase sqLiteDatabase;
// 获取单例
public static synchronized UserDBUtils getInstance(Context context, int version) {
if (version > 0 && userDBUtils == null) {
userDBUtils = new UserDBUtils(context, version);
} else if (userDBUtils == null) {
userDBUtils = new UserDBUtils(context);
}
return userDBUtils;
}
/**
* 构造函数,指定数据库版本
*
* @param context 上下文
* @param version 数据库版本
*/
public UserDBUtils(Context context, int version) {
// 参数一 当前对象, 参数2 连接的数据库名称
super(context, DB_DATABASE, null, version);
}
/**
* 构造函数,默认数据库版本
*
* @param context 上下文
*/
public UserDBUtils(Context context) {
super(context, DB_DATABASE, null, DB_VERSION);
}
/**
* 创建表
*
* @param db 数据库实例
*/
@Override
public void onCreate(SQLiteDatabase db) {
// 创建表
String table = "create table " + DB_TABLE + " (_id INTEGER not null primary key autoincrement, " +
"name VARCHAR not null, age INTEGER not null, height LONG not null, weight FLOAT " +
"not null, married INTEGER not null, update_time VARCHAR not null );";
db.execSQL(table);
}
/**
* 升级数据库,执行表结构变更语句
*
* @param db 数据库实例
* @param oldVersion 旧版本号
* @param newVersion 新版本号
*/
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
if (newVersion > 1) {
// android 的 alter 命令不支持一次添加多列, 只能分多次添加
String alter_sql = " alter table " + DB_TABLE + " add column phone varchar;";
db.execSQL(alter_sql);
alter_sql = " alter table " + DB_TABLE + " add column password varchar";
db.execSQL(alter_sql);
}
}
/**
* 打开数据库写连接
*
* @return SQLiteDatabase写连接实例
*/
public SQLiteDatabase openWriteLink() {
if (sqLiteDatabase == null || !sqLiteDatabase.isOpen()) {
sqLiteDatabase = userDBUtils.getWritableDatabase();
}
return sqLiteDatabase;
}
/**
* 打开数据库的读连接
*
* @return SQLiteDatabase读连接实例
*/
public SQLiteDatabase openReadLink() {
if (sqLiteDatabase == null || !sqLiteDatabase.isOpen()) {
sqLiteDatabase = userDBUtils.getReadableDatabase();
}
return sqLiteDatabase;
}
/**
* 关闭数据库连接
*/
public void closeLink() {
if (sqLiteDatabase != null && sqLiteDatabase.isOpen()) {
sqLiteDatabase.close();
sqLiteDatabase = null;
}
}
/**
* 新增一条数据
*
* @param userInfo 用户信息实例
* @return 是否成功插入
*/
public boolean insert(UserInfo userInfo) {
ArrayList<UserInfo> list = new ArrayList<>();
list.add(userInfo);
return insert(list);
}
/**
* 批量新增
*
* @param infoList 用户信息列表
* @return 是否成功插入
*/
public boolean insert(List<UserInfo> infoList) {
for (UserInfo userInfo : infoList) {
ContentValues contentValues = new ContentValues();
contentValues.put("name", userInfo.getName());
contentValues.put("age", userInfo.getAge());
contentValues.put("height", userInfo.getHeight());
contentValues.put("weight", userInfo.getWeight());
contentValues.put("married", userInfo.getMark());
contentValues.put("update_time", userInfo.getDate());
// 执行插入记录动作,该语句返回插入记录的行号
long insert = sqLiteDatabase.insert(DB_TABLE, null, contentValues);
if (insert != 1) return false;
}
return true;
}
/**
* 查询操作
*
* @param con 查询条件
* @return 查询结果集
*/
public ArrayList<UserInfo> query(String con) {
ArrayList<UserInfo> userInfos = new ArrayList<>();
String sql = "select * from " + DB_TABLE;
if (!TextUtils.isEmpty(con)) {
sql += " where " + con + ";";
} else {
sql += ";";
}
try (
// 执行查询语句, 返回结果集的游标
Cursor rawQuery = sqLiteDatabase.rawQuery(sql, null)
) {
// 循环游标,获取数据 , 获取每一列的数据
while (rawQuery.moveToNext()) {
int id = rawQuery.getInt(0);
String name = rawQuery.getString(1);
int age = rawQuery.getInt(2);
float height = rawQuery.getFloat(3);
Double weight = rawQuery.getDouble(4);
boolean married = rawQuery.getInt(5) == 1;
String dateTime = rawQuery.getString(6);
UserInfo userInfo = new UserInfo(id, name, weight, height, age, married, dateTime);
userInfos.add(userInfo);
}
}
return userInfos;
}
}
新增操作
public class MainActivity extends AppCompatActivity {
private UserDBUtils userDBUtils;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 获取布局中的控件
EditText nameView = findViewById(R.id.e_name);
EditText ageView = findViewById(R.id.e_age);
EditText heightView = findViewById(R.id.e_height);
EditText weightView = findViewById(R.id.e_weight);
CheckBox marriedView = findViewById(R.id.ck_married);
// 设置保存按钮的点击事件
findViewById(R.id.save).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 获取用户输入的数据
String name = nameView.getText().toString();
String age = ageView.getText().toString();
String height = heightView.getText().toString();
String weight = weightView.getText().toString();
boolean checked = marriedView.isChecked();
// 判断数据是否完整
if (TextUtils.isEmpty(name)) {
TotalUtils.show(MainActivity.this, "请填写姓名");
return;
} else if (TextUtils.isEmpty(age)) {
TotalUtils.show(MainActivity.this, "请填写年龄");
return;
} else if (TextUtils.isEmpty(height)) {
TotalUtils.show(MainActivity.this, "请填写身高");
return;
} else if (TextUtils.isEmpty(weight)) {
TotalUtils.show(MainActivity.this, "请填写体重");
return;
}
// 创建 UserInfo 对象
UserInfo userInfo = new UserInfo(name, weight, height, age, checked);
// 设置日期
userInfo.setDate(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(new Date()));
// 插入数据到数据库
userDBUtils.insert(userInfo);
// 显示写入数据库成功的提示
TotalUtils.show(MainActivity.this, "写入数据库成功!");
}
});
}
// 在活动初始化时, 获取数据库帮助实例
@Override
protected void onStart() {
super.onStart();
// 获取数据库帮助类实例
userDBUtils = UserDBUtils.getInstance(this, 1);
// 打开数据库帮助器的写连接
userDBUtils.openWriteLink();
}
// 在活动停止时关闭数据库连接
@Override
protected void onStop() {
super.onStop();
// 关闭数据库连接
userDBUtils.closeLink();
}
}
查询数据
/**
* 主活动类
*/
public class MainActivity extends AppCompatActivity {
private UserDBUtils userDBUtils; // 用户数据库工具类实例
private TextView view; // 文本视图实例
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 获取标签
view = findViewById(R.id.tv_weight);
}
/**
* 活动初始化时调用
*/
@Override
protected void onStart() {
super.onStart();
// 获取数据库帮助类
userDBUtils = UserDBUtils.getInstance(this, 1);
// 打开数据库帮助器的读连接
userDBUtils.openReadLink();
// 获取数据
readSQLite();
}
/**
* 显示数据库中的数据
*/
private void readSQLite() {
StringBuilder s = new StringBuilder();
// 查询数据库
ArrayList<UserInfo> query = userDBUtils.query(null);
// 循环遍历查询结果
for (UserInfo userInfo : query) {
s.append(userInfo.toString());
}
// 在文本视图中显示数据
view.setText(s.toString());
}
/**
* 活动销毁时调用
*/
@Override
protected void onStop() {
super.onStop();
// 关闭数据库连接
userDBUtils.closeLink();
}
}
存储卡
Android把外部存储分成了两块区域,一块是所有应用均可访问的公共空间,另一块是只有应用自己才可访问的私有空间。
Android在SD卡的 Android/data 目录下给每个应用又单独建了一个文件目录,用于给应用保存自己需要处理的临时文件。这个给每个应用单独建立的文件目录,只有当前应用才能够读写文件,其它应用是不允许进行读写的,故而Android/data 目录算是外部存储上的私有空间。
Android从7.0开始加强了SD卡的权限管理,App使用SD卡的公共控件前既需要事先声明权限,又需要在设置页面开启权限,使用私有空间无需另外设置权限。
公共空间读写文件需要加权限
<!--存储卡读写权限-->
<uses-permission-sdk-23 android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission-sdk-23 android:name="android.permission.READ_EXTERNAL_STORAGE" />
- 获取公共空间的存储路径,调用的是Environment类的getExternalStoragePublicDirectory方法
- 获取应用私有空间的存储路径,调用的是getExternalFilesDir方法
// 获取系统的公共空间 // Environment.DIRECTORY_DOWNLOADS 表示 download目录
File directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
// 获取当前 app 的私有路径
File externalFilesDir = getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS);
读取图片
Android的位图工具是 Bitmap,App 读写 Bitmap 可以使用性能更好的 BufferedOutputStream 和BufferedInputStream。
Android还提供了BitmapFactory工具用于读取各种来源的图片
- decodeResource:该方法可从资源文件中读取图片信息。
- decodeFile:该方法可将指定路径的图片读取到Bitmap对象。
- decodeStream:该方法从输入流中读取位图数据。
申请权限
-
API 版本: 代码中使用了
@RequiresApi(api = Build.VERSION_CODES.TIRAMISU)注解,要求在 Android 版本 Tiramisu(API 级别 33)及以上版本运行。这可能是根据您的应用程序需求,要求特定的 Android 版本。 -
动态权限申请: 通过
ContextCompat.checkSelfPermission检查是否有Manifest.permission.READ_MEDIA_IMAGES权限。如果没有权限,通过ActivityCompat.requestPermissions请求该权限。在onRequestPermissionsResult方法中处理权限请求的结果。 -
访问 DCIM 目录: 使用
Environment.getExternalStoragePublicDirectory获取外部存储上的 DCIM 目录。 -
显示图片: 在按钮点击事件中,通过获取 DCIM 目录下的第一个文件夹的第一个文件,创建文件的 URI,并将其设置到 ImageView 中显示。
-
Toast 提示: 在
onRequestPermissionsResult方法中,根据用户是否授予权限显示相应的 Toast 提示。
| 权限 | 用途 |
|---|---|
android.permission.CAMERA | 允许应用程序访问设备的相机,以拍摄照片或录制视频。 |
android.permission.READ_EXTERNAL_STORAGE | 允许应用程序读取设备外部存储,如图库中的图片。 |
android.permission.WRITE_EXTERNAL_STORAGE | 允许应用程序写入设备外部存储,用于保存文件或图片。 |
android.permission.READ_CONTACTS | 允许应用程序读取设备中的联系人信息。 |
android.permission.SEND_SMS | 允许应用程序发送短信。 |
android.permission.ACCESS_FINE_LOCATION | 允许应用程序访问设备的精准位置信息。 |
android.permission.RECORD_AUDIO | 允许应用程序录制音频。 |
android.permission.READ_PHONE_STATE | 允许应用程序读取设备的电话状态信息。 |
android.permission.BLUETOOTH | 允许应用程序执行与蓝牙相关的操作。 |
android.permission.INTERNET | 允许应用程序访问网络。 |
android.permission.ACCESS_NETWORK_STATE | 允许应用程序获取网络状态信息。 |
配置权限
<!--存储卡读写权限-->
<uses-permission-sdk-23 android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission-sdk-23 android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission-sdk-23 android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission-sdk-23 android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission-sdk-23 android:name="android.permission.READ_MEDIA_VIDEO" />
public class MainActivity extends AppCompatActivity {
private ImageView imageView;
private final ArrayList<File> arrayList = new ArrayList<>();
@RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 初始化 ImageView 对象
imageView = findViewById(R.id.imageView);
// 获取 DCIM 目录
File directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM);
// 检查是否有 READ_MEDIA_IMAGES 权限
int check = ContextCompat.checkSelfPermission(this, Manifest.permission.READ_MEDIA_IMAGES);
if (check == PackageManager.PERMISSION_GRANTED) {
// 已经授予权限,可以执行相关操作
System.out.println("Permission granted");
} else {
// 如果权限未被授予,请求权限
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_MEDIA_IMAGES}, 1);
}
// 设置按钮点击事件
findViewById(R.id.save).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 获取 DCIM 目录下的第一个文件夹的第一个文件
File[] files = directory.listFiles();
File file = files[0].listFiles()[0];
// 通过文件创建 URI,并将其设置到 ImageView 中显示
Uri parse = Uri.fromFile(file);
imageView.setImageURI(parse);
// 将文件添加到 ArrayList
arrayList.add(file);
}
});
}
@RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == 1) {
// 处理权限请求结果
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// 用户授予了 READ_MEDIA_IMAGES 权限
Toast.makeText(this, "授权成功!", Toast.LENGTH_SHORT).show();
} else {
// 用户拒绝了 READ_MEDIA_IMAGES 权限
Toast.makeText(this, "授权被拒绝!", Toast.LENGTH_SHORT).show();
}
}
}
}
应用程序
Application是Android的一大组件,在App运行过程中有且仅有一个Application对象贯穿整个生命周期。
自定义启动应用组件
<application android:name=".MainApplication"
创建 MainApplication 应用组件
这是一个应用程序的全局上下文。
- onCreate: 启动时调用
- onTerminate: 终止时调用
- onConfigurationChanged: 配置改变时调用
onTerminate 永远都不会调用
/**
* 自定义的应用程序类,继承自 Android 的 {@link Application} 类。
* 该类可以用于全局初始化、管理应用程序的生命周期等操作。
*/
public class MainApplication extends Application {
/**
* 在应用程序创建时调用。
*/
@Override
public void onCreate() {
super.onCreate();
// 在这里进行应用程序的初始化操作,例如配置全局变量、初始化第三方库等。
}
/**
* 在应用程序终止时调用。
*/
@Override
public void onTerminate() {
super.onTerminate();
// 在这里执行应用程序终止时的清理工作。
}
/**
* 在应用程序配置更改(例如屏幕旋转)时调用。
*
* @param newConfig 新的配置信息。
*/
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// 在这里处理应用程序配置更改的逻辑,如果有需要的话。
}
}
Application 全局变量
Application的生命周期覆盖了App运行的全过程。不像短暂的Activitv生命周期,一旦退出该页面,Activity实例就被销毁。因此,利用Application的全生命特性,能够在 Application 实例中存储全局变量
- 会频繁读取的信息,如用户名、手机号等。
- 不方便由意图传递的数据,例如位图对象、非字符串类型的集合对象等。
- 容易因频繁分配内存而导致内存泄漏的对象,如Handler对象等。
/**
* 自定义的应用程序类,继承自 Android 的 {@link Application} 类。
* 该类用于提供全局的应用程序上下文、全局变量等。
*/
public class MainApplication extends Application {
// 先声明一个静态实例
private static MainApplication mainApplication;
// 声明一个公共的信息映射,做全局变量使用
public HashMap<String, String> stringHashMap = new HashMap<>();
/**
* 获取应用程序的静态实例。
*
* @return 应用程序的实例。
*/
public static MainApplication getInstance() {
return mainApplication;
}
/**
* 在应用程序创建时调用。
*/
@Override
public void onCreate() {
super.onCreate();
mainApplication = this;
}
/**
* 在应用程序终止时调用。
*/
@Override
public void onTerminate() {
super.onTerminate();
// 在这里执行应用程序终止时的清理工作,如果有需要的话。
}
/**
* 在应用程序配置更改(例如屏幕旋转)时调用。
*
* @param newConfig 新的配置信息。
*/
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// 在这里处理应用程序配置更改的逻辑,如果有需要的话。
}
}
存入数据
MainApplication instance = MainApplication.getInstance();
instance.stringHashMap.put("name", "kun222");
获取全部数据
MainApplication instance = MainApplication.getInstance();
HashMap<String, String> stringHashMap = instance.stringHashMap;
String name = stringHashMap.get("name");
避免方法数过多的问题
为了解决方法数过多的问题,Android推出了名叫MultiDex的解决方案,也就是在打包时把应用分成多个dex文件,每个dex的方法数量均不超过65536个,由此规避了方法数过多的限制。
- 修改模块的build.gradle文件,导入指定版本的MultiDex库。
implementation 'androidx.multidex:multidex:2.0.1'
- 在 defaultConfig 节点添加以下配置, 表示开启多个 dex 功能
android {
defaultConfig {
// 避免方法数最多65536的问题
multiDexEnabled true
}
- 自定义的 Application 更换集成 MainApplication
public class MainApplication extends MultiDexApplication {
- android name 设置类
android:name=".MainApplication"
利用 Room 简化数据库操作
添加依赖
implementation 'androidx.room:room-runtime:2.4.2'
annotationProcessor 'androidx.room:room-compiler:2.4.2'
- 实体类
import androidx.room.Entity;
@Entity
public class Book {
}
- 配置字段信息
@Entity
public class Book {
// 表示该字段是主键, 不能重复
@PrimaryKey
// 表示非空字段
@NonNull
// 图书名称
private String name;
// 作者
private String author;
// 出版社
private String press;
// 价格
private double price;
}
- 持久层
- 查询: @Query
- 新增: @Insert
- 更新: @Update
- 删除: @Delete
@Dao
public interface BookDao {
// 设置查询语句
@Query("select * from book")
List<Book> getAllBook();
// 条件查询
@Query("select * from Book where name = :name")
Book getBookName(String name);
// 记录重复时替换原纪录
@Insert(onConflict = OnConflictStrategy.REPLACE)
int insertBook(Book book);
// 插入多条记录
@Insert
int insertList(List<Book> bookList);
// 更新信息
@Update(onConflict = OnConflictStrategy.REPLACE)
int updateBook(Book book);
// 删除
@Delete
int deleteBook(Book book);
// 删除所有, 使用自定义语句
@Query("delete from Book")
int deleteAllBook();
}
- 指定数据库层
- entities: 指定数据库的表
- version: 版本号
- exportSchema: 是否导出数据库信息的JSON 字符串, 设置true 还需要在 build.gradle中指定保存路径
/**
* entities: 指定数据库的表
* version: 版本号
* exportSchema: 是否导出数据库信息的JSON 字符串, 设置true 还需要在 build.gradle中指定保存路径
*/
@Database(entities = Book.class, version = 1, exportSchema = false)
public abstract class BookDatabase extends RoomDatabase {
// 获取该数据库中某张表的持久化对象
public abstract BookDao bookDao();
}
自定义图书数据库唯一实例
配置在 Application 中
public class MainApplication extends MultiDexApplication {
// 先声明一个静态实例
private static MainApplication mainApplication;
// 声明一个公共的信息映射, 做全局变量使用
public HashMap<String, String> stringHashMap = new HashMap<>();
// 图书 database 数据库
private BookDatabase bookDatabase;
// 获取实例
public static MainApplication getInstance() {
return mainApplication;
}
@Override
public void onCreate() {
super.onCreate();
mainApplication = this;
bookDatabase = Room.databaseBuilder(mainApplication, BookDatabase.class, "book")
// 允许迁移数据库 发生数据库变更时 room 默认删除原数据库再创建新数据库
// 如此一来记录会丢失 故而修改迁移方式
.addMigrations()
// 允许在主线程中操作数据库 默认不支持
.allowMainThreadQueries()
.build();
}
// 获取图书实例
public BookDatabase getBookDatabase() {
return bookDatabase;
}
@Override
public void onTerminate() {
super.onTerminate();
}
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
}
}
使用
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 为保存按钮设置点击监听器
findViewById(R.id.save).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 获取应用程序的实例
MainApplication instance = MainApplication.getInstance();
// 获取数据库实例
BookDatabase bookDatabase = instance.getBookDatabase();
// 获取 BookDao 实例
BookDao bookDao = bookDatabase.bookDao();
// 从用户输入的 EditText 中获取书的信息
String name = ((EditText) findViewById(R.id.name)).getText().toString();
String author = ((EditText) findViewById(R.id.author)).getText().toString();
String press = ((EditText) findViewById(R.id.press)).getText().toString();
double price = Double.parseDouble(((EditText) findViewById(R.id.price)).getText().toString());
// 创建 Book 对象并插入数据库,返回插入的主键
long insertBook = bookDao.insertBook(new Book(name, author, press, price));
// 打印插入的行数
Log.i("22", String.valueOf(insertBook));
}
});
}