当前位置:   article > 正文

【Android Bitmap】Bitmap解析与应用

android bitmap

一、Bitmap基本介绍

Bitmap也称为位图,是图片在内存中的表现形式,任何图片(JPEG, PNG, WEBP…)加载到内存后都是一个Bitmap对象。Bitmap实际是像素点的集合,假设它的宽高为width和height,那么该Bitmap就包含width*height个像素,它在内存中占用的内存就是(width*height*单个像素内存)。

为了减小图片在磁盘上所占的空间,将Bitmap保存到磁盘上时会进行压缩,图片的文件格式实际代表的是不同的压缩方式与压缩率,而将磁盘上的文件加载到内存中时就要进行解压缩。

1.1 图片格式介绍

常见的静态图片格式为JPEG、PNG和WEBP,它们有着不同的压缩方式,保存到本地后所占用的空间大小也不一样。

  1. JPEG是一种有损压缩格式,以24位颜色压缩存储单个位图,但是不支持透明度。使用JPEG进行压缩时需要选择适当的压缩率,避免图片质量太差。
  2. PNG是一种无损压缩格式,支持所有的颜色,由于是无损压缩,PNG一般用于APP图标这类对线条或者清晰度有要求的图片。由于PNG所占空间较大,目前一般将PNG转为WEBP使用。
  3. WEBP支持无损压缩和有损压缩,并且他的无损压缩率优于PNG,有损压缩率优于JPEG,同时它支持所有颜色,并支持多帧动画,唯一的缺点是压缩效率不如JPEG和PNG。
1.2 Bitmap色深

Bitmap的本质就是像素点的集合,它通过描述每个像素的ARGB信息来形成整张图片,其中A表示透明度通道,RGB表示红绿蓝3种颜色通道,每个通道的值都在0-255之间,因此8bit可以完整地表示1个通道,那么4x8=32bit可以表示一个完整的像素。但如果每个Bitmap都使用32bit来表示一个像素的话,对内存来说是一个较大的负担,因此对于质量要求不高的Bitmap来说,可以采用较少的内存去表示一个像素。

色深指的是每一个像素点用多少bit来存储ARGB值,色深越大,图片的色彩越丰富,一般来说色深有8bit、16bit、32bit等,Bitmap.Config中的色深配置如下。

ALPHA_8: 这种方案只存储透明度通道,色深为8bit,使用场景特殊,比如设置遮盖效果等。 ARGB_8888: ARGB每个通道值采8bit来表示,色深为32bit,每个像素点需要4byte的内存来存储数据,图片质量高,所占内存大。 ARGB_4444: ARGB每个通道都是4位,色深为16bit,由于这种配置下的图片质量较差,Android官方已经将其标为弃用。 RGB_565: 色深为16bit,RGB通道值分别占5、6、5位,但是没有存储A通道值,所以不支持透明度,可用于对清晰度要求不高的照片。 RGBA_F16: 色深为64bit,该配置主要用于广色域与HDR内容。 HARDWARE: 这是一种特殊配置,该配置下,Bitmap会被存储在显存中,并且Bitmap是不可变的。这种配置仅适用于,当Bitmap唯一的操作就是将自己绘制在屏幕上时。

我们可以根据对图片质量的要求创建不同色深的Bitmap,如果对显示质量有较高的要求可以使用ARGB_8888,一般来说使用RGB_565即可,这也可以减少OOM的概率。

Bitmap b = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 
  • 1
1.2 通过采样加载大图

ImageView是显示Bitmap的载体,一般情况下ImageView的宽高会小于Bitmap,如果将一个完整的Bitmap加载到偏小的ImageView中会浪费内存。关于这个问题,Android官方提供了一个优化方法。

