使用渐变和投影绘制圆角UIView

14

编辑:

我最终找到了一个非常简单的解决方案,使用CAGradientLayer类和CALayer绘图功能。
Ole Begemann发布了一个名为OBGradientView的优秀UIView包装器,用于CAGradientLayer类。
这个类允许您在应用程序中轻松创建渐变UIView。
然后,您可以使用CALayer绘图功能添加圆角和阴影值:

// Create the gradient view
OBGradientView *gradient = [[OBGradientView alloc] initWithFrame:someRect];
NSArray *colors = [NSArray arrayWithObjects:[UIColor redColor], [UIColor yellowColor], nil];
gradient.colors = colors;

// Set rounded corners and drop shadow
gradient.layer.cornerRadius = 5.0;
gradient.layer.shadowColor = [UIColor grayColor].CGColor;
gradient.layer.shadowOpacity = 1.0;
gradient.layer.shadowOffset = CGSizeMake(2.0, 2.0);
gradient.layer.shadowRadius = 3.0;

[self.view addSubview:gradient];
[gradient release];
不要忘记将 QuartzCore 框架添加到您的项目中。



原问题:

我正在开发一个自定义控件,它是一个圆角矩形按钮,填充有线性渐变,并带有阴影。 我已经通过这个答案完成了前两个步骤:链接文本

我的问题现在是如何在生成的形状下添加阴影。 实际上,上下文已被剪切为圆角矩形路径,因此当我使用 CGContextSetShadow 函数时,它不会绘制它。

我尝试通过两次绘制圆角矩形来解决这个问题,首先用纯色绘制它,以便绘制阴影,然后再用渐变填充重新绘制它。

它有点效果,但仍然可以看到一些像素位于第一次纯色绘制的形状的角落处,如下图所示:

http://img269.imageshack.us/img269/6489/capturedcran20100701192.png

它几乎很好,但还不完美...

这是我的 -drawRect: 实现:

static void addRoundedRectToPath(CGContextRef context, CGRect rect, float ovalWidth, float ovalHeight)
{
 float fw, fh;

 if (ovalWidth == 0 || ovalHeight == 0) {
  CGContextAddRect(context, rect);
  return;
 }
 CGContextSaveGState(context);
 CGContextTranslateCTM (context, CGRectGetMinX(rect), CGRectGetMinY(rect));
 CGContextScaleCTM (context, ovalWidth, ovalHeight);
 fw = CGRectGetWidth (rect) / ovalWidth;
 fh = CGRectGetHeight (rect) / ovalHeight;
 CGContextMoveToPoint(context, fw, fh/2);
 CGContextAddArcToPoint(context, fw, fh, fw/2, fh, 1);
 CGContextAddArcToPoint(context, 0, fh, 0, fh/2, 1);
 CGContextAddArcToPoint(context, 0, 0, fw/2, 0, 1);
 CGContextAddArcToPoint(context, fw, 0, fw, fh/2, 1);
 CGContextClosePath(context);
 CGContextRestoreGState(context);
}


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

 CGSize shadowOffset = CGSizeMake(10.0, 10.0);
 CGFloat blur = 5.0;

 rect.size.width -= shadowOffset.width + blur;
 rect.size.height -= shadowOffset.height + blur;

 CGContextSaveGState(context);
 addRoundedRectToPath(context, rect, _radius, _radius);
 CGContextSetShadow (context, shadowOffset, blur);
 CGContextDrawPath(context, kCGPathFill);
 CGContextRestoreGState(context);

 addRoundedRectToPath(context, rect, _radius, _radius);
    CGContextClip(context);

 CGFloat colors[] =
 {
  _gradientStartColor.red, _gradientStartColor.green, _gradientStartColor.blue, _gradientStartColor.alpha,
  _gradientEndColor.red, _gradientEndColor.green, _gradientEndColor.blue, _gradientEndColor.alpha
 };
 size_t num_locations = 2;
    CGFloat locations[2] = { 0.0, 1.0 };

 CGColorSpaceRef rgb = CGColorSpaceCreateDeviceRGB();
 CGGradientRef gradient = CGGradientCreateWithColorComponents(rgb, colors, locations, num_locations);

 CGRect currentBounds = self.bounds;
 CGPoint gStartPoint = CGPointMake(CGRectGetMidX(currentBounds), 0.0f);
 CGPoint gEndPoint = CGPointMake(CGRectGetMidX(currentBounds), CGRectGetMaxY(currentBounds));
 CGContextDrawLinearGradient(context, gradient, gStartPoint, gEndPoint, 0);

 CGColorSpaceRelease(rgb);
 CGGradientRelease(gradient);
}

