如何比CGContextStrokePath更快地渲染线条? [英] How can I render line faster than CGContextStrokePath?

查看:111
本文介绍了如何比CGContextStrokePath更快地渲染线条?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在使用CGContextStrokePath为图表绘制~768点。问题是每一秒我得到一个新的数据点,从而重绘图形。目前这已经占据了繁忙App的50%CPU。

I'm plotting ~768 points for a graph using CGContextStrokePath. The problem is that every second I get a new data point, and thus redraw the graph. This is currently taking 50% CPU in what's already a busy App.

图表绘图在UIView中的drawRect中完成。该图表是基于时间的,因此新数据点始终位于右侧。

Graph drawing is done in drawRect in a UIView. The graph is time based, so new data points always arrive on the right hand side.

我在考虑一些替代方法:

I'm thinking a few alternative approaches:


  1. 用GLKit绘制(以不支持旧设备为代价)并且似乎做了很多工作。

  2. 做某种屏幕抓取(renderInContext?),向左移动1 px,blit,并且只绘制一个最后两个数据点的行。

  3. 有一个非常宽的CALayer并沿着它平移?

  4. 平滑数据集,但这感觉就像作弊:)

  1. Draw with GLKit (at cost of not supporting older devices) and seems like a lot of work.
  2. Do some kind of screen grab (renderInContext?), shift left by 1 px, blit, and only draw a line for the last two data points.
  3. Have a very wide CALayer and pan along it?
  4. Smooth the data set, but this feels like cheating :)

我也可能错过了一些明显的东西,我看到的表现如此糟糕?

It's also possible I'm missing something obvious here that I'm seeing such poor performance?

    CGContextBeginPath(context);
CGContextSetLineWidth(context, 2.0);
UIColor *color = [UIColor whiteColor];
CGContextSetStrokeColorWithColor(context, [color CGColor]);
…
        CGContextAddLines(context, points, index);
        CGContextMoveToPoint(context, startPoint.x, startPoint.y);
        CGContextClosePath(context);

        CGContextStrokePath(context);


推荐答案

让我们实现一个使用一堆高的图形视图,瘦层,以减少所需的重绘量。当我们添加样本时,我们会将图层向左滑动,因此在任何时候,我们可能会在视图的左边缘悬挂一个图层,而在视图的右边缘悬挂一个图层:

Let's implement a graphing view that uses a bunch of tall, skinny layers to reduce the amount of redrawing needed. We'll slide the layers to the left as we add samples, so at any time, we probably have one layer hanging off the left edge of the view and one hanging off the right edge of the view:

您可以在我的github帐户上找到以下代码的完整工作示例。

You can find a complete working example of the code below on my github account.

让每层32点宽:

#define kLayerWidth 32

让我们说我们'每隔一个样本沿着X轴对样本进行分隔:

And let's say we're going to space the samples along the X axis at one sample per point:

#define kPointsPerSample 1

因此我们可以推断出每层的样本数量。让我们称一个层的样本为 tile

So we can deduce the number of samples per layer. Let's call one layer's worth of samples a tile:

#define kSamplesPerTile (kLayerWidth / kPointsPerSample)

当我们绘制图层时,我们不能只在图层内严格绘制样本。我们必须在每个边缘之后绘制一个或两个样本,因为这些样本的线穿过图层的边缘。我们将这些称为填充样本

When we're drawing a layer, we can't just draw the samples strictly inside the layer. We have to draw a sample or two past each edge, because the lines to those samples cross the edge of the layer. We'll call these the padding samples:

#define kPaddingSamples 2

iPhone屏幕的最大尺寸为320点,因此我们可以计算出我们需要保留的最大样本数量:

The maximum dimension of an iPhone screen is 320 points, so we can compute the maximum number of samples we need to retain:

#define kMaxVisibleSamples ((320 / kPointsPerSample) + 2 * kPaddingSamples)

(如果你想在iPad上运行,你应该更改320.)

(You should change the 320 if you want to run on an iPad.)

我们需要能够计算哪个图块包含给定的样本。正如您所看到的,即使样本数为负数,我们也希望这样做,因为它会使以后的计算变得更容易:

We'll need to be able to compute which tile contains a given sample. And as you'll see, we'll want to do this even if the sample number is negative, because it will make later computations easier:

