Drop cap with NSAttributedString

Chciałbym zrobić drop cap pierwszy znak w UILabel używając attributedText NSAttributedString tylko własność. Tak:

Http://www.interpretationbydesign.com/wp-content/uploads/2009/02/drop_caps.gif

Eksperymentowałem z dostosowaniem linii podstawowej dla zakresu pierwszego znaku do wartości ujemnej i działa to na wyrównanie górnej części pierwszego znaku z górną częścią reszty pierwszej linii. Ale nie znalazłem sposobu, aby inne linie płynęły w prawo o charakterze kropli.

Czy można to rozwiązać za pomocą NSAttributedString only, czy też muszę podzielić łańcuch i renderować go samemu używając Core Text?

Author: PeyloW, 2013-01-08

4 answers

CoreText nie może wykonywać Drop caps, ponieważ składa się z linii złożonych z przebiegów glifów. Drop cap obejmowałby wiele linii, które nie są obsługiwane.

Aby osiągnąć ten efekt, należy narysować nasadkę oddzielnie, a następnie narysować resztę tekstu w ścieżce, która go okrąża.

W skrócie: nie jest to możliwe w UILabel, możliwe, ale sporo pracy z CoreText.

Kroki, aby to zrobić z CoreText to:

  • Utwórz framesetter dla pojedynczego charakter.
  • get its bounds
  • Utwórz ścieżkę, która oszczędza ramkę Drop cap
  • Utwórz framesetter dla pozostałych znaków z tą ścieżką
  • narysuj pierwszy glif
  • narysuj resztę
 7
Author: Cocoanetics,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-02-01 06:27:31

Jak wszyscy wspominali, nie można tego zrobić tylko NSAttributedString. Nikolai ma właściwe podejście, używając CTFrameSetters. Jednak jest możliwe, aby Ustawiacz ramek renderował tekst w określonym obszarze (tj. zdefiniowanym przez ścieżkę CGPath).

Będziesz musiał utworzyć 2 framesetters, jeden dla drop cap, a drugi dla reszty tekstu.

Następnie chwytasz ramkę zrzutu i budujesz CGPathRef, która biegnie wokół przestrzeni ramki zrzutu cap.

Następnie renderujesz oba zestawy ramek do widoku.

Stworzyłem przykładowy projekt z obiektem o nazwie DropCapView, który jest podklasą UIView. Ten widok renderuje pierwszy znak i owija pozostały tekst wokół niego.

Wygląda tak:

dropcap na ios

Jest kilka kroków, więc dodałem link do projektu github hostującego przykład. W projekcie są komentarze, które Ci pomogą.

Projekt DropCap na GitHub

Będziesz musiał bawić się kształtem elementu textBox (tj. CGPathRef), aby wyściełać brzegi widoku i dokręcić go do litery Drop cap.

Oto bebechy metody rysowania:

- (void)drawRect:(CGRect)rect {
    //make sure that all the variables exist and are non-nil
    NSAssert(_text != nil, @"text is nil");
    NSAssert(_textColor != nil, @"textColor is nil");
    NSAssert(_fontName != nil, @"fontName is nil");
    NSAssert(_dropCapFontSize > 0, @"dropCapFontSize is <= 0");
    NSAssert(_textFontSize > 0, @"textFontSize is <=0");

    //convert the text aligment from NSTextAligment to CTTextAlignment
    CTTextAlignment ctTextAlignment = NSTextAlignmentToCTTextAlignment(_textAlignment);

    //create a paragraph style
    CTParagraphStyleSetting paragraphStyleSettings[] = { {
            .spec = kCTParagraphStyleSpecifierAlignment,
            .valueSize = sizeof ctTextAlignment,
            .value = &ctTextAlignment
        }
    };

    CFIndex settingCount = sizeof paragraphStyleSettings / sizeof *paragraphStyleSettings;
    CTParagraphStyleRef style = CTParagraphStyleCreate(paragraphStyleSettings, settingCount);

    //create two fonts, with the same name but differing font sizes
    CTFontRef dropCapFontRef = CTFontCreateWithName((__bridge CFStringRef)_fontName, _dropCapFontSize, NULL);
    CTFontRef textFontRef = CTFontCreateWithName((__bridge CFStringRef)_fontName, _textFontSize, NULL);

    //create a dictionary of style elements for the drop cap letter
    NSDictionary *dropCapDict = [NSDictionary dictionaryWithObjectsAndKeys:
                                (__bridge id)dropCapFontRef, kCTFontAttributeName,
                                _textColor.CGColor, kCTForegroundColorAttributeName,
                                style, kCTParagraphStyleAttributeName,
                                @(_dropCapKernValue) , kCTKernAttributeName,
                                nil];
    //convert it to a CFDictionaryRef
    CFDictionaryRef dropCapAttributes = (__bridge CFDictionaryRef)dropCapDict;

    //create a dictionary of style elements for the main text body
    NSDictionary *textDict = [NSDictionary dictionaryWithObjectsAndKeys:
                                 (__bridge id)textFontRef, kCTFontAttributeName,
                                 _textColor.CGColor, kCTForegroundColorAttributeName,
                                 style, kCTParagraphStyleAttributeName,
                                 nil];
    //convert it to a CFDictionaryRef
    CFDictionaryRef textAttributes = (__bridge CFDictionaryRef)textDict;

    //clean up, because the dictionaries now have copies
    CFRelease(dropCapFontRef);
    CFRelease(textFontRef);
    CFRelease(style);

    //create an attributed string for the dropcap
    CFAttributedStringRef dropCapString = CFAttributedStringCreate(kCFAllocatorDefault,
                                                                   (__bridge CFStringRef)[_text substringToIndex:1],
                                                                   dropCapAttributes);

    //create an attributed string for the text body
    CFAttributedStringRef textString = CFAttributedStringCreate(kCFAllocatorDefault,
                                                                (__bridge CFStringRef)[_text substringFromIndex:1],
                                                                   textAttributes);

    //create an frame setter for the dropcap
    CTFramesetterRef dropCapSetter = CTFramesetterCreateWithAttributedString(dropCapString);

    //create an frame setter for the dropcap
    CTFramesetterRef textSetter = CTFramesetterCreateWithAttributedString(textString);

    //clean up
    CFRelease(dropCapString);
    CFRelease(textString);

    //get the size of the drop cap letter
    CFRange range;
    CGSize maxSizeConstraint = CGSizeMake(200.0f, 200.0f);
    CGSize dropCapSize = CTFramesetterSuggestFrameSizeWithConstraints(dropCapSetter,
                                                                      CFRangeMake(0, 1),
                                                                      dropCapAttributes,
                                                                      maxSizeConstraint,
                                                                      &range);

    //create the path that the main body of text will be drawn into
    //i create the path based on the dropCapSize
    //adjusting to tighten things up (e.g. the *0.8,done by eye)
    //to get some padding around the edges of the screen
    //you could go to +5 (x) and self.frame.size.width -5 (same for height)
    CGMutablePathRef textBox = CGPathCreateMutable();
    CGPathMoveToPoint(textBox, nil, dropCapSize.width, 0);
    CGPathAddLineToPoint(textBox, nil, dropCapSize.width, dropCapSize.height * 0.8); 
    CGPathAddLineToPoint(textBox, nil, 0, dropCapSize.height * 0.8);
    CGPathAddLineToPoint(textBox, nil, 0, self.frame.size.height);
    CGPathAddLineToPoint(textBox, nil, self.frame.size.width, self.frame.size.height);
    CGPathAddLineToPoint(textBox, nil, self.frame.size.width, 0);
    CGPathCloseSubpath(textBox);

    //create a transform which will flip the CGContext into the same orientation as the UIView
    CGAffineTransform flipTransform = CGAffineTransformIdentity;
    flipTransform = CGAffineTransformTranslate(flipTransform,
                                               0,
                                               self.bounds.size.height);
    flipTransform = CGAffineTransformScale(flipTransform, 1, -1);

    //invert the path for the text box
    CGPathRef invertedTextBox = CGPathCreateCopyByTransformingPath(textBox,
                                                                   &flipTransform);
    CFRelease(textBox);

    //create the CTFrame that will hold the main body of text
    CTFrameRef textFrame = CTFramesetterCreateFrame(textSetter,
                                                    CFRangeMake(0, 0),
                                                    invertedTextBox,
                                                    NULL);
    CFRelease(invertedTextBox);
    CFRelease(textSetter);

    //create the drop cap text box
    //it is inverted already because we don't have to create an independent cgpathref (like above)
    CGPathRef dropCapTextBox = CGPathCreateWithRect(CGRectMake(_dropCapKernValue/2.0f,
                                                               0,
                                                               dropCapSize.width,
                                                               dropCapSize.height),
                                                    &flipTransform);
    CTFrameRef dropCapFrame = CTFramesetterCreateFrame(dropCapSetter,
                                                       CFRangeMake(0, 0),
                                                       dropCapTextBox,
                                                       NULL);
    CFRelease(dropCapTextBox);
    CFRelease(dropCapSetter);

    //draw the frames into our graphic context
    CGContextRef gc = UIGraphicsGetCurrentContext();
    CGContextSaveGState(gc); {
        CGContextConcatCTM(gc, flipTransform);
        CTFrameDraw(dropCapFrame, gc);
        CTFrameDraw(textFrame, gc);
    } CGContextRestoreGState(gc);
    CFRelease(dropCapFrame);
    CFRelease(textFrame);
}

P. S. to pochodzi z inspiracji: https://stackoverflow.com/a/9272955/1218605

 14
Author: C4 - Travis,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 11:54:27

Nie, Nie można tego zrobić tylko za pomocą NSAttributedString i standardowego ciągu znaków.

Ponieważ Drop cap jest właściwością akapitu, CTParagraphStyle musi zawierać informacje o drop cap. Jedyną właściwością CTParagraphStyle, która wpływa na wcięcie początku akapitu jest kCTParagraphStyleSpecifierFirstLineHeadIndent, ale dotyczy tylko pierwszego wiersza.

Po prostu nie ma sposobu, aby powiedzieć CTFramesetter Jak obliczyć początek dla drugiego i więcej wierszy.

Jedynym sposobem jest zdefiniowanie własnego atrybut i zapis kodu, aby narysować ciąg znaków za pomocą CTFramesetter i CTTypesetter, które potwierdzają ten niestandardowy atrybut.

 3
Author: Nikolai Ruhe,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-01-25 11:31:58

Nie jest to idealne rozwiązanie, ale powinieneś spróbować DTCoreText i renderować swoje normalne NSString jako formatted HTML. W HTML można "upuścić" literę.

 1
Author: CarlJ,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-01-25 10:48:36