Android 学习笔记(二)

201 阅读35分钟

服务的启动和停止

服务是在后台默默运行着的Android组件

  • onCreate:创建服务。
  • onStartCommand:开始服务,Android 2.0及以上版本使用。
返回值类型返回值说明
START_STICKY黏性的服务。如果服务进程被杀掉,就保留服务的状态为开始状态,但不保留传送的 Intent 对象。随后系统尝试重新创建服务,由于服务状态为开始状态,因此创建服务后一定会调用onStartCommand方法如果在此期间没有任何启动命令传送给服务,参数 Intent 就为空值
START_NOT_STICKY非黏性的服务。使用这个返回值时,如果服务被异常杀掉,系统就不会自动重启该服务
START_REDELIVER_INTENT重传 Intent 的服务。使用这个返回值时,如果服务被异常杀掉,系统就会自动重启该服务,并传入 Intent 的原值
START_STICKY_COMPATIBILITYSTART_STICKY 的兼容版本,但不保证服务被杀后一定能重启
  • onDestroy:销毁服务。
  • onBind:绑定服务。
  • onUnbind:解除绑定。返回值为true表示允许再次绑定,之后再绑定服务时不会调用onBind方法而是调用onRebind方法;返回值为false表示只能绑定一次,不能再次绑定。
  • onRebind:重新绑定。只有上次的onUnbind方法返回true时,再次绑定服务才会调用onRebind方法。

创建一个 Service

image.png

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目录下。

  1. drawable-ldpi:

    • 存放低分辨率的图片,例如 240x320 像素。
    • 适用于较老的低分辨率设备,目前在新设备中已经较少使用。
  2. drawable-mdpi:

    • 存放中等分辨率的图片,例如 320x480 像素。
    • 适用于一些中等分辨率的设备,已经较少使用。
  3. drawable-hdpi:

    • 存放高分辨率的图片,例如 480x800 像素。
    • 适用于一些较小屏幕但高分辨率的设备,通常在 4 英寸到 4.5 英寸的手机上使用。
  4. drawable-xhdpi:

    • 存放加高分辨率的图片,例如 720x1280 像素。
    • 适用于一些中等尺寸屏幕,通常在 5 英寸到 5.5 英寸的手机上使用。
  5. drawable-xxhdpi:

    • 存放超高分辨率的图片,例如 1080x1920 像素。
    • 适用于一些大屏幕设备,通常在 6 英寸到 6.5 英寸的手机上使用。
  6. drawable-xxxhdpi:

    • 存放超超高分辨率的图片,例如 1440x2560 像素。
    • 适用于一些大屏幕设备,通常在 7 英寸以上的平板计算机上使用。

形状图形

Shape图形又称形状图形,它用来描述常见的几何形状,包括矩形、圆角矩形、圆形、椭圆等等。

形状图形的定义文件是以shape标签为根节点的XML描述文件,它支持四种类型的形状(shape):

  1. rectangle:矩形。默认值
  2. oval:椭圆。此时corners节点会失效
  3. line:直线。此时必须设置stroke节点,不然会报错
  4. ring:圆环

除了根节点shape标签,形状图形还拥有下列规格标签:

  1. size(尺寸),它描述了形状图形的宽高尺寸。
  2. stroke(描边),它描述了形状图形的描边规格。
  3. corners(圆角),它描述了形状图形的圆角大小。
  4. solid(填充),它描述了形状图形的填充色彩。
  5. padding(间隔),它描述了形状图形与周围边界的间隔。
  6. 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按钮即可自动生成点九图片。

image.png

image.png

状态列表图形

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。

