Приветствую всех читателей. Попробуем продолжить нашу затею, начало которой здесь.
Итак, мы имеем кастомную 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. Можно отдельным методом в интерфейсе, можно вторым параметром дополнительно у цвету – дело вкуса. Добавил это в код.
Хотя для полной универсальности можно заставить его выдавать раздельно все компоненты – мало ли где понадобится. Не буду это сейчас реализовывать, думаю это не проблема даже для чайника.
Вот теперь все.
ссылка на оригинал статьи http://habrahabr.ru/post/254895/
Добавить комментарий