Android自定义View分享——仿微信朋友圈图片合并效果

写在前面

笔者近来在学习Android自定义View,收集了一些不算复杂但又“长得”还可以的自定义View效果实现,之前分享过两个效果:一个水平的进度条,一个圆形温度显示器,如果你有兴趣的话可以看看:
一个水平的进度条
一个圆形温度显示器

今天我要来分享的是这样的效果——图片合并
这里写图片描述
当你使用微信进入某一个人的朋友圈列表,如果他们某条信息所携带的图片超过一张,就会做成如图所示这种显示效果,是将多张图片合并在一起形成的预览效果。当然对于朋友圈的实现方式我不清楚,有可能是服务器端已经合并好图片了,返回给客户端的就是一张合并之后的图片,只要一个ImageView就可以了。不过对于自定义View的学习,你是否想过,如果服务器给你的是若干张图片,你要自己拼接成一张,该怎么搞,今天我们来讲这个。

所涉及的知识点

其实在绘图方面本文所牵扯到的知识点还没有多少,反而是关于bitmap的内存占用,控制显得更加重要。总的来说大概如下:

  1. canvas.drawBitmap()方法里面各个参数含义。
  2. 如果要显示的图片区域(或者像素点)明显的比原图片小(因为我们只是做缩略图),怎么节约内存使用。这将涉及到加载"bitmap"时利用"BitmapFactory.Options"对象计算压缩的比例等等知识。
  3. 一些很简单的数学计算,如前面的图片所示,是需要在一、二、三、四张图片的情况下,计算裁剪图片大小以及摆放在什么样的位置上。

设计思路

想象一下,如果我现在给你一张白纸(canvas,其实如果我说一个相框,可能更方便你想象),再给你一到四张照片,你要怎么“摆”出我的截图里的效果。其实很简单,笔者提出思路如下:

  1. 首先你需要判断下图片张数,然后分别进行处理。
  2. 如果是一张图片,我们就将整张原图“绘制”到我们的canvas上面。
  3. 如果是两张图片,我们就将它们分别横向压缩一半,然后分别绘制到canvas里面,每张图片占一半位置。
  4. 如果是三张图片,将第一张压缩一半,绘制到canvas的左半边,另外两张图片压缩成原来的四分之一,绘制到canvas右上角,右下角。
  5. 如果是四张图片,将四张图片全部都压缩成原来的四分之一,绘制到canvas的四个角上。
  6. 为了美观,图片之间画条白线分隔一下。

一步一步,切分代码

关于图片源,在你的项目当中,图片来源大多应该来自网络,不过作为一个样例,贪图方便(方便偷懒),图片来源直接来自本地,在drawable文件夹下面,所以我在生成bitmap时,调用的是"BitmapFactory.decodeResource()"方法。如果你的图片来自网络,可能需要别的方法,还有,如果你是用glide之类网络框架下载图片,请搞清楚这些框架下载图片之后对图片所做的事情,笔者曾经在实际项目里,使用一些框架导致数据错乱,需要另外进行其他调试。

  • 一张图片时的代码片段
    decodeSampledBitmapFromResource()方法是个自定义的内部方法,根据实际需要尺寸加载图片,用来防止内存耗尽,这个将在稍后展开来讲。
if(length == 1){
	//如果只有一张图片,则将该图片裁剪成合适大小,直接绘制就可以了
	bitmap = decodeSampledBitmapFromResource(drawableIds[0], measuredWidth, measuredHeight);
	//要截取的原图片的范围
	srcRect.set(0, 0, bitmap.getWidth(), bitmap.getHeight());
	//图片绘制在canvas上的范围
	dstRect.set(0, 0, measuredWidth, measuredHeight);
	canvas.drawBitmap(bitmap, srcRect, dstRect, bitmapPaint);
}
  • 两张图片时的代码片段
    两张图片,图片的最终宽度就成了控件宽度减去白色分隔线的宽度,再除以2。
