Как нарисовать линию градиента (постепенное появление / исчезновение) с помощью Core Graphics / iPhone?

Я умею рисовать простую линию:

CGContextSetRGBStrokeColor(context, 1.0, 1.0, 1.0, 1.0);
CGContextMoveToPoint(context, x, y);
CGContextAddLineToPoint(context, x2, y2);
CGContextStrokePath(context);

И я знаю, как создать градиентный прямоугольник, например:

CGColorSpaceRef myColorspace=CGColorSpaceCreateDeviceRGB();
size_t num_locations = 2;
CGFloat locations[2] = { 1.0, 0.0 };
CGFloat components[8] = { 0.0, 0.0, 0.0, 1.0,    1.0, 1.0, 1.0, 1.0 };

CGGradientRef myGradient = CGGradientCreateWithColorComponents(myColorspace, components, locations, num_locations);

CGPoint myStartPoint, myEndPoint;
myStartPoint.x = 0.0;
myStartPoint.y = 0.0;
myEndPoint.x = 0.0;
myEndPoint.y = 10.0;
CGContextDrawLinearGradient (context, myGradient, myStartPoint, myEndPoint, 0);

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


person Walchy    schedule 20.08.2009    source источник
comment
Небольшое примечание - выбранный в настоящее время ответ здесь неверен. Можно обвести произвольные пути градиентом, как показано в этом ответе.   -  person Benjohn    schedule 22.05.2015


Ответы (7)


Можно обводить произвольные контуры градиентом или любым другим эффектом заливки, например узором.

Как вы обнаружили, обведенные контуры не отображаются с текущим градиентом. Только контуры с заливкой используют градиент (когда вы превращаете их в клип, а затем рисуете градиент).

Однако в Core Graphics есть удивительно крутая процедура CGContextReplacePathWithStrokedPath, которая преобразует путь, который вы собираетесь обводить, в путь, который эквивалентен при заполнении.

За кулисами CGContextReplacePathWithStrokedPath строит краевой многоугольник вокруг вашего пути обводки и переключает его на путь, который вы определили. Я предполагаю, что движок рендеринга Core Graphics, вероятно, делает это в любом случае при вызовах CGContextStrokePath.

Вот документация Apple по этому поводу:

Quartz создает контур с обводкой, используя параметры текущего графического контекста. Новый контур создается таким образом, что при заливке рисуются те же пиксели, что и при обводке исходного контура. Вы можете использовать этот путь так же, как и путь любого контекста. Например, вы можете обрезать контурную версию пути, вызвав эту функцию с последующим вызовом функции CGContextClip.

Итак, преобразуйте свой путь во что-то, что вы можете заполнить, превратите его в клип и затем нарисуйте свой градиент. Эффект будет таким, как если бы вы обводили путь градиентом.

Код

Это будет выглядеть примерно так…

    // Get the current graphics context.
    //
    const CGContextRef context = UIGraphicsGetCurrentContext();

    // Define your stroked path. 
    //
    // You can set up **anything** you like here.
    //
    CGContextAddRect(context, yourRectToStrokeWithAGradient);

    // Set up any stroking parameters like line.
    //
    // I'm setting width. You could also set up a dashed stroke
    // pattern, or whatever you like.
    //
    CGContextSetLineWidth(context, 1);

    // Use the magical call.
    //
    // It turns your _stroked_ path in to a **fillable** one.
    //
    CGContextReplacePathWithStrokedPath(context);

    // Use the current _fillable_ path in to define a clipping region.
    //
    CGContextClip(context);

    // Draw the gradient.
    //
    // The gradient will be clipped to your original path.
    // You could use other fill effects like patterns here.
    //
    CGContextDrawLinearGradient(
      context, 
      yourGradient, 
      gradientTop, 
      gradientBottom, 
      0
    );

Дальнейшие примечания

Стоит выделить часть документации выше:

Кварц создает контур с обводкой, используя параметры текущего графического контекста.

Очевидный параметр - ширина линии. Однако используется состояние прорисовки всех линий, таких как образец штрихов, ограничение угла наклона, соединения линий, заглавные буквы, образцы штрихов и т. Д. Это делает подход чрезвычайно эффективным.

Дополнительные сведения см. В этом ответе в это ТАК вопрос.

person Benjohn    schedule 30.07.2014

После нескольких попыток я теперь уверен, что градиенты не влияют на штрихи, поэтому я думаю, что невозможно нарисовать градиентные линии с помощью CGContextStrokePath(). Для горизонтальных и вертикальных линий решение состоит в том, чтобы вместо этого использовать CGContextAddRect(), что, к счастью, именно то, что мне нужно. Я заменил

CGContextMoveToPoint(context, x, y);
CGContextAddLineToPoint(context, x2, y2);
CGContextStrokePath(context);

с участием

CGContextSaveGState(context);
CGContextAddRect(context, CGRectMake(x, y, width, height));
CGContextClip(context);
CGContextDrawLinearGradient (context, gradient, startPoint, endPoint, 0);
CGContextRestoreGState(context);

и все работает нормально. Спасибо Брэду Ларсону за решающий намек.

person Walchy    schedule 20.08.2009
comment
Я нашел то же самое. Казалось бы, градиенты требуют, чтобы к графическому контексту сначала применялся контур обрезки (или прямоугольник). Возможно, это связано с тем, что градиенты отображаются во внешней области, а не на самой границе пути (или прямоугольника)? - person Dalmazio; 29.07.2011
comment
Это не может отрисовать отстойную линию? - person AntiMoron; 17.03.2016

После того, как вы проведете линию, вы можете позвонить

CGContextClip(context);

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

Существует вероятность того, что линия будет слишком тонкой для отображения вашего градиента, и в этом случае вы можете использовать CGContextAddRect(), чтобы определить более толстую область.

Я представляю более сложный пример использования этого отсечения контекста в своем ответе здесь.

person Brad Larson    schedule 20.08.2009
comment
Я попробовал ваше предложение: CGContextSetLineWidth (context, 10.0); // должно быть достаточно толстым CGContextSetRGBStrokeColor (context, 0.0, 0.0, 0.0, 0.0); CGContextMoveToPoint (контекст, 50,50); CGContextAddLineToPoint (контекст, 100,100); CGContextStrokePath (контекст); CGContextClip (контекст); CGContextDrawLinearGradient (контекст, myGradient, myStartPoint, myEndPoint, 0); но это вызывает ‹Error›: doClip: пустой путь. В документации сказано, что CGContextStrokePath (context); очищает путь - без этого метода ошибка не выдается, но вообще ничего не рисуется. Любые идеи? - person Walchy; 20.08.2009

Вы можете использовать слои Core Animation. Для ваша строка, настроив ее свойство пути, а затем вы можете использовать CAGradientLayer в качестве маски слоя для вашего слоя-фигуры, что приведет к исчезновению линии.

Замените ваши вызовы CGContext ... вызовами CGPath ..., чтобы создать путь к строке. Задайте поле пути на слое, используя этот путь. Затем в вашем градиентном слое укажите цвета, которые вы хотите использовать (возможно, от черного к белому), а затем установите маску как линейный слой следующим образом:

 [gradientLayer setMask:lineLayer];

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

Дайте мне знать, если вам нужны разъяснения.

РЕДАКТИРОВАТЬ: Теперь, когда я думаю об этом, просто создайте один CAGradientLayer, который является шириной / высотой желаемой строки. Укажите цвета градиента (от черного к белому или от черного к прозрачному цвету), а также начальную и конечную точки, и это должно дать вам то, что вам нужно.

