What's the best approach to draw lines between views?

后端 未结 2 576
臣服心动
臣服心动 2020-12-04 16:58

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

相关标签:
2条回答
  • 2020-12-04 17:38

    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

    0 讨论(0)
  • 2020-12-04 17:46

    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 !

    0 讨论(0)
提交回复
热议问题