Effet d'ombre intérieure sur le calque UIView?

92

J'ai le CALayer suivant:

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];

Je voudrais y ajouter un effet d' ombre intérieure , mais je ne sais pas trop comment procéder. Je suppose que je serais obligé de dessiner dans drawRect, mais cela ajouterait le calque au-dessus des autres objets UIView, car il est censé être une barre derrière certains boutons, donc je ne sais pas quoi faire?

Je pourrais ajouter un autre calque, mais encore une fois, je ne sais pas comment obtenir l'effet d'ombre interne (comme ceci:

entrez la description de l'image ici

Aide appréciée ...

runmad
la source

Réponses:

108

Pour quiconque se demande comment dessiner une ombre intérieure en utilisant Core Graphics selon la suggestion de Costique, voici comment: (sur iOS, ajustez au besoin)

Dans votre drawRect: method ...

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);

Donc, il y a essentiellement les étapes suivantes:

  1. Créez votre chemin
  2. Définissez la couleur de remplissage souhaitée, ajoutez ce chemin au contexte et remplissez le contexte
  3. Créez maintenant un rectangle plus grand qui peut délimiter le chemin visible. Avant de fermer ce chemin, ajoutez le chemin visible. Fermez ensuite le tracé afin de créer une forme dont le tracé visible en est soustrait. Vous voudrez peut-être étudier les méthodes de remplissage (enroulement non nul de pair / impair) en fonction de la façon dont vous avez créé ces chemins. Essentiellement, pour obtenir les sous-chemins à «soustraire» lorsque vous les additionnez, vous devez les dessiner (ou plutôt les construire) dans des directions opposées, l'une dans le sens des aiguilles d'une montre et l'autre dans le sens inverse.
  4. Ensuite, vous devez définir votre chemin visible comme chemin de détourage sur le contexte, afin de ne rien dessiner en dehors de celui-ci à l'écran.
  5. Ensuite, configurez l'ombre sur le contexte, qui comprend le décalage, le flou et la couleur.
  6. Remplissez ensuite la grande forme avec le trou dedans. La couleur n'a pas d'importance, car si vous avez tout fait correctement, vous ne verrez pas cette couleur, juste l'ombre.
Daniel Thorpe
la source
Merci, mais est-il possible d'ajuster le rayon? Il est actuellement basé sur les limites, mais j'aimerais plutôt basé sur un rayon défini (comme 5.0f). Avec le code ci-dessus, c'est beaucoup trop arrondi.
runmad
2
@runmad Eh bien, vous pouvez créer n'importe quelle sorte de CGPath visible que vous voulez, l'exemple utilisé ici est juste un exemple, choisi par souci de concision. Si vous souhaitez créer un rect arrondi, vous pouvez simplement faire quelque chose comme: CGPath visiblePath = [UIBezierPath bezierPathWithRoundedRect: rect cornerRadius: radius] .CGPath J'espère que cela vous aidera.
Daniel Thorpe
4
@DanielThorpe: +1 pour la bonne réponse. J'ai corrigé le code de chemin rect arrondi (le vôtre s'est cassé lors du changement de rayon) et simplifié le code de chemin rect externe. J'espère que cela ne vous dérange pas.
Regexident
Comment puis-je configurer correctement l'ombre intérieure à partir de 4 directions, pas seulement 2?
Protocole
@Protocole vous pouvez définir le décalage sur {0,0}, mais utilisez un rayon d'ombre de, par exemple, 4.f.
Daniel Thorpe
47

Je sais que je suis en retard à cette soirée, mais cela m'aurait aidé à trouver tôt dans mes voyages ...

Pour donner du crédit là où le mérite est dû, il s'agit essentiellement d'une modification de l'élaboration de Daniel Thorpe sur la solution de Costique consistant à soustraire une région plus petite d'une région plus grande. Cette version est destinée à ceux qui utilisent la composition des calques au lieu de remplacer-drawRect:

La CAShapeLayerclasse peut être utilisée pour obtenir le même effet:

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];

