How can I add a gradient that spans two views?

2019-02-27 14:44发布

I know how to do (1) but how can I do (2)?

UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 320, 50)];
CAGradientLayer *gradient = [CAGradientLayer layer];

gradient.frame = view.bounds;
gradient.colors = @[(id)[UIColor blueColor].CGColor, (id)[UIColor redColor].CGColor];

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

enter image description here

2条回答
\"骚年 ilove
2楼-- · 2019-02-27 15:26

There are several ways you could do this. Here's one way:

  1. Create a UIView subclass named GradientView to manage the gradient layer. This is helpful because it means you can use the normal UIKit techniques to manage the gradient layout (auto layout constraints, autoresizing masks, UIKit animations).

  2. For each view that should participate in the common gradient, add a GradientView subview. Set up each GradientView's colors, locations, and start and end points identically.

  3. For each view that should participate in the common gradient, turn on clipsToBounds.

  4. Use auto layout constraints to make each GradientView span all of the participating superviews. (It's important to understand that constraints can cross superview/subview boundaries).

With this approach, auto layout takes care of making the gradient cover all of the views even if they change size or move around. For example, you won't have to do anything special to make the gradients animate nicely when the user rotates the device.

Thus, for your two-view example, I'm proposing that you set up a view hierarchy like this:

unclipped

In the view debugger screenshot above, I disabled clipping. You can see that the two gradient views have identical gradients and share the same screen space. The topGradient is a subview of topView and bottomGradient is a subview of bottomView.

If we turn clipping on, you'll only see the part of topGradient that fits inside topView's bounds, and you'll only see the part of bottomGradient that fits inside bottomView's bounds. Here's what it looks like with clipping enabled:

clipped

And here's a screen shot of my test program in the simulator:

screen shot

Here's the source code for GradientView:

@interface GradientView: UIView
@property (nonatomic, strong, readonly) CAGradientLayer *gradientLayer;
@end

@implementation GradientView
+ (Class)layerClass { return CAGradientLayer.class; }
- (CAGradientLayer *)gradientLayer { return (CAGradientLayer *)self.layer; }
@end

Here's the code I used to create all of the views:

- (void)viewDidLoad {
    [super viewDidLoad];

    UIView *topView = [[UIView alloc] initWithFrame:CGRectMake(20, 20, 100, 50)];
    topView.layer.cornerRadius = 10;
    topView.clipsToBounds = YES;
    UIView *topGradient = [self newGradientView];
    [topView addSubview:topGradient];
    [self.view addSubview:topView];

    UIView *bottomView = [[UIView alloc] initWithFrame:CGRectMake(20, 90, 100, 50)];
    bottomView.layer.cornerRadius = 10;
    bottomView.clipsToBounds = YES;
    UIView *bottomGradient = [self newGradientView];
    [bottomView addSubview:bottomGradient];
    [self.view addSubview:bottomView];

    [self constrainView:topGradient toCoverViews:@[topView, bottomView]];
    [self constrainView:bottomGradient toCoverViews:@[topView, bottomView]];
}

- (GradientView *)newGradientView {
    GradientView *gv = [[GradientView alloc] initWithFrame:CGRectZero];
    gv.translatesAutoresizingMaskIntoConstraints = NO;
    gv.gradientLayer.colors = @[(__bridge id)UIColor.blueColor.CGColor, (__bridge id)UIColor.redColor.CGColor];
    return gv;
}

And here's how I create the constraints that make a GradientView (or any view) cover a set of views:

- (void)constrainView:(UIView *)coverer toCoverViews:(NSArray<UIView *> *)coverees {
    for (UIView *coveree in coverees) {
        NSArray<NSLayoutConstraint *> *cs;

        cs = @[
               [coverer.leftAnchor constraintLessThanOrEqualToAnchor:coveree.leftAnchor],
               [coverer.rightAnchor constraintGreaterThanOrEqualToAnchor:coveree.rightAnchor],
               [coverer.topAnchor constraintLessThanOrEqualToAnchor:coveree.topAnchor],
               [coverer.bottomAnchor constraintGreaterThanOrEqualToAnchor:coveree.bottomAnchor]];
        [NSLayoutConstraint activateConstraints:cs];

        cs = @[
               [coverer.leftAnchor constraintEqualToAnchor:coveree.leftAnchor],
               [coverer.rightAnchor constraintEqualToAnchor:coveree.rightAnchor],
               [coverer.topAnchor constraintEqualToAnchor:coveree.topAnchor],
               [coverer.bottomAnchor constraintEqualToAnchor:coveree.bottomAnchor]];
        for (NSLayoutConstraint *c in cs) { c.priority = UILayoutPriorityDefaultHigh; }
        [NSLayoutConstraint activateConstraints:cs];
    }
}

The greaterThanOrEqual/lessThanOrEqual constraints, which (by default) have required priority, ensure that coverer covers the entire frame of each coveree. The equal constraints, which have lower priority, then ensure that coverer occupies the minimum space required to cover each coveree.

查看更多
ら.Afraid
3楼-- · 2019-02-27 15:28

You can do this by adding a view on top of the view with the gradient, then cutting out the shapes by making a mask out of a UIBezierPath, then adding that to the view on top (let's call it topView):

    let yourPath: UIBezierPath = //create the desired bezier path for your shapes
    let mask = CAShapeLayer()
    mask.path = yourPath.cgPath
    topView.layer.mask = mask
查看更多
登录 后发表回答