在视图之间绘制线条的最佳方法是什么?

发布于 2024-11-04 08:29:11 字数 374 浏览 0 评论 0原文

背景:我有一个自定义滚动视图(子类),其上有可拖动的 uiimageviews,基于我需要在 uiscrollview 的子视图中动态绘制一些线条的拖动。 (请注意,我需要将它们放在子视图中,因为稍后我需要更改视图的不透明度。)

因此,在我花费大量时间开发代码之前(我是新手,所以这需要我一段时间),我研究了哪些内容我需要做并找到一些可能的方法。只是想知道执行此操作的正确方法是什么。

  1. 创建 UIView 的子类并使用 drawRect 方法绘制我需要的线(但不确定如何使其动态读取值)
  2. 在子视图上使用 CALayers 并在那里绘制
  3. 使用 CGContext 函数创建绘制线方法
  4. 还有其他吗?

为帮助干杯

Background: I have a custom scrollview (subclassed) that has uiimageviews on it that are draggable, based on the drags I need to draw some lines dynamically in a subview of the uiscrollview. (Note I need them in a subview as at a later point i need to change the opacity of the view.)

So before I spend ages developing the code (i'm a newbie so it will take me a while) I looked into what i need to do and found some possible ways. Just wondering what the right way to do this.

  1. Create a subclass of UIView and use the drawRect method to draw the line i need (but unsure how to make it dynamically read in the values)
  2. On the subview use CALayers and draw on there
  3. Create a draw line method using CGContext functions
  4. Something else?

Cheers for the help

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

鲜肉鲜肉永远不皱 2024-11-11 08:29:11

从概念上讲,您的所有主张都是相似的。所有这些都会导致以下步骤(其中一些是由 UIKit 不可见地完成的):

  1. 在内存中设置位图上下文。
  2. 使用 Core Graphics 将线条绘制到位图中。
  3. 将此位图复制到 GPU 缓冲区(纹理)。
  4. 使用 GPU 构建层(视图)层次结构。

上述步骤中最昂贵的部分是前三点。它们会导致重复的内存分配、内存复制和 CPU/GPU 通信。另一方面,你真正想做的是轻量级的:画一条线,可能动画开始/结束点,宽度,颜色,alpha,......

有一个简单的方法可以做到这一点,完全避免所描述的开销:使用CALayer 为您的线条,而不是在 CPU 上重新绘制内容,只需用线条的颜色完全填充它(将其 backgroundColor 属性设置为线条的颜色。然后修改图层的位置、边界、变换属性, 。

当然,这种方法只能绘制直线,但也可以通过将 contents 属性设置为图像来修改以绘制复杂的视觉效果 例如,您可以使用这种技术在线条上产生模糊的发光效果,

尽管这种技术有其局限性,但我经常在 iPhone 和 Mac 上的不同应用程序中使用它,它总是具有显着的优越性。性能优于基于核心显卡的绘图。

编辑:计算图层属性的代码:

void setLayerToLineFromAToB(CALayer *layer, CGPoint a, CGPoint b, CGFloat lineWidth)
{
    CGPoint center = { 0.5 * (a.x + b.x), 0.5 * (a.y + b.y) };
    CGFloat length = sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y));
    CGFloat angle = atan2(a.y - b.y, a.x - b.x);

    layer.position = center;
    layer.bounds = (CGRect) { {0, 0}, { length + lineWidth, lineWidth } };
    layer.transform = CATransform3DMakeRotation(angle, 0, 0, 1);
}

第二次编辑:这是一个简单的测试项目显示了 Core Graphics 和基于 Core Animation 的渲染之间性能上的巨大差异。

第三次编辑:结果非常令人印象深刻:渲染 30 个可拖动视图,每个视图彼此连接(产生 435 条线),使用 Core Animation 在 iPad 2 上以 60Hz 流畅渲染。使用经典方法时,帧速率下降至 5 Hz,并且最终会出现内存警告。

核心图形与核心动画的性能比较

Conceptually all your propositions are similar. All of them would lead to the following steps (some of them done invisibly by UIKit):

  1. Setup a bitmap context in memory.
  2. Use Core Graphics to draw the line into the bitmap.
  3. Copy this bitmap to a GPU buffer (a texture).
  4. Compose the layer (view) hierarchy using the GPU.