该方法通过比较ImageView与Bitmap的大小并计算采样率,最终将采样后的小图加载到内存中。流程如下:在调用BitmapFactory.decodeXXX(res, resId, BitmapFactory.Options)解析图片时,先将BitmapFactory.OptionsinJustDecodeBounds 设为true,此时不会将图片加载到内存中,而是只得到Bitmap的宽高,随后通过图片宽高计算采样率。得到采样率后再将inJustDecodeBounds 设为false,再加载Bitmap时可以得到大图的采样图。

public static Bitmap decodeSampledBitmapFromResource(
            Resources res, int resId, int reqWidth, int reqHeight) {
    final BitmapFactory.Options options = new BitmapFactory.Options();
    // 该属性默认为false, 为true时不会将图片加载到内存中, 而是只计算宽高
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(res, resId, options);
    // 计算采样率
    options.inSampleSize = calculateInSampleSize(
            options, reqWidth, reqHeight);
    // 设置inJustDecodeBounds为false, 将图片加载到内存
    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeResource(res, resId, options);
}

public static int calculateInSampleSize(
            BitmapFactory.Options options, int reqWidth, int reqHeight) {
    // 获得Bitmap的宽高
    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;
        while ((halfHeight / inSampleSize) >= reqHeight
                && (halfWidth / inSampleSize) >= reqWidth) {
            inSampleSize *= 2;
        }
    }
    return inSampleSize;
} 
  • 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

如果使用Glide这类图片加载框架将Bitmap加载到ImageView中,框架会自动帮我们完成采样的工作。不过如果你需要获取某个Bitmap的缩略图,上述方法还是有用武之地的。

二、Bitmap相关应用

2.1 图片裁剪

图片裁剪是图片处理的基本功能,APP中的设置头像功能就需要用到图片裁剪,用户可以通过一个预览框在原图上裁剪出特定区域的图案。 系统本身提供了裁剪的功能,我们可以在A页面通过以下代码启动一个裁剪图片的Activity,裁剪结束在A页面的onActivityResult(...)中得到裁剪后的图片。

// uri为图片的地址
public void startCropPicture(Uri uri) {
    Intent intent = new Intent("com.android.camera.action.CROP");
    intent.setDataAndType(uri, "image/*");
    intent.putExtra("crop", "true");
    intent.putExtra("aspectX", 1); // 裁剪框比例
    intent.putExtra("aspectY", 1);
    intent.putExtra("outputX", 300); // 输出图片大小
    intent.putExtra("outputY", 300);
    intent.putExtra("scale", true);
    intent.putExtra("return-data", true);
    startActivityForResult(intent, REQUEST_CODE);
} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

图片裁剪的原理很简单,通过Bitmap.createBitmap(originBitmap, left, top, width, height)即可在originBitmap上裁出指定区域的Bitmap,因此我们可以自己实现一个裁剪头像的功能。需要注意的是,Bitmap.createBitmap(...)方法参数中传入的坐标和宽高都是基于原始Bitmap的,如果传入的参数超出了原始Bitmap的宽高就会抛出异常。

本文Demo实现了裁剪图案的基础功能,拖动裁剪框右下角可以修改框大小,拖动其他区域可以移动裁剪框,可以通过本文开头的链接下载代码。

除了Bitmap.createBitmap(originBitmap, left, top, width, height)裁剪出指定的Bitmap,也可以在绘制时直接指定Bitmap的绘制区域,通过Canvas.drawBitmap(Bitmap bitmap, Rect src, RectF dst, Paint paint)即可实现,其中src表示对原图片的裁剪区域,dst表示对裁剪后的图片绘制到View上的区域。

2.2 图片拼接

图片拼接常见于分享时拼接所有图片,达到"一图看尽所有"的效果。代码示例如下,为了方便演示,示例中将4张一样的正方形图片splitBitmap拼接到了一起。