À ce stade, si votre calque parent ne masque pas ses limites, vous verrez la zone supplémentaire du calque de masque autour des bords du calque. Ce sera 42 pixels de noir si vous venez de copier directement l'exemple. Pour vous en débarrasser, vous pouvez simplement en utiliser un autre CAShapeLayeravec le même chemin et le définir comme masque du calque d'ombre:

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

Je n'ai pas évalué cela moi-même, mais je soupçonne que l'utilisation de cette approche en conjonction avec la rastérisation est plus performante que la substitution -drawRect:.

Matt Wilding
la source
3
someInnerPath? Pourriez-vous expliquer cela un peu plus s'il vous plaît.
Moe
4
@Moe Cela peut être n'importe quel CGPath arbitraire que vous voulez. [[UIBezierPath pathWithRect:[shadowLayer bounds]] CGPath]étant le choix le plus simple.
Matt Wilding
Cheers for that Matt :-)
Moe
J'obtiens un rectangle noir (extérieur) pour le shadowLayer.path qui dessine correctement l'ombre intérieure. Comment puis-je m'en débarrasser (le rectangle extérieur noir)? On dirait que vous ne pouvez définir le fillColor que dans un contexte et vous n'en utilisez pas.
Olivier
11
Cela fonctionne très bien! J'ai téléchargé sur github avec quelques ajouts. Essayez :) github.com/inamiy/YIInnerShadowView
inamiy
35

Il est possible de dessiner une ombre intérieure avec Core Graphics en créant un grand chemin rectangle en dehors des limites, en soustrayant un chemin rectangle de la taille des limites et en remplissant le chemin résultant avec une ombre "normale" activée.

Cependant, comme vous devez le combiner avec un calque de dégradé, je pense qu'une solution plus simple consiste à créer une image PNG transparente en 9 parties de l'ombre intérieure et à l'étirer à la bonne taille. L'image d'ombre en 9 parties ressemblerait à ceci (sa taille est de 21x21 pixels):

texte alternatif

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);

Ensuite, définissez le cadre de innerShadowLayer et il devrait étirer correctement l'ombre.

Costique
la source
Ouais, je suppose que tu as raison. Je voulais juste que la couche soit aussi plate que possible. Je pourrais créer l'image dans Photoshop avec l'ombre intérieure et l'aspect dégradé, j'ai juste des problèmes avec les couleurs correspondant à 100% sur l'appareil lors de l'utilisation d'une image.
runmad
Oui, c'est un problème avec tous les dégradés et ombres, je ne peux tout simplement pas reproduire ces effets Photoshop 1: 1 sur iOS, comme j'essaye.
Costique
29

Une version simplifiée utilisant juste un CALayer, dans 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)
    }
}

Notez que le calque innerShadow ne doit pas avoir une couleur d'arrière-plan opaque, car elle sera rendue devant l'ombre.

Patrick Pijnappel
la source
La dernière ligne contient «layer». D'où est-ce que ça vient?
Charlie Seligman
@CharlieSeligman C'est le calque parent, qui peut être n'importe quel calque. Vous pouvez utiliser un calque personnalisé ou le calque de la vue (UIView a une propriété de calque).
Patrick Pijnappel
devrait être let innerShadow = CALayer(); innerShadow.frame = bounds. Sans limites appropriées, il ne dessinerait pas l'ombre appropriée. Merci quand même
haik.ampardjian
@noir_eagle Vrai, bien que vous souhaitiez probablement le configurer layoutSubviews()pour le garder synchronisé
Patrick Pijnappel
Droite! Soit dans layoutSubviews()ou dansdraw(_ rect)
haik.ampardjian
24

Un peu rond, mais cela évite d'avoir à utiliser des images (lire: facile à changer les couleurs, le rayon de l'ombre, etc.) et ce n'est que quelques lignes de code.

  1. Ajoutez un UIImageView en tant que première sous-vue de l'UIView sur laquelle vous voulez que le dropshadow. J'utilise IB, mais vous pouvez faire la même chose par programmation.

  2. En supposant que la référence à UIImageView est '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];

Attention: vous devez avoir une bordure, sinon l'ombre n'apparaît pas. [UIColor clearColor] ne fonctionne pas. Dans l'exemple, j'utilise une couleur différente, mais vous pouvez la modifier pour qu'elle ait la même couleur que le début de l'ombre. :)

