Android开发使用 SQLite 存储数据
一、创建SQLite数据库和表
我们可以通过SQLiteDatabase.openOrCreateDatabase()来创建一个数据库实例。
SQLiteDatabase db = openOrCreateDatabase(dbName, MODE_PRIVATE, null);
// openOrCreateDatabase(String name, int mode, CursorFactory factory)
// 第一个参数为创建数据库的名称
// 第二个参数为创建数据库的权限,其权限同内部文件存储数据权限相同。默认为MODE_PRIVATE。
// 第三个参数为CursorFactory对象,用于查询时返回Cursor的子类对象;或者传入null使用默认的factory构造。
在创建表的时候,我们可以使用一条SQL语句来完成。
cmd = "CREATE TABLE IF NOT EXISTS " + tableName + " (name VARCHAR, passwd VARCHAR)";
db.execSQL(cmd);
这样,我们可以发现在“/data/data/[PACKAGE_NAME]/databases”目录下生成了一个“myDB.db”数据库文件。
完整代码:
import android.database.sqlite.SQLiteDatabase;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
public class MainActivity extends AppCompatActivity {
private Button btnDB;
private Button btnTable;
private String dbName = "myDB";
private String cmd = "";
private SQLiteDatabase db;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btnDB = (Button)findViewById(R.id.btnDB);
btnTable = (Button)findViewById(R.id.btnTable);
btnDB.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
db = openOrCreateDatabase(dbName,MODE_PRIVATE,null);
}
});
btnTable.setOnClickListener(new View.OnClickListener() {
String tableName = "User";
@Override
public void onClick(View view) {
cmd = "CREATE TABLE IF NOT EXISTS " + tableName + " (name VARCHAR, passwd VARCHAR)";
db.execSQL(cmd);
}
});
}
}
二、添加、删除、修改
(1)SQL语句方法
cmd = "INSERT INTO " + tableName + " values ('Amy','123456')";
db.execSQL(cmd);
cmd = "UPDATE " + tableName + " SET passwd='654321' WHERE name='AMY')";
db.execSQL(cmd);
cmd = "DELETE FROM " + tableName + " WHERE name='Amy'";
db.execSQL(cmd);
(2)另一种方法
db.insert(String table, String nullColumnHack, ContentValues values);
db.update(String table, Contentvalues values, String whereClause, String whereArgs);
db.delete(String table, String whereClause, String whereArgs);
以上三个方法的第一个参数都是表示要操作的表名;insert中的第二个参数表示如果插入的数据每一列都为空的话,需要指定此行中某一列的名称,系统将此列设置为NULL,不至于出现错误;insert中的第三个参数是ContentValues类型的变量,是键值对组成的Map,key代表列名,value代表该列要插入的值;update的第二个参数也很类似,只不过它是更新该字段key为最新的value值,第三个参数whereClause表示WHERE表达式,比如“age > ? and age < ?”等,最后的whereArgs参数是占位符的实际参数值;delete方法的参数也是一样。
实例:
btnTable.setOnClickListener(new View.OnClickListener() {
String tableName = "User";
@Override
public void onClick(View view) {
cmd = "CREATE TABLE IF NOT EXISTS " + tableName + " (name VARCHAR, passwd VARCHAR)";
db.execSQL(cmd);
ContentValues cv = new ContentValues();
cv.put("name", "Amy");
cv.put("passwd", "123456");
db.insert(dbName, null, cv);
// cmd = "INSERT INTO " + tableName + " values ('Amy','123456')";
// db.execSQL(cmd);
cv = new ContentValues();
cv.put("passwd", "654321");
db.update(dbName, cv, "name=?", new String[]{"Amy"});
// cmd = "UPDATE " + tableName + " SET passwd='654321' WHERE name='AMY')";
// db.execSQL(cmd);
db.delete(dbName, "name=?", new String[]{"Amy"});
// cmd = "DELETE FROM " + tableName + " WHERE name='Amy'";
// db.execSQL(cmd);
}
});
三、数据库查询
对数据库的查询可以通过db.query()来实现,query方法一般包含8个参数:
db.query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit);
// table为查询表的名称
// columns为查询的字段名
// selection为查询的条件
// selectionArgs为查询条件的值
// groupBy为分组字段值
// having为分组后筛选条件
// orderBy为排序字段名
// limit为查询结果返回记录条数
查询的的结果通过Cursor返回。代表数据集的游标。
Cursor cursor = db.query(tableName, null, null, null, null, null, null);
String str = "";
if(cursor.getCount()!=0){ // 查询符合条件的记录个数
cursor.moveToFirst(); // 移动到第一个记录
for(int i=0; i<cursor.getCount();i++){
str += cursor.getString(0)+" "+cursor.getString(1)+"\n";
cursor.moveToNext(); // 移动到下一个记录
}
}
new AlertDialog.Builder(MainActivity.this).setTitle("Query")
.setMessage(str).setNegativeButton("OK",null).show();
四、SQLiteOpenHelper
除了常规的管理方法之外,Android SDK还提供了另外一种管理数据库的方法,SQLiteOpenHelper。它提供了一套自动执行的机制来创建、更新、打开数据库。
首先,我们继承SQLiteOpenHelper类,创建MyDBHelper类。
public class MyDBHelper extends SQLiteOpenHelper {
private String tableName = "User";
public MyDBHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
super(context, name, factory, version);
}
public void onCreate(SQLiteDatabase db) {
String cmd = "CREATE TABLE IF NOT EXISTS " + tableName + " (name VARCHAR, passwd VARCHAR)";
db.execSQL(cmd);
}
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
String cmd = "UPDATE " + tableName + " SET passwd='654321' WHERE name='AMY')";
db.execSQL(cmd);
}
public String showTable(){
SQLiteDatabase db = this.getReadableDatabase();
Cursor cursor = db.query(tableName, null, null, null, null, null, null);
String str = "";
if(cursor.getCount()!=0){ // 查询符合条件的记录个数
cursor.moveToFirst(); // 移动到第一个记录
for(int i=0; i<cursor.getCount();i++){
str += cursor.getString(0)+" "+cursor.getString(1)+"\n";
cursor.moveToNext(); // 移动到下一个记录
}
}
return str;
}
}
然后,我们便可以在MainActivity内使用我们定义的SQLiteOpenHelper类的方法。
btnShow = (Button) findViewById(R.id.btnShow);
btnShow.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
dbHelper = new MyDBHelper(MainActivity.this, dbName, null, 1);
new AlertDialog.Builder(MainActivity.this).setTitle("MyDBHelper")
.setMessage(dbHelper.showTable()).setNegativeButton("OK", null).show();
}
});
完整代码如下:
MainActivity.java
import android.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
public class MainActivity extends AppCompatActivity {
private Button btnShow;
private String dbName = "myDB";
private MyDBHelper dbHelper;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btnShow = (Button) findViewById(R.id.btnShow);
btnShow.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
dbHelper = new MyDBHelper(MainActivity.this, dbName, null, 1);
new AlertDialog.Builder(MainActivity.this).setTitle("MyDBHelper")
.setMessage(dbHelper.showTable()).setNegativeButton("OK", null).show();
}
});
}
}
MyDBHelper.java
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
public class MyDBHelper extends SQLiteOpenHelper {
private String tableName = "User";
public MyDBHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
super(context, name, factory, version);
}
public void onCreate(SQLiteDatabase db) {
String cmd = "CREATE TABLE IF NOT EXISTS " + tableName + " (name VARCHAR, passwd VARCHAR)";
db.execSQL(cmd);
}
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
String cmd = "UPDATE " + tableName + " SET passwd='654321' WHERE name='AMY')";
db.execSQL(cmd);
}
public String showTable(){
SQLiteDatabase db = this.getReadableDatabase();
Cursor cursor = db.query(tableName, null, null, null, null, null, null);
String str = "";
if(cursor.getCount()!=0){ // 查询符合条件的记录个数
cursor.moveToFirst(); // 移动到第一个记录
for(int i=0; i<cursor.getCount();i++){
str += cursor.getString(0)+" "+cursor.getString(1)+"\n";
cursor.moveToNext(); // 移动到下一个记录
}
}
return str;
}
}
另一篇 android开发之使用SQLite数据库存储
Android 集成了 SQLite 数据库
Android 在运行时(run-time)集成了 SQLite,所以每个 Android 应用程序都可以使用 SQLite 数据库。对于熟悉 SQL 的开发人员来时,在 Android 开发中使用 SQLite 相当简单。但是,由于 JDBC 会消耗太多的系统资源,所以 JDBC 对于手机这种内存受限设备来说并不合适。因此,Android 提供了一些新的 API 来使用 SQLite 数据库,Android 开发中,程序员需要学使用这些 API。
数据库存储在 data/< 项目文件夹 >/databases/ 下。
Android 开发中使用 SQLite 数据库
Activites 可以通过 Content Provider 或者 Service 访问一个数据库。下面会详细讲解如果创建数据库,添加数据和查询数据库。
创建数据库
Android 不自动提供数据库。在 Android 应用程序中使用 SQLite,必须自己创建数据库,然后创建表、索引,填充数据。Android 提供了 SQLiteOpenHelper 帮助你创建一个数据库,你只要继承 SQLiteOpenHelper 类,就可以轻松的创建数据库。SQLiteOpenHelper 类根据开发应用程序的需要,封装了创建和更新数据库使用的逻辑。SQLiteOpenHelper 的子类,至少需要实现三个方法:
构造函数,调用父类 SQLiteOpenHelper 的构造函数。这个方法需要四个参数:上下文环境(例如,一个 Activity),数据库名字,一个可选的游标工厂(通常是 Null),一个代表你正在使用的数据库模型版本的整数。
onCreate()方法,它需要一个 SQLiteDatabase 对象作为参数,根据需要对这个对象填充表和初始化数据。
onUpgrage() 方法,它需要三个参数,一个 SQLiteDatabase 对象,一个旧的版本号和一个新的版本号,这样你就可以清楚如何把一个数据库从旧的模型转变到新的模型。
下面示例代码展示了如何继承 SQLiteOpenHelper 创建数据库:
public class DatabaseHelper extends SQLiteOpenHelper {
DatabaseHelper(Context context, String name, CursorFactory cursorFactory, int version)
{
super(context, name, cursorFactory, version);
}
@Override
public void onCreate(SQLiteDatabase db) {
// TODO 创建数据库后,对数据库的操作
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// TODO 更改数据库版本的操作
}
@Override
public void onOpen(SQLiteDatabase db) {
super.onOpen(db);
// TODO 每次成功打开数据库后首先被执行
}
}
接下来讨论具体如何创建表、插入数据、删除表等等。调用 getReadableDatabase() 或 getWriteableDatabase() 方法,你可以得到 SQLiteDatabase 实例,具体调用那个方法,取决于你是否需要改变数据库的内容:
db=(new DatabaseHelper(getContext())).getWritableDatabase();
return (db == null) ? false : true;
上面这段代码会返回一个 SQLiteDatabase 类的实例,使用这个对象,你就可以查询或者修改数据库。
当你完成了对数据库的操作(例如你的 Activity 已经关闭),需要调用 SQLiteDatabase 的 Close() 方法来释放掉数据库连接。
创建表和索引
为了创建表和索引,需要调用 SQLiteDatabase 的 execSQL() 方法来执行 DDL 语句。如果没有异常,这个方法没有返回值。
例如,你可以执行如下代码:
db.execSQL("CREATE TABLE mytable (_id INTEGER PRIMARY KEY
AUTOINCREMENT, title TEXT, value REAL);");
这条语句会创建一个名为 mytable 的表,表有一个列名为 _id,并且是主键,这列的值是会自动增长的整数(例如,当你插入一行时,SQLite 会给这列自动赋值),另外还有两列:title( 字符 ) 和 value( 浮点数 )。 SQLite 会自动为主键列创建索引。
通常情况下,第一次创建数据库时创建了表和索引。如果你不需要改变表的 schema,不需要删除表和索引 . 删除表和索引,需要使用 execSQL() 方法调用 DROP INDEX 和 DROP TABLE 语句。
给表添加数据
上面的代码,已经创建了数据库和表,现在需要给表添加数据。有两种方法可以给表添加数据。
像上面创建表一样,你可以使用 execSQL() 方法执行 INSERT, UPDATE, DELETE 等语句来更新表的数据。execSQL() 方法适用于所有不返回结果的 SQL 语句。例如:
db.execSQL("INSERT INTO widgets (name, inventory)"+
VALUES ('Sprocket', 5)");
另一种方法是使用 SQLiteDatabase 对象的 insert(), update(), delete() 方法。这些方法把 SQL 语句的一部分作为参数。示例如下:
ContentValues cv=new ContentValues();
cv.put(Constants.TITLE, "example title");
cv.put(Constants.VALUE, SensorManager.GRAVITY_DEATH_STAR_I);
db.insert("mytable", getNullColumnHack(), cv);
update()方法有四个参数,分别是表名,表示列名和值的 ContentValues 对象,可选的 WHERE 条件和可选的填充 WHERE 语句的字符串,这些字符串会替换 WHERE 条件中的“?”标记。update() 根据条件,更新指定列的值,所以用 execSQL() 方法可以达到同样的目的。
WHERE 条件和其参数和用过的其他 SQL APIs 类似。例如:
String[] parms=new String[] {"this is a string"};
db.update("widgets", replacements, "name=?", parms);
delete() 方法的使用和 update() 类似,使用表名,可选的 WHERE 条件和相应的填充 WHERE 条件的字符串。
查询数据库
类似 INSERT, UPDATE, DELETE,有两种方法使用 SELECT 从 SQLite 数据库检索数据。
1 .使用 rawQuery() 直接调用 SELECT 语句;
使用 query() 方法构建一个查询。
Raw Queries
正如 API 名字,rawQuery() 是最简单的解决方法。通过这个方法你就可以调用 SQL SELECT 语句。例如:
Cursor c=db.rawQuery(
"SELECT name FROM sqlite_master WHERE type='table' AND name='mytable'", null);
在上面例子中,我们查询 SQLite 系统表(sqlite_master)检查 table 表是否存在。返回值是一个 cursor 对象,这个对象的方法可以迭代查询结果。
如果查询是动态的,使用这个方法就会非常复杂。例如,当你需要查询的列在程序编译的时候不能确定,这时候使用 query() 方法会方便很多。
Regular Queries
query() 方法用 SELECT 语句段构建查询。SELECT 语句内容作为 query() 方法的参数,比如:要查询的表名,要获取的字段名,WHERE 条件,包含可选的位置参数,去替代 WHERE 条件中位置参数的值,GROUP BY 条件,HAVING 条件。
除了表名,其他参数可以是 null。所以,以前的代码段可以可写成:
String[] columns={"ID", "inventory"};
String[] parms={"snicklefritz"};
Cursor result=db.query("widgets", columns, "name=?",parms, null, null, null);
使用游标
不管你如何执行查询,都会返回一个 Cursor,这是 Android 的 SQLite 数据库游标,使用游标,你可以:
通过使用 getCount() 方法得到结果集中有多少记录;
通过 moveToFirst(), moveToNext(), 和 isAfterLast() 方法遍历所有记录;
通过 getColumnNames() 得到字段名;
通过 getColumnIndex() 转换成字段号;
通过 getString(),getInt() 等方法得到给定字段当前记录的值;
通过 requery() 方法重新执行查询得到游标;
通过 close() 方法释放游标资源;
例如,下面代码遍历 mytable 表
Cursor result=db.rawQuery("SELECT ID, name, inventory FROM mytable");
result.moveToFirst();
while (!result.isAfterLast()) {
int id=result.getInt(0);
String name=result.getString(1);
int inventory=result.getInt(2);
// do something useful with these
result.moveToNext();
}
result.close();
结束语
如果你想要开发 Android 应用程序,一定需要在 Android 上存储数据,使用 SQLite 数据库是一种非常好的选择。本文介绍了如何在 Android 应用程序中使用 SQLite 数据库 ,主要介绍了在 Android 应用程序中使用 SQLite 创建数据库和表、添加数据、更新和检索数据,还介绍了比较常用的 SQLite 管理工具,通过阅读本文,你可以在 Android 中轻松操作 SQLite 数据库。
一、认识Android的IPC主板模式
系统架构设计最关键的任务就是组合(或称整合),而且最好是能与众不同、深具创新性组合。Android就擅用了主板模式,以通用性接口实践跨进程的IPC通信机制。由于Android是开源开放的系统,其源代码可成为大家观摩的范本。首先,其主板模式提供了IBinder通用性接口。如下图:
Android定义一个Binder父类来实现<通用性>的IBinder接口。如下图:
然后,以Java来撰写这个实现类,其Java代码如下:
// Android的源代码 // Binder.java // ------------------------------------------------------------- public class Binder implements IBinder { // .......... private int mObject; public Binder() { init(); // 其它代码 } public final boolean transact(int code, Parcel data, Parcel reply, int flags) throws RemoteException { // 其它代码 boolean r = onTransact(code, data, reply, flags); return r; } private boolean execTransact(int code, int dataObj, int replyObj, int flags) { Parcel data = Parcel.obtain(dataObj); Parcel reply = Parcel.obtain(replyObj); boolean res; res = onTransact(code, data, reply, flags); // 其它代码 return res; } protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException { } private native final void init(); } // End
这个Binder抽象父类的主要函数:
transact()函数-- 用来实作IBinder的transact()函数接口。
execTransact()函数-- 其角色与transact()函数是相同的,只是这是用来让C/C++本地程序来调用的。
onTransact()函数-- 这是一个抽象函数,让应用子类来覆写(Override)的。上述的transact()和execTransact()两者都是调用onTransact()函数来实现反向调用(IoC, Inversion of Control)的。
init()函数-- 这是一个本地(Native)函数,让JNI模块来实现这个函数。Binder()构造函数(Constructor)会调用这个init()本地函数。
这Binder.java是抽象类,它含有一个抽象)函数:onTransact()。于是,这个软件主板提供了两个接口:CI和接口。如下图:
这是标准型的主板模式。此图里的Binder抽象父类和两个接口,整合起来成为一个典型的软件主板。如下图:
这个Binder软件主板是用来整合两个进程里的软件模块(如类),所以我们称之为:。如下图:
基于这个主板,我们就能开始进行组合了。此时,可设计一个子类,并且装配到主板的接口上。如下图:
图 1 Binder进程间通信模型
Client和Server均通过函数ioctl与Binder驱动进行数据交互。ioctl是Linux中用于控制I/O设备的函数,提供了一种同时向设备发送控制参数和数据的手段。它是一个可变参数的函数,原型为:
<??#65533;"http://www.2cto.com/kf/ware/vc/" target="_blank" class="keylink">vcD4KPHByZSBjbGFzcz0="brush:java;">int ioctl(int fd, int cmd, ...);
fd是打开/dev/binder设备后得到的文件描述符,cmd是对设备的控制命令。该函数执行成功返回0,否则返回-1。
3.相关数据结构
当函数ioctl的第二个参数cmd为BINDER_WRITE_READ时,表示向Binder驱动发送一条读取或者写入/dev/binder设备的命令,Binder驱动会将对设备的读写“翻译”为对共享
内存区的读写。这条命令是Client和Server进行进程间通信时最重要、使用最频繁的控制命令。
传入BINDER_WRITE_READ的同时,会传入一个binder_write_read结构体的指针作为ioctl的第三个参数,该结构中的read_buffer和write_buffer字段分别指向将要读取或者写入的缓冲区。这两个缓冲区中的数据都是以“数据类型+数据内容”的格式顺序存放的,而且多条不同类型的数据连续存放,如图2所示。write_buffer中数据类型以“BC_”开头,而read_buffer中数据类型以 “BR_”开头,图2中以write_buffer中的数据为例。在所有的数据类型中,又以BC(R)_REPLY和BC(R)_TRANSACTION 最为重要:通过BC_TRANSACTION/BC_REPLY这对命令,发送方将数据发往接受方;通过BR_TRANSACTION /BR_REPLY,接收方读取发送方发来的数据。
数据的内容是一个binder_transaction_data结构。
图2 Binder IPC中各数据结构的关系
binder_transaction_data结构是对进程间通信数据的封装,可以看作网络通信中的一个数据包。其中的 sender_uid,sender_pid成员变量指明了此数据发送方的用户ID和进程ID,buffer成员变量指向进程间通信最核心的有效负载数据,data_size是有效负载数据的长度。在Binder机制中,sender_uid和sender_pid是在内核中由Binder驱动填入的,无法被伪造,保证了身份标记的可靠性,由此可见Binder进程间通信机制进行是安全的。
4.Binder之间的数据行为关系
Client和Server使用Binder机制进行进程间通信时,通过分析Client发往Server的数据或者分析Server读取的Client的请求数据,便可以识别出Client的具体行为。
例如,当Client想要得到定位信息,请求LocationServer获取定位数据时,会访问LocationServer的ILocationManager接口,发往LocationServer中的有效负载数
据中包含“android.location.ILocationManager”字符串。所以我们分析LocationServer读取的Client发来的请求数据,判断其中是否包含“android.location.ILocationManager”,
我们就可以知道Client是否正在试图访问用户的地理位置信息。
由于Android系统中每个应用程序都有自己唯一的UID,因此根据binder_transaction_data中的sender_uid,我们就可以获取Client具体代表的应用程序。这样
就获得了具体软件的具体行为。
通过对Android系统的分析,我们发现:虽然系统提供的服务多达几十种,但是实际上只有三个Server进程负责管理
Android系统Server进程与管理的服务
服务进程 管理的服务
com.android.phone 与通信功能相关的短信、电话服务
mediaserver 与媒体功能相关的视频、音频服务
system_server 其他服务,如地理位置、蓝牙、网络连接、程序安装卸载等
SharedPreferences简介
SharedPreferences是Android平台上一个轻量级的存储类,用来保存应用的一些常用配置,比如Activity状态,Activity暂停时,将此activity的状态保存到SharedPereferences中;当Activity重载,系统回调方法onSaveInstanceState时,再从SharedPreferences中将值取出。
SharedPreferences提供了java常规的Long、Int、String等类型数据的保存接口。
SharedPreferences类似过去Windows系统上的ini配置文件,但是它分为多种权限,可以全局共享访问。
提示最终是以xml方式来保存,整体效率来看不是特别的高,对于常规的轻量级而言比SQLite要好不少,如果真的存储量不大可以考虑自己定义文件格式。xml处理时Dalvik会通过自带底层的本地XML Parser解析,比如XMLpull方式,这样对于内存资源占用比较好。
操作模式
SharedPreferences数据的四种操作模式
Context.MODE_PRIVATE
Context.MODE_APPEND
Context.MODE_WORLD_READABLE
Context.MODE_WORLD_WRITEABLE
Context.MODE_PRIVATE:为默认操作模式,代表该文件是私有数据,只能被应用本身访问,在该模式下,写入的内容会覆盖原文件的内容
Context.MODE_APPEND:模式会检查文件是否存在,存在就往文件追加内容,否则就创建新文件.
Context.MODE_WORLD_READABLE和Context.MODE_WORLD_WRITEABLE用来控制其他应用是否有权限读写该文件.
MODE_WORLD_READABLE:表示当前文件可以被其他应用读取.
MODE_WORLD_WRITEABLE:表示当前文件可以被其他应用写入
SharedPreferences是Android中最容易理解的数据存储技术,实际上SharedPreferences处理的就是一个key-value(键值对)SharedPreferences常用来存储一些轻量级的数据。这类似于C++中Map的数据存储方式(实际上在最后生成的.xml文件内,就是以Map格式存储的)。
获取SharedPreferences的两种方式:
1、调用Context对象的getSharedPreferences()方法
2、调用Activity对象的getPreferences()方法
两种方式的区别:
调用Context对象的getSharedPreferences()方法获得的SharedPreferences对象可以被同一应用程序下的其他组件共享。
调用Activity对象的getPreferences()方法获得的SharedPreferences对象只能在该Activity中使用。
其中,getSharedPreferences()的方法原型为:
getSharedPreferences(String name, int mode);
// name: 生成xml文件的名称
// MODE_PRIVATE: 为默认操作模式,代表该文件是私有数据,只能被应用本身访问,在该模式下,写入的内容会覆盖原文件的内容
// MODE_APPEND: 模式会检查文件是否存在,存在就往文件追加内容,否则就创建新文件.
// MODE_WORLD_READABLE: 表示当前文件可以被其他应用读取,不推荐使用
// MODE_WORLD_WRITEABLE: 表示当前文件可以被其他应用写入,不推荐使用
使用SharedPreferences存储数据的方法如下:
//实例化SharedPreferences对象(第一步)
SharedPreferences sp = getSharedPreferences("test", MODE_PRIVATE);
//实例化SharedPreferences.Editor对象(第二步)
SharedPreferences.Editor editor = mySharedPreferences.edit();
//用putString的方法保存数据
editor.putString("UserName", etName.getText().toString());
editor.putString("Password", etPassword.getText().toString());
//提交当前数据
//editor.apply();
editor.commit();
//使用toast信息提示框提示成功写入数据
Toast.makeText(MainActivity.this, "注册成功", Toast.LENGTH_LONG).show();
使用SharedPreferences读取数据的方法如下:
SharedPreferences sp = getSharedPreferences(strLogInfo, MODE_APPEND);
String name = sp.getString("UserName", "");
String passwd = sp.getString("Password","");
if(etName.getText().toString().equals(name) && etPassword.getText().toString().equals(passwd)){
Toast.makeText(MainActivity.this, "登陆成功", Toast.LENGTH_LONG).show();
} else{
Toast.makeText(MainActivity.this, "登录失败", Toast.LENGTH_LONG).show();
}
在使用SharedPreferences之后,程序会在“/data/data/包名/shared_prefs/xxx.xml”生成的一个XML文件。文件名取决于getSharedPreferences的第一个参数名。
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="UserName">Name</string>
<string name="Password">Password</string>
</map>
完整代码如下:
import android.content.SharedPreferences;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
private EditText etName;
private EditText etPassword;
private Button btnLogin;
private Button btnLogup;
private String strLogInfo = "test";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
etName = (EditText)findViewById(R.id.etName);
etPassword = (EditText)findViewById(R.id.etPassword);
btnLogin = (Button)findViewById(R.id.btnLogin);
btnLogup = (Button)findViewById(R.id.btnLogup);
btnLogup.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
SharedPreferences sp = getSharedPreferences(strLogInfo, MODE_APPEND);
SharedPreferences.Editor editor = sp.edit();
editor.putString("UserName", etName.getText().toString());
editor.putString("Password", etPassword.getText().toString());
editor.commit();
Toast.makeText(MainActivity.this, "注册成功", Toast.LENGTH_LONG).show();
}
});
btnLogin.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
SharedPreferences sp = getSharedPreferences(strLogInfo, MODE_APPEND);
String name = sp.getString("UserName", "");
String passwd = sp.getString("Password","");
if(etName.getText().toString().equals(name) && etPassword.getText().toString().equals(passwd)){
Toast.makeText(MainActivity.this, "登陆成功", Toast.LENGTH_LONG).show();
} else{
Toast.makeText(MainActivity.this, "登录失败", Toast.LENGTH_LONG).show();
}
}
});
}
}
其实Android提供Intent让我们打开系统的相机,但是系统相机跟自己app风格不搭,而且用起来体验不好。所以我使用了SDK提供的camera API自定义了一个相机,并且在相机界面上面添加了参考线,有助于用户将题目拍正,提高ocr的识别率。
1、绘制参考线的代码
public class ReferenceLine extends View {
private Paint mLinePaint;
public ReferenceLine(Context context) {
super(context);
init();
}
public ReferenceLine(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public ReferenceLine(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mLinePaint = new Paint();
mLinePaint.setAntiAlias(true);
mLinePaint.setColor(Color.parseColor("#45e0e0e0"));
mLinePaint.setStrokeWidth(1);
}
@Override
protected void onDraw(Canvas canvas) {
int screenWidth = Utils.getScreenWH(getContext()).widthPixels;
int screenHeight = Utils.getScreenWH(getContext()).heightPixels;
int width = screenWidth/3;
int height = screenHeight/3;
for (int i = width, j = 0;i < screenWidth && j<2;i += width, j++) {
canvas.drawLine(i, 0, i, screenHeight, mLinePaint);
}
for (int j = height,i = 0;j < screenHeight && i < 2;j += height,i++) {
canvas.drawLine(0, j, screenWidth, j, mLinePaint);
}
}
}
2、自定义相机代码
这里主要是要创建一个SurfaceView,将摄像头的预览界面放到SurfaceView中显示。
package com.bbk.lling.camerademo.camare;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.hardware.Camera;
import android.hardware.Camera.AutoFocusCallback;
import android.hardware.Camera.PictureCallback;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.widget.RelativeLayout;
import android.widget.Toast;
import com.bbk.lling.camerademo.utils.Utils;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* @Class: CameraPreview
* @Description: 自定义相机
* @author: lling(www.cnblogs.com/liuling)
* @Date: 2015/10/25
*/
public class CameraPreview extends SurfaceView implements
SurfaceHolder.Callback, AutoFocusCallback {
private static final String TAG = "CameraPreview";
private int viewWidth = 0;
private int viewHeight = 0;
/** 监听接口 */
private OnCameraStatusListener listener;
private SurfaceHolder holder;
private Camera camera;
private FocusView mFocusView;
//创建一个PictureCallback对象,并实现其中的onPictureTaken方法
private PictureCallback pictureCallback = new PictureCallback() {
// 该方法用于处理拍摄后的照片数据
@Override
public void onPictureTaken(byte[] data, Camera camera) {
// 停止照片拍摄
try {
camera.stopPreview();
} catch (Exception e) {
}
// 调用结束事件
if (null != listener) {
listener.onCameraStopped(data);
}
}
};
// Preview类的构造方法
public CameraPreview(Context context, AttributeSet attrs) {
super(context, attrs);
// 获得SurfaceHolder对象
holder = getHolder();
// 指定用于捕捉拍照事件的SurfaceHolder.Callback对象
holder.addCallback(this);
// 设置SurfaceHolder对象的类型
holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
setOnTouchListener(onTouchListener);
}
// 在surface创建时激发
public void surfaceCreated(SurfaceHolder holder) {
Log.e(TAG, "==surfaceCreated==");
if(!Utils.checkCameraHardware(getContext())) {
Toast.makeText(getContext(), "摄像头打开失败!", Toast.LENGTH_SHORT).show();
return;
}
// 获得Camera对象
camera = getCameraInstance();
try {
// 设置用于显示拍照摄像的SurfaceHolder对象
camera.setPreviewDisplay(holder);
} catch (IOException e) {
e.printStackTrace();
// 释放手机摄像头
camera.release();
camera = null;
}
updateCameraParameters();
if (camera != null) {
camera.startPreview();
}
setFocus();
}
// 在surface销毁时激发
public void surfaceDestroyed(SurfaceHolder holder) {
Log.e(TAG, "==surfaceDestroyed==");
// 释放手机摄像头
camera.release();
camera = null;
}
// 在surface的大小发生改变时激发
public void surfaceChanged(final SurfaceHolder holder, int format, int w,
int h) {
// stop preview before making changes
try {
camera.stopPreview();
} catch (Exception e){
// ignore: tried to stop a non-existent preview
}
// set preview size and make any resize, rotate or
// reformatting changes here
updateCameraParameters();
// start preview with new settings
try {
camera.setPreviewDisplay(holder);
camera.startPreview();
} catch (Exception e){
Log.d(TAG, "Error starting camera preview: " + e.getMessage());
}
setFocus();
}
/**
* 点击显示焦点区域
*/
OnTouchListener onTouchListener = new OnTouchListener() {
@SuppressWarnings("deprecation")
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
int width = mFocusView.getWidth();
int height = mFocusView.getHeight();
mFocusView.setX(event.getX() - (width / 2));
mFocusView.setY(event.getY() - (height / 2));
mFocusView.beginFocus();
} else if (event.getAction() == MotionEvent.ACTION_UP) {
focusOnTouch(event);
}
return true;
}
};
/**
* 获取摄像头实例
* @return
*/
private Camera getCameraInstance() {
Camera c = null;
try {
int cameraCount = 0;
Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
cameraCount = Camera.getNumberOfCameras(); // get cameras number
for (int camIdx = 0; camIdx < cameraCount; camIdx++) {
Camera.getCameraInfo(camIdx, cameraInfo); // get camerainfo
// 代表摄像头的方位,目前有定义值两个分别为CAMERA_FACING_FRONT前置和CAMERA_FACING_BACK后置
if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {
try {
c = Camera.open(camIdx); //打开后置摄像头
} catch (RuntimeException e) {
Toast.makeText(getContext(), "摄像头打开失败!", Toast.LENGTH_SHORT).show();
}
}
}
if (c == null) {
c = Camera.open(0); // attempt to get a Camera instance
}
} catch (Exception e) {
Toast.makeText(getContext(), "摄像头打开失败!", Toast.LENGTH_SHORT).show();
}
return c;
}
private void updateCameraParameters() {
if (camera != null) {
Camera.Parameters p = camera.getParameters();
setParameters(p);
try {
camera.setParameters(p);
} catch (Exception e) {
Camera.Size previewSize = findBestPreviewSize(p);
p.setPreviewSize(previewSize.width, previewSize.height);
p.setPictureSize(previewSize.width, previewSize.height);
camera.setParameters(p);
}
}
}
/**
* @param p
*/
private void setParameters(Camera.Parameters p) {
List<String> focusModes = p.getSupportedFocusModes();
if (focusModes
.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
p.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
}
long time = new Date().getTime();
p.setGpsTimestamp(time);
// 设置照片格式
p.setPictureFormat(PixelFormat.JPEG);
Camera.Size previewSize = findPreviewSizeByScreen(p);
p.setPreviewSize(previewSize.width, previewSize.height);
p.setPictureSize(previewSize.width, previewSize.height);
p.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
if (getContext().getResources().getConfiguration().orientation != Configuration.ORIENTATION_LANDSCAPE) {
camera.setDisplayOrientation(90);
p.setRotation(90);
}
}
// 进行拍照,并将拍摄的照片传入PictureCallback接口的onPictureTaken方法
public void takePicture() {
if (camera != null) {
try {
camera.takePicture(null, null, pictureCallback);
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 设置监听事件
public void setOnCameraStatusListener(OnCameraStatusListener listener) {
this.listener = listener;
}
@Override
public void onAutoFocus(boolean success, Camera camera) {
}
public void start() {
if (camera != null) {
camera.startPreview();
}
}
public void stop() {
if (camera != null) {
camera.stopPreview();
}
}
/**
* 相机拍照监听接口
*/
public interface OnCameraStatusListener {
// 相机拍照结束事件
void onCameraStopped(byte[] data);
}
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
viewWidth = MeasureSpec.getSize(widthSpec);
viewHeight = MeasureSpec.getSize(heightSpec);
super.onMeasure(
MeasureSpec.makeMeasureSpec(viewWidth, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(viewHeight, MeasureSpec.EXACTLY));
}
/**
* 将预览大小设置为屏幕大小
* @param parameters
* @return
*/
private Camera.Size findPreviewSizeByScreen(Camera.Parameters parameters) {
if (viewWidth != 0 && viewHeight != 0) {
return camera.new Size(Math.max(viewWidth, viewHeight),
Math.min(viewWidth, viewHeight));
} else {
return camera.new Size(Utils.getScreenWH(getContext()).heightPixels,
Utils.getScreenWH(getContext()).widthPixels);
}
}
/**
* 找到最合适的显示分辨率 (防止预览图像变形)
* @param parameters
* @return
*/
private Camera.Size findBestPreviewSize(Camera.Parameters parameters) {
// 系统支持的所有预览分辨率
String previewSizeValueString = null;
previewSizeValueString = parameters.get("preview-size-values");
if (previewSizeValueString == null) {
previewSizeValueString = parameters.get("preview-size-value");
}
if (previewSizeValueString == null) { // 有些手机例如m9获取不到支持的预览大小 就直接返回屏幕大小
return camera.new Size(Utils.getScreenWH(getContext()).widthPixels,
Utils.getScreenWH(getContext()).heightPixels);
}
float bestX = 0;
float bestY = 0;
float tmpRadio = 0;
float viewRadio = 0;
if (viewWidth != 0 && viewHeight != 0) {
viewRadio = Math.min((float) viewWidth, (float) viewHeight)
/ Math.max((float) viewWidth, (float) viewHeight);
}
String[] COMMA_PATTERN = previewSizeValueString.split(",");
for (String prewsizeString : COMMA_PATTERN) {
prewsizeString = prewsizeString.trim();
int dimPosition = prewsizeString.indexOf('x');
if (dimPosition == -1) {
continue;
}
float newX = 0;
float newY = 0;
try {
newX = Float.parseFloat(prewsizeString.substring(0, dimPosition));
newY = Float.parseFloat(prewsizeString.substring(dimPosition + 1));
} catch (NumberFormatException e) {
continue;
}
float radio = Math.min(newX, newY) / Math.max(newX, newY);
if (tmpRadio == 0) {
tmpRadio = radio;
bestX = newX;
bestY = newY;
} else if (tmpRadio != 0 && (Math.abs(radio - viewRadio)) < (Math.abs(tmpRadio - viewRadio))) {
tmpRadio = radio;
bestX = newX;
bestY = newY;
}
}
if (bestX > 0 && bestY > 0) {
return camera.new Size((int) bestX, (int) bestY);
}
return null;
}
/**
* 设置焦点和测光区域
*
* @param event
*/
public void focusOnTouch(MotionEvent event) {
int[] location = new int[2];
RelativeLayout relativeLayout = (RelativeLayout)getParent();
relativeLayout.getLocationOnScreen(location);
Rect focusRect = Utils.calculateTapArea(mFocusView.getWidth(),
mFocusView.getHeight(), 1f, event.getRawX(), event.getRawY(),
location[0], location[0] + relativeLayout.getWidth(), location[1],
location[1] + relativeLayout.getHeight());
Rect meteringRect = Utils.calculateTapArea(mFocusView.getWidth(),
mFocusView.getHeight(), 1.5f, event.getRawX(), event.getRawY(),
location[0], location[0] + relativeLayout.getWidth(), location[1],
location[1] + relativeLayout.getHeight());
Camera.Parameters parameters = camera.getParameters();
parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
if (parameters.getMaxNumFocusAreas() > 0) {
List<Camera.Area> focusAreas = new ArrayList<Camera.Area>();
focusAreas.add(new Camera.Area(focusRect, 1000));
parameters.setFocusAreas(focusAreas);
}
if (parameters.getMaxNumMeteringAreas() > 0) {
List<Camera.Area> meteringAreas = new ArrayList<Camera.Area>();
meteringAreas.add(new Camera.Area(meteringRect, 1000));
parameters.setMeteringAreas(meteringAreas);
}
try {
camera.setParameters(parameters);
} catch (Exception e) {
}
camera.autoFocus(this);
}
/**
* 设置聚焦的图片
* @param focusView
*/
public void setFocusView(FocusView focusView) {
this.mFocusView = focusView;
}
/**
* 设置自动聚焦,并且聚焦的圈圈显示在屏幕中间位置
*/
public void setFocus() {
if(!mFocusView.isFocusing()) {
try {
camera.autoFocus(this);
mFocusView.setX((Utils.getWidthInPx(getContext())-mFocusView.getWidth()) / 2);
mFocusView.setY((Utils.getHeightInPx(getContext())-mFocusView.getHeight()) / 2);
mFocusView.beginFocus();
} catch (Exception e) {
}
}
}
}
3、Activity中使用自定义相机
public class TakePhoteActivity extends Activity implements CameraPreview.OnCameraStatusListener,
SensorEventListener {
private static final String TAG = "TakePhoteActivity";
public static final Uri IMAGE_URI = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
public static final String PATH = Environment.getExternalStorageDirectory()
.toString() + "/AndroidMedia/";
CameraPreview mCameraPreview;
CropImageView mCropImageView;
RelativeLayout mTakePhotoLayout;
LinearLayout mCropperLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 设置横屏
// setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
// 设置全屏
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
setContentView(R.layout.activity_take_phote);
// Initialize components of the app
mCropImageView = (CropImageView) findViewById(R.id.CropImageView);
mCameraPreview = (CameraPreview) findViewById(R.id.cameraPreview);
FocusView focusView = (FocusView) findViewById(R.id.view_focus);
mTakePhotoLayout = (RelativeLayout) findViewById(R.id.take_photo_layout);
mCropperLayout = (LinearLayout) findViewById(R.id.cropper_layout);
mCameraPreview.setFocusView(focusView);
mCameraPreview.setOnCameraStatusListener(this);
mCropImageView.setGuidelines(2);
mSensorManager = (SensorManager) getSystemService(Context.
SENSOR_SERVICE);
mAccel = mSensorManager.getDefaultSensor(Sensor.
TYPE_ACCELEROMETER);
}
boolean isRotated = false;
@Override
protected void onResume() {
super.onResume();
if(!isRotated) {
TextView hint_tv = (TextView) findViewById(R.id.hint);
ObjectAnimator animator = ObjectAnimator.ofFloat(hint_tv, "rotation", 0f, 90f);
animator.setStartDelay(800);
animator.setDuration(1000);
animator.setInterpolator(new LinearInterpolator());
animator.start();
View view = findViewById(R.id.crop_hint);
AnimatorSet animSet = new AnimatorSet();
ObjectAnimator animator1 = ObjectAnimator.ofFloat(view, "rotation", 0f, 90f);
ObjectAnimator moveIn = ObjectAnimator.ofFloat(view, "translationX", 0f, -50f);
animSet.play(animator1).before(moveIn);
animSet.setDuration(10);
animSet.start();
isRotated = true;
}
mSensorManager.registerListener(this, mAccel, SensorManager.SENSOR_DELAY_UI);
}
@Override
protected void onPause() {
super.onPause();
mSensorManager.unregisterListener(this);
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
Log.e(TAG, "onConfigurationChanged");
super.onConfigurationChanged(newConfig);
}
public void takePhoto(View view) {
if(mCameraPreview != null) {
mCameraPreview.takePicture();
}
}
public void close(View view) {
finish();
}
/**
* 关闭截图界面
* @param view
*/
public void closeCropper(View view) {
showTakePhotoLayout();
}
/**
* 开始截图,并保存图片
* @param view
*/
public void startCropper(View view) {
//获取截图并旋转90度
CropperImage cropperImage = mCropImageView.getCroppedImage();
Log.e(TAG, cropperImage.getX() + "," + cropperImage.getY());
Log.e(TAG, cropperImage.getWidth() + "," + cropperImage.getHeight());
Bitmap bitmap = Utils.rotate(cropperImage.getBitmap(), -90);
// Bitmap bitmap = mCropImageView.getCroppedImage();
// 系统时间
long dateTaken = System.currentTimeMillis();
// 图像名称
String filename = DateFormat.format("yyyy-MM-dd kk.mm.ss", dateTaken)
.toString() + ".jpg";
Uri uri = insertImage(getContentResolver(), filename, dateTaken, PATH,
filename, bitmap, null);
cropperImage.getBitmap().recycle();
cropperImage.setBitmap(null);
Intent intent = new Intent(this, ShowCropperedActivity.class);
intent.setData(uri);
intent.putExtra("path", PATH + filename);
intent.putExtra("width", bitmap.getWidth());
intent.putExtra("height", bitmap.getHeight());
intent.putExtra("cropperImage", cropperImage);
startActivity(intent);
bitmap.recycle();
finish();
super.overridePendingTransition(R.anim.fade_in,
R.anim.fade_out);
// doAnimation(cropperImage);
}
private void doAnimation(CropperImage cropperImage) {
ImageView imageView = new ImageView(this);
View view = LayoutInflater.from(this).inflate(
R.layout.image_view_layout, null);
((RelativeLayout) view.findViewById(R.id.root_layout)).addView(imageView);
RelativeLayout relativeLayout = ((RelativeLayout) findViewById(R.id.root_layout));
// relativeLayout.addView(imageView);
imageView.setX(cropperImage.getX());
imageView.setY(cropperImage.getY());
ViewGroup.LayoutParams lp = imageView.getLayoutParams();
lp.width = (int)cropperImage.getWidth();
lp.height = (int) cropperImage.getHeight();
imageView.setLayoutParams(lp);
imageView.setImageBitmap(cropperImage.getBitmap());
try {
getWindow().addContentView(view, lp);
} catch (Exception e) {
e.printStackTrace();
}
/*AnimatorSet animSet = new AnimatorSet();
ObjectAnimator translationX = ObjectAnimator.ofFloat(this, "translationX", cropperImage.getX(), 0);
ObjectAnimator translationY = ObjectAnimator.ofFloat(this, "translationY", cropperImage.getY(), 0);*/
TranslateAnimation translateAnimation = new TranslateAnimation(
0, -cropperImage.getX(), 0, -(Math.abs(cropperImage.getHeight() - cropperImage.getY())));// 当前位置移动到指定位置
RotateAnimation rotateAnimation = new RotateAnimation(0, -90,
Animation.ABSOLUTE, cropperImage.getX() ,Animation.ABSOLUTE, cropperImage.getY());
AnimationSet animationSet = new AnimationSet(true);
animationSet.addAnimation(translateAnimation);
animationSet.addAnimation(rotateAnimation);
animationSet.setFillAfter(true);
animationSet.setDuration(2000L);
imageView.startAnimation(animationSet);
// finish();
}
/**
* 拍照成功后回调
* 存储图片并显示截图界面
* @param data
*/
@Override
public void onCameraStopped(byte[] data) {
Log.i("TAG", "==onCameraStopped==");
// 创建图像
Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
// 系统时间
long dateTaken = System.currentTimeMillis();
// 图像名称
String filename = DateFormat.format("yyyy-MM-dd kk.mm.ss", dateTaken)
.toString() + ".jpg";
// 存储图像(PATH目录)
Uri source = insertImage(getContentResolver(), filename, dateTaken, PATH,
filename, bitmap, data);
//准备截图
try {
mCropImageView.setImageBitmap(MediaStore.Images.Media.getBitmap(this.getContentResolver(), source));
// mCropImageView.rotateImage(90);
} catch (IOException e) {
Log.e(TAG, e.getMessage());
}
showCropperLayout();
}
/**
* 存储图像并将信息添加入媒体数据库
*/
private Uri insertImage(ContentResolver cr, String name, long dateTaken,
String directory, String filename, Bitmap source, byte[] jpegData) {
OutputStream outputStream = null;
String filePath = directory + filename;
try {
File dir = new File(directory);
if (!dir.exists()) {
dir.mkdirs();
}
File file = new File(directory, filename);
if (file.createNewFile()) {
outputStream = new FileOutputStream(file);
if (source != null) {
source.compress(Bitmap.CompressFormat.JPEG, 100, outputStream);
} else {
outputStream.write(jpegData);
}
}
} catch (FileNotFoundException e) {
Log.e(TAG, e.getMessage());
return null;
} catch (IOException e) {
Log.e(TAG, e.getMessage());
return null;
} finally {
if (outputStream != null) {
try {
outputStream.close();
} catch (Throwable t) {
}
}
}
ContentValues values = new ContentValues(7);
values.put(MediaStore.Images.Media.TITLE, name);
values.put(MediaStore.Images.Media.DISPLAY_NAME, filename);
values.put(MediaStore.Images.Media.DATE_TAKEN, dateTaken);
values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
values.put(MediaStore.Images.Media.DATA, filePath);
return cr.insert(IMAGE_URI, values);
}
private void showTakePhotoLayout() {
mTakePhotoLayout.setVisibility(View.VISIBLE);
mCropperLayout.setVisibility(View.GONE);
}
private void showCropperLayout() {
mTakePhotoLayout.setVisibility(View.GONE);
mCropperLayout.setVisibility(View.VISIBLE);
mCameraPreview.start(); //继续启动摄像头
}
private float mLastX = 0;
private float mLastY = 0;
private float mLastZ = 0;
private boolean mInitialized = false;
private SensorManager mSensorManager;
private Sensor mAccel;
@Override
public void onSensorChanged(SensorEvent event) {
float x = event.values[0];
float y = event.values[1];
float z = event.values[2];
if (!mInitialized){
mLastX = x;
mLastY = y;
mLastZ = z;
mInitialized = true;
}
float deltaX = Math.abs(mLastX - x);
float deltaY = Math.abs(mLastY - y);
float deltaZ = Math.abs(mLastZ - z);
if(deltaX > 0.8 || deltaY > 0.8 || deltaZ > 0.8){
mCameraPreview.setFocus();
}
mLastX = x;
mLastY = y;
mLastZ = z;
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
}
}
actiity中注册了SensorEventListener,也就是使用传感器监听用户手机的移动,如果有一定距离的移动,则自动聚焦,这样体验好一点。
我对比了一下小猿搜题和学霸君两款app的拍照功能,个人感觉小猿搜题的体验要好一些,因为从主界面进入拍照界面,连个界面没有一个旋转的过渡,而学霸君就有一个过渡,有一丝丝的影响体验。也就是说学霸君的拍照界面是横屏的,在activity的onCreate方法里面调用了setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE)来设置全屏,而切换界面的时候又从竖屏切换为横屏,就会有个过渡的效果,影响了体验。
个人猜测小猿搜题是将拍照界面的activity设置为竖屏,而将摄像头直接旋转90度,这样就强制用户横屏拍摄,当然,拍完之后还要将图片旋转回来。所以我参考小猿搜题来实现的,毕竟体验为王嘛。
如上图(其实是竖屏),红色圈起来的其实是放到底部,然后将屏幕中间的文字旋转90度(带有动画,起了提示用户横屏拍照的作用),就给人的感觉是横屏的。了。
还有一点就是小猿搜题拍完照到截图过渡的很自然,感觉很流畅,估计是拍照和截图放在同一个activity中的,如果是两个activty,涉及到界面切换,肯定不会那么自然。所以我也将拍照和截图放在一个界面,拍照完就将自定义相机隐藏,将截图界面显示出来,这样切换就很流畅了。
项目中截图的功能我是从github上面找的一个开源库cropper:https://github.com/edmodo/cropper
因为ocr图片识别的代码是公司的,所以识别的功能没有添加到demo里面去。
帧动画
一开始我的想法是直接用帧动画来做,可是我太天真了,当帧数放到 50 几张的时候,已经在有些机器上奔溃了!所以这个方案否决!
GIF动图
虽然可以显示,但是已经卡的我,已经不想看了,直接放弃
视频
在这里,我突然想到我可以直接把他做成一个小视频啊,而且可以极限压缩视频。最终,视频大小被压缩到 500K 左右。此时已经基本可以满足需求了,但是我们有好多类似的动画,要求在每个动画切换的时候要有衔接感,不能有突兀的感觉,所有在这里视频就不能很好的完成任务了,所有再次放弃,已经泪牛满面了!!!!
SurfaceView + BitmapRegionDecoder +缓存
首先回答一下:为什么会想到这个解决方案?
首先在做帧动画的时候,大约每帧之间的时间差值是 40ms 可以说速度非常快了,在如此快速的图片切换上,自然而然的想到来了使用SurfaceView。
现在再来说说为什么想到要使用这个类 BitmapRegionDecoder .这个也是从我司游戏开发人员那儿得到的经验?他们在做游戏的时候,游戏中的切图都是放在一张大图上的,然后在根据对应的 xml,json 文件,获取相应的图片,接着再来切图。对此,我想能不能把所有的动图都放到同一张的图片上呢,之后在根据对应的描述文件,裁剪出我想要的图片呢!所以就用到了 BitmapRegionDecoder. 它的作用是:于显示图片的某一块矩形区域!之后,我在找设计人员商量一一下,把图片在尽量的压缩。之后从美工那儿获取的信息是这样的:
json格式的描述文件:
{"frames": [
{
"filename": "kidbot-正常闭眼0000",
"frame": {"x":0,"y":0,"w":360,"h":300},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {"x":0,"y":0,"w":360,"h":300},
"sourceSize": {"w":360,"h":300}
}
.....
}
png图片:
接下来就好做了,解析 json 格式的文件,裁剪图片。
最后说一下为什么使用缓存,其实很简单,因为切换的频率实在太高了,没有必要每次都从图片中裁剪,这里就把裁剪出来的 bitmap 缓存起来在用。从而介绍内存开销!
最后给出代码:
public class AnimView extends SurfaceView implements SurfaceHolder.Callback { private BitmapRegionDecoder bitmapRegionDecoder; private SurfaceHolder mHolder; private boolean isrunning = true; private AnimThread thread; private Paint mPaint; private int WIDTH = 0; private int HEIGHT = 0; private int state = -1; private boolean isstart = false; private boolean isblinkfirst = false; private int rate = 40; private int index = 0; private Matrix matrix; private Random rand; private Handler handler = new Handler() { public void handleMessage(android.os.Message msg) { isblinkfirst = true; }; }; private SparseArray<WeakReference<Bitmap>> weakBitmaps; private SparseArray<WeakReference<Bitmap>> cweakBitmaps; private BitmapFactory.Options options; public AnimView(Context context) { super(context); init(); } public AnimView(Context context, AttributeSet attrs) { super(context, attrs); init(); } public AnimView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } @SuppressLint("NewApi") private void init() { weakBitmaps = new SparseArray<WeakReference<Bitmap>>(); cweakBitmaps = new SparseArray<WeakReference<Bitmap>>(); mHolder = getHolder(); mHolder.addCallback(this); mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); setState(FaceBean.BLINK); mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); matrix = new Matrix(); float[] values = { -1f, 0.0f, 0.0f, 0.0f, 1f, 0.0f, 0.0f, 0.0f, 1.0f }; matrix.setValues(values); WindowManager manger = (WindowManager) getContext().getSystemService( Context.WINDOW_SERVICE); DisplayMetrics displayMetrics = new DisplayMetrics(); manger.getDefaultDisplay().getMetrics(displayMetrics); WIDTH = displayMetrics.widthPixels / 2; HEIGHT = displayMetrics.heightPixels / 2; rand = new Random(); options = new Options(); options.inPreferredConfig = Bitmap.Config.RGB_565; } @Override public void surfaceCreated(SurfaceHolder holder) { handler.sendEmptyMessageDelayed(0, 1000 * (4 + rand.nextInt(4))); thread = new AnimThread(); thread.start(); } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { } @Override public void surfaceDestroyed(SurfaceHolder holder) { if (thread != null) { thread.stopThread(); } } public class AnimThread extends Thread { @Override public void run() { super.run(); SurfaceHolder holder = mHolder; while (isrunning) { Canvas canvas = holder.lockCanvas(); if (canvas == null) continue; synchronized (AnimThread.class) { AnimBean.Frames frames; switch (state) { case FaceBean.BLINK: frames = KidbotRobotApplication.animBlink.getFrames() .get(index); if (frames.getFrame().getW() <= 0) { } else { Rect rect = new Rect(frames.getFrame().getX(), frames.getFrame().getY(), frames.getFrame() .getX() + frames.getSourceSize().getW(), frames.getFrame().getY() + frames.getSourceSize().getH()); WeakReference<Bitmap> weakBitmap = weakBitmaps .get(index); Bitmap map = null; if (weakBitmap == null) { map = bitmapRegionDecoder.decodeRegion(rect, options); weakBitmaps.put(index, new WeakReference<Bitmap>(map)); } else { map=weakBitmap.get(); if (map == null) { map = bitmapRegionDecoder.decodeRegion( rect, options); weakBitmaps.put(index, new WeakReference<Bitmap>(map)); } } if (map == null) { holder.unlockCanvasAndPost(canvas); continue; } mPaint.setXfermode(new PorterDuffXfermode( Mode.CLEAR)); canvas.drawPaint(mPaint); mPaint.setXfermode(new PorterDuffXfermode(Mode.SRC)); canvas.drawBitmap(map, (int) (WIDTH - (map.getWidth() * 1) - 150), (int) (HEIGHT - (map.getHeight() / 2)), mPaint); canvas.drawBitmap(map, (int) (WIDTH + 150), (int) (HEIGHT - (map.getHeight() / 2)), mPaint); if (index == 0) { } if (map.isRecycled()) { map.recycle(); } } if (!isstart) { if (index < KidbotRobotApplication.animBlink .getFrames().size()) { index++; if (index == KidbotRobotApplication.animBlink .getFrames().size()) { index--; isstart = true; if (rand.nextInt(10) <= 2) { index = 1; } } } else { index--; isstart = true; } } else { if (index > 0) { index--; if (index == 0) { isstart = false; } } else { index++; isstart = false; } } if (!isblinkfirst) { index = 0; } else { if (index == KidbotRobotApplication.animBlink .getFrames().size() - 1) { isblinkfirst = false; index = 0; handler.sendEmptyMessageDelayed(0, 1000 * (4 + rand.nextInt(4))); } } break; case FaceBean.ANGRY: frames = KidbotRobotApplication.animAngry.getFrames() .get(index); if (frames.getFrame().getW() <= 0) { } else { Rect rect = new Rect(frames.getFrame().getX(), frames.getFrame().getY(), frames.getFrame() .getX() + frames.getFrame().getW(), frames.getFrame().getH() + frames.getFrame().getX()); WeakReference<Bitmap> weakBitmap = weakBitmaps .get(index); Bitmap map = null; if (weakBitmap == null) { map = bitmapRegionDecoder.decodeRegion(rect, options); weakBitmaps.put(index, new WeakReference<Bitmap>(map)); } else { map=weakBitmap.get(); if (map == null) { map = bitmapRegionDecoder.decodeRegion( rect, options); weakBitmaps.put(index, new WeakReference<Bitmap>(map)); } } if (map == null) { holder.unlockCanvasAndPost(canvas); continue; } mPaint.setXfermode(new PorterDuffXfermode( Mode.CLEAR)); canvas.drawPaint(mPaint); mPaint.setXfermode(new PorterDuffXfermode(Mode.SRC)); Bitmap dstbmp =null; weakBitmap=cweakBitmaps.get(index); if(weakBitmap==null){ dstbmp = Bitmap.createBitmap(map, 0, 0, map.getWidth(), map.getHeight(), matrix, true); cweakBitmaps.put(index, new WeakReference<Bitmap>(dstbmp)); }else{ dstbmp=weakBitmap.get(); if(dstbmp==null){ dstbmp = Bitmap.createBitmap(map, 0, 0, map.getWidth(), map.getHeight(), matrix, true); cweakBitmaps.put(index, new WeakReference<Bitmap>(dstbmp)); } } canvas.drawBitmap( map, frames.getSpriteSourceSize().getX() + (int) (WIDTH - (map.getWidth() * 1) - 150), frames.getSpriteSourceSize().getY() + (int) (HEIGHT - (map.getHeight() / 2)), mPaint); canvas.drawBitmap(dstbmp, frames .getSpriteSourceSize().getX() + (int) (WIDTH + 150), frames .getSpriteSourceSize().getY() + (int) (HEIGHT - (map.getHeight() / 2)), mPaint); if (dstbmp.isRecycled()) { dstbmp.recycle(); } if (map.isRecycled()) { map.recycle(); } } if (!isstart) { if (index < KidbotRobotApplication.animAngry .getFrames().size()) { index++; if (index == KidbotRobotApplication.animAngry .getFrames().size()) { index--; isstart = true; } } else { index--; isstart = true; } } else { if (index > 0) { index--; if (index == 0) { isstart = false; } } else { index++; isstart = false; } } break; case FaceBean.HAPPY: frames = KidbotRobotApplication.animHappy.getFrames() .get(index); if (frames.getFrame().getW() <= 0) { } else { Rect rect = new Rect(frames.getFrame().getX(), frames.getFrame().getY(), frames.getFrame() .getX() + frames.getSourceSize().getW(), frames.getFrame().getY() + frames.getSourceSize().getH()); WeakReference<Bitmap> weakBitmap = weakBitmaps .get(index); Bitmap map = null; if (weakBitmap == null) { map = bitmapRegionDecoder.decodeRegion(rect, options); weakBitmaps.put(index, new WeakReference<Bitmap>(map)); } else { map=weakBitmap.get(); if (map == null) { map = bitmapRegionDecoder.decodeRegion( rect, options); weakBitmaps.put(index, new WeakReference<Bitmap>(map)); } } if (map == null) { holder.unlockCanvasAndPost(canvas); continue; } mPaint.setXfermode(new PorterDuffXfermode( Mode.CLEAR)); canvas.drawPaint(mPaint); mPaint.setXfermode(new PorterDuffXfermode(Mode.SRC)); Bitmap dstbmp =null; weakBitmap=cweakBitmaps.get(index); if(weakBitmap==null){ dstbmp = Bitmap.createBitmap(map, 0, 0, map.getWidth(), map.getHeight(), matrix, true); cweakBitmaps.put(index, new WeakReference<Bitmap>(dstbmp)); }else{ dstbmp=weakBitmap.get(); if(dstbmp==null){ dstbmp = Bitmap.createBitmap(map, 0, 0, map.getWidth(), map.getHeight(), matrix, true); cweakBitmaps.put(index, new WeakReference<Bitmap>(dstbmp)); } } canvas.drawBitmap( map, frames.getSpriteSourceSize().getX() + (int) (WIDTH - (map.getWidth() * 1) - 150), frames.getSpriteSourceSize().getY() + (int) (HEIGHT - (map.getHeight() / 2)), mPaint); canvas.drawBitmap(dstbmp, frames .getSpriteSourceSize().getX() + (int) (WIDTH + 150), frames .getSpriteSourceSize().getY() + (int) (HEIGHT - (map.getHeight() / 2)), mPaint); // if (dstbmp.isRecycled()) { // dstbmp.recycle(); // } // if (map.isRecycled()) { // map.recycle(); // } } if (!isstart) { if (index < KidbotRobotApplication.animHappy .getFrames().size()) { index++; if (index == KidbotRobotApplication.animHappy .getFrames().size()) { index--; isstart = true; } } else { index--; isstart = true; } } else { if (index > 0) { index--; if (index == 0) { isstart = false; } } else { index++; isstart = false; } } break; case FaceBean.RESOLVE: break; case FaceBean.RISUS: break; case FaceBean.SEERIGHT: break; case FaceBean.SAD: frames = KidbotRobotApplication.animSad.getFrames() .get(index); if (frames.getFrame().getW() <= 0) { } else { Rect rect = new Rect(frames.getFrame().getX(), frames.getFrame().getY(), frames.getFrame() .getX() + frames.getSourceSize().getW(), frames.getFrame().getY() + frames.getSourceSize().getH()); WeakReference<Bitmap> weakBitmap = weakBitmaps .get(index); Bitmap map = null; if (weakBitmap == null) { map = bitmapRegionDecoder.decodeRegion(rect, options); weakBitmaps.put(index, new WeakReference<Bitmap>(map)); } else { map=weakBitmap.get(); if (map == null) { map = bitmapRegionDecoder.decodeRegion( rect, options); weakBitmaps.put(index, new WeakReference<Bitmap>(map)); } } if (map == null) { holder.unlockCanvasAndPost(canvas); continue; } mPaint.setXfermode(new PorterDuffXfermode( Mode.CLEAR)); canvas.drawPaint(mPaint); mPaint.setXfermode(new PorterDuffXfermode(Mode.SRC)); Bitmap dstbmp =null; weakBitmap=cweakBitmaps.get(index); if(weakBitmap==null){ dstbmp = Bitmap.createBitmap(map, 0, 0, map.getWidth(), map.getHeight(), matrix, true); cweakBitmaps.put(index, new WeakReference<Bitmap>(dstbmp)); }else{ dstbmp=weakBitmap.get(); if(dstbmp==null){ dstbmp = Bitmap.createBitmap(map, 0, 0, map.getWidth(), map.getHeight(), matrix, true); cweakBitmaps.put(index, new WeakReference<Bitmap>(dstbmp)); } } canvas.drawBitmap( map, frames.getSpriteSourceSize().getX() + (int) (WIDTH - (map.getWidth() * 1) - 150), frames.getSpriteSourceSize().getY() + (int) (HEIGHT - (map.getHeight() / 2)), mPaint); canvas.drawBitmap(dstbmp, frames .getSpriteSourceSize().getX() + (int) (WIDTH + 150), frames .getSpriteSourceSize().getY() + (int) (HEIGHT - (map.getHeight() / 2)), mPaint); if (dstbmp.isRecycled()) { dstbmp.recycle(); } if (map.isRecycled()) { map.recycle(); } } if (!isstart) { if (index < KidbotRobotApplication.animSad .getFrames().size()) { index++; if (index == KidbotRobotApplication.animSad .getFrames().size()) { index--; isstart = true; } } else { index--; isstart = true; } } else { if (index > 0) { index--; if (index == 0) { isstart = false; } } else { index++; isstart = false; } } break; default: break; } } holder.unlockCanvasAndPost(canvas); try { Thread.sleep(rate); } catch (InterruptedException e) { e.printStackTrace(); } } } public void stopThread() { isrunning = false; try { join(); } catch (InterruptedException e) { e.printStackTrace(); } } } public synchronized void setRate(int rate) { this.rate = rate; } public int getState() { return this.state; } public synchronized void setState(int state) { // if (FaceBean.BLINK == this.state) { // while ((index != KidbotRobotApplication.animBlink.getFrames() // .size() - 1)) { // continue; // } // } cweakBitmaps.clear(); weakBitmaps.clear(); this.state = state; this.index = 0; switch (state) { case FaceBean.BLINK: try { bitmapRegionDecoder = BitmapRegionDecoder.newInstance( getContext().getAssets().open("kidbot_blink.png"), false); } catch (IOException e) { e.printStackTrace(); } break; case FaceBean.ANGRY: try { bitmapRegionDecoder = BitmapRegionDecoder.newInstance( getContext().getAssets().open("kidbot_angry.png"), false); } catch (IOException e) { e.printStackTrace(); } break; case FaceBean.HAPPY: try { bitmapRegionDecoder = BitmapRegionDecoder.newInstance( getContext().getAssets().open("kidbot_happy.png"), false); } catch (IOException e) { e.printStackTrace(); } break; case FaceBean.RESOLVE: try { bitmapRegionDecoder = BitmapRegionDecoder.newInstance( getContext().getAssets().open("kidbot_blink.png"), false); } catch (IOException e) { e.printStackTrace(); } break; case FaceBean.RISUS: try { bitmapRegionDecoder = BitmapRegionDecoder.newInstance( getContext().getAssets().open("kidbot_blink.png"), false); } catch (IOException e) { e.printStackTrace(); } break; case FaceBean.SEERIGHT: break; case FaceBean.SAD: try { bitmapRegionDecoder = BitmapRegionDecoder.newInstance( getContext().getAssets().open("kidbot_sad.png"), false); } catch (IOException e) { e.printStackTrace(); } break; } } public synchronized void setRunning(boolean isrunning) { this.isrunning = isrunning; } public synchronized void addIndex() { this.index++; } }
Android高效加载大图、多图解决方案,有效避免程序OOM
比如说系统图片库里展示的图片大都是用手机摄像头拍出来的,这些图片的分辨率会比我们手机屏幕的分辨率高得多。大家应该知道,我们编写的应用程序都是有一定内存限制的,程序占用了过高的内存就容易出现OOM(OutOfMemory)异常。
我们可以通过下面的代码看出每个应用程序最高可用内存是多少。
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
Log.d("TAG", "Max memory is " + maxMemory + "KB");
因此在展示高分辨率图片的时候,最好先将图片进行压缩。压缩后的图片大小应该和用来展示它的控件大小相近,在一个很小的ImageView上显示一张超大的图片不会带来任何视觉上的好处,但却会占用我们相当多宝贵的内存,而且在性能上还可能会带来负面影响。下面我们就来看一看,如何对一张大图片进行适当的压缩,让它能够以最佳大小显示的同时,还能防止OOM的出现。
BitmapFactory这个类提供了多个解析方法(decodeByteArray, decodeFile, decodeResource等)用于创建Bitmap对象,我们应该根据图片的来源选择合适的方法。比如SD卡中的图片可以使用decodeFile方法,网络上的图片可以使用decodeStream方法,资源文件中的图片可以使用decodeResource方法。这些方法会尝试为已经构建的bitmap分配内存,这时就会很容易导致OOM出现。为此每一种解析方法都提供了一个可选的BitmapFactory.Options参数,将这个参数的inJustDecodeBounds属性设置为true就可以让解析方法禁止为bitmap分配内存,返回值也不再是一个Bitmap对象,而是null。虽然Bitmap是null了,但是BitmapFactory.Options的outWidth、outHeight和outMimeType属性都会被赋值。这个技巧让我们可以在加载图片之前就获取到图片的长宽值和MIME类型,从而根据情况对图片进行压缩。如下代码所示:
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.id.myimage, options);
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
String imageType = options.outMimeType;
为了避免OOM异常,最好在解析每张图片的时候都先检查一下图片的大小,除非你非常信任图片的来源,保证这些图片都不会超出你程序的可用内存。
现在图片的大小已经知道了,我们就可以决定是把整张图片加载到内存中还是加载一个压缩版的图片到内存中。以下几个因素是我们需要考虑的:
预估一下加载整张图片所需占用的内存。
为了加载这一张图片你所愿意提供多少内存。
用于展示这张图片的控件的实际大小。
当前设备的屏幕尺寸和分辨率。
比如,你的ImageView只有128*96像素的大小,只是为了显示一张缩略图,这时候把一张1024*768像素的图片完全加载到内存中显然是不值得的。
那我们怎样才能对图片进行压缩呢?
通过设置BitmapFactory.Options中inSampleSize的值就可以实现。比如我们有一张2048*1536像素的图片,将inSampleSize的值设置为4,就可以把这张图片压缩成512*384像素。原本加载这张图片需要占用13M的内存,压缩后就只需要占用0.75M了(假设图片是ARGB_8888类型,即每个像素点占用4个字节)。下面的方法可以根据传入的宽和高,计算出合适的inSampleSize值:
public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) { // 源图片的高度和宽度 final int height = options.outHeight; final int width = options.outWidth; int inSampleSize = 1; if (height > reqHeight || width > reqWidth) { // 计算出实际宽高和目标宽高的比率 final int heightRatio = Math.round((float) height / (float) reqHeight); final int widthRatio = Math.round((float) width / (float) reqWidth); // 选择宽和高中最小的比率作为inSampleSize的值,这样可以保证最终图片的宽和高 // 一定都会大于等于目标的宽和高。 inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio; } return inSampleSize; }
使用这个方法,首先你要将BitmapFactory.Options的inJustDecodeBounds属性设置为true,解析一次图片。然后将BitmapFactory.Options连同期望的宽度和高度一起传递到到calculateInSampleSize方法中,就可以得到合适的inSampleSize值了。之后再解析一次图片,使用新获取到的inSampleSize值,并把inJustDecodeBounds设置为false,就可以得到压缩后的图片了。
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) { // 第一次解析将inJustDecodeBounds设置为true,来获取图片大小 final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeResource(res, resId, options); // 调用上面定义的方法计算inSampleSize值 options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); // 使用获取到的inSampleSize值再次解析图片 options.inJustDecodeBounds = false; return BitmapFactory.decodeResource(res, resId, options); }
下面的代码非常简单地将任意一张图片压缩成100*100的缩略图,并在ImageView上展示。
mImageView.setImageBitmap( decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));
使用图片缓存技术
在你应用程序的UI界面加载一张图片是一件很简单的事情,但是当你需要在界面上加载一大堆图片的时候,情况就变得复杂起来。在很多情况下,(比如使用ListView, GridView 或者 ViewPager 这样的组件),屏幕上显示的图片可以通过滑动屏幕等事件不断地增加,最终导致OOM。
为了保证内存的使用始终维持在一个合理的范围,通常会把被移除屏幕的图片进行回收处理。此时垃圾回收器也会认为你不再持有这些图片的引用,从而对这些图片进行GC操作。用这种思路来解决问题是非常好的,可是为了能让程序快速运行,在界面上迅速地加载图片,你又必须要考虑到某些图片被回收之后,用户又将它重新滑入屏幕这种情况。这时重新去加载一遍刚刚加载过的图片无疑是性能的瓶颈,你需要想办法去避免这个情况的发生。
这个时候,使用内存缓存技术可以很好的解决这个问题,它可以让组件快速地重新加载和处理图片。下面我们就来看一看如何使用内存缓存技术来对图片进行缓存,从而让你的应用程序在加载很多图片的时候可以提高响应速度和流畅性。
内存缓存技术对那些大量占用应用程序宝贵内存的图片提供了快速访问的方法。其中最核心的类是LruCache (此类在android-support-v4的包中提供) 。这个类非常适合用来缓存图片,它的主要算法原理是把最近使用的对象用强引用存储在 LinkedHashMap 中,并且把最近最少使用的对象在缓存值达到预设定值之前从内存中移除。
在过去,我们经常会使用一种非常流行的内存缓存技术的实现,即软引用或弱引用 (SoftReference or WeakReference)。但是现在已经不再推荐使用这种方式了,因为从 Android 2.3 (API Level 9)开始,垃圾回收器会更倾向于回收持有软引用或弱引用的对象,这让软引用和弱引用变得不再可靠。另外,Android 3.0 (API Level 11)中,图片的数据会存储在本地的内存当中,因而无法用一种可预见的方式将其释放,这就有潜在的风险造成应用程序的内存溢出并崩溃。
为了能够选择一个合适的缓存大小给LruCache, 有以下多个因素应该放入考虑范围内,例如:
你的设备可以为每个应用程序分配多大的内存?
设备屏幕上一次最多能显示多少张图片?有多少图片需要进行预加载,因为有可能很快也会显示在屏幕上?
你的设备的屏幕大小和分辨率分别是多少?一个超高分辨率的设备(例如 Galaxy Nexus) 比起一个较低分辨率的设备(例如 Nexus S),在持有相同数量图片的时候,需要更大的缓存空间。
图片的尺寸和大小,还有每张图片会占据多少内存空间。
图片被访问的频率有多高?会不会有一些图片的访问频率比其它图片要高?如果有的话,你也许应该让一些图片常驻在内存当中,或者使用多个LruCache 对象来区分不同组的图片。
你能维持好数量和质量之间的平衡吗?有些时候,存储多个低像素的图片,而在后台去开线程加载高像素的图片会更加的有效。
并没有一个指定的缓存大小可以满足所有的应用程序,这是由你决定的。你应该去分析程序内存的使用情况,然后制定出一个合适的解决方案。一个太小的缓存空间,有可能造成图片频繁地被释放和重新加载,这并没有好处。而一个太大的缓存空间,则有可能还是会引起 java.lang.OutOfMemory 的异常。
下面是一个使用 LruCache 来缓存图片的例子:
private LruCache<String, Bitmap> mMemoryCache; @Override protected void onCreate(Bundle savedInstanceState) { // 获取到可用内存的最大值,使用内存超出这个值会引起OutOfMemory异常。 // LruCache通过构造函数传入缓存值,以KB为单位。 int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); // 使用最大可用内存值的1/8作为缓存的大小。 int cacheSize = maxMemory / 8; mMemoryCache = new LruCache<String, Bitmap>(cacheSize) { @Override protected int sizeOf(String key, Bitmap bitmap) { // 重写此方法来衡量每张图片的大小,默认返回图片数量。 return bitmap.getByteCount() / 1024; } }; } public void addBitmapToMemoryCache(String key, Bitmap bitmap) { if (getBitmapFromMemCache(key) == null) { mMemoryCache.put(key, bitmap); } } public Bitmap getBitmapFromMemCache(String key) { return mMemoryCache.get(key); }
在这个例子当中,使用了系统分配给应用程序的八分之一内存来作为缓存大小。在中高配置的手机当中,这大概会有4兆(32/8)的缓存空间。一个全屏幕的 GridView 使用4张 800x480分辨率的图片来填充,则大概会占用1.5兆的空间(800*480*4)。因此,这个缓存大小可以存储2.5页的图片。
当向 ImageView 中加载一张图片时,首先会在 LruCache 的缓存中进行检查。如果找到了相应的键值,则会立刻更新ImageView ,否则开启一个后台线程来加载这张图片。
public void loadBitmap(int resId, ImageView imageView) { final String imageKey = String.valueOf(resId); final Bitmap bitmap = getBitmapFromMemCache(imageKey); if (bitmap != null) { imageView.setImageBitmap(bitmap); } else { imageView.setImageResource(R.drawable.image_placeholder); BitmapWorkerTask task = new BitmapWorkerTask(imageView); task.execute(resId); } }
BitmapWorkerTask 还要把新加载的图片的键值对放到缓存中。
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> { // 在后台加载图片。 @Override protected Bitmap doInBackground(Integer... params) { final Bitmap bitmap = decodeSampledBitmapFromResource( getResources(), params[0], 100, 100); addBitmapToMemoryCache(String.valueOf(params[0]), bitmap); return bitmap; } }
掌握了以上两种方法,不管是要在程序中加载超大图片,还是要加载大量图片,都不用担心OOM的问题了!不过仅仅是理论地介绍不知道大家能不能完全理解,在后面的文章中我会演示如何在实际程序中灵活运用上述技巧来避免程序OOM,敬请期待。
相关文章
- 有时为了网站安全和版权问题,会对自己写的php源码进行加密,在php加密技术上最常用的是zend公司的zend guard 加密软件,现在我们来图文讲解一下。 下面就简单说说如何...2016-11-25
- 下面我们来看一篇关于Android子控件超出父控件的范围显示出来方法,希望这篇文章能够帮助到各位朋友,有碰到此问题的朋友可以进来看看哦。 <RelativeLayout xmlns:an...2016-10-02
- ps软件是现在很多人都会使用到的,HSL面板在ps软件中又有着非常独特的作用。这次文章就给大家介绍下ps怎么使用HSL面板,还不知道使用方法的下面一起来看看。  ...2017-07-06
- 许多的朋友对于Plesk控制面板应用不是非常的了解特别是英文版的Plesk控制面板,在这里小编整理了一些关于Plesk控制面板常用的使用方案整理,具体如下。 本文基于Linu...2016-10-10
使用insertAfter()方法在现有元素后添加一个新元素
复制代码 代码如下: //在现有元素后添加一个新元素 function insertAfter(newElement, targetElement){ var parent = targetElement.parentNode; if (parent.lastChild == targetElement){ parent.appendChild(newEl...2014-05-31Android开发中findViewById()函数用法与简化
findViewById方法在android开发中是获取页面控件的值了,有没有发现我们一个页面控件多了会反复研究写findViewById呢,下面我们一起来看它的简化方法。 Android中Fin...2016-09-20- 如果我们的项目需要做来电及短信的功能,那么我们就得在Android模拟器开发这些功能,本来就来告诉我们如何在Android模拟器上模拟来电及来短信的功能。 在Android模拟...2016-09-20
- 大概有如下步骤 新建项目Bejs 新建文件package.json 新建文件Gruntfile.js 命令行执行grunt任务 一、新建项目Bejs源码放在src下,该目录有两个js文件,selector.js和ajax.js。编译后代码放在dest,这个grunt会...2014-06-07
使用percona-toolkit操作MySQL的实用命令小结
1.pt-archiver 功能介绍: 将mysql数据库中表的记录归档到另外一个表或者文件 用法介绍: pt-archiver [OPTION...] --source DSN --where WHERE 这个工具只是归档旧的数据,不会对线上数据的OLTP查询造成太大影响,你可以将...2015-11-24如何使用php脚本给html中引用的js和css路径打上版本号
在搜索引擎中搜索关键字.htaccess 缓存,你可以搜索到很多关于设置网站文件缓存的教程,通过设置可以将css、js等不太经常更新的文件缓存在浏览器端,这样访客每次访问你的网站的时候,浏览器就可以从浏览器的缓存中获取css、...2015-11-24- 夜神android模拟器如何设置代理呢?对于这个问题其实操作起来是非常的简单,下面小编来为各位详细介绍夜神android模拟器设置代理的方法,希望例子能够帮助到各位。 app...2016-09-20
- 为了增强android应用的用户体验,我们可以在一些Button按钮上自定义动态的设置一些样式,比如交互时改变字体、颜色、背景图等。 今天来看一个通过重写Button来动态实...2016-09-20
- 如果我们要在Android应用APP中加载html5页面,我们可以使用WebView,本文我们分享两个WebView加载html5页面实例应用。 实例一:WebView加载html5实现炫酷引导页面大多...2016-09-20
jQuery 1.9使用$.support替代$.browser的使用方法
jQuery 从 1.9 版开始,移除了 $.browser 和 $.browser.version , 取而代之的是 $.support 。 在更新的 2.0 版本中,将不再支持 IE 6/7/8。 以后,如果用户需要支持 IE 6/7/8,只能使用 jQuery 1.9。 如果要全面支持 IE,并混合...2014-05-31安装和使用percona-toolkit来辅助操作MySQL的基本教程
一、percona-toolkit简介 percona-toolkit是一组高级命令行工具的集合,用来执行各种通过手工执行非常复杂和麻烦的mysql和系统任务,这些任务包括: 检查master和slave数据的一致性 有效地对记录进行归档 查找重复的索...2015-11-24- 深入理解Android中View和ViewGroup从组成架构上看,似乎ViewGroup在View之上,View需要继承ViewGroup,但实际上不是这样的。View是基类,ViewGroup是它的子类。本教程我们深...2016-09-20
- 一、下载 mysqlsla [root@localhost tmp]# wget http://hackmysql.com/scripts/mysqlsla-2.03.tar.gz--19:45:45-- http://hackmysql.com/scripts/mysqlsla-2.03.tar.gzResolving hackmysql.com... 64.13.232.157Conn...2015-11-24
- 目前,JSON已经成为最流行的数据交换格式之一,各大网站的API几乎都支持它。我写过一篇《数据类型和JSON格式》,探讨它的设计思想。今天,我想总结一下PHP语言对它的支持,这是开发互联网应用程序(特别是编写API)必须了解的知识...2015-10-30
- 下面我们来看一篇关于Android自定义WebView网络视频播放控件开发例子,这个文章写得非常的不错下面给各位共享一下吧。 因为业务需要,以下代码均以Youtube网站在线视...2016-10-02
- java开发的Android应用,性能一直是一个大问题,,或许是Java语言本身比较消耗内存。本文我们来谈谈Android 性能优化之MemoryFile文件读写。 Android匿名共享内存对外A...2016-09-20