有没有其他方法来实现这个?

谢谢!

4个回答

24
为了创建一个带有渐变背景和阴影的圆角视图,我所做的是:
第一部分与问题中提供的非常相似,它使用CGPathAddArcToPoint创建了一个圆角矩形路径,如这篇文章中所描述的那样。下面是一张图片帮助我理解它: alt text 第二部分的工作流程如下:
在图形上下文中启用阴影,添加刚刚定义的路径,然后填充该路径。你无法仅将阴影应用于路径本身(路径不是图形状态的一部分),因此必须填充路径以使阴影出现(我想描边路径也可能有效果?)。 你不能仅仅将阴影应用于渐变色,因为它并不是真正的标准填充(有关更多信息,请参见这个帖子)。
一旦你拥有了一个填充的圆角矩形,用于创建阴影,你需要在其上方绘制渐变色。 所以重复添加路径,以设置剪裁区域,然后使用CGContextDrawLinearGradient绘制渐变。我认为你不能轻易地使用渐变"填充"路径,就像之前的标准填充步骤那样,所以你需要用渐变填充绘图区域,然后剪切到你感兴趣的圆角矩形区域。
- (void)drawRect:(CGRect)rect 
{
    [super drawRect:rect];

    CGGradientRef gradient = [self normalGradient];

    CGContextRef ctx = UIGraphicsGetCurrentContext(); 
    CGMutablePathRef outlinePath = CGPathCreateMutable(); 
    float offset = 5.0;
    float w  = [self bounds].size.width; 
    float h  = [self bounds].size.height; 
    CGPathMoveToPoint(outlinePath, nil, offset*2.0, offset); 
    CGPathAddArcToPoint(outlinePath, nil, offset, offset, offset, offset*2, offset); 
    CGPathAddLineToPoint(outlinePath, nil, offset, h - offset*2.0); 
    CGPathAddArcToPoint(outlinePath, nil, offset, h - offset, offset *2.0, h-offset, offset); 
    CGPathAddLineToPoint(outlinePath, nil, w - offset *2.0, h - offset); 
    CGPathAddArcToPoint(outlinePath, nil, w - offset, h - offset, w - offset, h - offset * 2.0, offset); 
    CGPathAddLineToPoint(outlinePath, nil, w - offset, offset*2.0); 
    CGPathAddArcToPoint(outlinePath, nil, w - offset , offset, w - offset*2.0, offset, offset); 
    CGPathCloseSubpath(outlinePath); 

    CGContextSetShadow(ctx, CGSizeMake(4,4), 3); 
    CGContextAddPath(ctx, outlinePath); 
    CGContextFillPath(ctx); 

    CGContextAddPath(ctx, outlinePath); 
    CGContextClip(ctx);
    CGPoint start = CGPointMake(rect.origin.x, rect.origin.y);
    CGPoint end = CGPointMake(rect.origin.x, rect.size.height);
    CGContextDrawLinearGradient(ctx, gradient, start, end, 0);

    CGPathRelease(outlinePath);
}

- (CGGradientRef)normalGradient
{

    NSMutableArray *normalGradientLocations = [NSMutableArray arrayWithObjects:
                                               [NSNumber numberWithFloat:0.0f],
                                               [NSNumber numberWithFloat:1.0f],
                                               nil];


    NSMutableArray *colors = [NSMutableArray arrayWithCapacity:2];

    UIColor *color = [UIColor colorWithRed:0.2745 green:0.2745 blue:0.2745 alpha:1.0];
    [colors addObject:(id)[color CGColor]];
    color = [UIColor colorWithRed:0.2 green:0.2 blue:0.2 alpha:1.0];
    [colors addObject:(id)[color CGColor]];
    NSMutableArray  *normalGradientColors = colors;

    int locCount = [normalGradientLocations count];
    CGFloat locations[locCount];
    for (int i = 0; i < [normalGradientLocations count]; i++)
    {
        NSNumber *location = [normalGradientLocations objectAtIndex:i];
        locations[i] = [location floatValue];
    }
    CGColorSpaceRef space = CGColorSpaceCreateDeviceRGB();

    CGGradientRef normalGradient = CGGradientCreateWithColors(space, (CFArrayRef)normalGradientColors, locations);
    CGColorSpaceRelease(space);

    return normalGradient;
}

