- 本文是对B站教程 动脑学院 Android教程 学习过程中所做的笔记。
- 文章分为上下两部分,此文是上部分,下部分链接为:Android基础教程——从入门到精通(下)
- 源视频教程并没有录制全,本文还补充了 Service 和 网络通信 的内容
- 文章介绍详细,示例代码丰富,相信跟着本教程可以打下很好的Android基础。
文章目录
一、开发环境搭建
- 安装android studio
- 安装 sdk(当前使用最新版33)
- 手动下载gradle(更新:弄完之后有时候没用,可以再试试挂梯子,换网络之类的)如果第一次启动AndroidStudio没有报错则无需设置,这里是因为我启动完之后下载gradle报错:
could not install gradle distribution from 'https://services.gradle.org/dist
可能是网络问题连接不到,所以手动下载。点击上面提示的链接下载压缩包,然后解压到C:\Users\OYMN\.gradle\wrapper\dists\gradle-7.2-bin\2dnblmf4td7x66yl1d74lt32g
- 安装模拟器使用androidstudio提供的模拟器,或者自行下载第三方安卓模拟器(雷电模拟器)
二、简单控件
1. 文本显示
设置文本内容有两种方式:
- 在 XML 文件中通过属性 android:text 设置文本
- 在 Java 代码中调用文本视图对象的 setText 方法设置文本
引用字符串资源:
- 在XML文件中引用(@string/xxx)
- 在Java代码中引用(R.string.xxx)
其余设置文本字体大小,颜色等都是可以通过关键词+代码提示很容易就能知道怎么写,这里就不赘述。
2. 按钮
Button继承于TextView,因此它们拥有的属性都是共通的。
除此之外,Button最重要的是点击事件。
- 点击监听器:通过setOnClickListener方法设置。按钮被按住少于500毫秒时,会触发点击事件。
- 长按监听器:通过setOnLongClickListener方法设置。按钮被按住超过500毫秒时,会触发长按事件。
3. 常用布局
(1)线性布局LinearLayout
特点:要不水平排列,要不竖直排列,通过orintation进行设置(horiztal为水平,vertical为竖直)
权重属性:通过layout_weight来设置,在线性布局的直接下级进行设置,表示该下级布局占据的宽高比例。
- layout_width填0dp时,layout_weight表示水平方向的宽度比例。
- layout_height填0dp时,layout_weight表示垂直方向的高度比例。
(3)相对布局RelativeLayout
相对布局中的视图位置由两个因素所影响:
- 与该视图平级的其他视图
- 上级视图(也就是它归属的RelativeLayout)
相对位置的一些取值:
(3)网格布局GridLayout
顾名思义该布局适用于表格类型的布局。
4. 图像显示
图片一般放在res/drawable目录下,设置图像显示一般有两种方法:
- 在XML文件中,通过属性android:src设置图片资源,属性值格式形如 @drawable/不含扩展名的图片名称。
- 在Java代码中,调用setImageResource方法设置图片资源,方法参数格式形如 R.drawable.不含扩展名的图片名称。
(1)图像的缩放问题:
ImageView本身默认图片居中显示,若要改变图片的显示方式,可通过scaleType属性设定,该属性的取值说明如下:
(2)图像按钮ImageButton:
ImageButton是显示图片的图像按钮,但它继承自ImageView,而非继承Button。
ImageButton和Button之间的区别有:
- Button既可显示文本也可显示图片,ImageButton只能显示图片不能显示文本。
- ImageButton上的图像可按比例缩放,而Button通过背景设置的图像会拉伸变形。
- Button只能靠背景显示一张图片,而ImageButton可分别在前景和背景显示图片,从而实现两张图片叠加的效果。
三、Activity
Activity是安卓开发四大组件之一,非常重要。
1. Activity的启动和结束
Activity的启动这里指的是跳转,从一个页面跳转到一个新的页面,就相当于启动了一个新的页面。
示例:
bt.setOnClickListener(newView.OnClickListener(){@OverridepublicvoidonClick(View v){Intent intent =newIntent();
intent.setClass(MainActivity.this,MainActivity2.class);startActivity(intent);}});
结束Activity:调用
finish()
。
2. Activity的生命周期
onCreate:此时将页面布局加载到内存中,初始化页面。
onStart:将页面展示在屏幕。
onResume:此时页面能够和用户进行交互。
onPause:页面进入暂停状态,无法和用户进行交互。
onStop:页面不在屏幕显示。
onDestory:回收Activity占用的资源,彻底销毁该Activity。
onRestart:onStop状态可以转为onRestart状态。
onNewIntent:重用已存在的活动实例。如果一个Activity已经启动了,并且存在与当前栈,而当前栈的启动模式为SingleTask,SingleInstance,SingleTop(此时在任务栈顶端),那么再次启动该Activity的话,并不会重新进行onCreate,而是会执行onNewIntent方法。
3. Activity的启动模式
Android允许在创建Activity时设置启动模式,通过启动模式控制Activity的出入栈行为。
(1)静态设置
设置方式:打开AndroidManifest.xml文件,给activity添加属性android:launchMode。如以下表示该activity使用standard标准模式,默认也是标准模式。
<activityandroid:name=".JumpFirstActivity"android:launchMode="standard"/>
launchMode的取值有:
(2)动态设置
通过 Intent 动态设置 Activity启动模式:
intent.setFlags();
4. Activity之间传递信息
Intent能够让Android各组件之间进行沟通。
Intent可以完成3部分工作:
- 表明本次通信从哪里来,往哪里走,要怎么走。
- 发送方可以携带消息给接收方,接收方可以从收到的Intent解析数据。
- 发送方如果想要知道接收方的处理结果,接收方也可以通过Intent返回结果。
Intent的一些组成元素:
(1)显式Intent和隐式Intent
1. 显式Intent
创建方式:
- 在Intent的构造函数中指定:
Intent intent =newIntent(this,NextActivity.class);
- 调用setClass指定:
Intent intent =newIntent();intent.setClass(this,NextActivity.class);
- 调用setComponent指定:
Intent intent =newIntent();ComponentName component =newComponentName(this,NextActivity.class);intent.setComponent(component);
2. 隐式Intent:
没有明确指定所要跳转的页面,而是通过一些动作字符串来让系统自动匹配。
通常是App不想向外暴露Activity的名称,只给出一些定义好的字符串。这些字符串可以自己定义,也有系统定义的。
常见的系统动作如下:
下面以调用系统拨号页面举例:
String phone ="12345";Intent intent =newIntent();//这里表示设置意图动作为准备拨号
intent.setAction(Intent.ACTION_DIAL);
intent.setData(Uri.parse("tel:"+ phone));startActivity(intent);
如果想要跳转到自己定义的activity:
步骤一:在AndroidManifest.xml找到该activity,添加action和category标签,同时设置exported为true,表示允许被其他activity调用。
步骤二:调用过程和上面一样:
Intent intent =newIntent();
intent.setAction("android.intent.action.activity2");
intent.addCategory(Intent.CATEGORY_DEFAULT);startActivity(intent);
(2)向下一个Activity发送消息:
Intent重载了很多putExtra方法用于传递各种类型的信息,包括整数类型,字符串等。但是显然通过调用putExtra方法会很不好管理,因为数据都是零碎传递。所以Android引入了Bundle,其内部是一个Map,使用起来也和Map一样。
示例:
Intent intent =newIntent(this,NextActivity.class);//通过bundle包装数据Bundle bundle =newBundle();
bundle.putString("stringKey","stringValue");
intent.putExtras(bundle);startActivity(intent);
然后下一个Activity就可以通过intent获取到所想要的数据了:
Bundle bundle =getIntent().getExtras();String stringValue = bundle.getString("stringKey");
(3)向上一个Activity返回消息:
上一个页面跳转到下一个页面,同时携带数据:
privateActivityResultLauncher<Intent> register;@OverrideprotectedvoidonCreate(@NullableBundle savedInstanceState){super.onCreate(savedInstanceState);setContentView(R.layout.activity_main2);findViewById(R.id.bt).setOnClickListener(this);//回调函数,返回到这个页面时所执行的程序
register =registerForActivityResult(newActivityResultContracts.StartActivityForResult(),newActivityResultCallback<ActivityResult>(){//回调函数@OverridepublicvoidonActivityResult(ActivityResult result){if(result !=null){Intent intent = result.getData();if(intent !=null&& result.getResultCode()==Activity.RESULT_OK){//获取到返回的数据Bundle bundle = intent.getExtras();//...}}}});}@OverridepublicvoidonClick(View v){Intent intent =newIntent(this,MainActivity3.class);//跳转下一页面
register.launch(intent);}
下一个页面接受到数据,处理之后返回结果给上一个页面:
Bundle bundle =getIntent().getExtras();//...页面进行处理//返回数据给上一个页面Bundle bundle =newBundle();
bundle.putString("stringKey","stringValue");
intent.putExtras(bundle);setResult(Activity.RESULT_OK, intent);finish();
5. Activity获取一些附加信息
(1)获取资源信息:
//获取strings.xml中的字符串资源String text =getString(R.string.text);//获取color.xml中的颜色资源int black =getColor(R.color.black);
(2)获取元数据信息:
try{//获取包管理器PackageManager pm =getPackageManager();//获取当前的Activity信息ActivityInfo activityInfo = pm.getActivityInfo(getComponentName(),PackageManager.GET_META_DATA);Bundle bundle = activityInfo.metaData;String text2 = bundle.getString("text2");}catch(PackageManager.NameNotFoundException e){
e.printStackTrace();}
四、数据存储
1. 共享参数SharedPreferences
(1)使用:
sharedPreferences是安卓的一个轻量级存储工具,采用的方式是key-value,以xml文件形式存在,文件路径为/data/data/应用包名/shared_prefs/文件名.xml。
适合场景:
- 简单且孤立的数据
- 文本数据,二进制数据则不合适
- 需要持久化的数据,也就是重启APP后数据仍然存在且有效。
实际开发中,sharedPreferences经常用来存储的数据有:APP的个性化配置信息,用户使用APP的行为信息等。
sharedPreferences对数据的存储和读取类似Map,提供put和set方法。
获取数据可以通过SharedPreferences对象获取:
//第一个参数表示文件名,第二个参数表示私有模式SharedPreferences shared =getSharedPreferences("fileName",MODE_PRIVATE);String name = shared.getString("name");
而存储数据则还需要借助Editor类:
SharedPreferences.Editor editor = shared.edit();
editor.putString("name","oymn");
editor.putInt("age",20);
editor.commit();
(2)应用实例:记住密码功能
- 声明一个共享参数对象,并在onCreate中调用getSharedPreferences方法获取共享参数的实例。
- 登录成功时,如果用户勾选了“记住密码”,就使用共享参数保存手机号码与密码。
所以在登录页面的onCreat方法中添加获取共享参数的代码:
// 从share_login.xml获取共享参数对象
mShared =getSharedPreferences("share_login",MODE_PRIVATE);// 获取共享参数保存的手机号码String phone = mShared.getString("phone","");// 获取共享参数保存的密码String password = mShared.getString("password","");
et_phone.setText(phone);// 往手机号码编辑框填写上次保存的手机号
et_password.setText(password);// 往密码编辑框填写上次保存的密码
接着在登录成功方法中添加保存功能:
// 如果勾选了“记住密码”,就把手机号码和密码都保存到共享参数中if(isRemember){SharedPreferences.Editor editor = mShared.edit();// 获得编辑器的对象
editor.putString("phone", et_phone.getText().toString());// 添加名叫phone的手机号码
editor.putString("password", et_password.getText().toString());// 添加名叫password的密码
editor.commit();// 提交编辑器中的修改}
2. 数据库SQLite
SQLite是安卓的一种小巧的嵌入式数据库,基本使用和思路和Mysql无异。
(1)SQLiteDatabase
java代码层面借助SQLiteDatabase来对SQLite进行操作。
//创建数据库text.dbSQLiteDatabase db =openOrCreateDatabase(getFileDir()+"/test.db",Context.MODE_PRIVATE,null);
(2)SQLiteOpenHelper
由于SQLiteDatabase存在局限性,一不小心就会重复打开数据库,处理数据库的升级也不方便;因此Android提供了数据库帮助器SQLiteOpenHelper,帮助开发者合理使用SQLite。
SQLiteOpenHelper的具体使用步骤如下:
- 步骤一,新建一个继承自SQLiteOpenHelper的数据库操作类,按提示重写onCreate和onUpgrade两个方法。其中,onCreate方法只在第一次打开数据库时执行,在此可以创建表结构;而onUpgrade方法在数据库版本升高时执行,在此可以根据新旧版本号变更表结构。
- 步骤二,为保证数据库安全使用,需要封装几个必要方法,包括获取单例对象、打开数据库连接、关闭数据库连接,说明如下: - 获取单例对象:确保在App运行过程中数据库只会打开一次,避免重复打开引起错误。- 打开数据库连接:SQLite有锁机制,即读锁和写锁的处理;故而数据库连接也分两种,读连接可调用getReadableDatabase方法获得,写连接可调用getWritableDatabase获得。- 关闭数据库连接:数据库操作完毕,调用数据库实例的close方法关闭连接。
- 步骤三, 提供对表记录增加、删除、修改、查询的操作方法。能被SQLite直接使用的数据结构是ContentValues类,它类似于映射Map,也提供了put和get方法存取键值对。 - 区别之处在于:ContentValues的键只能是字符串,不能是其他类型。ContentValues主要用于增加记录和更新记录,对应数据库的insert和update方法。- 记录的查询操作用到了游标类Cursor,调用query和rawQuery方法返回的都是Cursor对象,若要获取全部的查询结果,则需根据游标的指示一条一条遍历结果集合。Cursor的常用方法可分为3类,说明如下:
(3)代码举例:
publicclassUserDBHelperextendsSQLiteOpenHelper{privatestaticfinalStringDB_NAME="user.db";//数据库名称privatestaticfinalintDB_VERSION=1;//数据库的版本号privatestaticUserDBHelper helper =null;//单例privateSQLiteDatabase sdb =null;//数据库实例publicstaticfinalStringTABLE_NAME="user_info";//表名publicUserDBHelper(Context context){super(context,DB_NAME,null,DB_VERSION);}publicUserDBHelper(Context context,int version){super(context,DB_NAME,null, version);}//通过单例模式获取 UserDBHelper 的唯一实例publicstaticsynchronizedUserDBHelpergetInstance(Context context,int version){if(version >0&& helper ==null){
helper =newUserDBHelper(context, version);}elseif(helper ==null){
helper =newUserDBHelper(context);}return helper;}//打开读连接publicSQLiteDatabaseopenReadLink(){if(sdb ==null||!sdb.isOpen()){
sdb = helper.getReadableDatabase();}return sdb;}//打开写连接publicSQLiteDatabaseopenWriteLink(){if(sdb ==null||!sdb.isOpen()){
sdb = helper.getWritableDatabase();}return sdb;}//关闭数据库连接publicvoidcloseLink(){if(sdb !=null&& sdb.isOpen()){
sdb.close();
sdb =null;}}//创建数据库,执行建表语句@OverridepublicvoidonCreate(SQLiteDatabase db){//先删除已存在表String drop_sql ="drop table if exists "+TABLE_NAME+";";
db.execSQL(drop_sql);//创建表String create_sql ="CREATE TABLE IF NOT EXISTS "+TABLE_NAME+" ("+"_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,"+"name VARCHAR NOT NULL,"+"age INTEGER NOT NULL,"+"height INTEGER NOT NULL,"+"weight FLOAT NOT NULL,"+"married INTEGER NOT NULL,"+"update_time VARCHAR NOT NULL"//演示数据库升级时要先把下面这行注释+",phone VARCHAR"+",password VARCHAR"+");";
db.execSQL(create_sql);}//修改表结构@OverridepublicvoidonUpgrade(SQLiteDatabase db,int oldVersion,int newVersion){if(newVersion >1){//Android的ALTER命令不支持一次添加多列,只能分多次添加String alter_sql ="ALTER TABLE "+TABLE_NAME+" ADD COLUMN phone VARCHAR;";
db.execSQL(alter_sql);
alter_sql ="ALTER TABLE "+TABLE_NAME+" ADD COLUMN "+"password VARCHAR;";
db.execSQL(alter_sql);// 执行完整的SQL语}}//根据指定条件删除记录publicintdelete(String condition){return sdb.delete(TABLE_NAME, condition,null);}//删除全部记录publicintdeleteAll(){return sdb.delete(TABLE_NAME,"1=1",null);}//根据条件查询记录publicList<UserInfo>query(String condition){String sql =String.format("select rowid,_id,name,age,height,weight,married,update_time,"+"phone,password from %s where %s;",TABLE_NAME, condition);//执行查询语句,该语句返回结果集的游标Cursor cursor = sdb.rawQuery(sql,null);ArrayList<UserInfo> userInfos =newArrayList<>();//循环取出游标指向的结果集while(cursor.moveToNext()){UserInfo userInfo =newUserInfo();
userInfo.name = cursor.getString(2);
userInfo.age = cursor.getInt(3);
userInfos.add(userInfo);}
cursor.close();return userInfos;}//往表里添加一条记录publiclonginsert(UserInfo userinfo){ArrayList<UserInfo> userInfos =newArrayList<>();
userInfos.add(userinfo);returninsert(userInfos);}//往表里添加多条记录publiclonginsert(List<UserInfo> userInfos){long result =-1;for(UserInfo userInfo : userInfos){//如果名字相同,则更新记录if(userInfo.name !=null&& userInfo.name.length()>0){String condition =String.format("name = '%s'", userInfo.name);List<UserInfo> dbUserInfoList =query(condition);if(dbUserInfoList !=null&& dbUserInfoList.size()>0){update(userInfo, condition);//返回其id
result = dbUserInfoList.get(0).id;continue;}}//其余情况则说明记录不重复,添加新纪录ContentValues cv =newContentValues();
cv.put("name", userInfo.name);
cv.put("age", userInfo.age);
result = sdb.insert(TABLE_NAME,"", cv);if(result ==-1){return result;}}return result;}//根据指定条件更新表记录publicintupdate(UserInfo userInfo,String condition){ContentValues cv =newContentValues();
cv.put("name", userInfo.name);
cv.put("age", userInfo.age);return sdb.update(TABLE_NAME, cv, condition,null);}}
(4)优化记住密码:
上面通过SharedPreferences存储密码的方式还是存在一定的局限性,该方式只能记住一个用户的登录信息,当下一个用户登录后,上一个用户的信息将会被覆盖。正确的记住密码功能应该是输入手机号自动补充密码,因此,可以考虑使用数据库来进行存储。
主要的改造如下:
- 声明一个数据库的helper对象,在Activity的OnResume方法中获取数据库连接,在OnPause方法中关闭数据库连接。
privateUserDBHelper helper;@OverrideprotectedvoidonResume(){super.onResume();//获取数据库帮助器实例 (此处是单例,所以不怕重复获取)
helper =UserDBHelper.getInstance(this,1);//恢复页面时则获取连接
helper.openWriteLink();}@OverrideprotectedvoidonPause(){super.onPause();//暂停页面时就断开连接
helper.closeLink();}
- 登录成功后,如果用户勾选了记住密码功能,则保存到数据库。也就是在loginSuccess方法中添加如下:
if(isRemember){UserInfo info =newUserInfo();// 创建一个用户信息对象
info.phone = et_phone.getText().toString();
info.password = et_password.getText().toString();
info.update_time =DateUtil.getNowDateTime("yyyy-MM-dd HH:mm:ss");
mHelper.insert(info);// 往用户数据库添加登录成功的用户信息}
- 用户进行登录时,根据输入手机号自动查找密码:
// 根据手机号码查询指定记录publicUserInfoqueryByPhone(String phone){UserInfo info =null;List<UserInfo> infoList =query(String.format("phone='%s'", phone));if(infoList.size()>0){// 存在该号码的登录信息
info = infoList.get(0);}return info;}
3. 存储卡
(1)私有空间和公有空间
为了更规范地管理手机存储空间,Android从7.0开始将存储卡划分为私有存储和公共存储两大部分,也就是分区存储方式,系统给每个App都分配了默认的私有存储空间。App在私有空间上读写文件无须任何授权,但是若想在公共空间读写文件,则要在AndroidManifest.xml里面添加下述的权限配置。
<!-- 存储卡读写 --><uses-permissionandroid:name="android.permission.WRITE_EXTERNAL_STORAGE"/><uses-permissionandroid:name="android.permission.READ_EXTERNAL_STORAG"/>
但是即使App声明了完整的存储卡操作权限,系统仍然默认禁止该App访问公共空间。打开手机的系统设置界面,进入到具体应用的管理页面,会发现该应用的存储访问权限被禁止了。
既然存储卡分为公共空间和私有空间两部分,它们的空间路径获取也就有所不同。若想获取公共空间的存储路径,调用的是Environment.getExternalStoragePublicDirectory方法;若想获取应用私有空间的存储路径,调用的是getExternalFilesDir方法。
//获取系统的公共存储路径String publicPath =Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toString();//获取系统的私有存储路径String privatePath =getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString();boolean isLegacy =true;if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.Q){//Android10的存储空间默认采用分区方式,这里是判断是使用传统方式还是分区方式
isLegacy =Environment.isExternalStorageLegacy();}
(2)在存储卡上读写文件
文本文件的读写借助IO流 FileOutputStream(写文件)和 FileInputStream(读文件)
// 把字符串保存到指定路径的文本文件publicstaticvoidsaveText(String path,String txt){// 根据指定的文件路径构建文件输出流对象try(FileOutputStream fos =newFileOutputStream(path)){
fos.write(txt.getBytes());// 把字符串写入文件输出流}catch(Exception e){
e.printStackTrace();}}// 从指定路径的文本文件中读取内容字符串publicstaticStringopenText(String path){String readStr ="";// 根据指定的文件路径构建文件输入流对象try(FileInputStream fis =newFileInputStream(path)){byte[] b =newbyte[fis.available()];
fis.read(b);// 从文件输入流读取字节数组
readStr =newString(b);// 把字节数组转换为字符串}catch(Exception e){
e.printStackTrace();}return readStr;// 返回文本文件中的文本字符串}
(3)在存储卡上读写 图片文件
文本文件可以转化为对字符串的读写,而图像的读写就需要借助专门的位图工具Bitmap处理。不同图像来源获取Bitmap的方式不同,有三种:
- 从指定资源文件中获取:decodeResource,例如从资源文件img.png获取位图对象:
Bitmap bitmap =BitmapFactory.decodeResource(getResources(),R.drawable.img);
- 从指定路径下获取:decodeFile,但是要注意从Android10开始,该方法只能获取私有空间下的图片,公共空间下获取不了。
Bitmap bitmap =BitmapFactory.decodeFile("C:\\Users\\OYMN\\Pictures\\onepunch.jpg");
- 从指定的输入流中获取,比如使用IO流打开图片文件,然后作为参数传入decodeStream:
publicstaticBitmapopenImage(String path){Bitmap bitmap =null;// 声明一个位图对象// 根据指定的文件路径构建文件输入流对象try(FileInputStream fis =newFileInputStream(path)){
bitmap =BitmapFactory.decodeStream(fis);// 从文件输入流中解码位图数据}catch(Exception e){
e.printStackTrace();}return bitmap;// 返回图片文件中的位图数据}
获取到图片之后就可以通过ImageView的setImageBitmap进行设置了。
有多种读取图片的方式,但是写图片只有一种方式。通过Bitmap的compress方法将位图数据压缩到文件输出流:
publicstaticvoidsaveImage(String path,Bitmap bitmap){//根据文件路径构建文件输出流try(FileOutputStream fos =newFileOutputStream()){//将位图数据压缩到文件输出流
bitmap.compress(Bitmap.CompressFormat.JPEG,80, fos);}catch(Exception e){
e.printStackTrace();}}
以下演示一下完整的文件读写操作:
// 获取当前App的私有下载目录String path =getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString()+"/";// 从指定的资源文件中获取位图对象Bitmap bitmap =BitmapFactory.decodeResource(getResources(),R.drawable.huawei);String file_path = path +DateUtil.getNowDateTime("")+".jpeg";FileUtil.saveImage(file_path, bitmap);// 把位图对象保存为图片文件
tv_path.setText("图片文件的保存路径为:\n"+ file_path);
// 获取当前App的私有下载目录
mPath =getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString()+"/";// 获得指定目录下面的所有图片文件
mFilelist =FileUtil.getFileList(mPath,newString[]{".jpeg"});if(mFilelist.size()>0){// 打开并显示选中的图片文件内容String file_path = mFilelist.get(0).getAbsolutePath();
tv_content.setText("找到最新的图片文件,路径为"+file_path);// 显示存储卡图片文件的第一种方式:直接调用setImageURI方法//iv_content.setImageURI(Uri.parse(file_path)); // 设置图像视图的路径对象// 第二种方式:先调用BitmapFactory.decodeFile获得位图,再调用setImageBitmap方法//Bitmap bitmap = BitmapFactory.decodeFile(file_path);//iv_content.setImageBitmap(bitmap); // 设置图像视图的位图对象// 第三种方式:先调用FileUtil.openImage获得位图,再调用setImageBitmap方法Bitmap bitmap =FileUtil.openImage(file_path);
iv_content.setImageBitmap(bitmap);// 设置图像视图的位图对象
4. 应用组件Application
Application是Android的一大组件,在App运行期间只有一个Application对象贯穿整个应用的生命周期。因此,Application适合保存全局变量,主要是以下三类数据:
- 会频繁读取的信息:如用户名,手机号码等
- 不方便通过intent传递的数据,如位图对象,非字符串的集合对象等。
- 容易因频繁分配内存而导致内存泄漏的对象,如Handler处理器实例等。
通过Application实现对全局内存的读写:
- 先继承Application,并获取唯一实例:
publicclassMyApplicationextendsApplication{privatestaticMyApplication myApplication;//Application唯一实例publicMap<String,String> map =newHashMap<>();//当作全局变量,用来存储数据publicstaticMyApplicationgetInstance(){return myApplication;}@OverridepublicvoidonCreate(){super.onCreate();// 在打开应用时对静态的应用实例赋值
myApplication =this;}}
- 在AndroidManifest.xml 通过name属性添加该Application
- 接下来就可以通过该Application在整个App中存取数据了:
如在MainActivity6存储数据:
@OverrideprotectedvoidonCreate(Bundle savedInstanceState){super.onCreate(savedInstanceState);setContentView(R.layout.activity_main6);//存储数据MyApplication myApplication =MyApplication.getInstance();
myApplication.map.put("myKey","myValue");//跳转到MainActivity5View bt5 =findViewById(R.id.bt5);
bt5.setOnClickListener(newView.OnClickListener(){@OverridepublicvoidonClick(View v){Intent intent =newIntent(MainActivity6.this,MainActivity5.class);startActivity(intent);}});}
在MainActivity5中获取数据:
@OverrideprotectedvoidonCreate(Bundle savedInstanceState){super.onCreate(savedInstanceState);setContentView(R.layout.activity_main5);TextView tv =findViewById(R.id.tv);
tv.setText(MyApplication.getInstance().map.get("myKey"));//成功获取到数据}
5. 实战:购物车
五、内容共享
1. 在应用之间共享数据
接下来将介绍Android的四大组件之一ContentProvider,通过ContentProvider封装内部数据的外部访问接口,实现不同应用能够互相传输数据。
和ContentProvider搭配使用的还有:ContentResolver(内容解析器),ContentObserver(内容观察器)。
上面提到的SQLite可以操作自身的数据库,而ContentProvider则是作为中间接口,通过SQLiteOpenHelper和SQLiteDatabase间接操控数据库,实现为其他应用提供数据的功能。
使用举例如下:
- 创建一个UserInfoProvider,用来提供用户信息给外界应用在弹出的右键菜单中依次选择New→Other→Content Provider此时会自动修改两处地方:(1)一是在AndroidManifest.xml中添加该Provider的配置信息:(2)二是创建的这个Provider会继承ContentProvider,并重写了一些方法。
Server端代码:
publicclassUserInfoProviderextendsContentProvider{//这里是上面实现的dbHelper,用来操作本地数据库privateUserDBHelper userDBHelper;//初始化@OverridepublicbooleanonCreate(){//初始化 dbHelper
userDBHelper =UserDBHelper.getInstance(getContext());returntrue;}//插入//uri格式:content://com.example.secondandroidapp.UserInfoProvider/user@OverridepublicUriinsert(Uri uri,ContentValues values){//使用sqlite插入数据SQLiteDatabase db = userDBHelper.getWritableDatabase();
db.insert(UserDBHelper.TABLE_NAME,null, values);return uri;}//查询@OverridepublicCursorquery(Uri uri,String[] projection,String selection,String[] selectionArgs,String sortOrder){SQLiteDatabase db = userDBHelper.getReadableDatabase();return db.query(UserDBHelper.TABLE_NAME, projection, selection, selectionArgs,null,null,null);}//删除@Overridepublicintdelete(Uri uri,String selection,String[] selectionArgs){int count =0;switch(uriMatcher.match(uri)){//这种是uri不带参数:"content://com.example.secondandroidapp.UserInfoProvider/user"caseUSER:// 获取SQLite数据库的写连接SQLiteDatabase db = userDBHelper.getWritableDatabase();// 执行SQLite的删除操作,并返回删除记录的数目
count = db.delete(UserDBHelper.TABLE_NAME, selection,
selectionArgs);
db.close();break;//这种是uri带参数:"content://com.example.secondandroidapp.UserInfoProvider/user/2"caseUSERS:String id = uri.getLastPathSegment();SQLiteDatabase db2 = userDBHelper.getWritableDatabase();
count = db2.delete(UserDBHelper.TABLE_NAME,"id = ?",newString[]{id});
db2.close();break;}return count;}@OverridepublicStringgetType(Uri uri){// TODO: Implement this to handle requests for the MIME type of the data// at the given URI.thrownewUnsupportedOperationException("Not yet implemented");}@Overridepublicintupdate(Uri uri,ContentValues values,String selection,String[] selectionArgs){// TODO: Implement this to handle requests to update one or more rows.thrownewUnsupportedOperationException("Not yet implemented");}}
- 利用ContentProvider只实现服务端App的数据封装,如果客户端App想访问对方的内部数据,就要通过内容解析器ContentResolver访问。
ContentProvider的Uri结构如下:
content://authority/data_path/id
Client的代码如下:
publicclassMainActivity7extendsAppCompatActivity{privatestaticUriContentUri=Uri.parse("content://com.example.secondandroidapp.UserInfoProvider/user");@OverrideprotectedvoidonCreate(Bundle savedInstanceState){super.onCreate(savedInstanceState);setContentView(R.layout.activity_main7);Button insertButton =findViewById(R.id.insertButton);
insertButton.setOnClickListener(newView.OnClickListener(){@OverridepublicvoidonClick(View v){ContentValues values =newContentValues();
values.put("name","陈鸿荣");
values.put("age","20");//获取到ContentResolver之后调用插入方法进行插入getContentResolver().insert(ContentUri, values);}});Button deleteButton =findViewById(R.id.deleteButton);
deleteButton.setOnClickListener(newView.OnClickListener(){@OverridepublicvoidonClick(View v){// content://com.example.secondandroidapp.UserInfoProvider/user/2Uri uri =ContentUris.withAppendedId(ContentUri,2);int count =getContentResolver().delete(uri,null,null);}});}}
出于安全考虑,Android11需要事先声明需要访问的其他应用:
在AndroidManifest.xml中添加如下:
<queries><!--服务端应用包名 --><packageandroid:name="com.example.secondandroidapp"/><!--或者直接指定authorities--><!-- <provider android:authorities="com.example.secondandroidapp.UserInfoProvider"/> --></queries>
2. 使用内容组件获取通讯信息
(1)运行时动态申请权限
在上面讲公共存储空间与私有存储空间提到,App若想访问存储卡的公共空间,就要在AndroidManifest.xml里面添加下述的权限配置。
<!-- 存储卡读写 --><uses-permissionandroid:name="android.permission.WRITE_EXTERNAL_STORAGE"/><uses-permissionandroid:name="android.permission.READ_EXTERNAL_STORAG"/>
然而即使App声明了完整的存储卡操作权限,从Android 7.0开始,系统仍然默认禁止该App访问公共空间,必须到设置界面手动开启应用的存储卡权限才行。尽管此举是为用户隐私着想,可是人家咋知道要手工开权限呢?就算用户知道,去设置界面找到权限开关也颇费周折。为此Android支持在Java代码中处理权限,处理过程分为3个步骤:
- 检查App是否开启了指定权限:调用ContextCompat的checkSelfPermission方法
- 请求系统弹窗,以便用户选择是否开启权限:调用ActivityCompat的requestPermissions方法,即可命令系统自动弹出权限申请窗口。
- 判断用户的权限选择结果,是开启还是拒绝:重写活动页面的权限请求回调方法onRequestPermissionsResult,在该方法内部处理用户的权限选择结果
动态申请权限有两种方式:饿汉式 和 懒汉式。
接下来通过获取通讯权限和短信权限来进行举例说明:
首先是懒汉式:当需要某种权限的时候再去申请
publicclassPermissionUtil{//检查权限,返回true表示完全启用权限,返回false则表示为完全启用所有权限publicstaticbooleancheckPermission(Activity activity,String[] permissions,int requestCode){//Android6.0之后采取动态权限管理if(Build.VERSION.SDK_INT>Build.VERSION_CODES.M){int check =PackageManager.PERMISSION_GRANTED;// 0for(String permission : permissions){
check =ContextCompat.checkSelfPermission(activity, permission);if(check !=PackageManager.PERMISSION_GRANTED){break;}}//如果未开启该权限,则请求系统弹窗,好让用户选择是否开启权限if(check !=PackageManager.PERMISSION_GRANTED){//请求权限ActivityCompat.requestPermissions(activity, permissions, requestCode);returnfalse;}returntrue;}returnfalse;}//检查权限数组,返回true表示都已经授权publicstaticbooleancheckGrant(int[] grantResults){if(grantResults !=null){for(int grant : grantResults){if(grant !=PackageManager.PERMISSION_GRANTED){returnfalse;}}returntrue;}returnfalse;}}
通过两个按钮模拟分别获取权限:
publicclassPermissionLazyActivityextendsAppCompatActivity{//通讯录的读写权限privatestaticfinalString[]PERMISSION_CONTACT={Manifest.permission.READ_CONTACTS,Manifest.permission.WRITE_CONTACTS};//短信的读写权限privatestaticfinalString[]PERMISSION_SMS={Manifest.permission.SEND_SMS,Manifest.permission.RECEIVE_SMS};privatestaticfinalintREQUEST_CODE_CONTACTS=1;privatestaticfinalintREQUEST_CODE_SMS=2;@OverrideprotectedvoidonCreate(Bundle savedInstanceState){super.onCreate(savedInstanceState);setContentView(R.layout.activity_permission_lazy);//获取通讯录权限findViewById(R.id.btn_contact).setOnClickListener(newView.OnClickListener(){@OverridepublicvoidonClick(View v){PermissionUtil.checkPermission(PermissionLazyActivity.this,PERMISSION_CONTACT,REQUEST_CODE_CONTACTS);}});//获取短信权限findViewById(R.id.btn_sms).setOnClickListener(newView.OnClickListener(){@OverridepublicvoidonClick(View v){PermissionUtil.checkPermission(PermissionLazyActivity.this,PERMISSION_SMS,REQUEST_CODE_SMS);}});}// 用户选择权限结果后会调用该回调方法@OverridepublicvoidonRequestPermissionsResult(int requestCode,@NonNullString[] permissions,@NonNullint[] grantResults){super.onRequestPermissionsResult(requestCode, permissions, grantResults);switch(requestCode){caseREQUEST_CODE_CONTACTS:if(PermissionUtil.checkGrant(grantResults)){Log.d("hhh","通讯录获取成功");}else{Log.d("hhh","通讯录获取失败");//跳转到设置界面jumpToSettings();}break;caseREQUEST_CODE_SMS:if(PermissionUtil.checkGrant(grantResults)){Log.d("hhh","短信权限获取成功");}else{Log.d("hhh","短信权限获取失败");//跳转到设置界面jumpToSettings();}break;}}//跳转到设置界面privatevoidjumpToSettings(){Intent intent =newIntent();
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData(Uri.fromParts("package",getPackageName(),null));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);startActivity(intent);}}
另外还需要在AndroidManifest.xml中配置:(在低版本中只需要配置这些信息即可,高版本就需要上面的动态申请权限)
<!-- 开启通讯录权限--><uses-permissionandroid:name="android.permission.READ_CONTACTS"/><uses-permissionandroid:name="android.permission.WRITE_CONTACTS"/><!-- 开启短信收发权限--><uses-permissionandroid:name="android.permission.SEND_SMS"/><uses-permissionandroid:name="android.permission.RECEIVE_SMS"/>
效果如下:
懒汉式:在页面打开之后就一次性需要用户获取所有权限。
publicclassPermissionHungryActivityextendsAppCompatActivity{//所需全部读写权限privatestaticfinalString[]PERMISSIONS={Manifest.permission.READ_CONTACTS,Manifest.permission.WRITE_CONTACTS,Manifest.permission.SEND_SMS,Manifest.permission.RECEIVE_SMS};//privatestaticfinalintREQUEST_CODE_ALL=0;@OverrideprotectedvoidonCreate(Bundle savedInstanceState){super.onCreate(savedInstanceState);setContentView(R.layout.activity_permission_lazy);//检查是否拥有所有所需权限PermissionUtil.checkPermission(this,PERMISSIONS,REQUEST_CODE_ALL);}// 用户选择权限结果后会调用该回调方法@OverridepublicvoidonRequestPermissionsResult(int requestCode,@NonNullString[] permissions,@NonNullint[] grantResults){super.onRequestPermissionsResult(requestCode, permissions, grantResults);switch(requestCode){caseREQUEST_CODE_ALL:if(PermissionUtil.checkGrant(grantResults)){Log.d("hhh","所有权限获取成功");}else{//部分权限获取失败for(int i =0; i < grantResults.length; i++){if(grantResults[i]!=PackageManager.PERMISSION_GRANTED){//判断是什么权限获取失败switch(permissions[i]){caseManifest.permission.WRITE_CONTACTS:caseManifest.permission.READ_CONTACTS:Log.d("hhh","通讯录获取失败");jumpToSettings();break;caseManifest.permission.SEND_SMS:caseManifest.permission.RECEIVE_SMS:Log.d("hhh","短信权限获取失败");jumpToSettings();break;}}}}break;}}//跳转到设置界面privatevoidjumpToSettings(){Intent intent =newIntent();
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData(Uri.fromParts("package",getPackageName(),null));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);startActivity(intent);}}
(2)使用ContentResolver读写联系人
手机中通讯录的主要表结构有:
raw_contacts
表:
data表:记录了用户的通讯录所有数据,包括手机号,显示名称等,但是里面的mimetype_id表示不同的数据类型,这与表mimetypes表中的id相对应,raw_contact_id 与上面的 raw_contacts表中的 id 相对应。
mimetypes表:
所以,插入步骤如下:
- 首先往raw_contacts表中插入一条数据得到id
- 接着由于一个联系人有姓名,电话号码,邮箱,因此需要分三次插入data表中,将raw_contact_id和上面得到的id进行关联
下面是往通讯录插入和查询联系人的代码:
publicclassContactActivityextendsAppCompatActivityimplementsView.OnClickListener{privateEditText et_contact_name;privateEditText et_contact_phone;privateEditText et_contact_email;@OverrideprotectedvoidonCreate(Bundle savedInstanceState){super.onCreate(savedInstanceState);setContentView(R.layout.activity_contact);
et_contact_name =findViewById(R.id.et_contact_name);
et_contact_phone =findViewById(R.id.et_contact_phone);
et_contact_email =findViewById(R.id.et_contact_email);findViewById(R.id.btn_add_contact).setOnClickListener(this);findViewById(R.id.btn_read_contact).setOnClickListener(this);}@OverridepublicvoidonClick(View v){switch(v.getId()){caseR.id.btn_add_contact:// 创建一个联系人对象Contact contact =newContact();
contact.name = et_contact_name.getText().toString().trim();
contact.phone = et_contact_phone.getText().toString().trim();
contact.email = et_contact_email.getText().toString().trim();// 方式一,使用ContentResolver多次写入,每次一个字段// addContacts(getContentResolver(), contact);// 方式二,批处理方式// 每一次操作都是一个 ContentProviderOperation,构建一个操作集合,然后一次性执行// 好处是,要么全部成功,要么全部失败,保证了事务的一致性addFullContacts(getContentResolver(), contact);Toast.makeText(this,"添加联系人成功!",Toast.LENGTH_SHORT).show();break;caseR.id.btn_read_contact:readPhoneContacts(getContentResolver());break;}}//往通讯录添加一个联系人信息(姓名,号码,邮箱)privatevoidaddContacts(ContentResolver contentResolver,Contact contact){//得到rawContentIdContentValues values =newContentValues();//插入记录得到idUri uri = contentResolver.insert(ContactsContract.RawContacts.CONTENT_URI, values);long rawContentId =ContentUris.parseId(uri);//插入名字ContentValues name =newContentValues();//关联上面得到的联系人id
name.put(ContactsContract.Contacts.Data.RAW_CONTACT_ID, rawContentId);//关联联系人姓名的类型
name.put(ContactsContract.Contacts.Data.MIMETYPE,ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE);//关联联系人姓名
name.put(ContactsContract.Data.DATA2, contact.name);
contentResolver.insert(ContactsContract.Data.CONTENT_URI, name);//插入电话号码ContentValues phone =newContentValues();//关联上面得到的联系人id
phone.put(ContactsContract.Contacts.Data.RAW_CONTACT_ID, rawContentId);//关联联系人电话号码的类型
phone.put(ContactsContract.Contacts.Data.MIMETYPE,ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE);//关联联系人电话号码
phone.put(ContactsContract.Data.DATA1, contact.phone);//指定该号码是家庭号码还是工作号码 (家庭)
phone.put(ContactsContract.Data.DATA2,ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE);
contentResolver.insert(ContactsContract.Data.CONTENT_URI, phone);//插入邮箱ContentValues email =newContentValues();//关联上面得到的联系人id
email.put(ContactsContract.Contacts.Data.RAW_CONTACT_ID, rawContentId);//关联联系人邮箱的类型
email.put(ContactsContract.Contacts.Data.MIMETYPE,ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE);//关联联系人邮箱
email.put(ContactsContract.Data.DATA1, contact.email);//指定该号码是家庭邮箱还是工作邮箱
email.put(ContactsContract.Data.DATA2,ContactsContract.CommonDataKinds.Phone.TYPE_WORK);
contentResolver.insert(ContactsContract.Data.CONTENT_URI, email);}//事务操作,四个插入操作一次性提交privatevoidaddFullContacts(ContentResolver contentResolver,Contact contact){//创建一个插入联系人主记录的内容操作器ContentProviderOperation op_main =ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)//没有实际意义,不加这个会报错(不加这个导致没有创建ContentValue,导致报错).withValue(ContactsContract.RawContacts.ACCOUNT_NAME,null).build();//创建一个插入联系人姓名记录的内容操作器ContentProviderOperation op_name =ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)//将第0个操作的id,即raw_contacts中的id作为data表中的raw_contact_id.withValueBackReference(ContactsContract.Contacts.Data.RAW_CONTACT_ID,0).withValue(ContactsContract.Contacts.Data.MIMETYPE,ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE).withValue(ContactsContract.Data.DATA2, contact.name).build();//创建一个插入联系人电话号码记录的内容操作器ContentProviderOperation op_phone =ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)//将第0个操作的id,即raw_contacts中的id作为data表中的raw_contact_id.withValueBackReference(ContactsContract.Contacts.Data.RAW_CONTACT_ID,0).withValue(ContactsContract.Contacts.Data.MIMETYPE,ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE).withValue(ContactsContract.Data.DATA1, contact.phone).withValue(ContactsContract.Data.DATA2,ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE).build();//创建一个插入联系人邮箱记录的内容操作器ContentProviderOperation op_email =ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)//将第0个操作的id,即raw_contacts中的id作为data表中的raw_contact_id.withValueBackReference(ContactsContract.Contacts.Data.RAW_CONTACT_ID,0).withValue(ContactsContract.Contacts.Data.MIMETYPE,ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE).withValue(ContactsContract.Data.DATA1, contact.email).withValue(ContactsContract.Data.DATA2,ContactsContract.CommonDataKinds.Phone.TYPE_WORK).build();//全部放在集合中一次性提交ArrayList<ContentProviderOperation> operations =newArrayList<>();
operations.add(op_main);
operations.add(op_name);
operations.add(op_phone);
operations.add(op_email);try{//批量提交四个操作
contentResolver.applyBatch(ContactsContract.AUTHORITY, operations);}catch(OperationApplicationException e){
e.printStackTrace();}catch(RemoteException e){
e.printStackTrace();}}//读取联系人@SuppressLint("Range")privatevoidreadPhoneContacts(ContentResolver contentResolver){//先查询raw_contacts表,再根据raw_contacts_id表 查询data表Cursor cursor = contentResolver.query(ContactsContract.RawContacts.CONTENT_URI,newString[]{ContactsContract.RawContacts._ID},null,null,null);while(cursor.moveToNext()){int rawContactId = cursor.getInt(0);Uri uri =Uri.parse("content://com.android.contacts/contacts/"+ rawContactId +"/data");Cursor dataCursor = contentResolver.query(uri,newString[]{ContactsContract.Contacts.Data.MIMETYPE,ContactsContract.Contacts.Data.DATA1,ContactsContract.Contacts.Data.DATA2},null,null,null);Contact contact =newContact();while(dataCursor.moveToNext()){String data1 = dataCursor.getString(dataCursor.getColumnIndex(ContactsContract.Contacts.Data.DATA1));String mimeType = dataCursor.getString(dataCursor.getColumnIndex(ContactsContract.Contacts.Data.MIMETYPE));switch(mimeType){//是姓名caseContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE:
contact.name = data1;break;//邮箱caseContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE:
contact.email = data1;break;//手机caseContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE:
contact.phone = data1;break;}}
dataCursor.close();// RawContacts 表中出现的 _id,不一定在 Data 表中都会有对应记录if(contact.name !=null){Log.d("hhh", contact.toString());}}
cursor.close();}}
页面如下:
(3)使用ContentObserver监听短信
ContentResolver获取数据采用的是主动查询方式,有查询就有数据,没查询就没数据。ContentResolver能够实时获取新增的数据,最常见的业务场景是短信验证码。为了替用户省事,App通常会监控手机刚收到的短信验证码,并自动填写验证码输入框。这时就用到了内容观察器ContentObserver,事先给目标内容注册一个观察器,目标内容的数据一旦发生变化,就马上触发观察器的监听事件,从而执行开发者预先定义的代码。
示例代码如下:(记得在Manifest.xml中开启权限和动态开启权限)
publicclassMonitorSmsActivityextendsAppCompatActivity{privateSmsGetObserver mObserver;@OverrideprotectedvoidonCreate(Bundle savedInstanceState){super.onCreate(savedInstanceState);setContentView(R.layout.activity_monitor_sms);// 给指定Uri注册内容观察器,一旦发生数据变化,就触发观察器的onChange方法Uri uri =Uri.parse("content://sms");// notifyForDescendents:// false :表示精确匹配,即只匹配该Uri,true :表示可以同时匹配其派生的Uri
mObserver =newSmsGetObserver(this);getContentResolver().registerContentObserver(uri,true, mObserver);}@OverrideprotectedvoidonDestroy(){super.onDestroy();//取消注册getContentResolver().unregisterContentObserver(mObserver);}privatestaticclassSmsGetObserverextendsContentObserver{privatefinalContext mContext;publicSmsGetObserver(Context context){super(newHandler(Looper.getMainLooper()));this.mContext = context;}//回调@SuppressLint("Range")@OverridepublicvoidonChange(boolean selfChange,@NullableUri uri){super.onChange(selfChange, uri);// onChange会多次调用,收到一条短信会调用两次onChange// mUri===content://sms/raw/20// mUri===content://sms/inbox/20// 安卓7.0以上系统,点击标记为已读,也会调用一次// mUri===content://sms// 收到一条短信都是uri后面都会有确定的一个数字,对应数据库的_id,比如上面的20if(uri ==null){return;}if(uri.toString().contains("content://sms/raw")||
uri.toString().equals("content://sms")){return;}// 通过内容解析器获取符合条件的结果集游标Cursor cursor = mContext.getContentResolver().query(uri,newString[]{"address","body","date"},null,null,"date DESC");if(cursor.moveToNext()){// 短信的发送号码String sender = cursor.getString(cursor.getColumnIndex("address"));// 短信内容String content = cursor.getString(cursor.getColumnIndex("body"));Log.d("ning",String.format("sender:%s,content:%s", sender, content));}
cursor.close();}}}
3. 在应用之间共享文件
(1)使用相册图片发送彩信
(2)借助FileProvider发送彩信
(3)借助FileProvider安装应用
版权归原作者 OYMN 所有, 如有侵权,请联系我们删除。