person Matt Long    schedule 20.08.2009
comment
Вы правы, это идеально подошло бы мне, но, к сожалению, это доступно только с версией 3 ОС iPhone, и я хочу использовать как минимум 2.2. Спасибо, в любом случае... - person Walchy; 20.08.2009
comment
Я полагаю, вы имели в виду [gradientLayer setMask: LineLayer]; - правильно? Вы маскируете CAGradientLayer с помощью CAShapeLayer. - person Palimondo; 20.08.2011
comment
Блестяще. Многому научился на этом. - person Ian1971; 07.10.2011
comment
@Palimondo Оба варианта верны, в зависимости от того, хотите ли вы сплошную линию, которая исчезает, или вам нужна линия с градиентной заливкой, которая остается сплошной на всем протяжении. - person uliwitness; 03.05.2016

CGContextMoveToPoint(context, frame.size.width-200, frame.origin.y+10);
CGContextAddLineToPoint(context, frame.size.width-200, 100-10);
CGFloat colors[16] = { 0,0, 0, 0,
    0, 0, 0, .8,
    0, 0, 0, .8,
    0, 0,0 ,0 };
CGColorSpaceRef baseSpace = CGColorSpaceCreateDeviceRGB();
CGGradientRef gradient = CGGradientCreateWithColorComponents(baseSpace, colors, NULL, 4);

CGContextSaveGState(context);
CGContextAddRect(context, CGRectMake(frame.size.width-200,10, 1, 80));
CGContextClip(context);
CGContextDrawLinearGradient (context, gradient, CGPointMake(frame.size.width-200, 10), CGPointMake(frame.size.width-200,80), 0);
CGContextRestoreGState(context);

это работа для меня.

person Bimawa    schedule 30.05.2012

Я создал Swift-версию ответа Бенджона.

class MyView: UIView {
    override func draw(_ rect: CGRect) {
    super.draw(rect)

    guard let context = UIGraphicsGetCurrentContext() else {
        return
    }

    context.addPath(createPath().cgPath)
    context.setLineWidth(15)
    context.replacePathWithStrokedPath()
    context.clip()

    let gradient = CGGradient(colorsSpace: nil, colors: [UIColor.red.cgColor, UIColor.yellow.cgColor, UIColor.green.cgColor] as CFArray, locations: [0, 0.4, 1.0])!
    let radius:CGFloat = 200
    let center = CGPoint(x: bounds.width / 2, y: bounds.height / 2)
    context.drawRadialGradient(gradient, startCenter: center, startRadius: 10, endCenter: center, endRadius: radius, options: .drawsBeforeStartLocation)
    }
}

Если метод createPath () создает треугольник, вы получите что-то вроде этого:

введите описание изображения здесь

Надеюсь, это кому-нибудь поможет!

person Jordan    schedule 10.08.2020

Версия Swift 5

Этот код работал у меня.

override func draw(_ rect: CGRect) {
    super.draw(rect)
    
    guard let context = UIGraphicsGetCurrentContext() else {
        return
    }
    
    drawGradientBorder(rect, context: context)
}

fileprivate 
func drawGradientBorder(_ rect: CGRect, context: CGContext) {
    context.saveGState()
    let path = ribbonPath()
    context.setLineWidth(5.0)
    context.addPath(path.cgPath)
    context.replacePathWithStrokedPath()
    context.clip()
    
    let baseSpace = CGColorSpaceCreateDeviceRGB()
    let gradient = CGGradient(colorsSpace: baseSpace, colors: [start.cgColor, end.cgColor] as CFArray, locations: [0.0 ,1.0])!
    context.drawLinearGradient(gradient, start: CGPoint(x: 0, y: rect.height/2), end: CGPoint(x: rect.width, y: rect.height/2), options: [])
    context.restoreGState()
}

fileprivate 
func ribbonPath() -> UIBezierPath {
    let path = UIBezierPath()
    path.move(to: CGPoint.zero)
    path.addLine(to: CGPoint(x: bounds.maxX - gradientWidth, y: 0))
    path.addLine(to: CGPoint(x: bounds.maxX - gradientWidth - ribbonGap, y: bounds.maxY))
    path.addLine(to: CGPoint(x: 0, y: bounds.maxY))
    path.close()
    return path
}
person Li Jin    schedule 29.03.2021