private Bitmap getJointBitmap(Bitmap splitBitmap) {
    int width = splitBitmap.getWidth();
    int height = splitBitmap.getHeight();
    Bitmap bitmap = Bitmap.createBitmap(
                width * 2, height * 2, Bitmap.Config.RGB_565);
    Canvas canvas = new Canvas(bitmap);
    canvas.drawBitmap(splitBitmap, 0, 0, null);
    canvas.drawBitmap(splitBitmap, width, 0, null);
    canvas.drawBitmap(splitBitmap, 0, height, null);
    canvas.drawBitmap(splitBitmap, width, height, null);
    return bitmap;
} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

拼接图片前先创建了一个宽高都为splitBitmap宽高2倍的Bitmap作为容器,然后通过Canvas在容器的四个位置绘制4张一样的图片,最终效果如下。

2.3 矩阵变换

Bitmap是像素点的集合,我们可以通过矩阵运算改变每个像素点的位置,达到图形变换的效果。Android中可以通过Matrix类来进行变换,Matrix本身是一个3x3的矩阵,可以通过Matrix m = new Matrix()新建一个单位矩阵,原始矩阵的值如下所示。

[1 0 0]
[0 1 0]
[0 0 1] 
  • 1
  • 2
  • 3

Matrix中各个位置的变换信息如下所示,scale表示缩放,skew表示错切,trans表示平移,persp等表示透视参数。Bitmap中的每个像素点可以使用一个3x1的矩阵表示,其中x表示当前像素点的横坐标,y表示纵坐标。用该矩阵左乘Bitmap中的所有像素后,就能得到变换后的图像。

[scaleX  skewX   transX]     [x]     [scaleX * x + skewX * y + transX]
[skewY   scaleY  transY]  x  [y]  =  [skewY * x + scaleY * y + transY]
[persp0  persp1  persp2]     [1]     [persp0 * x + persp1 * y + persp2] 
  • 1
  • 2
  • 3

简单来说,Matrix是一个容器,保存了用户期望的矩阵变换信息。在将Matrix应用于Bitmap之前,我们可以对其进行各种操作,将变换信息保存进去。矩阵运算可以实现平移、旋转、缩放、错切,因此Matrix也为提供了类似方法。

setTranslate(float dx,float dy): 控制 Matrix 进行位移。
setSkew(float kx,float ky): 控制 Matrix 进行倾斜,kx、ky为X、Y方向上的比例。
setSkew(float kx,float ky,float px,float py): 控制 Matrix 以 px, py 为轴心进行倾斜,kx、ky为X、Y方向上的倾斜比例
setRotate(float degrees): 控制 Matrix 进行 depress 角度的旋转,轴心为(0,0)
setRotate(float degrees,float px,float py): 控制 Matrix 进行 depress 角度的旋转,轴心为(px,py)
setScale(float sx,float sy): 设置 Matrix 进行缩放,sx, sy 为 X, Y方向上的缩放比例。
setScale(float sx,float sy,float px,float py): 设置 Matrix 以(px,py)为轴心进行缩放,sx、sy 为 X、Y方向上的缩放比例 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

很多时候矩阵变换并不是单一的平移、旋转或缩放,这些变换经常结合在一起使用,此时setXXX()方法无法满足要求。因此Matrix提供了preXXX()postXXX()方法来组合多个矩阵操作,多个矩阵操作之间通过乘法运算。由于矩阵的乘法是不满足交换律的,因此进行矩阵运算时需要注意乘法的顺序。

在使用preXXX()postXXX()时,我们可以将矩阵变换的所有计算看成一个乘法列表,调用preXXX()方法时就是向列表头部添加操作,调用postXXX()时就是向列表尾部添加操作。例如以下代码中矩阵乘法的执行顺序就是2->1->3->4。需要注意的是,setXXX()方法会重置Matrix的变换,如果对下方执行完4个运算的Matrix调用setTranslate()方法,那么该Matrix就只有平移效果了。

Matrix matrix = new Matrix();
matrix.preScale(...); // 1
matrix.preTranslate(...); // 2
matrix.postTranslate(...); // 3
matrix.postRotate(...); // 4 
  • 1
  • 2
  • 3
  • 4
  • 5