非常感谢Beno提供的这么详细的解释!你的示例完美地运行了。我只会更正您的CGGradientRef声明,从CGGradientRef *normalGradient更正为CGGradientRef normalGradient。 - Thomas Desert
你能解释一下如何从这些参数中创建切线和点吗?我不明白如何从4个参数中创建两条线和两个点... - ryyst
@super_tomtom 能否有人解释一下这是如何工作的?我将一个自定义的UIView子类化,并将这个函数作为它的drawRect覆盖,但我仍然没有圆角或投影效果? - skålfyfan

2

我有一种解决方案,不需要预填路径。优点是阴影可以使用渐变的透明效果(即如果渐变从不透明到透明,阴影也将部分透明),而且更简单。

它大致如下:

CGContextSetShadowWithColor();
CGContextBeginTransparencyLayer();

CGContextSaveGState();
CGContextClip();
CGGradientCreateWithColorComponents();
CGContextRestoreGState();

CGContextEndTransparencyLayer();
CGContextSetShadowWithColor(..., NULL);

我想这是因为CGContextBeginTransparencyLayer/CGContextEndTransparencyLayer在剪辑区域之外,而阴影被应用于该层(其中包含渐变填充路径)。至少对我来说似乎是有效的。

1

对于阴影,您可以使用CGContextSetShadow()

这段代码将绘制带有阴影的内容:

- (void)drawTheRealThingInContext:(CGContextRef)ctx 
{   
        // calculate x, y, w, h and inset here...

    CGContextMoveToPoint(ctx, x+inset, y);
    CGContextAddLineToPoint(ctx, x+w-inset, y);
    CGContextAddArcToPoint(ctx, x+w, y, x+w, y+inset, inset);
    CGContextAddLineToPoint(ctx, x+w, y+w-inset);
    CGContextAddArcToPoint(ctx,x+w, y+w, x+w-inset, y+w, inset);
    CGContextAddLineToPoint(ctx, x+inset, y+w);
    CGContextAddArcToPoint(ctx,x, y+w, x, y+w-inset, inset);
    CGContextAddLineToPoint(ctx, x, y+inset);
    CGContextAddArcToPoint(ctx,x, y, x+inset, y, inset);    
}
- (void)drawRect:(CGRect)rect {

    CGContextRef ctx = UIGraphicsGetCurrentContext();
    CGFloat color[4];color[0] = 1.0;color[1] = 1.0;color[2] = 1.0;color[3] = 1.0;
    CGFloat scolor[4];scolor[0] = 0.4;scolor[1] = 0.4;scolor[2] = 0.4;scolor[3] = 0.8;

    CGContextSetFillColor(ctx, color);

    CGContextSaveGState(ctx);
    CGSize  myShadowOffset = CGSizeMake (3,  -3);
    CGContextSetShadow (ctx, myShadowOffset, 1);

    CGContextBeginPath(ctx);

    [self drawTheRealThingInContext:ctx];

    CGContextFillPath(ctx);
    CGContextRestoreGState(ctx);
}

是的,这肯定有效,但我的问题是我想用渐变填充形状。 为此,我需要使用CGContextClip函数。一旦上下文被剪切,似乎就不再绘制阴影了。 - Thomas Desert

1

你(原始)的问题是在绘制渐变时再次绘制了阴影。这个阴影具有(0,0)偏移和一点模糊,只在角落处透过。在CGContextDrawLinearGradient(...)之前的行中添加以下内容:

CGContextSetShadowWithColor(context, CGSizeMake(0, 0), 0, NULL);

空值颜色将禁用阴影并去除角落效果。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接