📜 ⬆️ ⬇️

The taste and color 2 - not RGB one

Greetings to all readers. Let's try to continue our venture, the beginning of which is here .

So, we have a custom View with a multicolored circle, from which it is now necessary to pull out the color selected by the user. Before plunging into the wilds of calculations, let's start by organizing some markers, pointers of a selected color. We will not complicate and make them in the form of simple lines - arrows. For them, we need a new Paint and sizes. In order not to repeat in the future, let's calculate all the necessary parameters at once. I deliberately write a bunch of separate variables for clarity.

Our ads and methods take the form:
')
// ,         protected static final int SET_COLOR = 0; protected static final int SET_SATUR = 1; protected static final int SET_ALPHA = 2; //  ,        . // (-   ) private int mMode; float cx; float cy; float rad_1; // float rad_2; // float rad_3; // float r_centr; //    float r_sel_c; // float r_sel_s; // float r_sel_a; //    //   private Paint p_color = new Paint(Paint.ANTI_ALIAS_FLAG); private Paint p_satur = new Paint(Paint.ANTI_ALIAS_FLAG); private Paint p_alpha = new Paint(Paint.ANTI_ALIAS_FLAG); private Paint p_white = new Paint(Paint.ANTI_ALIAS_FLAG); private Paint p_handl = new Paint(Paint.ANTI_ALIAS_FLAG); private Paint p_centr = new Paint(Paint.ANTI_ALIAS_FLAG); private float deg_col; //   private float deg_sat; //  -  private float deg_alp; // ******************** private float lc; // private float lm; //     private float lw; // private void calcSizes() { // // cx = size * 0.5f; cy = cx; lm = size * 0.043f; lw = size * 0.035f; rad_1 = size * 0.44f; r_sel_c = size * 0.39f; rad_2 = size * 0.34f; r_sel_s = size * 0.29f; rad_3 = size * 0.24f; r_sel_a = size * 0.19f; r_centr = size * 0.18f; lc = size * 0.08f; p_color.setStrokeWidth(lc); p_satur.setStrokeWidth(lc); p_alpha.setStrokeWidth(lc); } 


First you need to make sure that we choose the color on the outer ring. To do this to the coordinates of the distance from the center horizontally and vertically (in our code it is a and b in ACTION_DOWN), add another one - the distance from the center in a straight line. According to all the laws of geometry we call it "c". And then we calculate, remembering the works of citizen Pythagoras:

 float c = (float) Math.sqrt(a * a + b * b); 


It now remains to verify that the point of contact is on the outer ring, that is, with a larger inner radius of the ring. At the same time, running ahead, we will perform these checks for the remaining rings that are not yet existing. And set the flags. In the end:

  case MotionEvent.ACTION_DOWN: float a = Math.abs(event.getX() - cx); float b = Math.abs(event.getY() - cy); float c = (float) Math.sqrt(a * a + b * b); if (c > r_sel_c) mode = SET_COLOR; else if (c < r_sel_c && c > r_sel_s) mode = SET_SATUR; else if (c < r_sel_s && c > r_sel_a) mode = SET_ALPHA; else if (c < r_centr) listener.onDismiss(mColor, alpha); break; 


Note - we only check the distance from the center in ACTION_DOWN. That is, by poking a finger into the outer ring, we can then crawl along our View as much as possible even outside of the color selection zone, the color will change. Until we finger again and change the mode flag.

Now in ACTION_MOVE we will receive new coordinates and determine the selected color, saturation or transparency. In order not to litter onTouch we will take out the math in separate methods. Well and call invalidate () I think it is better to place here. We made it:

  case MotionEvent.ACTION_MOVE: float x = event.getX() - cx; float y = event.getY() - cy; switch (mMode) { case SET_COLOR: setColScale(getAngle(x, y)); break; case SET_SATUR: setSatScale(getAngle(x, y)); break; case SET_ALPHA: setAlphaScale(getAngle(x, y)); break; } invalidate(); break; } 


Methods of type two in one. Consider more. getAngle (x, y) - based on the coordinates, we determine the angle between the position of the finger and the center of the View. Something like this:

  protected float getAngle(float x, float y) { float deg = 0; if (x != 0) deg = y / x; deg = (float) Math.toDegrees(Math.atan(deg)); if (x < 0) deg += 180; else if (x > 0 && y < 0) deg += 360; return deg; } 


