我怎样才能用 CoreGraphics 画一条线,它的踪迹会在一定长度后开始消失?

How can I draw a line with CoreGraphics, whose trail will begin to disappear at a certain length?

这里可以看到我说的,从1:04开始。 5-10 秒后,您就会明白我所说的 "trail will begin to disappear".

的意思

当用户在屏幕上移动时,我当前的应用会绘制一条微弱的细线。但是,当我设置 imageView.image = nil.

时,该行一直存在直到 -touchesEnded:withEvent:

我想要实现的是一条正在被主动绘制的线条,当您绘制线条时,线条中最旧的部分将变得更加透明,直到它最终消失。画线可以基于时间,也可以基于当前线的长度。

我怎样才能做到这一点?

我不知道你目前是怎么做的,但这就是我要做的...

  1. 创建一个自定义对象来存储一小部分轨迹,以及 alphadelay
  2. touchesMoved:中计算用户触摸位置的变化并据此生成新的子路径,然后将其包装在自定义对象中。

  3. 绘制 -drawRect: 方法中的所有子路径,以及它们给定的 alpha。

  4. 设置一个CADisplayLink来更新子路径的alpha和延迟。

所以首先,让我们定义我们的自定义对象...

/// Represents a small portion of a trail. 
@interface trailSubPath : NSObject

/// The subpath of the trail.
@property (nonatomic) CGPathRef path;

/// The alpha of this section.
@property (nonatomic) CGFloat alpha;

/// The delay before the subpath fades
@property (nonatomic) CGFloat delay;

@end

我们也给它一个方便的初始化程序,让它在以后看起来更漂亮...

@implementation trailSubPath

+(instancetype) subPathWithPath:(CGPathRef)path alpha:(CGFloat)alpha delay:(CGFloat)delay {
    trailSubPath* subpath = [[self alloc] init];
    subpath.path = path;
    subpath.alpha = alpha;
    subpath.delay = delay;
    return subpath;
}

@end

我们还可以在 UIView 的顶部定义一些常量(如果您还没有创建一个子类,因为我们将使用 -drawRect: )

/// How long before a subpath starts to fade.
static CGFloat const pathFadeDelay = 5.0;

/// How long the fading of the subpath goes on for.
static CGFloat const pathFadeDuration = 1.0;

/// The stroke width of the path.
static CGFloat const pathStrokeWidth = 3.0;

在您的 UIView 中,您将要存储 NSMutableArraytrailSubPath 对象,以及我们稍后需要的一些其他变量。

我决定使用 CADisplayLink 来处理 trailSubPath 对象的更新。这样,代码将 运行 在所有设备上以相同的速度(以在较慢的设备上较低的 FPS 为代价)。

@implementation view {

    UIColor* trailColor; // The stroke color of the trail
    NSMutableArray* trailSubPaths; // The array of trailSubPaths

    CGPoint lastPoint; // Last point the user touched
    BOOL touchedDown; // Whether the user is touching the screen

    CADisplayLink* displayLink; // A display link in order to allow the code to run at the same speed on different devices
}

-initWithFrame: 方法中,我们将进行一些基本设置...

-(instancetype) initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame:frame]) {

        trailSubPaths = [NSMutableArray array];
        trailColor = [UIColor redColor];

        self.backgroundColor = [UIColor whiteColor];
    }
    return self;
}

现在让我们设置 UIResponder 触摸方法...

-(void) touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    lastPoint = [[[event allTouches] anyObject] locationInView:self];
    touchedDown = YES;

    [displayLink invalidate]; // In case it's already running.
    displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkDidFire)];
    [displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}

-(void) touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    if (touchedDown) {

        CGPoint p = [[[event allTouches] anyObject] locationInView:self];

        CGMutablePathRef mutablePath = CGPathCreateMutable(); // Create a new subpath
        CGPathMoveToPoint(mutablePath, nil, lastPoint.x, lastPoint.y);
        CGPathAddLineToPoint(mutablePath, nil, p.x, p.y);

        // Create new subpath object
        [trailSubPaths addObject:[trailSubPath subPathWithPath:CGPathCreateCopy(mutablePath) alpha:1.0 delay:pathFadeDelay]];

        CGPathRelease(mutablePath);

        lastPoint = p;
    }
}

-(void) touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    touchedDown = NO;
}

-(void) touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self touchesEnded:touches withEvent:event];
}

没什么复杂的,它只是计算触摸在-touchesMoved:上的位置变化,并据此生成一个新的直线子路径。然后将其包装在我们的 trailSubPath 中并添加到数组中。

现在,我们需要在 CADisplayLink 更新方法中设置逻辑。这将只计算 alpha 的变化和子路径的延迟,并删除任何已经淡出的子路径:

-(void) displayLinkDidFire {

    // Calculate change in alphas and delays.
    CGFloat deltaAlpha = displayLink.duration/pathFadeDuration;
    CGFloat deltaDelay = displayLink.duration;

    NSMutableArray* subpathsToRemove = [NSMutableArray array];

    for (trailSubPath* subpath in trailSubPaths) {

        if (subpath.delay > 0) subpath.delay -= deltaDelay;
        else subpath.alpha -= deltaAlpha;

        if (subpath.alpha < 0) { // Remove subpath
            [subpathsToRemove addObject:subpath];
            CGPathRelease(subpath.path);
        }
    }

    [trailSubPaths removeObjectsInArray:subpathsToRemove];

    // Cancel running if nothing else to do.
    if (([trailSubPaths count] == 0) && !touchedDown) [displayLink invalidate];
    else [self setNeedsDisplay];
}

最后,我们只想重写 drawRect: 方法,以便在 Core Graphics 中绘制我们所有的 trailSubPath 对象:

- (void)drawRect:(CGRect)rect {

    CGContextRef ctx = UIGraphicsGetCurrentContext();
    CGContextSetStrokeColorWithColor(ctx, trailColor.CGColor);
    CGContextSetLineWidth(ctx, pathStrokeWidth);

    for (trailSubPath* subpath in trailSubPaths) {
        CGContextAddPath(ctx, subpath.path);
        CGContextSetAlpha(ctx, subpath.alpha);
        CGContextStrokePath(ctx);
    }

}

看起来代码很多,但我相信您现在已经设置了一半来画线了!

请注意,根据长度使试验淡出的一种简单方法是将 CADisplayLink 更新方法中对 setNeedsDisplay 的调用移至 -touchesMoved: 方法,并在 -touchesEnded:.

上使显示 link 无效

呸。结束了...我做过的最长的回答。


完成的结果

完整项目:https://github.com/hamishknight/Fading-Trail-Path