开发者

How to draw a tapered line + oval shadow in Cocoa

Background:

The shot below is of Mail.app in OS X Lion. When the source list gets too long, a nice shadowy line appears just above the buttons at the bottom of the source list. When you scroll, the source list moves under that shadowy line. When you expand the window so that everything in the source list fits without scrolling, the shadowy line disappears.

The question:

How can I draw this shadowy line using Cocoa? I'm aware of NSShadow and such, but it seems to me there's more going on here than just a shadow. There's a line that subtly fades to points (as if开发者_JAVA百科 you applied a gradient mask to each end in Photoshop.) Likewise, the shadow is oval and tapers off as you approach the end of the lines. So it's not just a regular NSShadow, is it? (It's definitely not an image, as it scales nicely when you expand the width of the source view.)

Any tips on how to approach drawing this shape would be greatly appreciated.

How to draw a tapered line + oval shadow in Cocoa

And for the sticklers out there, no, this does not violate the NDA, as Mail.app has been shown publicly by Apple.


General Idea:

.

  1. Create a layer "Layer A" with dimensions 150px × 10px
    and fill it with a Gradient with:
    • lower color: #535e71 opacity: 33%
    • upper color: #535e71 opacity: 0%
  2. Create a layer "Layer B" with dimensions 150px × 1px
    and fill it with solid #535e71 opacity: 50%
  3. Compose "Layer A" and "Layer B" together into "Layer C".
  4. Apply reflected gradient mask from #ffffff to #000000 to "Layer C".

Visual Steps:

How to draw a tapered line + oval shadow in Cocoa

Functional Code:

MyView.h:

#import <Cocoa/Cocoa.h>

@interface MyView : NSView {
@private

}

@end

MyView.m:

#import "MyView.h"

@implementation MyView

- (CGImageRef)maskForRect:(NSRect)dirtyRect {
    NSSize size = [self bounds].size;
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    CGContextRef context = CGBitmapContextCreate(NULL, size.width, size.height, 8, 0, colorSpace, kCGImageAlphaPremultipliedLast);

    CGContextClipToRect(context, *(CGRect*)&dirtyRect);

    CGRect rect = CGRectMake(0.0, 0.0, size.width, size.height);

    size_t num_locations = 3;
    CGFloat locations[3] = { 0.0, 0.5, 1.0 };
    CGFloat components[12] = {
        1.0, 1.0, 1.0, 1.0,  // Start color
        0.0, 0.0, 0.0, 1.0,  // Middle color
        1.0, 1.0, 1.0, 1.0,  // End color
    };

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

    CGPoint myStartPoint = CGPointMake(CGRectGetMinX(rect), CGRectGetMinY(rect));
    CGPoint myEndPoint = CGPointMake(CGRectGetMaxX(rect), CGRectGetMinY(rect));

    CGContextDrawLinearGradient(context, myGradient, myStartPoint, myEndPoint, 0);

    CGImageRef theImage = CGBitmapContextCreateImage(context);
    CGImageRef theMask = CGImageMaskCreate(CGImageGetWidth(theImage), CGImageGetHeight(theImage), CGImageGetBitsPerComponent(theImage), CGImageGetBitsPerPixel(theImage), CGImageGetBytesPerRow(theImage), CGImageGetDataProvider(theImage), NULL, YES);

    [(id)theMask autorelease];

    CGColorSpaceRelease(colorSpace);
    CGContextRelease(context);

    return theMask;
}

- (void)drawRect:(NSRect)dirtyRect {
    NSRect nsRect = [self bounds];
    CGRect rect = *(CGRect*)&nsRect;
    CGRect lineRect = CGRectMake(rect.origin.x, rect.origin.y, rect.size.width, (CGFloat)1.0);

    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();

    CGContextRef context = (CGContextRef) [[NSGraphicsContext currentContext] graphicsPort];
    CGContextClipToRect(context, *(CGRect*)&dirtyRect);
    CGContextClipToMask(context, rect, [self maskForRect:dirtyRect]);

    size_t num_locations = 2;
    CGFloat locations[2] = { 0.0, 1.0 };
    CGFloat components[8] = {
        0.315, 0.371, 0.450, 0.3,  // Bottom color
        0.315, 0.371, 0.450, 0.0  // Top color
    };

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

    CGPoint myStartPoint = CGPointMake(CGRectGetMinX(rect), CGRectGetMinY(rect));
    CGPoint myEndPoint = CGPointMake(CGRectGetMinX(rect), CGRectGetMaxY(rect));

    CGContextDrawLinearGradient(context, myGradient, myStartPoint, myEndPoint, 0);

    CGContextSetRGBFillColor(context, 0.315, 0.371, 0.450, 0.5 );
    CGContextFillRect(context, lineRect);

    CGColorSpaceRelease(colorSpace);    
}

@end

(My first time using pure low-level CoreGraphics, thus possibly sub-par optimal, open for improvements.)

This is an actual screenshot of what the code above produces:

How to draw a tapered line + oval shadow in Cocoa


The drawing stretches to the view's dimensions.

(I formerly had two techniques shown here: "Technique A" & "Technique B".
"Technique B" provided superior results and was way simpler to implement as well, so I ditched "Technique A".
Some comments may still refer to "Technique A" though. Just ignore them and enjoy the fully functional code snippet.).


How about an image that's stretched horizontally?

Or if you know how you'd make one in Photoshop, you could apply those same steps programmatically.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