Voir le commentaire de bbrame ci-dessous sur la UIColorFromRGBmacro.

jinglesthula
la source
Je l'ai laissé de côté, mais je suppose que vous le feriez dans le cadre de l'ajout de la vue d'image - assurez-vous de définir le cadre sur le même droit que l'UIView parent. Si vous utilisez IB, définissez les entretoises et les ressorts à droite pour avoir la taille de l'ombre avec la vue si vous souhaitez modifier le cadre de la vue parent. Dans le code, il devrait y avoir un masque de redimensionnement que vous pouvez OU faire de même, AFAIK.
jinglesthula
C'est le moyen le plus simple maintenant, mais sachez que les méthodes d'ombre CALayer ne sont disponibles que dans iOS 3.2 et versions ultérieures. Je supporte 3.1, donc j'entoure la définition de ces attributs dans un if ([layer respondsToSelector: @selector (setShadowColor :)]) {
DougW
Cela ne semble pas fonctionner pour moi. Au moins sur xcode 4.2 et ios simulator 4.3. Pour faire apparaître l'ombre, je dois ajouter une couleur d'arrière-plan ... à quel point l'ombre tombante n'apparaît qu'à l'extérieur.
Andrea
@Andrea - gardez à l'esprit la mise en garde que j'ai mentionnée ci-dessus. Je pense qu'une couleur d'arrière-plan ou une bordure pourrait avoir le même effet de «lui donner quelque chose à ajouter à l'ombre». Quant à son apparition à l'extérieur, si l'UIImageView n'est pas une sous-vue de celle sur laquelle vous voulez l'ombre intérieure, c'est peut-être ça - je devrais regarder votre code pour voir.
jinglesthula
Juste pour rectifier ma déclaration précédente ... le code fonctionne réellement ... il me manquait quelque chose mais malheureusement je ne m'en souviens pas pour le moment. :) Alors ... merci d'avoir partagé cet extrait de code.
Andrea
17

Mieux vaut tard que jamais...

Voici une autre approche, probablement pas meilleure que celles déjà publiées, mais c'est sympa et simple -

-(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];
}
SomaMan
la source
@SomaMan est-il possible de définir l'ombre avec un côté spécifique uniquement? Comme seulement en haut ou en haut / en bas ou en haut / droite etc.
Mitesh Dobareeya
8

Au lieu de dessiner une ombre intérieure par drawRect ou ajoutez UIView à la vue. Vous pouvez directement Ajouter CALayer à la bordure, par exemple: si je veux un effet d'ombre intérieure sur le bas de 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];

Cela ajoute une ombre intérieure inférieure pour UIView cible

Jerry Zhang
la source
6

Voici une version de swift, changez startPointet endPointfaites-le de chaque côté.

        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)
William Hu
la source
A travaillé pour moi !! Je vous remercie.
iUser
5

Voici votre solution, que j'ai exportée depuis 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);
}
Dmitriy Kirakosyan
la source
3

Je suis très en retard à la fête mais je voudrais redonner à la communauté .. C'est une méthode que j'ai écrite pour supprimer l'image d'arrière-plan UITextField car je fournissais une bibliothèque statique et AUCUNE ressource ... J'ai utilisé ceci pour un écran de saisie de code PIN de quatre instances UITextField pouvant afficher un caractère brut ou (BOOL) [self isUsingBullets] ou (BOOL) [self usingAsterisks] dans ViewController. L'application est pour iPhone / iPhone Retina / iPad / iPad Retina donc je n'ai pas à fournir quatre images ...

#import <QuartzCore/QuartzCore.h>

