Приветствую всех читателей. Попробуем продолжить нашу затею, начало которой здесь.
Итак, мы имеем кастомную View с разноцветным кружочком, из которого теперь необходимо выдернуть выбранный пользователем цвет. Перед тем как окунуться в дебри расчетов давайте для начала организуем какие-нибудь маркеры-указатели выбранного цвета. Не будем усложнять и сделаем их в виде простых линий – стрелок. Для них нам понадобится новая Paint и размеры. Чтобы не повторяться в дальнейшем, давайте рассчитаем сразу все необходимые параметры. Я сознательно пишу кучу отдельных переменных для наглядности.
Наши объявления и методы приобретают вид:
// Константы, определяющие что именно мы устанавливаем в данный момент
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);
}
Для начала надо убедиться, что мы выбираем именно цвет на наружном кольце. Для этого к координатам расстояния от центра по горизонтали и по вертикали (в нашем коде это a и b в ACTION_DOWN), добавляем еще одну – расстояние от центра по прямой. По всем законам геометрии обзовем ее «с». И тут же вычислим, вспомнив труды гражданина Пифагора:
float c = (float) Math.sqrt(a * a + b * b);
Теперь остается проверить, что место касания находится на наружном кольце, то есть с больше внутреннего радиуса кольца. Заодно, забегая вперед, выполним эти проверки для остальных еще не существующих колец. И выставим флаги. В конечном итоге:
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;
Заметьте – проверку расстояния от центра мы выполняем только в ACTION_DOWN. То есть ткнув пальцем в наружное кольцо, мы можем потом сколько угодно елозить по нашей View даже за пределами зоны выбора цвета, меняться будет именно цвет. Пока мы не ткнем пальцем повторно и не сменим флаг mode.
Теперь в ACTION_MOVE будем получать новые координаты и определять выбранный цвет, насыщенность или прозрачность. Чтобы не засорять onTouch вынесем математику в отдельные методы. Ну и вызов invalidate() я думаю лучше сюда же поместить. У нас получилось:
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;
}
Методы типа два в одном. Рассмотрим подробнее. getAngle(x, y) – на основании координат определяем угол между положением пальца и центром View. Что-то типа такого:
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;
}
На выходе получаем угол в градусах, который теперь необходимо как-то связать с цветом в этом секторе нашего градиента. На этом мысль зашла в тупик. Извращенческие идеи вычисления координат пикселов и анализа их цвета я как-то сразу отбросил. В голове вертелись слова пингвина из Мадагаскара – «Ковальски, предложите варианты…». В роли Ковальского выступил Гугл. И вот что он сказал.
Оказывается есть жизнь и на других планетах. И вместо такого родного и понятного ARGB там используют какой-то непонятный HSV. Что это за зверь такой? Например первая его буква? Вики заявляет, что это «Hue – цветовой тон… Варьируется в пределах 0 – 360…». Прикидываете, какое совпадение? А остальные буквы? S – Saturation – да это же наше второе кольцо! А V – Value – это яркость. И Андроид тут же предлагает нам пару функций:
Color.HSVToColor(int, float[]);
Color.colorToHSV(int, float[]);
Параметр int в первой функции – прозрачность, вспоминаем про наше третье кольцо. Во второй функции int это непосредственно цвет. И в обеих функциях float[] это массив из трех элементов, первый из которых соответственно буквам HSV и есть значение цвета палитры от 0 до 360. Жизнь, похоже, налаживается.
Объявляем массивы argb и hsv для хранения компонентов нашего цвета:
private int[] argb = new int[] { 255, 0, 0, 0};
private float[] hsv = new float[] {0, 1f, 1f};
И просто подставляем полученный ранее угол в градусах в качестве первого элемента массива.
protected void setColScale(float f) {
deg_col = f;
hsv[0] = f;
mColor = Color.HSVToColor(argb[0], hsv);
p_center.setColor(mColor);
}
Теперь у нас есть цвет, угол и полное право рисовать второе кольцо и стрелки. Вот код:
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);
}
Очень похоже на предыдущий код, тот же массив для шейдера, тот же градиент. Только теперь в нем 5 цветов, каждый из которых мы выдираем из HSV. Причем насыщенность и яркость задаем вручную от 0 до 1, а в первый (в смысле нулевой) элемент массива я почему-то засунул значение угла. Более правильно было бы видеть там имеющееся у нас значение hsv[0], но это ведь одна и та же величина. В качестве доказательства я даже переправил в двух местах. Так что не забываем, что deg_col == hsv[0]. Ну угол мне первый под руку попался, простите.
Результат:
Думаю, всем понятно, что этот метод должен вызываться в onDraw(), как и следующие. Дада, мы вполне уже можем рисовать третье кольцо:
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);
}
И стрелочки:
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);
}
У кого-нибудь возник вопрос – зачем в последнем методе локальная переменная d? Возможно, это признаки моей паранойи. Если использовать непосредственно глобальную переменную deg_col или другие, за время отрисовки юзер может их изменить, водя пальцем по экрану. Понятное дело, что за те микросекунды отрисовки изменения будут ничтожными. Но тем не менее функции
c.rotate(deg_col, cx, cy);
и
c.rotate(-deg_col, cx, cy);
будут поворачивать Canvas на разную величину. И разница эта будет постепенно накапливаться.
Ну не забываем, конечно, задать свойства для наших Paint по вкусу. У меня это как-то так:
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) я пропустил в прошлой статье.
Возвращаемся к нашим 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);
}
Ну что, нам осталось как-то вывести полученный результат. Тут опять же дело вкуса и конкретного варианта использования. Кому-то удобнее значение в Preference писать, кому-то Intent слать во все стороны. Я предлагаю организовать нашему View интерфейс, как у настоящего взрослого и самостоятельного контрола. Значение цвета мы можем слать однократно по нажатию на центр круга, можем в реалтайме, по мере изменения цвета в OnTouch. Гулять так гулять, сделаем и то, и другое:
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;
}
Надеюсь, ничего не забыл. А, да. Желательно иметь возможность передавать в наш ColorPicker текущее значение цвета. Добавляем:
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);
}
P.S: Еще один нюанс выяснился при практическом использовании. Попытка применить полученный цвет к картинкам (в виде ColorFilter) не меняет их прозрачность. Или я что-то пропустил? Если да – надеюсь, меня поправят более опытные товарищи. Пришлось использовать метод setAlpha, предварительно получив значение прозрачности методом Color.alpha(mColor). Значение int 0-255, а setAlpha(int) в последнее время deprecated. Требуется float от 0 до 1 (типа setAlpha((float) Color.alpha(mColor) / 255));
Раз уж мы претендуем на универсальность нашего контрола, есть смысл засунуть эти вычисления в него. И выдавать прозрачность формата float 0-1. Можно отдельным методом в интерфейсе, можно вторым параметром дополнительно у цвету – дело вкуса. Добавил это в код.
Хотя для полной универсальности можно заставить его выдавать раздельно все компоненты – мало ли где понадобится. Не буду это сейчас реализовывать, думаю это не проблема даже для чайника.
Вот теперь все.
Автор: OJV