if(length == 2){
	//如果有两张图片,则两张图片各占左右一半位置,中间画一条分隔线
	//两张图片中间分隔线的宽度
	int lineWidth = 4;
	//图片的目标宽度
	int dstWidth = (measuredWidth-lineWidth)/2;
	//绘制第一张图片
	bitmap = decodeSampledBitmapFromResource(drawableIds[0], dstWidth, measuredHeight);
	srcRect.set(0, 0, bitmap.getWidth(), bitmap.getHeight());
	dstRect.set(0, 0, dstWidth, measuredHeight);
	canvas.drawBitmap(bitmap, srcRect, dstRect, bitmapPaint);
	//绘制分割线
	linePaint.setColor(Color.WHITE);
	canvas.drawLine(dstWidth, 0, dstWidth+lineWidth, getMeasuredHeight(), linePaint);
	//绘制第二张图片
	bitmap = decodeSampledBitmapFromResource(drawableIds[1], dstWidth, getMeasuredHeight());
	srcRect.set(0, 0, bitmap.getWidth(), bitmap.getHeight());
	dstRect.set(dstWidth+lineWidth, 0, getMeasuredWidth(), getMeasuredHeight());
	canvas.drawBitmap(bitmap, srcRect, dstRect, bitmapPaint);
}

三张图片和四张图片的情况,类似,只要按着上面的逻辑来,就差不多。是不是感觉很简单?确实,其实相当一部分的自定义View,不像想象中的那么复杂。而且这次分享的这一个效果,其重点也并不在绘制的逻辑上面,而是在于从一个来源(或者说叫做大小,尺寸)不确定的图片上面,根据你自己需要的大小,加载、裁剪出合适尺寸的图片,同时还要考虑内存占用,不要发生OOM。

加载尺寸不确定的Bitmap时的内存占用问题

解决这个问题的思路是:

  1. 先将"BitmapFactory.Options"对象的"inJustDecodeBounds"属性设置为true,这样子能获取图片相关信息。
  2. 根据我们所需要的最终尺寸,以及图片原来信息,计算以及设置压缩比例。
  3. 设置好压缩比例,将刚才的那个属性设置为false,将一个“比较小的”bitmap给加载进来。
  4. 通过"Bitmap.createScaledBitmap(Bitmap, int , int , boolean)"方法,得到最终我们要的尺寸的Bitmap。
    关于这个解决思路背后的原理,推荐参考这篇博客:
    根据ImageView的大小来压缩Bitmap,避免OOM
    具体代码跟下面的完整用例贴在一起。

完整用例

类代码

public class MergePictureView extends View{

    //要显示的图片资源数组(即要合并的图片)
    private int[] drawableIds;
    //裁剪图片时的裁剪区域
    private Rect srcRect = new Rect();
    //要将图片绘制到哪一个区域
    private Rect dstRect = new Rect();
    private Paint bitmapPaint = new Paint();
    private Paint linePaint = new Paint();

    public MergePictureView(Context context) {
        super(context);
    }

