Освой Android играючи

Сайт Александра Климова

Шкодим

/* Моя кошка замечательно разбирается в программировании. Стоит мне объяснить проблему ей - и все становится ясно. */
John Robbins, Debugging Applications, Microsoft Press, 2000

Класс Canvas

Центрируем текст

Класс android.graphics.Canvas (Холст) предоставляет методы для рисования, которые отображают графические примитивы на исходном растровом изображении. При этом надо сначала подготовить кисть (класс Paint), который позволяет указывать, как именно графические примитивы должны отображаться на растровом изображении (цвет, обводка, стиль, сглаживание шрифта и т.д.).

Android поддерживает полупрозрачность, градиентные заливки, округлённые прямоугольники и сглаживание. Из-за ограниченных ресурсов векторная графика пока что не поддерживается, вместо этого используется традиционная растровая перерисовка.

Canvas работает с пикселями, поэтому следует заботиться о конвертации единиц dp в px и наоборот при необходимости. Начало координат находится в левом верхнем углу.

Получить доступ к холсту можно через объект Bitmap или компонент View. Очень часто разработчики создают свой собственный компонент, наследуясь от View, и рисуют на его холсте для реализации своих замыслов.

Методы

Ниже представлены некоторые методы класса Canvas, которые что-то рисуют.

  • drawARGB()/drawRGB()/drawColor(). Заполняет холст сплошным цветом.
  • drawArc(). Рисует дугу между двумя углами внутри заданной прямоугольной области.
  • drawBitmap(). Рисует растровое изображение на холсте. Вы можете изменять внешний вид целевой картинки, указывая итоговый размер или используя матрицу для преобразования.
  • drawBitmapMesh(). Рисует изображение с использованием сетки, с помощью которой можно управлять отображением итоговой картинки, перемещая точки внутри неё.
  • drawCircle(). Рисует круг/окружность с определённым радиусом вокруг заданной точки.
  • drawLine(s)(). Рисует линию (или последовательность линий) между двумя точками.
  • drawOval(). Рисует овал на основе прямоугольной области.
  • drawPaint(). Закрашивает весь холст с помощью заданного объекта Paint.
  • drawPath(). Рисует указанный контур, используется для хранения набора графических примитивов в виде единого объекта.
  • drawPicture(). Рисует объект Picture внутри заданного прямоугольника.
  • drawPoint(). Рисует точку в заданном месте.
  • drawPosText(). Рисует текстовую строку, учитывая смещение для каждого символа.
  • drawRect(). Рисует прямоугольник.
  • drawRoundRect(). Рисует прямоугольник с закруглёнными углами.
  • drawText(). Рисует текстовую строку на холсте. Шрифт, размер, цвет и свойства отображения текста задаются в соответствующем объекте Paint.
  • drawTextOnPath(). Рисует текст, который отображается вокруг определённого контура.
  • drawVertices(). Рисует набор треугольников в виде совокупности вершинных (вертексных) точек.
  • rotate() и restore(). Вращение холста
  • Методы scale() и translate(). Изменение и перемещение координатной системы

Мы уже изучали основы рисования в первом месяце обучения (Работаем с графикой. Основы). Можно вернуться к этому проекту, закомментировать код вывода графики и продолжить изучение рисования при помощи методов класса Canvas.

Метод drawArc()


public void drawArc(RectF oval, float startAngle, float sweepAngle, boolean useCenter, Paint paint)

public void drawArc(float left, float top, float right, float bottom, 
    float startAngle, float sweepAngle, boolean useCenter, Paint paint)

В API 21 появилась перегруженная версия метода, в котором можно указать координаты двух точек вместо RectF.

Метод drawArc() позволяет рисовать дуги и сектора. Ниже приводится код для трёх вариантов: сектор с заливкой (похож на PacMan), сектор без заливки (контур) и часть дуги:


float width = 400f;
float height = 240f;
float radius = 100f;

Path path = new Path();
path.addCircle(width, height, radius, Path.Direction.CW);
Paint paint = new Paint();
paint.setColor(Color.WHITE); // установим белый цвет
paint.setStrokeWidth(5);
paint.setStyle(Paint.Style.FILL); // заливаем
paint.setAntiAlias(true);