static inline NSInteger tileForSampleIndex(NSInteger sampleIndex) {
    // I need this to round toward -∞ even if sampleIndex is negative.
    return (NSInteger)floorf((float)sampleIndex / kSamplesPerTile);
}



实例变量



现在,要实现 GraphView ,我们需要一些实例变量。我们需要存储我们用来绘制图形的图层。我们希望能够根据图表的哪个图块查找每个图层:

Instance Variables

Now, to implement GraphView, we'll need some instance variables. We'll need to store the layers that we're using to draw the graph. And we want to be able to look up each layer according to which tile it's graphing:

@implementation GraphView {

    // Each key in _tileLayers is an NSNumber whose value is a tile number.
    // The corresponding value is the CALayer that displays the tile's samples.
    // There will be tiles that don't have a corresponding layer.
    NSMutableDictionary *_tileLayers;

在实际项目中,您希望将样本存储在模型对象中并给出视图对模型的引用。但是对于这个例子,我们只是将样本存储在视图中:

In a real project, you'd want to store the samples in a model object and give the view a reference to the model. But for this example, we'll just store the samples in the view:

    // Samples are stored in _samples as instances of NSNumber.
    NSMutableArray *_samples;

由于我们不想存储任意数量的样本,我们将丢弃旧样本当 _samples 变大时。但如果我们大多假装我们从不丢弃样本,它将简化实施。为此,我们会跟踪已收到的样本总数。

Since we don't want to store an arbitrarily large number of samples, we'll discard old samples when _samples gets big. But it will simplify the implementation if we can mostly pretend that we never discard samples. To do that, we keep track of the total number of samples ever received.

    // I discard old samples from _samples when I have more than
    // kMaxTiles' worth of samples.  This is the total number of samples
    // ever collected, including discarded samples.
    NSInteger _totalSampleCount;

我们应该避免阻塞主线程,因此我们将在单独的GCD队列上进行绘制。我们需要跟踪需要在该队列上绘制哪些区块。为避免多次绘制待处理的图块,我们使用一个集合(消除重复)而不是数组:

We should avoid blocking the main thread, so we'll do our drawing on a separate GCD queue. We need to keep track of which tiles need to be drawn on that queue. To avoid drawing a pending tile more than once, we use a set (which eliminates duplicates) instead of an array:

    // Each member of _tilesToRedraw is an NSNumber whose value
    // is a tile number to be redrawn.
    NSMutableSet *_tilesToRedraw;

这是我们将在其上进行绘图的GCD队列。

And here's the GCD queue on which we'll do the drawing.

    // Methods prefixed with rq_ run on redrawQueue.
    // All other methods run on the main queue.
    dispatch_queue_t _redrawQueue;
}



初始化/销毁



要使这个视图工作,无论你是在代码中还是在nib中创建它,我们都需要两个初始化方法:

Initialization / Destruction

To make this view work whether you create it in code or in a nib, we need two initialization methods:

- (id)initWithFrame:(CGRect)frame {
    if ((self = [super initWithFrame:frame])) {
        [self commonInit];
    }
    return self;
}

- (void)awakeFromNib {
    [self commonInit];
}

两种方法都调用 commonInit 进行真正的初始化:

Both methods call commonInit to do the real initialization:

- (void)commonInit {
    _tileLayers = [[NSMutableDictionary alloc] init];
    _samples = [[NSMutableArray alloc] init];
    _tilesToRedraw = [[NSMutableSet alloc] init];
    _redrawQueue = dispatch_queue_create("MyView tile redraw", 0);
}

ARC不会为我们清理GCD队列:

ARC won't clean up the GCD queue for us:

- (void)dealloc {
    if (_redrawQueue != NULL) {
        dispatch_release(_redrawQueue);
    }
}



添加样本



要添加新样本,我们选择一个随机数并将其附加到 _samples 。我们还增加 _totalSampleCount 。如果 _samples 变大,我们会丢弃最旧的样本。

Adding a sample

To add a new sample, we pick a random number and append it to _samples. We also increment _totalSampleCount. We discard the oldest samples if _samples has gotten big.

