Как создать рисунок

Попробуйте Compose
Jetpack Compose – это рекомендуемый набор инструментов для создания пользовательского интерфейса в Android. Узнайте, как работать с макетами в Compose.

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

Подробнее об объектах Drawable…

Переопределите onDraw()

Самый важный шаг при рисовании пользовательского представления – переопределить метод onDraw(). Параметр onDraw() – это объект Canvas, который представление может использовать для отрисовки. Класс Canvas определяет методы для рисования текста, линий, битовых карт и многих других графических примитивов. Эти методы можно использовать в onDraw() для создания собственного пользовательского интерфейса.

Сначала создайте объект Paint. В следующем разделе Paint рассматривается более подробно.

Создание объектов рисования

Фреймворк android.graphics делит рисование на две области:

  • Что рисовать – за это отвечает Canvas.
  • Как рисовать, обрабатывается Paint.

Например, Canvas предоставляет метод для рисования линии, а Paint – методы для определения цвета этой линии. В Canvas есть метод для рисования прямоугольника, а Paint определяет, нужно ли залить этот прямоугольник цветом или оставить пустым. Canvas определяет фигуры, которые можно нарисовать на экране, а Paint – цвет, стиль, шрифт и другие параметры каждой фигуры.

Прежде чем что-либо рисовать, создайте один или несколько объектов Paint. В примере ниже это делается в методе init. Этот метод вызывается из конструктора в Java, но в Kotlin его можно инициализировать в строке.

Kotlin

@ColorInt
private var textColor    // Obtained from style attributes.

@Dimension
private var textHeight   // Obtained from style attributes.

private val textPaint = Paint(ANTI_ALIAS_FLAG).apply {
    color = textColor
    if (textHeight == 0f) {
        textHeight = textSize
    } else {
        textSize = textHeight
    }
}

private val piePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
    style = Paint.Style.FILL
    textSize = textHeight
}

private val shadowPaint = Paint(0).apply {
    color = 0x101010
    maskFilter = BlurMaskFilter(8f, BlurMaskFilter.Blur.NORMAL)
}

Java

private Paint textPaint;
private Paint piePaint;
private Paint shadowPaint;

@ColorInt
private int textColor;       // Obtained from style attributes.

@Dimension
private float textHeight;    // Obtained from style attributes.

private void init() {
   textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
   textPaint.setColor(textColor);
   if (textHeight == 0) {
       textHeight = textPaint.getTextSize();
   } else {
       textPaint.setTextSize(textHeight);
   }

   piePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
   piePaint.setStyle(Paint.Style.FILL);
   piePaint.setTextSize(textHeight);

   shadowPaint = new Paint(0);
   shadowPaint.setColor(0xff101010);
   shadowPaint.setMaskFilter(new BlurMaskFilter(8, BlurMaskFilter.Blur.NORMAL));
   ...
}

Заблаговременное создание объектов – важный шаг оптимизации. Представления часто перерисовываются, а многие графические объекты требуют сложной инициализации. Создание объектов рисования в методе onDraw() значительно снижает производительность и может замедлить работу интерфейса.

Обработка событий макета

Чтобы правильно нарисовать пользовательское представление, узнайте, какого оно размера. Сложные пользовательские представления часто требуют выполнения нескольких вычислений макета в зависимости от размера и формы их области на экране. Никогда не делайте предположений о размере представления на экране. Даже если представление используется только в одном приложении, оно должно поддерживать разные размеры экрана, плотность пикселей и соотношение сторон как в портретном, так и в горизонтально режиме.

Хотя в классе View есть много методов для обработки измерений, большинство из них не нужно переопределять. Если вашему представлению не требуется специальный контроль над его размером, переопределите только один метод: onSizeChanged().

onSizeChanged() вызывается, когда представлению впервые назначается размер, а также при изменении размера представления по любой причине. Рассчитывайте позиции, размеры и другие значения, связанные с размером представления, в методе onSizeChanged(), а не пересчитывайте их каждый раз при отрисовке. В следующем примере onSizeChanged() – это место, где представление вычисляет ограничивающий прямоугольник диаграммы и относительное положение текстовой метки и других визуальных элементов.

Когда представлению назначается размер, менеджер макета предполагает, что размер включает отступы представления. При расчете размера представления учитывайте значения отступов. Вот фрагмент кода из onSizeChanged(), который показывает, как это сделать:

Kotlin

private val showText    // Obtained from styled attributes.
private val textWidth   // Obtained from styled attributes.

override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
    super.onSizeChanged(w, h, oldw, oldh)
    // Account for padding.
    var xpad = (paddingLeft + paddingRight).toFloat()
    val ypad = (paddingTop + paddingBottom).toFloat()

    // Account for the label.
    if (showText) xpad += textWidth.toFloat()
    val ww = w.toFloat() - xpad
    val hh = h.toFloat() - ypad

    // Figure out how big you can make the pie.
    val diameter = Math.min(ww, hh)
}

Java

private Boolean showText;    // Obtained from styled attributes.
private int textWidth;       // Obtained from styled attributes.

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    // Account for padding.
    float xpad = (float)(getPaddingLeft() + getPaddingRight());
    float ypad = (float)(getPaddingTop() + getPaddingBottom());

    // Account for the label.
    if (showText) xpad += textWidth;

    float ww = (float)w - xpad;
    float hh = (float)h - ypad;

    // Figure out how big you can make the pie.
    float diameter = Math.min(ww, hh);
}

Если вам нужен более точный контроль над параметрами макета представления, реализуйте onMeasure(). Параметры этого метода – View.MeasureSpec значения, которые сообщают, какой размер родительский элемент хочет задать для вашего представления и является ли этот размер жестким максимумом или просто рекомендацией. Для оптимизации эти значения хранятся в виде упакованных целых чисел. Чтобы распаковать информацию, хранящуюся в каждом целом числе, используйте статические методы View.MeasureSpec.

Ниже приведен пример реализации onMeasure(). В этой реализации он пытается сделать свою область достаточно большой, чтобы диаграмма была такой же большой, как и ее метка:

Kotlin

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    // Try for a width based on your minimum.
    val minw: Int = paddingLeft + paddingRight + suggestedMinimumWidth
    val w: Int = View.resolveSizeAndState(minw, widthMeasureSpec, 1)

    // Whatever the width is, ask for a height that lets the pie get as big as
    // it can.
    val minh: Int = View.MeasureSpec.getSize(w) - textWidth.toInt() + paddingBottom + paddingTop
    val h: Int = View.resolveSizeAndState(minh, heightMeasureSpec, 0)

    setMeasuredDimension(w, h)
}

Java

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   // Try for a width based on your minimum.
   int minw = getPaddingLeft() + getPaddingRight() + getSuggestedMinimumWidth();
   int w = resolveSizeAndState(minw, widthMeasureSpec, 1);

   // Whatever the width is, ask for a height that lets the pie get as big as it
   // can.
   int minh = MeasureSpec.getSize(w) - (int)textWidth + getPaddingBottom() + getPaddingTop();
   int h = resolveSizeAndState(minh, heightMeasureSpec, 0);

   setMeasuredDimension(w, h);
}

В этом коде есть три важных элемента:

  • При расчетах учитываются поля представления. Как упоминалось ранее, это ответственность представления.
  • Для создания окончательных значений ширины и высоты используется вспомогательный метод resolveSizeAndState(). Этот вспомогательный метод возвращает подходящее значение View.MeasureSpec, сравнивая необходимый размер представления со значением, переданным в onMeasure().
  • onMeasure() не имеет возвращаемого значения. Вместо этого метод передает результаты, вызывая setMeasuredDimension(). Вызов этого метода обязателен. Если вы не добавите этот вызов, класс View выдаст исключение времени выполнения.

Рисование

После того как вы определили код создания и измерения объектов, можно реализовать onDraw(). Каждое представление реализует onDraw() по-своему, но есть некоторые общие операции, которые выполняются в большинстве представлений:

  • Нарисуйте текст с помощью drawText(). Укажите гарнитуру с помощью метода setTypeface(), а цвет текста – с помощью метода setColor().
  • Рисуйте простые фигуры с помощью drawRect(), drawOval() и drawArc(). Чтобы изменить тип заливки, контура или того и другого, вызовите метод setStyle().
  • Для рисования более сложных фигур используйте класс Path. Определите фигуру, добавив линии и кривые в объект Path, а затем нарисуйте ее с помощью drawPath(). Как и в случае с простыми фигурами, контуры можно обводить, заливать или делать и то, и другое в зависимости от setStyle().
  • Градиентные заливки определяются путем создания объектов LinearGradient. Вызовите setShader(), чтобы использовать LinearGradient для закрашенных фигур.
  • Нарисуйте битовые карты с помощью drawBitmap().