float center_x, center_y;
center_x = 240;
center_y = 220;

final RectF oval = new RectF();
oval.set(center_x - radius, center_y - radius, center_x + radius,
		center_y + radius);
canvas.drawArc(oval, 45, 270, true, paint); // рисуем пакмана

// рисуем большого пакмана без заливки
paint.setStyle(Paint.Style.STROKE);
oval.set(center_x - 200f, center_y - 200f, center_x + 200f,
		center_y + 200f);
canvas.drawArc(oval, 45, 270, true, paint);
		
paint.setStyle(Paint.Style.STROKE);

// рисуем разорванное кольцо
center_y = 540;
oval.set(center_x - radius,
center_y - radius,
center_x + radius,
center_y + radius);
canvas.drawArc(oval, 135, 270, false, paint);

drawArc

Метод drawBitmap()

Вывести готовое изображение просто.


canvas.drawBitmap(mBitmap, 450, 530, mPaint);

Метод drawCircle()

Первые два аргумента определяют координаты центра окружности/круга, следующий аргумент — её радиус в пикселах, последний — объект Paint. В зависимости от выбранного стиля кисти можно нарисовать закрашенный круг или только окружность.

Нарисуем зелёный круг.


float w, h, cx, cy, radius;
w = getWidth();
h = getHeight();
cx = w / 2;
cy = h / 2;

if (w > h) {
	radius = h / 4;
} else {
	radius = w / 4;
}

Paint paint = new Paint();
paint.setColor(Color.GREEN); // установим зелёный цвет
paint.setStyle(Paint.Style.FILL);

canvas.drawCircle(cx, cy, radius, paint);

No EmbossMaskFilter

drawLine(s)()

Простой метод - указываем начальные и конечные координаты отрезка.


package ru.alexanderklimov.simplepaint;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.view.View;

public class Draw2D extends View {

    private Paint mPaint;

    public Draw2D(Context context) {
        super(context);
        init();
    }

    private void init() {
        mPaint = new Paint();
        mPaint.setColor(Color.BLUE);
        mPaint.setStrokeWidth(10);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawLine(0, 0, 480, 650, mPaint);
        canvas.drawLine(480, 0, 0, 650, mPaint);
    }
}

drawLine

Метод drawOval()

Метод drawOval() рисует овалы. Естественно, если вы зададите одинаковые размеры ширины и высоты, то получите круг/окружность.


@Override
protected void onDraw(Canvas canvas) {
    mPaint.setStyle(Paint.Style.STROKE);
    mPaint.setColor(Color.GRAY);
    mPaint.setAntiAlias(true);
    // овал по всему экрану
    RectF oval1 = new RectF(0, 0, getWidth(), getHeight());
    canvas.drawOval(oval1, mPaint);

    // овал в виде круга (одинаковые размеры высоты и ширины)
    mPaint.setStyle(Paint.Style.FILL);
    mPaint.setColor(Color.RED);
    RectF oval2 = new RectF(50, 50, 150, 150);
    canvas.drawOval(oval2, mPaint);

    // маленький синий овал
    mPaint.setColor(Color.BLUE);
    RectF oval3 = new RectF(250, 50, 350, 300);
    canvas.drawOval(oval3, mPaint);
}

drawOval

Если вам нужно наклонить овал в ту или иную сторону, то поверните холст на требуемый угол с помощью метода rotate(). Не забудьте потом повернуть холст обратно, что следующие фигуры выводились нормально.

Повернём синий овал из предыдущего примера:


float rotate_center_x = 200;
float rotate_center_y = 200;
float rotate_angle = 45;

// поворачиваем холст
canvas.rotate(-rotate_angle, rotate_center_x, rotate_center_y);

// маленький синий овал
paint.setColor(Color.BLUE);
RectF oval3 = new RectF(250, 50, 350, 300);
canvas.drawOval(oval3, paint);

// возвращаем холст на прежний угол
canvas.rotate(rotate_angle, rotate_center_x, rotate_center_y);