2.4 颜色变换

颜色变换主要通过ColorFilter进行,通过Paint.setColorFilter(ColorFilter filter)可以设置颜色过滤器,该过滤器会对每一个像素的颜色进行过滤,得到最终的图像。ColorFilter有3个子类,这里主要介绍ColorMatrixColorFilter。

2.4.1 ColorMatrixColorFilter

该颜色过滤器通过矩阵进行色彩变换,先来介绍一下色彩矩阵,Android中的色彩是以ARGB的形式存储的,我们可以通过ColorMatrix修改颜色的值,ColorMatrix定义了一个4x5的float矩阵,矩阵的4行分别表示在RGBA上的向量,其范围值在0f-2f之间,如果为1就是原效果。每一行的第5列表示偏移量,就是指在当前通道上增大或减小多少。

ColorMatrix colorMatrix = new ColorMatrix(new float[]{
		1, 0, 0, 0, 0,
		0, 1, 0, 0, 0,
		0, 0, 1, 0, 0,
		0, 0, 0, 1, 0,
}); 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

ColorMatrix与颜色之间的运算如下所示,其实就是矩阵运算,与上一节的Matrix类似。

[a, b, c, d, e]     [R]     [a*R + b*G + c*B + d*A + e]
[f, g, h, i, j]     [G]     [f*R + g*G + h*B + i*A + j]
[k, l, m, n, o]  x  [B]  =  [k*R + l*G + m*B + n*A + o]
[p, q, r, s, t]     [A]     [p*R + q*G + r*B + s*A + t]
                    [1] 
  • 1
  • 2
  • 3
  • 4
  • 5

有了ColorMatrix,就可以对Bitmap上所有的颜色进行修改,例如调整每个通道的值,将初始值1改为0.5,就可以将Bitmap变暗。不过我不太了解色彩,也没在项目中实际使用过,感兴趣的朋友可以看参考1。

2.5 图像混合

图像混合是指对两张原始图像(我们称为DST和SRC)的内容按某种规则合成,从而形成一张包含DST和SRC特点的新图像。例如DST为圆形图像,SRC为照片,可以将它们合成为圆形照片。

Android通过PorterDuffXfermode实现图像混合,它实际上是通过公式对两张图像在Canvas上的所有像素进行ARGB运算,最终在每个像素点得到新的ARGB值。需要注意的是,在onDraw(Canvas)方法中进行图像混合时,先绘制的图像为DST,后绘制的图像为SRC,因此需要注意图像的绘制顺序。

PorterDuffXfermode一共提供了18种混合模式,它们的计算公式如下,Sa表示SRC的ALPHA通道,Sc表示SRC的颜色;Da表示DST的ALPHA通道,Dc表示DST的颜色。以CLEAR为例,该模式会清除SRC区域的所有内容。

合成模式公式
CLEAR[0, 0]
SRC[Sa, Sc]
DST[Da, Dc]
SRC_OVER[Sa + (1 - Sa)*Da, Rc = Sc + (1 - Sa)*Dc]
DST_OVER[Sa + (1 - Sa)*Da, Rc = Dc + (1 - Da)*Sc]
SRC_IN[Sa * Da, Sc * Da]
DST_IN[Sa * Da, Sa * Dc]
SRC_OUT[Sa * (1 - Da), Sc * (1 - Da)]
DST_OUT[Da * (1 - Sa), Dc * (1 - Sa)]
SRC_ATOP[Da, Sc * Da + (1 - Sa) * Dc]
DST_ATOP[Sa, Sa * Dc + Sc * (1 - Da)]
XOR[Sa + Da - 2 * Sa * Da, Sc * (1 - Da) + (1 - Sa) * Dc]
DARKEN[Sa + Da - Sa * Da, Sc * (1 - Da) + Dc * (1 - Sa) + min(Sc, Dc)]
LIGHTEN[Sa + Da - Sa * Da, Sc * (1 - Da) + Dc * (1 - Sa) + max(Sc, Dc)]
MULTIPLY[Sa * Da, Sc * Dc]
SCREEN[Sa + Da - Sa * Da, Sc + Dc - Sc * Dc]
ADDSaturate(S + D)

