最近工作特忙,好久没静下心总结一些开发中的心得,后面会陆续写一些文章总结一下最近遇到的问题和一些收获吧~
闲话少说,今天想跟大家分享的是,在android中,如何后台将一个view绘制成图片,并简单梳理下其中遇到的坑。很多app都有这么一个功能,当用户完成了app的某个任务时,产品希望用户点击分享的时候,能动态绘制出一张图片,让用户的分享的内容更加生动化。举个例子,比如扇贝单词的打卡,点击分享到新浪微博的时候,app会动态在后台生成一张图片,用户确认分享就会将这张图片分享出去。首先确认一下分享的图片上包含的元素吧:
- ui提供的图片素材
- 用户的信息,比如昵称,头像等
- 本次任务完成的数据,比如跑了多少km啊,背了多少单词啊,复杂点的话还会包括一张网络图片,需要加载完毕后再生成指定的图片
比如说向下图这样(这算个广告吧)
首先可以确认的是,直接在View上布局不是一件难事,需要在代码中操作的信息如前面提到的用户信息啊,本次任务的数据啊,和两张需要异步加载的图片(轨迹图和头像)。
首先这不是一个简单的截屏,有些app会将分享的图片先展示给用户,然后当前页面“截屏”,生成一张图片,然后调取第三方的图片分享,总结来说要么是通过View.getDrawingCache()方法拿到当前View的缓存,要么是直接调用Bitmap.createBitmap()生成Bitmap。我们简单分析一下这两种做法:
方法1 View.getDrawingCache() 只适用于分享的View已经完整展示在用户的屏幕上,超出屏幕范围内的内容是不在生成的Bitmap内的。因为android手机的屏幕尺寸差异太大,通常我们需要生成的图片不会很短,所以很难保证这点,同时如果当前展示的View和最终生成的图片有一些差异的话,比如某个按钮不显示,某个文字换个内容等,就没办法用这种办法了。
第二种其实也是我们最终采用的方式,不过没那么简单,先来看这样一种做法,在实践中证明存在挺多问题。不过确实有人在采用,还是说一下吧
假设我当前是在A页面,我要分享出去的B图片和A页面只需要隐藏分享按钮,这种方法的做法是:
vShare.setVisibility(INVISIBLE);
Bitmap b = Bitmap.createBitmap( v.getLayoutParams().width, v.getLayoutParams().height, Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(b);
v.layout(v.getLeft(), v.getTop(), v.getRight(), v.getBottom());
v.draw(c);
return b;
说一下问题在哪,首先生成Bitmap的操作应该是后台异步操作,当前app应该有一个阻断态,我们会发现,用户会观察到分享按钮消失了,然后生成图片后又再次出现,一个按钮也许还ok,如果界面上的差异很大,这种方式给用户的体验就很不好。其次,也是最重要的,通常我们View的布局的宽高都是类似于macth_parent,wrap_content, 分享出去的图片尺寸无法控制(完全取决于手机的屏幕尺寸),图片中的一些元素的宽度(例如异步加载的网络图片)经过试验发现是异常的,原因我暂时还不清楚,大家可以和我探讨一下。
像我这个项目中需要生成的图片,和分享页面可以说差别非常大,那么我们该如何处理呢?
首先单独写一个布局,宽高全都是固定值。我设置的宽高单位是dp,也就是说我生成的图片的实际宽高取决于用户手机的屏幕密度,好处在于低配手机通常性能是首要考虑目标,尺寸过大很容易导致OOM,这些低配手机的屏幕密度一般都不高,而同时高配手机上,生成的分享图片如果不够清晰,给用户的体验就很不好(想像一下高清屏幕上的颗粒图吧)。因为涉及到数据的展示,我这边采取自定义View的方式,假设名称叫ShareView,它需要对外暴露这样几个方法:
- 根据用户是否登陆,绘制不同的样式
- 接收相关数据,填充指定View
- 提供一个生成分享图片链接Uri的方法,并在适当的时机,回调外部的生成图片成功或失败的回调方法。
思路确定,这里只提供最核心的代码:
/**
* 创建分享的图片文件
*/
public String createShareFile() {
Bitmap bitmap = createBitmap();
//将生成的Bitmap插入到手机的图片库当中,获取到图片路径
String filePath = MediaStore.Images.Media.insertImage(getContext().getContentResolver(), bitmap, null, null);
//及时回收Bitmap对象,防止OOM
if (!bitmap.isRecycled()) {
bitmap.recycle();
}
//转uri之前必须判空,防止保存图片失败
if (TextUtils.isEmpty(filePath)) {
return "";
}
return getRealPathFromURI(getContext(), Uri.parse(filePath));
}
/**
* 创建分享Bitmap
*/
private Bitmap createBitmap() {
//自定义ViewGroup,一定要手动调用测量,布局的方法
measure(getLayoutParams().width, getLayoutParams().height);
layout(0, 0, getMeasuredWidth(), getMeasuredHeight());
//如果图片对透明度无要求,可以设置为RGB_565
Bitmap bitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
draw(canvas);
return bitmap;
}
private static String getRealPathFromURI(Context context, Uri contentUri) {
Cursor cursor = null;
try {
String[] proj = {MediaStore.Images.Media.DATA};
cursor = context.getContentResolver().query(contentUri, proj, null, null, null);
if (cursor == null) {
return "";
}
int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
cursor.moveToFirst();
return cursor.getString(column_index);
} finally {
if (cursor != null) {
cursor.close();
}
}
}
核心代码交代完了,说一下一些小点吧:
- 网络图片需要加载完成后再回调生成图片成功的方法,例如在Glide的RequestListener等。
- 从职责单一的角度,我们需要定义一个图片分享管理器的类,类似于ImageShareManager,诸如处理子线程创建分享图片,管理生成的图片(登陆/未登录),具体如何布局代码,我想大家都有自己的想法~
- 由于内存考虑和第三方分享图片时的限制,生成的图片大小需要在实践中自己把握,像我项目中现在分享出去的图片宽高为360dp*892dp, 1080p的手机生成的图片大概150k,基本上都能成功分享。只遇到2k屏幕的手机,朋友圈分享图片失败(微信聊天,qq,qq空间,新浪微博都可以),于是针对2k屏幕,判断下屏幕密度,如果大于3.0的,我会采用宽高缩小的布局文件。我在项目中尝试过直接缩放Bitmap,结果发现图片质量模糊的非常厉害,UI无法接受,应该是强制缩放的效果本身就很差,建议还是对这部分手机单独处理吧。
- 分享到微信聊天的图片,需要设置缩略图,否则对方聊天界面不打开大图是看不到东西的,另外,网上所谓的32k的限制我没遇到,就算是也肯定指缩略图,原图不超过200k应该还是可以的。(吐槽一下当时几个同事都信誓旦旦说图片不能超过32k,害得我折腾了好久,又是缩小布局,又是缩放Bitmap,最后发现原图根本没那个限制,转过头来问那几个同事,语气又不那么确定了...总之不能轻信很多没经过验证的说法)
谢谢大家