At the output we get the angle in degrees, which now needs to be somehow linked to the color in this sector of our gradient. At the thought came to a standstill. Perverted ideas of calculating the coordinates of the pixels and analyzing their color, I somehow immediately dropped. In his head the words of a penguin from Madagascar were spinning - “Kowalski, suggest options ...”. Google played the role of Kowalski. And that's what he said.

It turns out there is life on other planets. And instead of such a native and understandable ARGB, they use some kind of incomprehensible HSV there. What kind of animal is this? For example, his first letter? Wiki declares that this is “Hue - color tone ... Varies between 0 - 360 ...”. Do you think what a coincidence? And the rest of the letters? S - Saturation - yes this is our second ring! And V - Value is brightness. And Android immediately offers us a couple of functions:

 Color.HSVToColor(int, float[]); Color.colorToHSV(int, float[]); 


The int parameter in the first function is transparency, remembering about our third ring. In the second function, int is the color itself. And in both functions, float [] is an array of three elements, the first of which, respectively, for the letters HSV is the color value of the palette from 0 to 360. Life seems to be getting better.

We declare argb and hsv arrays for storing our color components:

  private int[] argb = new int[] { 255, 0, 0, 0}; private float[] hsv = new float[] {0, 1f, 1f}; 


And simply substitute the previously obtained angle in degrees as the first element of the array.

  protected void setColScale(float f) { deg_col = f; hsv[0] = f; mColor = Color.HSVToColor(argb[0], hsv); p_center.setColor(mColor); } 


Now we have the color, the angle and the full right to draw the second ring and arrows. Here is the code:

  private void drawSaturGradient(Canvas c) { SweepGradient s = null; int[] sg = new int[] { Color.HSVToColor(new float[] {deg_col, 1, 0}), Color.HSVToColor(new float[] {deg_col, 1, 1}), Color.HSVToColor(new float[] { hsv[0], 0, 1}), Color.HSVToColor(new float[] { hsv[0], 0, 0.5f}), Color.HSVToColor(new float[] {deg_col, 1, 0}) }; s = new SweepGradient(cx, cy, sg, null); p_satur.setShader(s); c.drawCircle(cx, cy, rad_2, p_satur); } 


Very similar to the previous code, the same array for the shader, the same gradient. Only now it has 5 colors, each of which we tear out of HSV. Moreover, we set the saturation and brightness manually from 0 to 1, and for some reason I put the angle value into the first (in the sense of zero) array element. It would be more correct to see the value hsv [0] we have, but this is the same value. As proof, I even ferried in two places. So don't forget that deg_col == hsv [0]. Well, the corner came first to me, sorry.

Result:

image

