Android 架構(gòu)之?dāng)?shù)據(jù)庫(kù)框架搭建
前言:
你還在苦惱的寫(xiě)SQL么?你還在為數(shù)據(jù)庫(kù)升級(jí)而煩惱么?你還在因查詢數(shù)據(jù)而寫(xiě)繁瑣不可用的代碼么? 在這,這些都將不復(fù)存在!在本篇中,將會(huì)讓你一點(diǎn)一滴從無(wú)到有創(chuàng)建一個(gè)不再為數(shù)據(jù)庫(kù)而煩惱的框架。
在開(kāi)始之前我們先欣賞一下本章實(shí)現(xiàn)的最終效果 效果展示 
如圖所示:
- 對(duì)應(yīng)的
model,可直接成為表結(jié)構(gòu),不再寫(xiě)對(duì)應(yīng)的Create table xxx對(duì)應(yīng)的SQL了 - 對(duì)應(yīng)
model的Dao層,里面封裝了數(shù)據(jù)表的基本操作(增刪改查) - 對(duì)應(yīng)的增刪改查操作,再也不用SQL了,全用對(duì)象處理
接下來(lái)開(kāi)始實(shí)戰(zhàn)了
1、先創(chuàng)建對(duì)應(yīng)相關(guān)操作的注解
1.1 bTable 標(biāo)識(shí)表
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface DbTable {
//表名
String value();
}
1.2 DbPrimaryKey 標(biāo)識(shí)主鍵
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DbPrimaryKey {
//表列名
String value();
//是否為自動(dòng)增長(zhǎng)
boolean isAuto() default false;
}
1.3 DbFiled 標(biāo)識(shí)成員屬性
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DbFiled {
//表列名
String value();
/*
這里可以像主鍵一樣,添加其他屬性,比如是否唯一約束,是否非空等
甚至可以將主鍵的約束放在這里來(lái),只是表明可以這樣做,具體怎樣擴(kuò)展,完全可以按你們想法來(lái)
*/
}
2、創(chuàng)建對(duì)應(yīng)表操作類Dao層
2.1 建 待實(shí)現(xiàn)的基層 IBaseDao
public interface IBaseDao<T> {
Long insert(T entity);
int update(T entity, T where);
/**
* 刪除數(shù)據(jù)
*
* @param where
* @return
*/
int delete(T where);
/**
* 查詢數(shù)據(jù)
*/
List<T> query(T where);
List<T> query(T where, String groupBy, String orderBy, String having, Integer startIndex,
Integer limit);
}
代碼分析:
這里創(chuàng)建了基類 IBaseDao ,擁有待實(shí)現(xiàn)的增刪改查, T 代表對(duì)應(yīng)的 數(shù)據(jù)表結(jié)構(gòu)的 model。
2.2 建已實(shí)現(xiàn)的基層 BaseDao
public class BaseDao<T> implements IBaseDao<T> {
private static final String TAG = "hqk";
/**
* 持有數(shù)據(jù)庫(kù)操作類的引用
*/
private SQLiteDatabase database;
/**
* 持有操作數(shù)據(jù)庫(kù)表所對(duì)應(yīng)的java類型
* User
*/
private Class<T> entityClass;
/**
* 保證實(shí)例化一次
*/
private boolean isInit = false;
private String tableName;
// 檢查表
private HashMap<String, Field> cacheMap;
protected BaseDao() {
}
protected synchronized boolean init(Class<T> entity, SQLiteDatabase sqLiteDatabase) {
if (!isInit) {
//初始化完了 自動(dòng)建表
entityClass = entity;
database = sqLiteDatabase;
if (entity.getAnnotation(DbTable.class) == null) {
tableName = entity.getClass().getSimpleName();
} else {
tableName = entity.getAnnotation(DbTable.class).value();
}
if (!database.isOpen()) {
return false;
}
String sql = createTable();
database.execSQL(sql);
//建立好映射關(guān)系
initCacheMap();
isInit = true;
}
return true;
}
/**
* 將真實(shí)表中的列名 + 成員變量進(jìn)行 映射
* 緩存對(duì)應(yīng)的 表 Model里的屬性名以及對(duì)應(yīng)表列名
*/
private void initCacheMap() {
cacheMap = new HashMap<>();
//這里沒(méi)有必要查詢 對(duì)應(yīng)表中的任何數(shù)據(jù),只想要對(duì)應(yīng)表列名,所以 這 limit 0
String sql = "select * from " + tableName + " limit 0";
Cursor cursor = database.rawQuery(sql, null);
String[] columnNames = cursor.getColumnNames();
Field[] columnFields = entityClass.getDeclaredFields();
//獲取對(duì)應(yīng)表中的列名數(shù)組,以及對(duì)應(yīng)表Model里面的屬性數(shù)組
for (String columnName : columnNames) {
Field resultField = null;
for (Field field : columnFields) {
//拿到對(duì)應(yīng)屬性的注解值
String fieldAnnotationName = field.getAnnotation(DbFiled.class).value();
//如果對(duì)應(yīng)的屬性注解值與數(shù)據(jù)庫(kù)表列名相同,則拿到對(duì)應(yīng)屬性值
if (columnName.equals(fieldAnnotationName)) {
resultField = field;
break;
}
}
if (resultField != null) {
cacheMap.put(columnName, resultField);
}
}
}
/**
* 組裝 創(chuàng)建表的SQL語(yǔ)句
*
* @return
*/
private String createTable() {
StringBuffer stringBuffer = new StringBuffer();
//開(kāi)始組裝 SQL語(yǔ)句
stringBuffer.append("create table if not exists ");
stringBuffer.append(tableName + " (");
Field[] fields = entityClass.getDeclaredFields();
for (Field field : fields) {
Class type = field.getType();
String primaryKey = null;
try {
primaryKey = field.getAnnotation(DbPrimaryKey.class).value();
} catch (Exception e) {
}
Log.i(TAG, "createTable primaryKey " + primaryKey);
Log.i(TAG, "createTable type " + type);
if (type == String.class) {
if (null == primaryKey) {
stringBuffer.append(field.getAnnotation(DbFiled.class).value() + " TEXT,");
} else {
stringBuffer.append(field.getAnnotation(DbFiled.class).value() + " TEXT PRIMARY KEY,");
}
} else if (type == Double.class) {
if (null == primaryKey) {
stringBuffer.append(field.getAnnotation(DbFiled.class).value() + " DOUBLE,");
} else {
stringBuffer.append(field.getAnnotation(DbFiled.class).value() + " DOUBLE PRIMARY KEY,");
}
} else if (type == Integer.class) {
if (null == primaryKey) {
stringBuffer.append(field.getAnnotation(DbFiled.class).value() + " INTEGER,");
} else {
boolean isAuto = field.getAnnotation(DbPrimaryKey.class).isAuto();
if (isAuto) {
stringBuffer.append(field.getAnnotation(DbFiled.class).value() + " INTEGER PRIMARY KEY AUTOINCREMENT,");
} else {
stringBuffer.append(field.getAnnotation(DbFiled.class).value() + " INTEGER PRIMARY KEY,");
}
}
} else if (type == Long.class) {
if (null == primaryKey) {
stringBuffer.append(field.getAnnotation(DbFiled.class).value() + " BIGINT,");
} else {
stringBuffer.append(field.getAnnotation(DbFiled.class).value() + " BIGINT PRIMARY KEY,");
}
} else if (type == byte[].class) {
if (null == primaryKey) {
stringBuffer.append(field.getAnnotation(DbFiled.class).value() + " BLOB,");
} else {
stringBuffer.append(field.getAnnotation(DbFiled.class).value() + " BLOB PRIMARY KEY,");
}
} else {
/*
不支持的類型
*/
continue;
}
}
//循環(huán)完成后,最后一項(xiàng)會(huì)有 逗號(hào) ,如果最后一個(gè)是逗號(hào),則刪除最后一個(gè)字符
if (stringBuffer.charAt(stringBuffer.length() - 1) == ',') {
stringBuffer.deleteCharAt(stringBuffer.length() - 1);
}
//SQL 語(yǔ)句 收尾
stringBuffer.append(")");
Log.i(TAG, "createTable: " + stringBuffer.toString());
return stringBuffer.toString();
}
@Override
public Long insert(T entity) {
Map<String, String> map = getValues(entity);
ContentValues contentValues = getContentValues(map);
return database.insert(tableName, null, contentValues);
}
/**
* 獲取對(duì)應(yīng) model 屬性以及對(duì)應(yīng)的注解值(表列名值)
*
* @param entity 對(duì)應(yīng) 表結(jié)構(gòu)的model
* @return 返回 key= 列名,value=屬性的值 map集合
*/
private Map<String, String> getValues(T entity) {
HashMap<String, String> map = new HashMap<>();
//獲取對(duì)應(yīng)緩存 model 里面的屬性鍵
Iterator<Field> fieldIterator = cacheMap.values().iterator();
while (fieldIterator.hasNext()) {
Field field = fieldIterator.next();
field.setAccessible(true);
try {
Object object = field.get(entity);
if (object == null) {
continue;
}
String value = object.toString();
String key = field.getAnnotation(DbFiled.class).value();
//遍歷 取出對(duì)應(yīng) 屬性的值 以及對(duì)應(yīng)的 注解值,并添加至Map里
if (!TextUtils.isEmpty(key) && !TextUtils.isEmpty(value)) {
map.put(key, value);
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
return map;
}
/**
* 數(shù)據(jù)庫(kù)數(shù)據(jù)結(jié)構(gòu)的封裝
*
* @param map 帶有 以表列名為鍵,的map
* @return 數(shù)據(jù)庫(kù)需要的封裝格式
*/
private ContentValues getContentValues(Map<String, String> map) {
ContentValues contentValues = new ContentValues();
Set keys = map.keySet();
Iterator<String> iterator = keys.iterator();
while (iterator.hasNext()) {
String key = iterator.next();
String value = map.get(key);
if (value != null) {
contentValues.put(key, value);
}
}
return contentValues;
}
@Override
public int update(T entity, T where) {
Map values = getValues(entity);
ContentValues contentValues = getContentValues(values);
//條件
Map whereMap = getValues(where);
Condition condition = new Condition(whereMap);
return database.update(tableName, contentValues, condition.whereClause, condition.whereArgs);
}
class Condition {
String whereClause;
String[] whereArgs;
public Condition(Map<String, String> whereClause) {
boolean flag = false;
if (true && flag) {
}
ArrayList list = new ArrayList();
StringBuilder stringBuilder = new StringBuilder();
// 這里之所以先添加 1=1 這個(gè)條件 是因?yàn)?
// SQL where 后面需要給條件判斷,而下面 while 循環(huán) 直接添加了 and
// SQL 語(yǔ)句就變成了 where and 這顯然不符合SQL語(yǔ)句
// 因此 加上 1=1 就變成了 where 1=1 and xx。起了一個(gè)呈上去下的作用
stringBuilder.append("1=1");
Set keys = whereClause.keySet();
Iterator iterator = keys.iterator();
while (iterator.hasNext()) {
String key = (String) iterator.next();
String value = whereClause.get(key);
if (value != null) {
stringBuilder.append(" and " + key + " =?");
list.add(value);
}
}
this.whereClause = stringBuilder.toString();
this.whereArgs = (String[]) list.toArray(new String[list.size()]);
}
}
@Override
public int delete(T where) {
Map map = getValues(where);
Condition condition = new Condition(map);
return database.delete(tableName, condition.whereClause, condition.whereArgs);
}
@Override
public List<T> query(T where) {
return query(where, null, null, null, null, null
);
}
//所有 條件
@Override
public List<T> query(T where, String groupBy, String orderBy, String having,Integer startIndex,
Integer limit) {
String limitString=null;
if(startIndex!=null&&limit!=null)
{
limitString=startIndex+" , "+limit;
}
Map map=getValues(where);
Condition condition=new Condition(map);
Cursor cursor= database.query(tableName, null, condition.whereClause,
condition.whereArgs,
groupBy, having,
orderBy, limitString
);
// 封裝 --返回
List<T> result = getResult(cursor, where);
cursor.close();
return result;
}
private List<T> getResult(Cursor cursor, T where) {
ArrayList list=new ArrayList();
Object item;
while (cursor.moveToNext()) {
try {
// cachmap ---對(duì)象中的成員變量 Filed annotion-- tb_name
//cacheMap name ---Filed 1
// tb_name ---Filed 2
item=where.getClass().newInstance();
Iterator iterator=cacheMap.entrySet().iterator();
while (iterator.hasNext())
{
Map.Entry entry= (Map.Entry) iterator.next();
//tb_name
/**
* 得到列名
*/
String colomunName= (String) entry.getKey();
// 通過(guò)列名查找到游標(biāo)的索性
Integer colmunIndex= cursor.getColumnIndex(colomunName);
// Filed
//反射的成員 cursor
Field field= (Field) entry.getValue();
Class type=field.getType();
if(colmunIndex!=-1)
{
//
if (type == String.class) {
field.set(item, cursor.getString(colmunIndex));
}else if(type==Double.class)
{
field.set(item,cursor.getDouble(colmunIndex));
}else if(type==Integer.class)
{
field.set(item,cursor.getInt(colmunIndex));
}else if(type==Long.class)
{
field.set(item,cursor.getLong(colmunIndex));
}else if(type==byte[].class)
{
field.set(item,cursor.getBlob(colmunIndex));
/*
不支持的類型
*/
}else {
continue;
}
}
}
list.add(item);
} catch ( Exception e) {
e.printStackTrace();
}
}
return list;
}
}
代碼分析:
在這個(gè)BaseDao 里面,幾乎分擔(dān)了數(shù)據(jù)表大部分的臟活累活,根據(jù)model結(jié)構(gòu)自動(dòng)生成對(duì)應(yīng)SQL并創(chuàng)建對(duì)應(yīng)表,以及基礎(chǔ)的增刪改查操作。
2.3 建對(duì)應(yīng)model 的Dao層
1.UserDao
public class UserDao<User> extends BaseDao<User> {
@Override
public Long insert(User entity) {
return super.insert(entity);
}
@Override
public List<User> query(User where) {
return super.query(where);
}
@Override
public int delete(User where) {
return super.delete(where);
}
@Override
public int update(User entity, User where) {
return super.update(entity, where);
}
@Override
public List<User> query(User where, String groupBy, String orderBy, String having, Integer startIndex, Integer limit) {
return super.query(where, groupBy, orderBy, having, startIndex, limit);
}
}
2.PhotoDao
public class PhotoDao<Photo> extends BaseDao<Photo> {
@Override
public Long insert(Photo entity) {
return super.insert(entity);
}
@Override
public int update(Photo entity, Photo where) {
return super.update(entity, where);
}
@Override
public List<Photo> query(Photo where) {
return super.query(where);
}
@Override
public int delete(Photo where) {
return super.delete(where);
}
}
代碼分析:
雖然 BaseDao 已經(jīng)完成了幾乎所有的操作,但是一旦遇到多表查詢的時(shí)候,光是一個(gè)BaseDao遠(yuǎn)遠(yuǎn)不夠。所以這里還是選擇創(chuàng)建不同model的Dao層,并繼承與BaseDao。也就是說(shuō),有多少表,最好就創(chuàng)建對(duì)應(yīng)多少個(gè)Dao層。
3、創(chuàng)建數(shù)據(jù)庫(kù)工廠
public class BaseDaoFactory {
private final String TAG = "hqk";
private SQLiteDatabase sqLiteDatabase;
private String sqliteDatabasePath;
private static BaseDaoFactory instance = new BaseDaoFactory();
//餓漢單例模式
public static BaseDaoFactory getInstance() {
return instance;
}
public BaseDaoFactory() {
//讀者可隨意更改路徑以及對(duì)應(yīng)數(shù)據(jù)庫(kù)名,這里演示暫時(shí)放在根目錄
sqliteDatabasePath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/hqk.db";
sqLiteDatabase = SQLiteDatabase.openOrCreateDatabase(sqliteDatabasePath, null);
Log.i(TAG, "sqliteDatabasePath : " + sqliteDatabasePath);
Log.i(TAG, "sqLiteDatabase : " + sqLiteDatabase.getPath());
}
/**
* @param clazz
* @param entityClass
* @param <R> 我們?cè)谶@可以把它看成某一個(gè)對(duì)象,它繼承與 BaseDao<T> ,而里面的T 就是下面的那個(gè)空對(duì)象
* @param <T> 我們?cè)谶@可以吧它看成某一個(gè)空對(duì)象 T
* @return
*/
public synchronized <R extends BaseDao<T>, T> R createBaseDao(Class<R> clazz, Class<T> entityClass) {
BaseDao baseDao = null;
try {
baseDao = clazz.newInstance();
baseDao.init(entityClass, sqLiteDatabase);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
return (R) baseDao;
}
}
代碼分析:
這里也沒(méi)啥好分析的,就一個(gè)數(shù)據(jù)庫(kù)創(chuàng)建,以及對(duì)應(yīng)model的初始化。唯一值得注意的就是初始化的時(shí)候用了倆個(gè)泛型,具體什么意思,可按照代碼注釋理解。
4、創(chuàng)建對(duì)應(yīng)model
1.User
@DbTable("tb_user")
public class User {
@DbPrimaryKey(value = "tb_id", isAuto = true)
@DbFiled("tb_id")
public Integer id;
@DbFiled("tb_name")
public String name;//
@DbFiled("tb_age")
public Integer age;
public User(String name, Integer age) {
this.name = name;
this.age = age;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public User() {
}
}
2.Photo
@DbTable("tb_photo")
public class Photo {
@DbFiled("time")
private String time;
@DbFiled("id")
private Long id;
@DbFiled("path")
private String path;
public Photo( ) {
}
public Photo(String time, Long id, String path) {
this.time = time;
this.id = id;
this.path = path;
}
public void setTime(String time) {
this.time = time;
}
public void setId(Long id) {
this.id = id;
}
public void setPath(String path) {
this.path = path;
}
}
代碼分析:
這倆類就是對(duì)應(yīng)表結(jié)構(gòu)model 類,用到了對(duì)應(yīng)注解,相信通過(guò)注解能夠清楚知道對(duì)應(yīng)表結(jié)構(gòu)是怎樣的。
5、最終使用
ainActivity
public class MainActivity extends AppCompatActivity {
UserDao<User> userDao;
PhotoDao<Photo> photoDao;
private ArrayList<User> listUser = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
requestPermission(this);
}
public void save(View view) {
User user = new User("hqk", 18);
long size = userDao.insert(user);
Photo photo = new Photo("time", System.currentTimeMillis(), "path");
long photoSize = photoDao.insert(photo);
Toast.makeText(this, "save line : " + size, Toast.LENGTH_LONG).show();
}
public void update(View view) {
User where = new User();
where.setAge(18);
int size = userDao.update(new User("TOM", 99), where);
Toast.makeText(this, "update Size : " + size, Toast.LENGTH_LONG).show();
}
public void delete(View view) {
User where = new User();
where.setAge(18);
int size = userDao.delete(where);
Toast.makeText(this, "delete Size : " + size, Toast.LENGTH_LONG).show();
}
public void queryList(View view) {
listUser.clear();
listUser.addAll(userDao.query(new User()));
Toast.makeText(this, "查詢條數(shù)為:" + listUser.size(), Toast.LENGTH_LONG).show();
}
public void requestPermission(
Activity activity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && ActivityCompat.checkSelfPermission(activity,
Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(activity, new String[]{
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
}, 1);
return;
}
createTable();
}
private void createTable() {
userDao = BaseDaoFactory.getInstance().createBaseDao(UserDao.class, User.class);
photoDao = BaseDaoFactory.getInstance().createBaseDao(PhotoDao.class, Photo.class);
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
createTable();
}
}
到此這篇關(guān)于Android 架構(gòu)之?dāng)?shù)據(jù)庫(kù)框架搭建的文章就介紹到這了,更多相關(guān)Android 架構(gòu)數(shù)據(jù)庫(kù)框架搭建內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android中獲得正在運(yùn)行的程序和系統(tǒng)服務(wù)的方法
這篇文章主要介紹了Android中獲得正在運(yùn)行的程序和系統(tǒng)服務(wù)的方法,分別是對(duì)ActivityManager.RunningAppProcessInfo類和ActivityManager.RunningServiceInfo類的使用,需要的朋友可以參考下2016-02-02
Android自定義控件實(shí)現(xiàn)可左右滑動(dòng)的導(dǎo)航條
這篇文章主要介紹了Android自定義控件實(shí)現(xiàn)可左右滑動(dòng)的導(dǎo)航條,能響應(yīng)快速滑動(dòng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-07-07
Android自定義view實(shí)現(xiàn)圓環(huán)效果實(shí)例代碼
本文通過(guò)實(shí)例代碼給大家介紹了Android自定義view實(shí)現(xiàn)圓環(huán)效果,代碼簡(jiǎn)單易懂,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2018-07-07
android shape實(shí)現(xiàn)陰影或模糊邊效果
這篇文章主要介紹了android shape實(shí)現(xiàn)陰影或模糊邊效果,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-10-10
Android ListView填充數(shù)據(jù)的方法
這篇文章主要介紹了Android ListView填充數(shù)據(jù)的方法的相關(guān)資料,大多數(shù)實(shí)現(xiàn)這樣的功能都是使用XML文件,這里就說(shuō)下不使用布局文件如何實(shí)現(xiàn),需要的朋友可以參考下2017-09-09
Android開(kāi)發(fā)之時(shí)間日期組件用法實(shí)例
這篇文章主要介紹了Android開(kāi)發(fā)之時(shí)間日期組件用法,主要介紹了TimePicker和DatePicker組件,對(duì)于Android程序開(kāi)發(fā)有不錯(cuò)的借鑒價(jià)值,需要的朋友可以參考下2014-08-08
flutter實(shí)現(xiàn)頭部tabTop滾動(dòng)欄
這篇文章主要為大家詳細(xì)介紹了flutter實(shí)現(xiàn)頭部tabTop滾動(dòng)欄,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03
Android 判斷當(dāng)前網(wǎng)絡(luò)是否可用簡(jiǎn)單實(shí)例
這篇文章主要介紹了Android 判斷當(dāng)前網(wǎng)絡(luò)是否可用簡(jiǎn)單實(shí)例的相關(guān)資料,需要的朋友可以參考下2017-06-06
Android實(shí)現(xiàn)壓縮字符串的方法示例
最近在做Android開(kāi)發(fā),遇到了需要壓縮字符串的功能,下面這篇文章主要給大家介紹了Android實(shí)現(xiàn)壓縮字符串的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面跟著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。2017-08-08
快速解決Android適配底部返回鍵等虛擬鍵盤(pán)的問(wèn)題
今天小編就為大家分享一篇快速解決Android適配底部返回鍵等虛擬鍵盤(pán)的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-07-07

