7

编辑:我想我应该使用 UILabel 而不是 UITextView 因为我不希望突出显示使用带有“复制/全选/定义”弹出框的系统范围的蓝色。

我正在尝试构建自定义文本视图,并且正在寻求正确方法的帮助。我是一个 iOS n00b,所以主要是寻找关于如何最好地解决问题的想法。

我想构建一个自定义文本视图,具有以下行为:

  1. 视图从小开始。如果它收到点击它会增长(动画)到父视图的大小作为模式窗口(左上图,然后右上图)
  2. 在这种大状态下,如果点击单个单词,该单词会以某种方式突出显示,并且调用委托方法传递包含被点击的字符串(左下图)

新文本视图
(来源:telliott.net

我怀疑复杂性将在于识别被点击的单词,所以让我们从那里开始。我可以想到几种尝试方法:

  1. 子类 UILabel。添加触摸手势识别器。当识别到触摸时,以某种方式获取触摸点的 (xy) 坐标,查看视图显示的字符串并根据位置确定必须按下哪个单词。然而,我可以看到这很快就变得非常复杂了。我不确定如何获取触摸的 (x,y) 坐标(尽管我认为这很简单),或者如何获取每个字符的设备相关文本宽度等。我宁愿不走这条路,除非有人可以说服我这不会很可怕!
  2. 子类 UIView,并通过为每个单词添加 UILabel 来伪造句子。测量每个 UILabel 的宽度并自己布置它们。这似乎是一种更明智的方法,尽管我担心当我开始尝试时,以这种方式布置文本也会比我想象的更难。

有没有更明智的方法?你能以其他方式检索在 UILabel 中触及的单词吗?

如果我选择选项 2,那么我认为从小文本到大文本的动画可能会很复杂,因为这些文字会以有趣的方式换行。所以我想有另一个子视图——这次是一个 UITextView——来将句子保持在小状态。将此动画放大,然后将其隐藏,同时以每个单词一个视图显示我的 UIView。

任何想法表示赞赏。提前致谢 :)

4

2 回答 2

9

更新

由于在 CoreText 中添加了 NSLayoutManager,这对于 iOS 7 来说变得容易多了。如果您正在处理 UITextView,您可以将布局管理器作为视图的属性访问。在我的情况下,我想坚持使用 UILabel,所以你必须创建一个相同大小的布局管理器,即:

NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:labelText];
NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
[textStorage addLayoutManager:layoutManager];
CGRect bounds = label.bounds;
NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:bounds.size];
[layoutManager addTextContainer:textContainer];

现在你只需要找到被点击的字符的索引,很简单!

NSUInteger characterIndex = [layoutManager characterIndexForPoint:location
                                                  inTextContainer:textContainer
                         fractionOfDistanceBetweenInsertionPoints:NULL];

这使得查找单词本身变得微不足道:

if (characterIndex < textStorage.length) {
  [labelText.string enumerateSubstringsInRange:NSMakeRange(0, textStorage.length)
                                       options:NSStringEnumerationByWords
                                    usingBlock:^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop) {
                                      if (NSLocationInRange(characterIndex, enclosingRange)) {
                                        // Do your thing with the word, at range 'enclosingRange'
                                        *stop = YES;
                                      }
                                    }];
}

原始答案,适用于 iOS < 7

感谢@JP Hribovsek 提供了一些使其工作的技巧,我设法解决了这个问题,足以满足我的目的。感觉有点 hacky,并且对于大量文本可能不太适用,但对于一次段落(这是我需要的)它很好。

我创建了一个简单的 UILabel 子类,它允许我设置插入值:

#import "WWLabel.h"

#define WWLabelDefaultInset 5

@implementation WWLabel

@synthesize topInset, leftInset, bottomInset, rightInset;

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        self.topInset = WWLabelDefaultInset;
        self.bottomInset = WWLabelDefaultInset;
        self.rightInset = WWLabelDefaultInset;
        self.leftInset = WWLabelDefaultInset;
    }
    return self;
}

- (void)drawTextInRect:(CGRect)rect
{
    UIEdgeInsets insets = {self.topInset, self.leftInset,
        self.bottomInset, self.rightInset};

    return [super drawTextInRect:UIEdgeInsetsInsetRect(rect, insets)];
}

然后我创建了一个包含我的自定义标签的 UIView 子类,并在点击时为标签中的每个单词构建文本的大小,直到大小超过点击位置的大小——这就是被点击的单词。它不是完美的,但现在工作得很好。

然后我使用了一个简单的 NSAttributedString 来突出显示文本:

#import "WWPhoneticTextView.h"
#import "WWLabel.h"

#define WWPhoneticTextViewInset 5
#define WWPhoneticTextViewDefaultColor [UIColor blackColor]
#define WWPhoneticTextViewHighlightColor [UIColor yellowColor]

#define UILabelMagicTopMargin 5
#define UILabelMagicLeftMargin -5

@implementation WWPhoneticTextView {
    WWLabel *label;
    NSMutableAttributedString *labelText;
    NSRange tappedRange;
}

