- 本文是對(duì)B站教程 動(dòng)腦學(xué)院 Android教程 學(xué)習(xí)過程中所做的筆記。
- 文章分為上下兩部分,此文是上部分,下部分鏈接為:Android基礎(chǔ)教程——從入門到精通(下)
- 源視頻教程并沒有錄制全,本文還補(bǔ)充了 Service 和 網(wǎng)絡(luò)通信 的內(nèi)容
- 文章介紹詳細(xì),示例代碼豐富,相信跟著本教程可以打下很好的Android基礎(chǔ)。
一、開發(fā)環(huán)境搭建
- 安裝android studio
- 安裝 sdk(當(dāng)前使用最新版33)
-
手動(dòng)下載gradle
(更新:弄完之后有時(shí)候沒用,可以再試試掛梯子,換網(wǎng)絡(luò)之類的)
如果第一次啟動(dòng)AndroidStudio沒有報(bào)錯(cuò)則無需設(shè)置,這里是因?yàn)槲覇?dòng)完之后下載gradle報(bào)錯(cuò):
could not install gradle distribution from 'https://services.gradle.org/dist
可能是網(wǎng)絡(luò)問題連接不到,所以手動(dòng)下載。
點(diǎn)擊上面提示的鏈接下載壓縮包,然后解壓到
C:\Users\OYMN\.gradle\wrapper\dists\gradle-7.2-bin\2dnblmf4td7x66yl1d74lt32g
-
安裝模擬器
使用androidstudio提供的模擬器,或者自行下載第三方安卓模擬器(雷電模擬器)
二、簡(jiǎn)單控件
1. 文本顯示
設(shè)置文本內(nèi)容有兩種方式:
- 在 XML 文件中通過屬性 android:text 設(shè)置文本
- 在 Java 代碼中調(diào)用文本視圖對(duì)象的 setText 方法設(shè)置文本
引用字符串資源:
- 在XML文件中引用(@string/xxx)
- 在Java代碼中引用(R.string.xxx)
其余設(shè)置文本字體大小,顏色等都是可以通過關(guān)鍵詞+代碼提示很容易就能知道怎么寫,這里就不贅述。
2. 按鈕
Button繼承于TextView,因此它們擁有的屬性都是共通的。
除此之外,Button最重要的是點(diǎn)擊事件。
-
點(diǎn)擊監(jiān)聽器:通過setOnClickListener方法設(shè)置。按鈕被按住少于500毫秒時(shí),會(huì)觸發(fā)點(diǎn)擊事件。
-
長(zhǎng)按監(jiān)聽器:通過setOnLongClickListener方法設(shè)置。按鈕被按住超過500毫秒時(shí),會(huì)觸發(fā)長(zhǎng)按事件。
3. 常用布局
(1)線性布局LinearLayout
特點(diǎn):要不水平排列,要不豎直排列,通過orintation進(jìn)行設(shè)置(horiztal為水平,vertical為豎直)
權(quán)重屬性:通過layout_weight來設(shè)置,在線性布局的直接下級(jí)進(jìn)行設(shè)置,表示該下級(jí)布局占據(jù)的寬高比例。
- layout_width填0dp時(shí),layout_weight表示水平方向的寬度比例。
- layout_height填0dp時(shí),layout_weight表示垂直方向的高度比例。
(3)相對(duì)布局RelativeLayout
相對(duì)布局中的視圖位置由兩個(gè)因素所影響:
- 與該視圖平級(jí)的其他視圖
- 上級(jí)視圖(也就是它歸屬的RelativeLayout)
相對(duì)位置的一些取值:
(3)網(wǎng)格布局GridLayout
顧名思義該布局適用于表格類型的布局。
4. 圖像顯示
圖片一般放在res/drawable目錄下,設(shè)置圖像顯示一般有兩種方法:
- 在XML文件中,通過屬性android:src設(shè)置圖片資源,屬性值格式形如 @drawable/不含擴(kuò)展名的圖片名稱。
- 在Java代碼中,調(diào)用setImageResource方法設(shè)置圖片資源,方法參數(shù)格式形如 R.drawable.不含擴(kuò)展名的圖片名稱。
(1)圖像的縮放問題:
ImageView本身默認(rèn)圖片居中顯示,若要改變圖片的顯示方式,可通過scaleType屬性設(shè)定,該屬性的取值說明如下:
(2)圖像按鈕ImageButton:
ImageButton是顯示圖片的圖像按鈕,但它繼承自ImageView,而非繼承Button。
ImageButton和Button之間的區(qū)別有:
- Button既可顯示文本也可顯示圖片,ImageButton只能顯示圖片不能顯示文本。
- ImageButton上的圖像可按比例縮放,而Button通過背景設(shè)置的圖像會(huì)拉伸變形。
- Button只能靠背景顯示一張圖片,而ImageButton可分別在前景和背景顯示圖片,從而實(shí)現(xiàn)兩張圖片疊加的效果。
三、Activity
Activity是安卓開發(fā)四大組件之一,非常重要。
1. Activity的啟動(dòng)和結(jié)束
Activity的啟動(dòng)這里指的是跳轉(zhuǎn),從一個(gè)頁面跳轉(zhuǎn)到一個(gè)新的頁面,就相當(dāng)于啟動(dòng)了一個(gè)新的頁面。
示例:
bt.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v) {
Intent intent = new Intent();
intent.setClass(MainActivity.this, MainActivity2.class);
startActivity(intent);
}
});
結(jié)束Activity:調(diào)用 finish()
。
2. Activity的生命周期
onCreate:此時(shí)將頁面布局加載到內(nèi)存中,初始化頁面。
onStart:將頁面展示在屏幕。
onResume:此時(shí)頁面能夠和用戶進(jìn)行交互。
onPause:頁面進(jìn)入暫停狀態(tài),無法和用戶進(jìn)行交互。
onStop:頁面不在屏幕顯示。
onDestory:回收Activity占用的資源,徹底銷毀該Activity。
onRestart:onStop狀態(tài)可以轉(zhuǎn)為onRestart狀態(tài)。
onNewIntent:重用已存在的活動(dòng)實(shí)例。如果一個(gè)Activity已經(jīng)啟動(dòng)了,并且存在與當(dāng)前棧,而當(dāng)前棧的啟動(dòng)模式為SingleTask,SingleInstance,SingleTop(此時(shí)在任務(wù)棧頂端),那么再次啟動(dòng)該Activity的話,并不會(huì)重新進(jìn)行onCreate,而是會(huì)執(zhí)行onNewIntent方法。
3. Activity的啟動(dòng)模式
Android允許在創(chuàng)建Activity時(shí)設(shè)置啟動(dòng)模式,通過啟動(dòng)模式控制Activity的出入棧行為。
(1)靜態(tài)設(shè)置
設(shè)置方式:打開AndroidManifest.xml文件,給activity添加屬性android:launchMode。如以下表示該activity使用standard標(biāo)準(zhǔn)模式,默認(rèn)也是標(biāo)準(zhǔn)模式。
<activity android:name=".JumpFirstActivity" android:launchMode="standard" />
launchMode的取值有:
(2)動(dòng)態(tài)設(shè)置
通過 Intent 動(dòng)態(tài)設(shè)置 Activity啟動(dòng)模式:
intent.setFlags();
4. Activity之間傳遞信息
Intent能夠讓Android各組件之間進(jìn)行溝通。
Intent可以完成3部分工作:
- 表明本次通信從哪里來,往哪里走,要怎么走。
- 發(fā)送方可以攜帶消息給接收方,接收方可以從收到的Intent解析數(shù)據(jù)。
- 發(fā)送方如果想要知道接收方的處理結(jié)果,接收方也可以通過Intent返回結(jié)果。
Intent的一些組成元素:
(1)顯式Intent和隱式Intent
1. 顯式Intent
創(chuàng)建方式:
-
在Intent的構(gòu)造函數(shù)中指定:
Intent intent = new Intent(this, NextActivity.class);
-
調(diào)用setClass指定:
Intent intent = new Intent(); intent.setClass(this, NextActivity.class);
-
調(diào)用setComponent指定:
Intent intent = new Intent(); ComponentName component = new ComponentName(this, NextActivity.class); intent.setComponent(component);
2. 隱式Intent:
沒有明確指定所要跳轉(zhuǎn)的頁面,而是通過一些動(dòng)作字符串來讓系統(tǒng)自動(dòng)匹配。
通常是App不想向外暴露Activity的名稱,只給出一些定義好的字符串。這些字符串可以自己定義,也有系統(tǒng)定義的。
常見的系統(tǒng)動(dòng)作如下:
下面以調(diào)用系統(tǒng)撥號(hào)頁面舉例:
String phone = "12345";
Intent intent = new Intent();
//這里表示設(shè)置意圖動(dòng)作為準(zhǔn)備撥號(hào)
intent.setAction(Intent.ACTION_DIAL);
intent.setData(Uri.parse("tel:" + phone));
startActivity(intent);
如果想要跳轉(zhuǎn)到自己定義的activity:
步驟一:在AndroidManifest.xml找到該activity,添加action和category標(biāo)簽,同時(shí)設(shè)置exported為true,表示允許被其他activity調(diào)用。
步驟二:調(diào)用過程和上面一樣:
Intent intent = new Intent();
intent.setAction("android.intent.action.activity2");
intent.addCategory(Intent.CATEGORY_DEFAULT);
startActivity(intent);
(2)向下一個(gè)Activity發(fā)送消息:
Intent重載了很多putExtra方法用于傳遞各種類型的信息,包括整數(shù)類型,字符串等。但是顯然通過調(diào)用putExtra方法會(huì)很不好管理,因?yàn)閿?shù)據(jù)都是零碎傳遞。所以Android引入了Bundle,其內(nèi)部是一個(gè)Map,使用起來也和Map一樣。
示例:
Intent intent = new Intent(this, NextActivity.class);
//通過bundle包裝數(shù)據(jù)
Bundle bundle = new Bundle();
bundle.putString("stringKey", "stringValue");
intent.putExtras(bundle);
startActivity(intent);
然后下一個(gè)Activity就可以通過intent獲取到所想要的數(shù)據(jù)了:
Bundle bundle = getIntent().getExtras();
String stringValue = bundle.getString("stringKey");
(3)向上一個(gè)Activity返回消息:
上一個(gè)頁面跳轉(zhuǎn)到下一個(gè)頁面,同時(shí)攜帶數(shù)據(jù):
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);
//回調(diào)函數(shù),返回到這個(gè)頁面時(shí)所執(zhí)行的程序
register = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), new ActivityResultCallback<ActivityResult>() {
//回調(diào)函數(shù)
@Override
public void onActivityResult(ActivityResult result) {
if (result != null) {
Intent intent = result.getData();
if (intent != null && result.getResultCode() == Activity.RESULT_OK) {
//獲取到返回的數(shù)據(jù)
Bundle bundle = intent.getExtras();
//...
}
}
}
});
}
@Override
public void onClick(View v) {
Intent intent = new Intent(this, MainActivity3.class);
//跳轉(zhuǎn)下一頁面
register.launch(intent);
}
下一個(gè)頁面接受到數(shù)據(jù),處理之后返回結(jié)果給上一個(gè)頁面:
Bundle bundle = getIntent().getExtras();
//...頁面進(jìn)行處理
//返回?cái)?shù)據(jù)給上一個(gè)頁面
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)獲取元數(shù)據(jù)信息:
try {
//獲取包管理器
PackageManager pm = getPackageManager();
//獲取當(dāng)前的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();
}
四、數(shù)據(jù)存儲(chǔ)
1. 共享參數(shù)SharedPreferences
(1)使用:
sharedPreferences是安卓的一個(gè)輕量級(jí)存儲(chǔ)工具,采用的方式是key-value,以xml文件形式存在,文件路徑為/data/data/應(yīng)用包名/shared_prefs/文件名.xml。
適合場(chǎng)景:
- 簡(jiǎn)單且孤立的數(shù)據(jù)
- 文本數(shù)據(jù),二進(jìn)制數(shù)據(jù)則不合適
- 需要持久化的數(shù)據(jù),也就是重啟APP后數(shù)據(jù)仍然存在且有效。
實(shí)際開發(fā)中,sharedPreferences經(jīng)常用來存儲(chǔ)的數(shù)據(jù)有:APP的個(gè)性化配置信息,用戶使用APP的行為信息等。
sharedPreferences對(duì)數(shù)據(jù)的存儲(chǔ)和讀取類似Map,提供put和set方法。
獲取數(shù)據(jù)可以通過SharedPreferences對(duì)象獲取:
//第一個(gè)參數(shù)表示文件名,第二個(gè)參數(shù)表示私有模式
SharedPreferences shared = getSharedPreferences("fileName", MODE_PRIVATE);
String name = shared.getString("name");
而存儲(chǔ)數(shù)據(jù)則還需要借助Editor類:
SharedPreferences.Editor editor = shared.edit();
editor.putString("name", "oymn");
editor.putInt("age", 20);
editor.commit();
(2)應(yīng)用實(shí)例:記住密碼功能
- 聲明一個(gè)共享參數(shù)對(duì)象,并在onCreate中調(diào)用getSharedPreferences方法獲取共享參數(shù)的實(shí)例。
- 登錄成功時(shí),如果用戶勾選了“記住密碼”,就使用共享參數(shù)保存手機(jī)號(hào)碼與密碼。
所以在登錄頁面的onCreat方法中添加獲取共享參數(shù)的代碼:
// 從share_login.xml獲取共享參數(shù)對(duì)象
mShared = getSharedPreferences("share_login", MODE_PRIVATE);
// 獲取共享參數(shù)保存的手機(jī)號(hào)碼
String phone = mShared.getString("phone", "");
// 獲取共享參數(shù)保存的密碼
String password = mShared.getString("password", "");
et_phone.setText(phone); // 往手機(jī)號(hào)碼編輯框填寫上次保存的手機(jī)號(hào)
et_password.setText(password); // 往密碼編輯框填寫上次保存的密碼
接著在登錄成功方法中添加保存功能:
// 如果勾選了“記住密碼”,就把手機(jī)號(hào)碼和密碼都保存到共享參數(shù)中
if (isRemember) {
SharedPreferences.Editor editor = mShared.edit(); // 獲得編輯器的對(duì)象
editor.putString("phone", et_phone.getText().toString()); // 添加名叫phone的手機(jī)號(hào)碼
editor.putString("password", et_password.getText().toString()); // 添加名叫password的密碼
editor.commit(); // 提交編輯器中的修改
}
2. 數(shù)據(jù)庫SQLite
SQLite是安卓的一種小巧的嵌入式數(shù)據(jù)庫,基本使用和思路和Mysql無異。
(1)SQLiteDatabase
java代碼層面借助SQLiteDatabase來對(duì)SQLite進(jìn)行操作。
//創(chuàng)建數(shù)據(jù)庫text.db
SQLiteDatabase db = openOrCreateDatabase(getFileDir() + "/test.db", Context.MODE_PRIVATE, null);
(2)SQLiteOpenHelper
由于SQLiteDatabase存在局限性,一不小心就會(huì)重復(fù)打開數(shù)據(jù)庫,處理數(shù)據(jù)庫的升級(jí)也不方便;因此Android提供了數(shù)據(jù)庫幫助器SQLiteOpenHelper,幫助開發(fā)者合理使用SQLite。
SQLiteOpenHelper的具體使用步驟如下:
- 步驟一,新建一個(gè)繼承自SQLiteOpenHelper的數(shù)據(jù)庫操作類,按提示重寫onCreate和onUpgrade兩個(gè)方法。其中,onCreate方法只在第一次打開數(shù)據(jù)庫時(shí)執(zhí)行,在此可以創(chuàng)建表結(jié)構(gòu);而onUpgrade方法在數(shù)據(jù)庫版本升高時(shí)執(zhí)行,在此可以根據(jù)新舊版本號(hào)變更表結(jié)構(gòu)。
- 步驟二,為保證數(shù)據(jù)庫安全使用,需要封裝幾個(gè)必要方法,包括獲取單例對(duì)象、打開數(shù)據(jù)庫連接、關(guān)閉數(shù)據(jù)庫連接,說明如下:
- 獲取單例對(duì)象:確保在App運(yùn)行過程中數(shù)據(jù)庫只會(huì)打開一次,避免重復(fù)打開引起錯(cuò)誤。
- 打開數(shù)據(jù)庫連接:SQLite有鎖機(jī)制,即讀鎖和寫鎖的處理;故而數(shù)據(jù)庫連接也分兩種,讀連接可調(diào)用getReadableDatabase方法獲得,寫連接可調(diào)用getWritableDatabase獲得。
- 關(guān)閉數(shù)據(jù)庫連接:數(shù)據(jù)庫操作完畢,調(diào)用數(shù)據(jù)庫實(shí)例的close方法關(guān)閉連接。
- 步驟三, 提供對(duì)表記錄增加、刪除、修改、查詢的操作方法。能被SQLite直接使用的數(shù)據(jù)結(jié)構(gòu)是ContentValues類,它類似于映射Map,也提供了put和get方法存取鍵值對(duì)。
- 區(qū)別之處在于:ContentValues的鍵只能是字符串,不能是其他類型。ContentValues主要用于增加記錄和更新記錄,對(duì)應(yīng)數(shù)據(jù)庫的insert和update方法。
- 記錄的查詢操作用到了游標(biāo)類Cursor,調(diào)用query和rawQuery方法返回的都是Cursor對(duì)象,若要獲取全部的查詢結(jié)果,則需根據(jù)游標(biāo)的指示一條一條遍歷結(jié)果集合。Cursor的常用方法可分為3類,說明如下:
(3)代碼舉例:
public class UserDBHelper extends SQLiteOpenHelper {
private static final String DB_NAME = "user.db"; //數(shù)據(jù)庫名稱
private static final int DB_VERSION = 1; //數(shù)據(jù)庫的版本號(hào)
private static UserDBHelper helper = null; //單例
private SQLiteDatabase sdb = null; //數(shù)據(jù)庫實(shí)例
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 的唯一實(shí)例
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;
}
//關(guān)閉數(shù)據(jù)庫連接
public void closeLink() {
if (sdb != null && sdb.isOpen()) {
sdb.close();
sdb = null;
}
}
//創(chuàng)建數(shù)據(jù)庫,執(zhí)行建表語句
@Override
public void onCreate(SQLiteDatabase db) {
//先刪除已存在表
String drop_sql = "drop table if exists " + TABLE_NAME + ";";
db.execSQL(drop_sql);
//創(chuàng)建表
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"
//演示數(shù)據(jù)庫升級(jí)時(shí)要先把下面這行注釋
+ ",phone VARCHAR" + ",password VARCHAR"
+ ");";
db.execSQL(create_sql);
}
//修改表結(jié)構(gòu)
@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); // 執(zhí)行完整的SQL語
}
}
//根據(jù)指定條件刪除記錄
public int delete(String condition) {
return sdb.delete(TABLE_NAME, condition, null);
}
//刪除全部記錄
public int deleteAll() {
return sdb.delete(TABLE_NAME, "1=1", null);
}
//根據(jù)條件查詢記錄
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);
//執(zhí)行查詢語句,該語句返回結(jié)果集的游標(biāo)
Cursor cursor = sdb.rawQuery(sql, null);
ArrayList<UserInfo> userInfos = new ArrayList<>();
//循環(huán)取出游標(biāo)指向的結(jié)果集
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;
}
}
//其余情況則說明記錄不重復(fù),添加新紀(jì)錄
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;
}
//根據(jù)指定條件更新表記錄
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)優(yōu)化記住密碼:
上面通過SharedPreferences存儲(chǔ)密碼的方式還是存在一定的局限性,該方式只能記住一個(gè)用戶的登錄信息,當(dāng)下一個(gè)用戶登錄后,上一個(gè)用戶的信息將會(huì)被覆蓋。正確的記住密碼功能應(yīng)該是輸入手機(jī)號(hào)自動(dòng)補(bǔ)充密碼,因此,可以考慮使用數(shù)據(jù)庫來進(jìn)行存儲(chǔ)。
主要的改造如下:
- 聲明一個(gè)數(shù)據(jù)庫的helper對(duì)象,在Activity的OnResume方法中獲取數(shù)據(jù)庫連接,在OnPause方法中關(guān)閉數(shù)據(jù)庫連接。
private UserDBHelper helper;
@Override
protected void onResume() {
super.onResume();
//獲取數(shù)據(jù)庫幫助器實(shí)例 (此處是單例,所以不怕重復(fù)獲取)
helper = UserDBHelper.getInstance(this, 1);
//恢復(fù)頁面時(shí)則獲取連接
helper.openWriteLink();
}
@Override
protected void onPause() {
super.onPause();
//暫停頁面時(shí)就斷開連接
helper.closeLink();
}
- 登錄成功后,如果用戶勾選了記住密碼功能,則保存到數(shù)據(jù)庫。也就是在loginSuccess方法中添加如下:
if (isRemember) {
UserInfo info = new UserInfo(); // 創(chuàng)建一個(gè)用戶信息對(duì)象
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); // 往用戶數(shù)據(jù)庫添加登錄成功的用戶信息
}
- 用戶進(jìn)行登錄時(shí),根據(jù)輸入手機(jī)號(hào)自動(dòng)查找密碼:
// 根據(jù)手機(jī)號(hào)碼查詢指定記錄
public UserInfo queryByPhone(String phone) {
UserInfo info = null;
List<UserInfo> infoList = query(String.format("phone='%s'", phone));
if (infoList.size() > 0) { // 存在該號(hào)碼的登錄信息
info = infoList.get(0);
}
return info;
}
3. 存儲(chǔ)卡
(1)私有空間和公有空間
為了更規(guī)范地管理手機(jī)存儲(chǔ)空間,Android從7.0開始將存儲(chǔ)卡劃分為私有存儲(chǔ)和公共存儲(chǔ)兩大部分,也就是分區(qū)存儲(chǔ)方式,系統(tǒng)給每個(gè)App都分配了默認(rèn)的私有存儲(chǔ)空間。App在私有空間上讀寫文件無須任何授權(quán),但是若想在公共空間讀寫文件,則要在AndroidManifest.xml里面添加下述的權(quán)限配置。
<!-- 存儲(chǔ)卡讀寫 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAG"/>
但是即使App聲明了完整的存儲(chǔ)卡操作權(quán)限,系統(tǒng)仍然默認(rèn)禁止該App訪問公共空間。打開手機(jī)的系統(tǒng)設(shè)置界面,進(jìn)入到具體應(yīng)用的管理頁面,會(huì)發(fā)現(xiàn)該應(yīng)用的存儲(chǔ)訪問權(quán)限被禁止了。
既然存儲(chǔ)卡分為公共空間和私有空間兩部分,它們的空間路徑獲取也就有所不同。若想獲取公共空間的存儲(chǔ)路徑,調(diào)用的是Environment.getExternalStoragePublicDirectory方法;若想獲取應(yīng)用私有空間的存儲(chǔ)路徑,調(diào)用的是getExternalFilesDir方法。
//獲取系統(tǒng)的公共存儲(chǔ)路徑
String publicPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toString();
//獲取系統(tǒng)的私有存儲(chǔ)路徑
String privatePath = getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString();
boolean isLegacy = true;
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q){
//Android10的存儲(chǔ)空間默認(rèn)采用分區(qū)方式,這里是判斷是使用傳統(tǒng)方式還是分區(qū)方式
isLegacy = Environment.isExternalStorageLegacy();
}
(2)在存儲(chǔ)卡上讀寫文件
文本文件的讀寫借助IO流 FileOutputStream(寫文件)和 FileInputStream(讀文件)
// 把字符串保存到指定路徑的文本文件
public static void saveText(String path, String txt) {
// 根據(jù)指定的文件路徑構(gòu)建文件輸出流對(duì)象
try (FileOutputStream fos = new FileOutputStream(path)) {
fos.write(txt.getBytes()); // 把字符串寫入文件輸出流
} catch (Exception e) {
e.printStackTrace();
}
}
// 從指定路徑的文本文件中讀取內(nèi)容字符串
public static String openText(String path) {
String readStr = "";
// 根據(jù)指定的文件路徑構(gòu)建文件輸入流對(duì)象
try (FileInputStream fis = new FileInputStream(path)) {
byte[] b = new byte[fis.available()];
fis.read(b); // 從文件輸入流讀取字節(jié)數(shù)組
readStr = new String(b); // 把字節(jié)數(shù)組轉(zhuǎn)換為字符串
} catch (Exception e) {
e.printStackTrace();
}
return readStr; // 返回文本文件中的文本字符串
}
(3)在存儲(chǔ)卡上讀寫 圖片文件
文本文件可以轉(zhuǎn)化為對(duì)字符串的讀寫,而圖像的讀寫就需要借助專門的位圖工具Bitmap處理。不同圖像來源獲取Bitmap的方式不同,有三種:
- 從指定資源文件中獲?。篸ecodeResource,例如從資源文件img.png獲取位圖對(duì)象:
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.img);
- 從指定路徑下獲取:decodeFile,但是要注意從Android10開始,該方法只能獲取私有空間下的圖片,公共空間下獲取不了。
Bitmap bitmap = BitmapFactory.decodeFile("C:\\Users\\OYMN\\Pictures\\onepunch.jpg");
- 從指定的輸入流中獲取,比如使用IO流打開圖片文件,然后作為參數(shù)傳入decodeStream:
public static Bitmap openImage(String path) {
Bitmap bitmap = null; // 聲明一個(gè)位圖對(duì)象
// 根據(jù)指定的文件路徑構(gòu)建文件輸入流對(duì)象
try (FileInputStream fis = new FileInputStream(path)) {
bitmap = BitmapFactory.decodeStream(fis); // 從文件輸入流中解碼位圖數(shù)據(jù)
} catch (Exception e) {
e.printStackTrace();
}
return bitmap; // 返回圖片文件中的位圖數(shù)據(jù)
}
獲取到圖片之后就可以通過ImageView的setImageBitmap進(jìn)行設(shè)置了。
有多種讀取圖片的方式,但是寫圖片只有一種方式。通過Bitmap的compress方法將位圖數(shù)據(jù)壓縮到文件輸出流:
public static void saveImage(String path, Bitmap bitmap){
//根據(jù)文件路徑構(gòu)建文件輸出流
try(FileOutputStream fos = new FileOutputStream()){
//將位圖數(shù)據(jù)壓縮到文件輸出流
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, fos);
}catch(Exception e){
e.printStackTrace();
}
}
以下演示一下完整的文件讀寫操作:
// 獲取當(dāng)前App的私有下載目錄
String path = getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString() +
"/";
// 從指定的資源文件中獲取位圖對(duì)象
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.huawei);
String file_path = path + DateUtil.getNowDateTime("") + ".jpeg";
FileUtil.saveImage(file_path, bitmap); // 把位圖對(duì)象保存為圖片文件
tv_path.setText("圖片文件的保存路徑為:\n" + file_path);
// 獲取當(dāng)前App的私有下載目錄
mPath = getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString() + "/";
// 獲得指定目錄下面的所有圖片文件
mFilelist = FileUtil.getFileList(mPath, new String[]{".jpeg"});
if (mFilelist.size() > 0) {
// 打開并顯示選中的圖片文件內(nèi)容
String file_path = mFilelist.get(0).getAbsolutePath();
tv_content.setText("找到最新的圖片文件,路徑為"+file_path);
// 顯示存儲(chǔ)卡圖片文件的第一種方式:直接調(diào)用setImageURI方法
//iv_content.setImageURI(Uri.parse(file_path)); // 設(shè)置圖像視圖的路徑對(duì)象
// 第二種方式:先調(diào)用BitmapFactory.decodeFile獲得位圖,再調(diào)用setImageBitmap方法
//Bitmap bitmap = BitmapFactory.decodeFile(file_path);
//iv_content.setImageBitmap(bitmap); // 設(shè)置圖像視圖的位圖對(duì)象
// 第三種方式:先調(diào)用FileUtil.openImage獲得位圖,再調(diào)用setImageBitmap方法
Bitmap bitmap = FileUtil.openImage(file_path);
iv_content.setImageBitmap(bitmap); // 設(shè)置圖像視圖的位圖對(duì)象
4. 應(yīng)用組件Application
Application是Android的一大組件,在App運(yùn)行期間只有一個(gè)Application對(duì)象貫穿整個(gè)應(yīng)用的生命周期。因此,Application適合保存全局變量,主要是以下三類數(shù)據(jù):
-
會(huì)頻繁讀取的信息:如用戶名,手機(jī)號(hào)碼等
-
不方便通過intent傳遞的數(shù)據(jù),如位圖對(duì)象,非字符串的集合對(duì)象等。
-
容易因頻繁分配內(nèi)存而導(dǎo)致內(nèi)存泄漏的對(duì)象,如Handler處理器實(shí)例等。
通過Application實(shí)現(xiàn)對(duì)全局內(nèi)存的讀寫:
- 先繼承Application,并獲取唯一實(shí)例:
public class MyApplication extends Application {
private static MyApplication myApplication; //Application唯一實(shí)例
public Map<String, String> map = new HashMap<>(); //當(dāng)作全局變量,用來存儲(chǔ)數(shù)據(jù)
public static MyApplication getInstance(){
return myApplication;
}
@Override
public void onCreate() {
super.onCreate();
// 在打開應(yīng)用時(shí)對(duì)靜態(tài)的應(yīng)用實(shí)例賦值
myApplication = this;
}
}
- 在AndroidManifest.xml 通過name屬性添加該Application
- 接下來就可以通過該Application在整個(gè)App中存取數(shù)據(jù)了:
如在MainActivity6存儲(chǔ)數(shù)據(jù):
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main6);
//存儲(chǔ)數(shù)據(jù)
MyApplication myApplication = MyApplication.getInstance();
myApplication.map.put("myKey", "myValue");
//跳轉(zhuǎn)到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中獲取數(shù)據(jù):
@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")); //成功獲取到數(shù)據(jù)
}
5. 實(shí)戰(zhàn):購(gòu)物車
五、內(nèi)容共享
1. 在應(yīng)用之間共享數(shù)據(jù)
接下來將介紹Android的四大組件之一ContentProvider,通過ContentProvider封裝內(nèi)部數(shù)據(jù)的外部訪問接口,實(shí)現(xiàn)不同應(yīng)用能夠互相傳輸數(shù)據(jù)。
和ContentProvider搭配使用的還有:ContentResolver(內(nèi)容解析器),ContentObserver(內(nèi)容觀察器)。
上面提到的SQLite可以操作自身的數(shù)據(jù)庫,而ContentProvider則是作為中間接口,通過SQLiteOpenHelper和SQLiteDatabase間接操控?cái)?shù)據(jù)庫,實(shí)現(xiàn)為其他應(yīng)用提供數(shù)據(jù)的功能。
使用舉例如下:
-
創(chuàng)建一個(gè)UserInfoProvider,用來提供用戶信息給外界應(yīng)用
在彈出的右鍵菜單中依次選擇New→Other→Content Provider
此時(shí)會(huì)自動(dòng)修改兩處地方:
(1)一是在AndroidManifest.xml中添加該P(yáng)rovider的配置信息:
(2)二是創(chuàng)建的這個(gè)Provider會(huì)繼承ContentProvider,并重寫了一些方法。
Server端代碼:
public class UserInfoProvider extends ContentProvider {
//這里是上面實(shí)現(xiàn)的dbHelper,用來操作本地?cái)?shù)據(jù)庫
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插入數(shù)據(jù)
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不帶參數(shù):"content://com.example.secondandroidapp.UserInfoProvider/user"
case USER:
// 獲取SQLite數(shù)據(jù)庫的寫連接
SQLiteDatabase db = userDBHelper.getWritableDatabase();
// 執(zhí)行SQLite的刪除操作,并返回刪除記錄的數(shù)目
count = db.delete(UserDBHelper.TABLE_NAME, selection,
selectionArgs);
db.close();
break;
//這種是uri帶參數(shù):"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只實(shí)現(xiàn)服務(wù)端App的數(shù)據(jù)封裝,如果客戶端App想訪問對(duì)方的內(nèi)部數(shù)據(jù),就要通過內(nèi)容解析器ContentResolver訪問。
ContentProvider的Uri結(jié)構(gòu)如下: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之后調(diào)用插入方法進(jìn)行插入
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需要事先聲明需要訪問的其他應(yīng)用:
在AndroidManifest.xml中添加如下:
<queries>
<!--服務(wù)端應(yīng)用包名 -->
<package android:name="com.example.secondandroidapp"/>
<!--或者直接指定authorities-->
<!-- <provider android:authorities="com.example.secondandroidapp.UserInfoProvider"/> -->
</queries>
2. 使用內(nèi)容組件獲取通訊信息
(1)運(yùn)行時(shí)動(dòng)態(tài)申請(qǐng)權(quán)限
在上面講公共存儲(chǔ)空間與私有存儲(chǔ)空間提到,App若想訪問存儲(chǔ)卡的公共空間,就要在AndroidManifest.xml里面添加下述的權(quán)限配置。
<!-- 存儲(chǔ)卡讀寫 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAG" />
然而即使App聲明了完整的存儲(chǔ)卡操作權(quán)限,從Android 7.0開始,系統(tǒng)仍然默認(rèn)禁止該App訪問公共空間,必須到設(shè)置界面手動(dòng)開啟應(yīng)用的存儲(chǔ)卡權(quán)限才行。盡管此舉是為用戶隱私著想,可是人家咋知道要手工開權(quán)限呢?就算用戶知道,去設(shè)置界面找到權(quán)限開關(guān)也頗費(fèi)周折。為此Android支持在Java代碼中處理權(quán)限,處理過程分為3個(gè)步驟:
-
檢查App是否開啟了指定權(quán)限:
調(diào)用ContextCompat的checkSelfPermission方法
-
請(qǐng)求系統(tǒng)彈窗,以便用戶選擇是否開啟權(quán)限:
調(diào)用ActivityCompat的requestPermissions方法,即可命令系統(tǒng)自動(dòng)彈出權(quán)限申請(qǐng)窗口。
-
判斷用戶的權(quán)限選擇結(jié)果,是開啟還是拒絕:
重寫活動(dòng)頁面的權(quán)限請(qǐng)求回調(diào)方法onRequestPermissionsResult,在該方法內(nèi)部處理用戶的權(quán)限選擇結(jié)果
動(dòng)態(tài)申請(qǐng)權(quán)限有兩種方式:餓漢式 和 懶漢式。
接下來通過獲取通訊權(quán)限和短信權(quán)限來進(jìn)行舉例說明:
首先是懶漢式:當(dāng)需要某種權(quán)限的時(shí)候再去申請(qǐng)
public class PermissionUtil {
//檢查權(quán)限,返回true表示完全啟用權(quán)限,返回false則表示為完全啟用所有權(quán)限
public static boolean checkPermission(Activity activity, String[] permissions, int requestCode){
//Android6.0之后采取動(dòng)態(tài)權(quán)限管理
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;
}
}
//如果未開啟該權(quán)限,則請(qǐng)求系統(tǒng)彈窗,好讓用戶選擇是否開啟權(quán)限
if(check != PackageManager.PERMISSION_GRANTED){
//請(qǐng)求權(quán)限
ActivityCompat.requestPermissions(activity, permissions, requestCode);
return false;
}
return true;
}
return false;
}
//檢查權(quán)限數(shù)組,返回true表示都已經(jīng)授權(quán)
public static boolean checkGrant(int[] grantResults) {
if(grantResults != null){
for (int grant : grantResults) {
if(grant != PackageManager.PERMISSION_GRANTED){
return false;
}
}
return true;
}
return false;
}
}
通過兩個(gè)按鈕模擬分別獲取權(quán)限:
public class PermissionLazyActivity extends AppCompatActivity {
//通訊錄的讀寫權(quán)限
private static final String[] PERMISSION_CONTACT = {
Manifest.permission.READ_CONTACTS,
Manifest.permission.WRITE_CONTACTS
};
//短信的讀寫權(quán)限
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);
//獲取通訊錄權(quán)限
findViewById(R.id.btn_contact).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
PermissionUtil.checkPermission(PermissionLazyActivity.this, PERMISSION_CONTACT, REQUEST_CODE_CONTACTS);
}
});
//獲取短信權(quán)限
findViewById(R.id.btn_sms).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
PermissionUtil.checkPermission(PermissionLazyActivity.this, PERMISSION_SMS, REQUEST_CODE_SMS);
}
});
}
// 用戶選擇權(quán)限結(jié)果后會(huì)調(diào)用該回調(diào)方法
@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", "通訊錄獲取失敗");
//跳轉(zhuǎn)到設(shè)置界面
jumpToSettings();
}
break;
case REQUEST_CODE_SMS:
if(PermissionUtil.checkGrant(grantResults)){
Log.d("hhh", "短信權(quán)限獲取成功");
}else{
Log.d("hhh", "短信權(quán)限獲取失敗");
//跳轉(zhuǎn)到設(shè)置界面
jumpToSettings();
}
break;
}
}
//跳轉(zhuǎn)到設(shè)置界面
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中配置:(在低版本中只需要配置這些信息即可,高版本就需要上面的動(dòng)態(tài)申請(qǐng)權(quán)限)
<!-- 開啟通訊錄權(quán)限-->
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
<!-- 開啟短信收發(fā)權(quán)限-->
<uses-permission android:name="android.permission.SEND_SMS"/>
<uses-permission android:name="android.permission.RECEIVE_SMS"/>
效果如下:
懶漢式:在頁面打開之后就一次性需要用戶獲取所有權(quán)限。
public class PermissionHungryActivity extends AppCompatActivity {
//所需全部讀寫權(quán)限
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);
//檢查是否擁有所有所需權(quán)限
PermissionUtil.checkPermission(this, PERMISSIONS, REQUEST_CODE_ALL);
}
// 用戶選擇權(quán)限結(jié)果后會(huì)調(diào)用該回調(diào)方法
@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", "所有權(quán)限獲取成功");
}else{
//部分權(quán)限獲取失敗
for (int i = 0; i < grantResults.length; i++) {
if(grantResults[i] != PackageManager.PERMISSION_GRANTED){
//判斷是什么權(quán)限獲取失敗
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", "短信權(quán)限獲取失敗");
jumpToSettings();
break;
}
}
}
}
break;
}
}
//跳轉(zhuǎn)到設(shè)置界面
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讀寫聯(lián)系人
手機(jī)中通訊錄的主要表結(jié)構(gòu)有:
raw_contacts
表:
data表:記錄了用戶的通訊錄所有數(shù)據(jù),包括手機(jī)號(hào),顯示名稱等,但是里面的mimetype_id表示不同的數(shù)據(jù)類型,這與表mimetypes表中的id相對(duì)應(yīng),raw_contact_id 與上面的 raw_contacts表中的 id 相對(duì)應(yīng)。
mimetypes表:
所以,插入步驟如下:
- 首先往raw_contacts表中插入一條數(shù)據(jù)得到id
- 接著由于一個(gè)聯(lián)系人有姓名,電話號(hào)碼,郵箱,因此需要分三次插入data表中,將raw_contact_id和上面得到的id進(jìn)行關(guān)聯(lián)
下面是往通訊錄插入和查詢聯(lián)系人的代碼:
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:
// 創(chuàng)建一個(gè)聯(lián)系人對(duì)象
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多次寫入,每次一個(gè)字段
// addContacts(getContentResolver(), contact);
// 方式二,批處理方式
// 每一次操作都是一個(gè) ContentProviderOperation,構(gòu)建一個(gè)操作集合,然后一次性執(zhí)行
// 好處是,要么全部成功,要么全部失敗,保證了事務(wù)的一致性
addFullContacts(getContentResolver(), contact);
Toast.makeText(this, "添加聯(lián)系人成功!", Toast.LENGTH_SHORT).show();
break;
case R.id.btn_read_contact:
readPhoneContacts(getContentResolver());
break;
}
}
//往通訊錄添加一個(gè)聯(lián)系人信息(姓名,號(hào)碼,郵箱)
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();
//關(guān)聯(lián)上面得到的聯(lián)系人id
name.put(ContactsContract.Contacts.Data.RAW_CONTACT_ID, rawContentId);
//關(guān)聯(lián)聯(lián)系人姓名的類型
name.put(ContactsContract.Contacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE);
//關(guān)聯(lián)聯(lián)系人姓名
name.put(ContactsContract.Data.DATA2, contact.name);
contentResolver.insert(ContactsContract.Data.CONTENT_URI, name);
//插入電話號(hào)碼
ContentValues phone = new ContentValues();
//關(guān)聯(lián)上面得到的聯(lián)系人id
phone.put(ContactsContract.Contacts.Data.RAW_CONTACT_ID, rawContentId);
//關(guān)聯(lián)聯(lián)系人電話號(hào)碼的類型
phone.put(ContactsContract.Contacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE);
//關(guān)聯(lián)聯(lián)系人電話號(hào)碼
phone.put(ContactsContract.Data.DATA1, contact.phone);
//指定該號(hào)碼是家庭號(hào)碼還是工作號(hào)碼 (家庭)
phone.put(ContactsContract.Data.DATA2, ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE);
contentResolver.insert(ContactsContract.Data.CONTENT_URI, phone);
//插入郵箱
ContentValues email = new ContentValues();
//關(guān)聯(lián)上面得到的聯(lián)系人id
email.put(ContactsContract.Contacts.Data.RAW_CONTACT_ID, rawContentId);
//關(guān)聯(lián)聯(lián)系人郵箱的類型
email.put(ContactsContract.Contacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE);
//關(guān)聯(lián)聯(lián)系人郵箱
email.put(ContactsContract.Data.DATA1, contact.email);
//指定該號(hào)碼是家庭郵箱還是工作郵箱
email.put(ContactsContract.Data.DATA2, ContactsContract.CommonDataKinds.Phone.TYPE_WORK);
contentResolver.insert(ContactsContract.Data.CONTENT_URI, email);
}
//事務(wù)操作,四個(gè)插入操作一次性提交
private void addFullContacts(ContentResolver contentResolver, Contact contact) {
//創(chuàng)建一個(gè)插入聯(lián)系人主記錄的內(nèi)容操作器
ContentProviderOperation op_main = ContentProviderOperation
.newInsert(ContactsContract.RawContacts.CONTENT_URI)
//沒有實(shí)際意義,不加這個(gè)會(huì)報(bào)錯(cuò)(不加這個(gè)導(dǎo)致沒有創(chuàng)建ContentValue,導(dǎo)致報(bào)錯(cuò))
.withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null)
.build();
//創(chuàng)建一個(gè)插入聯(lián)系人姓名記錄的內(nèi)容操作器
ContentProviderOperation op_name = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
//將第0個(gè)操作的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();
//創(chuàng)建一個(gè)插入聯(lián)系人電話號(hào)碼記錄的內(nèi)容操作器
ContentProviderOperation op_phone = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
//將第0個(gè)操作的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();
//創(chuàng)建一個(gè)插入聯(lián)系人郵箱記錄的內(nèi)容操作器
ContentProviderOperation op_email = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
//將第0個(gè)操作的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 = new ArrayList<>();
operations.add(op_main);
operations.add(op_name);
operations.add(op_phone);
operations.add(op_email);
try {
//批量提交四個(gè)操作
contentResolver.applyBatch(ContactsContract.AUTHORITY, operations);
} catch (OperationApplicationException e) {
e.printStackTrace();
} catch (RemoteException e) {
e.printStackTrace();
}
}
//讀取聯(lián)系人
@SuppressLint("Range")
private void readPhoneContacts(ContentResolver contentResolver) {
//先查詢r(jià)aw_contacts表,再根據(jù)raw_contacts_id表 查詢data表
Cursor cursor = contentResolver.query(ContactsContract.RawContacts.CONTENT_URI, new String[]{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, new String[]{ContactsContract.Contacts.Data.MIMETYPE, ContactsContract.Contacts.Data.DATA1, ContactsContract.Contacts.Data.DATA2}, null, null, null);
Contact contact = new Contact();
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) {
//是姓名
case ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE:
contact.name = data1;
break;
//郵箱
case ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE:
contact.email = data1;
break;
//手機(jī)
case ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE:
contact.phone = data1;
break;
}
}
dataCursor.close();
// RawContacts 表中出現(xiàn)的 _id,不一定在 Data 表中都會(huì)有對(duì)應(yīng)記錄
if (contact.name != null) {
Log.d("hhh", contact.toString());
}
}
cursor.close();
}
}
頁面如下:
(3)使用ContentObserver監(jiān)聽短信
ContentResolver獲取數(shù)據(jù)采用的是主動(dòng)查詢方式,有查詢就有數(shù)據(jù),沒查詢就沒數(shù)據(jù)。ContentResolver能夠?qū)崟r(shí)獲取新增的數(shù)據(jù),最常見的業(yè)務(wù)場(chǎng)景是短信驗(yàn)證碼。為了替用戶省事,App通常會(huì)監(jiān)控手機(jī)剛收到的短信驗(yàn)證碼,并自動(dòng)填寫驗(yàn)證碼輸入框。這時(shí)就用到了內(nèi)容觀察器ContentObserver,事先給目標(biāo)內(nèi)容注冊(cè)一個(gè)觀察器,目標(biāo)內(nèi)容的數(shù)據(jù)一旦發(fā)生變化,就馬上觸發(fā)觀察器的監(jiān)聽事件,從而執(zhí)行開發(fā)者預(yù)先定義的代碼。
文章來源:http://www.zghlxwxcb.cn/news/detail-781755.html
示例代碼如下:(記得在Manifest.xml中開啟權(quán)限和動(dòng)態(tài)開啟權(quán)限)文章來源地址http://www.zghlxwxcb.cn/news/detail-781755.html
public class MonitorSmsActivity extends AppCompatActivity {
private SmsGetObserver mObserver;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_monitor_sms);
// 給指定Uri注冊(cè)內(nèi)容觀察器,一旦發(fā)生數(shù)據(jù)變化,就觸發(fā)觀察器的onChange方法
Uri uri = Uri.parse("content://sms");
// notifyForDescendents:
// false :表示精確匹配,即只匹配該Uri,true :表示可以同時(shí)匹配其派生的Uri
mObserver = new SmsGetObserver(this);
getContentResolver().registerContentObserver(uri, true, mObserver);
}
@Override
protected void onDestroy() {
super.onDestroy();
//取消注冊(cè)
getContentResolver().unregisterContentObserver(mObserver);
}
private static class SmsGetObserver extends ContentObserver {
private final Context mContext;
public SmsGetObserver(Context context) {
super(new Handler(Looper.getMainLooper()));
this.mContext = context;
}
//回調(diào)
@SuppressLint("Range")
@Override
public void onChange(boolean selfChange, @Nullable Uri uri) {
super.onChange(selfChange, uri);
// onChange會(huì)多次調(diào)用,收到一條短信會(huì)調(diào)用兩次onChange
// mUri===content://sms/raw/20
// mUri===content://sms/inbox/20
// 安卓7.0以上系統(tǒng),點(diǎn)擊標(biāo)記為已讀,也會(huì)調(diào)用一次
// mUri===content://sms
// 收到一條短信都是uri后面都會(huì)有確定的一個(gè)數(shù)字,對(duì)應(yīng)數(shù)據(jù)庫的_id,比如上面的20
if (uri == null) {
return;
}
if (uri.toString().contains("content://sms/raw") ||
uri.toString().equals("content://sms")) {
return;
}
// 通過內(nèi)容解析器獲取符合條件的結(jié)果集游標(biāo)
Cursor cursor = mContext.getContentResolver().query(uri, new String[]{"address", "body", "date"}, null, null, "date DESC");
if (cursor.moveToNext()) {
// 短信的發(fā)送號(hào)碼
String sender = cursor.getString(cursor.getColumnIndex("address"));
// 短信內(nèi)容
String content = cursor.getString(cursor.getColumnIndex("body"));
Log.d("ning", String.format("sender:%s,content:%s", sender, content));
}
cursor.close();
}
}
}
3. 在應(yīng)用之間共享文件
(1)使用相冊(cè)圖片發(fā)送彩信
(2)借助FileProvider發(fā)送彩信
(3)借助FileProvider安裝應(yīng)用
到了這里,關(guān)于Android基礎(chǔ)教程——從入門到精通(上)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!