【worktile开发小记】TextView中Markdown的显示
1.引言
在worktile中,无论是免费版还是企业版,在显示任务、日程、文档、评论等等这些的描述的时候,都可以支持markdown,然而一般在java中的markdown解析,可能会有很多人用markdown4j这个第三方库,但是我没有使用这个库,而是使用了Bypass这个库。这是一个用C语言编写的Markdown解析第三方库,声称速度是Markdown4j此类使用Html的库的4倍。
2.Bypass
注意: 使用Bypass需要会使用NDK
1.1 编译
因为这个第三方库是用C语言编写的,所以在Android上使用的时候,需要编译好的so包,幸运的是,这个库已经提供好了Android.mk文件,所以我们可以很方便地编译Bypass库。
不过,这个库需要boost的支持,需要boost安装在系统上。详情请见:安装boost
1.2 java和jni
除了C语言库之外,Bypass还需要用jni来调用C语言函数,用SpannableString来负责在Android TextView显示不同的样子,所以还需要一个java library。
一般来说,在Android中引入java library,只需在gradle dependencies加入就好,可是在我这里总是有错误(我不清楚原因是什么,也许可能是因为我使用gradkle-experimental的原因),所以我就把Bypass的源代码复制到了工程中(同时我也修改了部分源代码),但是要注意路径,因为jni的函数名是Java_in_uncod_android_bypass开头的。
我的目录结构如下:
1.3 How-To
3.ImgeGetter
如果Markdown中包含图片,需要使用IamgeSpan将占位符变为图片,这时候需要使用ImageGetter来从内存、文件、网络等等来获取并处理图片。以下贴出我的ImageGetter代码(以下代码有一部分是很久前我在网上看到的,原作者已经不记得了,如果原作者看到并认为我侵权,我会立即从博客上删除或者修改代码)
public class Markdown {
public static class MarkdownImageGetter implements Bypass.ImageGetter {
private String cacheDirPath;
Context context;
TextView textView;
public MarkdownImageGetter(Context context, TextView tv) {
this.context = context;
this.textView = tv;
cacheDirPath = context.getApplicationContext().getCacheDir().getPath() + "/image/";
}
@Override
public Drawable getDrawable(String source) {
// 1、取缓存
if (LCApplication.mTaskDetailImageMap.containsKey(source)) {
SoftReference<Bitmap> cacheBitmap = LCApplication.mTaskDetailImageMap
.get(source);
Bitmap softBitmap = null;
if (cacheBitmap != null) {
softBitmap = cacheBitmap.get();
Drawable softDrawable = new BitmapDrawable(softBitmap);
softDrawable.setBounds(getDefaultBitmapRounds(softDrawable, context));
return softDrawable;
}
}
// 2、取文件
String fileName = source.substring(source.lastIndexOf("/") + 1);
File file = new File(cacheDirPath + fileName);
if (file.exists()) {
Bitmap fileBitmap = BitmapUtils.createImageThumbnail(cacheDirPath + fileName);
Drawable fileDrawable = new BitmapDrawable(fileBitmap);
fileDrawable.setBounds(getDefaultBitmapRounds(fileDrawable, context));
return fileDrawable;
}
// 3、取网络请求
URLDrawable uRLDrawable = new URLDrawable();
LoadImageAsync loadBitmap = new LoadImageAsync(uRLDrawable);
loadBitmap.execute(source, fileName);
return uRLDrawable;
}
/**
* 图片保存到本地
*
*/
public void saveBitmap2File(InputStream in, String dirPath, String fileName)
throws IOException {
if (in == null) {
return;
}
if (dirPath == null || dirPath.equals("")) {
return;
}
if (fileName == null || fileName.equals("")) {
return;
}
if (!Environment.getExternalStorageState().equals(
Environment.MEDIA_MOUNTED)) {
return;
}
File dir = new File(dirPath);
if (!dir.exists()) {
dir.mkdirs();
}
File file = new File(dirPath + fileName);
if (!file.exists()) {
file.createNewFile();
}
OutputStream out = new FileOutputStream(file);
byte[] buffer = new byte[1024];
int length = -1;
while ((length = in.read(buffer)) != -1) {
out.write(buffer, 0, length);
}
out.flush();
out.close();
}
//网络下载图片
public class LoadImageAsync extends AsyncTask<String, Void, Drawable> {
URLDrawable mUrlDrawable;
public LoadImageAsync(URLDrawable mUrlDrawable) {
this.mUrlDrawable = mUrlDrawable;
}
@Override
protected Drawable doInBackground(String... arg0) {
String url = arg0[0];
String fileName = arg0[1];
URL Url;
Drawable drawable = null;
try {
Url = new URL(url);
InputStream in = Url.openStream();
// 保存到文件
saveBitmap2File(in, cacheDirPath, fileName);
in.close();
File file = new File(cacheDirPath + fileName);
if (file.exists()) {
Bitmap bitmap = BitmapUtils.createImageThumbnail(
cacheDirPath + fileName);
// 保存到缓存
if (!LCApplication.mTaskDetailImageMap
.containsKey(url)) {
LCApplication.mTaskDetailImageMap.put(url,
new SoftReference<Bitmap>(bitmap));
}
drawable = new BitmapDrawable(bitmap);
drawable.setBounds(getDefaultBitmapRounds(drawable, context));
}
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return drawable;
}
@Override
protected void onPostExecute(Drawable result) {
if (result != null) {
mUrlDrawable.drawable = result;
MarkdownImageGetter.this.textView.invalidate();
MarkdownImageGetter.this.textView.setHeight((MarkdownImageGetter.this.textView.getHeight()
+ result.getIntrinsicHeight()));
}
}
}
public class URLDrawable extends BitmapDrawable {
protected Drawable drawable;
@Override
public void draw(Canvas canvas) {
if (drawable != null) {
drawable.draw(canvas);
}
}
}
//图片显示大小
public Rect getDefaultBitmapRounds(Drawable drawable, Context context) {
int right = 0;
int bottom = 0;
float lengthWidthRatio;
WindowManager windowManager = ((Activity)context).getWindowManager();
if (drawable != null) {
lengthWidthRatio = (float) drawable.getIntrinsicHeight() / (float) drawable.getIntrinsicWidth();
right = windowManager.getDefaultDisplay().getWidth() - 2 * UnitConversion.dp2px(context, 16);
bottom = (int)(right * lengthWidthRatio);
}
return new Rect(0, 0, right, bottom);
}
}
}
其中,LCApplication.mTaskDetailImageMap是一个key为url,value为java弱引用的HaskMap,作用显而易见。
另外,有可能图片过大,所以无论如何,我都会对图片做处理,压缩之后的图片长宽最大都是800px,以下是图片压缩的代码:
public static Bitmap createImageThumbnail(String filePath) {
Bitmap bitmap = null;
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inJustDecodeBounds = true;
BitmapFactory.decodeFile(filePath, opts);
opts.inSampleSize = computeSampleSize(opts, -1, 800 * 800);
opts.inJustDecodeBounds = false;
try {
bitmap = BitmapFactory.decodeFile(filePath, opts);
} catch (Exception e) {
// TODO: handle exception
}
return bitmap;
}
public static int computeSampleSize(BitmapFactory.Options options, int minSideLength, int maxNumOfPixels) {
int initialSize = computeInitialSampleSize(options, minSideLength, maxNumOfPixels);
int roundedSize;
if (initialSize <= 8) {
roundedSize = 1;
while (roundedSize < initialSize) {
roundedSize <<= 1;
}
} else {
roundedSize = (initialSize + 7) / 8 * 8;
}
return roundedSize;
}
private static int computeInitialSampleSize(BitmapFactory.Options options, int minSideLength, int maxNumOfPixels) {
double w = options.outWidth;
double h = options.outHeight;
int lowerBound = (maxNumOfPixels == -1) ? 1 : (int) Math.ceil(Math.sqrt(w * h / maxNumOfPixels));
int upperBound = (minSideLength == -1) ? 800 : (int) Math.min(Math.floor(w / minSideLength), Math.floor(h / minSideLength));
if (upperBound < lowerBound) {
// return the larger one when there is no overlapping zone.
return lowerBound;
}
if ((maxNumOfPixels == -1) && (minSideLength == -1)) {
return 1;
} else if (minSideLength == -1) {
return lowerBound;
} else {
return upperBound;
}
}
关于ImageGetter我没有过多的文字描述,各位看代码就好,如果我所写的有什么不对不好不清楚的地方,欢迎交流。