- (void)addRandomSample {
    [_samples addObject:[NSNumber numberWithFloat:120.f * ((double)arc4random() / UINT32_MAX)]];
    ++_totalSampleCount;
    [self discardSamplesIfNeeded];

然后,我们检查是否已经开始新的磁贴。如果是这样,我们会找到绘制最旧图块的图层,并重复使用它来绘制新创建的图块。

Then, we check if we've started a new tile. If so, we find the layer that was drawing the oldest tile, and reuse it to draw the newly-created tile.

    if (_totalSampleCount % kSamplesPerTile == 1) {
        [self reuseOldestTileLayerForNewestTile];
    }

现在我们重新计算所有图层的布局,这将在左边a这样新的样本将在图表中显示。

Now we recompute the layout of all the layers, which will to the left a bit so the new sample will be visible in the graph.

    [self layoutTileLayers];

最后,我们将瓷砖添加到重绘队列。

Finally, we add tiles to the redraw queue.

    [self queueTilesForRedrawIfAffectedByLastSample];
}

我们不想一次丢弃一个样本。那将是低效的。相反,我们让垃圾堆积了一段时间,然后一次性扔掉:

We don't want to discard samples one at a time. That would be inefficient. Instead, we let the garbage build up for a while, then throw it away all at once:

- (void)discardSamplesIfNeeded {
    if (_samples.count >= 2 * kMaxVisibleSamples) {
        [_samples removeObjectsInRange:NSMakeRange(0, _samples.count - kMaxVisibleSamples)];
    }
}

要为新图块重复使用图层,我们需要找到最旧的图块的图层:

To reuse a layer for the new tile, we need to find the layer of the oldest tile:

- (void)reuseOldestTileLayerForNewestTile {
    // The oldest tile's layer should no longer be visible, so I can reuse it as the new tile's layer.
    NSInteger newestTile = tileForSampleIndex(_totalSampleCount - 1);
    NSInteger reusableTile = newestTile - _tileLayers.count;
    NSNumber *reusableTileObject = [NSNumber numberWithInteger:reusableTile];
    CALayer *layer = [_tileLayers objectForKey:reusableTileObject];

现在我们可以从 _tileLayers 中删除​​它旧密钥下的字典并将其存储在新密钥下:

Now we can remove it from the _tileLayers dictionary under the old key and store it under the new key:

    [_tileLayers removeObjectForKey:reusableTileObject];
    [_tileLayers setObject:layer forKey:[NSNumber numberWithInteger:newestTile]];

默认情况下,当我们将重用的图层移动到新位置时,Core Animation会将其滑动为动画。我们不希望这样,因为它将是一个大的空橙色矩形滑过我们的图形。我们想立即移动它:

By default, when we move the reused layer to its new position, Core Animation will animate it sliding over. We don't want that, because it will be a big empty orange rectangle sliding across our graph. We want to move it instantly:

    // The reused layer needs to move instantly to its new position,
    // lest it be seen animating on top of the other layers.
    [CATransaction begin]; {
        [CATransaction setDisableActions:YES];
        layer.frame = [self frameForTile:newestTile];
    } [CATransaction commit];
}

当我们添加样本时,我们总是想重新绘制包含的图块例子。如果新样本位于前一个区块的填充范围内,我们还需要重绘前一个区块。

When we add a sample, we'll always want to redraw the tile containing the sample. We also need to redraw the prior tile, if the new sample is within the padding range of the prior tile.

- (void)queueTilesForRedrawIfAffectedByLastSample {
    [self queueTileForRedraw:tileForSampleIndex(_totalSampleCount - 1)];

    // This redraws the second-newest tile if the new sample is in its padding range.
    [self queueTileForRedraw:tileForSampleIndex(_totalSampleCount - 1 - kPaddingSamples)];
}

将图块排队以进行重绘只是将其添加到重绘集中并调度一个块以在 _redrawQueue 上重绘它。

Queuing a tile for redraw is just a matter of adding it to the redraw set and dispatching a block to redraw it on _redrawQueue.

- (void)queueTileForRedraw:(NSInteger)tile {
    [_tilesToRedraw addObject:[NSNumber numberWithInteger:tile]];
    dispatch_async(_redrawQueue, ^{
        [self rq_redrawOneTile];
    });
}



布局



系统会在 GraphView 首次出现时发送 layoutSubviews ,并且只要其大小发生变化(例如设备旋转调整大小)。当我们真正准备出现在屏幕上时,我们只得到 layoutSubviews 消息,并设置我们的最终边界。所以 layoutSubviews 是设置切片图层的好地方。

Layout

The system will send layoutSubviews to the GraphView when it first appears, and any time its size changes (such as if a device rotation resizes it). And we only get the layoutSubviews message when we're really about to appear on the screen, with our final bounds set. So layoutSubviews is a good place to set up the tile layers.

