• Building Type Mode for Stories on iOS and Android
  • Posted by Instagram Engineering
  • The Nuggets translation Project
  • Permanent link to this article: github.com/xitu/gold-m…
  • Translator: Kim Xixi
  • Proofreader: ALVINYEH, Jasonxia23

Instagram recently launched Type Mode, a new way to post creative, dynamic text styles and backgrounds to stories. Type Mode was an interesting challenge for us because it was one of our innovations: letting people share on Stories without photos or videos — we wanted to make sure that Type Mode remained a fun, customizable and visually expressive experience.

Implementing Type Mode seamlessly on iOS and Android has its own set of challenges, including dynamically resizing text and customizing fill backgrounds. In this article, you’ll see how we can do this on iOS and Android.

Dynamically resize text input

In Type Mode, we want to create a text input experience that allows people to emphasize specific words or phrases. One way is to build text styles that align at both ends, dynamically resizing each line to fill a given width (used in Modern, neon, and bold on Instagram).

iOS

The main challenge for iOS is rendering dynamically sized text in the native UITextView, which allows users to enter text in a quick and familiar way.

Resize text before storing it

When you type a line of text, the text size should shrink as you type until you reach the minimum font size.

In order to achieve this requirement, we combined the UITextView typingAttributes, NSAttributedString and NSLayoutManager.

First, we need to calculate what font and size our text will appear in. We can use [NSLayoutManager enumerateLineFragmentsForGlyphRange: usingBlock:] to grab the current input on the line. Based on this range, we can create a string with a size to calculate the minimum font size.

CGFloat pointSize = 24.0; / / casual
NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:string attributes:@{NSFontAttributeName: [UIFont fontWithName:fontName size:pointSize]}];
CGFloat textWidth = CGRectGetWidth([attributedString boundingRectWithSize:CGSizeMake(CGFLOAT_MAX.CGFLOAT_MAX) options:NULL context:nil]);
CGFloat scaleFactor = (textViewContainerWidth / textWidth);
CGFloat preferredFontSize = (pointSize * scaleFactor);
return CLAMP_MIN_MAX(preferredFontSize, minimumFontSize, maximumFontSize) // Hold the font between the maximum and minimum values
Copy the code

To draw the text at the correct size, we need to use our new font size in typingAttributes of the UITextView. UITextView. TypingAttributes is used to set the user’s input text attributes. In [id < UITextViewDelegate > textView: shouldChangeTextInRange: replacementText:] method implemented in more appropriate.

- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
    NSMutableDictionary *typingAttributes = [textView.typingAttributes mutableCopy];
    typingAttributes[NSFontAttributeName] = [UIFont fontWithDescriptor:fontDescriptor size:calculatedFontSize];
    textView.typingAttributes = typingAttributes;
    return YES;
}
Copy the code

This means that the font size shrinks as the user enters until a specified minimum value is reached. The UITextView will wrap our text as it normally does.

Collate text after storing it

After our text is committed to the text store, we may need to clean up some size properties. Our text may have been wrapped, or the user can “emphasize” by manually adding a newline character and writing larger text on a separate line.

A good place to put this logic is the [ID

textViewDidChange:] method. This occurs after the text has been submitted to the text store and originally typeset by the text engine.

To get a list of character ranges per line, we can use NSLayoutManager.

NSMutableArray<NSValue *> *lineRanges = [NSMutableArray array];
[textView.layoutManager enumerateLineFragmentsForGlyphRange:NSMakeRange(0, layoutManager.numberOfGlyphs) usingBlock:^(CGRect rect, CGRect usedRect, NSTextContainer * _Nonnull textContainer, NSRange glyphRange, BOOL * _Nonnull stop) {
    NSRange characterRange = [layoutManager characterRangeForGlyphRange:glyphRange actualGlyphRange:NULL];
    [lineRanges addObject:[NSValue valueWithRange:characterRange]];
}];
Copy the code

We then need to manipulate NSTextStorage by setting properties on the range of the correct font size for each row.

Edit NSTextStorage has three steps, it itself is NSMutableAttributedString subclasses.

  1. call[textStorage beginEditing]To indicate that we are making one or more changes to the text store.
  2. Send some edit information toNSTextStorage. In our case,NSFontAttributeNameProperty should be set to the correct font size for the corresponding row. We can use a similar method to calculate font size, just as we did before.
for (NSValue *lineRangeValue in lineRanges) {
    NSRange lineRange = lineRangeValue.rangeValue;
    const CGFloat fontSize = ... // Use the same font size method as above
    [textStorage setAttributes:@{NSFontAttributeName : [UIFont fontWithDescriptor:fontDescriptor size:fontSize]} range:lineRange];
}
Copy the code
  1. call[textStorage endEditing]To indicate that we are done editing the text store. This calls[NSTextStorage processEditing]Method that fixes the properties of the text in the scope we changed. This will also call the correctNSTextStorageDelegateMethods.

TextKit is a powerful and modern API tightly integrated with UIKit. Many text experiences can be designed with it, and almost every new version of iOS releases some text-related API. With TextKit you can do everything from creating custom text containers to modifying the actual generated glyphs. And because it’s built on top of CoreText and integrates with apis like UITextView, text entry and editing still feel like a native iOS experience.

Android

Android doesn’t have a way to align both ends out of the box, but the framework’s API gives us all the tools we need for our own implementation.

The first step is to lay out the text with the minimum text size. We’ll expand it later, but this will tell us how many lines there are and where to break them:

TextPaint textPaint = new TextPaint();
textPaint.setTextSize(SIZE_MIN);
Layout layout =
    new StaticLayout(
        text,
        textPaint,
        availableWidth,
        Layout.Alignment.ALIGN_CENTER,
        1 /* spacingMult */.0 /* spacingAdd */.true /*includePad */);
