I have two LinearGradients which I want to merge:
rgb(0, 0, 0)
to rgb(0, 255, 0)
(black to green)rgb(0, 0, 0)
to rgb(0, 0, 255)
(black to blue)My code looks like this:
Shader horizontal = new LinearGradient(0, 0, width, 0, new float[]{Color.rgb(0, 0, 0), Color.rgb(0, 255, 0)}, null, Shader.TileMode.CLAMP); Shader vertical = new LinearGradient(0, 0, 0, height, new float[]{Color.rgb(0, 0, 0), Color.rgb(0, 0, 255)}, null, Shader.TileMode.CLAMP); ComposeShader shader = new ComposeShader(horizontal, vertical, mode); paint.setShader(shader);
The red value may change but the two others are constant. I want to use the resulting gradient in a color picker. It has to look like this: (you can see it on here too, you have to click on the R letter on the right pane of the color picker)
I tried several PorterDuff modes, a few came close but none matched what I need. SCREEN
is almost perfect but sometimes it's too light. ADD
show red values smaller than 128 as if it was 0. MULTIPLY
fills the square with one solid color and that's it. I also tried setting the colors of the gradients to alpha 128. This makes ADD
too dark, XOR
and SCREEN
too pale.
How can I make this gradient correctly? What PorterDuff mode should I use?
I draw the cursor the same color as the selected color to test if the gradient is correctly drawn. (Selected color is calculated with coordinates) For all pivot values except value, the cursor hard to see/invisible.
Looks like the white gradient turns transparent too quickly. To make it I dew two lineargradients then merged them with ComposeShader
and SRC_OVER
PorterDuff mode. Then I draw a black rectangle with transparency corresponding to the value (brightness) value. I can post code if you need.
EDIT:
I am going to make some assumptions. Based on the link you referenced, I'll assume you'll want to do something similar where you can change the "pivot" color in real time using a slider control like the vertical slider to the right. Also I'll assume that you want to switch between red/green/blue as the pivot color.
Here's how to increase your performance:
int
array for the colors once and reuse that array.With all those things in mind, here's a rewrite of the routine:
private void changeColor(int w, int h, int[] pixels, char pivotColor, int pivotColorValue, boolean initial) {
if (pivotColorValue < 0 || pivotColorValue > 255) {
throw new IllegalArgumentException("color value must be between 0 and 255, was " + pivotColorValue);
}
if (initial) {
// set all the bits of the color
int alpha = 0xFF000000;
for (int y = 0; y < h; y++) {
for (int x = 0; x < w; x++) {
int r = 0, b = 0, g = 0;
switch (pivotColor) {
case 'R':
case 'r':
r = pivotColorValue << 16;
g = (256 * x / w) << 8;
b = 256 * y / h;
break;
case 'G':
case 'g':
r = (256 * x / w) << 16;
g = pivotColorValue << 8;
b = 256 * y / h;
break;
case 'B':
case 'b':
r = (256 * x / w) << 16;
g = (256 * y / h) << 8;
b = pivotColorValue;
break;
}
int index = y * w + x;
pixels[index] = alpha | r | g | b;
}
}
} else {
// only set the bits of the color that is changing
int colorBits = 0;
switch (pivotColor) {
case 'R':
case 'r':
colorBits = pivotColorValue << 16;
break;
case 'G':
case 'g':
colorBits = pivotColorValue << 8;
break;
case 'B':
case 'b':
colorBits = pivotColorValue;
break;
}
for (int i = 0; i < pixels.length; i++) {
switch (pivotColor) {
case 'R':
case 'r':
pixels[i] = (pixels[i] & 0xFF00FFFF) | colorBits;
break;
case 'G':
case 'g':
pixels[i] = (pixels[i] & 0xFFFF00FF) | colorBits;
break;
case 'B':
case 'b':
pixels[i] = (pixels[i] & 0xFFFFFF00) | colorBits;
break;
}
}
}
Here's how I tested it:
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private ImageView mImageView;
private Bitmap mBitmap;
private int[] mPixels;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
setTitle("Demo");
mPixels = new int[256 * 256];
mBitmap = Bitmap.createBitmap(256, 256, Bitmap.Config.ARGB_8888);
mImageView = (ImageView) findViewById(R.id.imageview);
long start = SystemClock.elapsedRealtime();
changeColor(256, 256, mPixels, 'r', 0, true);
mBitmap.setPixels(mPixels, 0, 256, 0, 0, 256, 256);
mImageView.setImageBitmap(mBitmap);
long elapsed = SystemClock.elapsedRealtime() - start;
Log.d(TAG, "initial elapsed time: " + elapsed + " ms");
SeekBar seekBar = (SeekBar) findViewById(R.id.seekbar);
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
long start = SystemClock.elapsedRealtime();
changeColor(256, 256, mPixels, 'r', progress, false);
mBitmap.setPixels(mPixels, 0, 256, 0, 0, 256, 256);
mImageView.setImageBitmap(mBitmap);
long elapsed = SystemClock.elapsedRealtime() - start;
Log.d(TAG, "elapsed time: " + elapsed + " ms");
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) { }
@Override
public void onStopTrackingTouch(SeekBar seekBar) { }
});
}
// changeColor method goes here
}
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/activity_main"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin">
<ImageView
android:id="@+id/imageview"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:scaleType="fitCenter"/>
<SeekBar
android:id="@+id/seekbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="24dp"
android:max="255"/>
</LinearLayout>
Try that out and see if it performs well enough for you. I thought it was reasonable.
I think the underlying Skia library has a Porter-Duff mode that would do this, but it's not available in android.graphics.PorterDuff.Mode
.
Okay fine, I guess we'll just have to do it our damn selves:
private Bitmap makeColorPicker(int w, int h, int r) {
if (r < 0 || r > 255) {
throw new IllegalArgumentException("red value must be between 0 and 255, was " + r);
}
// need to manage memory, OutOfMemoryError could happen here
int[] pixels = new int[w * h];
int baseColor = 0xFF000000 | (r << 16); // alpha and red value
for (int y = 0; y < h; y++) {
for (int x = 0; x < w; x++) {
int g = (256 * x / w) << 8;
int b = 256 * y / h;
int index = y * w + x;
pixels[index] = baseColor | g | b;
}
}
return Bitmap.createBitmap(pixels, w, h, Bitmap.Config.ARGB_8888);
}
Regarding HSV:
Once you switch to HSV color space, some different options open up for you. Now compositing two images like you were originally considering makes sense. I'm just going to give you the thousand words versions of the images. Please don't make me open up PhotoShop.
Pivot on Hue:
I'm picturing a two-way gradient image that could be rendered at development time. This gradient would have zero alpha at the upper right corner, full black at the bottom edge and full white at the upper left corner. As you move through the hue angles, you would just draw a solid color rectangle underneath this image. The color would be the desired hue at full saturation and brightness, so you would see just this color in the upper right corner.
Pivot on Saturation:
Here I'm picturing two gradient images, both could be rendered at development time. The first would be full saturation, where you see the horizontal rainbow at the top blending into black at the bottom. The second would be zero saturation, with white at the top and black at the bottom. You draw the rainbow gradient on the bottom, then draw the white/black gradient on top. Changing the alpha of the top image from zero to full will show the change from full saturation to zero saturation.
Pivot on Brightness (Value)
For this I'm picturing a black rectangle base with another image that is also a horizontal rainbow gradient than tweens vertically to white at the bottom (full brightness). Now you pivot on brightness by changing the rainbow image from full alpha to zero alpha, revealing the black rectangle underneath.
I'd have to do some math to make sure these alpha composites represent the actual color space, but I think I'm pretty close.