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