在这次one新版本的UI设计中,其中关于头像的设计中,一部分是圆角头像,一部分是圆形图像,关于这部分的实现有两种方法:第一种就是通过图片的叠加来实现,这种方法相对比较简单,但是如果有多种规格大小的就得提供相应这么多种的图片,第二种就是通过代码实现,这部分实现起来稍微麻烦点,但是如果一旦实现那么代码就可以重用,代码中用起来就比较方便。于是查找了各种资料,今天就来分享下代码实现的方法。
UI的开发最推荐的还是组件式开发,对于多个地方通用的东西抽成一个组件,这样不管是代码复用还是提供给别人使用都是比较方便的做法。在这次UI设计中,有圆形图片,圆角图片,不排除以后还有其他图片,如椭圆图片之类的,所以这次UI组件的开发也采用了继承的方法。
下面就来看下代码实现:
package com.boohee.widgets;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Xfermode;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.widget.ImageView;
public abstract class MaskedImage extends ImageView {
private static final Xfermode MASK_XFERMODE;
private Bitmap mask;
private Paint paint;
static {
PorterDuff.Mode localMode = PorterDuff.Mode.DST_IN;
MASK_XFERMODE = new PorterDuffXfermode(localMode);
}
public MaskedImage(Context paramContext) {
super(paramContext);
}
public MaskedImage(Context paramContext, AttributeSet paramAttributeSet) {
super(paramContext, paramAttributeSet);
}
public MaskedImage(Context paramContext, AttributeSet paramAttributeSet, int paramInt) {
super(paramContext, paramAttributeSet, paramInt);
}
public abstract Bitmap createMask();
protected void onDraw(Canvas paramCanvas) {
Drawable localDrawable = getDrawable();
if (localDrawable == null)
return;
try {
if (this.paint == null) {
Paint localPaint1 = new Paint();
this.paint = localPaint1;
this.paint.setFilterBitmap(false);
Paint localPaint2 = this.paint;
Xfermode localXfermode1 = MASK_XFERMODE;
Xfermode localXfermode2 = localPaint2.setXfermode(localXfermode1);
}
float f1 = getWidth();
float f2 = getHeight();
int i = paramCanvas.saveLayer(0.0F, 0.0F, f1, f2, null, 31);
int j = getWidth();
int k = getHeight();
localDrawable.setBounds(0, 0, j, k);
localDrawable.draw(paramCanvas);
if ((this.mask == null) || (this.mask.isRecycled())) {
Bitmap localBitmap1 = createMask();
this.mask = localBitmap1;
}
Bitmap localBitmap2 = this.mask;
Paint localPaint3 = this.paint;
paramCanvas.drawBitmap(localBitmap2, 0.0F, 0.0F, localPaint3);
paramCanvas.restoreToCount(i);
return;
} catch (Exception localException) {
StringBuilder localStringBuilder = new StringBuilder()
.append("Attempting to draw with recycled bitmap. View ID = ");
System.out.println("localStringBuilder=="+localStringBuilder);
}
}
}
public class CircularImage extends MaskedImage {
public CircularImage(Context paramContext) {
super(paramContext);
}
public CircularImage(Context paramContext, AttributeSet paramAttributeSet) {
super(paramContext, paramAttributeSet);
}
public CircularImage(Context paramContext, AttributeSet paramAttributeSet, int paramInt) {
super(paramContext, paramAttributeSet, paramInt);
}
public Bitmap createMask() {
int i = getWidth();
int j = getHeight();
Bitmap.Config localConfig = Bitmap.Config.ARGB_8888;
Bitmap localBitmap = Bitmap.createBitmap(i, j, localConfig);
Canvas localCanvas = new Canvas(localBitmap);
Paint localPaint = new Paint(1);
localPaint.setColor(-16777216);
float f1 = getWidth();
float f2 = getHeight();
RectF localRectF = new RectF(0.0F, 0.0F, f1, f2);
localCanvas.drawOval(localRectF, localPaint);
return localBitmap;
}
}
public class RoundedCornersImage extends MaskedImage {
private static final int DEFAULT_CORNER_RADIUS = 8;
private int cornerRadius = DEFAULT_CORNER_RADIUS;
public RoundedCornersImage(Context paramContext) {
super(paramContext);
}
public RoundedCornersImage(Context paramContext, AttributeSet paramAttributeSet) {
super(paramContext, paramAttributeSet);
int[] arrayOfInt = R.styleable.RoundedCornersImage;
TypedArray a = paramContext.obtainStyledAttributes(paramAttributeSet, arrayOfInt);
int i = a.getDimensionPixelSize(0, DEFAULT_CORNER_RADIUS);
this.cornerRadius = i;
a.recycle();
}
public Bitmap createMask() {
int i = getWidth();
int j = getHeight();
Bitmap.Config localConfig = Bitmap.Config.ARGB_8888;
Bitmap localBitmap = Bitmap.createBitmap(i, j, localConfig);
Canvas localCanvas = new Canvas(localBitmap);
Paint localPaint = new Paint(1);
localPaint.setColor(-16777216);
float f1 = getWidth();
float f2 = getHeight();
RectF localRectF = new RectF(0.0F, 0.0F, f1, f2);
float f3 = this.cornerRadius;
float f4 = this.cornerRadius;
localCanvas.drawRoundRect(localRectF, f3, f4, localPaint);
return localBitmap;
}
}
如果以后新增了椭圆图片,那么只须新建一个椭圆图片累继承自MaskedImage,然后重写createMask()方法即可。
至此,代码实现已完成,只需要在xml中像使用android自带组件的方式使用我们的自定义组件即可,如:
<com.boohee.widgets.RoundedCornersImage
android:id="@+id/rounded_image"
android:layout_width="60dp"
android:layout_height="60dp" />
前面有一篇blog提到在Android开发中我们一般有两种方式使用sqlite,第一种是在application中手动创建,然后程序中管理数据库的升级;第二种是预先放置一份sqlite数据库,程序中使用的时候仅是查询功能,并不会涉及到更改、删除操作。这种情况下多是起到提供一个基础资源库的作用,如预先放置的一些提醒励志语句、以及预先放置的一些食物数据等。今天就来总结下如何管理assets文件夹下的sqlite数据库。
数据库管理一般都会伴随着升级,试想放在assets文件夹下的数据库升级是该怎么处理呢?
首先放在assets文件夹里的sqlite文件一定是我们事先经过处理好的数据库,包括里面的数据也是我们人为的生成的,如我们的app其实就是根据后端mysql转换成的sqlite,但是后端的数据是会不断完善以及不断变化的,所以伴随着我们的app端的sqlite也会是不断完善的,我想这种情况下大多数的策略是后端重新生成一份最新的sqlite文件,然后等到app发布的时候直接拷贝并覆盖原来旧的数据库。基于这种场景参考了github上一些资料,定义了一个SqliteAssetHelper来管理数据库的升级。下面看代码:
public class SQLiteAssetHelper extends SQLiteOpenHelper {
static final String TAG = SQLiteAssetHelper.class.getSimpleName();
private static final String ASSET_DB_PATH = "databases";
private final Context mContext;
private final String mName;
private final CursorFactory mFactory;
private final int mNewVersion;
private SQLiteDatabase mDatabase = null;
private boolean mIsInitializing = false;
private String mDatabasePath;
private String mArchivePath;
public SQLiteAssetHelper(Context context, String name, CursorFactory factory, int version) {
super(context, name, factory, version);
if (version < 1) throw new IllegalArgumentException("Version must be >= 1, was " + version);
if (name == null) throw new IllegalArgumentException("Databse name cannot be null");
mContext = context;
mName = name;
mFactory = factory;
mNewVersion = version;
mArchivePath = ASSET_DB_PATH + "/" + name + ".zip";
mDatabasePath = context.getApplicationInfo().dataDir + "/databases";
}
@Override
public void onCreate(SQLiteDatabase db) {
// do nothing - createOrOpenDatabase() is called in
// getWritableDatabase() to handle database creation.
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
@Override
public synchronized SQLiteDatabase getWritableDatabase() {
if (mDatabase != null && mDatabase.isOpen() && !mDatabase.isReadOnly()) {
return mDatabase; // The database is already open for business
}
if (mIsInitializing) {
throw new IllegalStateException("getWritableDatabase called recursively");
}
// If we have a read-only database open, someone could be using it
// (though they shouldn't), which would cause a lock to be held on
// the file, and our attempts to open the database read-write would
// fail waiting for the file lock. To prevent that, we acquire the
// lock on the read-only database, which shuts out other users.
boolean success = false;
SQLiteDatabase db = null;
//if (mDatabase != null) mDatabase.lock();
try {
mIsInitializing = true;
db = createOrOpenDatabase(false);
int version = db.getVersion();
Log.e(TAG, "old version:" + version);
Log.e(TAG, "new version:" + mNewVersion);
// do force upgrade
if (version != 0 && version < mNewVersion) {
db = createOrOpenDatabase(true);
version = db.getVersion();
}
onOpen(db);
success = true;
return db;
} finally {
mIsInitializing = false;
if (success) {
if (mDatabase != null) {
try { mDatabase.close(); } catch (Exception e) { }
//mDatabase.unlock();
}
mDatabase = db;
} else {
//if (mDatabase != null) mDatabase.unlock();
if (db != null) db.close();
}
}
}
@Override
public synchronized SQLiteDatabase getReadableDatabase() {
if (mDatabase != null && mDatabase.isOpen()) {
return mDatabase; // The database is already open for business
}
if (mIsInitializing) {
throw new IllegalStateException("getReadableDatabase called recursively");
}
try {
return getWritableDatabase();
} catch (SQLiteException e) {
if (mName == null) throw e; // Can't open a temp database read-only!
Log.e(TAG, "Couldn't open " + mName + " for writing (will try read-only):", e);
}
SQLiteDatabase db = null;
try {
mIsInitializing = true;
String path = mContext.getDatabasePath(mName).getPath();
db = SQLiteDatabase.openDatabase(path, mFactory, SQLiteDatabase.OPEN_READONLY);
if (db.getVersion() != mNewVersion) {
throw new SQLiteException("Can't upgrade read-only database from version " +
db.getVersion() + " to " + mNewVersion + ": " + path);
}
onOpen(db);
Log.w(TAG, "Opened " + mName + " in read-only mode");
mDatabase = db;
return mDatabase;
} finally {
mIsInitializing = false;
if (db != null && db != mDatabase) db.close();
}
}
private SQLiteDatabase createOrOpenDatabase(boolean force) throws SQLiteAssetException {
SQLiteDatabase db = returnDatabase();
if (db != null) {
// database already exists
if (force) {
Log.w(TAG, "forcing database upgrade!");
copyDatabaseFromAssets();
db = returnDatabase();
db.setVersion(mNewVersion);
}
return db;
} else {
// database does not exist, copy it from assets and return it
copyDatabaseFromAssets();
db = returnDatabase();
db.setVersion(mNewVersion);
return db;
}
}
private SQLiteDatabase returnDatabase(){
try {
SQLiteDatabase db = SQLiteDatabase.openDatabase(mDatabasePath + "/" + mName, mFactory, SQLiteDatabase.OPEN_READWRITE);
Log.i(TAG, "successfully opened database " + mName);
return db;
} catch (SQLiteException e) {
Log.w(TAG, "could not open database " + mName + " - " + e.getMessage());
return null;
}
}
private void copyDatabaseFromAssets() throws SQLiteAssetException {
Log.e(TAG, "copying database from assets...");
try {
InputStream zipFileStream = mContext.getAssets().open(mArchivePath);
File f = new File(mDatabasePath + "/");
if (!f.exists()) { f.mkdir(); }
ZipInputStream zis = getFileFromZip(zipFileStream);
if (zis == null) {
throw new SQLiteAssetException("Archive is missing a SQLite database file");
}
writeExtractedFileToDisk(zis, new FileOutputStream(mDatabasePath + "/" + mName));
Log.e(TAG, "database copy complete");
} catch (FileNotFoundException fe) {
SQLiteAssetException se = new SQLiteAssetException("Missing " + mArchivePath + " file in assets or target folder not writable");
se.setStackTrace(fe.getStackTrace());
throw se;
} catch (IOException e) {
SQLiteAssetException se = new SQLiteAssetException("Unable to extract " + mArchivePath + " to data directory");
se.setStackTrace(e.getStackTrace());
throw se;
}
}
private void writeExtractedFileToDisk(ZipInputStream zin, OutputStream outs) throws IOException {
byte[] buffer = new byte[1024];
int length;
while ((length = zin.read(buffer))>0){
outs.write(buffer, 0, length);
}
outs.flush();
outs.close();
zin.close();
}
private ZipInputStream getFileFromZip(InputStream zipFileStream) throws FileNotFoundException, IOException {
ZipInputStream zis = new ZipInputStream(zipFileStream);
ZipEntry ze = null;
while ((ze = zis.getNextEntry()) != null) {
Log.e(TAG, "extracting file: '" + ze.getName() + "'...");
return zis;
}
return null;
}
}
使用时只需把实现准备好的sqlite文件压缩成zip包放在assets文件夹下的databases目录,然后定义一个Helper继承自SqliteAssetHelper,如下代码:
public class DBHelper extends SQLiteAssetHelper {
private static final String DATABASE_NAME = "bhdb.sqlite";
private static final int DATABASE_VERSION = 1;
public DBHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
}
升级的时候只需要改DATABASE_VERSION的值就好了。
优点:管理assets文件夹下的数据库简单方便,把sqlite文件以zip包的形式放在程序中,减少包大小。 缺点:每次更新只能覆盖原来的数据,及时是少量数据更新也是这种方式。如果少量数据更新的话打算以执行sql的方式来更新数据那可以参考下面这个项目, android-sqlite-asset-helper
在Android开发中,我们使用android.util.Log来打印日志,方便我们的开发调试。但是这些代码不想在发布后执行,我们并不想在软件发布后调试日志被其他开发者看到,现在我的方法是设置一个全局变量,标记软件为Debug模式还是Release模式。来看下代码:
public class Log {
private static final boolean DEBUG = true;
public static void i(String tag, String msg) {
if (DEBUG)
android.util.Log.i(tag, msg);
}
public static void e(String tag, String msg) {
if (DEBUG)
android.util.Log.e(tag, msg);
}
public static void d(String tag, String msg) {
if (DEBUG)
android.util.Log.d(tag, msg);
}
public static void v(String tag, String msg) {
if (DEBUG)
android.util.Log.v(tag, msg);
}
public static void w(String tag, String msg) {
if (DEBUG)
android.util.Log.w(tag, msg);
}
}
这样打包发布之前只要改下DEBUG=false就行了,但是每次在发布之前都要手动去改这个变量,不是很方便,而且不排除开发者忘记改的情况。那么有没有更好更方便的做法呢?
ADT(r17)发布以后,Google为我们提供了一种新的调试机制,即BuildConfig.DEBUG。
ADT 17.0.0的New build features第二条如下描述:
Added a feature that allows you to run some code only in debug mode. Builds now generate a class called BuildConfig containing a DEBUGconstant that is automatically set according to your build type. You can check the (BuildConfig.DEBUG) constant in your code to run debug-only functions.
即:新增了一个特性,允许开发者只在Debug模式下运行部分代码。Builds会生成一个叫做BuildConfig的类,该类包含一个名为DEBUG的常量,其常量值会依据开发者的Build类型自动设定。如此,便可以利用BuildConfig.DEBUG来实现只在Debug模式下运行的代码。
如果你的ADT已经更新到17及以上版本,可以尝试在Eclipse中新建一个Android工程,你会发现和R.java同级目录下多了一个叫做BuildConfig.java的类,其内容如下:
/** Automatically generated file. DO NOT MODIFY */
package com.boohee.one;
public final class BuildConfig {
public final static boolean DEBUG = true;
}
这样只需要改动一行代码就ok了,
private static final boolean DEBUG = BuildConifg.DEBUG;
在上面提到,DEBUG会根据Build类型自动设定。那么Build类型又从哪里区分呢?很简单,点开Eclipse的Project菜单便可见分晓,如下图:
可见,Build类型分为Build Project和Build Automatically,即手动和自动。
需要注意的是,如果直接通过Eclipse运行Project,则不论Build是手动还是自动,DEBUG均不会被设定为false。这是为什么呢?这就牵涉到Android 签名的问题,这里只简单提一下,不赘述:直接通过Eclipse运行Project,Eclipse会在工程Build完毕后在bin目录下生成一个apk,这个apk的签名是调试模式(debug mode),和发布模式(release mode)签名生成的apk略有不同。如此,该问题产生原因便浮出水面。
此时肯定会有人说,直接使用Android Tools–>Export Signed Application Package导出的release mode apk,其DEBUG就是false。这是不对的。在生成Release版时,需要区分Build的类型。如果选择的是自动Build,那么DEBUG仍然会被设定为true。所以在生成Release版时,请按照下面这个步骤进行打包,BuildConfig.DEBUG会被修改为false:
Project -> Build Automatically,即取消Build Automatically
Project -> Clean
Project -> Build
Android Tools -> Export Android application
我们都知道,在Android开发中,一般都使用sqlite来进行数据的存储与访问,而使用sqlite也可分为两种方式,一是在app中手动创建database,这种情况下大多使用SQLiteOpenHelper来进行数据库的创建与升级管理;另一种则是我们事先处理一份只读的数据库放在assets文件夹里,使用的时候则必须先把sqlite文件从assets文件里里拷贝到程序中才可使用,这种情况下的升级则是通过判断版本号来直接覆盖拷贝。
在one的项目中以上两种做法都用到了,本地放了一份只读的部分食物数据,大概1.1M左右,用户的记录数据如:饮食、运动、体重记录存储在自己创建的另一个数据库中。这看似没什么问题,实际测试中也没发现有什么异常。项目上线后后台监测会有一些异常出现,大概意思是food_groups找不到这个表之类的,由于异常次数比较少,加上本地测试也没重现,所以也没在意。但是这个异常每一个版本都会出现,加上偶尔有个别用户反馈食物查询不了,甚是纳闷。
通过询问几位用户以及后台错误的版本分布来看,大多集中在2.2。这个时候便定位到了问题,2.2版本一下的已经非常少了,实际测试时公司也没有那么低版本的手机了,所以一直没有重现,于是在模拟器上用2.1版本进行测试,果然查询模块无法查看。通过监测日志发现,在2.1版本copy数据库时失败,但是在2.3以上版本却一直正常。经过google查看了很多资料,加上自己实际的测试才发现在2.3以下版本的系统中放在assets文件夹里的数据库文件大小不能超过1M,于是经过重新整理把文件缩小到900多k,再在2.1版本的模拟器上运行,一些ok了。
安卓平台上的版本兼容问题确实是令人比较头痛的事,但是4.0之后这些兼容问题不再那么明显了,就目前来看我们的app还是要兼容大多数的系统版本的,后台的异常还是有很大作用的,在测试机器不充分的条件下,这种方式还是非常依赖的。相信等4.0以后的版本普及之后,兼容问题便不用花费太多精力了。
最近做数据同步时遇到一个问题,在下载数据时需要批量的向sqlite插入数据,虽然数据不算多,但是实际测试中每插入一条数据需要将近50ms的时间,这意味着100条数据就需要花费5s左右的时间,对于用户来说,体验太差了,必须要优化。
在google了之后,发现了sqlite的事务处理问题,在sqlite插入数据的时候默认一条语句就是一个事务,有多少条数据就有多少次磁盘操作。明白了这个,解决方案就有了,在批量插入数据的时候,只开启一个事务,这样只会进行一次磁盘操作,代码如下:
db.beginTransaction();
try {
for (...) {
db.execSQL("...", new Object[]{});
}
db.setTransactionSuccessful();
} catch (Exception e) {
} finally {
db.endTransaction();
}
使用事务后性能有明显的提升,以批量操作100条为例,由原来的5s优化成了现在的1s。