drawOval

В API 21 появилась перегруженная версия метода, в котором можно указать координаты двух точек вместо RectF:


void drawOval (float left, float top, 
        float right, float bottom, 
        Paint paint)

Метод drawPaint()

Метод позволяет закрасить весь холст одним цветом.


@Override
protected void onDraw(Canvas canvas) {
    // закрашиваем холст жёлтым цветом
    mPaint.setColor(Color.YELLOW);
    // стиль Заливка
    mPaint.setStyle(Paint.Style.FILL);
    canvas.drawPaint(mPaint);
}

Метод drawRect()

У метода существует три перегруженные версии для рисования прямоугольника. Рассмотрим один из них:


@Override
protected void onDraw(Canvas canvas) {
	super.onDraw(canvas);
	
	// создаём пустой прямоугольник и задаём координаты верхней левой и нижней правой точек
	Rect myRect = new Rect();
	myRect.set(0, canvas.getHeight()/2, canvas.getWidth(), canvas.getHeight());
	// кисть
	Paint greenPaint = new Paint();
	// цвет кисти - зелёный
	greenPaint.setColor(Color.GREEN);
	// тип - заливка
	greenPaint.setStyle(Paint.Style.FILL);
	// закрашиваем зелёным прямоугольником вторую половину экрана
	canvas.drawRect(myRect, greenPaint);
}

Метод drawRoundRect()

Для рисования прямоугольников с закруглёнными углами используется метод drawRoundRect (RectF rect, float rx, float ry, Paint paint).

В параметрах указываются ограничивающий прямоугольник, радиусы овалов для скругления углов и кисть.

Реализуем три разных способа:


float width = (float) getWidth();
float height = (float) getHeight();
float radius;

if (width > height) {
	radius = height / 4;
} else {
	radius = width / 4;
}

Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setColor(Color.WHITE);
paint.setStrokeWidth(5);
paint.setStyle(Paint.Style.FILL);

float center_x, center_y;
center_x = width / 4;
center_y = height / 4;

final RectF rect = new RectF();
rect.set(center_x - radius, center_y - radius, center_x + radius,
		center_y + radius);
canvas.drawRoundRect(rect, 50, 50, paint);

paint.setStyle(Paint.Style.STROKE);
center_x = width / 2;
center_y = height / 2;
rect.set(center_x - radius,

center_y - radius, center_x + radius, center_y + radius);
canvas.drawRoundRect(rect, 100, 50, paint);

paint.setStyle(Paint.Style.STROKE);
center_x = width * 3 / 4;
center_y = height * 3 / 4;
rect.set(center_x - radius, center_y - radius, center_x + radius,
		center_y + radius);
canvas.drawRoundRect(rect, 100, 100, paint);

drawRoundRect

В API 21 появилась перегруженная версия метода, в котором можно указать координаты двух точек вместо RectF.

Метод drawPath()

Для рисования соединённых отрезков можно использовать метод drawPath(), указав в параметрах настройки для рисования и массив координат точек. Для удобства добавим в класс Draw2D новый класс Pt, который позволит быстро создать массив точек с заданными координатами. Далее настраиваем объекты для рисования и формируем путь через созданный массив. В результате получим кошкин дом.


// Класс для создания точки
class Pt {
	float x, y;

	Pt(float _x, float _y) {
		x = _x;
		y = _y;
	}
}

// создаём массив точек
Pt[] myPath = { new Pt(300, 50), new Pt(200, 200), new Pt(200, 400),
			new Pt(400, 400), new Pt(400, 200), new Pt(300, 50), };
			
@Override
protected void onDraw(Canvas canvas) {
	super.onDraw(canvas);
	
	Paint paint = new Paint();
	paint.setColor(Color.WHITE);
	paint.setStrokeWidth(3);
	paint.setStyle(Paint.Style.STROKE);
	Path path = new Path();

	// переходим в первую точку рисования
	path.moveTo(myPath[0].x, myPath[0].y);

	// рисуем отрезки по заданным точкам
	for (int i = 1; i < myPath.length; i++){
		path.lineTo(myPath[i].x, myPath[i].y);
	}

	// выводим результат
	canvas.drawPath(path, paint);
}

