Skip to content

Instantly share code, notes, and snippets.

@hansemannn
Created November 1, 2016 11:25
Show Gist options
  • Save hansemannn/eb81b09926a64b208268741da4cf3711 to your computer and use it in GitHub Desktop.
Save hansemannn/eb81b09926a64b208268741da4cf3711 to your computer and use it in GitHub Desktop.
/**
* Appcelerator Titanium Mobile
* Copyright (c) 2009-2015 by Appcelerator, Inc. All Rights Reserved.
* Licensed under the terms of the Apache Public License
* Please see the LICENSE included with this distribution for details.
*/
#ifdef USE_TI_UILABEL
#import "TiUILabel.h"
#import "TiUILabelProxy.h"
#import "TiUtils.h"
#import "UIImage+Resize.h"
#import <CoreText/CoreText.h>
#ifdef USE_TI_UIATTRIBUTEDSTRING
#import "TiUIAttributedStringProxy.h"
#endif
@implementation TiUILabel
#pragma mark Internal
#ifdef TI_USE_AUTOLAYOUT
-(void)initializeTiLayoutView
{
[super initializeTiLayoutView];
[self setDefaultHeight:TiDimensionAutoSize];
[self setDefaultWidth:TiDimensionAutoSize];
}
#endif
-(id)init
{
if (self = [super init]) {
padding = CGRectZero;
initialLabelFrame = CGRectZero;
verticalAlign = UIControlContentVerticalAlignmentFill;
}
return self;
}
-(void)dealloc
{
RELEASE_TO_NIL(label);
RELEASE_TO_NIL(wrapperView);
[super dealloc];
}
- (BOOL)interactionDefault
{
// by default, labels don't have any interaction unless you explicitly add
// it via addEventListener
return NO;
}
-(CGSize)sizeForFont:(CGFloat)suggestedWidth
{
NSAttributedString *value = [label attributedText];
CGSize maxSize = CGSizeMake(suggestedWidth<=0 ? 480 : suggestedWidth, 10000);
CGSize shadowOffset = [label shadowOffset];
requiresLayout = YES;
if ((suggestedWidth > 0) && [[label text] hasSuffix:@" "]) {
// (CGSize)sizeWithFont:(UIFont *)font constrainedToSize:(CGSize)size lineBreakMode:(UILineBreakMode)lineBreakMode method truncates
// the string having trailing spaces when given size parameter width is equal to the expected return width, so we adjust it here.
maxSize.width += 0.00001;
}
CGSize returnVal = [value boundingRectWithSize:maxSize
options:NSStringDrawingUsesLineFragmentOrigin
context:nil].size;
CGSize size = CGSizeMake(ceilf(returnVal.width), ceilf(returnVal.height));
if (shadowOffset.width > 0)
{
// if we have a shadow and auto, we need to adjust to prevent
// font from clipping
size.width += shadowOffset.width + 10;
}
return size;
}
-(CGFloat)contentWidthForWidth:(CGFloat)suggestedWidth
{
/*
Why both? sizeThatFits returns the width with line break mode tail truncation and we like to
have atleast enough space to display one word. On the otherhand font measurement is unsuitable for
attributed strings till we move to the new measurement API. Hence take both and return MAX.
*/
CGFloat sizeThatFitsResult = [[self label] sizeThatFits:CGSizeMake(suggestedWidth, 0)].width;
CGFloat fontMeasurementResult = [self sizeForFont:suggestedWidth].width;
return (MAX(sizeThatFitsResult, fontMeasurementResult));
}
-(CGFloat)contentHeightForWidth:(CGFloat)width
{
return [[self label] sizeThatFits:CGSizeMake(width, 0)].height;
}
-(void)padLabel
{
#ifndef TI_USE_AUTOLAYOUT
CGSize actualLabelSize = [[self label] sizeThatFits:CGSizeMake(initialLabelFrame.size.width, 0)];
UIControlContentVerticalAlignment alignment = verticalAlign;
if (alignment == UIControlContentVerticalAlignmentFill) {
//IOS7 layout issue fix with attributed string.
if (actualLabelSize.height < initialLabelFrame.size.height) {
alignment = UIControlContentVerticalAlignmentCenter;
} else {
alignment = UIControlContentVerticalAlignmentTop;
}
}
if (alignment != UIControlContentVerticalAlignmentFill && ([label numberOfLines] != 1)) {
CGFloat originX = 0;
switch (label.textAlignment) {
case NSTextAlignmentRight:
originX = (initialLabelFrame.size.width - actualLabelSize.width);
break;
case NSTextAlignmentCenter:
originX = (initialLabelFrame.size.width - actualLabelSize.width)/2.0;
break;
default:
break;
}
if (originX < 0) {
originX = 0;
}
CGRect labelRect = CGRectMake(originX, 0, actualLabelSize.width, actualLabelSize.height);
switch (alignment) {
case UIControlContentVerticalAlignmentBottom:
labelRect.origin.y = initialLabelFrame.size.height - actualLabelSize.height;
break;
case UIControlContentVerticalAlignmentCenter:
labelRect.origin.y = (initialLabelFrame.size.height - actualLabelSize.height)/2;
if (labelRect.origin.y < 0) {
labelRect.size.height = (initialLabelFrame.size.height - labelRect.origin.y);
}
break;
default:
if (initialLabelFrame.size.height < actualLabelSize.height) {
labelRect.size.height = initialLabelFrame.size.height;
}
break;
}
[label setFrame:CGRectIntegral(labelRect)];
}
else {
[label setFrame:initialLabelFrame];
}
if ([self backgroundImageLayer] != nil && !CGRectIsEmpty(initialLabelFrame))
{
[self updateBackgroundImageFrameWithPadding];
}
return;
#endif
}
#ifndef TI_USE_AUTOLAYOUT
// FIXME: This isn't quite true. But the brilliant soluton wasn't so brilliant, because it screwed with layout in unpredictable ways.
// Sadly, there was a brilliant solution for fixing the blurring here, but it turns out there's a
// quicker fix: Make sure the label itself has an even height and width. Everything else is irrelevant.
-(void)setCenter:(CGPoint)newCenter
{
[super setCenter:CGPointMake(floorf(newCenter.x), floorf(newCenter.y))];
}
#endif
-(void)frameSizeChanged:(CGRect)frame bounds:(CGRect)bounds
{
#ifndef TI_USE_AUTOLAYOUT
initialLabelFrame = bounds;
[wrapperView setFrame:initialLabelFrame];
#endif
[self padLabel];
[super frameSizeChanged:frame bounds:bounds];
}
-(UILabel*)label
{
if (label==nil)
{
label = [[UILabel alloc] initWithFrame:CGRectZero];
label.backgroundColor = [UIColor clearColor];
label.numberOfLines = 0;
#ifndef TI_USE_AUTOLAYOUT
wrapperView = [[UIView alloc] initWithFrame:[self bounds]];
[wrapperView addSubview:label];
wrapperView.clipsToBounds = YES;
[wrapperView setUserInteractionEnabled:NO];
[self addSubview:wrapperView];
#else
[self addSubview:label];
#endif
minFontSize = 0;
}
return label;
}
-(BOOL)proxyHasGestureListeners
{
return [super proxyHasGestureListeners] || [(TiViewProxy*)[self proxy] _hasListeners:@"link" checkParent:NO];
}
-(void)ensureGestureListeners
{
if ([(TiViewProxy*)[self proxy] _hasListeners:@"link" checkParent:NO]) {
[[self gestureRecognizerForEvent:@"singletap"] setEnabled:YES];
}
[super ensureGestureListeners];
}
-(void)handleListenerRemovedWithEvent:(NSString *)event
{
ENSURE_UI_THREAD_1_ARG(event);
// unfortunately on a remove, we have to check all of them
// since we might be removing one but we still have others
if ([event isEqualToString:@"link"] || [event isEqualToString:@"singletap"]) {
BOOL enableListener = [self.proxy _hasListeners:@"singletap"] || [(TiViewProxy*)[self proxy] _hasListeners:@"link" checkParent:NO];
[[self gestureRecognizerForEvent:@"singletap"] setEnabled:enableListener];
} else {
[super handleListenerRemovedWithEvent:event];
}
}
- (NSTextContainer*)currentTextContainer
{
NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:self.label.bounds.size];
textContainer.lineFragmentPadding = 0;
textContainer.maximumNumberOfLines = (NSUInteger)self.label.numberOfLines;
textContainer.lineBreakMode = self.label.lineBreakMode;
return [textContainer autorelease];
}
- (NSUInteger)characterIndexAtPoint:(NSMutableAttributedString*)theString atPoint:(CGPoint)point
{
NSTextContainer* textContainer = self.currentTextContainer;
NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:theString];
NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
[textStorage addLayoutManager:layoutManager];
[layoutManager addTextContainer:textContainer];
[textStorage release];
// UILabel centers its text vertically, so adjust the point coordinates accordingly
NSRange glyphRange = [layoutManager glyphRangeForTextContainer:textContainer];
CGRect wholeTextRect = [layoutManager boundingRectForGlyphRange:glyphRange
inTextContainer:textContainer];
point.y -= (CGRectGetHeight(self.bounds)-CGRectGetHeight(wholeTextRect))/2;
// Bail early if point outside the whole text bounding rect
if (!CGRectContainsPoint(wholeTextRect, point)) {
RELEASE_TO_NIL(layoutManager);
return NSNotFound;
}
// ask the layoutManager which glyph is under this tapped point
NSUInteger glyphIdx = [layoutManager glyphIndexForPoint:point
inTextContainer:textContainer
fractionOfDistanceThroughGlyph:NULL];
// as explained in Apple's documentation the previous method returns the nearest glyph
// if no glyph was present at that point. So if we want to ensure the point actually
// lies on that glyph, we should check that explicitly
CGRect glyphRect = [layoutManager boundingRectForGlyphRange:NSMakeRange(glyphIdx, 1)
inTextContainer:textContainer];
if (CGRectContainsPoint(glyphRect, point)) {
NSUInteger index = [layoutManager characterIndexForGlyphAtIndex:glyphIdx];
RELEASE_TO_NIL(layoutManager);
return index;
} else {
RELEASE_TO_NIL(layoutManager);
return NSNotFound;
}
}
-(BOOL)checkLinkAttributeForString:(NSMutableAttributedString*)theString atPoint:(CGPoint)p
{
NSUInteger idx = [self characterIndexAtPoint:theString atPoint:p];
if (idx == NSNotFound) {
return NO;
}
NSRange theRange = NSMakeRange(0, 0);
NSString *url = nil;
NSString *url2 = nil;
#ifdef USE_TI_UIATTRIBUTEDSTRING
TiUIAttributedStringProxy *tempString = [[self proxy] valueForKey:@"attributedString"];
url = [tempString getLink:idx];
#endif
if (url == nil || url.length == 0) {
return NO;
}
NSDictionary *eventDict = [NSDictionary dictionaryWithObjectsAndKeys:
url, @"url",
[NSArray arrayWithObjects:NUMUINTEGER(theRange.location), NUMUINTEGER(theRange.length),nil],@"range",
nil];
[[self proxy] fireEvent:@"link" withObject:eventDict propagate:NO reportSuccess:NO errorCode:0 message:nil];
return YES;
}
-(void)recognizedTap:(UITapGestureRecognizer*)recognizer
{
BOOL testLink = (label != nil) &&([(TiViewProxy*)[self proxy] _hasListeners:@"link" checkParent:NO]);
CGPoint tapPoint = [recognizer locationInView:self];
NSDictionary *event = [TiUtils pointToDictionary:tapPoint];
if ([recognizer numberOfTouchesRequired] == 2) {
[self.proxy fireEvent:@"twofingertap" withObject:event];
}
else if ([recognizer numberOfTapsRequired] == 2) {
//Because double-tap suppresses touchStart and double-click, we must do this:
if ([self.proxy _hasListeners:@"touchstart"])
{
[self.proxy fireEvent:@"touchstart" withObject:event propagate:YES];
}
if ([self.proxy _hasListeners:@"dblclick"]) {
[self.proxy fireEvent:@"dblclick" withObject:event propagate:YES];
}
[self.proxy fireEvent:@"doubletap" withObject:event];
}
else {
[self.proxy fireEvent:@"singletap" withObject:event];
if (testLink) {
NSMutableAttributedString* optimizedAttributedText = [label.attributedText mutableCopy];
if (optimizedAttributedText != nil) {
// use label's font and lineBreakMode properties in case the attributedText does not contain such attributes
[label.attributedText enumerateAttributesInRange:NSMakeRange(0, [label.attributedText length]) options:0 usingBlock:^(NSDictionary *attrs, NSRange range, BOOL *stop) {
if (!attrs[(NSString*)kCTFontAttributeName]) {
[optimizedAttributedText addAttribute:(NSString*)kCTFontAttributeName value:label.font range:range];
}
if (!attrs[(NSString*)kCTParagraphStyleAttributeName]) {
NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
[paragraphStyle setLineBreakMode:label.lineBreakMode];
[optimizedAttributedText addAttribute:(NSString*)kCTParagraphStyleAttributeName value:paragraphStyle range:range];
RELEASE_TO_NIL(paragraphStyle);
}
}];
// modify kCTLineBreakByTruncatingTail lineBreakMode to kCTLineBreakByWordWrapping
[optimizedAttributedText enumerateAttribute:(NSString*)kCTParagraphStyleAttributeName inRange:NSMakeRange(0, [optimizedAttributedText length]) options:0 usingBlock:^(id value, NSRange range, BOOL *stop) {
NSMutableParagraphStyle* paragraphStyle = [value mutableCopy];
if ([paragraphStyle lineBreakMode] == NSLineBreakByTruncatingTail) {
[paragraphStyle setLineBreakMode:NSLineBreakByWordWrapping];
}
[optimizedAttributedText removeAttribute:(NSString*)kCTParagraphStyleAttributeName range:range];
[optimizedAttributedText addAttribute:(NSString*)kCTParagraphStyleAttributeName value:paragraphStyle range:range];
RELEASE_TO_NIL(paragraphStyle);
}];
[self checkLinkAttributeForString:optimizedAttributedText atPoint:tapPoint];
[optimizedAttributedText release];
}
}
}
}
- (id)accessibilityElement
{
return [self label];
}
-(void)setHighlighted:(BOOL)newValue
{
[[self label] setHighlighted:newValue];
}
- (void)didMoveToSuperview
{
/*
* Because of how we re-use the same cells in both a tableview and its
* search table, there is the chance that the label is transported between
* the two views before a selected search row is deselected. In other
* words, make sure we're not highlighted when changing superviews.
*/
[self setHighlighted:NO];
[super didMoveToSuperview];
}
- (void)didMoveToWindow
{
/*
* See above
*/
[self setHighlighted:NO];
[super didMoveToWindow];
}
-(BOOL)isHighlighted
{
return [[self label] isHighlighted];
}
#pragma mark Public APIs
-(void)setVerticalAlign_:(id)value
{
verticalAlign = [TiUtils intValue:value def:UIControlContentVerticalAlignmentFill];
if (verticalAlign < UIControlContentVerticalAlignmentCenter || verticalAlign > UIControlContentVerticalAlignmentBottom) {
verticalAlign = UIControlContentVerticalAlignmentFill;
}
if (label != nil) {
[self padLabel];
}
}
-(void)setMaxLines_:(id)value
{
ENSURE_TYPE(value, NSNumber);
[[self label] setNumberOfLines:[TiUtils floatValue:value]];
[self padLabel];
[(TiViewProxy *)[self proxy] contentsWillChange];
}
-(void)setText_:(id)text
{
[[self label] setText:[TiUtils stringValue:text]];
[self padLabel];
[(TiViewProxy *)[self proxy] contentsWillChange];
}
-(void)setColor_:(id)color
{
UIColor * newColor = [[TiUtils colorValue:color] _color];
[[self label] setTextColor:(newColor != nil)?newColor:[UIColor darkTextColor]];
}
-(void)setEllipsize_:(id)value
{
ENSURE_SINGLE_ARG(value, NSNumber);
if ([[TiUtils stringValue:value] isEqualToString:@"true"]) {
[[self label] setLineBreakMode:NSLineBreakByTruncatingTail];
return;
}
[[self label] setLineBreakMode:[TiUtils intValue:value]];
}
-(void)setHighlightedColor_:(id)color
{
UIColor * newColor = [[TiUtils colorValue:color] _color];
[[self label] setHighlightedTextColor:(newColor != nil)?newColor:[UIColor lightTextColor]];
}
-(void)setFont_:(id)font
{
[[self label] setFont:[[TiUtils fontValue:font] font]];
if (minFontSize > 4) {
CGFloat ratio = minFontSize/label.font.pointSize;
[label setMinimumScaleFactor:ratio];
}
[(TiViewProxy *)[self proxy] contentsWillChange];
}
-(void)setMinimumFontSize_:(id)size
{
minFontSize = [TiUtils floatValue:size];
if (minFontSize < 4) { // Beholden to 'most minimum' font size
[[self label] setAdjustsFontSizeToFitWidth:NO];
[label setMinimumScaleFactor:0.0];
[label setNumberOfLines:0];
}
else {
[[self label] setNumberOfLines:1];
[label setAdjustsFontSizeToFitWidth:YES];
CGFloat ratio = minFontSize/label.font.pointSize;
[label setMinimumScaleFactor:ratio];
}
}
-(void) updateBackgroundImageFrameWithPadding
{
CGRect backgroundFrame = CGRectMake(self.bounds.origin.x - padding.origin.x,
self.bounds.origin.y - padding.origin.y,
self.bounds.size.width + padding.origin.x + padding.size.width,
self.bounds.size.height + padding.origin.y + padding.size.height);
[self backgroundImageLayer].frame = backgroundFrame;
}
-(void)setAttributedString_:(id)arg
{
#ifdef USE_TI_UIATTRIBUTEDSTRING
ENSURE_SINGLE_ARG(arg, TiUIAttributedStringProxy);
[[self proxy] replaceValue:arg forKey:@"attributedString" notification:NO];
[[self label] setAttributedText:[arg attributedString]];
[self padLabel];
[(TiViewProxy *)[self proxy] contentsWillChange];
#endif
}
-(void)setBackgroundPaddingLeft_:(id)left
{
padding.origin.x = [TiUtils floatValue:left];
[self updateBackgroundImageFrameWithPadding];
}
-(void)setBackgroundPaddingRight_:(id)right
{
padding.size.width = [TiUtils floatValue:right];
[self updateBackgroundImageFrameWithPadding];
}
-(void)setBackgroundPaddingTop_:(id)top
{
padding.origin.y = [TiUtils floatValue:top];
[self updateBackgroundImageFrameWithPadding];
}
-(void)setBackgroundPaddingBottom_:(id)bottom
{
padding.size.height = [TiUtils floatValue:bottom];
[self updateBackgroundImageFrameWithPadding];
}
-(void)setTextAlign_:(id)alignment
{
[[self label] setTextAlignment:[TiUtils textAlignmentValue:alignment]];
[self padLabel];
}
-(void)setShadowColor_:(id)color
{
if (color==nil)
{
[[self label] setShadowColor:nil];
}
else
{
color = [TiUtils colorValue:color];
[[self label] setShadowColor:[color _color]];
}
}
-(void)setShadowOffset_:(id)value
{
CGPoint p = [TiUtils pointValue:value];
CGSize size = {p.x,p.y};
[[self label] setShadowOffset:size];
}
@end
#endif
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment