写在前面
笔者近来在学习Android自定义View,收集了一些不算复杂但又“长得”还可以的自定义View效果实现。
今天我要来分享的是这样的效果——图片合并
当你使用微信进入某一个人的朋友圈列表,如果他们某条信息所携带的图片超过一张,就会做成如图所示这种显示效果,是将多张图片合并在一起形成的预览效果。当然对于朋友圈的实现方式我不清楚,有可能是服务器端已经合并好图片了,返回给客户端的就是一张合并之后的图片,只要一个ImageView就可以了。不过对于自定义View的学习,你是否想过,如果服务器给你的是若干张图片,你要自己拼接成一张,该怎么搞,今天我们来讲这个。
所涉及的知识点
其实在绘图方面本文所牵扯到的知识点还没有多少,反而是关于bitmap的内存占用,控制显得更加重要。总的来说大概如下:
canvas.drawBitmap()方法里面各个参数含义。
如果要显示的图片区域(或者像素点)明显的比原图片小(因为我们只是做缩略图),怎么节约内存使用。这将涉及到加载”bitmap”时利用”BitmapFactory.Options”对象计算压缩的比例等等知识。
一些很简单的数学计算,如前面的图片所示,是需要在一、二、三、四张图片的情况下,计算裁剪图片大小以及摆放在什么样的位置上。
设计思路
想象一下,如果我现在给你一张白纸(canvas,其实如果我说一个相框,可能更方便你想象),再给你一到四张照片,你要怎么“摆”出我的截图里的效果。其实很简单,笔者提出思路如下:
首先你需要判断下图片张数,然后分别进行处理。
如果是一张图片,我们就将整张原图“绘制”到我们的canvas上面。
如果是两张图片,我们就将它们分别横向压缩一半,然后分别绘制到canvas里面,每张图片占一半位置。
如果是三张图片,将第一张压缩一半,绘制到canvas的左半边,另外两张图片压缩成原来的四分之一,绘制到canvas右上角,右下角。
如果是四张图片,将四张图片全部都压缩成原来的四分之一,绘制到canvas的四个角上。
为了美观,图片之间画条白线分隔一下。
一步一步,切分代码
关于图片源,在你的项目当中,图片来源大多应该来自网络,不过作为一个样例,贪图方便(方便偷懒),图片来源直接来自本地,在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时的内存占用问题
解决这个问题的思路是:
先将”BitmapFactory.Options”对象的”inJustDecodeBounds”属性设置为true,这样子能获取图片相关信息。
根据我们所需要的最终尺寸,以及图片原来信息,计算以及设置压缩比例。
设置好压缩比例,将刚才的那个属性设置为false,将一个“比较小的”bitmap给加载进来。
通过”Bitmap.createScaledBitmap(Bitmap, int , int , boolean)”方法,得到最终我们要的尺寸的Bitmap。
关于这个解决思路背后的原理,推荐参考这篇博客:
具体代码跟下面的完整用例贴在一起。
完整用例
类代码
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); }
原文链接:http://www.apkbus.com/blog-847095-77111.html