drawPath

Путь можно составлять не только из точек, но и из фигур, например, дуг. Сначала формируем дугу, добавляем её в путь при помощи метода Path.addArc(), повторяем операцию снова несколько раз, а в конце выводим окончательный вариант:


float radius = 150;

Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setColor(Color.WHITE);
paint.setStrokeWidth(5);
paint.setStyle(Paint.Style.STROKE);
Path path = new Path();

float center_x, center_y;
center_x = 0;
center_y = 0;

final RectF oval = new RectF();
oval.set(center_x - radius, center_y - radius, center_x + radius,
		center_y + radius);
path.addArc(oval, 10, 70); // первая дуга

oval.set(center_x - radius - 10, center_y - radius - 10, center_x + radius + 20,
		center_y + radius + 20);
path.addArc(oval, 5, 80); // вторая дуга

oval.set(center_x - radius- 20, center_y - radius -20, center_x + radius + 40,
		center_y + radius + 40);
path.addArc(oval, 0, 90); // третья дуга

canvas.drawPath(path, paint); // выводим всё вместе

drawPath

Можно нарисовать символ парашюта:


Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setColor(Color.WHITE);
paint.setStrokeWidth(5);
paint.setStyle(Paint.Style.STROKE);
Path path = new Path();


path.moveTo(160.0f, 240.0f);
path.lineTo(140.0f, 200.0f);
path.addArc(new RectF(140, 180, 180, 220), -180, 180);
path.lineTo(160.0f, 240.0f);
path.close(); 

canvas.drawPath(path, paint);

addArc

Метод drawPoint()

Простой метод для рисования точки в нужно месте указанной кистью. Для координат используются значения типа float.


canvas.drawPoint(1F, 0F, mPaint);

Метод drawText()

С помощью метода drawText() можно выводить текст в заданной позиции. Добавим сначала несколько эффектов, чтобы казалось, что текст парит над поверхностью:


@Override
protected void onDraw(Canvas canvas) {
	super.onDraw(canvas);
	
	canvas.drawColor(Color.GRAY);

    Paint shadowPaint = new Paint();
    shadowPaint.setAntiAlias(true);
    shadowPaint.setColor(Color.WHITE);
    shadowPaint.setTextSize(35.0f);
    shadowPaint.setStrokeWidth(2.0f);
    shadowPaint.setStyle(Paint.Style.STROKE);
    shadowPaint.setShadowLayer(5.0f, 10.0f, 10.0f, Color.BLACK);
    
    canvas.drawText("http://developer.alexanderklimov.ru/android", 20, 200, shadowPaint);
}

drawText()

Центрируем текст

Есть небольшая тонкость, если вам захочется вывести текст в центре холста. Проблем с вычислением центра холста и размером текста нет. Центр можно найти, разделив пополам значения ширины и высоты холста. А ширину и высоту текста можно узнать через метод кисти getTextBounds(), который возвращает ограничивающий прямоугольник.

Но вычисление ширины текста через textBounds.width(); приводит к небольшому смещению. Лучше воспользоваться методом кисти measureText(). Тогда текст отцентрируется точнее.


package ru.alexanderklimov.canvas;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.view.View;

public class Draw2D extends View {

    private Paint mPaint = new Paint();
    private Rect mTextBoundRect = new Rect();

    public Draw2D(Context context) {
        super(context);
    }

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

        float textWidth, textHeight;
        String text = "МЯЧ";

        float width, height, centerX, centerY, radius;
        width = getWidth();
        height = getHeight();
        centerX = width / 2;
        centerY = height / 2;

        if (width > height) {
            radius = height / 4;
        } else {
            radius = width / 4;
        }

        // стиль Заливка
        mPaint.setStyle(Paint.Style.FILL);

        // закрашиваем холст белым цветом
        mPaint.setColor(Color.WHITE);
        canvas.drawPaint(mPaint);

