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

18
背景:我有一个自定义的ScrollView(子类)上有一些UIImageview,可拖动。基于这些拖动操作,我需要在UIScrollView的子视图中动态绘制一些线条。(请注意,我需要将它们放在子视图中,因为稍后需要更改视图的不透明度。)
所以,在我花费很长时间开发代码之前(我是新手,所以需要一段时间),我研究了我需要做的事情,找到了一些可能的方法。只是想知道正确的做法。
  1. 创建一个UIView的子类,并使用drawRect方法来绘制所需的线条(但不确定如何动态读取值)
  2. 在子视图上使用CALayers并在其中绘制
  3. 创建一个使用CGContext函数的画线方法
  4. 还有其他方法吗?
感谢您的帮助
2个回答

45
概念上,您的所有提议都很相似。所有这些提议都将导致以下步骤(其中一些在UIKit中隐式完成):
  1. 在内存中设置位图上下文。
  2. 使用Core Graphics将线条绘制到位图中。
  3. 将此位图复制到GPU缓冲区(纹理)中。
  4. 使用GPU组合层次结构(视图)。
以上步骤中昂贵的部分是前三个步骤。它们会导致重复的内存分配,内存复制和CPU / GPU通信。另一方面,您真正想做的是轻量级:画一条线,可能会动态更改起点/终点、宽度、颜色、alpha等。
有一种简单的方法可以完全避免上述开销:为您的线条使用CALayer,但不是在CPU上重新绘制内容,而是完全填充它的颜色(将其backgroundColor属性设置为线条的颜色)。然后修改图层的属性(如位置、边界、变换)以使CALayer覆盖您线条的确切区域。
当然,这种方法只能画直线。但可以通过将contents属性设置为图像来修改它以绘制复杂的视觉效果。例如,您可以使用此技术在线条上创建模糊边缘或发光效果。
尽管这种技术有其局限性,但我在iPhone上的不同应用程序以及Mac上经常使用它。它始终比基于Core Graphics的绘图具有明显的优势性能。
编辑:计算图层属性的代码:
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);
}

第二次编辑: 这里是一个简单的测试项目,展示了基于核心图形和核心动画的渲染性能之间的巨大差异。

第三次编辑: 结果非常惊人:在iPad 2上,使用Core Animation每秒渲染30个可拖动的视图,每个视图相互连接(结果为435条线),帧率平稳在60Hz。而使用传统方法时,帧率下降至5Hz,并最终出现内存警告。

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


给定两个点p1p2,如何正确地定位/旋转图层?虽然使用简单的绘图解决方案(无论是UIView还是CALayer)计算正确的位置和变换比较困难,但是这也比较高效。我有一些想法,但都不是特别好。 - DarkDust
2
@DarkDust 添加了代码以展示如何设置图层属性。 - Nikolai Ruhe
2
@pqnet:您可以通过在应用程序的Info.plist中设置UIViewEdgeAntialiasing来为核心动画层打开抗锯齿功能。 - Nikolai Ruhe
@NikolaiRuhe 很棒的例子,但是如何获取两条直线的交点?你有什么想法吗? - Chintan
1
@Maverick,这个问题已经被回答了很多次,例如在这里:https://dev59.com/pHRB5IYBdhLWcg3wpYmo - Nikolai Ruhe
显示剩余7条评论

5
首先,在iOS上进行绘图需要一个上下文,当在屏幕上绘制时,你无法在drawRect:(UIView)或drawLayer:inContext:(CALayer)之外获取上下文。这意味着选项3不可行(如果你打算在drawRect:方法之外执行它)。
你可以选择使用CALayer,但我建议在这里使用UIView。就我所理解的设置而言,你有这个:
    UIScrollView
    |     |    |
ViewA   ViewB  LineView

所以,LineView是ViewA和ViewB的兄弟视图,需要足够大以覆盖两个视图,并排列在两者前面(并设置了setOpaque:NO)。
LineView的实现非常简单:给它两个类型为CGPoint的属性point1point2。选择性地,自己实现setPoint1:/setPoint2:方法,以便每次更改点时始终调用 [self setNeedsDisplay];,从而重新绘制自身。
在LineView的drawRect:中,你只需要使用CoreGraphicsUIBezierPath之一来绘制线条。要使用哪个更多或少是品味问题。当你想使用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];
}

现在的关键是获取点1和点2的正确坐标。我假设你有一个可以查看所有视图的控制器。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
}

由于我不知道您是如何实现拖动的,因此无法告诉您如何在控制器上调用此方法。如果完全封装在视图中且控制器未涉及,则应该使用 NSNotification,每次视图拖动到新坐标时会发布一次通知。控制器将侦听通知并调用上述方法来更新 LineView。

最后一点提示:在 LineView 的initWithFrame: 方法中,您可能希望调用 setUserInteractionEnabled:NO,以便触摸线条时可以穿过线条到达线条下方的视图。

祝编码愉快!


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接