Custom View в Android — суть, рисование и обработка событий

Автор: IT Sectr Опубликовано: 2026-07-20 Время чтения: 9 мин

Custom View — это пользовательский компонент интерфейса в Android, создаваемый путём наследования от класса View или его подклассов (Button, TextView, ImageView) и переопределения ключевых методов: onDraw для рисования, onMeasure для измерения размеров и onTouchEvent для обработки касаний. По данным Google Android Developer Guide (2024), Custom View используется, когда стандартные компоненты Android SDK не обеспечивают требуемое поведение или внешний вид — например, для кастомной анимации, нестандартных форм, специализированных графиков и игровых элементов.

Главное

  • Custom View — создаётся наследованием от View или его подклассов с переопределением onDraw, onMeasure, onTouchEvent.
  • Рисование выполняется через Canvas API — геометрические фигуры, текст, пути, Bitmap и анимация.
  • onMeasure отвечает за корректные размеры View с учётом layout-параметров и padding.
  • Кастомные XML-атрибуты объявляются в res/values/attrs.xml и читаются в конструкторе View.
  • Обработка касаний onTouchEvent позволяет добавить поддержку жестов, drag-and-drop и мультитач.

Что такое Custom View в Android?

Custom View — это класс, наследующий android.view.View (или один из его подклассов), который переопределяет системные методы для реализации собственной логики рисования, измерения и обработки событий. Custom View — это фундаментальный механизм Android для создания уникальных UI-компонентов.

Android предоставляет два подхода к созданию Custom View: наследование от View (полностью кастомная отрисовка через onDraw) и наследование от существующего View-подкласса (например, переопределение Button или TextView для добавления функциональности при сохранении базового поведения).

Базовый конструктор Custom View должен принимать Context и AttributeSet — это позволяет системе создавать View из XML-разметки. Если View будет использоваться только из кода, достаточно конструктора с Context. Третий конструктор со стилем нужен для поддержки тем Android.

kotlin
class CustomView
    @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

    private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = Color.BLUE
        strokeWidth = 4f
        style = Paint.Style.FILL
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        canvas.drawCircle(width / 2f,
            height / 2f,
            50f, paint)
    }
}

Минимальный Custom View состоит из конструктора и onDraw. Однако для корректной работы в Layout-системе Android необходимо также переопределить onMeasure — иначе View может отображаться с нулевыми размерами или неправильно реагировать на layout-параметры wrap_content.

По данным Google I/O 2023, кастомные View используются в 65% приложений из топ-100 Google Play. Наиболее частые причины создания Custom View: нестандартные анимации (35%), кастомные графики и диаграммы (25%), специализированные элементы управления (20%) и брендированные компоненты (20%).

onMeasure и onLayout: управление размерами

onMeasure — это метод, в котором View сообщает системе свои желаемые размеры на основе переданных MeasureSpec (родительские ограничения). Без правильной реализации onMeasure View может иметь нулевую высоту при wrap_content или занимать всю площадь экрана при match_parent.

MeasureSpec состоит из режима (EXACTLY, AT_MOST, UNSPECIFIED) и значения. EXACTLY — родитель задал точный размер (match_parent или фиксированное значение). AT_MOST — родитель задал максимум (wrap_content). UNSPECIFIED — без ограничений (ScrollView, ListView).

kotlin
override fun onMeasure(
    widthMeasureSpec: Int,
    heightMeasureSpec: Int
) {
    val desiredWidth = paddingLeft + paddingRight
        + DEFAULT_WIDTH
    val desiredHeight = paddingTop + paddingBottom
        + DEFAULT_HEIGHT

    val measuredWidth = MeasureSpec.getSize(widthMeasureSpec)
    val modeWidth = MeasureSpec.getMode(widthMeasureSpec)

    val resultWidth = when (modeWidth) {
        MeasureSpec.EXACTLY -> measuredWidth
        MeasureSpec.AT_MOST ->
            desiredWidth.coerceAtMost(measuredWidth)
        else -> desiredWidth
    }

    val resultHeight = resolveSize(desiredHeight,
        heightMeasureSpec)

    setMeasuredDimension(resultWidth, resultHeight)
}

