Bitmap称为位图,内部结构是像素矩阵排列。它由A、R、G、B通道组成,其中A代表Alpha,R代表Red,G代表Green。我们在开发中,通常把图片转为Bitmap来处理。

一、Bitmap结构类型

Bitmap按照内部结构,分为6种类型Config:ALPHA_8、RGB_565、ARGB_4444、ARGB_8888、RGBA_F16、HARDWARE。常用类型是RGB_565和ARGB_8888,在Android中默认使用ARGB_8888来创建Bitmap。

ALPHA_8:只使用一个Alpha单通道,共占1个字节
RGB_565:RGB通道按5:6:5比例排列,共占2个字节
ARGB_4444:ARGB通道各占4bits,共占2个字节(已弃用)
ARGB_8888:ARGB通道各占8bits,共占4个字节
RGBA_F16:RGBA通道各占16bits,共占8个字节
HARDWARE:适用于只保存在图像内存的情况,Bitmap不可修改

二、创建Bitmap

Android的API提供创建的Bitmap方法包括:匿名共享内存Bitmap、缩放Bitmap、硬件Bitmap、通用Bitmap。

1、匿名共享内存:createAshmemBitmap(),共享内存利于进程间传递

2、缩放:createScaledBitmap(Bitmap src, int dstWidth, int dstHeight, boolean filter)

3、硬件:Bitmap createHardwareBitmap(GraphicBuffer graphicBuffer)

4、通用:createBitmap(int width, int height, Config config)

三、Bitmap拷贝

Bitmap可以拷贝成一个新的Bitmap,也可以拷贝到Buffer中,或者从Buffer拷贝中出来。

1、拷贝新Bitmap:copy(Config config, boolean isMutable),与原Bitmap的像素密度和色彩空间一致

2、像素拷贝到Buffer:copyPixelsToBuffer(Buffer dst),Buffer的像素与原Bitmap保持一致

3、从Buffer拷贝像素:copyPixelsFromBuffer(Buffer src),如果要再次从Buffer读取,需要先调用rewind方法

四、Bitmap压缩

Bitmap支持压缩格式包括:PNG、JPEG、WEBP,可设置压缩质量0—100。整个过程是把Bitmap压缩并输出到指定的OutputStream,以指定格式保存,也就是Bitmap保存为图片到内存里。调用的压缩方法:compress(CompressFormat format, int quality, OutputStream stream)。提供的压缩格式:

public enum CompressFormat {
    JPEG    (0),
    PNG     (1),
    WEBP    (2);

    CompressFormat(int nativeInt) {
        this.nativeInt = nativeInt;
    }
    final int nativeInt;
}

五、获取Bitmap大小

我们可以获取Bitmap的每行占用字节数、占用的总字节数和分配内存的字节数。

1、每行占用字节数:getRowBytes()

2、占用的总字节数:getByteCount(),通过每行占用字节数*高度得出

3、分配内存字节数:getAllocationByteCount(),如果Bitmap有重配置,可能比getByteCount()的结果更大

六、读写Bitmap的像素数组

如果要修改Bitmap的像素,我们可以把Bitmap的像素数组读出来,然后修改,再重新赋值给Bitmap。前提是Bitmap是mutable可修改的,可以通过isMutable()方法来判断该Bitmap是否支持修改。

1、获取像素点:getPixel(int x, int y)

2、获取像素数组:getPixels(int[] pixels, int offset, int stride, int x, int y, int width, int height)

3、修改像素点:setPixel(int x, int y, @ColorInt int color)

4、修改像素数组:setPixels(@ColorInt int[] pixels, int offset, int stride, int x, int y, int width, int height)

七、获取Bitmap

BitmapFactory提供四种方式来获取Bitmap:文件、资源、字节数组、输入流。

1、从文件获取:decodeFile(String pathName, Options opts)

2、从资源获取:decodeResource(Resources res, int id, Options opts)

3、从字节数组获取:decodeByteArray(byte[] data, int offset, int length, Options opts)

4、从输入流获取:decodeStream(InputStream is, Rect outPadding, Options opts)

------------------------------------------------Bitmap进阶与扩展---------------------------------------------------

八、Bitmap与Mat转换

