Мне нравится 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
. - Разбивайте ваш дизайн на маленькие слои и рисуйте каждый независимо.
Что касается последнего пункта, для меня это один из лучших уроков программирования в целом: когда сталкиваетесь с большой и сложной задачей, разбейте её на более мелкие, более простые задачи.
Автор: Devcolibri