На вкус и цвет 2 – не RGB единым

от автора

Приветствую всех читателей. Попробуем продолжить нашу затею, начало которой здесь.

Итак, мы имеем кастомную 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]. Ну угол мне первый под руку попался, простите.

Результат:

image

Думаю, всем понятно, что этот метод должен вызываться в 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *