Michael Schneider 8897614f0e [Layout] Layout API based on content area (#2110)
* Initial commit for adding a size constraint to ASLayoutable's

* Some more commits

* Fix sample projects in extra/

* Remove preferredFrameSize test of ASEditableTextNode

* Remove preferredFrameSize from examples_extra

* Add deprecation warning to -[ASDisplayNode preferredFrameSize]

* Add deprecation warning to -[ASDisplayNode measureWithSizeRange:]

* Commit

* Commit

* Remove ASRelativeSizeRange

* Make ASRelativeSize private

* Adjust examples

* Improve setting of -[ASLayoutable size] with points or fractions

* Add ASWrapperLayoutSpec

* Improve creation of ASRelativeDimension

* Add `preferredFrameSize` back and add deprecated logging

* Add `layoutSpecBlock` setter and getter and add locking for it

* Add better documentation and fix macros to create ASRelativeDimension

* Create new ASSizeRangeMake with just a CGSize as parameter

* Update Kitten and Social App Layout example

* Add layoutThatFits: and deprecate measure:

* Rename ASRelativeDimension to ASDimension

* Fix examples for ASDimension renaming

* Remove fancy height and width setter

* Fix ASDimension helper

* Rename -[ASLayout layoutableObject] to -[ASLayout layoutable]

* Update layout related methods and more clearer documentation around how to use it

* Deprecate old ASLayout class constructors

* Don't unnecessary recalculate layout if constrained or parent size did not change

* Use shared pointer for ASDisplayNodeLayout

* Fix rebase conflicts

* Add documentation and move implementation in mm file of ASDisplayNodeLayout

* Fix test errors

* Rename ASSize to ASLayoutableSize

* Address comments

* Rename setSizeFromCGSize to setSizeWithCGSize

* Improve inline functions in ASDimension

* Fix rebase conflicts
2016-09-07 08:44:48 -07:00

347 lines
13 KiB
Objective-C

//
// PostNode.m
// Sample
//
// Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
// This source code is licensed under the BSD-style license found in the
// LICENSE file in the root directory of this source tree. An additional grant
// of patent rights can be found in the PATENTS file in the same directory.
//
// 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
// FACEBOOK 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 "PostNode.h"
#import "Post.h"
#import "TextStyles.h"
#import "LikesNode.h"
#import "CommentsNode.h"
#import "ASRelativeSize.h"
#define PostNodeDividerColor [UIColor lightGrayColor]
@interface PostNode() <ASNetworkImageNodeDelegate, ASTextNodeDelegate>
@property (strong, nonatomic) Post *post;
@property (strong, nonatomic) ASDisplayNode *divider;
@property (strong, nonatomic) ASTextNode *nameNode;
@property (strong, nonatomic) ASTextNode *usernameNode;
@property (strong, nonatomic) ASTextNode *timeNode;
@property (strong, nonatomic) ASTextNode *postNode;
@property (strong, nonatomic) ASImageNode *viaNode;
@property (strong, nonatomic) ASNetworkImageNode *avatarNode;
@property (strong, nonatomic) ASNetworkImageNode *mediaNode;
@property (strong, nonatomic) LikesNode *likesNode;
@property (strong, nonatomic) CommentsNode *commentsNode;
@property (strong, nonatomic) ASImageNode *optionsNode;
@end
@implementation PostNode
#pragma mark - Lifecycle
- (instancetype)initWithPost:(Post *)post
{
self = [super init];
if (self) {
_post = post;
// Name node
_nameNode = [[ASTextNode alloc] init];
_nameNode.attributedText = [[NSAttributedString alloc] initWithString:_post.name attributes:[TextStyles nameStyle]];
_nameNode.maximumNumberOfLines = 1;
[self addSubnode:_nameNode];
// Username node
_usernameNode = [[ASTextNode alloc] init];
_usernameNode.attributedText = [[NSAttributedString alloc] initWithString:_post.username attributes:[TextStyles usernameStyle]];
_usernameNode.flexShrink = YES; //if name and username don't fit to cell width, allow username shrink
_usernameNode.truncationMode = NSLineBreakByTruncatingTail;
_usernameNode.maximumNumberOfLines = 1;
[self addSubnode:_usernameNode];
// Time node
_timeNode = [[ASTextNode alloc] init];
_timeNode.attributedText = [[NSAttributedString alloc] initWithString:_post.time attributes:[TextStyles timeStyle]];
[self addSubnode:_timeNode];
// Post node
_postNode = [[ASTextNode alloc] init];
// Processing URLs in post
NSString *kLinkAttributeName = @"TextLinkAttributeName";
if (![_post.post isEqualToString:@""]) {
NSMutableAttributedString *attrString = [[NSMutableAttributedString alloc] initWithString:_post.post attributes:[TextStyles postStyle]];
NSDataDetector *urlDetector = [NSDataDetector dataDetectorWithTypes:NSTextCheckingTypeLink error:nil];
[urlDetector enumerateMatchesInString:attrString.string options:kNilOptions range:NSMakeRange(0, attrString.string.length) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop){
if (result.resultType == NSTextCheckingTypeLink) {
NSMutableDictionary *linkAttributes = [[NSMutableDictionary alloc] initWithDictionary:[TextStyles postLinkStyle]];
linkAttributes[kLinkAttributeName] = [NSURL URLWithString:result.URL.absoluteString];
[attrString addAttributes:linkAttributes range:result.range];
}
}];
// Configure node to support tappable links
_postNode.delegate = self;
_postNode.userInteractionEnabled = YES;
_postNode.linkAttributeNames = @[ kLinkAttributeName ];
_postNode.attributedText = attrString;
_postNode.passthroughNonlinkTouches = YES; // passes touches through when they aren't on a link
}
[self addSubnode:_postNode];
// Media
if (![_post.media isEqualToString:@""]) {
_mediaNode = [[ASNetworkImageNode alloc] init];
_mediaNode.backgroundColor = ASDisplayNodeDefaultPlaceholderColor();
_mediaNode.cornerRadius = 4.0;
_mediaNode.URL = [NSURL URLWithString:_post.media];
_mediaNode.delegate = self;
_mediaNode.imageModificationBlock = ^UIImage *(UIImage *image) {
UIImage *modifiedImage;
CGRect rect = CGRectMake(0, 0, image.size.width, image.size.height);
UIGraphicsBeginImageContextWithOptions(image.size, false, [[UIScreen mainScreen] scale]);
[[UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:8.0] addClip];
[image drawInRect:rect];
modifiedImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return modifiedImage;
};
[self addSubnode:_mediaNode];
}
// User pic
_avatarNode = [[ASNetworkImageNode alloc] init];
_avatarNode.backgroundColor = ASDisplayNodeDefaultPlaceholderColor();
_avatarNode.width = ASDimensionMakeWithPoints(44);
_avatarNode.height = ASDimensionMakeWithPoints(44);
_avatarNode.cornerRadius = 22.0;
_avatarNode.URL = [NSURL URLWithString:_post.photo];
_avatarNode.imageModificationBlock = ^UIImage *(UIImage *image) {
UIImage *modifiedImage;
CGRect rect = CGRectMake(0, 0, image.size.width, image.size.height);
UIGraphicsBeginImageContextWithOptions(image.size, false, [[UIScreen mainScreen] scale]);
[[UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:44.0] addClip];
[image drawInRect:rect];
modifiedImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return modifiedImage;
};
[self addSubnode:_avatarNode];
// Hairline cell separator
_divider = [[ASDisplayNode alloc] init];
[self updateDividerColor];
[self addSubnode:_divider];
// Via
if (_post.via != 0) {
_viaNode = [[ASImageNode alloc] init];
_viaNode.image = (_post.via == 1) ? [UIImage imageNamed:@"icon_ios.png"] : [UIImage imageNamed:@"icon_android.png"];
[self addSubnode:_viaNode];
}
// Bottom controls
_likesNode = [[LikesNode alloc] initWithLikesCount:_post.likes];
[self addSubnode:_likesNode];
_commentsNode = [[CommentsNode alloc] initWithCommentsCount:_post.comments];
[self addSubnode:_commentsNode];
_optionsNode = [[ASImageNode alloc] init];
_optionsNode.image = [UIImage imageNamed:@"icon_more"];
[self addSubnode:_optionsNode];
}
return self;
}
- (void)updateDividerColor
{
/*
* UITableViewCell traverses through all its descendant views and adjusts their background color accordingly
* either to [UIColor clearColor], although potentially it could use the same color as the selection highlight itself.
* After selection, the same trick is performed again in reverse, putting all the backgrounds back as they used to be.
* But in our case, we don't want to have the background color disappearing so we reset it after highlighting or
* selection is done.
*/
_divider.backgroundColor = PostNodeDividerColor;
}
#pragma mark - ASDisplayNode
- (void)didLoad
{
// enable highlighting now that self.layer has loaded -- see ASHighlightOverlayLayer.h
self.layer.as_allowsHighlightDrawing = YES;
[super didLoad];
}
- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
{
// Flexible spacer between username and time
ASLayoutSpec *spacer = [[ASLayoutSpec alloc] init];
spacer.flexGrow = YES;
// NOTE: This inset is not actually required by the layout, but is an example of the upward propogation of layoutable
// properties. Specifically, .flexGrow from the child is transferred to the inset spec so they can expand together.
// Without this capability, it would be required to set insetSpacer.flexGrow = YES;
ASInsetLayoutSpec *insetSpacer =
[ASInsetLayoutSpec
insetLayoutSpecWithInsets:UIEdgeInsetsMake(0, 0, 0, 0)
child:spacer];
// Horizontal stack for name, username, via icon and time
NSMutableArray *layoutSpecChildren = [@[_nameNode, _usernameNode, insetSpacer] mutableCopy];
if (_post.via != 0) {
[layoutSpecChildren addObject:_viaNode];
}
[layoutSpecChildren addObject:_timeNode];
ASStackLayoutSpec *nameStack =
[ASStackLayoutSpec
stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal
spacing:5.0
justifyContent:ASStackLayoutJustifyContentStart
alignItems:ASStackLayoutAlignItemsCenter
children:layoutSpecChildren];
nameStack.alignSelf = ASStackLayoutAlignSelfStretch;
// bottom controls horizontal stack
ASStackLayoutSpec *controlsStack =
[ASStackLayoutSpec
stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal
spacing:10
justifyContent:ASStackLayoutJustifyContentStart
alignItems:ASStackLayoutAlignItemsCenter
children:@[_likesNode, _commentsNode, _optionsNode]];
// Add more gaps for control line
controlsStack.spacingAfter = 3.0;
controlsStack.spacingBefore = 3.0;
NSMutableArray *mainStackContent = [[NSMutableArray alloc] init];
[mainStackContent addObject:nameStack];
[mainStackContent addObject:_postNode];
if (![_post.media isEqualToString:@""]){
// Only add the media node if an image is present
if (_mediaNode.image != nil) {
CGFloat imageRatio = (_mediaNode.image != nil ? _mediaNode.image.size.height / _mediaNode.image.size.width : 0.5);
ASRatioLayoutSpec *imagePlace =
[ASRatioLayoutSpec
ratioLayoutSpecWithRatio:imageRatio
child:_mediaNode];
imagePlace.spacingAfter = 3.0;
imagePlace.spacingBefore = 3.0;
[mainStackContent addObject:imagePlace];
}
}
[mainStackContent addObject:controlsStack];
// Vertical spec of cell main content
ASStackLayoutSpec *contentSpec =
[ASStackLayoutSpec
stackLayoutSpecWithDirection:ASStackLayoutDirectionVertical
spacing:8.0
justifyContent:ASStackLayoutJustifyContentStart
alignItems:ASStackLayoutAlignItemsStretch
children:mainStackContent];
contentSpec.flexShrink = YES;
// Horizontal spec for avatar
ASStackLayoutSpec *avatarContentSpec =
[ASStackLayoutSpec
stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal
spacing:8.0
justifyContent:ASStackLayoutJustifyContentStart
alignItems:ASStackLayoutAlignItemsStart
children:@[_avatarNode, contentSpec]];
return [ASInsetLayoutSpec
insetLayoutSpecWithInsets:UIEdgeInsetsMake(10, 10, 10, 10)
child:avatarContentSpec];
}
- (void)layout
{
[super layout];
// Manually layout the divider.
CGFloat pixelHeight = 1.0f / [[UIScreen mainScreen] scale];
_divider.frame = CGRectMake(0.0f, 0.0f, self.calculatedSize.width, pixelHeight);
}
#pragma mark - ASCellNode
- (void)setHighlighted:(BOOL)highlighted
{
[super setHighlighted:highlighted];
[self updateDividerColor];
}
- (void)setSelected:(BOOL)selected
{
[super setSelected:selected];
[self updateDividerColor];
}
#pragma mark - <ASTextNodeDelegate>
- (BOOL)textNode:(ASTextNode *)richTextNode shouldHighlightLinkAttribute:(NSString *)attribute value:(id)value atPoint:(CGPoint)point
{
// Opt into link highlighting -- tap and hold the link to try it! must enable highlighting on a layer, see -didLoad
return YES;
}
- (void)textNode:(ASTextNode *)richTextNode tappedLinkAttribute:(NSString *)attribute value:(NSURL *)URL atPoint:(CGPoint)point textRange:(NSRange)textRange
{
// The node tapped a link, open it
[[UIApplication sharedApplication] openURL:URL];
}
#pragma mark - ASNetworkImageNodeDelegate methods.
- (void)imageNode:(ASNetworkImageNode *)imageNode didLoadImage:(UIImage *)image
{
[self setNeedsLayout];
}
@end