Создайте собственный рисунок

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

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

Дополнительные сведения см. в разделе Обзор Drawables .

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

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

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

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

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

  • Что рисовать, решает Canvas .
  • Как рисовать, занимается Paint .

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

Прежде чем что-либо рисовать, создайте один или несколько объектов Paint . В следующем примере это делается с помощью метода init . Этот метод вызывается из конструктора Java, но в 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)
}

Ява

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() , показывающий, как это сделать:

Котлин

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)
}

Ява

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() . В этой реализации он пытается сделать свою область достаточно большой, чтобы диаграмма была такой же большой, как и ее метка:

Котлин

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)
}

Ява

@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);
}

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

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

Рисовать

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

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

Следующий код рисует смесь текста, линий и фигур:

Котлин

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
)

Ява

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 , который применяет общие графические эффекты, такие как размытие, цветовые фильтры, эффекты шейдеров Android и многое другое, для View объектов и иерархий рендеринга. Эффекты можно комбинировать в виде цепочки эффектов, состоящей из внутреннего и внешнего эффекта, или в виде смешанных эффектов. Поддержка этой функции зависит от вычислительной мощности устройства.

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

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

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

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