На вкус и цвет или Раскраска для Андроид
Снова доброго времени суток всем. В этой статье хотелось бы продолжить тему кастомных View, а именно диалога выбора цвета.Небольшое отступление: Предвосхищая негативные отзывы, хочу заранее предупредить — статья рассчитана на новичков в программировании. Начиная знакомиться с разработкой под Андроид, я столкнулся (да и сейчас регулярно сталкиваюсь) с интересным фактом: в интернете воз и маленькая тележка информации посвящена установке и настройке Эклипса, потом красочно описывают, как нажать 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.drawColor (Color.BLUE); invalidate (); } } Теперь наш класс обязан появиться в разделе Custom & Library Views палитры элементов. Тащим его на разметку нашего ХеллоВорда.
Должно получиться что-то вроде этого:
Получилось? Замечательно выходит.
Переходим к рисованию. Давайте начнем снаружи. Нам необходимо кольцо с градиентом цветов. Кольцо, кто не в курсе, рисуется так:
@Override protected void onDraw (Canvas c) { super.onDraw©; c.drawCircle (cx, cy, rad_1, p_color); invalidate (); } То есть нам необходим радиус окружности и кисть (или краска, как там 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; } 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; И вперед — вычислять цвета. Вот где простор для критики открывается, ибо не дружу я особо с математикой. Поэтому продолжить предлагаю в следующей статье.