The expensive part of the above steps are the first three points. They lead to repeated memory allocation, memory copying, and CPU/GPU communication. On the other hand, what you really want to do is lightweight: Draw a line, probably animating start/end points, width, color, alpha, ...

There's an easy way to do this, completely avoiding the described overhead: Use a CALayer for your line, but instead of redrawing the contents on the CPU just fill it completely with the line's color (setting its backgroundColor property to the line's color. Then modify the layer's properties for position, bounds, transform, to make the CALayer cover the exact area of your line.

Of course, this approach can only draw straight lines. But it can also be modified to draw complex visual effects by setting the contents property to an image. You could, for example have fuzzy edges of a glow effect on the line, using this technique.

Though this technique has its limitations, I used it quite often in different apps on the iPhone as well as on the Mac. It always had dramatically superior performance than the core graphics based drawing.

Edit: Code to calculate layer properties:

void setLayerToLineFromAToB(CALayer *layer, CGPoint a, CGPoint b, CGFloat lineWidth)
{
    CGPoint center = { 0.5 * (a.x + b.x), 0.5 * (a.y + b.y) };
    CGFloat length = sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y));
    CGFloat angle = atan2(a.y - b.y, a.x - b.x);

    layer.position = center;
    layer.bounds = (CGRect) { {0, 0}, { length + lineWidth, lineWidth } };
    layer.transform = CATransform3DMakeRotation(angle, 0, 0, 1);
}

2nd Edit: Here's a simple test project which shows the dramatical difference in performance between Core Graphics and Core Animation based rendering.

3rd Edit: The results are quite impressive: Rendering 30 draggable views, each connected to each other (resulting in 435 lines) renders smoothly at 60Hz on an iPad 2 using Core Animation. When using the classic approach, the framerate drops to 5 Hz and memory warnings eventually appear.

Performance comparison Core Graphics vs. Core Animation

羞稚 2024-11-11 08:29:11

首先,要在 iOS 上绘图,您需要一个上下文,而在屏幕上绘图时,您无法在 drawRect: (UIView) 或 drawLayer:inContext: (CALayer) 之外获取上下文。这意味着选项 3 不可用(如果您打算在 drawRect: 方法之外执行此操作)。

你可以选择 CALayer,但我会选择 UIView。据我了解您的设置,您有这样的:

    UIScrollView
    |     |    |
ViewA   ViewB  LineView

所以 LineView 是 ViewA 和 ViewB 的兄弟,需要足够大以覆盖 ViewA 和 ViewB 并被安排在两者前面(并且具有 setOpaque :没有设置)。

LineView 的实现非常简单:给它两个 CGPoint 类型的属性 point1point2。或者,您可以自己实现 setPoint1:/setPoint2: 方法,以便它始终调用 [self setNeedsDisplay];,以便在某个点被绘制后它会重新绘制自身改变了。

在 LineView 的 drawRect: 中,您只需使用 CoreGraphics 或与 UIBezierPath。使用哪一种或多或少取决于品味。当您喜欢使用 CoreGraphics 时,您可以这样做:

- (void)drawRect:(CGRect)rect
{
     CGContextRef context = UIGraphicsGetCurrentContext();
     // Set up color, line width, etc. first.
     CGContextMoveToPoint(context, point1);
     CGContextAddLineToPoint(context, point2);
     CGContextStrokePath(context);
}

使用 NSBezierPath,它看起来非常相似:

- (void)drawRect:(CGRect)rect
{
    UIBezierPath *path = [UIBezierPath bezierPath];
    // Set up color, line width, etc. first.
    [path moveToPoint:point1];
    [path addLineToPoint:point2];
    [path stroke];
}

现在的神奇之处在于获取 point1 和 point2 的正确坐标。我假设您有一个可以看到所有视图的控制器。 UIView 有两个很好的实用方法, convertPoint:toView:convertPoint:fromView: 您将在此处需要。这是控制器的虚拟代码,它将导致 LineView 在 ViewA 和 ViewB 的中心之间绘制一条线:

