Наиболее важная часть пользовательского представления — это его внешний вид. Создание пользовательского представления может быть простым или сложным в зависимости от потребностей вашего приложения. В этом документе описаны некоторые из наиболее распространенных операций.
Для получения более подробной информации см. Обзор 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) }
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 имеет множество методов для обработки измерений, большинство из них не требуют переопределения. Если вашему 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) }
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() . В этой реализации он пытается увеличить свою область настолько, чтобы диаграмма по размеру соответствовала метке:
Котлин
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); }
В этом коде следует отметить три важных момента:
- При расчетах учитывается отступ (padding) элемента представления. Как уже упоминалось, это ответственность самого представления.
- Вспомогательный метод
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 )
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() .