首先,我们需要创建或删除图层必要时,我们有适合我们规模的层。然后我们需要通过适当地设置它们的框架来布置图层。最后,对于每个图层,我们需要将其图块排队以进行重绘。

First, we need to create or remove layers as necessary so we have the right layers for our size. Then we need to lay out the layers by setting their frames appropriately. Finally, for each layer, we need to queue its tile for redraw.

- (void)layoutSubviews {
    [self adjustTileDictionary];
    [CATransaction begin]; {
        // layoutSubviews only gets called on a resize, when I will be
        // shuffling layers all over the place.  I don't want to animate
        // the layers to their new positions.
        [CATransaction setDisableActions:YES];
        [self layoutTileLayers];
    } [CATransaction commit];
    for (NSNumber *key in _tileLayers) {
        [self queueTileForRedraw:key.integerValue];
    }
}

调整磁贴词典意味着为每个设置一个图层可见图块和删除不可见图块的图层。我们每次都会从头开始重新设置字典,但我们会尝试重用我们已创建的图层。需要图层的图块是最新的图块和前面的图块,因此我们有足够的图层来覆盖视图。

Adjusting the tile dictionary means setting up a layer for each visible tile and removing layers for non-visible tiles. We'll just reset the dictionary from scratch each time, but we'll try to reuse the layer's we've already created. The tiles that need layers are the newest tile, and preceding tiles so we have enough layers to cover the view.

- (void)adjustTileDictionary {
    NSInteger newestTile = tileForSampleIndex(_totalSampleCount - 1);
    // Add 1 to account for layers hanging off the left and right edges.
    NSInteger tileLayersNeeded = 1 + ceilf(self.bounds.size.width / kLayerWidth);
    NSInteger oldestTile = newestTile - tileLayersNeeded + 1;

    NSMutableArray *spareLayers = [[_tileLayers allValues] mutableCopy];
    [_tileLayers removeAllObjects];
    for (NSInteger tile = oldestTile; tile <= newestTile; ++tile) {
        CALayer *layer = [spareLayers lastObject];
        if (layer) {
            [spareLayers removeLastObject];
        } else {
            layer = [self newTileLayer];
        }
        [_tileLayers setObject:layer forKey:[NSNumber numberWithInteger:tile]];
    }

    for (CALayer *layer in spareLayers) {
        [layer removeFromSuperlayer];
    }
}

第一次通过,任何时候视图获得足够宽,我们需要创建新的图层。在我们创建视图时,我们会告诉它避免动画其内容或位置。否则它将默认为它们设置动画。

The first time through, and any time the view gets sufficiently wider, we need to create new layers. While we're creating the view, we'll tell it to avoid animating its contents or position. Otherwise it will animate them by default.

- (CALayer *)newTileLayer {
    CALayer *layer = [CALayer layer];
    layer.backgroundColor = [UIColor greenColor].CGColor;
    layer.actions = [NSDictionary dictionaryWithObjectsAndKeys:
        [NSNull null], @"contents",
        [NSNull null], @"position",
        nil];
    [self.layer addSublayer:layer];
    return layer;
}

实际上布局切片图层只需设置每个图层的框架:

Actually laying out the tile layers is just a matter of setting each layer's frame:

- (void)layoutTileLayers {
    [_tileLayers enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
        CALayer *layer = obj;
        layer.frame = [self frameForTile:[key integerValue]];
    }];
}

当然,技巧是计算每一层的框架。 y,width和height部分很容易:

Of course the trick is computing the frame for each layer. And the y, width, and height parts are easy enough:

- (CGRect)frameForTile:(NSInteger)tile {
    CGRect myBounds = self.bounds;
    CGFloat x = [self xForTile:tile myBounds:myBounds];
    return CGRectMake(x, myBounds.origin.y, kLayerWidth, myBounds.size.height);
}

要计算图块框架的x坐标,我们计算x坐标磁贴中的第一个示例:

To compute the x coordinate of the tile's frame, we compute the x coordinate of the first sample in the tile:

- (CGFloat)xForTile:(NSInteger)tile myBounds:(CGRect)myBounds {
    return [self xForSampleAtIndex:tile * kSamplesPerTile myBounds:myBounds];
}

计算样本的x坐标需要一点思考。我们希望最新的样本位于视图的右边缘,而第二个最新的样本 kPointsPerSample 指向左侧,依此类推:

Computing the x coordinate for a sample requires a little thought. We want the newest sample to be at the right edge of the view, and the second-newest to be kPointsPerSample points to the left of that, and so on:

- (CGFloat)xForSampleAtIndex:(NSInteger)index myBounds:(CGRect)myBounds {
    return myBounds.origin.x + myBounds.size.width - kPointsPerSample * (_totalSampleCount - index);
}



重绘



现在我们可以谈谈如何实际绘制瓷砖。我们将在单独的GCD队列上进行绘制。我们不能同时安全地从两个线程访问大多数Cocoa Touch对象,所以我们需要在这里小心。我们将在 _redrawQueue 上运行的所有方法上使用前缀 rq _ 来提醒自己我们不是在主线程上。

Redrawing

Now we can talk about how to actually draw tiles. We're going to do the drawing on a separate GCD queue. We can't safely access most Cocoa Touch objects from two threads simultaneously, so we need to be careful here. We'll use a prefix of rq_ on all the methods that run on _redrawQueue to remind ourselves that we're not on the main thread.

要重绘一个图块,我们需要获取图块编号,图块的图形边界以及要绘制的点。所有这些都来自我们可能在主线程上修改的数据结构,因此我们只需要在主线程上访问它们。所以我们派遣回主队列:

To redraw one tile, we need to get the tile number, the graphical bounds of the tile, and the points to draw. All of those things come from data structures that we might be modifying on the main thread, so we need to access them only on the main thread. So we dispatch back to the main queue:

- (void)rq_redrawOneTile {
    __block NSInteger tile;
    __block CGRect bounds;
    CGPoint pointStorage[kSamplesPerTile + kPaddingSamples * 2];
    CGPoint *points = pointStorage; // A block cannot reference a local variable of array type, so I need a pointer.
    __block NSUInteger pointCount;
    dispatch_sync(dispatch_get_main_queue(), ^{
        tile = [self dequeueTileToRedrawReturningBounds:&bounds points:points pointCount:&pointCount];
    });

碰巧我们可能没有任何瓷砖可以重绘。如果你回头看看 queueTilesForRedrawIfAffectedByLastSample ,你会发现它通常会尝试将同一个图块排队两次。由于 _tilesToRedraw 是一个集合(不是数组),所以重复被丢弃,但无论如何都会调度 rq_redrawOneTile 两次。所以我们需要检查一下我们实际上有一个要重绘的图块:

It so happens that we might not have any tiles to redraw. If you look back at queueTilesForRedrawIfAffectedByLastSample, you'll see that it usually tries to queue the same tile twice. Since _tilesToRedraw is a set (not an array), the duplicate was discarded, but rq_redrawOneTile was dispatched twice anyway. So we need to check that we actually have a tile to redraw:

    if (tile == NSNotFound)
        return;

现在我们需要实际绘制瓷砖的样品:

Now we need to actually draw the tile's samples:

    UIImage *image = [self rq_imageWithBounds:bounds points:points pointCount:pointCount];

最后我们需要更新图块的图层以显示新图像。我们只能触摸主线程上的图层:

Finally we need to update the tile's layer to show the new image. We can only touch a layer on the main thread:

    dispatch_async(dispatch_get_main_queue(), ^{
        [self setImage:image forTile:tile];
    });
}

以下是我们实际绘制图层图像的方法。我会假设你知道足够的核心图形来遵循这个:

Here's how we actually draw the image for the layer. I will assume you know enough Core Graphics to follow this:

- (UIImage *)rq_imageWithBounds:(CGRect)bounds points:(CGPoint *)points pointCount:(NSUInteger)pointCount {
    UIGraphicsBeginImageContextWithOptions(bounds.size, YES, 0); {
        CGContextRef gc = UIGraphicsGetCurrentContext();
        CGContextTranslateCTM(gc, -bounds.origin.x, -bounds.origin.y);

        [[UIColor orangeColor] setFill];
        CGContextFillRect(gc, bounds);

        [[UIColor whiteColor] setStroke];
        CGContextSetLineWidth(gc, 1.0);
        CGContextSetLineJoin(gc, kCGLineCapRound);
        CGContextBeginPath(gc);
        CGContextAddLines(gc, points, pointCount);
        CGContextStrokePath(gc);
    }
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return image;
}

但是我们仍然需要获得图块,图形边界和点数画画。我们发送回主线程来执行此操作:

But we still have to get the tile, the graphics bounds, and the points to draw. We dispatched back to the main thread to do it:

// I return NSNotFound if I couldn't dequeue a tile.
// The `pointsOut` array must have room for at least
// kSamplesPerTile + 2*kPaddingSamples elements.
- (NSInteger)dequeueTileToRedrawReturningBounds:(CGRect *)boundsOut points:(CGPoint *)pointsOut pointCount:(NSUInteger *)pointCountOut {
    NSInteger tile = [self dequeueTileToRedraw];
    if (tile == NSNotFound)
        return NSNotFound;

图形边界只是图块的边界,就像我们之前计算的设置框架一样图层:

The graphics bounds are just the bounds of the tile, just like we computed earlier to set the frame of the layer:

    *boundsOut = [self frameForTile:tile];

我需要在拼贴的第一个样本之前从填充样本开始绘制图形。但是,在有足够的样本填充视图之前,我的瓷砖编号实际上可能是负数!所以我需要确保不要尝试以负索引访问样本:

I need to start graphing from the padding samples before the first sample of the tile. But, prior to having enough samples to fill the view, my tile number may actually be negative! So I need to be sure not to try to access a sample at a negative index:

    NSInteger sampleIndex = MAX(0, tile * kSamplesPerTile - kPaddingSamples);

我们还需要确保在我们运行时不要尝试超过样本的末尾计算我们停止绘图的样本:

We also need to make sure we don't try to run past the end of the samples when we compute the sample at which we stop graphing:

    NSInteger endSampleIndex = MIN(_totalSampleCount, tile * kSamplesPerTile + kSamplesPerTile + kPaddingSamples);

当我实际访问样本值时,我需要考虑我丢弃的样本:

And when I actually access the sample values, I need to account for the samples I've discarded:

    NSInteger discardedSampleCount = _totalSampleCount - _samples.count;

现在我们可以计算实际的点数:

Now we can compute the actual points to graph:

    CGFloat x = [self xForSampleAtIndex:sampleIndex myBounds:self.bounds];
    NSUInteger count = 0;
    for ( ; sampleIndex < endSampleIndex; ++sampleIndex, ++count, x += kPointsPerSample) {
        pointsOut[count] = CGPointMake(x, [[_samples objectAtIndex:sampleIndex - discardedSampleCount] floatValue]);
    }

我可以返回点数和瓷砖:

And I can return the number of points and the tile:

    *pointCountOut = count;
    return tile;
}

以下是我们实际从重绘队列中拉出一个磁贴的方法。请记住,队列可能为空:

Here's how we actually pull a tile off the redraw queue. Remember that the queue might be empty:

- (NSInteger)dequeueTileToRedraw {
    NSNumber *number = [_tilesToRedraw anyObject];
    if (number) {
        [_tilesToRedraw removeObject:number];
        return number.integerValue;
    } else {
        return NSNotFound;
    }
}

最后,这是我们实际设置图块层的方法内容到新图像。请记住,我们已调度回主队列执行此操作:

And finally, here's how we actually set the tile layer's contents to the new image. Remember that we dispatched back to the main queue to do this:

- (void)setImage:(UIImage *)image forTile:(NSInteger)tile {
    CALayer *layer = [_tileLayers objectForKey:[NSNumber numberWithInteger:tile]];
    if (layer) {
        layer.contents = (__bridge id)image.CGImage;
    }
}



让它更性感



如果你做了所有这些,它将正常工作。但实际上,当新样本进入时,通过动画重新定位图层,可以使它看起来更漂亮。这很容易。我们只修改 newTileLayer ,以便为位置添加动画属性:

- (CALayer *)newTileLayer {
    CALayer *layer = [CALayer layer];
    layer.backgroundColor = [UIColor greenColor].CGColor;
    layer.actions = [NSDictionary dictionaryWithObjectsAndKeys:
        [NSNull null], @"contents",
        [self newTileLayerPositionAnimation], @"position",
        nil];
    [self.layer addSublayer:layer];
    return layer;
}

我们创建这样的动画:

- (CAAnimation *)newTileLayerPositionAnimation {
    CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position"];
    animation.duration = 0.1;
    animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
    return animation;
}

您需要设置持续时间以匹配新样本到达的速度。

You will want to set the duration to match the speed at which new samples arrive.

这篇关于如何比CGContextStrokePath更快地渲染线条?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