openCV的图像处理开发中,操作对象是Mat,而我们Android平台是Bitmap。这时候就需要Bitmap与Mat的互相转换了,需要用到Android平台的openCV SDK,使用Utils类提供的方法来转换。

1、Bitmap转Mat:Utils.bitmapToMat(bitmap, mat);

2、Mat转Bitmap:Utils.matToBitmap(mat, newBitmap);

九:孪生兄弟mipmap

mipmap使用纹理映射技术,压缩率比bitmap高一倍,所占内存比bitmap少一半。在Android开发中,Google官方建议Launcher的icon图标保存在mipmap文件夹下。Bitmap类也提供方法判断是否支持mipmap:boolean hasMipMap()。

十、图片解码为Bitmap

在Android API 28以后,提供ImageDecoder类,可以把编码的图片解码为Bitmap或Drawable,其中支持的图片格式有:PNG、JPEG、WEBP、GIF、HEIF。这个类的功能有点类似BitmapFactory,同样是把图片转为Bitmap。然鹅ImageDecoder功能比较强大。除了解码图片,还支持解码图片头信息监听回调、图片剪裁、设置图片分辨率等等。首先看下图片解码操作:

@TargetApi(28)
private void decodeImage(){
    ImageDecoder.Source source = ImageDecoder.createSource(getResources(), R.drawable.ferrari);
    try {
        Bitmap bitmap = ImageDecoder.decodeBitmap(source);
        imgDecode.setImageBitmap(bitmap);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

如果要实现解码时,对图片头信息监听,可以使用OnHeaderDecodedListener来回调,其中info包含图片宽和高。操作如下:

private void decodeImage(){
    ImageDecoder.Source source = ImageDecoder.createSource(getResources(), R.drawable.ferrari);
    try {
        //使用OnHeaderDecodedListener监听解码信息
        Bitmap bitmap = ImageDecoder.decodeBitmap(source, new ImageDecoder.OnHeaderDecodedListener() {
            @Override
            public void onHeaderDecoded(ImageDecoder decoder, ImageDecoder.ImageInfo info, ImageDecoder.Source source) {
                Log.e("onHeaderDecoded", "width&&height="+info.getSize());
            }
        });
        imgDecode.setImageBitmap(bitmap);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

如果要剪裁,可以在OnHeaderDecodedListener回调中,调用setCrop方法对图片剪裁:

@TargetApi(28)
private ImageDecoder.OnHeaderDecodedListener headerDecodedListener = new ImageDecoder.OnHeaderDecodedListener() {
    @Override
    public void onHeaderDecoded(ImageDecoder decoder, ImageDecoder.ImageInfo info, ImageDecoder.Source source) {
        //剪裁图片
        decoder.setCrop(new Rect(0, 0, 300, 200));
    }
};

如果要重新设置分辨率,同样地,可以在OnHeaderDecodedListener回调中,调用setTargetSize(int width, int height)或者setTargetSampleSize(int sampleSize)来设置。

十一、加载高清大图

Android SDK提供BitmapRegionDecoder 来加载高清大图,首先调用newInstance(String pathName, boolean isShareable)方法来创建实例对象,然后是调用decodeRegion(Rect rect, Options options)来解码图片的矩形区域,解码出来是一个bitmap对象:

private void decodeLargeImage(){
    String rootPath = Environment.getExternalStorageDirectory().getPath();
    String filePath = rootPath + File.separator + "large.jpg";
    try {
        //传参可以是图片路径、输入流、字节数组
        BitmapRegionDecoder regionDecoder = BitmapRegionDecoder.newInstance(filePath, false);
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        options.inSampleSize = 1;//设置加载图片分辨率
        options.inJustDecodeBounds = false;
        Rect rect = new Rect(0, 0, 100, 100);//待解码的矩形区域
        Bitmap bitmap = regionDecoder.decodeRegion(rect, options);
        imgDecode.setImageBitmap(bitmap);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

左边是原图,右边是加载图片的一部分:

Android Bitmap 修改部分 android bitmap rgba_Android

      

Android Bitmap 修改部分 android bitmap rgba_Android_02

十二、ndk操作Bitmap

Android有提供ndk操作Bitmap的方法,主要用到bitmap.h头文件,另外在Android.mk添加jnigraphics库依赖(如果是cmake编译,在target_link_libraries中添加jnigraphics):

LOCAL_LDLIBS += -ljnigraphics

bitmap.h头文件主要提供三个方法:

//获取bitmap对应图片的宽、高、格式
int AndroidBitmap_getInfo(JNIEnv* env, jobject jbitmap, AndroidBitmapInfo* info);
//上锁,从bitmap获取像素数据赋值给addrPtr指针
int AndroidBitmap_lockPixels(JNIEnv* env, jobject jbitmap, void** addrPtr);
//解锁
int AndroidBitmap_unlockPixels(JNIEnv* env, jobject jbitmap);

包含头文件:

#include <jni.h>
#include <android/bitmap.h>

在java层传递jobject类型的bitmap到native层,然后获取到像素数组作进一步处理:

JNIEXPORT jobject JNICALL Java_com_frank_ndk_modifyBitmap
        (JNIEnv *env, jobject obj, jobject bitmap) {

    AndroidBitmapInfo bitmapInfo;//bitmap信息:宽、高、格式
    unsigned char* bitmapPtr;//像素数组
    int ret = AndroidBitmap_getInfo(env, bitmap, &bitmapInfo);//获取bitmap信息
    if (ret != ANDROID_BITMAP_RESULT_SUCCESS){
        return NULL;
    }
    AndroidBitmap_lockPixels(env, bitmap, &bitmapPtr);//上锁
    ...//操作像素数组
    AndroidBitmap_unlockPixels(env, bitmap);//解锁
    return bitmap;
}

十三、使用BitmapShader渲染Bitmap

Android中有提供BitmapShader来把Bitmap渲染成图片,以自定义圆角图片为例,操作步骤包含:创建BitmapShader、计算缩放系数、设置缩放矩阵、渲染圆角图片。在自定义View的onDraw方法调用以上步骤即可,代码如下:

//利用BitmapShader渲染自定义圆角图片
private void drawShader(Canvas canvas){
    //使用bitmap创建shader
    BitmapShader shader = new BitmapShader(mBitmap, Shader.TileMode.MIRROR, Shader.TileMode.MIRROR);
    //计算bitmap宽与高的较小值
    int size = Math.min(mBitmap.getWidth(), mBitmap.getHeight());
    //计算缩放系数
    float scale = mRadius * 2.0f / size;
    mMatrix.setScale(scale, scale);
    //shader设置matrix矩阵
    shader.setLocalMatrix(mMatrix);
    //shader传递给paint
    mPaint.setShader(shader);
    //渲染圆角图片
    canvas.drawCircle(mRadius, mRadius, mRadius, mPaint);
}

十四、使用RenderNode来绘制Bitmap

在Android Q中,新增RenderNode用于构建硬件加速渲染层次布局。每一个RenderNode包含一个DisplayList和一系列属性,它把复杂的布局切分成更小区域,这样局部更新时所花费代价更小。它需要使用RecordingCanvas来绘制,仅支持硬件加速场景,可以使用Canvas.isHardwareAccelerated()来判断是否支持硬件加速 。

1、创建RenderNode,设置待渲染的矩形区域

RenderNode renderNode = RenderNode.create("myRenderNode");
renderNode.setLeftTopRightBottom(0, 0, 50, 50); // Set the size to 50x50
RecordingCanvas canvas = renderNode.startRecording();
try { 
     canvas.drawRect(rect);
} finally {
     renderNode.endRecording();
}

2、在View中绘制RenderNode

protected void onDraw(Canvas canvas) {
    if (canvas instanceof RecordingCanvas) {
        RecordingCanvas recordingCanvas = (RecordingCanvas) canvas;
        if (!myRenderNode.hasDisplayList()) {
             updateDisplayList(myRenderNode);
        }
        recordingCanvas.drawRenderNode(myRenderNode);
    }
}

3、遍历Bitmap来分块渲染

private void createDisplayList() {
    mRenderNode = RenderNode.create("mRenderNode");
    mRenderNode.setLeftTopRightBottom(0, 0, width, height);
    RecordingCanvas canvas = mRenderNode.startRecording();
    try {
        for (Bitmap b : mBitmaps) {
            canvas.drawBitmap(b, 0.0f, 0.0f, null);
            canvas.translate(0.0f, b.getHeight());
        }
    } finally {
        mRenderNode.endRecording();
    }
}

4、释放资源

renderNode.discardDisplayList();