笔者近来在学习Android自定义View,收集了一些不算复杂但又“长得”还可以的自定义View效果实现,之前分享过两个效果:一个水平的进度条,一个圆形温度显示器,如果你有兴趣的话可以看看:
一个水平的进度条
一个圆形温度显示器
今天我要来分享的是这样的效果——图片合并
当你使用微信进入某一个人的朋友圈列表,如果他们某条信息所携带的图片超过一张,就会做成如图所示这种显示效果,是将多张图片合并在一起形成的预览效果。当然对于朋友圈的实现方式我不清楚,有可能是服务器端已经合并好图片了,返回给客户端的就是一张合并之后的图片,只要一个ImageView就可以了。不过对于自定义View的学习,你是否想过,如果服务器给你的是若干张图片,你要自己拼接成一张,该怎么搞,今天我们来讲这个。
其实在绘图方面本文所牵扯到的知识点还没有多少,反而是关于bitmap的内存占用,控制显得更加重要。总的来说大概如下:
想象一下,如果我现在给你一张白纸(canvas,其实如果我说一个相框,可能更方便你想象),再给你一到四张照片,你要怎么“摆”出我的截图里的效果。其实很简单,笔者提出思路如下:
关于图片源,在你的项目当中,图片来源大多应该来自网络,不过作为一个样例,贪图方便(方便偷懒),图片来源直接来自本地,在drawable文件夹下面,所以我在生成bitmap时,调用的是"BitmapFactory.decodeResource()"方法。如果你的图片来自网络,可能需要别的方法,还有,如果你是用glide之类网络框架下载图片,请搞清楚这些框架下载图片之后对图片所做的事情,笔者曾经在实际项目里,使用一些框架导致数据错乱,需要另外进行其他调试。
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);
}
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。
解决这个问题的思路是:
类代码
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文件里
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"。