如果你使用过PorterDuffXfermode,你可能见过下面这张图,Android官方的样例就是这个效果,不过官方只提供了16种混合模式的样例,我在Demo中把ADD和OVERLAY也添加了进去。

当然你也可能见过这张图。

乍一看,这两张图中的DST和SRC原始图像都是一样的,但是为什么使用了同样的混合模式后,显示的结果不同呢?

关键就在于DST和SRC的大小,第一张图中的DST和SRC都是Bitmap,它们的大小与Canvas相等,只是在Bitmap的某个区域绘制了圆和矩形。Demo代码如下,可以看到makeDst()makeSrc()中创建的Bitmap与整个View(或者说Canvas)是相等的。这也解释了为什么第一张图的CLEAR模式下的结果是空白的,因为SRC的大小是整个View的大小,CLEAR模式表示清除SRC区域的内容,最终将整个View的内容清除了。

public class XFerModeView extends View {

    private Paint mPaint;
    private PorterDuffXfermode mPorterDuffXfermode;
    private int mWidth;
    private int mHeight;

    // 省略构造方法......

    private void init() {
        setLayerType(LAYER_TYPE_SOFTWARE, null);
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPorterDuffXfermode = new PorterDuffXfermode(PorterDuff.Mode.CLEAR);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if (mWidth != w || mHeight != h) {
            mWidth = w;
            mHeight = h;
            invalidate();
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        drawBackground(canvas);
        int sc = canvas.saveLayer(0, 0, mWidth, mHeight, null);
        drawCompositionInFullSize(canvas);
        canvas.restoreToCount(sc);
    }

    private void drawBackground(Canvas canvas) {
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(5);
        mPaint.setColor(Color.BLACK);
        canvas.drawRect(0, 0, mWidth, mHeight, mPaint);
    }

    private void drawCompositionInFullSize(Canvas canvas) {
        mPaint.setStyle(Paint.Style.FILL);
        Bitmap dst = makeDst();
        Bitmap src = makeSrc();
        // 绘制DST
        canvas.drawBitmap(dst, 0, 0, mPaint);
        // 设置图像混合模式
        mPaint.setXfermode(mPorterDuffXfermode);
        // 绘制SRC
        canvas.drawBitmap(src, 0, 0, mPaint);
        // 清除图像混合模式
        mPaint.setXfermode(null);
    }

    private Bitmap makeDst() {
        Bitmap bm = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888);
        Canvas c = new Canvas(bm);
        mPaint.setColor(0xFFFFCC44);
        c.drawOval(10, 10, mWidth * 3f / 4, mHeight * 3f / 4, mPaint);
        return bm;
    }

    private Bitmap makeSrc() {
        Bitmap bm = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888);
        Canvas c = new Canvas(bm);
        mPaint.setColor(0xFF66AAFF);
        c.drawRect(mWidth * 1f / 3, mHeight * 1f / 3,
                mWidth * 19f / 20, mHeight * 19f / 20, mPaint);
        return bm;
    } 
  • 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
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71

在第二张图中,绘制SRC和DST时创建的图像大小就是圆或矩形的大小,最终的结果也与第一张图有所不同,修改后的代码如下。还是以CLEAR模式为例,此时清除的就只是矩形区域SRC的图像,可以看到DST中与SRC相交的部分被清除了。

public class XFerModeView extends View {