- (void)connectTheViews
{
    CGPoint p1, p2;
    CGRect frame;
    frame = [viewA frame];
    p1 = CGPointMake(CGRectGetMidX(frame), CGRectGetMidY(frame));
    frame = [viewB frame];
    p2 = CGPointMake(CGRectGetMidX(frame), CGRectGetMidY(frame));
    // Convert them to coordinate system of the scrollview
    p1 = [scrollView convertPoint:p1 fromView:viewA];
    p2 = [scrollView convertPoint:p2 fromView:viewB];
    // And now into coordinate system of target view.
    p1 = [scrollView convertPoint:p1 toView:lineView];
    p2 = [scrollView convertPoint:p2 toView:lineView];
    // Set the points.
    [lineView setPoint1:p1];
    [lineView setPoint2:p2];
    [lineView setNeedsDisplay]; // If the properties don't set it already
}

由于我不知道您如何实现拖动,所以我无法告诉您如何触发在 ViewA 和 ViewB 的中心之间调用此方法控制器。如果它完全封装在您的视图中并且不涉及控制器,那么我会选择每次将视图拖动到新坐标时发布的 NSNotification。控制器将侦听通知并调用上述方法来更新 LineView。

最后一点:您可能需要致电 setUserInteractionEnabled:NO 在 LineView 的 initWithFrame: 方法中,以便在线上的触摸将进入线下的视图。

编码愉快!

First, for drawing on iOS you need a context and when drawing on the screen you cannot get the context outside of drawRect: (UIView) or drawLayer:inContext: (CALayer). This means option 3 is out (if you meant to do it outside a drawRect: method).

You could go for a CALayer, but I'd go for a UIView here. As far as I have understood your setup, you have this:

    UIScrollView
    |     |    |
ViewA   ViewB  LineView

So LineView is a sibling of ViewA and ViewB, would need be big enough to cover both ViewA and ViewB and is arranged to be in front of both (and has setOpaque:NO set).

The implementation of LineView would be pretty straight forward: give it two properties point1 and point2 of type CGPoint. Optionally, implement the setPoint1:/setPoint2: methods yourself so it always calls [self setNeedsDisplay]; so it redraws itself once a point has been changed.

In LineView's drawRect:, all you need to is draw the line either with CoreGraphics or with UIBezierPath. Which one to use is more or less a matter of taste. When you like to use CoreGraphics, you do it like this:

- (void)drawRect:(CGRect)rect
{
     CGContextRef context = UIGraphicsGetCurrentContext();
     // Set up color, line width, etc. first.
     CGContextMoveToPoint(context, point1);
     CGContextAddLineToPoint(context, point2);
     CGContextStrokePath(context);
}

Using NSBezierPath, it'd look quite similar:

- (void)drawRect:(CGRect)rect
{
    UIBezierPath *path = [UIBezierPath bezierPath];
    // Set up color, line width, etc. first.
    [path moveToPoint:point1];
    [path addLineToPoint:point2];
    [path stroke];
}

The magic is now getting the correct coordinates for point1 and point2. I assume you have a controller that can see all the views. UIView has two nice utility methods, convertPoint:toView: and convertPoint:fromView: that you'll need here. Here's dummy code for the controller that would cause the LineView to draw a line between the centers of ViewA and ViewB:

- (void)connectTheViews
{
    CGPoint p1, p2;
    CGRect frame;
    frame = [viewA frame];
    p1 = CGPointMake(CGRectGetMidX(frame), CGRectGetMidY(frame));
    frame = [viewB frame];
    p2 = CGPointMake(CGRectGetMidX(frame), CGRectGetMidY(frame));
    // Convert them to coordinate system of the scrollview
    p1 = [scrollView convertPoint:p1 fromView:viewA];
    p2 = [scrollView convertPoint:p2 fromView:viewB];
    // And now into coordinate system of target view.
    p1 = [scrollView convertPoint:p1 toView:lineView];
    p2 = [scrollView convertPoint:p2 toView:lineView];
    // Set the points.
    [lineView setPoint1:p1];
    [lineView setPoint2:p2];
    [lineView setNeedsDisplay]; // If the properties don't set it already
}

Since I don't know how you've implemented the dragging I can't tell you how to trigger calling this method on the controller. If it's done entirely encapsulated in your views and the controller is not involved, I'd go for a NSNotification that you post every time the view is dragged to a new coordinate. The controller would listen for the notification and call the aforementioned method to update the LineView.

One last note: you might want to call setUserInteractionEnabled:NO on your LineView in its initWithFrame: method so that a touch on the line will go through to the view under the line.

Happy coding !

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文