Android中一种对图片展示为不同形状的处理姿势

在Android UI开发中,经常会需要去展示各种各样的图片,如果只是一个新闻,一件商品的图片展示,可能产品或者美工没有什么太多的要求,默认的矩形展示就可以了。但是如果涉及到用户头像这种的,可能需要已不同于普通矩形的样式展示出来,比如说,圆角矩形,圆形等,感觉就是为了突出效果的目的呢,whatever,反正产品就要这种展示,我们苦逼的就需要去实现。
改版之后的快链

1,常规实现方式:

最最常规的实现方式,这个百度一下就能出来一坨,关键点就是使用了Paint.setXfermode()方法。Xfermode,这个合成词应该翻译成图像混合模式,就是用来指定使用Paint绘制的图像与原有的图像以怎样一个体位合成。
Xfermode有三个直接子类:

  1. AvoidXfermode //已被标注为废弃了
  2. PixelXorXfermode //已被标注为废弃了
  3. PorterDuffXfermode //主要使用对象

于是当前主要的设置方式就是:Paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.XXX));其中的XXX有几种呢,如下:PorterDuff.Mode

*图像中,dest图像为蓝色的矩形,首先被绘制;src图像为黄色的原型,最后被绘制;中间一步就是在绘制src之前的设置Paint的xfermode。各种XXX代表的意思如下:

ADD:饱和相加,对图像饱和度进行相加,不常用

  CLEAR:清除图像

  DARKEN:变暗,较深的颜色覆盖较浅的颜色,若两者深浅程度相同则混合

  DST:只显示目标图像

  DST_ATOP:在源图像和目标图像相交的地方绘制【目标图像】,在不相交的地方绘制【源图像】,相交处的效果受到源图像和目标图像alpha的影响

  DST_IN:只在源图像和目标图像相交的地方绘制【目标图像】,绘制效果受到源图像对应地方透明度影响

  DST_OUT:只在源图像和目标图像不相交的地方绘制【目标图像】,在相交的地方根据源图像的alpha进行过滤,源图像完全不透明则完全过滤,完全透明则不过滤

  DST_OVER:将目标图像放在源图像上方

  LIGHTEN:变亮,与DARKEN相反,DARKEN和LIGHTEN生成的图像结果与Android对颜色值深浅的定义有关

  MULTIPLY:正片叠底,源图像素颜色值乘以目标图像素颜色值除以255得到混合后图像像素颜色值

  OVERLAY:叠加

  SCREEN:滤色,色调均和,保留两个图层中较白的部分,较暗的部分被遮盖

  SRC:只显示源图像

  SRC_ATOP:在源图像和目标图像相交的地方绘制【源图像】,在不相交的地方绘制【目标图像】,相交处的效果受到源图像和目标图像alpha的影响

  SRC_IN:只在源图像和目标图像相交的地方绘制【源图像】

  SRC_OUT:只在源图像和目标图像不相交的地方绘制【源图像】,相交的地方根据目标图像的对应地方的alpha进行过滤,目标图像完全不透明则完全过滤,完全透明则不过滤

  SRC_OVER:将源图像放在目标图像上方

  XOR:在源图像和目标图像相交的地方之外绘制它们,在相交的地方受到对应alpha和色值影响,如果完全不透明则相交处完全不绘制

现在明确了XXX参数的含义,做出一个圆形的用户头像ImageView就不困难了

1
2
3
4
5
6
7
8
9
10
public class CircleImageView extends View{
...
...
@Override
public void onDraw(Canvas canvas){
canvas.drawCircle();
paint.setXfermode(PorterDuffXfermode.Mode.SRC_ATOP);
canvas.drawBitmap(Avatar, x, y, paint);
}
}

大体的代码就如上,首先绘制Dest(Circle),然后设置Paint,再然后使用Avatar和这个Paint进一步绘制到canvas上,就完成了用户的圆形头像功能。
要十分注意这里的绘制顺序,如果顺序颠倒了,先绘制Avatar,然后设置Paint(Mode也相应的发生变化),然后绘制Circle,就会无法达到我们需要的效果。这是为什么呢?android PorterDuffXferMode真正的效果测试集合(对比官方demo)
其文将两个图像的size相较于官方demo做了一些变化,然后就会得到一些大相径庭的效果,非常值得细细一看。至于上面这个问题,

文作者总结说:因为我们的Xfermode 叠合裁剪,都是建立在不同的层级上,重新画一个bitmap会新开一层。

第一种:先画circle 在canvas那层,再画Bitmap,新开了一层,中间镶嵌Xfermode,成功。

