В первой части моей серии Создание графика с помощью Quartz 2D я объяснил фоновую работу. Гистограммы — популярный вид графиков, поэтому давайте научимся их рисовать.
Прежде всего, я предлагаю закомментировать строки кода, которые рисуют фоновое изображение. Мы знаем, как это сделать, если нужно, но давайте сделаем все как можно проще.
Во-вторых, было бы неплохо оставить некоторое пространство между нашими барами, поэтому давайте увеличим горизонтальный шаг. В GraphView.h
kStepX
#define kStepX 70
Рисование Баров
Давайте добавим в GraphView.ma метод, который будет рисовать один бар за раз. Убедитесь, что этот метод определен перед drawRect
- (void)drawBar:(CGRect)rect context:(CGContextRef)ctx
{
}
Мы передаем прямоугольник в метод, чтобы заполнить его полосой, и графический контекст для рисования. Я считаю, что простой прямоугольник, заполненный красивым градиентом, лучше всего подходит для баров, поэтому давайте научимся рисовать его. Если вы предпочитаете, вы можете изменить следующий код и нарисовать, скажем, прямоугольник со скругленными углами, но, опять же, я предпочитаю сделать все проще.
Мы собираемся нарисовать прямоугольник как путь, поэтому весь код рисования будет окружен следующими двумя линиями:
CGContextBeginPath(ctx);
...
CGContextClosePath(ctx);
Код для определения градиента может быть несколько многословным, поэтому для начала давайте закрасим наши прямоугольники сплошным цветом. Вот одна строка кода, которая подготовит среду для рисования:
CGContextSetGrayFillColor(ctx, 0.2, 0.7);
Второй параметр указывает, насколько темной должна быть заливка, где 0 означает черный, а 1 означает белый. В нашем случае это темно-серый. Последний параметр определяет прозрачность заливки: 0 полностью прозрачен, а 1 полностью непрозрачен. В нашем случае это на 70% непрозрачно.
Фактическое рисование занимает четыре строки кода, и я полагаю, что вы можете легко догадаться, что здесь происходит из названий функций:
CGContextMoveToPoint(ctx, CGRectGetMinX(rect), CGRectGetMinY(rect));
CGContextAddLineToPoint(ctx, CGRectGetMaxX(rect), CGRectGetMinY(rect));
CGContextAddLineToPoint(ctx, CGRectGetMaxX(rect), CGRectGetMaxY(rect));
CGContextAddLineToPoint(ctx, CGRectGetMinX(rect), CGRectGetMaxY(rect));
Наконец, на шаге 3 нам нужно зафиксировать то, что было нарисовано:
CGContextFillPath(ctx);
Вот законченный метод для рисования сплошной полосы:
- (void)drawBar:(CGRect)rect context:(CGContextRef)ctx
{
CGContextBeginPath(ctx);
CGContextSetGrayFillColor(ctx, 0.2, 0.7);
CGContextMoveToPoint(ctx, CGRectGetMinX(rect), CGRectGetMinY(rect));
CGContextAddLineToPoint(ctx, CGRectGetMaxX(rect), CGRectGetMinY(rect));
CGContextAddLineToPoint(ctx, CGRectGetMaxX(rect), CGRectGetMaxY(rect));
CGContextAddLineToPoint(ctx, CGRectGetMinX(rect), CGRectGetMaxY(rect));
CGContextClosePath(ctx);
CGContextFillPath(ctx);
}
Данные Графика
Далее нам нужно позаботиться о данных, отображаемых на графике. Как правило, данные могут быть доставлены каким-то веб-сервисом. Это может быть, например, количество посетителей вашего сайта в месяц. Однако для простоты мы собираемся жестко закодировать данные со значениями от 0 до 1, где 1 будет означать столбец, принимающий всю высоту графика, а 0 означает отсутствие столбца вообще. Поместите эту строку кода где-нибудь вне любого метода в GraphView.m
float data[] = {0.7, 0.4, 0.9, 1.0, 0.2, 0.85, 0.11, 0.75, 0.53, 0.44, 0.88, 0.77};
Давайте добавим пару констант, которые помогут нам позиционировать и определять размеры баров:
#define kBarTop 10
#define kBarWidth 40
Наконец, нам нужно нарисовать столбцы, соответствующие тестовым значениям. В самом конце drawRect
// Draw the bars
float maxBarHeight = kGraphHeight - kBarTop - kOffsetY;
for (int i = 0; i < sizeof(data); i++)
{
float barX = kOffsetX + kStepX + i * kStepX - kBarWidth / 2;
float barY = kBarTop + maxBarHeight - maxBarHeight * data[i];
float barHeight = maxBarHeight * data[i];
CGRect barRect = CGRectMake(barX, barY, kBarWidth, barHeight);
[self drawBar:barRect context:context];
}
Вы должны быть в состоянии понять, что здесь происходит без дополнительных объяснений, просто имейте в виду, что координата Y увеличивается сверху вниз. И вот результат, которого мы достигли до сих пор:
График уже выглядит довольно хорошо и может быть полезен для некоторых приложений, как есть. Возможно, вы захотите использовать другой цвет вместо серого, но это легко сделать. Вот ссылка на CGContext Reference , где вы найдете все методы, которые вам могут понадобиться.
Однако график будет выглядеть значительно лучше, если мы заполним столбцы градиентом. Посмотрим, как это можно сделать.
Градиентная заливка
То, как градиенты определяются и используются в Кварце, несколько многословно, но оно дает нам большую силу. Вот все, что нам нужно знать, чтобы заполнить наши бары градиентами.
Во-первых, нам нужно решить, сколько цветов мы будем использовать для градиента. Мы можем использовать любое число, но для наших целей должно быть достаточно трех цветов. Давайте определим их, перечислив их красный, зеленый, синий и альфа-компоненты:
CGFloat components[12] = {0.2314, 0.5686, 0.4, 1.0, // Start color
0.4727, 1.0, 0.8157, 1.0, // Second color
0.2392, 0.5686, 0.4118, 1.0}; // End color
Затем нам нужно решить, где расположить эти цвета в градиенте, где 0 означает начало рисунка, а 1 означает конец рисунка. Вот один из возможных вариантов для наших трех цветов:
CGFloat locations[3] = {0.0, 0.33, 1.0};
Нам также нужно будет явно определить количество мест:
size_t num_locations = 3;
Наконец, нам нужно создать цветовое пространство, а затем, используя всю подготовленную информацию, мы можем построить градиент:
CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceRGB();
CGGradientRef gradient = CGGradientCreateWithColorComponents(colorspace, components, locations, num_locations);
Когда вам больше не нужен градиент, вы должны освободить и градиент, и цветовое пространство:
CGColorSpaceRelease(colorspace);
CGGradientRelease(gradient);
Непосредственно перед использованием градиента нам нужно указать, где шаблон будет начинаться и заканчиваться, в терминах графического пространства. Мы используем CGRect
CGPoint startPoint = rect.origin;
CGPoint endPoint = CGPointMake(rect.origin.x + rect.size.width, rect.origin.y);
И, наконец, вот линия, которая делает фактический рисунок:
// Draw the gradient
CGContextDrawLinearGradient(ctx, gradient, startPoint, endPoint, 0);
Вот весь код, который готовит, рисует и выпускает градиент:
// Prepare the resources
CGFloat components[12] = {0.2314, 0.5686, 0.4, 1.0, // Start color
0.4727, 1.0, 0.8157, 1.0, // Second color
0.2392, 0.5686, 0.4118, 1.0}; // End color
CGFloat locations[3] = {0.0, 0.33, 1.0};
size_t num_locations = 3;
CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceRGB();
CGGradientRef gradient = CGGradientCreateWithColorComponents(colorspace, components, locations, num_locations);
CGPoint startPoint = rect.origin;
CGPoint endPoint = CGPointMake(rect.origin.x + rect.size.width, rect.origin.y);
// Draw the gradient
CGContextDrawLinearGradient(ctx, gradient, startPoint, endPoint, 0);
// Release the resources
CGColorSpaceRelease(colorspace);
CGGradientRelease(gradient);
В этот момент у нас может возникнуть искушение отказаться от кода, который мы использовали ранее для рисования и заполнения панели, и просто нарисовать градиент. Если мы сделаем это, однако, результат будет отличаться от того, что мы ожидали:
Похоже, градиент не совсем понимает, сколько места он должен занимать. Нам нужно как-то ограничить область рисования размерами стержня. Это где обтравочный контур становится полезным.
Обтравочные контуры
Вот как мы собираемся это сделать. Во-первых, мы нарисуем столбец в виде заполненного прямоугольника, как мы делали раньше, но вместо того, чтобы зафиксировать рисунок и сделать его видимым, мы расскажем о графическом контексте: только что нарисованная полоса определяет единственное пространство, в котором вы разрешено рисовать с этого момента. Эта длинная фраза может быть переведена в довольно короткую строку кода:
CGContextClip(ctx);
Мы должны быть в состоянии снять ограничение сразу же после рисования стержня. Для этого мы скажем контексту помнить его состояние неограниченной свободы прямо перед применением обтравочного контура:
CGContextSaveGState(ctx);
И сразу после того, как градиент был нарисован с использованием обтравочного контура, мы собираемся восстановить начальное состояние контекста:
CGContextRestoreGState(ctx);
Вот полное решение для рисования панели с градиентной заливкой, со всеми шагами, которые мы упомянули выше, в правильном порядке:
- (void)drawBar:(CGRect)rect context:(CGContextRef)ctx
{
// Prepare the resources
CGFloat components[12] = {0.2314, 0.5686, 0.4, 1.0, // Start color
0.4727, 1.0, 0.8157, 1.0, // Second color
0.2392, 0.5686, 0.4118, 1.0}; // End color
CGFloat locations[3] = {0.0, 0.33, 1.0};
size_t num_locations = 3;
CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceRGB();
CGGradientRef gradient = CGGradientCreateWithColorComponents(colorspace, components, locations, num_locations);
CGPoint startPoint = rect.origin;
CGPoint endPoint = CGPointMake(rect.origin.x + rect.size.width, rect.origin.y);
// Create and apply the clipping path
CGContextBeginPath(ctx);
CGContextSetGrayFillColor(ctx, 0.2, 0.7);
CGContextMoveToPoint(ctx, CGRectGetMinX(rect), CGRectGetMinY(rect));
CGContextAddLineToPoint(ctx, CGRectGetMaxX(rect), CGRectGetMinY(rect));
CGContextAddLineToPoint(ctx, CGRectGetMaxX(rect), CGRectGetMaxY(rect));
CGContextAddLineToPoint(ctx, CGRectGetMinX(rect), CGRectGetMaxY(rect));
CGContextClosePath(ctx);
CGContextSaveGState(ctx);
CGContextClip(ctx);
// Draw the gradient
CGContextDrawLinearGradient(ctx, gradient, startPoint, endPoint, 0);
CGContextRestoreGState(ctx);
// Release the resources
CGColorSpaceRelease(colorspace);
CGGradientRelease(gradient);
}
Самостоятельно
Конечно, есть место для совершенствования. Поскольку мы снова и снова используем один и тот же градиент, было бы эффективнее создать его только один раз, а затем повторно использовать для рисования столько столбцов, сколько необходимо, вместо того, чтобы воссоздавать градиент для каждого бара. Однако позвольте мне оставить этот рефакторинг для вас. Вот что мы должны увидеть при запуске этого кода:
Теперь у нас есть гистограмма, которая близка к завершению. Нам понадобятся некоторые ярлыки, и мы должны будем реагировать на прикосновения, но эти темы будут рассмотрены в более поздней части серии. Другой популярный вид графиков — это линейный график, и в следующей части серии мы научимся рисовать их, включая градиенты, а также несколько других приятных настроек.
Кварц 2D Индекс
Серия Александра Колесникова «Создание графика с использованием Quartz 2D» была разбита на 5 частей. Вы можете сослаться на серию, используя Quartz 2D Tag, и получить доступ к отдельным статьям, используя ссылки ниже.
- Создание графика с помощью Quartz 2D: часть 1
- Создание графика с помощью Quartz 2D: часть 2
- Создание графика с помощью Quartz 2D: часть 3
- Создание графика с помощью Quartz 2D: часть 4
- Создание графика с помощью Quartz 2D: часть 5