Эффект внутренней тени на слое UIView? - PullRequest
89 голосов
/ 13 декабря 2010

У меня есть следующий CALayer:

CAGradientLayer *gradient = [CAGradientLayer layer];
gradient.frame = CGRectMake(8, 57, 296, 30);
gradient.cornerRadius = 3.0f;
gradient.colors = [NSArray arrayWithObjects:(id)[RGB(130, 0, 140) CGColor], (id)[RGB(108, 0, 120) CGColor], nil];
[self.layer insertSublayer:gradient atIndex:0];

Я бы хотел добавить к нему эффект Внутренняя тень , но я не совсем уверен, как это сделать. Полагаю, мне нужно было бы рисовать в drawRect, однако это добавило бы слой поверх других объектов UIView, поскольку он должен быть полосой за некоторыми кнопками, поэтому я не знаю, что делать?

Я мог бы добавить еще один слой, но, опять же, не уверен, как добиться эффекта внутренней тени (например:

enter image description here

Помощь оценена ...

Ответы [ 15 ]

106 голосов
/ 04 апреля 2011

Для всех, кто интересуется, как нарисовать внутреннюю тень с использованием Core Graphics в соответствии с рекомендациями Costique, вот как: (на iOS отрегулируйте по мере необходимости)

В вашем drawRect: метод ...

CGRect bounds = [self bounds];
CGContextRef context = UIGraphicsGetCurrentContext();
CGFloat radius = 0.5f * CGRectGetHeight(bounds);


// Create the "visible" path, which will be the shape that gets the inner shadow
// In this case it's just a rounded rect, but could be as complex as your want
CGMutablePathRef visiblePath = CGPathCreateMutable();
CGRect innerRect = CGRectInset(bounds, radius, radius);
CGPathMoveToPoint(visiblePath, NULL, innerRect.origin.x, bounds.origin.y);
CGPathAddLineToPoint(visiblePath, NULL, innerRect.origin.x + innerRect.size.width, bounds.origin.y);
CGPathAddArcToPoint(visiblePath, NULL, bounds.origin.x + bounds.size.width, bounds.origin.y, bounds.origin.x + bounds.size.width, innerRect.origin.y, radius);
CGPathAddLineToPoint(visiblePath, NULL, bounds.origin.x + bounds.size.width, innerRect.origin.y + innerRect.size.height);
CGPathAddArcToPoint(visiblePath, NULL,  bounds.origin.x + bounds.size.width, bounds.origin.y + bounds.size.height, innerRect.origin.x + innerRect.size.width, bounds.origin.y + bounds.size.height, radius);
CGPathAddLineToPoint(visiblePath, NULL, innerRect.origin.x, bounds.origin.y + bounds.size.height);
CGPathAddArcToPoint(visiblePath, NULL,  bounds.origin.x, bounds.origin.y + bounds.size.height, bounds.origin.x, innerRect.origin.y + innerRect.size.height, radius);
CGPathAddLineToPoint(visiblePath, NULL, bounds.origin.x, innerRect.origin.y);
CGPathAddArcToPoint(visiblePath, NULL,  bounds.origin.x, bounds.origin.y, innerRect.origin.x, bounds.origin.y, radius);
CGPathCloseSubpath(visiblePath);

// Fill this path
UIColor *aColor = [UIColor redColor];
[aColor setFill];
CGContextAddPath(context, visiblePath);
CGContextFillPath(context);


// Now create a larger rectangle, which we're going to subtract the visible path from
// and apply a shadow
CGMutablePathRef path = CGPathCreateMutable();
//(when drawing the shadow for a path whichs bounding box is not known pass "CGPathGetPathBoundingBox(visiblePath)" instead of "bounds" in the following line:)
//-42 cuould just be any offset > 0
CGPathAddRect(path, NULL, CGRectInset(bounds, -42, -42));

// Add the visible path (so that it gets subtracted for the shadow)
CGPathAddPath(path, NULL, visiblePath);
CGPathCloseSubpath(path);

// Add the visible paths as the clipping path to the context
CGContextAddPath(context, visiblePath); 
CGContextClip(context);         


// Now setup the shadow properties on the context
aColor = [UIColor colorWithRed:0.0f green:0.0f blue:0.0f alpha:0.5f];
CGContextSaveGState(context);
CGContextSetShadowWithColor(context, CGSizeMake(0.0f, 1.0f), 3.0f, [aColor CGColor]);   

// Now fill the rectangle, so the shadow gets drawn
[aColor setFill];   
CGContextSaveGState(context);   
CGContextAddPath(context, path);
CGContextEOFillPath(context);

// Release the paths
CGPathRelease(path);    
CGPathRelease(visiblePath);

Итак, по сути, есть следующие шаги:

  1. Создайте свой путь
  2. Установите нужный цвет заливки, добавьте этот путь в контекст и заполнитеcontext
  3. Теперь создайте больший прямоугольник, который может ограничивать видимый путь.Прежде чем закрыть этот путь, добавьте видимый путь.Затем закройте путь, чтобы создать фигуру с видимой траекторией, вычтенной из нее.Возможно, вы захотите исследовать методы заполнения (ненулевое наматывание четного / нечетного) в зависимости от того, как вы создали эти пути.В сущности, чтобы заставить подпуть «вычитать», когда вы складываете их вместе, вам нужно нарисовать их (или, скорее, построить) в противоположных направлениях, один по часовой стрелке, а другой против часовой стрелки.
  4. Тогда вам нужноустановить видимый путь в качестве обтравочного контура в контексте, чтобы вы ничего не рисовали вне его на экране.
  5. Затем настройте тень для контекста, которая включает смещение, размытие и цвет.
  6. Затем заполните большую форму с отверстием в нем.Цвет не имеет значения, потому что если вы все сделали правильно, вы не увидите этот цвет, только тень.
46 голосов
/ 11 июля 2012

Я знаю, что опаздываю на эту вечеринку, но это помогло бы мне рано найти поездку ...

Чтобы отдать должное должному, это, по сути, модификация разработок Дэниела Торпана решение Costique вычитания меньшего региона из большего региона.Эта версия предназначена для тех, кто использует композицию слоя вместо переопределения -drawRect:

. Класс CAShapeLayer можно использовать для достижения того же эффекта:

CAShapeLayer* shadowLayer = [CAShapeLayer layer];
[shadowLayer setFrame:[self bounds]];

// Standard shadow stuff
[shadowLayer setShadowColor:[[UIColor colorWithWhite:0 alpha:1] CGColor]];
[shadowLayer setShadowOffset:CGSizeMake(0.0f, 0.0f)];
[shadowLayer setShadowOpacity:1.0f];
[shadowLayer setShadowRadius:5];

// Causes the inner region in this example to NOT be filled.
[shadowLayer setFillRule:kCAFillRuleEvenOdd];

// Create the larger rectangle path.
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, CGRectInset(bounds, -42, -42));

// Add the inner path so it's subtracted from the outer path.
// someInnerPath could be a simple bounds rect, or maybe
// a rounded one for some extra fanciness.
CGPathAddPath(path, NULL, someInnerPath);
CGPathCloseSubpath(path);

[shadowLayer setPath:path];
CGPathRelease(path);

[[self layer] addSublayer:shadowLayer];

На этом этапе, если ваш родительскийслой не маскируется до его границ, вы увидите дополнительную область маскирующего слоя по краям слоя.Это будет 42 пикселя черного цвета, если вы просто скопировали пример напрямую.Чтобы избавиться от него, вы можете просто использовать другой CAShapeLayer с тем же путем и установить его в качестве маски теневого слоя:

CAShapeLayer* maskLayer = [CAShapeLayer layer];
[maskLayer setPath:someInnerPath];
[shadowLayer setMask:maskLayer];

Я сам не тестировал это, но подозреваю, чтоиспользование этого подхода в сочетании с растеризацией является более производительным, чем переопределение -drawRect:.

35 голосов
/ 13 декабря 2010

Можно нарисовать внутреннюю тень с помощью Core Graphics, сделав большой прямоугольный путь за пределами границ, вычтя прямоугольный путь размером с границы и заполнив полученный путь «нормальной» тенью.

Однако, поскольку вам нужно объединить его с градиентным слоем, я думаю, что более простое решение - создать прозрачное PNG-изображение внутренней тени из 9 частей и растянуть его до нужного размера. Теневое изображение из 9 частей будет выглядеть так (его размер 21x21 пикселей):

alt text

CALayer *innerShadowLayer = [CALayer layer];
innerShadowLayer.contents = (id)[UIImage imageNamed: @"innershadow.png"].CGImage;
innerShadowLayer.contentsCenter = CGRectMake(10.0f/21.0f, 10.0f/21.0f, 1.0f/21.0f, 1.0f/21.0f);

Затем установите рамку innerShadowLayer, и она должна правильно растянуть тень.

26 голосов
/ 15 июня 2015

Упрощенная версия, использующая только CALayer, в Swift:

import UIKit

final class FrameView : UIView {
    init() {
        super.init(frame: CGRect.zero)
        backgroundColor = UIColor.white
    }

    @available(*, unavailable)
    required init?(coder decoder: NSCoder) { fatalError("unavailable") }

    override func layoutSubviews() {
        super.layoutSubviews()
        addInnerShadow()
    }

    private func addInnerShadow() {
        let innerShadow = CALayer()
        innerShadow.frame = bounds
        // Shadow path (1pt ring around bounds)
        let path = UIBezierPath(rect: innerShadow.bounds.insetBy(dx: -1, dy: -1))
        let cutout = UIBezierPath(rect: innerShadow.bounds).reversing()
        path.append(cutout)
        innerShadow.shadowPath = path.cgPath
        innerShadow.masksToBounds = true
        // Shadow properties
        innerShadow.shadowColor = UIColor(white: 0, alpha: 1).cgColor // UIColor(red: 0.71, green: 0.77, blue: 0.81, alpha: 1.0).cgColor
        innerShadow.shadowOffset = CGSize.zero
        innerShadow.shadowOpacity = 1
        innerShadow.shadowRadius = 3
        // Add
        layer.addSublayer(innerShadow)
    }
}

Обратите внимание, что слой innerShadow не должен иметь непрозрачный цвет фона, поскольку он будет визуализироваться перед тенью.

23 голосов
/ 01 июня 2011

Немного обойдется, но при этом не нужно использовать изображения (читай: легко менять цвета, радиус тени и т. Д.), И это всего лишь несколько строк кода.

  1. Добавьте UIImageView в качестве первого подпредставления UIView, на котором вы хотите создать тень.Я использую IB, но вы можете сделать то же самое программно.

  2. При условии, что ссылка на UIImageView - 'innerShadow'

