首页IT科技android实战教程(Android基础教程——从入门到精通(上))

android实战教程(Android基础教程——从入门到精通(上))

时间2025-05-05 04:19:56分类IT科技浏览3438
导读:本文是对B站教程 动脑学院 Android教程 学习过程中所做的笔记。 文章分为上下两部分,此文是上部分,下部分链接为:Android基础教程——从入门到精通(下) 源视频教程并没有录制全,本文还补充了 Service 和 网络通信 的内容 文章介绍详细,示例代码丰富,相信跟着本...

本文是对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(new View.OnClickListener(){ @Override public void onClick(View v) { Intent intent = new Intent(); 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标准模式           ,默认也是标准模式     。

<activity android: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 = new Intent(this, NextActivity.class);

调用setClass指定:

Intent intent = new Intent(); intent.setClass(this, NextActivity.class);

调用setComponent指定:

Intent intent = new Intent(); ComponentName component = new ComponentName(this, NextActivity.class); intent.setComponent(component); 2. 隐式Intent:

没有明确指定所要跳转的页面                  ,而是通过一些动作字符串来让系统自动匹配                  。

通常是App不想向外暴露Activity的名称                 ,只给出一些定义好的字符串           。这些字符串可以自己定义,也有系统定义的。

常见的系统动作如下:

下面以调用系统拨号页面举例:

String phone = "12345"; Intent intent = new Intent(); //这里表示设置意图动作为准备拨号 intent.setAction(Intent.ACTION_DIAL); intent.setData(Uri.parse("tel:" + phone)); startActivity(intent);

如果想要跳转到自己定义的activity:

步骤一:在AndroidManifest.xml找到该activity            ,添加action和category标签                 ,同时设置exported为true     ,表示允许被其他activity调用                  。

步骤二:调用过程和上面一样:

Intent intent = new Intent(); intent.setAction("android.intent.action.activity2"); intent.addCategory(Intent.CATEGORY_DEFAULT); startActivity(intent);

(2)向下一个Activity发送消息:

Intent重载了很多putExtra方法用于传递各种类型的信息            ,包括整数类型                  ,字符串等                 。但是显然通过调用putExtra方法会很不好管理     ,因为数据都是零碎传递。所以Android引入了Bundle      ,其内部是一个Map                  ,使用起来也和Map一样            。

示例:

Intent intent = new Intent(this, NextActivity.class); //通过bundle包装数据 Bundle bundle = new Bundle(); bundle.putString("stringKey", "stringValue"); intent.putExtras(bundle); startActivity(intent);

然后下一个Activity就可以通过intent获取到所想要的数据了:

Bundle bundle = getIntent().getExtras(); String stringValue = bundle.getString("stringKey");

(3)向上一个Activity返回消息:

上一个页面跳转到下一个页面           ,同时携带数据:

private ActivityResultLauncher<Intent> register; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main2); findViewById(R.id.bt).setOnClickListener(this); //回调函数      ,返回到这个页面时所执行的程序 register = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), new ActivityResultCallback<ActivityResult>() { //回调函数 @Override public void onActivityResult(ActivityResult result) { if (result != null) { Intent intent = result.getData(); if (intent != null && result.getResultCode() == Activity.RESULT_OK) { //获取到返回的数据 Bundle bundle = intent.getExtras(); //... } } } }); } @Override public void onClick(View v) { Intent intent = new Intent(this, MainActivity3.class); //跳转下一页面 register.launch(intent); }

下一个页面接受到数据                  ,处理之后返回结果给上一个页面:

Bundle bundle = getIntent().getExtras(); //...页面进行处理 //返回数据给上一个页面 Bundle bundle = new Bundle(); 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.db SQLiteDatabase 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)代码举例:

public class UserDBHelper extends SQLiteOpenHelper { private static final String DB_NAME = "user.db"; //数据库名称 private static final int DB_VERSION = 1; //数据库的版本号 private static UserDBHelper helper = null; //单例 private SQLiteDatabase sdb = null; //数据库实例 public static final String TABLE_NAME = "user_info"; //表名 public UserDBHelper(Context context) { super(context, DB_NAME, null, DB_VERSION); } public UserDBHelper(Context context, int version) { super(context, DB_NAME, null, version); } //通过单例模式获取 UserDBHelper 的唯一实例 public static synchronized UserDBHelper getInstance(Context context, int version) { if (version > 0 && helper == null) { helper = new UserDBHelper(context, version); } else if (helper == null) { helper = new UserDBHelper(context); } return helper; } //打开读连接 public SQLiteDatabase openReadLink() { if (sdb == null || !sdb.isOpen()) { sdb = helper.getReadableDatabase(); } return sdb; } //打开写连接 public SQLiteDatabase openWriteLink() { if (sdb == null || !sdb.isOpen()) { sdb = helper.getWritableDatabase(); } return sdb; } //关闭数据库连接 public void closeLink() { if (sdb != null && sdb.isOpen()) { sdb.close(); sdb = null; } } //创建数据库            ,执行建表语句 @Override public void onCreate(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); } //修改表结构 @Override public void onUpgrade(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语 } } //根据指定条件删除记录 public int delete(String condition) { return sdb.delete(TABLE_NAME, condition, null); } //删除全部记录 public int deleteAll() { return sdb.delete(TABLE_NAME, "1=1", null); } //根据条件查询记录 public List<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 = new ArrayList<>(); //循环取出游标指向的结果集 while (cursor.moveToNext()) { UserInfo userInfo = new UserInfo(); userInfo.name = cursor.getString(2); userInfo.age = cursor.getInt(3); userInfos.add(userInfo); } cursor.close(); return userInfos; } //往表里添加一条记录 public long insert(UserInfo userinfo) { ArrayList<UserInfo> userInfos = new ArrayList<>(); userInfos.add(userinfo); return insert(userInfos); } //往表里添加多条记录 public long insert(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 = new ContentValues(); cv.put("name", userInfo.name); cv.put("age", userInfo.age); result = sdb.insert(TABLE_NAME, "", cv); if(result == -1){ return result; } } return result; } //根据指定条件更新表记录 public int update(UserInfo userInfo, String condition) { ContentValues cv = new ContentValues(); cv.put("name", userInfo.name); cv.put("age", userInfo.age); return sdb.update(TABLE_NAME, cv, condition, null); } }

(4)优化记住密码:

上面通过SharedPreferences存储密码的方式还是存在一定的局限性           ,该方式只能记住一个用户的登录信息      ,当下一个用户登录后                  ,上一个用户的信息将会被覆盖                  。正确的记住密码功能应该是输入手机号自动补充密码           ,因此,可以考虑使用数据库来进行存储     。

主要的改造如下:

声明一个数据库的helper对象                  ,在Activity的OnResume方法中获取数据库连接                 ,在OnPause方法中关闭数据库连接      。 private UserDBHelper helper; @Override protected void onResume() { super.onResume(); //获取数据库帮助器实例 (此处是单例,所以不怕重复获取) helper = UserDBHelper.getInstance(this, 1); //恢复页面时则获取连接 helper.openWriteLink(); } @Override protected void onPause() { super.onPause(); //暂停页面时就断开连接 helper.closeLink(); } 登录成功后            ,如果用户勾选了记住密码功能                 ,则保存到数据库                  。也就是在loginSuccess方法中添加如下: if (isRemember) { UserInfo info = new UserInfo(); // 创建一个用户信息对象 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); // 往用户数据库添加登录成功的用户信息 } 用户进行登录时     ,根据输入手机号自动查找密码: // 根据手机号码查询指定记录 public UserInfo queryByPhone(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-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android: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(读文件)

// 把字符串保存到指定路径的文本文件 public static void saveText(String path, String txt) { // 根据指定的文件路径构建文件输出流对象 try (FileOutputStream fos = new FileOutputStream(path)) { fos.write(txt.getBytes()); // 把字符串写入文件输出流 } catch (Exception e) { e.printStackTrace(); } } // 从指定路径的文本文件中读取内容字符串 public static String openText(String path) { String readStr = ""; // 根据指定的文件路径构建文件输入流对象 try (FileInputStream fis = new FileInputStream(path)) { byte[] b = new byte[fis.available()]; fis.read(b); // 从文件输入流读取字节数组 readStr = new String(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: public static Bitmap openImage(String path) { Bitmap bitmap = null; // 声明一个位图对象 // 根据指定的文件路径构建文件输入流对象 try (FileInputStream fis = new FileInputStream(path)) { bitmap = BitmapFactory.decodeStream(fis); // 从文件输入流中解码位图数据 } catch (Exception e) { e.printStackTrace(); } return bitmap; // 返回图片文件中的位图数据 }

获取到图片之后就可以通过ImageView的setImageBitmap进行设置了            。

有多种读取图片的方式                  ,但是写图片只有一种方式                 。通过Bitmap的compress方法将位图数据压缩到文件输出流:

public static void saveImage(String path, Bitmap bitmap){ //根据文件路径构建文件输出流 try(FileOutputStream fos = new FileOutputStream()){ //将位图数据压缩到文件输出流 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, new String[]{".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     ,并获取唯一实例: public class MyApplication extends Application { private static MyApplication myApplication; //Application唯一实例 public Map<String, String> map = new HashMap<>(); //当作全局变量            ,用来存储数据 public static MyApplication getInstance(){ return myApplication; } @Override public void onCreate() { super.onCreate(); // 在打开应用时对静态的应用实例赋值 myApplication = this; } } 在AndroidManifest.xml 通过name属性添加该Application 接下来就可以通过该Application在整个App中存取数据了:

如在MainActivity6存储数据:

@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main6); //存储数据 MyApplication myApplication = MyApplication.getInstance(); myApplication.map.put("myKey", "myValue"); //跳转到MainActivity5 View bt5 = findViewById(R.id.bt5); bt5.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent(MainActivity6.this, MainActivity5.class); startActivity(intent); } }); }

在MainActivity5中获取数据:

@Override protected void onCreate(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端代码:

public class UserInfoProvider extends ContentProvider { //这里是上面实现的dbHelper,用来操作本地数据库 private UserDBHelper userDBHelper; //初始化 @Override public boolean onCreate() { //初始化 dbHelper userDBHelper = UserDBHelper.getInstance(getContext()); return true; } //插入 //uri格式:content://com.example.secondandroidapp.UserInfoProvider/user @Override public Uri insert(Uri uri, ContentValues values) { //使用sqlite插入数据 SQLiteDatabase db = userDBHelper.getWritableDatabase(); db.insert(UserDBHelper.TABLE_NAME, null, values); return uri; } //查询 @Override public Cursor query(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); } //删除 @Override public int delete(Uri uri, String selection, String[] selectionArgs) { int count = 0; switch (uriMatcher.match(uri)) { //这种是uri不带参数:"content://com.example.secondandroidapp.UserInfoProvider/user" case USER: // 获取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" case USERS: String id = uri.getLastPathSegment(); SQLiteDatabase db2 = userDBHelper.getWritableDatabase(); count = db2.delete(UserDBHelper.TABLE_NAME, "id = ?", new String[]{id}); db2.close(); break; } return count; } @Override public String getType(Uri uri) { // TODO: Implement this to handle requests for the MIME type of the data // at the given URI. throw new UnsupportedOperationException("Not yet implemented"); } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { // TODO: Implement this to handle requests to update one or more rows. throw new UnsupportedOperationException("Not yet implemented"); } }

利用ContentProvider只实现服务端App的数据封装                 ,如果客户端App想访问对方的内部数据,就要通过内容解析器ContentResolver访问      。

ContentProvider的Uri结构如下:content://authority/data_path/id

Client的代码如下:

public class MainActivity7 extends AppCompatActivity { private static Uri ContentUri = Uri.parse("content://com.example.secondandroidapp.UserInfoProvider/user"); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main7); Button insertButton = findViewById(R.id.insertButton); insertButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { ContentValues values = new ContentValues(); values.put("name", "陈鸿荣"); values.put("age", "20"); //获取到ContentResolver之后调用插入方法进行插入 getContentResolver().insert(ContentUri, values); } }); Button deleteButton = findViewById(R.id.deleteButton); deleteButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // content://com.example.secondandroidapp.UserInfoProvider/user/2 Uri uri = ContentUris.withAppendedId(ContentUri, 2); int count = getContentResolver().delete(uri, null, null); } }); } }

出于安全考虑            ,Android11需要事先声明需要访问的其他应用:

在AndroidManifest.xml中添加如下:

<queries> <!--服务端应用包名 --> <package android:name="com.example.secondandroidapp"/> <!--或者直接指定authorities--> <!-- <provider android:authorities="com.example.secondandroidapp.UserInfoProvider"/> --> </queries>

2. 使用内容组件获取通讯信息

(1)运行时动态申请权限

在上面讲公共存储空间与私有存储空间提到                 ,App若想访问存储卡的公共空间     ,就要在AndroidManifest.xml里面添加下述的权限配置                  。

<!-- 存储卡读写 --> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAG" />

然而即使App声明了完整的存储卡操作权限            ,从Android 7.0开始                  ,系统仍然默认禁止该App访问公共空间     ,必须到设置界面手动开启应用的存储卡权限才行           。尽管此举是为用户隐私着想      ,可是人家咋知道要手工开权限呢?就算用户知道                  ,去设置界面找到权限开关也颇费周折。为此Android支持在Java代码中处理权限           ,处理过程分为3个步骤:

检查App是否开启了指定权限:

调用ContextCompat的checkSelfPermission方法

请求系统弹窗      ,以便用户选择是否开启权限:

调用ActivityCompat的requestPermissions方法                  ,即可命令系统自动弹出权限申请窗口                  。

判断用户的权限选择结果           ,是开启还是拒绝:

重写活动页面的权限请求回调方法onRequestPermissionsResult,在该方法内部处理用户的权限选择结果

动态申请权限有两种方式:饿汉式 和 懒汉式                 。

接下来通过获取通讯权限和短信权限来进行举例说明:

首先是懒汉式:当需要某种权限的时候再去申请

public class PermissionUtil { //检查权限                  ,返回true表示完全启用权限                 ,返回false则表示为完全启用所有权限 public static boolean checkPermission(Activity activity, String[] permissions, int requestCode){ //Android6.0之后采取动态权限管理 if(Build.VERSION.SDK_INT > Build.VERSION_CODES.M){ int check = PackageManager.PERMISSION_GRANTED; // 0 for (String permission : permissions) { check = ContextCompat.checkSelfPermission(activity, permission); if(check != PackageManager.PERMISSION_GRANTED){ break; } } //如果未开启该权限,则请求系统弹窗            ,好让用户选择是否开启权限 if(check != PackageManager.PERMISSION_GRANTED){ //请求权限 ActivityCompat.requestPermissions(activity, permissions, requestCode); return false; } return true; } return false; } //检查权限数组                 ,返回true表示都已经授权 public static boolean checkGrant(int[] grantResults) { if(grantResults != null){ for (int grant : grantResults) { if(grant != PackageManager.PERMISSION_GRANTED){ return false; } } return true; } return false; } }

通过两个按钮模拟分别获取权限:

public class PermissionLazyActivity extends AppCompatActivity { //通讯录的读写权限 private static final String[] PERMISSION_CONTACT = { Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS }; //短信的读写权限 private static final String[] PERMISSION_SMS = { Manifest.permission.SEND_SMS, Manifest.permission.RECEIVE_SMS }; private static final int REQUEST_CODE_CONTACTS = 1; private static final int REQUEST_CODE_SMS = 2; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_permission_lazy); //获取通讯录权限 findViewById(R.id.btn_contact).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { PermissionUtil.checkPermission(PermissionLazyActivity.this, PERMISSION_CONTACT, REQUEST_CODE_CONTACTS); } }); //获取短信权限 findViewById(R.id.btn_sms).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { PermissionUtil.checkPermission(PermissionLazyActivity.this, PERMISSION_SMS, REQUEST_CODE_SMS); } }); } // 用户选择权限结果后会调用该回调方法 @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); switch (requestCode){ case REQUEST_CODE_CONTACTS: if(PermissionUtil.checkGrant(grantResults)){ Log.d("hhh", "通讯录获取成功"); }else{ Log.d("hhh", "通讯录获取失败"); //跳转到设置界面 jumpToSettings(); } break; case REQUEST_CODE_SMS: if(PermissionUtil.checkGrant(grantResults)){ Log.d("hhh", "短信权限获取成功"); }else{ Log.d("hhh", "短信权限获取失败"); //跳转到设置界面 jumpToSettings(); } break; } } //跳转到设置界面 private void jumpToSettings(){ Intent intent = new Intent(); 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-permission android:name="android.permission.READ_CONTACTS"/> <uses-permission android:name="android.permission.WRITE_CONTACTS"/> <!-- 开启短信收发权限--> <uses-permission android:name="android.permission.SEND_SMS"/> <uses-permission android:name="android.permission.RECEIVE_SMS"/>

效果如下:

懒汉式:在页面打开之后就一次性需要用户获取所有权限。

public class PermissionHungryActivity extends AppCompatActivity { //所需全部读写权限 private static final String[] PERMISSIONS = { Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS, Manifest.permission.SEND_SMS, Manifest.permission.RECEIVE_SMS }; // private static final int REQUEST_CODE_ALL = 0; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_permission_lazy); //检查是否拥有所有所需权限 PermissionUtil.checkPermission(this, PERMISSIONS, REQUEST_CODE_ALL); } // 用户选择权限结果后会调用该回调方法 @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); switch (requestCode){ case REQUEST_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]){ case Manifest.permission.WRITE_CONTACTS: case Manifest.permission.READ_CONTACTS: Log.d("hhh", "通讯录获取失败"); jumpToSettings(); break; case Manifest.permission.SEND_SMS: case Manifest.permission.RECEIVE_SMS: Log.d("hhh", "短信权限获取失败"); jumpToSettings(); break; } } } } break; } } //跳转到设置界面 private void jumpToSettings(){ Intent intent = new Intent(); 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进行关联

下面是往通讯录插入和查询联系人的代码:

public class ContactActivity extends AppCompatActivity implements View.OnClickListener { private EditText et_contact_name; private EditText et_contact_phone; private EditText et_contact_email; @Override protected void onCreate(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); } @Override public void onClick(View v) { switch (v.getId()) { case R.id.btn_add_contact: // 创建一个联系人对象 Contact contact = new Contact(); 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; case R.id.btn_read_contact: readPhoneContacts(getContentResolver()); break; } } //往通讯录添加一个联系人信息(姓名     ,号码      ,邮箱) private void addContacts(ContentResolver contentResolver, Contact contact) { //得到rawContentId ContentValues values = new ContentValues(); //插入记录得到id Uri uri = contentResolver.insert(ContactsContract.RawContacts.CONTENT_URI, values); long rawContentId = ContentUris.parseId(uri); //插入名字 ContentValues name = new ContentValues(); //关联上面得到的联系人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 = new ContentValues(); //关联上面得到的联系人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 = new ContentValues(); //关联上面得到的联系人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); } //事务操作                  ,四个插入操作一次性提交 private void addFullContacts(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<ContentProviderOp

创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!

展开全文READ MORE
cross application sets(Crossdomain Ajax with CrossOrigin Resource Sharing)