- (void)setTextFieldInnerGradient:(UITextField *)textField
{

    [textField setSecureTextEntry:self.isUsingBullets];
    [textField setBackgroundColor:[UIColor blackColor]];
    [textField setTextColor:[UIColor blackColor]];
    [textField setBorderStyle:UITextBorderStyleNone];
    [textField setClipsToBounds:YES];

    [textField.layer setBorderColor:[[UIColor blackColor] CGColor]];
    [textField.layer setBorderWidth:1.0f];

    // make a gradient off-white background
    CAGradientLayer *gradient = [CAGradientLayer layer];
    CGRect gradRect = CGRectInset([textField bounds], 3, 3);    // Reduce Width and Height and center layer
    gradRect.size.height += 2;  // minimise Bottom shadow, rely on clipping to remove these 2 pts.

    gradient.frame = gradRect;
    struct CGColor *topColor = [UIColor colorWithWhite:0.6f alpha:1.0f].CGColor;
    struct CGColor *bottomColor = [UIColor colorWithWhite:0.9f alpha:1.0f].CGColor;
    // We need to use this fancy __bridge object in order to get the array we want.
    gradient.colors = [NSArray arrayWithObjects:(__bridge id)topColor, (__bridge id)bottomColor, nil];
    [gradient setCornerRadius:4.0f];
    [gradient setShadowOffset:CGSizeMake(0, 0)];
    [gradient setShadowColor:[[UIColor whiteColor] CGColor]];
    [gradient setShadowOpacity:1.0f];
    [gradient setShadowRadius:3.0f];

    // Now we need to Blur the edges of this layer "so it blends"
    // This rasterizes the view down to 4x4 pixel chunks then scales it back up using bilinear filtering...
    // it's EXTREMELY fast and looks ok if you are just wanting to blur a background view under a modal view.
    // To undo it, just set the rasterization scale back to 1.0 or turn off rasterization.
    [gradient setRasterizationScale:0.25];
    [gradient setShouldRasterize:YES];

    [textField.layer insertSublayer:gradient atIndex:0];

    if (self.usingAsterisks) {
        [textField setFont:[UIFont systemFontOfSize:80.0]];
    } else {
        [textField setFont:[UIFont systemFontOfSize:40.0]];
    }
    [textField setTextAlignment:UITextAlignmentCenter];
    [textField setEnabled:NO];
}

J'espère que cela aide quelqu'un car ce forum m'a aidé.

Foxnolds
la source
3

Consultez l'excellent article de Inner Shadows in Quartz de Chris Emery qui explique comment les ombres intérieures sont dessinées par PaintCode et donne un extrait de code propre et soigné:

- (void)drawInnerShadowInContext:(CGContextRef)context
                        withPath:(CGPathRef)path
                     shadowColor:(CGColorRef)shadowColor
                          offset:(CGSize)offset
                      blurRadius:(CGFloat)blurRadius 
{
    CGContextSaveGState(context);

    CGContextAddPath(context, path);
    CGContextClip(context);

    CGColorRef opaqueShadowColor = CGColorCreateCopyWithAlpha(shadowColor, 1.0);

    CGContextSetAlpha(context, CGColorGetAlpha(shadowColor));
    CGContextBeginTransparencyLayer(context, NULL);
        CGContextSetShadowWithColor(context, offset, blurRadius, opaqueShadowColor);
        CGContextSetBlendMode(context, kCGBlendModeSourceOut);
        CGContextSetFillColorWithColor(context, opaqueShadowColor);
        CGContextAddPath(context, path);
        CGContextFillPath(context);
    CGContextEndTransparencyLayer(context);

    CGContextRestoreGState(context);

    CGColorRelease(opaqueShadowColor);
}
voiger
la source
3

Voici ma solution dans Swift 4.2. Voudriez-vous essayer?

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)
      }
    } 
  }
}
Arco
la source
Et si je ne l'utilise pas comme une classe séparée, mais comme dans mon code, le contexte (ctx) est nul quand j'obtiens ceci:let ctx = UIGraphicsGetCurrentContext
Mohsin Khubaib Ahmed
@MohsinKhubaibAhmed Vous pouvez obtenir le contexte actuel par méthode UIGraphicsGetCurrentContext à récupérer lorsque certaines vues poussent leur contexte sur la pile.
Arco
@Arco J'ai eu des problèmes lorsque j'ai fait pivoter l'appareil. J'ai ajouté 'override commodité init (layer: Any) {self.init ()}'. Maintenant, aucune erreur ne s'affiche!
Yuma Technical Inc.
Ajout de init (layer: Any) pour corriger le crash.
Nik Kov
2

Solution évolutive utilisant CALayer dans Swift

