Подтвердить что ты не робот

Капля крышки с помощью NSAttributedString

Я хотел бы сделать первый символ буквицы в UILabel, используя только свойство attributedText NSAttributedString. Как это:


(источник: interprebydesign.com)

Я экспериментировал с настройкой базовой линии для диапазона первого символа на отрицательное значение, и это работает для выравнивания вершины первого символа с вершиной остальной части первой строки. Но я не нашел способа заставить другие строки перетекать справа от ограниченного символа.

Можно ли это решить с помощью NSAttributedString only, или мне нужно разделить строку и сделать ее самостоятельно, используя Core Text?

4b9b3361

Ответ 1

CoreText не может делать кепки, потому что состоит из строк, состоящих из глифов. Крышка отсечки будет охватывать несколько строк, которые не поддерживаются.

Чтобы добиться этого эффекта, вам придется рисовать кепку отдельно, а затем рисовать остальную часть текста по пути, который вокруг него.

Короче говоря: невозможно в UILabel, возможно, но честно работать с CoreText.

Шагами для этого с CoreText являются:

  • создать набор кадров для одиночного символа.
  • получить свои границы
  • создайте путь, который избавит вас от рамки капли.
  • создайте набор фреймов для остальных символов с помощью этого пути.
  • рисовать первый глиф
  • Привлекательный отдых

Ответ 2

Как и все остальные, это невозможно сделать только с помощью NSAttributedString. Николай имеет правильный подход, используя CTFrameSetters. Тем не менее, можно сказать, что обработчик фреймов может отображать текст в определенной области (т.е. Определен CGPath).

Вам нужно будет создать 2 набора фреймов, один для кепки, а другой для остальной части текста.

Затем вы зацепите рамку капли и создайте CGPathRef, который пробегает пространство кадра капли.

Затем вы визуализируете оба набора фреймов в свой вид.

Я создал образец проекта с объектом DropCapView, который является подклассом UIView. Этот вид отображает первый символ и обертывает оставшийся текст вокруг него.

Он выглядит следующим образом:

dropcap on ios

Есть несколько шагов, поэтому я добавил ссылку на проект github, на котором размещен пример. В проекте есть комментарии, которые помогут вам.

Проект DropCap на GitHub

Вам нужно будет поиграть с формой элемента textBox (т.е. CGPathRef) для заполнения по краям представления и затянуть его также с помощью буквы капли.

Вот мужество метода рисования:

- (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. это приходит с некоторым вдохновением: fooobar.com/questions/307482/...

Ответ 3

Нет, это невозможно сделать только с NSAttributedString и стандартным строковым рисунком.

Так как капля крышки является свойством абзаца, то CTParagraphStyle должен содержать информацию о палитре. Единственное свойство в CTParagraphStyle, которое влияет на отступ начала абзаца, равно kCTParagraphStyleSpecifierFirstLineHeadIndent, но это влияет только на первую строку.

Невозможно рассказать CTFramesetter, как рассчитать начало второй и более строк.

Единственный способ - определить свой собственный атрибут и написать код для рисования строки с помощью CTFramesetter и CTTypesetter, которые подтверждают этот пользовательский атрибут.

Ответ 4

Если вы используете UITextView, вы можете использовать textView.textContainer.exclusionPaths, как Дэнни П предложил здесь.

Пример в Swift:

class WrappingTextVC: UIViewController {
  override func viewDidLoad() {
    super.viewDidLoad()

    let textView = UITextView()
    textView.translatesAutoresizingMaskIntoConstraints = false
    textView.text = "ropcap example. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris aliquam vulputate ex. Fusce interdum ultricies justo in tempus. Sed ornare justo in purus dignissim, et rutrum diam pulvinar. Quisque tristique eros ligula, at dictum odio tempor sed. Fusce non nisi sapien. Donec libero orci, finibus ac libero ac, tristique pretium ex. Aenean eu lorem ut nulla elementum imperdiet. Ut posuere, nulla ut tincidunt viverra, diam massa tincidunt arcu, in lobortis erat ex sed quam. Mauris lobortis libero magna, suscipit luctus lacus imperdiet eu. Ut non dignissim lacus. Vivamus eget odio massa. Aenean pretium eget erat sed ornare. In quis tortor urna. Quisque euismod, augue vel pretium suscipit, magna diam consequat urna, id aliquet est ligula id eros. Duis eget tristique orci, quis porta turpis. Donec commodo ullamcorper purus. Suspendisse et hendrerit mi. Nulla pellentesque semper nibh vitae vulputate. Pellentesque quis volutpat velit, ut bibendum magna. Morbi sagittis, erat rutrum  Suspendisse potenti. Nulla facilisi. Praesent libero est, tincidunt sit amet tempus id, blandit sit amet mi. Morbi sed odio nunc. Mauris lobortis elementum orci, at consectetur nisl egestas a. Pellentesque vel lectus maximus, semper lorem eget, accumsan mi. Etiam semper tellus ac leo porta lobortis."
    textView.backgroundColor = .lightGray
    textView.textColor = .black
    view.addSubview(textView)

    textView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20).isActive = true
    textView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20).isActive = true
    textView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20).isActive = true
    textView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -40).isActive = true

    let dropCap = UILabel()
    dropCap.text = "D"
    dropCap.font = UIFont.boldSystemFont(ofSize: 60)
    dropCap.backgroundColor = .lightText
    dropCap.sizeToFit()
    textView.addSubview(dropCap)

    textView.textContainer.exclusionPaths = [UIBezierPath(rect: dropCap.frame)]
  }
}

Результат:

Text wraps around dropcap

Полный пример на github

Ответ 5

Не идеальное решение, но вы должны дать DTCoreText попробовать свой обычный NSString как formatted HTML. Внутри HTML можно "Бросить шапку" на букву.