Android制作卡片类长图方法设计

在实现需求过程中,产品喜欢上了分享大图卡片。但是发现每个地方分享的卡片大图还不一样。经过分析发现还是能够找到一点共性。先上图看看最基础的卡片样子。由于分享的卡片在四张图片上下都需要自定义一些其他控件,主要有文字商品信息等等。

在实现之前先思考了一下,需要解决的问题有以下几个:

  1. 因为每个地方的分享大图是不一样的,需要找出共性部分,并将不同之处方便替换;
  2. 在卡片中的几张图片还没下载完就吊起分享肯定是不行的。这就需要监听图片下载了;
  3. 分享到的App对大图都是有大小限制的,其中微博限制最大2M,这样就需要进行图片压缩。

公共部分

在实现的过程中,需要一个layout文件,先将公共部分设置好,像上半部分和下半部分都是共有的,所以直接放在布局中。在布局的最外层需要使用到ScrollView,因为卡片可能会很长,只有ScrollView能够达到这种长图效果。此时在布局中需要预留出一个位置来放置下载好的图片,预留的包裹图片的控件叫做mShareImgContainer,在mShareImgContainer的上下也需要各预留一个ViewGroup,放置会变化的东西。公共的部分都很好做,下面仔细说一下下载图和放置图的处理部分。

非公共部分

图2

如果没有非公共部分,可以不使用此方法。非公共部分由于分享的内容不同会经常变动ui效果,因此这里采用了策略模式,将分享图片占位的顶部和底部留出,作为自定义部分,要使用此方法的需要自定义上下两部分的可以自制一个策略类,并将此方法设置到ShareImgManager中。具体代码如下:

ShareImgManager shareImgManager = new ShareImgManager(getContext());
shareImgManager.setShareImgStrategy(new ShareUserCommentImg(getContext(), goodsComment, mShowGoodsComment.getGoods()));//自定义ui部分
shareImgManager.addImgBottomPart();

其中的ShareUserCommentImg类实现了ShareImgManager.ShareImgStrategy接口,接口包含了两个方法,分别是分享大图的上下两个部分,可以将相应的view指定进去。只要返回相应的view,在ShareImgManager里面会将这两部分加入进去,看下这个类的具体代码:

public class ShareUserCommentImg implements ShareImgManager.ShareImgStrategy {
private Context mContext;
private GoodsComment mGoodsComment;
private CommentGoods mCommentGoods;
public ShareUserCommentImg(Context context, GoodsComment goodsComment, CommentGoods commentGoods) {
    this.mContext = context;
    mGoodsComment = goodsComment;
    mCommentGoods = commentGoods;
}

@Override
public View imgTopPart() {
    return null;
}

@Override
public View imgBottomPart() {
    if (mCommentGoods == null) return null;
    View mShareView = LayoutInflater.from(mContext).inflate(R.layout.share_user_comment_img_bottom_part, null);
    TextView commentGoodsTitle = (TextView) mShareView.findViewById(R.id.comment_goods_title);
    KaolaImageView belongGoodsIv = (KaolaImageView) mShareView.findViewById(R.id.belong_goods_iv);
    TextView commentGoodsPrice = (TextView) mShareView.findViewById(R.id.comment_goods_price);
    TextView shareCommentContent = (TextView) mShareView.findViewById(R.id.share_comment_content);
    ImageLoaderManager.startLoad(new ImageLoaderBuilder().setKaolaImageView(belongGoodsIv)
            .setImgUrl(mCommentGoods.getImageUrl()).setWidthHeight(35, 35));
    commentGoodsPrice.setText("¥" + StringUtils.formatFloat(mCommentGoods.getActualCurrentPriceForApp()));
    commentGoodsTitle.setText(mCommentGoods.getTitle());
    shareCommentContent.setText(Html.fromHtml("<font color=\"#D22147\">@" + mGoodsComment.getNicknameKaola() + ": </font>" +
            mGoodsComment.getCommentContent()));
    return mShareView;
}

}

显示图片的部分

创建好加载图片的ImageView控件之后,要add进去。此时我在做的时候发现了一个问题就是控件虽然add进去了,也看到占了很大的位置,但是只能显示一片空白,并没有图片显示。后来发现,是因为我们使用的是Facebook的Fresco下载图片框架问题,fresco在控件未attach到当前视图的时候是不会去下载图片的,要在ondraw之后才会去下载图片,因此在塞完控件之后只要draw一下,fresco就会异步去下载图片。看一下这个方法的代码:

