Практический пример создания собственного View-компонента

https://proandroiddev.com/building-a-custom-view-a-practical-example-2753cb9d0e80
  • Перевод

Практический пример создания собственного View-компонента


Мне нравится 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)
}

И объединение слоёв даст нам желаемый результат:


Результат


Итог


  1. Не бойтесь создавать собственные View-компоненты (при необходимости).
  2. Изучите основные API Canvas и Paint.
  3. Разбивайте ваш дизайн на маленькие слои и рисуйте каждый независимо.

Что касается последнего пункта, для меня это один из лучших уроков программирования в целом: когда сталкиваетесь с большой и сложной задачей, разбейте её на более мелкие, более простые задачи.

Поделиться публикацией

Комментарии 4

    0
    Неплохая статья, спасибо!
    Однако немного не хватило информации про скроллы, транзишены, паддинги и тому подобные извращения
      0

      Редкий пример на Dribble ограничивается статическим макетом. В придачу идет куча анимаций. Например при смене набор данных, при смене периода просмотра. Ни слова про масштабирование данных. Как быть, когда в наборе присутствие и большие и малые величины.

        0

        И тут на помощь приходит flutter, с его виджетами, деревом и удобнейшей анимацией, не говоря о flex штуках типа row, column и так далее

          0

          И тут на помощь приходит flutter, с его виджетами, деревом и удобнейший анимацией, не говоря о flex штуках типа row, column и так далее

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое