From 3e8437ae559487362fae3525beb79c534c213a51 Mon Sep 17 00:00:00 2001 From: 单军华 Date: Thu, 12 Jul 2018 13:44:34 +0800 Subject: [PATCH] bug修复和功能优化 --- screendisplay/Pods/KILabel/KILabel/Source/KILabel.m | 767 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 files changed, 767 insertions(+), 0 deletions(-) diff --git a/screendisplay/Pods/KILabel/KILabel/Source/KILabel.m b/screendisplay/Pods/KILabel/KILabel/Source/KILabel.m new file mode 100755 index 0000000..37dbbff --- /dev/null +++ b/screendisplay/Pods/KILabel/KILabel/Source/KILabel.m @@ -0,0 +1,767 @@ +/*********************************************************************************** + * + * The MIT License (MIT) + * + * Copyright (c) 2013 Matthew Styles + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + ***********************************************************************************/ + +#import "KILabel.h" + +NSString * const KILabelLinkTypeKey = @"linkType"; +NSString * const KILabelRangeKey = @"range"; +NSString * const KILabelLinkKey = @"link"; + +#pragma mark - Private Interface + +@interface KILabel() + +// Used to control layout of glyphs and rendering +@property (nonatomic, retain) NSLayoutManager *layoutManager; + +// Specifies the space in which to render text +@property (nonatomic, retain) NSTextContainer *textContainer; + +// Backing storage for text that is rendered by the layout manager +@property (nonatomic, retain) NSTextStorage *textStorage; + +// Dictionary of detected links and their ranges in the text +@property (nonatomic, copy) NSArray *linkRanges; + +// State used to trag if the user has dragged during a touch +@property (nonatomic, assign) BOOL isTouchMoved; + +// During a touch, range of text that is displayed as selected +@property (nonatomic, assign) NSRange selectedRange; + +@end + +#pragma mark - Implementation + +@implementation KILabel +{ + NSMutableDictionary *_linkTypeAttributes; +} + +#pragma mark - Construction + +- (id)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self) + { + [self setupTextSystem]; + } + + return self; +} + +- (id)initWithCoder:(NSCoder *)aDecoder +{ + self = [super initWithCoder:aDecoder]; + if (self) + { + [self setupTextSystem]; + } + + return self; +} + +// Common initialisation. Must be done once during construction. +- (void)setupTextSystem +{ + // Create a text container and set it up to match our label properties + _textContainer = [[NSTextContainer alloc] init]; + _textContainer.lineFragmentPadding = 0; + _textContainer.maximumNumberOfLines = self.numberOfLines; + _textContainer.lineBreakMode = self.lineBreakMode; + _textContainer.size = self.frame.size; + + // Create a layout manager for rendering + _layoutManager = [[NSLayoutManager alloc] init]; + _layoutManager.delegate = self; + [_layoutManager addTextContainer:_textContainer]; + + // Attach the layou manager to the container and storage + [_textContainer setLayoutManager:_layoutManager]; + + // Make sure user interaction is enabled so we can accept touches + self.userInteractionEnabled = YES; + + // Don't go via public setter as this will have undesired side effect + _automaticLinkDetectionEnabled = YES; + + // All links are detectable by default + _linkDetectionTypes = KILinkTypeOptionAll; + + // Link Type Attributes. Default is empty (no attributes). + _linkTypeAttributes = [NSMutableDictionary dictionary]; + + // Don't underline URL links by default. + _systemURLStyle = NO; + + // By default we hilight the selected link during a touch to give feedback that we are + // responding to touch. + _selectedLinkBackgroundColor = [UIColor colorWithWhite:0.95 alpha:1.0]; + + // Establish the text store with our current text + [self updateTextStoreWithText]; +} + +#pragma mark - Text and Style management + +- (void)setAutomaticLinkDetectionEnabled:(BOOL)decorating +{ + _automaticLinkDetectionEnabled = decorating; + + // Make sure the text is updated properly + [self updateTextStoreWithText]; +} + +- (void)setLinkDetectionTypes:(KILinkTypeOption)linkDetectionTypes +{ + _linkDetectionTypes = linkDetectionTypes; + + // Make sure the text is updated properly + [self updateTextStoreWithText]; +} + +- (NSDictionary *)linkAtPoint:(CGPoint)location +{ + // Do nothing if we have no text + if (_textStorage.string.length == 0) + { + return nil; + } + + // Work out the offset of the text in the view + CGPoint textOffset = [self calcGlyphsPositionInView]; + + // Get the touch location and use text offset to convert to text cotainer coords + location.x -= textOffset.x; + location.y -= textOffset.y; + + NSUInteger touchedChar = [_layoutManager glyphIndexForPoint:location inTextContainer:_textContainer]; + + // If the touch is in white space after the last glyph on the line we don't + // count it as a hit on the text + NSRange lineRange; + CGRect lineRect = [_layoutManager lineFragmentUsedRectForGlyphAtIndex:touchedChar effectiveRange:&lineRange]; + if (CGRectContainsPoint(lineRect, location) == NO) + return nil; + + // Find the word that was touched and call the detection block + for (NSDictionary *dictionary in self.linkRanges) + { + NSRange range = [[dictionary objectForKey:KILabelRangeKey] rangeValue]; + + if ((touchedChar >= range.location) && touchedChar < (range.location + range.length)) + { + return dictionary; + } + } + + return nil; +} + +// Applies background color to selected range. Used to hilight touched links +- (void)setSelectedRange:(NSRange)range +{ + // Remove the current selection if the selection is changing + if (self.selectedRange.length && !NSEqualRanges(self.selectedRange, range)) + { + [_textStorage removeAttribute:NSBackgroundColorAttributeName range:self.selectedRange]; + } + + // Apply the new selection to the text + if (range.length && _selectedLinkBackgroundColor != nil) + { + [_textStorage addAttribute:NSBackgroundColorAttributeName value:_selectedLinkBackgroundColor range:range]; + } + + // Save the new range + _selectedRange = range; + + [self setNeedsDisplay]; +} + +- (void)setNumberOfLines:(NSInteger)numberOfLines +{ + [super setNumberOfLines:numberOfLines]; + + _textContainer.maximumNumberOfLines = numberOfLines; +} + +- (void)setText:(NSString *)text +{ + // Pass the text to the super class first + [super setText:text]; + + // Update our text store with an attributed string based on the original + // label text properties. + if (!text) + { + text = @""; + } + + NSAttributedString *attributedText = [[NSAttributedString alloc] initWithString:text attributes:[self attributesFromProperties]]; + [self updateTextStoreWithAttributedString:attributedText]; +} + +- (void)setAttributedText:(NSAttributedString *)attributedText +{ + // Pass the text to the super class first + [super setAttributedText:attributedText]; + + [self updateTextStoreWithAttributedString:attributedText]; +} + +- (void)setSystemURLStyle:(BOOL)systemURLStyle +{ + _systemURLStyle = systemURLStyle; + + // Force refresh + self.text = self.text; +} + +- (NSDictionary*)attributesForLinkType:(KILinkType)linkType +{ + NSDictionary *attributes = _linkTypeAttributes[@(linkType)]; + + if (!attributes) + { + attributes = @{NSForegroundColorAttributeName : self.tintColor}; + } + + return attributes; +} + +- (void)setAttributes:(NSDictionary*)attributes forLinkType:(KILinkType)linkType +{ + if (attributes) + { + _linkTypeAttributes[@(linkType)] = attributes; + } + else + { + [_linkTypeAttributes removeObjectForKey:@(linkType)]; + } + + // Force refresh text + self.text = self.text; +} + +#pragma mark - Text Storage Management + +- (void)updateTextStoreWithText +{ + // Now update our storage from either the attributedString or the plain text + if (self.attributedText) + { + [self updateTextStoreWithAttributedString:self.attributedText]; + } + else if (self.text) + { + [self updateTextStoreWithAttributedString:[[NSAttributedString alloc] initWithString:self.text attributes:[self attributesFromProperties]]]; + } + else + { + [self updateTextStoreWithAttributedString:[[NSAttributedString alloc] initWithString:@"" attributes:[self attributesFromProperties]]]; + } + + [self setNeedsDisplay]; +} + +- (void)updateTextStoreWithAttributedString:(NSAttributedString *)attributedString +{ + if (attributedString.length != 0) + { + attributedString = [KILabel sanitizeAttributedString:attributedString]; + } + + if (self.isAutomaticLinkDetectionEnabled && (attributedString.length != 0)) + { + self.linkRanges = [self getRangesForLinks:attributedString]; + attributedString = [self addLinkAttributesToAttributedString:attributedString linkRanges:self.linkRanges]; + } + else + { + self.linkRanges = nil; + } + + if (_textStorage) + { + // Set the string on the storage + [_textStorage setAttributedString:attributedString]; + } + else + { + // Create a new text storage and attach it correctly to the layout manager + _textStorage = [[NSTextStorage alloc] initWithAttributedString:attributedString]; + [_textStorage addLayoutManager:_layoutManager]; + [_layoutManager setTextStorage:_textStorage]; + } +} + +// Returns attributed string attributes based on the text properties set on the label. +// These are styles that are only applied when NOT using the attributedText directly. +- (NSDictionary *)attributesFromProperties +{ + // Setup shadow attributes + NSShadow *shadow = shadow = [[NSShadow alloc] init]; + if (self.shadowColor) + { + shadow.shadowColor = self.shadowColor; + shadow.shadowOffset = self.shadowOffset; + } + else + { + shadow.shadowOffset = CGSizeMake(0, -1); + shadow.shadowColor = nil; + } + + // Setup color attributes + UIColor *color = self.textColor; + if (!self.isEnabled) + { + color = [UIColor lightGrayColor]; + } + else if (self.isHighlighted) + { + color = self.highlightedTextColor; + } + + // Setup paragraph attributes + NSMutableParagraphStyle *paragraph = [[NSMutableParagraphStyle alloc] init]; + paragraph.alignment = self.textAlignment; + + // Create the dictionary + NSDictionary *attributes = @{NSFontAttributeName : self.font, + NSForegroundColorAttributeName : color, + NSShadowAttributeName : shadow, + NSParagraphStyleAttributeName : paragraph, + }; + return attributes; +} + +/** + * Returns array of ranges for all special words, user handles, hashtags and urls in the specfied + * text. + * + * @param text Text to parse for links + * + * @return Array of dictionaries describing the links. + */ +- (NSArray *)getRangesForLinks:(NSAttributedString *)text +{ + NSMutableArray *rangesForLinks = [[NSMutableArray alloc] init]; + + if (self.linkDetectionTypes & KILinkTypeOptionUserHandle) + { + [rangesForLinks addObjectsFromArray:[self getRangesForUserHandles:text.string]]; + } + + if (self.linkDetectionTypes & KILinkTypeOptionHashtag) + { + [rangesForLinks addObjectsFromArray:[self getRangesForHashtags:text.string]]; + } + + if (self.linkDetectionTypes & KILinkTypeOptionURL) + { + [rangesForLinks addObjectsFromArray:[self getRangesForURLs:self.attributedText]]; + } + + return rangesForLinks; +} + +- (NSArray *)getRangesForUserHandles:(NSString *)text +{ + NSMutableArray *rangesForUserHandles = [[NSMutableArray alloc] init]; + + // Setup a regular expression for user handles and hashtags + static NSRegularExpression *regex = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSError *error = nil; + regex = [[NSRegularExpression alloc] initWithPattern:@"(?<!\\w)@([\\w\\_]+)?" options:0 error:&error]; + }); + + // Run the expression and get matches + NSArray *matches = [regex matchesInString:text options:0 range:NSMakeRange(0, text.length)]; + + // Add all our ranges to the result + for (NSTextCheckingResult *match in matches) + { + NSRange matchRange = [match range]; + NSString *matchString = [text substringWithRange:matchRange]; + + if (![self ignoreMatch:matchString]) + { + [rangesForUserHandles addObject:@{KILabelLinkTypeKey : @(KILinkTypeUserHandle), + KILabelRangeKey : [NSValue valueWithRange:matchRange], + KILabelLinkKey : matchString + }]; + } + } + + return rangesForUserHandles; +} + +- (NSArray *)getRangesForHashtags:(NSString *)text +{ + NSMutableArray *rangesForHashtags = [[NSMutableArray alloc] init]; + + // Setup a regular expression for user handles and hashtags + static NSRegularExpression *regex = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSError *error = nil; + regex = [[NSRegularExpression alloc] initWithPattern:@"(?<!\\w)#([\\w\\_]+)?" options:0 error:&error]; + }); + + // Run the expression and get matches + NSArray *matches = [regex matchesInString:text options:0 range:NSMakeRange(0, text.length)]; + + // Add all our ranges to the result + for (NSTextCheckingResult *match in matches) + { + NSRange matchRange = [match range]; + NSString *matchString = [text substringWithRange:matchRange]; + + if (![self ignoreMatch:matchString]) + { + [rangesForHashtags addObject:@{KILabelLinkTypeKey : @(KILinkTypeHashtag), + KILabelRangeKey : [NSValue valueWithRange:matchRange], + KILabelLinkKey : matchString, + }]; + } + } + + return rangesForHashtags; +} + + +- (NSArray *)getRangesForURLs:(NSAttributedString *)text +{ + NSMutableArray *rangesForURLs = [[NSMutableArray alloc] init];; + + // Use a data detector to find urls in the text + NSError *error = nil; + NSDataDetector *detector = [[NSDataDetector alloc] initWithTypes:NSTextCheckingTypeLink error:&error]; + + NSString *plainText = text.string; + + NSArray *matches = [detector matchesInString:plainText + options:0 + range:NSMakeRange(0, text.length)]; + + // Add a range entry for every url we found + for (NSTextCheckingResult *match in matches) + { + NSRange matchRange = [match range]; + + // If there's a link embedded in the attributes, use that instead of the raw text + NSString *realURL = [text attribute:NSLinkAttributeName atIndex:matchRange.location effectiveRange:nil]; + if (realURL == nil) + realURL = [plainText substringWithRange:matchRange]; + + if (![self ignoreMatch:realURL]) + { + if ([match resultType] == NSTextCheckingTypeLink) + { + [rangesForURLs addObject:@{KILabelLinkTypeKey : @(KILinkTypeURL), + KILabelRangeKey : [NSValue valueWithRange:matchRange], + KILabelLinkKey : realURL, + }]; + } + } + } + + return rangesForURLs; +} + +- (BOOL)ignoreMatch:(NSString*)string +{ + return [_ignoredKeywords containsObject:[string lowercaseString]]; +} + +- (NSAttributedString *)addLinkAttributesToAttributedString:(NSAttributedString *)string linkRanges:(NSArray *)linkRanges +{ + NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithAttributedString:string]; + + for (NSDictionary *dictionary in linkRanges) + { + NSRange range = [[dictionary objectForKey:KILabelRangeKey] rangeValue]; + KILinkType linkType = [dictionary[KILabelLinkTypeKey] unsignedIntegerValue]; + + NSDictionary *attributes = [self attributesForLinkType:linkType]; + + // Use our tint color to hilight the link + [attributedString addAttributes:attributes range:range]; + + // Add an URL attribute if this is a URL + if (_systemURLStyle && ((KILinkType)[dictionary[KILabelLinkTypeKey] unsignedIntegerValue] == KILinkTypeURL)) + { + // Add a link attribute using the stored link + [attributedString addAttribute:NSLinkAttributeName value:dictionary[KILabelLinkKey] range:range]; + } + } + + return attributedString; +} + +#pragma mark - Layout and Rendering + +- (CGRect)textRectForBounds:(CGRect)bounds limitedToNumberOfLines:(NSInteger)numberOfLines +{ + // Use our text container to calculate the bounds required. First save our + // current text container setup + CGSize savedTextContainerSize = _textContainer.size; + NSInteger savedTextContainerNumberOfLines = _textContainer.maximumNumberOfLines; + + // Apply the new potential bounds and number of lines + _textContainer.size = bounds.size; + _textContainer.maximumNumberOfLines = numberOfLines; + + // Measure the text with the new state + CGRect textBounds = [_layoutManager usedRectForTextContainer:_textContainer]; + + // Position the bounds and round up the size for good measure + textBounds.origin = bounds.origin; + textBounds.size.width = ceil(textBounds.size.width); + textBounds.size.height = ceil(textBounds.size.height); + + if (textBounds.size.height < bounds.size.height) + { + // Take verical alignment into account + CGFloat offsetY = (bounds.size.height - textBounds.size.height) / 2.0; + textBounds.origin.y += offsetY; + } + + // Restore the old container state before we exit under any circumstances + _textContainer.size = savedTextContainerSize; + _textContainer.maximumNumberOfLines = savedTextContainerNumberOfLines; + + return textBounds; +} + +- (void)drawTextInRect:(CGRect)rect +{ + // Don't call super implementation. Might want to uncomment this out when + // debugging layout and rendering problems. + // [super drawTextInRect:rect]; + + // Calculate the offset of the text in the view + NSRange glyphRange = [_layoutManager glyphRangeForTextContainer:_textContainer]; + CGPoint glyphsPosition = [self calcGlyphsPositionInView]; + + // Drawing code + [_layoutManager drawBackgroundForGlyphRange:glyphRange atPoint:glyphsPosition]; + [_layoutManager drawGlyphsForGlyphRange:glyphRange atPoint:glyphsPosition]; +} + +// Returns the XY offset of the range of glyphs from the view's origin +- (CGPoint)calcGlyphsPositionInView +{ + CGPoint textOffset = CGPointZero; + + CGRect textBounds = [_layoutManager usedRectForTextContainer:_textContainer]; + textBounds.size.width = ceil(textBounds.size.width); + textBounds.size.height = ceil(textBounds.size.height); + + if (textBounds.size.height < self.bounds.size.height) + { + CGFloat paddingHeight = (self.bounds.size.height - textBounds.size.height) / 2.0; + textOffset.y = paddingHeight; + } + + return textOffset; +} + +- (void)setFrame:(CGRect)frame +{ + [super setFrame:frame]; + + _textContainer.size = self.bounds.size; +} + +- (void)setBounds:(CGRect)bounds +{ + [super setBounds:bounds]; + + _textContainer.size = self.bounds.size; +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + + // Update our container size when the view frame changes + _textContainer.size = self.bounds.size; +} + +- (void)setIgnoredKeywords:(NSSet *)ignoredKeywords +{ + NSMutableSet *set = [NSMutableSet setWithCapacity:ignoredKeywords.count]; + + [ignoredKeywords enumerateObjectsUsingBlock:^(id obj, BOOL *stop) { + [set addObject:[obj lowercaseString]]; + }]; + + _ignoredKeywords = [set copy]; +} + +#pragma mark - Interactions + +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event +{ + _isTouchMoved = NO; + + // Get the info for the touched link if there is one + NSDictionary *touchedLink; + CGPoint touchLocation = [[touches anyObject] locationInView:self]; + touchedLink = [self linkAtPoint:touchLocation]; + + if (touchedLink) + { + self.selectedRange = [[touchedLink objectForKey:KILabelRangeKey] rangeValue]; + } + else + { + [super touchesBegan:touches withEvent:event]; + } +} + +- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event +{ + [super touchesMoved:touches withEvent:event]; + + _isTouchMoved = YES; +} + +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event +{ + [super touchesEnded:touches withEvent:event]; + + // If the user dragged their finger we ignore the touch + if (_isTouchMoved) + { + self.selectedRange = NSMakeRange(0, 0); + + return; + } + + // Get the info for the touched link if there is one + NSDictionary *touchedLink; + CGPoint touchLocation = [[touches anyObject] locationInView:self]; + touchedLink = [self linkAtPoint:touchLocation]; + + if (touchedLink) + { + NSRange range = [[touchedLink objectForKey:KILabelRangeKey] rangeValue]; + NSString *touchedSubstring = [touchedLink objectForKey:KILabelLinkKey]; + KILinkType linkType = (KILinkType)[[touchedLink objectForKey:KILabelLinkTypeKey] intValue]; + + [self receivedActionForLinkType:linkType string:touchedSubstring range:range]; + } + else + { + [super touchesBegan:touches withEvent:event]; + } + + self.selectedRange = NSMakeRange(0, 0); +} + +- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event +{ + [super touchesCancelled:touches withEvent:event]; + + // Make sure we don't leave a selection when the touch is cancelled + self.selectedRange = NSMakeRange(0, 0); +} + +- (void)receivedActionForLinkType:(KILinkType)linkType string:(NSString*)string range:(NSRange)range +{ + switch (linkType) + { + case KILinkTypeUserHandle: + if (_userHandleLinkTapHandler) + { + _userHandleLinkTapHandler(self, string, range); + } + break; + + case KILinkTypeHashtag: + if (_hashtagLinkTapHandler) + { + _hashtagLinkTapHandler(self, string, range); + } + break; + + case KILinkTypeURL: + if (_urlLinkTapHandler) + { + _urlLinkTapHandler(self, string, range); + } + break; + } +} + +#pragma mark - Layout manager delegate + +-(BOOL)layoutManager:(NSLayoutManager *)layoutManager shouldBreakLineByWordBeforeCharacterAtIndex:(NSUInteger)charIndex +{ + // Don't allow line breaks inside URLs + NSRange range; + NSURL *linkURL = [layoutManager.textStorage attribute:NSLinkAttributeName atIndex:charIndex effectiveRange:&range]; + + return !(linkURL && (charIndex > range.location) && (charIndex <= NSMaxRange(range))); +} + ++ (NSAttributedString *)sanitizeAttributedString:(NSAttributedString *)attributedString +{ + // Setup paragraph alignement properly. IB applies the line break style + // to the attributed string. The problem is that the text container then + // breaks at the first line of text. If we set the line break to wrapping + // then the text container defines the break mode and it works. + // NOTE: This is either an Apple bug or something I've misunderstood. + + // Get the current paragraph style. IB only allows a single paragraph so + // getting the style of the first char is fine. + NSRange range; + NSParagraphStyle *paragraphStyle = [attributedString attribute:NSParagraphStyleAttributeName atIndex:0 effectiveRange:&range]; + + if (paragraphStyle == nil) + { + return attributedString; + } + + // Remove the line breaks + NSMutableParagraphStyle *mutableParagraphStyle = [paragraphStyle mutableCopy]; + mutableParagraphStyle.lineBreakMode = NSLineBreakByWordWrapping; + + // Apply new style + NSMutableAttributedString *restyled = [[NSMutableAttributedString alloc] initWithAttributedString:attributedString]; + [restyled addAttribute:NSParagraphStyleAttributeName value:mutableParagraphStyle range:NSMakeRange(0, restyled.length)]; + + return restyled; +} + +@end -- Gitblit v1.8.0