第二种: 先画bitmap,新开了一层,再画circle,还是在bitmap那层,中间镶嵌 Xfermode,不成功。

综上,如果改变了绘制顺序,想要成功的显示出想要的效果,就需要将绘制Circle这一步也产生一个Bitmap,然后将这个Bitmap绘制在Canvas上面。总之,就是需要把每一步都产生一个图层,这样xfermode才能在图层中间进行具体的操作。

以上,为产生圆角矩形,圆形等图像的常规步骤,一般情况下,就可以应付产品了。

改版之后的快链

当然,有一般,就会有二般的情况,而且这个二般的情况,处理起来还非常蛋疼。。。

2,一种不常规的做法:

现在的产品都趋于大同,比如说底栏tab,比如说右上角的更多button,比如说圆角矩形。然后我们的产品就想着怎么做些改变,提高安装量,提高日活,这样才能有收入。浏览器首页改版,最突出的一块就剩快链了,所以,就琢么着怎么把快链做的有点新意,更美观一些,然后就给我派了不算大也不算小的活,改版快链模块,功能上没有什么大的变化,也就是能拖动调整位置,能长按编辑,删除,添加等等。这些都还好说,但是,在UI上,产品想做点改变,原先每一个快链都是圆角矩形,竞品的也多是圆角矩形,或者矩形,而我们要跟他们不一样,最终定的方案,是做成拉美曲线的outline。
先放一张改完版之后的lame curve版本的快链,好有个直观感受:
改版之后的快链
有没有感到图片确实比圆角矩形在边缘过渡的更圆滑,顺畅一些。。。

下面就说说我苦逼实现的过程:

首先,什么是lame curve,刚开始知道方案的时候,我是一脸懵逼。后来查了查,才算了解了什么是Lame Curve

首先是lame的公式:lame_math

这是需要我去实现的特定lame,你们感受一下 lame_math

四阶方程,两个变量,当然可以使用Path,将根据方程算出的x,y值串联起来,画在canvas上面,然后设置Paint,再将快链的原始矩形图标绘制到canvas,最终完成lame 外形的快链图标。

but,数学都还给老师了,即使还记得怎么求解xy,但是需要把xy都计算出来,收敛于一个固定值,精确度的问题,再加上本身我对canvas上面的绘制就不是太熟练,所以,把这个方案当成了下下策。

lame_math

另外想招,可以让美工把图标都处理好,下发给我的时候已经是拉美曲线外形的了,这样移动端也省去的计算,这种方案对像淘宝,京东首页应该非常适用,因为他们的首页固定就8-10个图标,除了点击,没有给用户提供更多的交互途径,完全可以后台做完,直接下发展示就over了。但是浏览器一类的应用,首页的快链,都是可以用户自主编辑的,他完全可以自己定义一个快速链接,URL,名称完全自定义,世界上的网站千千万,每一个网站的favicon都预处理,要累死美工。lame_math。so,这个方案也被pass了。

说句题外话,有时候当你想不到好的方案,对bug一筹莫展,找不到头绪的时候。适当的暂时放下来,搁置在一旁,忙一下其他的事情,有时候过一段时间回头看看,说不定就突然有了思路,知道bug在哪里了。

走神的时候看到圆形头像的处理过程,想到既然 Circle和Avatar都可以做成Bitmap,分别绘制在Canvas上面,lame曲线跟Circle的作用是一样的,提供一个轮廓而已,完全可以让美工给做一个lame的模板,然后在绘制Avatar。