`

[[innerShadow layer] setMasksToBounds:YES];
[[innerShadow layer] setCornerRadius:12.0f];        
[[innerShadow layer] setBorderColor:[UIColorFromRGB(180, 180, 180) CGColor]];
[[innerShadow layer] setBorderWidth:1.0f];
[[innerShadow layer] setShadowColor:[UIColorFromRGB(0, 0, 0) CGColor]];
[[innerShadow layer] setShadowOffset:CGSizeMake(0, 0)];
[[innerShadow layer] setShadowOpacity:1];
[[innerShadow layer] setShadowRadius:2.0];

Предостережение: у вас должна быть граница, иначе тень не появится.[UIColor clearColor] не работает.В примере я использую другой цвет, но вы можете возиться с ним, чтобы получить тот же цвет, что и начало тени.:)

См. Комментарий bbrame ниже о макросе UIColorFromRGB.

17 голосов
/ 25 февраля 2014

Лучше поздно, чем никогда ...

Вот другой подход, вероятно, не лучше, чем уже опубликованный, но он приятный и простой -

-(void)drawInnerShadowOnView:(UIView *)view
{
    UIImageView *innerShadowView = [[UIImageView alloc] initWithFrame:view.bounds];

    innerShadowView.contentMode = UIViewContentModeScaleToFill;
    innerShadowView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;

    [view addSubview:innerShadowView];

    [innerShadowView.layer setMasksToBounds:YES];

    [innerShadowView.layer setBorderColor:[UIColor lightGrayColor].CGColor];
    [innerShadowView.layer setShadowColor:[UIColor blackColor].CGColor];
    [innerShadowView.layer setBorderWidth:1.0f];

    [innerShadowView.layer setShadowOffset:CGSizeMake(0, 0)];
    [innerShadowView.layer setShadowOpacity:1.0];

    // this is the inner shadow thickness
    [innerShadowView.layer setShadowRadius:1.5];
}
8 голосов
/ 23 октября 2013

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

innerShadowOwnerLayer = [[CALayer alloc]init];
innerShadowOwnerLayer.frame = CGRectMake(0, V.frame.size.height+2, V.frame.size.width, 2);
innerShadowOwnerLayer.backgroundColor = [UIColor whiteColor].CGColor;

innerShadowOwnerLayer.shadowColor = [UIColor blackColor].CGColor;
innerShadowOwnerLayer.shadowOffset = CGSizeMake(0, 0);
innerShadowOwnerLayer.shadowRadius = 10.0;
innerShadowOwnerLayer.shadowOpacity = 0.7;

[V.layer addSubLayer:innerShadowOwnerLayer];

Это добавит нижнюю внутреннюю тень для целевого UIView

6 голосов
/ 14 октября 2015

Вот версия swift, измените startPoint и endPoint, чтобы сделать ее на каждой стороне.

        let layer = CAGradientLayer()
        layer.startPoint    = CGPointMake(0.5, 0.0);
        layer.endPoint      = CGPointMake(0.5, 1.0);
        layer.colors        = [UIColor(white: 0.1, alpha: 1.0).CGColor, UIColor(white: 0.1, alpha: 0.5).CGColor, UIColor.clearColor().CGColor]
        layer.locations     = [0.05, 0.2, 1.0 ]
        layer.frame         = CGRectMake(0, 0, self.view.frame.width, 60)
        self.view.layer.insertSublayer(layer, atIndex: 0)
5 голосов
/ 16 октября 2014

Это ваше решение, которое я экспортировал из PaintCode :

-(void) drawRect:(CGRect)rect
{
    CGContextRef context = UIGraphicsGetCurrentContext();

    //// Shadow Declarations
    UIColor* shadow = UIColor.whiteColor;
    CGSize shadowOffset = CGSizeMake(0, 0);
    CGFloat shadowBlurRadius = 10;

    //// Rectangle Drawing
    UIBezierPath* rectanglePath = [UIBezierPath bezierPathWithRect: self.bounds];
    [[UIColor blackColor] setFill];
    [rectanglePath fill];

    ////// Rectangle Inner Shadow
    CGContextSaveGState(context);
    UIRectClip(rectanglePath.bounds);
    CGContextSetShadowWithColor(context, CGSizeZero, 0, NULL);

    CGContextSetAlpha(context, CGColorGetAlpha([shadow CGColor]));
    CGContextBeginTransparencyLayer(context, NULL);
    {
        UIColor* opaqueShadow = [shadow colorWithAlphaComponent: 1];
        CGContextSetShadowWithColor(context, shadowOffset, shadowBlurRadius, [opaqueShadow CGColor]);
        CGContextSetBlendMode(context, kCGBlendModeSourceOut);
        CGContextBeginTransparencyLayer(context, NULL);

        [opaqueShadow setFill];
        [rectanglePath fill];

        CGContextEndTransparencyLayer(context);
    }
    CGContextEndTransparencyLayer(context);
    CGContextRestoreGState(context);
}
3 голосов
/ 08 августа 2018

Вот мое решение в Swift 4.2. Хотите попробовать?

final class ACInnerShadowLayer : CAShapeLayer {

  var innerShadowColor: CGColor? = UIColor.black.cgColor {
    didSet { setNeedsDisplay() }
  }

  var innerShadowOffset: CGSize = .zero {
    didSet { setNeedsDisplay() }
  }

  var innerShadowRadius: CGFloat = 8 {
    didSet { setNeedsDisplay() }
  }

  var innerShadowOpacity: Float = 1 {
    didSet { setNeedsDisplay() }
  }

  override init() {
    super.init()

    masksToBounds = true
    contentsScale = UIScreen.main.scale

    setNeedsDisplay()
  }

  override init(layer: Any) {
      if let layer = layer as? InnerShadowLayer {
          innerShadowColor = layer.innerShadowColor
          innerShadowOffset = layer.innerShadowOffset
          innerShadowRadius = layer.innerShadowRadius
          innerShadowOpacity = layer.innerShadowOpacity
      }
      super.init(layer: layer)
  }

  required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

  override func draw(in ctx: CGContext) {
    ctx.setAllowsAntialiasing(true)
    ctx.setShouldAntialias(true)
    ctx.interpolationQuality = .high

    let colorspace = CGColorSpaceCreateDeviceRGB()

    var rect = bounds
    var radius = cornerRadius

    if borderWidth != 0 {
      rect = rect.insetBy(dx: borderWidth, dy: borderWidth)
      radius -= borderWidth
      radius = max(radius, 0)
    }

    let innerShadowPath = UIBezierPath(roundedRect: rect, cornerRadius: radius).cgPath
    ctx.addPath(innerShadowPath)
    ctx.clip()

    let shadowPath = CGMutablePath()
    let shadowRect = rect.insetBy(dx: -rect.size.width, dy: -rect.size.width)
    shadowPath.addRect(shadowRect)
    shadowPath.addPath(innerShadowPath)
    shadowPath.closeSubpath()

    if let innerShadowColor = innerShadowColor, let oldComponents = innerShadowColor.components {
      var newComponets = Array<CGFloat>(repeating: 0, count: 4) // [0, 0, 0, 0] as [CGFloat]
      let numberOfComponents = innerShadowColor.numberOfComponents

      switch numberOfComponents {
      case 2:
        newComponets[0] = oldComponents[0]
        newComponets[1] = oldComponents[0]
        newComponets[2] = oldComponents[0]
        newComponets[3] = oldComponents[1] * CGFloat(innerShadowOpacity)
      case 4:
        newComponets[0] = oldComponents[0]
        newComponets[1] = oldComponents[1]
        newComponets[2] = oldComponents[2]
        newComponets[3] = oldComponents[3] * CGFloat(innerShadowOpacity)
      default:
        break
      }

      if let innerShadowColorWithMultipliedAlpha = CGColor(colorSpace: colorspace, components: newComponets) {
        ctx.setFillColor(innerShadowColorWithMultipliedAlpha)
        ctx.setShadow(offset: innerShadowOffset, blur: innerShadowRadius, color: innerShadowColorWithMultipliedAlpha)
        ctx.addPath(shadowPath)
        ctx.fillPath(using: .evenOdd)
      }
    } 
  }
}
...