I think everyone understands that this method should be called in onDraw (), like the following. Dada, we can already quite draw the third ring:

  private void drawAlphaGradient(Canvas c) { //           //    c.drawCircle(cx, cy, rad_3 - lw, p_white); c.drawCircle(cx, cy, rad_3, p_white); c.drawCircle(cx, cy, rad_3 + lw, p_white); //   RGB    int ir = Color.red(mColor); int ig = Color.green(mColor); int ib = Color.blue(mColor); //     –       int e = Color.argb(0, ir, ig, ib); int[] mCol = new int[] {mColor, e}; //     Shader sw = new SweepGradient(cx, cy, mCol, null); p_alpha.setShader(sw); c.drawCircle(cx, cy, rad_3, p_alpha); } 


And arrows:

  private void drawLines(Canvas c) { float d = deg_col; c.rotate(d, cx, cy); c.drawLine(cx + rad_1 + lm, cy, cx + rad_1 - lm, cy, p_handl); c.rotate(-d, cx, cy); d = deg_sat; c.rotate(d, cx, cy); c.drawLine(cx + rad_2 + lm, cy, cx + rad_2 - lm, cy, p_handl); c.rotate(-d, cx, cy); d = deg_alp; c.rotate(d, cx, cy); c.drawLine(cx + rad_3 + lm, cy, cx + rad_3 - lm, cy, p_handl); c.rotate(-d, cx, cy); } 


Did anyone have a question - why in the last method is the local variable d? Perhaps these are signs of my paranoia. If you use the global variable deg_col or others directly, during the drawing time, the user can change them by running a finger across the screen. It is clear that changes will be negligible over those microseconds of drawing. But nevertheless functions
 c.rotate(deg_col, cx, cy); 

and
 c.rotate(-deg_col, cx, cy); 


will rotate the canvas by a different amount. And this difference will gradually accumulate.

Well, do not forget, of course, set the properties for our Paint to taste. I have this somehow:
  private void init(Context context) { setFocusable(true); p_color.setStyle(Style.STROKE); p_satur.setStyle(Style.STROKE); p_alpha.setStyle(Style.STROKE); p_center.setStyle(Style.FILL_AND_STROKE); p_white.setStrokeWidth(2); p_white.setColor(Color.WHITE); p_white.setStyle(Style.STROKE); p_handl.setStrokeWidth(5); p_handl.setColor(Color.WHITE); p_handl.setStrokeCap(Cap.ROUND); setOnTouchListener(this); } 


setFocusable (true) I missed in the last article.

Back to our OnTouch.

  protected void setSatScale(float f) { deg_sat = f; if (f < 90) { hsv[1] = 1; hsv[2] = f / 90; } else if (f >= 90 && f < 180) { hsv[1] = 1 - (f - 90) / 90; hsv[2] = 1; } else { hsv[1] = 0; hsv[2] = 1 - (f - 180) / 180; } mColor = Color.HSVToColor(argb[0], hsv); p_center.setColor(mColor); } protected void setAlphaScale(float f) { deg_alp = f; argb[0] = (int) (255 - f / 360 * 255); mColor = Color.HSVToColor(argb[0], hsv); alpha = (float) Color.alpha(mColor) / 255; p_center.setColor(mColor); } 


Well, we have to somehow get the result. Here again, a matter of taste and a specific use case. For some, it is more convenient to write a value in the Preference, to send someone an Intent in all directions. I propose to organize our View interface, like a real adult and independent controls. The color value we can send once by pressing the center of the circle, we can in real-time, as the color changes in OnTouch. Walk so walk, do both, and another:

 private OnColorChangeListener listener; public interface OnColorChangeListener { public void onDismiss(int val, float alpha); public void onColorChanged(int val, float alpha); } public void setOnColorChangeListener(OnColorChangeListener l) { this.listener = l; } 


  OnTouch: case MotionEvent.ACTION_DOWN: … … else if (c < r_centr) { listener.onDismiss(mColor, alpha); } break; case MotionEvent.ACTION_MOVE: … … listener.onColorChanged(mColor, alpha); break; } return true; } 


I hope I have not forgotten anything. Oh yes. It is desirable to be able to transfer the current color value to our ColorPicker. Add:

  public void setUsedColor(int color, float a) { mColor = color; Color.colorToHSV(mColor, hsv); setColScale(hsv[0]); float deg = 0; if (hsv[1] == 1) deg = 90 * hsv[2]; else if (hsv[2] == 1) deg = 180 - 90 * hsv[1]; else if (hsv[1] == 0) deg = 360 - 180 * hsv[2]; setSatScale(deg); setAlphaScale(360 - 360 * a); } 


PS: Another nuance was found in practical use. Attempting to apply the resulting color to images (in the form of ColorFilter) does not change their transparency. Or did I miss something? If so, I hope more experienced comrades will correct me. I had to use the setAlpha method, previously getting the value of transparency using the Color.alpha (mColor) method. The value of int is 0-255, and setAlpha (int) has recently been deprecated. A float from 0 to 1 is required (of type setAlpha ((float) Color.alpha (mColor) / 255));

Since we pretend to the universality of our control, it makes sense to stick these calculations into it. And give out the transparency of the float format 0-1. It is possible by a separate method in the interface, it is possible by the second parameter in addition to color - a matter of taste. Added it to the code.

Although for complete versatility, you can force it to give out separately all the components - you never know where it is needed. I will not implement it now, I think this is not a problem even for a teapot.

Now that's it.

Source: https://habr.com/ru/post/254895/


All Articles