有了思路,赶紧当下做做试试,也许冥冥中天注定,我随手拿来写demo用的图片资源成了我能实现这个功能的关键。
又but,很快就遇到了问题:
1,之前是绘制一个Circle在Canvas上面,系统有api,很方便这样做。但是如果是读取一个lame Bitmap,然后将其绘制在Canvas上,接着在绘制另一个Bitmap,此时就会抛出异常:Immutable bitmap passed to Canvas constructor,什么意思呢?就是Android中不允许对res或者网络提供的图片资源进行修改,咱们第一步将lame曲线的模板图片绘制完,在绘制其他bitmap,此时就是对lame 模板的修改,系统是不允许的,当然,这个也好规避绕过去:把他存起来,在读进来,就变成Mutable的了,哈哈哈。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/**
* Converts a immutable bitmap to a mutable bitmap. This operation doesn't
* allocates more memory that there is already allocated.
*
* @param imgIn
* - Source image. It will be released, and should not be used
* more
* @return a copy of imgIn, but muttable.
*/
private static Bitmap convertToMutable(Bitmap imgIn) throws Exception {
// this is the file going to use temporally to save the bytes.
// This file will not be a image, it will store the raw image data.
File file = new File(Environment.getExternalStorageDirectory()
+ File.separator + "temp.tmp");
// Open an RandomAccessFile
// Make sure you have added uses-permission
// android:name="android.permission.WRITE_EXTERNAL_STORAGE"
// into AndroidManifest.xml file
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
// get the width and height of the source bitmap.
int width = imgIn.getWidth();
int height = imgIn.getHeight();
Bitmap.Config type = imgIn.getConfig();
// Copy the byte to the file
// Assume source bitmap loaded using options.inPreferredConfig =
// Config.ARGB_8888;
FileChannel channel = randomAccessFile.getChannel();
MappedByteBuffer map = channel.map(FileChannel.MapMode.READ_WRITE, 0,
imgIn.getRowBytes() * height);
imgIn.copyPixelsToBuffer(map);
// recycle the source bitmap, this will be no longer used.
imgIn.recycle();
System.gc();// try to force the bytes from the imgIn to be released
// Create a new bitmap to load the bitmap again. Probably the memory
// will be available.
imgIn = Bitmap.createBitmap(width, height, type);
map.position(0);
// load it back from temporary
imgIn.copyPixelsFromBuffer(map);
// close the temporary file and channel , then delete that also
channel.close();
randomAccessFile.close();
// delete the temp file
file.delete();
return imgIn;
}

转换完lame模板,剩下的步骤就是按部就班了。

当时我随便在工程资源文件里面挑了一张当做lame模板lame_math,另一张当做要做曲线变换的显示图标,观察结果发现,按照我的想法,确实两者有一部分发生了交集,模板蓝色的部分,确实显示了图标,但是透明区域还是透明的,没有显示图标,没有达到想要的效果。

第二天我让美工给我提供了一个蓝色边线,中间非透明的lame曲线的图片,继续做实验,测试各种lame模板的搭配,比如红色边界线,白色的中间填充等等。最终发现,能达到我需要的效果的lame模板必须是,边界蓝色,中间非透明黑色的,就如下图这样:







有了模板,有了原图标tria,那么下一步就duang的搞定了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
//ClipType是一个自定义的比例工具,因为我们的快链展示为原图片中间的87%,或者全size,所以做了一个转换工具,可忽略此步
public static Bitmap drawTarget(Context context, Bitmap srcBitmap, float scale) {
Bitmap copyBitmap = null;
try{
int mLameW = 0, mLameH = 0;
if(mLameCurveBitmap == null){
Bitmap targetBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.lame_curve_bg);
mLameCurveBitmap = convertToMutable(destBitmap);
mLameBgWidth = mLameCurveBitmap.getWidth();
mLameBgHeight = mLameCurveBitmap.getHeight();
}
copyBitmap = Bitmap.createBitmap(mLameCurveBitmap);
mLameW = mLameBgWidth;
mLameH = mLameBgHeight;
Canvas mCanvas = new Canvas(copyBitmap);
if (srcBitmap != null) {
int srcWidth = srcBitmap.getWidth();
int srcHeight = srcBitmap.getHeight();
float scaleX = scale*mLameW / srcWidth;
float scaleY = scale*mLameH / srcHeight;
float maxScale = Math.max(scaleX, scaleY);
Matrix matrix = new Matrix();
matrix.postScale(maxScale, maxScale);
Paint mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setColor(Color.GRAY);
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
Bitmap srcBitmap2 = Bitmap.createBitmap(srcBitmap, 0, 0, srcWidth,
srcHeight, matrix, true);
float offsetX = ClipType.getOffset(srcWidth, mLameW, maxScale, scale);
float offsetY = ClipType.getOffset(srcHeight, mLameH, maxScale, scale);
mCanvas.drawBitmap(srcBitmap2, offsetX, offsetY, mPaint);
}
return copyBitmap;
}catch(Exception e){
return getDefaultBitmap(context, scale);
}
}

当然,上述代码是针对我们项目做了调整和优化的,但也有优化上面的不足,具体情况具体分析吧,各位。

3,Show一下做成的成果:

拿着上面的各种模板,还有原始图标(本来想拿搜狗浏览器的图标呢,结果我们图标没有方的,只有圆的,做出来效果不明显,让我很方,只好给搜狗搜索做做硬广了 :P)

tria

具体为什么lame模板必须是上述那样子的,我暂时还没有搞明白,tria
因为Canvas绘制都是直接native的方法,本人的c、c++也只是皮毛水平,就不想自虐了,有兴趣研究的,如果知道,劳烦告知我一下其中缘由,先谢谢了~~~