    // 省略重复代码......

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        drawBackground(canvas);
        int sc = canvas.saveLayer(0, 0, mWidth, mHeight, null);
        drawCompositionInSelfSize(canvas);
        canvas.restoreToCount(sc);
    }

    /**
     * 混合图像的大小只有可见区域大小
     */
    private void drawCompositionInSelfSize(Canvas canvas) {
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setColor(0xFFFFCC44);
        canvas.drawOval(10, 10, mWidth * 3f / 4, mHeight * 3f / 4, mPaint);
        mPaint.setXfermode(mPorterDuffXfermode);
        mPaint.setColor(0xFF66AAFF);
        canvas.drawRect(mWidth * 1f / 3, mHeight * 1f / 3,
                mWidth * 19f / 20, mHeight * 19f / 20, mPaint);
        mPaint.setXfermode(null);
    }
} 
  • 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

使用图像混合时还有一个要注意的地方:上述代码在onDraw(Canvas)方法中绘制混合图像时会先调用int sc = canvas.saveLayer(...)生成一个新的图层(Layer),sc表示图层的编号,随后在新Layer上绘制DST和SRC,绘制完后将该Layer添加到Canvas上。那么这里为什么需要新的Layer来绘制DST和SRC,而不是直接在Canvas上绘制呢?

Layer可以理解为画布Canvas的一个层级,默认情况下Canvas只有一个Layer,所有的绘制都在同一图层上。当需要绘制多层图像时,可以通过canvas.saveLayer(...)生成新的Layer,在新Layer上绘制的内容是独立的,不会影响到其他Layer的内容,调用canvas. restoreToCount(int sc)时将该Layer覆盖到Canvas现有的图像上。Canvas通过栈的形式管理Layer,示意图如下。

之前提到进行图像混合时,先绘制的内容是DST,后绘制的是SRC。如果不新建Layer的话,在绘制SRC时,Canvas上的所有内容都会被当作DST,所以背景等内容也会参与图像混合,很容易得到错误的效果。

以上就是图像混合的基本介绍,图像混合的应用场景比较广泛,这里介绍几种常见的场景。

2.5.1 图像切割

图像切割用于将图像切割成特定的形状。可以是常见形状如圆形或圆角矩形,也可以切割为五角星这样的非常规形状,使用这一类非常规形状时需要该形状的底图。

将图像裁剪为圆角矩形时比较简单,在onDraw(Canvas)中新建图层,绘制圆角矩形作为DST,再绘制原图作为SRC即可,此时图像混合模式应设置为SRC_IN,代码如下,decodeSampledBitmapFromResource(...)就是1.2节的大图采样。

public class RoundCornerView extends View {

    private Paint mPaint;
    private PorterDuffXfermode mFerMode;
    private Bitmap mBitmap;
    private Rect mBitmapRect;
    private int mWidth;
    private int mHeight;

    // 省略构造函数...

    private void init() {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mFerMode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if (mWidth != w || mHeight != h) {
            mWidth = w;
            mHeight = h;
            mBitmap = BitmapUtils.decodeSampledBitmapFromResource(
                    getContext().getResources(), R.drawable.compress_test, mWidth, mHeight);
            mBitmapRect = new Rect(0, 0, mWidth, mHeight);
            invalidate();
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int sc = canvas.saveLayer(0, 0, mWidth, mHeight, null);
        canvas.drawRoundRect(0, 0, mWidth, mHeight, 50, 50, mPaint);
        mPaint.setXfermode(mFerMode);
        canvas.drawBitmap(mBitmap, null, mBitmapRect, mPaint);
        mPaint.setXfermode(null);
        canvas.restoreToCount(sc);
    }

    ......
} 
  • 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

最终效果如下,同理可以将图像切割为圆形等基础形状。


如果要将图像切割为五角星这样的图案,就需要使用一张五角星的底图,需要注意的是,底图上五角星以外的部分应该是透明的,否则切割出来还是原来的形状。其代码与切割为圆角矩形大同小异,只需要将绘制圆角矩形的部分换成绘制五角星即可。

public class StarPicView extends View {

    private Paint mPaint;
    private PorterDuffXfermode mMode;
    private Bitmap mBgBitmap;
    private Bitmap mBitmap;
    private int mWidth, mHeight;
    private Rect mDrawRect;