1702532401097.jpg

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类:

  1. 管理类,用于数据库层面的操作
  • openDatabase:打开指定路径的数据库
  • isOpen:判断数据库是否已打开
  • close:关闭数据库
  • getVersion:获取数据库的版本号
  • setVersion:设置数据库的版本号
  1. 事务类,用于事务层面的操作
  • beginTransaction:开始事务
  • setTransactionSuccessful:设置事务的成功标志
  • endTransaction:结束事务
  1. 数据处理类,用于数据表层面的操作
  • 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。

  1. 新建一个继承自SQLiteOpenHelper的数据库操作类,提示重写onCreate和onUpgrade两个方法。
  2. 封装保证数据库安全的必要方法,包括以下三种。
  • 获取单例对象:确保App运行时数据库只被打开一次,避免重复打开引起错误。
  • 打开数据库连接:读连接可调用SQLiteOpenHelper的getReadableDatabase方法获得,写连接可调用getWritableDatabase获得。
  • 关闭数据库连接:数据库操作完了,调用SQLiteDatabase对象的close方法关闭连接。
  1. 提供对表记录进行增加、删除、修改、查询的操作方法。

游标

调用SQLiteDatabase的query和rawQuery方法时,返回的都是Cursor对象,因此获取查询结果要根据游标的指示一条一条遍历结果集合。

Cursor的常用方法可分为3类:

  1. 游标控制类方法,用于指定游标的状态。
  • close:关闭游标。
  • isClosed:判断游标是否关闭。
  • isFirst:判断游标是否在开头。
  • isLast:判断游标是否在末尾。
  1. 游标移动类方法,把游标移动到指定位置。
  • moveToFirst:移动游标到开头。
  • moveToLast:移动游标到末尾。
  • moveToNext:移动游标到下一条记录。
  • moveToPrevious:移动游标到上一条记录。
  • move:往后移动游标若干条记录。
  • moveToPosition:移动游标到指定位置的记录。
  1. 获取记录类方法,可获取记录的数量、类型以及取值。
  • 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:该方法从输入流中读取位图数据。

申请权限

  1. API 版本: 代码中使用了 @RequiresApi(api = Build.VERSION_CODES.TIRAMISU) 注解,要求在 Android 版本 Tiramisu(API 级别 33)及以上版本运行。这可能是根据您的应用程序需求,要求特定的 Android 版本。

  2. 动态权限申请: 通过 ContextCompat.checkSelfPermission 检查是否有 Manifest.permission.READ_MEDIA_IMAGES 权限。如果没有权限,通过 ActivityCompat.requestPermissions 请求该权限。在 onRequestPermissionsResult 方法中处理权限请求的结果。

  3. 访问 DCIM 目录: 使用 Environment.getExternalStoragePublicDirectory 获取外部存储上的 DCIM 目录。

  4. 显示图片: 在按钮点击事件中,通过获取 DCIM 目录下的第一个文件夹的第一个文件,创建文件的 URI,并将其设置到 ImageView 中显示。

  5. 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 实例中存储全局变量

  1. 会频繁读取的信息,如用户名、手机号等。
  2. 不方便由意图传递的数据,例如位图对象、非字符串类型的集合对象等。
  3. 容易因频繁分配内存而导致内存泄漏的对象,如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个,由此规避了方法数过多的限制。

  1. 修改模块的build.gradle文件,导入指定版本的MultiDex库。
implementation 'androidx.multidex:multidex:2.0.1'
  1. 在 defaultConfig 节点添加以下配置, 表示开启多个 dex 功能
android {
    defaultConfig {
        // 避免方法数最多65536的问题
        multiDexEnabled true
    }
  1. 自定义的 Application 更换集成 MainApplication
public class MainApplication extends MultiDexApplication {
  1. android name 设置类
android:name=".MainApplication"

利用 Room 简化数据库操作

添加依赖

implementation 'androidx.room:room-runtime:2.4.2'
annotationProcessor 'androidx.room:room-compiler:2.4.2'
  1. 实体类
import androidx.room.Entity;

@Entity
public class Book {

}
  1. 配置字段信息
@Entity
public class Book {
    // 表示该字段是主键, 不能重复
    @PrimaryKey
    // 表示非空字段
    @NonNull
    // 图书名称
    private String name;
    // 作者
    private String author;
    // 出版社
    private String press;
    // 价格
    private double price;
}
  1. 持久层
  • 查询: @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();
}
  1. 指定数据库层
  • 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));
        }
    });
}