resolveSize — это утилитарный метод Android, который упрощает реализацию onMeasure. Он принимает желаемый размер и MeasureSpec и возвращает корректное значение: для EXACTLY — точный размер, для AT_MOST — минимум из желаемого и максимального, для UNSPECIFIED — желаемый размер.

onLayout для View (не ViewGroup) обычно не переопределяется — он вызывается родителем для позиционирования дочерних элементов. Для ViewGroup onLayout обязателен — в нём нужно вызвать layout() для каждого дочернего View.

onDraw и Canvas API: рисование

onDraw — сердце Custom View. Этот метод вызывается Android при каждой перерисовке View. В него передаётся объект Canvas, через который выполняется всё рисование: геометрические фигуры, текст, пути, Bitmap и анимации.

Canvas API предоставляет методы для всех базовых операций: drawCircle, drawRect, drawLine, drawPath, drawText, drawBitmap, drawArc, drawOval. Для настройки стиля используется Paint — объект, определяющий цвет, толщину, стиль заливки, шрифт, тени и эффекты.

kotlin
override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)

    // Background
    canvas.drawRect(0f, 0f,
        width.toFloat(), height.toFloat(), bgPaint)

    // Text
    canvas.drawText("Custom View",
        paddingLeft.toFloat(),
        paddingTop.toFloat() + textPaint.textSize,
        textPaint)

    // Circle
    canvas.drawCircle(
        width / 2f,
        height / 2f,
        radius, circlePaint)

    // Path (Bezier curve)
    val path = Path().apply {
        moveTo(0f, height.toFloat())
        quadTo(width / 2f, 0f,
            width.toFloat(), height.toFloat())
    }
    canvas.drawPath(path, pathPaint)
}

Paint — это конфигуратор стиля рисования. Антив aliasing (Paint.ANTI_ALIAS_FLAG) обязателен для гладких краёв. Subpixel text (SUBPIXEL_TEXT_FLAG) улучшает качество текста. Style.FILL, STROKE и FILL_AND_STROKE определяют, будет ли фигура закрашена, обведена контуром или и то и другое.

Важно: не создавайте объекты Paint, Path, Rect в onDraw — это приводит к аллокациям в цикле отрисовки и вызывает сборку мусора, что снижает FPS. Создавайте все объекты рисования в конструкторе или в методе init. Для анимации используйте ValueAnimator или ObjectAnimator с invalidate() для запуска перерисовки.

Кастомные XML-атрибуты в attrs.xml

Кастомные атрибуты позволяют настраивать Custom View из XML-разметки так же, как стандартные android:layout_width или android:background. Атрибуты объявляются в файле res/values/attrs.xml с указанием имени, типа и опционального значения по умолчанию.

Типы атрибутов: string, integer, float, boolean, color, dimension, enum, flag, fraction, reference (ссылка на ресурс). Для каждого типа Android автоматически парсит значение из XML и передаёт его в TypedArray.

xml
<!-- res/values/attrs.xml -->
<resources>
    <declare-styleable name="CustomView">
        <attr name="circleColor"
            format="color" />
        <attr name="circleRadius"
            format="dimension" />
        <attr name="labelText"
            format="string" />
        <attr name="showAnimation"
            format="boolean" />
    </declare-styleable>
</resources>

В конструкторе Custom View атрибуты читаются через context.obtainStyledAttributes, который возвращает TypedArray. TypedArray предоставляет типизированные методы доступа: getColor, getDimension, getString, getBoolean, getInt. После чтения TypedArray обязательно нужно вызвать recycle() для освобождения ресурсов.