    public MergePictureView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getMeasureSize(widthMeasureSpec), getMeasureSize(heightMeasureSpec));
    }

    @Override
    protected void onDraw(Canvas canvas) {
        if(drawableIds == null || drawableIds.length == 0){
            super.onDraw(canvas);
            return;
        }
        int length = drawableIds.length;
        int measuredWidth = getMeasuredWidth();
        int measuredHeight = getMeasuredHeight();
        Bitmap bitmap;
        if(length == 1){
            //如果只有一张图片,则将该图片裁剪成合适大小,直接绘制就可以了
            bitmap = decodeSampledBitmapFromResource(drawableIds[0], measuredWidth, measuredHeight);
            srcRect.set(0, 0, bitmap.getWidth(), bitmap.getHeight());
            dstRect.set(0, 0, measuredWidth, measuredHeight);
            canvas.drawBitmap(bitmap, srcRect, dstRect, bitmapPaint);
        }else if(length == 2){
            //如果有两张图片,则两张图片各占左右一半位置,中间画一条分隔线
            //两张图片中间分隔线的宽度
            int lineWidth = 4;
            //图片的目标宽度
            int dstWidth = (measuredWidth-lineWidth)/2;
            //绘制第一张图片
            bitmap = decodeSampledBitmapFromResource(drawableIds[0], dstWidth, measuredHeight);
            srcRect.set(0, 0, bitmap.getWidth(), bitmap.getHeight());
            dstRect.set(0, 0, dstWidth, measuredHeight);
            canvas.drawBitmap(bitmap, srcRect, dstRect, bitmapPaint);
            //绘制分割线
            linePaint.setColor(Color.WHITE);
            canvas.drawLine(dstWidth, 0, dstWidth+lineWidth, getMeasuredHeight(), linePaint);
            //绘制第二张图片
            bitmap = decodeSampledBitmapFromResource(drawableIds[1], dstWidth, getMeasuredHeight());
            srcRect.set(0, 0, bitmap.getWidth(), bitmap.getHeight());
            dstRect.set(dstWidth+lineWidth, 0, getMeasuredWidth(), getMeasuredHeight());
            canvas.drawBitmap(bitmap, srcRect, dstRect, bitmapPaint);
        }else if(length == 3){
            //如果有三张图片,则第一张图片在左边占一半位置,其余两张在右边占四分之一位置,图片之间画线分隔
            //左右分割线宽度,上下分割线高度
            int leftRightWidth = 4, topBottomHeight = 4;
            //每一张图片的宽度
            int dstWidth = (getMeasuredWidth()-leftRightWidth)/2;
            //绘制第一张图片
            bitmap = decodeSampledBitmapFromResource(drawableIds[0], dstWidth, getMeasuredHeight());
            srcRect.set(0, 0, bitmap.getWidth(), bitmap.getHeight());
            dstRect.set(0, 0, dstWidth, getMeasuredHeight());
            canvas.drawBitmap(bitmap, srcRect, dstRect, bitmapPaint);
            //绘制左右分割线
            linePaint.setColor(Color.WHITE);
            canvas.drawLine(dstWidth, 0, dstWidth+leftRightWidth, getMeasuredHeight(), linePaint);
            //绘制第二张图片
            bitmap = decodeSampledBitmapFromResource(drawableIds[1], dstWidth, getMeasuredHeight()/2);
            srcRect.set(0, 0, bitmap.getWidth(), bitmap.getHeight());
            dstRect.set(dstWidth+leftRightWidth, 0, getMeasuredWidth(), (getMeasuredHeight()-topBottomHeight)/2);
            canvas.drawBitmap(bitmap, srcRect, dstRect, bitmapPaint);
            //回执上下分割线
            canvas.drawLine(measuredWidth/2, measuredHeight/2, measuredWidth, measuredHeight/2, linePaint);
            //绘制第三张图片
            Bitmap thirdBitmap = decodeSampledBitmapFromResource(drawableIds[2], dstWidth, getMeasuredHeight());
            srcRect.set(0, 0, thirdBitmap.getWidth(), thirdBitmap.getHeight());
            dstRect.set(dstWidth+leftRightWidth, (measuredHeight-topBottomHeight)/2+topBottomHeight, measuredWidth, getMeasuredHeight());
            canvas.drawBitmap(thirdBitmap, srcRect, dstRect, bitmapPaint);
        }else{
            //四张以及以上图片统一处理,最多只能显示四张,将四张图片已“田”字形分布
            //这是分割线的尺寸,横线的高,竖线的宽,都等于他
            int lineSize = 4;
            //四张图片都是相同宽度,相同高度
            int dstWidth = (measuredWidth-lineSize)/2;
            int dstHeight = (measuredHeight-lineSize)/2;
            //先将四张图画上去
            bitmap = decodeSampledBitmapFromResource(drawableIds[0], dstWidth, dstHeight);
            srcRect.set(0, 0, bitmap.getWidth(), bitmap.getHeight());
            dstRect.set(0, 0, dstWidth, dstHeight);
            canvas.drawBitmap(bitmap, srcRect, dstRect, bitmapPaint);

            bitmap = decodeSampledBitmapFromResource(drawableIds[1], dstWidth, dstHeight);
            srcRect.set(0, 0, bitmap.getWidth(), bitmap.getHeight());
            dstRect.set(dstWidth+lineSize, 0, measuredWidth, dstHeight);
            canvas.drawBitmap(bitmap, srcRect, dstRect, bitmapPaint);

            bitmap = decodeSampledBitmapFromResource(drawableIds[2], dstWidth, dstHeight);
            srcRect.set(0, 0, bitmap.getWidth(), bitmap.getHeight());
            dstRect.set(0, dstHeight+lineSize, dstWidth, measuredHeight);
            canvas.drawBitmap(bitmap, srcRect, dstRect, bitmapPaint);

            bitmap = decodeSampledBitmapFromResource(drawableIds[3], dstWidth, dstHeight);
            srcRect.set(0, 0, bitmap.getWidth(), bitmap.getHeight());
            dstRect.set(dstWidth+lineSize, dstHeight+lineSize, measuredWidth,measuredHeight);
            canvas.drawBitmap(bitmap, srcRect, dstRect, bitmapPaint);
            //最后画两条分割线
            canvas.drawLine(dstWidth, 0, dstWidth, measuredHeight, linePaint);
            canvas.drawLine(0, dstHeight, measuredWidth, dstHeight, linePaint);
        }
        super.onDraw(canvas);
    }


    public void setDrawableIds(int[] drawableIds){
        this.drawableIds = drawableIds;
        invalidate();
    }

    /**
     * 从Resources中加载图片
     * @param resId 图片资源
     * @param reqWidth 目标宽度
     * @param reqHeight 目标高度
     * @return
     */
    private Bitmap decodeSampledBitmapFromResource(int resId, int reqWidth, int reqHeight) {
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true; // 设置成了true,不占用内存,只获取bitmap宽高
        BitmapFactory.decodeResource(getResources(), resId, options); // 读取图片长宽,目的是得到图片的宽高
        options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); // 调用上面定义的方法计算inSampleSize值
        // 使用获取到的inSampleSize值再次解析图片
        options.inJustDecodeBounds = false;
        Bitmap src = BitmapFactory.decodeResource(getResources(), resId, options); // 载入一个稍大的缩略图
        return createScaleBitmap(src, reqWidth, reqHeight, options.inSampleSize); // 通过得到的bitmap,进一步得到目标大小的缩略图
    }

    //根据"measureSpec"返回具体尺寸值
    private static int getMeasureSize(int measureSpec){
        int measureMode = MeasureSpec.getMode(measureSpec);
        //先给一个默认值
        int measureSize = 200;
        if(measureMode == MeasureSpec.EXACTLY){
            measureSize = MeasureSpec.getSize(measureSpec);
        }else if(measureMode == MeasureSpec.AT_MOST){
            measureSize = Math.min(measureSize, MeasureSpec.getSize(measureSpec));
        }
        return measureSize;
    }

    /**
     * 计算图片的压缩比率
     * @param options 参数
     * @param reqWidth 目标的宽度
     * @param reqHeight 目标的高度
     * @return inSampleSize 压缩比率
     */
    private 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 halfHeight = height / 2;
            final int halfWidth = width / 2;
            // Calculate the largest inSampleSize value that is a power of 2 and keeps both
            // height and width larger than the requested height and width.
            while ((halfHeight / inSampleSize) > reqHeight && (halfWidth / inSampleSize) > reqWidth) {
                inSampleSize *= 2;
            }
        }
        return inSampleSize;
    }

    /**
     * 通过传入的bitmap,进行压缩,得到符合标准的bitmap
     * @param src 原图片Bitmap
     * @param dstWidth 目标宽度
     * @param dstHeight 目标高度
     * @return 压缩后的图片Bitmap
     */
    private static Bitmap createScaleBitmap(Bitmap src, int dstWidth, int dstHeight, int inSampleSize) {
        // 如果是放大图片,filter决定是否平滑,如果是缩小图片,filter无影响,我们这里是缩小图片,所以直接设置为false
        Bitmap dst = Bitmap.createScaledBitmap(src, dstWidth, dstHeight, false);
        //如果图片有缩放,回收原来的图片
        if (src != dst) src.recycle();
        return dst;
    }

}

在XML文件里

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" 
	android:layout_width="match_parent"
    android:layout_height="match_parent">

    <!--四张图片展示效果-->
    <com.jf.simplecustomview.view.MergePictureView
        android:id="@+id/merge_picture_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="start"/>
</LinearLayout>

Activity里面

public class MergePictureActivity extends AppCompatActivity{

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_merge_picture);

        //展示四张图片合并效果
        int[] resourcesIdsFirst = new int[]{R.mipmap.t1, R.mipmap.t2, R.mipmap.t3, R.mipmap.t4};
        ((MergePictureView)findViewById(R.id.merge_picture_view)).setDrawableIds(resourcesIdsFirst);
}

项目源码:
https://github.com/kingfarou/SimpleCustomView

这个项目里面集合了好几个自定义View,本文所对应的View类名叫做"MergePictureView"。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值