    // 省略构造函数......

    private void init() {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mMode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if (mWidth != w || mHeight != h) {
            mWidth = w;
            mHeight = h;
            mBgBitmap = BitmapUtils.decodeSampledBitmapFromResource(
                    getContext().getResources(), R.drawable.star4, mWidth, mHeight);
            mBitmap = BitmapUtils.decodeSampledBitmapFromResource(
                    getContext().getResources(), R.drawable.icon3, mWidth, mHeight);
            mDrawRect = new Rect(0, 0, mWidth, mHeight);
            invalidate();
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int sc = canvas.saveLayer(0, 0, mWidth, mHeight, null);
        canvas.drawBitmap(mBgBitmap, null, mDrawRect, mPaint);
        mPaint.setXfermode(mMode);
        canvas.drawBitmap(mBitmap, null, mDrawRect, mPaint);
        mPaint.setXfermode(null);
        canvas.restoreToCount(sc);
    }
} 
  • 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

最终的效果如下。

2.5.2 色彩合成

色彩合成可以为图片添加新的效果,当使用纯色与照片混合时,可以改变图片整体的色调。例如黄色可以让图片具有泛黄的怀旧效果,红色可以让图片更温暖。以下代码通过SCREEN混合模式将半透明的红色与图片混合。

public class ColorComposeView extends View {

    ......

    private void init() {
        setLayerType(LAYER_TYPE_SOFTWARE, null);
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mMode = new PorterDuffXfermode(PorterDuff.Mode.SCREEN);
    }

