当背景为黑色时,位图 setPixels 将丢失 alpha 通道

Bitmap setPixels will lose alpha channel when background is black

我正在尝试使用 canvas.drawBitmap() 方法在 Android 中绘制自定义视图。但是,我发现如果我在本机 JNI 代码中执行此操作并且背景为黑色,alpha 通道将会丢失。总结一下,案例是:

  1. 当背景为白色时调用javabitmap.setPixels()并在NDK中设置位图像素颜色,两个位图都正确显示
  2. 当背景为黑色时调用javabitmap.setPixels()并在NDK中设置位图像素颜色,只有java绘制的位图API显示正确,用NDK绘制的丢失alpha通道

问题是为什么白底可以,黑底不行?我是不是遗漏了什么或做错了什么?

布局XML文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp" >

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@android:color/black"
        android:orientation="horizontal"
        android:padding="16dp" >

        <com.example.android.TestView
            android:id="@+id/testview1"
            android:layout_width="320px"
            android:layout_height="320px"
            android:layout_margin="16dp" />

        <com.example.android.TestView
            android:id="@+id/testview2"
            android:layout_width="320px"
            android:layout_height="320px"
            android:layout_margin="16dp" />
    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@android:color/white"
        android:orientation="horizontal"
        android:padding="16dp" >

        <com.example.android.TestView
            android:id="@+id/testview3"
            android:layout_width="320px"
            android:layout_height="320px"
            android:layout_margin="16dp" />

        <com.example.android.TestView
            android:id="@+id/testview4"
            android:layout_width="320px"
            android:layout_height="320px"
            android:layout_margin="16dp" />
    </LinearLayout>

</LinearLayout>

MainActivity.java :

package com.example.android;
import com.example.android.R;

import android.app.Activity;
import android.os.Bundle;

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        TestView tv2 = (TestView) findViewById(R.id.testview2);
        TestView tv4 = (TestView) findViewById(R.id.testview4);
        tv2.setDrawFromNative();
        tv4.setDrawFromNative();
    }
}

TestView.java :

package com.example.android;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.view.View;

public class TestView extends View {
    private Bitmap mBitmap;
    private boolean mDrawFromNative;
    private static final int WIDTH = 320;
    private static final int HEIGHT = 320;

    static {
        System.loadLibrary("bitmaptest");
    }
    private native void nativeDrawBitmap(Object bitmap);

    private static void javaDrawBitmap(Bitmap bitmap) {
        int pixels[] = new int[WIDTH * HEIGHT];
        for (int i = 0; i < pixels.length; i++) {
            pixels[i] = 0x88FF0000;
        }
        bitmap.setPixels(pixels, 0, WIDTH, 0, 0, WIDTH, HEIGHT);
    }

    public TestView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mBitmap = Bitmap.createBitmap(WIDTH, HEIGHT, Bitmap.Config.ARGB_8888);
    }

    public void setDrawFromNative() {
        mDrawFromNative = true;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        if(mDrawFromNative) {
            nativeDrawBitmap(mBitmap);
        } else {
            javaDrawBitmap(mBitmap);
        }
        canvas.drawBitmap(mBitmap, 0, 0, null);
    }
}

TestNative.cpp:

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

#define LOGD(...)  __android_log_print(ANDROID_LOG_DEBUG,LOG_TAG,__VA_ARGS__)
#define LOGW(...)  __android_log_print(ANDROID_LOG_WARN,LOG_TAG,__VA_ARGS__)
#define LOGE(...)  __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)

#define LOG_TAG "BMPTEST"

extern "C" {
void Java_com_example_android_TestView_nativeDrawBitmap(JNIEnv* env, jobject thiz, jobject bitmap) {

    AndroidBitmapInfo info;
    void* dst_pixels;
    int   ret;

    if((ret = AndroidBitmap_getInfo(env, bitmap, &info)) < 0) {
        LOGE("AndroidBitmap_getInfo() failed ! error=%d", ret);
        return;
    }
    if ((ret = AndroidBitmap_lockPixels(env, bitmap, &dst_pixels)) < 0) {
        LOGE("AndroidBitmap_lockPixels() failed ! error=%d", ret);
        return;
    }

    unsigned int *dst = (unsigned int *)dst_pixels;
    for(int i=0; i< info.width * info.height; i++) {
            *(dst+i) = (0x88<<24 | 0xff | 0x00<<8 | 0x00<<16); //(ARGB->ABGR)
    }
    AndroidBitmap_unlockPixels(env, bitmap);
}
}

Android.mk 本机代码:

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := libbitmaptest
LOCAL_SRC_FILES := \
    TestNative.cpp
LOCAL_LDLIBS += -llog -ljnigraphics
include $(BUILD_SHARED_LIBRARY)

结果截图:

Android 存储带有预乘 alpha 的位图。当您从 Java 调用 setPixels() 时,RGB 颜色值会自动乘以 alpha 值并存储在位图中。但是,当您从本机代码调用 Android_lockPixels() 然后直接写入内存时,您需要自己进行预乘,否则结果会出错。如果您将代码更改为:

 int premultipliedR = (0xff * 0x88) >> 8;
 for(int i=0; i< info.width * info.height; i++) {
        *(dst+i) = (0x88<<24 | premultipliedR | 0x00<<8 | 0x00<<16);

那么两个 Bitmap 应该渲染相同。

那么为什么 看起来 位图在背景为黑色时似乎失去了 alpha 通道,而在白色背景下却没有?好吧,根据您选择的数字,事实证明这只是巧合。

基本的 alpha 混合公式为:

 dest.r = ((dest.r * (256 - source.a)) + (source.r * source.a)) >> 8;
 dest.g = ((dest.g * (256 - source.a)) + (source.g * source.a)) >> 8;
 dest.b = ((dest.b * (256 - source.a)) + (source.b * source.a)) >> 8;

其中 dest 是背景像素,源是位图中的像素。预乘 alpha 将其更改为:

 dest.r = ((dest.r * (256 - source.a)) >> 8) + source.premultiplied_r;
 dest.g = ((dest.g * (256 - source.a)) >> 8) + source.premultiplied_g;
 dest.b = ((dest.b * (256 - source.a)) >> 8) + source.premultiplied_b;

这节省了一堆乘法。结果都固定为 255。我并不是说这是使用的确切公式,但它非常接近它。

插入数字,对于您的 Java 位图,预乘的 r、g、b 将是 0x87(或 0x88,具体取决于它们如何进行舍入等)、0x00 和 0x00。对于您的本机位图,它们将是 0xff、0x00 和 0x00,因为您没有预乘。将其与黑色背景进行 Alpha 混合与仅添加零相同,因为 dest. rgb 值均为零。所以 结果看起来不同

在白色背景的情况下,dest.gdest.b 在两种情况下的结果相同,因为预乘 gb 值在 Java 和本机位图中均为零。在dest.r的情况下,结果应该是255。在本机位图的情况下,由于预乘r的错误值导致值溢出,但它被限制为255,所以结果看起来一样

简而言之,预乘 r 值对于您的本机位图来说太高了,因此在结果本应为 < 255. 在结果应该是 255 的情况下,它是否太高并不重要,因为它无论如何都会被限制在 255。