        // Рисуем желтый круг
        mPaint.setAntiAlias(true);
        mPaint.setColor(Color.YELLOW);
        canvas.drawCircle(centerX, centerY, radius, mPaint);

        // Рисуем текст
        mPaint.setColor(Color.BLUE);
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setAntiAlias(true);
        mPaint.setTextSize(100);

        // Подсчитаем размер текста
        mPaint.getTextBounds(text, 0, text.length(), mTextBoundRect);
        //textWidth = textBounds.width();
        // Используем measureText для измерения ширины
        textWidth = mPaint.measureText(text);
        textHeight = mTextBoundRect.height();

        canvas.drawText(text,
                centerX - (textWidth / 2f),
                centerY + (textHeight /2f),
                mPaint
        );
    }
}

drawText()

Пример на Kotlin с дополнительной информацией.

Методы rotate() и restore()

Холст во время рисования можно вращать. Во многих ситуациях такой приём менее затратный по ресурсам, чем рисование самого объекта под углом. Суть в следующем: вы поворачиваете холст на нужный градус, рисуете фигуру, а затем возвращаете холст на место при помощи метода restore(), чтобы следующие фигуры рисовались в ожидаемых местах. Иначе остальные фигуры будут рисоваться уже относительно поворота.

В примере с овалом уже использовался данный метод. В примере Работаем с графикой. Основы мы также поворачивали холст, чтобы вывести текст под углом.

Вращение происходит вокруг начальной точки холста (0, 0). Но можно также использовать перегруженную версию метода rotate(float degrees, float px, float py), в которой можно указать координаты точки поворота.

Методы scale() и translate()

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

Чтобы установить свою систему координат, нужно произвести трансформацию. В следующем примере мы установим координаты в диапазоне от 0 до 10 и нарисуем график в стандартном виде из точки 0,0 в левом нижнем углу в точку 10,10 в верхнем правом углу.


@Override
protected void onDraw(Canvas canvas) {
    mPaint.setStyle(Paint.Style.STROKE);
    mPaint.setColor(Color.GRAY);
    mPaint.setAntiAlias(true);

    canvas.save();
    float xMax = 10F;
    float xMin = 0F;
    float yMax = 10F;
    float yMin = 0F;
    float width = canvas.getWidth();
    float height = canvas.getHeight();
    canvas.scale(width / (xMax - xMin), -height / (yMax - yMin));
    canvas.translate(-xMin + 0.2F, -yMax + 0.2F);
    mPaint.setStrokeWidth(.1f);

    // Ось X
    canvas.drawLine(0F, 0F, 10F, 0F, mPaint);

    // Ось Y
    canvas.drawLine(0F, 0F, 0F, 10F, mPaint);

    mPaint.setColor(Color.RED);
    canvas.drawLine(0F, 0F, 10F, 10F, mPaint);

    // Точки вдоль оси X
    canvas.drawPoint(1F, 0F, mPaint);
    canvas.drawPoint(2F, 0F, mPaint);
    canvas.drawPoint(3F, 0F, mPaint);

    // Точки вдоль оси Y
    canvas.drawPoint(0F, 1F, mPaint);
    canvas.drawPoint(0F, 2F, mPaint);
    canvas.drawPoint(0F, 3F, mPaint);

    canvas.restore();
}

Для наглядности я добавил на оси несколько точек. Следует обратить внимание, что мы задали диапазон от 0 до 10 и все размеры должны масштабироваться в новых величинах, в том числе и ширина обводки в методе setStrokeWidth(). Поэтому значения должны быть достаточно маленькими, иначе толщина обводки может просто оказаться больше самой фигуры. Кстати, в некоторых случаях с текстом и другими методами рисование масштабирование может сыграть злую шутку и дробные значения не позволят увидеть текст и некоторые линии. В этих случаях приходиться создавать цепочку преобразований, когда временно масштаб увеличивается до нормальных размеров, рисуется текст с подходящим размером шрифта, затем опять всё уменьшается и т.д. Это долгая история.

scale and translate

Дополнительные материалы

Примеры для Kotlin

Генератор котов

Реклама