Cut Out Shape with Animation

后端 未结 5 1375
盖世英雄少女心
盖世英雄少女心 2020-11-30 19:11

I want to do something similar to the following:

How to mask an image in IOS sdk?

I want to cover the entire screen with translucent black. Then, I want to c

5条回答
  •  旧巷少年郎
    2020-11-30 20:01

    Here's an answer that works with multiple independent, possibly overlapping spotlights.

    I'll set up my view hierarchy like this:

    SpotlightsView with black background
        UIImageView with `alpha`=.5 (“dim view”)
        UIImageView with shape layer mask (“bright view”)
    

    The dim view will appear dimmed because its alpha mixes its image with the black of the top-level view.

    The bright view is not dimmed, but it only shows where its mask lets it. So I just set the mask to contain the spotlight areas and nowhere else.

    Here's what it looks like:

    enter image description here

    I'll implement it as a subclass of UIView with this interface:

    // SpotlightsView.h
    
    #import 
    
    @interface SpotlightsView : UIView
    
    @property (nonatomic, strong) UIImage *image;
    
    - (void)addDraggableSpotlightWithCenter:(CGPoint)center radius:(CGFloat)radius;
    
    @end
    

    I'll need QuartzCore (also called Core Animation) and the Objective-C runtime to implement it:

    // SpotlightsView.m
    
    #import "SpotlightsView.h"
    #import 
    #import 
    

    I'll need instance variables for the subviews, the mask layer, and an array of individual spotlight paths:

    @implementation SpotlightsView {
        UIImageView *_dimImageView;
        UIImageView *_brightImageView;
        CAShapeLayer *_mask;
        NSMutableArray *_spotlightPaths;
    }
    

    To implement the image property, I just pass it through to your image subviews:

    #pragma mark - Public API
    
    - (void)setImage:(UIImage *)image {
        _dimImageView.image = image;
        _brightImageView.image = image;
    }
    
    - (UIImage *)image {
        return _dimImageView.image;
    }
    

    To add a draggable spotlight, I create a path outlining the spotlight, add it to the array, and flag myself as needing layout:

    - (void)addDraggableSpotlightWithCenter:(CGPoint)center radius:(CGFloat)radius {
        UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(center.x - radius, center.y - radius, 2 * radius, 2 * radius)];
        [_spotlightPaths addObject:path];
        [self setNeedsLayout];
    }
    

    I need to override some methods of UIView to handle initialization and layout. I'll handle being created either programmatically or in a xib or storyboard by delegating the common initialization code to a private method:

    #pragma mark - UIView overrides
    
    - (instancetype)initWithFrame:(CGRect)frame
    {
        if (self = [super initWithFrame:frame]) {
            [self commonInit];
        }
        return self;
    }
    
    - (instancetype)initWithCoder:(NSCoder *)aDecoder {
        if (self = [super initWithCoder:aDecoder]) {
            [self commonInit];
        }
        return self;
    }
    

    I'll handle layout in separate helper methods for each subview:

    - (void)layoutSubviews {
        [super layoutSubviews];
        [self layoutDimImageView];
        [self layoutBrightImageView];
    }
    

    To drag the spotlights when they are touched, I need to override some UIResponder methods. I want to handle each touch separately, so I just loop over the updated touches, passing each one to a helper method:

    #pragma mark - UIResponder overrides
    
    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
        for (UITouch *touch in touches){
            [self touchBegan:touch];
        }
    }
    
    - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
        for (UITouch *touch in touches){
            [self touchMoved:touch];
        }
    }
    
    - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
        for (UITouch *touch in touches) {
            [self touchEnded:touch];
        }
    }
    
    - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
        for (UITouch *touch in touches) {
            [self touchEnded:touch];
        }
    }
    

    Now I'll implement the private appearance and layout methods.

    #pragma mark - Implementation details - appearance/layout
    

    First I'll do the common initialization code. I want to set my background color to black, since that is part of making the dimmed image view dim, and I want to support multiple touches:

    - (void)commonInit {
        self.backgroundColor = [UIColor blackColor];
        self.multipleTouchEnabled = YES;
        [self initDimImageView];
        [self initBrightImageView];
        _spotlightPaths = [NSMutableArray array];
    }
    

    My two image subviews will be configured mostly the same way, so I'll call another private method to create the dim image view, then tweak it to actually be dim:

    - (void)initDimImageView {
        _dimImageView = [self newImageSubview];
        _dimImageView.alpha = 0.5;
    }
    

    I'll call the same helper method to create the bright view, then add its mask sublayer:

    - (void)initBrightImageView {
        _brightImageView = [self newImageSubview];
        _mask = [CAShapeLayer layer];
        _brightImageView.layer.mask = _mask;
    }
    

    The helper method that creates both image views sets the content mode and adds the new view as a subview:

    - (UIImageView *)newImageSubview {
        UIImageView *subview = [[UIImageView alloc] init];
        subview.contentMode = UIViewContentModeScaleAspectFill;
        [self addSubview:subview];
        return subview;
    }
    

    To lay out the dim image view, I just need to set its frame to my bounds:

    - (void)layoutDimImageView {
        _dimImageView.frame = self.bounds;
    }
    

    To lay out the bright image view, I need to set its frame to my bounds, and I need to update its mask layer's path to be the union of the individual spotlight paths:

    - (void)layoutBrightImageView {
        _brightImageView.frame = self.bounds;
        UIBezierPath *unionPath = [UIBezierPath bezierPath];
        for (UIBezierPath *path in _spotlightPaths) {
            [unionPath appendPath:path];
        }
        _mask.path = unionPath.CGPath;
    }
    

    Note that this isn't a true union that encloses each point once. It relies on the fill mode (the default, kCAFillRuleNonZero) to ensure that repeatedly-enclosed points are included in the mask.

    Next up, touch handling.

    #pragma mark - Implementation details - touch handling
    

    When UIKit sends me a new touch, I'll find the individual spotlight path containing the touch, and attach the path to the touch as an associated object. That means I need an associated object key, which just needs to be some private thing I can take the address of:

    static char kSpotlightPathAssociatedObjectKey;
    

    Here I actually find the path and attach it to the touch. If the touch is outside any of my spotlight paths, I ignore it:

    - (void)touchBegan:(UITouch *)touch {
        UIBezierPath *path = [self firstSpotlightPathContainingTouch:touch];
        if (path == nil)
            return;
        objc_setAssociatedObject(touch, &kSpotlightPathAssociatedObjectKey,
            path, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    

    When UIKit tells me a touch has moved, I see if the touch has a path attached. If so, I translate (slide) the path by the amount that the touch has moved since I last saw it. Then I flag myself for layout:

    - (void)touchMoved:(UITouch *)touch {
        UIBezierPath *path = objc_getAssociatedObject(touch,
            &kSpotlightPathAssociatedObjectKey);
        if (path == nil)
            return;
        CGPoint point = [touch locationInView:self];
        CGPoint priorPoint = [touch previousLocationInView:self];
        [path applyTransform:CGAffineTransformMakeTranslation(
            point.x - priorPoint.x, point.y - priorPoint.y)];
        [self setNeedsLayout];
    }
    

    I don't actually need to do anything when the touch ends or is cancelled. The Objective-C runtime will de-associated the attached path (if there is one) automatically:

    - (void)touchEnded:(UITouch *)touch {
        // Nothing to do
    }
    

    To find the path that contains a touch, I just loop over the spotlight paths, asking each one if it contains the touch:

    - (UIBezierPath *)firstSpotlightPathContainingTouch:(UITouch *)touch {
        CGPoint point = [touch locationInView:self];
        for (UIBezierPath *path in _spotlightPaths) {
            if ([path containsPoint:point])
                return path;
        }
        return nil;
    }
    
    @end
    

    I have uploaded a full demo to github.

提交回复
热议问题