int lineCount = layout.getLineCount();
Copy the code

Next, we need to browse the layout and resize each line of text individually. There is no direct way to get a perfect size for a line, but we can easily estimate the maximum size by binary search without forcing line breaks:

int lowSize = SIZE_MIN;
int highSize = SIZE_MAX;
int currentSize = lowSize + (int) Math.floor((highSize - lowSize) / 2f);
while (low < current) {
  if (hasLineBreak(text, currentSize)) {
    highSize = currentSize;
  } else {
    lowSize = currentSize;
  }
  currentSize = lowSize + (int) Math.floor((highSize - lowSize) / 2f);
}
Copy the code

Once we find the right size for each line of text, we can apply it to a span. Span allows us to use different text sizes for each line of text, rather than a single text size for the entire string:

text.setSpan(
    new AbsoluteSizeSpan(textSize),
    layout.getLineStart(lineNumber),
    layout.getLineEnd(lineNumber),
    Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
Copy the code

Each line of text is now filled with the appropriate width! We can repeat this process each time the text changes to dynamically adjust the text.

Custom Background

We also want to use Type Mode to let people emphasize words and phrases through the context of the text (for typewriter typeface and bold).

iOS

Another way we can leverage NSLayoutManager is to draw custom background fills. Although NSAttributedString can use NSBackgroundColorAttributeName attributes set the background color, but it is a custom, nor extension.

For example, if we use the NSBackgroundColorAttributeName, background of the whole text view will be populated. We can’t exclude line Spaces, leave gaps between lines, or fill the background with rounded corners. Thankfully, NSLayoutManager gives us a way to rewrite the drawing background fill. We need to create a NSLayoutManager subclasses override drawBackgroundForGlyphRange: atPoint:.

@interface IGSomeCustomLayoutManager : NSLayoutManager
@end 
@implementation IGSomeCustomLayoutManager
- (void)drawBackgroundForGlyphRange:(NSRange)glyphsToShow atPoint:(CGPoint)origin {
    // Draw custom background fill
    [superdrawBackgroundForGlyphRange:glyphsToShow atPoint:origin]; }}];@end
Copy the code

Through drawBackgroundForGlyphRange: atPoint method, We can use again [NSLayoutManager enumerateLineFragmentsForGlyphRange: usingBlock] to get the scope of each line segment glyph. Then use [NSLayoutManager boundingRectForGlyphRange: inTextContainer] for every line of the bounding rectangle.

- (void)drawBackgroundForGlyphRange:(NSRange)glyphsToShow atPoint:(CGPoint)origin {
  [self enumerateLineFragmentsForGlyphRange:NSMakeRange(0.self.numberOfGlyphs) usingBlock:^(CGRect rect, CGRect usedRect, NSTextContainer * _Nonnull textContainer, NSRange glyphRange, BOOL * _Nonnull stop) {
       CGRect lineBoundingRect = [self boundingRectForGlyphRange:glyphRange inTextContainer:textContainer];
       CGRect adjustedLineRect = CGRectOffset(lineBoundingRect, origin.x + kSomePadding, origin.y + kSomePadding);
       UIBezierPath *fillColorPath = [UIBezierPath bezierPathWithRoundedRect:adjustedLineRect cornerRadius:kSomeCornerRadius];
       [[UIColor redColor] setFill];
       [fillColorPath fill];
  }];
}
Copy the code

This allows us to draw a background fill for any text with a specified shape and spacing. NSLayoutManager can also be used to draw other text properties, such as strikeouts and underscores.

Android

At first glance, it feels like this should be easy to do on Android. We can add a span to change the text background color:

new CharacterStyle() {
  @Override
  public void updateDrawState(TextPaint textPaint) { textPaint.bgColor = color; }}Copy the code

This was a good first try (and the first code we built), but it had some limitations:

  1. The background was so tightly wrapped around the text that it was impossible to adjust the spacing.
  2. The background is rectangular and you can’t adjust the rounded corners.

To solve these problems, we tried using LineBackgroundSpan. We’ve already used it to render a circular bubble background for classic fonts, so it should naturally work with new text styles as well. Unfortunately, our new use case found a subtle bug in the Layout framework class. If your text has multiple instances of LineBackgroundSpan on different lines, the Layout will not traverse them correctly, and some of them may never be rendered.

Thankfully, we can avoid framing errors by applying a single LineBackgroundSpan to the entire string, and then drawing each background span ourselves in turn:

class BackgroundCoordinator implements LineBackgroundSpan {
  @Override
  public void drawBackground(
      Canvas canvas,
      Paint paint,
      int left,
      int right,
      int top,
      int baseline,
      int bottom,
      CharSequence text,
      int start,
      int end,
      int currentLine) {
    Spanned spanned = (Spanned) text;
    for(BackgroundSpan span : spanned.getSpans(start, end, BackgroundSpan.class)) { span.draw(canvas, spanned); }}}class BackgroundSpan {
  public void draw(Canvas canvas, Spanned spanned) {
    // Custom background rendering...}}Copy the code

conclusion

Instagram has a very strong culture of prototyping, and the design team’s Type Mode prototype gave us a real user experience in every iteration. For example, with a neon style, we need a way to get a single color from the palette and then generate the interior and glow colors for the text. The designer of this project used a few methods in his prototype, and when he found something he liked, we basically just copied his logic on Android and iOS. This level of collaboration with the design team is a special part of the launch and makes the development process very efficient.

If you’re interested in working with us on Story, check out our careers page for jobs in Menlo Park, New York, and San Francisco.

Christopher Wendel and Patrick Theisen are Instagram’s iOS and Android engineers, respectively.


The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.