问题描述
编辑:我想我应该使用UILabel而不是UITextView,因为我不希望突出显示使用系统范围的蓝色和'copy / select all / define'popover。我正在尝试构建一个自定义文本视图,我正在寻找一些正确方法的帮助。我是一个iOS n00b,所以主要是寻找有关如何最好地解决问题的想法。
我想构建一个自定义文本视图,具有以下行为:
- 查看从小开始。如果它收到一个点击它会增长(动画)到父视图的大小作为模态窗口(左上图,然后右上图)
- 在这个大的状态下,如果一个人单词被点击,单词以某种方式突出显示,并且委托方法被称为传递包含被点击的字符串(左下图)
我怀疑复制是在识别被点击的单词,所以让我们从那里开始。我可以想到几种尝试方法:
- 子类UILabel。添加触摸手势识别器。识别触摸时,以某种方式获取触摸点的(x.y)坐标,查看视图显示的字符串,并根据位置确定必须按下哪个字。然而,我可以通过自动换行很快看到这变得非常复杂。我不确定如何获得触摸的(x,y)坐标(虽然我认为这很简单),或者如何获得每个角色等设备相关的文本宽度。我宁愿不去这条路线,除非有人可以说服我这不会太可怕!
- 子类UIView,并通过为每个单词添加UILabel伪造句子。测量每个UILabel的宽度并自己铺设。这似乎是一种更明智的方法,虽然我担心以这种方式布置文本也会比我开始尝试时的想法更难。
有更明智的方法吗?你可以用其他方式检索UILabel中触及的单词吗?
如果我选择选项2,那么我认为动画来自小 - >大文本可能是复杂的,因为这些词会以有趣的方式包裹起来。所以我想要另一个子视图 - 这次是UITextView - 将句子保持在小状态。将此动画设置为大,然后将其隐藏,同时以每个单词一个视图显示我的UIView。
任何想法都表示赞赏。在此先感谢:)
更新
由于在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)){
//在范围内用词做你的事'enclosingRange'
* stop = YES;
}
}];
}
原始答案,适用于iOS< 7
感谢@JP Hribovsek提供了一些有关此工作的提示,我设法为我的目的解决了这个问题。它感觉有点hacky,对于大型文本可能不会很好,但对于一次的段落(这是我需要的)它没关系。
我创建了一个简单的UILabel子类,允许我设置插入值:
#importWWLabel.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;
}
返回自我;
}
- (void)drawTextInRect:(CGRect)rect
{
UIEdgeInsets insets = {self.topInset,self.leftInset,
self。 bottomInset,self.rightInset};
return [super drawTextInRect:UIEdgeInsetsInsetRect(rect,insets)];
}
然后我创建了一个包含我的自定义标签的UIView子类,并在点击后构建标签中每个单词的文本大小,直到大小超过点按位置的大小 - 这是被点击的单词。它不是完美的,但现在效果还不错。
然后我用一个简单的NSAttributedString来突出显示文字:
#importWWPhoneticTextView.h
#importWWLabel.h
#define WWPhoneticTextViewInset 5
#define WWPhoneticTextViewDefaultColor [UIColor blackColor]
#define WWPhoneticTextViewHighlightColor [UIColor yellowColor]
#define UILabelMagicTopMargin 5
#define UILabelMagicLeftMargin -5
@implementation WWPhoneticTextView {
WWLabel *标签;
NSMutableAttributedString * labelText;
NSRange tappedRange;
}
// ...跳过初始化方法,非常简单,只需调用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];
}
//设置点击处理
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)
{
//获取点击的位置,并对文本视图进行标准化(无边距)
CGPoint tapPoint = [sender locationInView:sender.view];
tapPoint.x = tapPoint.x - WWPhoneticTextViewInset - UILabelMagicLeftMargin;
tapPoint.y = tapPoint.y - WWPhoneticTextViewInset - UILabelMagicTopMargin;
//迭代每个单词,并检查单词是否包含正确行中的分接点
__block NSString * partialString = @;
__block NSString * lineString = @;
__block int currentLineHeight = label.font.pointSize;
[label.text enumerateSubstringsInRange:NSMakeRange(0,[label.text length])选项: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];
//找到部分字符串的大小,如果我们点击单词
CGSize partialStringSize = [partialString sizeWithFont:label.font constrainedToSize:sizeForText lineBreakMode:label.lineBreakMode] ;
if(partialStringSize.height> currentLineHeight){
//文本换行到新行
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;
}
}];
}
}
EDIT: I'm thinking I should use a UILabel instead of a UITextView as I don't want the highlight to use the system wide blue with 'copy/select all/define' popover.
I'm trying to build a custom text view, and I'm looking for some help with the right approach. I'm an iOS n00b so mainly looking for ideas about how best to approach the problem.
I want to build a custom text view, with the following behaviour:
- View starts off small. If it receives a tap it grows (animated) to the size of the parent view as a modal window (top left picture, then top right picture)
- In this large state, if an individual word is tapped, the word highlights in some fashion, and a delegate method is called passing a string containing the that was tapped (bottom left picture)
New Text View http://telliott.net/static/NewTextView.png
I suspect the complexity is going to be in the identifying the word that was clicked, so let's start there. I can think of a couple of ways of trying this:
- Subclass UILabel. Add a touch gesture recognizer. When a touch is identified, get the (x.y) coordinates of the touch point somehow, look at the string the view is showing and figure out which word must have been pressed based on position. I can see this getting very complicated pretty quickly with word wrapping however. I'm not sure how to get the (x,y) coordinates of the touch (although I assume that's pretty simple), or how to get device dependant text widths for each character etc. I'd rather not go down this route unless someone can convince me it's not going to be horrible!
- Subclass UIView, and fake the sentence by adding a UILabel for each word. Measure the width of each UILabel and lay them out myself. This seems like a more sensible approach, although I worry that laying out the text in this way is also going to be harder than I think by the time I start trying.
Is there a more sensible way? Can you retrieve a word that was touched in a UILabel some other way?
If I go for option 2, then I think the animation from small -> large text might be complicated, as the words will wrap in interesting ways. So I was thinking of having another subview - this time a UITextView - to hold the sentence in the small state. Animate this to large, then hide it, and at the same time reveal my UIView with one-view-per-word.
Any thoughts appreciated. Thanks in advance :)
Update
This got a lot easier for iOS 7, thanks to the addition of NSLayoutManager in CoreText. If you're dealing with a UITextView you can access the layout manager as a property of the view. In my case I wanted to stick with a UILabel, so you have to create a layout manager with the same size, i.e:
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];
Now you just need to find the index of the character that was clicked, which is simple!
NSUInteger characterIndex = [layoutManager characterIndexForPoint:location
inTextContainer:textContainer
fractionOfDistanceBetweenInsertionPoints:NULL];
Which makes it trivial to find the word itself:
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;
}
}];
}
Original Answer, which works for iOS < 7
Thanks to @JP Hribovsek for some tips getting this working, I managed to solve this well enough for my purposes. It feels a little hacky, and likely wouldn't work too well for large bodies of text, but for paragraphs at a time (which is what I need) it's fine.
I created a simple UILabel subclass that allows me to set the inset value:
#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)];
}
Then I created a UIView subclass that contained my custom label, and on tap constructed the size of the text for each word in the label, until the size exceeded that of the tap location - this is the word that was tapped. It's not prefect, but works well enough for now.
I then used a simple NSAttributedString to highlight the text:
#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;
}
}];
}
}
这篇关于在UITextView中选择一个单词的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!