// ... skipped init methods, very simple, just call through to configureView

- (void)configureView
{
    if(!label) {
        tappedRange.location = NSNotFound;
        tappedRange.length = 0;

        label = [[WWLabel alloc] initWithFrame:[self bounds]];
        [label setLineBreakMode:NSLineBreakByWordWrapping];
        [label setNumberOfLines:0];
        [label setBackgroundColor:[UIColor clearColor]];
        [label setTopInset:WWPhoneticTextViewInset];
        [label setLeftInset:WWPhoneticTextViewInset];
        [label setBottomInset:WWPhoneticTextViewInset];
        [label setRightInset:WWPhoneticTextViewInset];

        [self addSubview:label];
    }


    // Setup tap handling
    UITapGestureRecognizer *singleFingerTap = [[UITapGestureRecognizer alloc]
                                               initWithTarget:self action:@selector(handleSingleTap:)];
    singleFingerTap.numberOfTapsRequired = 1;
    [self addGestureRecognizer:singleFingerTap];
}

- (void)setText:(NSString *)text
{
    labelText = [[NSMutableAttributedString alloc] initWithString:text];
    [label setAttributedText:labelText];
}

- (void)handleSingleTap:(UITapGestureRecognizer *)sender
{
    if (sender.state == UIGestureRecognizerStateEnded)
    {
        // Get the location of the tap, and normalise for the text view (no margins)
        CGPoint tapPoint = [sender locationInView:sender.view];
        tapPoint.x = tapPoint.x - WWPhoneticTextViewInset - UILabelMagicLeftMargin;
        tapPoint.y = tapPoint.y - WWPhoneticTextViewInset - UILabelMagicTopMargin;

        // Iterate over each word, and check if the word contains the tap point in the correct line
        __block NSString *partialString = @"";
        __block NSString *lineString = @"";
        __block int currentLineHeight = label.font.pointSize;
        [label.text enumerateSubstringsInRange:NSMakeRange(0, [label.text length]) options:NSStringEnumerationByWords usingBlock:^(NSString* word, NSRange wordRange, NSRange enclosingRange, BOOL* stop){

            CGSize sizeForText = CGSizeMake(label.frame.size.width-2*WWPhoneticTextViewInset, label.frame.size.height-2*WWPhoneticTextViewInset);
            partialString = [NSString stringWithFormat:@"%@ %@", partialString, word];

            // Find the size of the partial string, and stop if we've hit the word
            CGSize partialStringSize  = [partialString sizeWithFont:label.font constrainedToSize:sizeForText lineBreakMode:label.lineBreakMode];

            if (partialStringSize.height > currentLineHeight) {
                // Text wrapped to new line
                currentLineHeight = partialStringSize.height;
                lineString = @"";
            }
            lineString = [NSString stringWithFormat:@"%@ %@", lineString, word];

            CGSize lineStringSize  = [lineString sizeWithFont:label.font constrainedToSize:label.frame.size lineBreakMode:label.lineBreakMode];
            lineStringSize.width = lineStringSize.width + WWPhoneticTextViewInset;

            if (tapPoint.x < lineStringSize.width && tapPoint.y > (partialStringSize.height-label.font.pointSize) && tapPoint.y < partialStringSize.height) {
                NSLog(@"Tapped word %@", word);
                if (tappedRange.location != NSNotFound) {
                    [labelText addAttribute:NSForegroundColorAttributeName value:[UIColor blackColor] range:tappedRange];
                }

                tappedRange = wordRange;
                [labelText addAttribute:NSForegroundColorAttributeName value:[UIColor redColor] range:tappedRange];
                [label setAttributedText:labelText];
                *stop = YES;
            }
        }];        
    }
}
于 2012-11-12T01:58:01.107 回答
1

UITextView 已经有一个在选择更改时触发的委托方法(请注意,在 textview 中移动光标相当于更改选择,用户实际上不需要“选择”任何文本来调用它):

- (void)textViewDidChangeSelection:(UITextView *)textView

每当触发此操作时,获取 selectedRange 如下所示:

NSRange range=textView.selectedRange;

如果用户应该能够手动移动光标或选择整个单词,那么你就完成了,否则,只需在 selectedRange 处的字符串周围添加一些处理,以确定光标周围的单词是什么,并突出显示它用你选择的方法。
例如,您可以枚举文本视图中的所有单词,并找出哪个包含当前选择(或光标),然后选择整个单词(这是突出显示先前 iOS 6 的一种方式)

- (void)textViewDidChangeSelection:(UITextView *)textView{
NSRange range=textView.selectedRange;
[textView.text enumerateSubstringsInRange:NSMakeRange(0, [textView.text length]) options:NSStringEnumerationByWords usingBlock:^(NSString* word, NSRange wordRange, NSRange enclosingRange, BOOL* stop){
    NSRange intersectionRange=NSIntersectionRange(range,wordRange);
    if(interesectionRange.length>0){
        [textView setSelectedRange:wordRange];
    }
}];
}
于 2012-11-10T03:03:08.250 回答