Снова доброго времени суток всем. В этой статье хотелось бы продолжить тему кастомных View, а именно диалога выбора цвета. 
Небольшое отступление: Предвосхищая негативные отзывы, хочу заранее предупредить — статья рассчитана на новичков в программировании. Начиная знакомиться с разработкой под Андроид, я столкнулся (да и сейчас регулярно сталкиваюсь) с интересным фактом: в интернете воз и маленькая тележка информации посвящена установке и настройке Эклипса, потом красочно описывают, как нажать New – Android Application Project и получить свой первый ХеллоВорд. А потом сразу сервисы – потоки – биндинги – хендлеры и всякие прочие базы данных. Посередине – пустота. Как раз эту пустоту я и пытаюсь заполнить, облегчить переход от чайника хотя бы к кофейнику так сказать. Очень надеюсь, что помогу кому-то найти нечто нужное без лишних мучений. И не могу в очередной раз не отметить еще одну особенность Рунета: Не так давно в очередной разнаступил на грабли столкнулся я с проблемой, можно сказать смешной для знающего человека, но для меня незнакомой. Довольно быстро нашел решение на каком-то англоязычном форуме. Топик состоял из двух постов. Вопрос и ответ. Все! Если кому интересно будет – найду ссылку. Да, кстати, возникший вопрос относится и к данной статье, так что дальше я это отмечу. Так вот, а посмотреть на наши форумы – жуть. Каждый вопрос порождает несколько страниц флуда на тему «читай документацию», ответа же в большинстве случаев так и нет.
Ну ладно, извините за многословие, наболело, так сказать. Продолжим под катом.
 
Итак, в очередном проекте столкнулся я с необходимостью изменения цветов каких-то элементов в программе. То есть нужен соответствующий элемент управления, назовем его ColorPicker. В стандартном наборе таковой отсутствует, то есть каждый извращается как может. Да что там говорить, даже у Майкрософта на ББ нет единства, посмотрите на диалоги выбора цвета в Ворде скажем и в том же Paint.
На смартфоне не так просто попасть пальцем в нужную точку, вводить вручную значения тоже не очень удобно. Поиск в интернете, тем не менее, предлагает либо некоторое подобие вышеупомянутых смартфононеудобных Paintообразных диалогов, либо максимально упрощенные варианты типа тройки ползунков для RGB, отдельный ползунок для альфа-канала, или вообще фиксированный набор из нескольких цветных плиток. Да и не люблю я чужие библиотеки в свои проекты пихать, поэтому поиск был довольно беглым. А хотелось всего и сразу. И в одном месте.
В итоге после длительных раздумий родилось вот такое:

То есть все довольно просто. Первое (наружное) кольцо представляет градиент GRB. Второе в нижней половине регулирует насыщенность выбранного цвета, переходя затем в оттенки серого. Ну и третье кольцо – прозрачность. Все под рукой одновременно. Центральный круг отображает непосредственно текущий выбранный цвет и параллельно выполняет функцию кнопки OK.
Ну а теперь, если кому понравилось это цветное чудовище, давайте попробуем все это дело накодить.
Поскольку рисовать не так уж много, да и скорость отрисовки некритична, отнаследуемся от View и будем рисовать на Canvas. Для правильного позиционирования наших художеств надо знать размеры, размеры самой View тоже хочется контролировать, поэтому переопределим метод onMeasure(). Получается вот такая заготовка:
Теперь наш класс обязан появиться в разделе Custom & Library Views палитры элементов. Тащим его на разметку нашего ХеллоВорда.
Должно получиться что-то вроде этого:

Получилось? Замечательно выходит.
Переходим к рисованию. Давайте начнем снаружи. Нам необходимо кольцо с градиентом цветов. Кольцо, кто не в курсе, рисуется так:
То есть нам необходим радиус окружности и кисть (или краска, как там Paint переводится).
Добавляем к нашему коду:
Если теперь все это запустить, будет как с тем сусликом – кольца не видно, но оно есть. Просто мы ему цвет не задали. Не ему, вернее, а нашей Paint p_color.
С цветом будем делать следующее. Помните же, бывают такие шейдеры. И градиенты. Определяем массив из четырех цветов:
int[] mColors = new int[] {Color.RED, Color.GREEN, Color.BLUE, Color.RED};
(Из четырех – чтоб не было резкого перехода. От красного начали и к красному вернулись).
Применяем градиент к шейдеру, шейдер к кисти, кисть к окружности,яйцо в утке, утка в зайце…
Смотрим:

Красивенько получилось, правда?
А вот второе кольцо рисовать пока рановато. Как мы помним, там должен быть нарисован градиент насыщенности выбранного цвета. А ничего еще не выбрано. Надо заставить нашу View реагировать на конечности пользователя. То есть используем метод OnTouch. А чтобы наш класс был вполне себе самостоятельным, изобразим OnTouch непосредственно внутри класса:
После чего Эклипс заботливо предлагает добавить метод:
Вроде все просто, но тут и вылезли те самые грабли, упомянутые в начале статьи. Эклипс упорно что-то требовал. Как выяснилось ему хотелось дополнить onTouch:
И соответственно метод:
После чего все успокоились. Теперь мы смело получаем координаты в onTouch:
И вперед — вычислять цвета. Вот где простор для критики открывается, ибо не дружу я особо с математикой. Поэтому продолжить предлагаю в следующей статье.
              
            Небольшое отступление: Предвосхищая негативные отзывы, хочу заранее предупредить — статья рассчитана на новичков в программировании. Начиная знакомиться с разработкой под Андроид, я столкнулся (да и сейчас регулярно сталкиваюсь) с интересным фактом: в интернете воз и маленькая тележка информации посвящена установке и настройке Эклипса, потом красочно описывают, как нажать New – Android Application Project и получить свой первый ХеллоВорд. А потом сразу сервисы – потоки – биндинги – хендлеры и всякие прочие базы данных. Посередине – пустота. Как раз эту пустоту я и пытаюсь заполнить, облегчить переход от чайника хотя бы к кофейнику так сказать. Очень надеюсь, что помогу кому-то найти нечто нужное без лишних мучений. И не могу в очередной раз не отметить еще одну особенность Рунета: Не так давно в очередной раз
Ну ладно, извините за многословие, наболело, так сказать. Продолжим под катом.
Итак, в очередном проекте столкнулся я с необходимостью изменения цветов каких-то элементов в программе. То есть нужен соответствующий элемент управления, назовем его ColorPicker. В стандартном наборе таковой отсутствует, то есть каждый извращается как может. Да что там говорить, даже у Майкрософта на ББ нет единства, посмотрите на диалоги выбора цвета в Ворде скажем и в том же Paint.
На смартфоне не так просто попасть пальцем в нужную точку, вводить вручную значения тоже не очень удобно. Поиск в интернете, тем не менее, предлагает либо некоторое подобие вышеупомянутых смартфононеудобных Paintообразных диалогов, либо максимально упрощенные варианты типа тройки ползунков для RGB, отдельный ползунок для альфа-канала, или вообще фиксированный набор из нескольких цветных плиток. Да и не люблю я чужие библиотеки в свои проекты пихать, поэтому поиск был довольно беглым. А хотелось всего и сразу. И в одном месте.
В итоге после длительных раздумий родилось вот такое:

То есть все довольно просто. Первое (наружное) кольцо представляет градиент GRB. Второе в нижней половине регулирует насыщенность выбранного цвета, переходя затем в оттенки серого. Ну и третье кольцо – прозрачность. Все под рукой одновременно. Центральный круг отображает непосредственно текущий выбранный цвет и параллельно выполняет функцию кнопки OK.
Ну а теперь, если кому понравилось это цветное чудовище, давайте попробуем все это дело накодить.
Поскольку рисовать не так уж много, да и скорость отрисовки некритична, отнаследуемся от View и будем рисовать на Canvas. Для правильного позиционирования наших художеств надо знать размеры, размеры самой View тоже хочется контролировать, поэтому переопределим метод onMeasure(). Получается вот такая заготовка:
public class ColorPicker extends View {
	private float				cx;
	private float				cy;
	private int					size;
	public ColorPicker(Context context) {
		this(context, null);
	}
	public ColorPicker(Context context, AttributeSet attrs) {
		this(context, attrs, 0);
	}
	public ColorPicker(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		init(context);
	}
	private void init(Context context) {
		// Метод на потом, будем тут что-нибудь делать при создании класса
	}
	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		int mWidth = measure(widthMeasureSpec);
		int mHeight = measure(heightMeasureSpec);
		size = Math.min(mWidth, mHeight);
		setMeasuredDimension(size, size);
		// Вычислили размер доступной области, определили что меньше
		// и установили размер нашей View в виде квадрата со стороной в 
		// высоту или ширину экрана в зависимости от ориентации.
		// Вместо Math.min как вариант можно использовать getConfiguration,
		// величину size можно умножать на какие-нибудь коэффициенты, 
		// задавая размер View относительно размера экрана. Например так:
		/*int orient = getResources().getConfiguration().orientation;
		switch (orient) {
		case Configuration.ORIENTATION_PORTRAIT:
			size = (int) (measureHeight * port);
			break;
		case Configuration.ORIENTATION_LANDSCAPE:
			size = (int) (measureHeight * land);
			break;
		}*/
		
		calculateSizes();
		// И запустили метод для расчетов всяких наших размеров
	}
	private int measure(int measureSpec) {
		int result = 0;
		int specMoge = MeasureSpec.getMode(measureSpec);
		int specSize = MeasureSpec.getSize(measureSpec);
		if (specMoge == MeasureSpec.UNSPECIFIED) result = 200;
		else result = specSize;
		return result;
	}
	private void calculateSizes() {
		cx = size * 0.5f;
		cy = cx;
		// Забегая вперед вычислили координаты центра нашего квадрата.
	}
	@Override
	protected void onDraw(Canvas c) {
		super.onDraw(c);
		// Ну а тут будем рисовать
		// Для начала проверим, что все работает – нарисуем просто фон
		c.drawColor(Color.BLUE);
		//invalidate() будем потом вызывать
	}
}
Теперь наш класс обязан появиться в разделе Custom & Library Views палитры элементов. Тащим его на разметку нашего ХеллоВорда.
Должно получиться что-то вроде этого:

Получилось? Замечательно выходит.
Переходим к рисованию. Давайте начнем снаружи. Нам необходимо кольцо с градиентом цветов. Кольцо, кто не в курсе, рисуется так:
	@Override
	protected void onDraw(Canvas c) {
		super.onDraw(c);
		c.drawCircle(cx, cy, rad_1, p_color);
	}
То есть нам необходим радиус окружности и кисть (или краска, как там Paint переводится).
Добавляем к нашему коду:
private Paint p_color	= new Paint(Paint.ANTI_ALIAS_FLAG);
	private void calculateSizes() {
		cx = size * 0.5f;
		cy = cx;
		// вычисляем радиус
		rad_1 = size * 0.44f;
		// 0.44 – ну понравилось мне это число. Можно подобрать свое
		// Теперь кисть:
		p_color.setStrokeWidth(size * 0.08f);
		// То есть ставим толщину линии 0.08 от size
		// опять же можно экспериментировать
	}
Если теперь все это запустить, будет как с тем сусликом – кольца не видно, но оно есть. Просто мы ему цвет не задали. Не ему, вернее, а нашей Paint p_color.
С цветом будем делать следующее. Помните же, бывают такие шейдеры. И градиенты. Определяем массив из четырех цветов:
int[] mColors = new int[] {Color.RED, Color.GREEN, Color.BLUE, Color.RED};
(Из четырех – чтоб не было резкого перехода. От красного начали и к красному вернулись).
Применяем градиент к шейдеру, шейдер к кисти, кисть к окружности,
		Shader s = new SweepGradient(cx, cy, mColors, null);
		p_color.setShader(s);
Смотрим:

Красивенько получилось, правда?
А вот второе кольцо рисовать пока рановато. Как мы помним, там должен быть нарисован градиент насыщенности выбранного цвета. А ничего еще не выбрано. Надо заставить нашу View реагировать на конечности пользователя. То есть используем метод OnTouch. А чтобы наш класс был вполне себе самостоятельным, изобразим OnTouch непосредственно внутри класса:
// в наш пока скучающий метод init() добавляем
setOnTouchListener(this);
// Эклипс ругается, поддаемся на его уговоры
public class ColorPicker extends View implements OnTouchListener
После чего Эклипс заботливо предлагает добавить метод:
@Override
	public boolean onTouch(View v, MotionEvent event) {
		switch (event.getAction()) {
		// Тут мы определяем, что сделал юзер
		case MotionEvent.ACTION_DOWN:
			// Тут надо будет запомнить координаты - типа на каком кольце начат свайп
		break;
		case MotionEvent.ACTION_MOVE:
			// А тут собственно будем получать значение
		break;
		}
 invalidate();
		return true;
}
Вроде все просто, но тут и вылезли те самые грабли, упомянутые в начале статьи. Эклипс упорно что-то требовал. Как выяснилось ему хотелось дополнить onTouch:
		case MotionEvent.ACTION_UP:
			v.performClick();
			break;
И соответственно метод:
	@Override
	public boolean performClick() {
		return super.performClick();
	}
После чего все успокоились. Теперь мы смело получаем координаты в onTouch:
		case MotionEvent.ACTION_DOWN:
			float a = Math.abs(event.getX() - cx);
			float b = Math.abs(event.getY() - cy);
			break;
		case MotionEvent.ACTION_MOVE:
			float x = event.getX() - cx;
			float y = event.getY() - cy;
			break;
И вперед — вычислять цвета. Вот где простор для критики открывается, ибо не дружу я особо с математикой. Поэтому продолжить предлагаю в следующей статье.
Комментарии (8)
 - andkulikov05.04.2015 12:19+1- у вас небольшая проблема тут есть: 
 не нужно вызвать invalidate() внутри onDraw.
 invalidate это и есть просьба вьюшки заново вызвать onDraw — перерисоваться. и так у вас onDraw может вызываться бесконечно, потому что при каждой отрисовке в конце мы просим все перерисовать опять
 - OJV Автор05.04.2015 14:01+1- Да, проглядел. Потом я её в onTouch перенёс, в продолжении статьи она там. А тут проглядел. Спасибо. 
 
           
 

hardex
В вашем HSV+A куда-то делся V.
dougrinch
Немного не так. Присмотритесь повнимательнее, там весь второй круг — граница плоскости SV.
hardex
Тут сложно сказать, потому что автор рисует круг градиентом всего из нескольких точек, а потом собирается вычислять цвет по углам. Так или иначе, степеней свободы у цвета 4, а кругов — 3.