В следующем примере кода показано, как нарисовать текст, линии и фигуры:

Kotlin

private val data = mutableListOf<Item>() // A list of items that are displayed.

private var shadowBounds = RectF()       // Calculated in onSizeChanged.
private var pointerRadius: Float = 2f    // Obtained from styled attributes.
private var pointerX: Float = 0f         // Calculated in onSizeChanged.
private var pointerY: Float = 0f         // Calculated in onSizeChanged.
private var textX: Float = 0f            // Calculated in onSizeChanged.
private var textY: Float = 0f            // Calculated in onSizeChanged.
private var bounds = RectF()             // Calculated in onSizeChanged.
private var currentItem: Int = 0         // The index of the currently selected item.

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

    canvas.apply {
        // Draw the shadow.
        drawOval(shadowBounds, shadowPaint)

        // Draw the label text.
        drawText(data[currentItem].label, textX, textY, textPaint)

        // Draw the pie slices.
        data.forEach {item ->
            piePaint.shader = item.shader
            drawArc(
                bounds,
                360 - item.endAngle,
                item.endAngle - item.startAngle,
                true,
                piePaint
            )
        }

        // Draw the pointer.
        drawLine(textX, pointerY, pointerX, pointerY, textPaint)
        drawCircle(pointerX, pointerY, pointerRadius, textPaint)
    }
}

// Maintains the state for a data item.
private data class Item(
    var label: String,      
    var value: Float = 0f,

    @ColorInt
    var color: Int = 0,

    // Computed values.
    var startAngle: Float = 0f,
    var endAngle: Float = 0f,

    var shader: Shader
)

Java

private List<Item> data = new ArrayList<Item>();  // A list of items that are displayed.

private RectF shadowBounds;                       // Calculated in onSizeChanged.
private float pointerRadius;                      // Obtained from styled attributes.
private float pointerX;                           // Calculated in onSizeChanged.
private float pointerY;                           // Calculated in onSizeChanged.
private float textX;                              // Calculated in onSizeChanged.
private float textY;                              // Calculated in onSizeChanged.
private RectF bounds;                             // Calculated in onSizeChanged.
private int currentItem = 0;                      // The index of the currently selected item.

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    // Draw the shadow.
    canvas.drawOval(
            shadowBounds,
            shadowPaint
    );

    // Draw the label text.
    canvas.drawText(data.get(currentItem).label, textX, textY, textPaint);

    // Draw the pie slices.
    for (int i = 0; i < data.size(); ++i) {
        Item it = data.get(i);
        piePaint.setShader(it.shader);
        canvas.drawArc(
                bounds,
                360 - it.endAngle,
                it.endAngle - it.startAngle,
                true, 
                piePaint
        );
    }

    // Draw the pointer.
    canvas.drawLine(textX, pointerY, pointerX, pointerY, textPaint);
    canvas.drawCircle(pointerX, pointerY, pointerRadius, textPaint);
}

// Maintains the state for a data item.
private class Item {
    public String label;
    public float value;
    @ColorInt
    public int color;

    // Computed values.
    public int startAngle;
    public int endAngle;

    public Shader shader;
}    

Применение графических эффектов

В Android 12 (уровень API 31) добавлен класс RenderEffect, который позволяет применять к объектам View и иерархиям рендеринга распространенные графические эффекты, такие как размытие, цветовые фильтры, эффекты шейдеров Android и т. д. Вы можете комбинировать эффекты, создавая цепочки из внутренних и внешних эффектов, или смешивать их. Поддержка этой функции зависит от вычислительной мощности устройства.

Вы также можете применить эффекты к базовому объекту RenderNode для View, вызвав View.setRenderEffect(RenderEffect).

Чтобы реализовать объект RenderEffect, выполните следующие действия:

view.setRenderEffect(RenderEffect.createBlurEffect(radiusX, radiusY, SHADER_TILE_MODE))

Вы можете создать представление программно или извлечь его из макета XML и получить с помощью привязки представлений или findViewById().