Avec le décrit InnerShadowLayer vous pouvez également activer les ombres intérieures pour des arêtes spécifiques uniquement, à l'exclusion des autres. (par exemple, vous pouvez activer les ombres intérieures uniquement sur les bords gauche et supérieur de votre vue)

Vous pouvez ensuite ajouter un InnerShadowLayerà votre vue en utilisant:

init(...) {

    // ... your initialization code ...

    super.init(frame: .zero)
    layer.addSublayer(shadowLayer)
}

public override func layoutSubviews() {
    super.layoutSubviews()
    shadowLayer.frame = bounds
}

InnerShadowLayer la mise en oeuvre

/// Shadow is a struct defining the different kinds of shadows
public struct Shadow {
    let x: CGFloat
    let y: CGFloat
    let blur: CGFloat
    let opacity: CGFloat
    let color: UIColor
}

/// A layer that applies an inner shadow to the specified edges of either its path or its bounds
public class InnerShadowLayer: CALayer {
    private let shadow: Shadow
    private let edge: UIRectEdge

    public init(shadow: Shadow, edge: UIRectEdge) {
        self.shadow = shadow
        self.edge = edge
        super.init()
        setupShadow()
    }

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

    public override func layoutSublayers() {
        updateShadow()
    }

    private func setupShadow() {
        shadowColor = shadow.color.cgColor
        shadowOpacity = Float(shadow.opacity)
        shadowRadius = shadow.blur / 2.0
        masksToBounds = true
    }

    private func updateShadow() {
        shadowOffset = {
            let topWidth: CGFloat = 0
            let leftWidth = edge.contains(.left) ? shadow.y / 2 : 0
            let bottomWidth: CGFloat = 0
            let rightWidth = edge.contains(.right) ? -shadow.y / 2 : 0

            let topHeight = edge.contains(.top) ? shadow.y / 2 : 0
            let leftHeight: CGFloat = 0
            let bottomHeight = edge.contains(.bottom) ? -shadow.y / 2 : 0
            let rightHeight: CGFloat = 0

            return CGSize(width: [topWidth, leftWidth, bottomWidth, rightWidth].reduce(0, +),
                          height: [topHeight, leftHeight, bottomHeight, rightHeight].reduce(0, +))
        }()

        let insets = UIEdgeInsets(top: edge.contains(.top) ? -bounds.height : 0,
                                  left: edge.contains(.left) ? -bounds.width : 0,
                                  bottom: edge.contains(.bottom) ? -bounds.height : 0,
                                  right: edge.contains(.right) ? -bounds.width : 0)
        let path = UIBezierPath(rect: bounds.inset(by: insets))
        let cutout = UIBezierPath(rect: bounds).reversing()
        path.append(cutout)
        shadowPath = path.cgPath
    }
}
Pietro Basso
la source
1

ce code a fonctionné pour moi

class InnerDropShadowView: UIView {
    override func draw(_ rect: CGRect) {
        //Drawing code
        let context = UIGraphicsGetCurrentContext()
        //// Shadow Declarations
        let shadow: UIColor? = UIColor.init(hexString: "a3a3a3", alpha: 1.0) //UIColor.black.withAlphaComponent(0.6) //UIColor.init(hexString: "d7d7da", alpha: 1.0)
        let shadowOffset = CGSize(width: 0, height: 0)
        let shadowBlurRadius: CGFloat = 7.5
        //// Rectangle Drawing
        let rectanglePath = UIBezierPath(rect: bounds)
        UIColor.groupTableViewBackground.setFill()
        rectanglePath.fill()
        ////// Rectangle Inner Shadow
        context?.saveGState()
        UIRectClip(rectanglePath.bounds)
        context?.setShadow(offset: CGSize.zero, blur: 0, color: nil)
        context?.setAlpha((shadow?.cgColor.alpha)!)
        context?.beginTransparencyLayer(auxiliaryInfo: nil)
        do {
            let opaqueShadow: UIColor? = shadow?.withAlphaComponent(1)
            context?.setShadow(offset: shadowOffset, blur: shadowBlurRadius, color: opaqueShadow?.cgColor)
            context!.setBlendMode(.sourceOut)
            context?.beginTransparencyLayer(auxiliaryInfo: nil)
            opaqueShadow?.setFill()
            rectanglePath.fill()
            context!.endTransparencyLayer()
        }
        context!.endTransparencyLayer()
        context?.restoreGState()
    }
}
Antony Raphel
la source
0