kotlin
init {
    val typedArray = context.obtainStyledAttributes(
        attrs,
        R.styleable.CustomView
    )

    circleColor = typedArray.getColor(
        R.styleable.CustomView_circleColor,
        Color.BLUE
    )
    circleRadius = typedArray.getDimension(
        R.styleable.CustomView_circleRadius,
        50f
    )
    labelText = typedArray.getString(
        R.styleable.CustomView_labelText
    ) ?: "Default"

    typedArray.recycle()
}

Использование в XML: добавьте пространство имён app (xmlns:app="http://schemas.android.com/apk/res-auto") и используйте кастомные атрибуты как app:circleColor="@color/red". Android Studio будет autocomplete и проверять типы атрибутов, если объявление в attrs.xml корректно.

Обработка касаний с onTouchEvent

onTouchEvent — метод, вызываемый при каждом касании View. Он получает объект MotionEvent с информацией о типе события (ACTION_DOWN, ACTION_MOVE, ACTION_UP, ACTION_CANCEL), координатах, давлении и количестве пальцев (мультитач).

Для обработки сложных жестов (свайп, масштабирование, долгое нажатие) используйте GestureDetector или ScaleGestureDetector в комбинации с onTouchEvent. GestureDetector упрощает распознавание onSingleTapUp, onFling, onLongPress, onDoubleTap и других стандартных жестов.

kotlin
private val gestureDetector = GestureDetector(
    context, object : GestureDetector.SimpleOnGestureListener() {
    override fun onSingleTapUp(e: MotionEvent): Boolean {
        handleTap(e.x, e.y)
        return true
    }
})

override fun onTouchEvent(event: MotionEvent): Boolean {
    val handled = gestureDetector.onTouchEvent(event)

    when (event.action) {
        MotionEvent.ACTION_MOVE -> {
            currentX = event.x
            currentY = event.y
            invalidate()
            return true
        }
    }

    return handled || super.onTouchEvent(event)
}

Хит-тестинг — определение, попал ли touch в определённую область View. Для прямоугольных областей используйте Rect.contains(x, y). Для кругов — проверка расстояния от центра: sqrt(dx^2 + dy^2) < radius. Для произвольных фигур — Path.op() или Region.contains().

Мультитач обрабатывается через MotionEvent.getPointerCount() и getPointerId(index). Каждый палец получает уникальный ID, который сохраняется от ACTION_DOWN до ACTION_POINTER_UP. ACTION_MOVE может содержать данные по всем активным пальцам — используйте getHistoricalX/Y для интерполяции движения.

Оптимизация производительности Custom View

Custom View может стать узким местом производительности, если onDraw вызывается часто (анимация, скролл) или содержит тяжёлые операции. Android предоставляет несколько механизмов для оптимизации отрисовки кастомных компонентов.

Layer type для сложной отрисовки

View.setLayerType позволяет переключить рендеринг View на программный (LAYER_TYPE_SOFTWARE) или аппаратный (LAYER_TYPE_HARDWARE) слой. Software layer полезен для сложной Canvas-графики, которая не поддерживается аппаратным ускорением — например, drawTextOnPath или сложные Path-эффекты.

invalidate только нужной области

invalidate(Rect) — перерисовывает только указанную область View, а не весь компонент. Это критически важно для больших Custom View (графики, карты, канвасы для рисования), где полная перерисовка каждого кадра вызывает падение FPS. Используйте postInvalidateOnAnimation() для синхронизации с vsync.

kotlin
class EfficientCustomView
    @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null
) : View(context, attrs) {

    // Objects created once in constructor
    private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
    private val rect = Rect()
    private val position = PointF()

    fun updatePosition(x: Float, y: Float) {
        position.set(x, y)
        // Redraw only area around the point
        rect.set(
            (x - 10).toInt(),
            (y - 10).toInt(),
            (x + 10).toInt(),
            (y + 10).toInt()
        )
        invalidate(rect)
    }
}

Аппаратное ускорение включено по умолчанию на Android 3.0+ (API 11+). Canvas операции drawCircle, drawRect, drawBitmap аппаратно ускоряются и выполняются на GPU. Однако drawTextOnPath, drawVertices, сложные clipping-операции не ускоряются — для них используйте LAYER_TYPE_SOFTWARE.

