
Мне нравится Dribbble. Там есть много крутых и вдохновляющих дизайн-проектов. Но если вы разработчик, то часто чувство прекрасного быстро сменяется на отчаяние, когда вы начинаете думать о том, как реализовать этот крутой дизайн.
В этой статье я покажу вам пример такого дизайна и его реализацию, но перед этим давайте поговорим о решении проблемы в целом.
Самый простой способ — использовать какую-то библиотеку, закрывающую наши потребности. Теперь не поймите меня неправильно, я большой сторонник подхода «не изобретать велосипед». Есть отличные библиотеки с открытым исходным кодом, и когда мне будет нужно загружать изображения или реализовывать REST API, Glide/Picasso и Retrofit очень здорово помогут мне.
Но когда вам нужно реализовать какой-то необычный дизайн, это не всегда лучший выбор. Вам нужно будет потратить время на поиск хорошей, поддерживаемой библиотеки, которая будет делать что-то подобное. Затем вам нужно заглянуть в код, чтобы убедиться, что там написано что-то адекватное. Вам нужно будет уделить больше времени пониманию настроек и конфигураций, которыми вы сможете управлять для использования библиотеки �� ваших задачах. И давайте будем честными, скорее всего, библиотека не покроет ваших нужд на 100%, и вам нужно будет пойти на некоторые компромиссы с дизайнерами.
Поэтому я говорю о том, что зачастую проще и лучше создать свой собственный View-компонент. Когда я говорю «собственный View-компонент», я имею в виду расширение класса View, переопределение метода onDraw() и использование Paint и Canvas для рисования View-компонента. Это может показаться страшным, если вы не делали этого раньше, потому что у этих классов есть много методов и свойств, но вы можете сосредоточиться на основных:
canvas.drawRect()— укажите координаты углов и нарисуете прямоугольник;
canvas.drawRoundRect()— дополнительно укажите радиус, и углы прямоугольника будут закруглены;
canvas.drawPath()— это более сложный, но и более мощный способ создания собственной фигуры с помощью линий и кривых;
canvas.drawText()— для рисования текста на канвасе (с помощьюPaintвы сможете контролировать размер, цвет и другие свойства);
canvas.drawCircle()— укажите центральную точку и радиус и получится круг;
canvas.drawArc()— укажите ограничивающий прямоугольник, а также начальный и поворотный углы для рисования дуги;
paint.style— указывает, будет ли нарисованная фигура заполнена, обведена или и то, и другое;
paint.color— указывает цвет (включая прозрачность);
paint.strokeWidth— управляет шириной для обводки фигур;
paint.pathEffect— позволяет влиять на геометрию рисуемой фигуры;
paint.shader— позволяет рисовать градиенты.
Помните, иногда вам может понадобиться использовать другие API, но даже овладев этими методами, вы сможете рисовать очень сложные фигуры.
Практический пример
Вот такой дизайн предлагает нам Pepper:

Здесь много чего интересного, но давайте разберём всё на мелкие кусочки.
Шаг 1. Рассчитать позиции маркеров
private fun calcPositions(markers: List<Marker>) { val max = markers.maxBy { it.value } val min = markers.minBy { it.value } pxPerUnit = chartHeight / (max - min) zeroY = max * pxPerUnit + paddingTop val step = (width - 2 * padding - scalesWidth) / (markers.size - 1) for ((i, marker) in markers.withIndex()) { val x = step * i + paddingLeft val y = zeroY - entry.value * pxPerUnit marker.currentPos.x = x marker.currentPos.y = y } }
Мы находим минимальное и максимальное значения, вычисляем соотношение пикселей на единицу, размер шага по горизонтали между маркерами и позиции X и Y.
Шаг 2. Нарисовать градиент

// prepare the gradient paint val colors = intArrayOf(colorStart, colorEnd)) val gradient = LinearGradient( 0f, paddingTop, 0f, zeroY, colors, null, CLAMP ) gradientPaint.style = FILL gradientPaint.shader = gradient private fun drawGradient(canvas: Canvas) { path.reset() path.moveTo(paddingLeft, zeroY) for (marker in markers) { path.lineTo(marker.targetPos.x, entry.targetPos.y) } // close the path path.lineTo(markers.last().targetPos.x, zeroY) path.lineTo(paddingLeft, zeroY) canvas.drawPath(path, gradientPaint) }
Мы создаем фигуру, начиная с левого края, проводя линию между каждым маркером и завершая фигуру в начальной точке. Затем рисуем эту фигуру, используя краску с градиентным шейдером.
Шаг 3. Нарисовать сетку

// prepare the guideline paint dottedPaint.style = STROKE dottedPaint.strokeWidth = DOTTED_STROKE_WIDTH_DP dottedPaint.pathEffect = DashPathEffect(floatArrayOf(INTERVAL, INTERVAL), 0f) private fun drawGuidelines(canvas: Canvas) { val first = findFirstDayOfWeekInMonth(markers) for (i in first..markers.lastIndex step 7) { val marker = markers[i] guidelinePath.reset() guidelinePath.moveTo(entry.currentPos.x, paddingTop) guidelinePath.lineTo(entry.currentPos.x, zeroY) canvas.drawPath(guidelinePath, dottedPaint) } }
Мы настраиваем краску, чтобы она рисовала пунктиром. Затем мы используем специальный цикл языка Kotlin, который позволяет нам перебирать маркеры с шагом 7 (количество дней в неделе). Для каждого маркера мы берём координату X и рисуем вертикальную пунктирную линию от вершины графика до zeroY.
Шаг 4. Нарисовать график и маркеры

private fun drawLineAndMarkers(canvas: Canvas) { var previousMarker: Marker? = null for (marker in markers) { if (previousMarker != null) { // draw the line val p1 = previousMarker.currentPos val p2 = marker.currentPos canvas.drawLine(p1.x, p1.y, p2.x, p2.y, strokePaint) } previousMarker = marker // draw the marker canvas.drawCircle( marker.currentPos.x, marker.currentPos.y, pointRadius, pointPaint ) } }
Мы перебираем маркеры, рисуем для каждого из них закрашенный круг и простую линию от предыдущего маркера до текущего.
Шаг 5. Нарисовать кнопки недель

private fun drawWeeks(canvas: Canvas) { for ((i, week) in weeks.withIndex()) { textPaint.getTextBounds(week, 0, week.length, rect) val x = middle(i) val y = zeroY + rect.height() val halfWidth = rect.width() / 2f val halfHeight = rect.height() / 2f val left = x - halfWidth - padding val top = y - halfHeight - padding val right = x + halfWidth + padding val bottom = y + halfHeight + padding rect.set(left, top, right, bottom) paint.color = bgColor paint.style = FILL canvas.drawRoundRect(rect, radius, radius, paint) paint.color = strokeColor paint.style = STROKE canvas.drawRoundRect(rect, radius, radius, paint) canvas.drawText(week, x, keyY, textPaint) } }
Мы перебираем метки недель, находим координату X середины недели и начинаем рисовать кнопку по слоям: сначала рисуем фон с закругленными углами, затем границу и, наконец, текст. Мы настраиваем краску перед рисованием каждого слоя.
Шаг 6. Нарисовать числовые маркеры справа

private fun drawGraduations(canvas: Canvas) { val x = markers.last().currentPos.x + padding for (value in graduations) { val y = zeroY - scale * pxPerUnit val formatted = NumberFormat.getIntegerInstance().format(value) canvas.drawText(formatted, x, y, textPaint) } }
Координата X — это позиция последнего маркера плюс некоторый отступ. Координата Y рассчитывается с использованием соотношения пикселей на единицу. Мы форматируем число в строку (при необходимости добавляем разделитель тысяч) и рисуем текст.
Вот и всё, теперь наш onDraw() будет выглядеть так:
override fun onDraw(canvas: Canvas) { drawGradient(canvas) drawGuidelines(canvas) drawLineAndMarkers(canvas) drawWeeks(canvas) drawGraduations(canvas) }
И объединение слоёв даст нам желаемый результат:

Итог
- Не бойтесь создавать собственные
View-компоненты (при необходимости). - Изучите основные API
CanvasиPaint. - Разбивайте ваш дизайн на маленькие слои и рисуйте каждый независимо.
Что касается последнего пункта, для меня это один из лучших уроков программирования в целом: когда сталкиваетесь с большой и сложной задачей, разбейте её на более мелкие, более простые задачи.