Il y a du code ici qui peut le faire pour vous. Si vous changez le calque dans votre vue (en + (Class)layerClassremplaçant), en JTAInnerShadowLayer, vous pouvez définir l'ombre interne sur le calque de retrait dans votre méthode init et il fera le travail pour vous. Si vous souhaitez également dessiner le contenu d'origine, assurez-vous d'appeler setDrawOriginalImage:yesle calque de retrait. Il y a un article de blog sur la façon dont cela fonctionne ici .

James Snook
la source
@MiteshDobareeya Je viens de tester les deux liens et ils semblent fonctionner correctement (y compris dans un onglet privé). Quel lien vous causait des problèmes?
James Snook
Pouvez-vous s'il vous plaît regarder cette implémentation du code shadow interne. Cela ne fonctionne que dans la méthode ViewDidAppear. Et montre un scintillement. drive.google.com/open?id=1VtCt7UFYteq4UteT0RoFRjMfFnbibD0E
Mitesh Dobareeya
0

Utilisation de la couche de dégradé:

UIView * mapCover = [UIView new];
mapCover.frame = map.frame;
[view addSubview:mapCover];

CAGradientLayer * vertical = [CAGradientLayer layer];
vertical.frame = mapCover.bounds;
vertical.colors = [NSArray arrayWithObjects:(id)[UIColor whiteColor].CGColor,
                        (id)[[UIColor whiteColor] colorWithAlphaComponent:0.0f].CGColor,
                        (id)[[UIColor whiteColor] colorWithAlphaComponent:0.0f].CGColor,
                        (id)[UIColor whiteColor].CGColor, nil];
vertical.locations = @[@0.01,@0.1,@0.9,@0.99];
[mapCover.layer insertSublayer:vertical atIndex:0];

CAGradientLayer * horizontal = [CAGradientLayer layer];
horizontal.frame = mapCover.bounds;
horizontal.colors = [NSArray arrayWithObjects:(id)[UIColor whiteColor].CGColor,
                     (id)[[UIColor whiteColor] colorWithAlphaComponent:0.0f].CGColor,
                     (id)[[UIColor whiteColor] colorWithAlphaComponent:0.0f].CGColor,
                     (id)[UIColor whiteColor].CGColor, nil];
horizontal.locations = @[@0.01,@0.1,@0.9,@0.99];
horizontal.startPoint = CGPointMake(0.0, 0.5);
horizontal.endPoint = CGPointMake(1.0, 0.5);
[mapCover.layer insertSublayer:horizontal atIndex:0];
Johnny Rockex
la source
0

Il existe une solution simple: dessinez simplement l'ombre normale et faites-la pivoter, comme ceci

@objc func shadowView() -> UIView {
        let shadowView = UIView(frame: .zero)
        shadowView.backgroundColor = .white
        shadowView.layer.shadowColor = UIColor.grey.cgColor
        shadowView.layer.shadowOffset = CGSize(width: 0, height: 2)
        shadowView.layer.shadowOpacity = 1.0
        shadowView.layer.shadowRadius = 4
        shadowView.layer.compositingFilter = "multiplyBlendMode"
        return shadowView
    }

func idtm_addBottomShadow() {
        let shadow = shadowView()
        shadow.transform = transform.rotated(by: 180 * CGFloat(Double.pi))
        shadow.transform = transform.rotated(by: -1 * CGFloat(Double.pi))
        shadow.translatesAutoresizingMaskIntoConstraints = false
        addSubview(shadow)
        NSLayoutConstraint.activate([
            shadow.leadingAnchor.constraint(equalTo: leadingAnchor),
            shadow.trailingAnchor.constraint(equalTo: trailingAnchor),
            shadow.bottomAnchor.constraint(equalTo: bottomAnchor),
            shadow.heightAnchor.constraint(equalToConstant: 1),
            ])
    }
iago849
la source