По данным Android Performance Patterns (Google, 2023), основные причины лагов Custom View — аллокации в onDraw (создание объектов на каждый кадр), перерисовка всего View при изменении одного элемента и отсутствие LayerType.HARDWARE для статического контента. Исправление этих трёх проблем даёт прирост FPS с 30 до 60 в большинстве сценариев.

МетодНазначениеЧастота вызова
onDrawРисование содержимого ViewПри каждом invalidate()
onMeasureОпределение размеров ViewПри изменении layout
onLayoutПозиционирование дочерних ViewПосле onMeasure
onTouchEventОбработка касанийПри каждом касании
onSizeChangedРеакция на изменение размераПри первом layout и изменении

Часто задаваемые вопросы

Когда использовать Custom View вместо Compose?

Custom View (Canvas API) оправдан, когда нужна максимальная производительность кастомной отрисовки (графики, видеоредакторы, карты), интеграция с существующим View-based кодом или поддержка Android версий ниже API 21. Jetpack Compose — современный подход для большинства новых проектов, использующий декларативный UI и лучше подходящий для динамических интерфейсов.

Обязательно ли переопределять onMeasure?

Да, если View использует wrap_content в XML-разметке. Без onMeasure wrap_content будет работать как match_parent, так как стандартная реализация View.onMeasure не задаёт дефолтный размер. Если View всегда имеет фиксированный размер или match_parent, onMeasure можно не переопределять.

Как анимировать Custom View?

Используйте ValueAnimator или ObjectAnimator для изменения свойств View (цвет, радиус, позиция) и вызывайте invalidate() в колбэке анимации для перерисовки. ValueAnimator работает на главном потоке и синхронизирован с vsync. Для сложных анимаций (физика, частицы) используйте Choreographer.FrameCallback или Android Animation Framework.

Как обработать мультитач в Custom View?

В onTouchEvent используйте MotionEvent.getPointerCount() для определения количества пальцев и getPointerId(i) для отслеживания каждого пальца по уникальному ID. ACTION_POINTER_DOWN / ACTION_POINTER_UP — события добавления/удаления пальца. Для масштабирования используйте ScaleGestureDetector в паре с onTouchEvent — он упрощает детекцию pinch-to-zoom.

Можно ли использовать Custom View в Jetpack Compose?

Да, через AndroidView — composable-функцию, которая внедряет View-based элемент в Compose-иерархию. AndroidView принимает фабрику для создания View и колбэк update для синхронизации состояния. Это позволяет переиспользовать существующие Custom View в новых Compose-проектах без полного переписывания.

Итоги

  • Custom View — пользовательский компонент Android, создаваемый наследованием от View с переопределением onDraw, onMeasure и onTouchEvent для кастомного рисования и обработки.
  • onDraw и Canvas API — основа рисования: геометрические фигуры, текст, Bitmap, пути — с конфигурацией через Paint для цвета, стиля и шрифта.
  • onMeasure важен для корректной работы wrap_content; использует MeasureSpec (EXACTLY, AT_MOST, UNSPECIFIED) и resolveSize для вычисления размеров.
  • Кастомные атрибуты через attrs.xml и TypedArray позволяют настраивать Custom View из XML-разметки с поддержкой autocomplete в Android Studio.
  • onTouchEvent с GestureDetector обеспечивает обработку касаний, жестов и мультитач, а хит-тестинг определяет область взаимодействия.
  • Производительность: создавайте объекты один раз в конструкторе, используйте invalidate(Rect) для частичной перерисовки и LayerType для сложных эффектов.
  • Custom View интегрируется с Jetpack Compose через AndroidView, что позволяет переиспользовать готовые компоненты в новых Compose-проектах.

Мы разработаем мобильное приложение под ключ

IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.

Обсудить проект

Читайте также