public static Bitmap getBitmap(ScrollView scrollView) {
    int width = scrollView.getWidth();
    int height = scrollView.getHeight();
    scrollView.setBackgroundColor(Color.WHITE);
    if (0 == width || 0 == height) {
        scrollView.measure(View.MeasureSpec.makeMeasureSpec(750, View.MeasureSpec.EXACTLY),
                View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
        scrollView.layout(0, 0, scrollView.getMeasuredWidth(), scrollView.getMeasuredHeight());
        width = scrollView.getWidth();
        height = scrollView.getHeight();
    }
    Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
    final Canvas canvas = new Canvas(bitmap);
    scrollView.draw(canvas);
    return bitmap;
}

此时虽然进行了measure等一系列操作,但是只是占了位置,图片还没显示上去,所以要等图片下载完成之后再执行一遍才可以看到图片。

接下来就是下载图片了,在下载图片的时候需要一个计数器,只有图片全部下载完成之后才吊起分享功能。不然会出现某一个白色的情况。

计数器到达规定数量的时候,意味着图片已经下载完,此时再次调用上面的方法,重新绘制一遍,就会生成一个可分享的Bitmap,但是此时还并不能作为分享图片,因为图片可能过大,所以需要压缩。

压缩图片的部分

图片压缩方法主要有两种:

1.质量压缩

质量压缩,方法如下:

image.compress(Bitmap.CompressFormat.JPEG, 50, baos);

其中第一个参数为所需要压缩的图片格式,可选三个格式JPEG,PNG和WEBP,试过这三个格式,发现JPEG的压缩方式是最好的。PNG是无损压缩,WEBP如果需要压缩的话,耗时是JPEG的好几倍,而且压缩效果也不好。所以不是推荐的压缩格式。第二个参数是需要压缩到的图片质量,第三个参数是图片输入流。

关于压缩的原理,看了一下源码,发现谷歌使用的也是libjpeg这个被广泛使用的开源JPEG图像处理库,android并没有直接使用libjpeg,而是使用了一个封装库Skia,通过Skia来使用libjpeg的。Skia对libjpeg进行了良好的封装,基于这个引擎可以很方便为操作系统、浏览器等开发图像处理功能。libjpeg在压缩图像的时候,有一个参数叫optimize_coding,官方文档中是这样写的:

boolean optimize_coding

TRUE causes the compressor to compute optimal Huffman coding tables
for the image. This requires an extra pass over the data and
therefore costs a good deal of space and time. The default is
FALSE, which tells the compressor to use the supplied or default
Huffman tables. In most cases optimal tables save only a few percent
of file size compared to the default tables. Note that when this is
TRUE, you need not supply Huffman tables at all, and any you do
supply will be overwritten.

意思就是如果设置optimize_coding为TRUE,将会使得压缩图像过程中基于图像数据计算哈弗曼表,由于这个计算会显著消耗空间和时间,默认值被设置为FALSE。谷歌的Skia项目工程师们最终没有设置这个参数,optimize_coding在Skia中默认的等于了FALSE,这就意味着更差的图片质量和更大的图片文件。关于为什么Android图片比ios差,可以参考这篇文章:为什么Android的图片质量会比iPhone的差?

采用这种方法进行压缩,并不会减少图片的像素,官方文档也解释说, 它会让图片重新构造, 但是有可能图像的位深(即色深)和每个像素的透明度会变化,也就是说以jpeg格式压缩后, 原来图片中透明的元素将消失.所以这种格式很可能造成失真。

2.尺寸压缩

尺寸压缩主要使用到的是BitmapFactory.Options这个参数,BitmapFactory.Options的成员变量主要有:

public Bitmap inBitmap;//重用Bitmap
public boolean inJustDecodeBounds;//设置只去读图片的附加信息(宽高),不去解析真实的Bitmap
public int inSampleSize;// 设置图片的缩放比例(宽和高)
public int inDensity;
public int inTargetDensity;//inScaled设置为true的时候,如果InDensity和inTragetDensity都不是0并且不相等,那么bitmap就会被放缩成inTargtDensity
public int inScreenDensity;//正在使用的屏幕的像素密度
public boolean inScaled;
public int outWidth;
public int outHeight;
public String outMimeType;

inJustDecodeBounds:这个参数设置为true的时候,BitmapFactory通过decodeResource或者decodeFile解码图片时,将会返回空(null)的Bitmap对象,这样可以避免Bitmap的内存分配,但是它可以返回Bitmap的宽度、高度以及MimeType。

inSampleSize:在获取了图片的宽高之后,在知道屏幕宽高的情况下,就可以通过两个的比例对图片进行压缩到屏幕尺寸进行显示。inSampleSize就是缩放比例,但是这个变量必须设置2的倍数,如果设置的非2的倍数,他也会自动变换成最近的2的指数倍数。例如:inSampleSize=4,宽高都会变为原来的1/4,整体就会缩小16倍,压缩比例还是很可观的,但是压缩到之前的16倍效果上就会差很多。

inPreferredConfig:

Bitmap.Config ARGB_4444:每个像素占四位,即A=4,R=4,G=4,B=4,那么一个像素点占4+4+4+4=16位

Bitmap.Config ARGB_8888:每个像素占四位,即A=8,R=8,G=8,B=8,那么一个像素点占8+8+8+8=32位

Bitmap.Config RGB_565:每个像素占四位,即R=5,G=6,B=5,没有透明度,那么一个像素点占5+6+5=16位

Bitmap.Config ALPHA_8:每个像素占四位,只有透明度,没有颜色。

3.我使用的压缩方法
public static Bitmap compressImage(Bitmap image, int minSize) {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    image.compress(Bitmap.CompressFormat.JPEG, 100, baos);
    int bitmapSize = baos.toByteArray().length / 1024;
    if (bitmapSize / minSize < 4) {//图片需要缩小到1/4以内,用质量压缩
        int options = minSize * 200 / bitmapSize;
        while (baos.toByteArray().length / 1024 > minSize && options > 0) {
            // 重置baos
            baos.reset();
            // 这里压缩options%,把压缩后的数据存放到baos中
            image.compress(Bitmap.CompressFormat.JPEG, options, baos);
            // 每次都减少10
            options -= 10;
        }
        byte[] array = baos.toByteArray();
        if (options >= 30) {
            return decodeByteArray(array, 0, array.length);
        }
    }
    //图片需要缩小到1/4以上,用尺寸压缩
    BitmapFactory.Options opts = new BitmapFactory.Options();
    opts.inPreferredConfig = Bitmap.Config.RGB_565;
    opts.inSampleSize = 1;
    Bitmap bitmap = image;
    image.compress(Bitmap.CompressFormat.JPEG, 100, baos);
    byte[] array = baos.toByteArray();
    while (bitmap.getByteCount() / 1024 > 2048) {
        opts.inSampleSize *= 2;
        bitmap = BitmapFactory.decodeByteArray(array, 0, array.length, opts);
    }
    return bitmap;
}

分享方法的使用

接下来就是ShareImgManager的使用过程,因为之前已经设置好了公共和非公共部分,所以在这里只要设置部分数据,并且调用createImgCard方法即可,看下代码:

ShareImgManager shareImgManager = new ShareImgManager(getContext());
shareImgManager.setShareImgStrategy(new ShareUserCommentImg(getContext(), goodsComment, mShowGoodsComment.getGoods()));//自定义ui部分
shareImgManager.addImgBottomPart();
ShareImgManager shareImgManager = new ShareImgManager(getContext());
shareImgManager.setShareImgStrategy(new ShareUserCommentImg(getContext(), goodsComment, mShowGoodsComment.getGoods()));//自定义ui部分
shareImgManager.addImgBottomPart();
shareImgManager.setData(getContext(), "http://www.kaola.com/product/" + mShowGoodsComment.getGoods().getGoodsId() + ".html",R.drawable.ic_comment_share_head);//设置二维码链接和头部的背景图
shareImgManager.createImgCard(getContext(), imgUrls, new ShareImgManager.ShareImgListener() {
      @Override
      public void createPicSuccess(String imgName) {
           GoodsDetailUtils.shareComment(getActivity(), imgName, mCommentListLv);
      }

      @Override
      public void createPicFailed() {
           ToastUtils.show("网络请求失败");
      }
});

参考文档和延伸阅读

  1. App图片压缩裁剪原理和上传方案,以及那些有趣的事儿...
  2. Android图片编码机制深度解析(Bitmap,Skia,libJpeg)
  3. Android Bitmap 优化(1) - 图片压缩
  4. https://github.com/bither/bither-android-lib
  5. 非常全面的 Android Bitmap 知识点梳理
  6. 你的 Bitmap 究竟占多大内存?
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,398评论 25 707
  • 先发一张昨天去看我雷哥演唱会的皂片然后再说正文哈哈。 简介 由于工作原因,boss下达的任务就大概说了对图片进行压...
    我叫王菜鸟阅读 5,201评论 2 16
  • 此文章首发: https://mp.weixin.qq.com/s/QZ-XTsO7WnNvpnbr3DWQmg ...
    Shawn_Dut阅读 1,684评论 1 10
  • 测试覆盖率,简单的说,就是评价测试活动覆盖产品代码的指标。测试的目的,是确认产品代码按照预期一样工作,也可以看作是...
    一个坏员工阅读 2,145评论 1 2
  • 这几年,随着互联网的流行,人们的真实交流越来越少,微信越来越多。提到微信,我们不得不想到红包和微商这两个词,且不论...
    笑雨阅读 92评论 0 0