    ......

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawColor(Color.WHITE);
        int sc = canvas.saveLayer(0, 0, mWidth, mHeight, null);
        canvas.drawColor(0x44FF0000);
        mPaint.setXfermode(mMode);
        canvas.drawBitmap(mBitmap, null, mRect, mPaint);
        mPaint.setXfermode(null);
        canvas.restoreToCount(sc);
    }
} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

怎么样,是不是觉得小姐姐看上去都温柔了一些?

出了纯色混合,也可以将两张图片进行合成,例如通过一张毛玻璃的底图,可以为照片添加一定的模糊效果,底图如下所示。

绘制时将底图作为DST,将照片作为SRC,混合模式使用OVERLAY,代码与绘制切割五角星的大同小异,不再赘述。最终得到如下的效果。

2.5.3 图像alpha渐变

之前遇到一个很有意思的UI需求,给定一张底图,要求绘制时图像的透明度从上到下是渐变的1-0,效果如下所示。因为background是白色,所以透明部分透出来的是白色。

这个其实也很简单,在本地新建一个渐变的drawable,然后通过XOR模式进行混合,具体代码可参考文章开头代码。

三、图片压缩

3.1 质量压缩

质量压缩减小的是图片在磁盘上的体积大小,通过Bitmap.compress(CompressFormat, quality, outputStream)将Bitmap保存到本地时,可以选择对应的文件格式以及质量标准,文件格式包含JPEG、PNG和WEBP三种,质量的取值为0-100,0代表最差质量,100代表最高质量。WEBP格式将会在API30被弃用,取而代之的是WEBP_LOSSLESS和WEBP_LOSSY,用于更清晰地描述是无损压缩还是有损压缩。

将Bitmap保存为60质量标准的jpeg的代码如下。

private void qualityCompressJPG() {
    OutputStream os = getOutputStreamByName("jpgFile60.jpeg");
    if (os != null) {
        mOriginBitmap.compress(Bitmap.CompressFormat.JPEG, 60, os);
    }
}

private OutputStream getOutputStreamByName(String fileName) {
    BufferedOutputStream bos = null;
    File dir = new File(FILE_DIR);
    boolean dirExist = true;
    if (!dir.exists()) {
        dirExist = dir.mkdirs();
    }
    if (dirExist) {
        File file = new File(dir, fileName);
        if (file.exists()) {
            file.delete();
        }
        try {
            boolean fileExist;
            fileExist = file.createNewFile();
            if (fileExist) {
                bos = new BufferedOutputStream(new FileOutputStream(file));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    return bos;
} 
  • 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

三种图片格式中,PNG为无损压缩,因此当文件格式选择PNG时Bitmap.compress(CompressFormat, quality, outputStream)方法会无视quality参数。下图为保存同一张图片时,选择不同的格式与不同质量的对比。可以发现WEBP格式所占用的空间是比较理想的,如果APP的体积比较大,可以考虑把资源文件转化为WEBP来节省空间。


3.2 尺寸压缩

尺寸压缩就是指压缩原始Bitmap的宽高,通过减小像素个数来减小Bitmap占用的空间,这种压缩方式下,不管Bitmap所占的内存,还是图片保存到磁盘上所占的空间都会有显著的减小。

尺寸压缩可以通过Bitmap.createScaledBitmap(Bitmap src, int dstWidth, int dstHeight, boolean filter)来创建压缩后的Bitmap,filter参数可以简单地理解为,如果为true的话就会消耗更长的时间获得更好的图片质量,false则相反。

private void sizeCompress1(int scale) {
    int width = mOriginBitmap.getWidth() / scale;
    int height = mOriginBitmap.getHeight() / scale;
    Bitmap b = Bitmap.createScaledBitmap(
                    mOriginBitmap, width, height, false);
    OutputStream os = getOutputStreamByName("sizeCompress1.webp");
    if (os != null) {
        b.compress(Bitmap.CompressFormat.WEBP, 100, os);
    }
} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

通过Bitmap b = Bitmap.createScaledBitmap(src, w, h, filter)来创建Bitmap时,它的Config是基于原始Bitmap的,如果原始Bitmap的Config是ARGB_8888而压缩后又不需要这么高的清晰度,那么可以选择新建RGB_565的Bitmap,并通过Canvas将压缩后的图片绘制上去。

private void sizeCompress2(int scale) {
    int width = mOriginBitmap.getWidth() / scale;
    int height = mOriginBitmap.getHeight() / scale;
    Bitmap b = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
    Canvas canvas = new Canvas(b);
    Rect rect = new Rect(0, 0, width, height);
    canvas.drawBitmap(mOriginBitmap, null, rect, null);
    OutputStream os = getOutputStreamByName("sizeCompress2_565.webp");
    if (os != null) {
        b.compress(Bitmap.CompressFormat.WEBP, 100, os);
    }
} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

如果第2种方式选择ARGB_8888,创建出来的Bitmap其所占的内存(通过Bitmap.getByteCount()计算)以及保存到磁盘上的大小都与第1种方式相同。如果选择RGB_565,相比于第1种方式,Bitmap所占的内存会缩小一半,保存到磁盘上后的体积也会适当减小。

最后,如果大伙有什么好的学习方法或建议欢迎大家在评论中积极留言哈,希望大家能够共同学习、共同努力、共同进步。

小编在这里祝小伙伴们在未来的日子里都可以 升职加薪,当上总经理,出任CEO,迎娶白富美,走上人生巅峰!!

不论遇到什么困难,都不应该成为我们放弃的理由!

很多人在刚接触这个行业的时候或者是在遇到瓶颈期的时候,总会遇到一些问题,比如学了一段时间感觉没有方向感,不知道该从那里入手去学习,需要一份小编整理出来的学习资料的关注我主页或者点击扫描下方二维码免费领取~

这里是关于我自己的Android 学习,面试文档,视频收集大整理,有兴趣的伙伴们可以看看~

如果你看到了这里,觉得文章写得不错就给个赞呗?如果你觉得那里值得改进的,请给我留言,一定会认真查询,修正不足,谢谢。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/2023面试高手/article/detail/303782
推荐阅读
相关标签
  

闽ICP备14008679号