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