Rework layout of conversation input toolbar.

pull/1/head
Matthew Chen 6 years ago
parent 8a8d1f43f4
commit 6ff6ee2e2e

@ -1,5 +1,5 @@
// //
// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // Copyright (c) 2019 Open Whisper Systems. All rights reserved.
// //
#import "ConversationInputTextView.h" #import "ConversationInputTextView.h"
@ -26,12 +26,7 @@ NS_ASSUME_NONNULL_BEGIN
[self setTranslatesAutoresizingMaskIntoConstraints:NO]; [self setTranslatesAutoresizingMaskIntoConstraints:NO];
self.delegate = self; self.delegate = self;
self.backgroundColor = nil;
self.backgroundColor = (Theme.isDarkThemeEnabled ? UIColor.ows_gray90Color : UIColor.ows_gray02Color);
self.layer.borderColor
= (Theme.isDarkThemeEnabled ? [Theme.primaryColor colorWithAlphaComponent:0.06f].CGColor
: [Theme.primaryColor colorWithAlphaComponent:0.12f].CGColor);
self.layer.borderWidth = 0.5f;
self.scrollIndicatorInsets = UIEdgeInsetsMake(4, 4, 4, 4); self.scrollIndicatorInsets = UIEdgeInsetsMake(4, 4, 4, 4);
@ -56,7 +51,14 @@ NS_ASSUME_NONNULL_BEGIN
// We need to do these steps _after_ placeholderView is configured. // We need to do these steps _after_ placeholderView is configured.
self.font = [UIFont ows_dynamicTypeBodyFont]; self.font = [UIFont ows_dynamicTypeBodyFont];
self.textContainerInset = UIEdgeInsetsMake(7.0f, 12.0f, 7.0f, 12.0f); CGFloat hMarginLeading = 12.f;
CGFloat hMarginTrailing = 24.f;
self.textContainerInset = UIEdgeInsetsMake(7.f,
CurrentAppContext().isRTL ? hMarginTrailing : hMarginLeading,
7.f,
CurrentAppContext().isRTL ? hMarginLeading : hMarginTrailing);
self.textContainer.lineFragmentPadding = 0;
self.contentInset = UIEdgeInsetsZero;
[self ensurePlaceholderConstraints]; [self ensurePlaceholderConstraints];
[self updatePlaceholderVisibility]; [self updatePlaceholderVisibility];

@ -48,19 +48,17 @@ const CGFloat kMaxTextViewHeight = 98;
@property (nonatomic, readonly) ConversationStyle *conversationStyle; @property (nonatomic, readonly) ConversationStyle *conversationStyle;
@property (nonatomic, readonly) ConversationInputTextView *inputTextView; @property (nonatomic, readonly) ConversationInputTextView *inputTextView;
@property (nonatomic, readonly) UIStackView *contentRows; @property (nonatomic, readonly) UIStackView *hStack;
@property (nonatomic, readonly) UIStackView *composeRow; @property (nonatomic, readonly) UIStackView *vStack;
@property (nonatomic, readonly) UIButton *attachmentButton; @property (nonatomic, readonly) UIButton *attachmentButton;
@property (nonatomic, readonly) UIButton *sendButton; @property (nonatomic, readonly) UIButton *sendButton;
@property (nonatomic, readonly) UIButton *voiceMemoButton; @property (nonatomic, readonly) UIButton *voiceMemoButton;
@property (nonatomic, readonly) UIView *quotedReplyWrapper;
@property (nonatomic, readonly) UIView *linkPreviewWrapper;
@property (nonatomic) CGFloat textViewHeight; @property (nonatomic) CGFloat textViewHeight;
@property (nonatomic, readonly) NSLayoutConstraint *textViewHeightConstraint; @property (nonatomic, readonly) NSLayoutConstraint *textViewHeightConstraint;
#pragma mark -
@property (nonatomic, nullable) UIView *quotedMessagePreview;
#pragma mark - Voice Memo Recording UI #pragma mark - Voice Memo Recording UI
@property (nonatomic, nullable) UIView *voiceMemoUI; @property (nonatomic, nullable) UIView *voiceMemoUI;
@ -73,7 +71,6 @@ const CGFloat kMaxTextViewHeight = 98;
@property (nonatomic, nullable) NSArray<NSLayoutConstraint *> *layoutContraints; @property (nonatomic, nullable) NSArray<NSLayoutConstraint *> *layoutContraints;
@property (nonatomic) UIEdgeInsets receivedSafeAreaInsets; @property (nonatomic) UIEdgeInsets receivedSafeAreaInsets;
@property (nonatomic, nullable) InputLinkPreview *inputLinkPreview; @property (nonatomic, nullable) InputLinkPreview *inputLinkPreview;
@property (nonatomic, nullable) LinkPreviewView *linkPreviewView;
@property (nonatomic) BOOL wasLinkPreviewCancelled; @property (nonatomic) BOOL wasLinkPreviewCancelled;
@end @end
@ -122,7 +119,6 @@ const CGFloat kMaxTextViewHeight = 98;
self.autoresizingMask = UIViewAutoresizingFlexibleHeight; self.autoresizingMask = UIViewAutoresizingFlexibleHeight;
_inputTextView = [ConversationInputTextView new]; _inputTextView = [ConversationInputTextView new];
self.inputTextView.layer.cornerRadius = kMinTextViewHeight / 2.0f;
self.inputTextView.textViewToolbarDelegate = self; self.inputTextView.textViewToolbarDelegate = self;
self.inputTextView.font = [UIFont ows_dynamicTypeBodyFont]; self.inputTextView.font = [UIFont ows_dynamicTypeBodyFont];
[self.inputTextView setContentHuggingHorizontalLow]; [self.inputTextView setContentHuggingHorizontalLow];
@ -171,31 +167,55 @@ const CGFloat kMaxTextViewHeight = 98;
self.userInteractionEnabled = YES; self.userInteractionEnabled = YES;
_composeRow = [[UIStackView alloc] _quotedReplyWrapper = [UIView containerView];
initWithArrangedSubviews:@[ self.attachmentButton, self.inputTextView, self.voiceMemoButton, self.sendButton ]]; self.quotedReplyWrapper.hidden = YES;
self.composeRow.axis = UILayoutConstraintAxisHorizontal;
self.composeRow.layoutMarginsRelativeArrangement = YES;
self.composeRow.layoutMargins = UIEdgeInsetsMake(6, 6, 6, 6);
self.composeRow.alignment = UIStackViewAlignmentBottom;
self.composeRow.spacing = 8;
_contentRows = [[UIStackView alloc] initWithArrangedSubviews:@[ self.composeRow ]]; _linkPreviewWrapper = [UIView containerView];
self.contentRows.axis = UILayoutConstraintAxisVertical; self.linkPreviewWrapper.hidden = YES;
[self addSubview:self.contentRows]; _vStack = [[UIStackView alloc]
[self.contentRows autoPinEdgeToSuperviewEdge:ALEdgeTop]; initWithArrangedSubviews:@[ self.quotedReplyWrapper, self.linkPreviewWrapper, self.inputTextView ]];
[self.contentRows autoPinEdgeToSuperviewSafeArea:ALEdgeBottom]; self.vStack.axis = UILayoutConstraintAxisVertical;
_hStack = [[UIStackView alloc]
initWithArrangedSubviews:@[ self.attachmentButton, self.vStack, self.voiceMemoButton, self.sendButton ]];
self.hStack.axis = UILayoutConstraintAxisHorizontal;
self.hStack.layoutMarginsRelativeArrangement = YES;
self.hStack.layoutMargins = UIEdgeInsetsMake(6, 6, 6, 6);
self.hStack.alignment = UIStackViewAlignmentBottom;
self.hStack.spacing = 8;
[self addSubview:self.hStack];
[self.hStack autoPinEdgeToSuperviewEdge:ALEdgeTop];
[self.hStack autoPinEdgeToSuperviewSafeArea:ALEdgeBottom];
// See comments on updateContentLayout:. // See comments on updateContentLayout:.
if (@available(iOS 11, *)) { if (@available(iOS 11, *)) {
self.contentRows.insetsLayoutMarginsFromSafeArea = NO; self.vStack.insetsLayoutMarginsFromSafeArea = NO;
self.composeRow.insetsLayoutMarginsFromSafeArea = NO; self.hStack.insetsLayoutMarginsFromSafeArea = NO;
self.insetsLayoutMarginsFromSafeArea = NO; self.insetsLayoutMarginsFromSafeArea = NO;
} }
self.contentRows.preservesSuperviewLayoutMargins = NO; self.vStack.preservesSuperviewLayoutMargins = NO;
self.composeRow.preservesSuperviewLayoutMargins = NO; self.hStack.preservesSuperviewLayoutMargins = NO;
self.preservesSuperviewLayoutMargins = NO; self.preservesSuperviewLayoutMargins = NO;
OWSLayerView *contentBorderView = [[OWSLayerView alloc] init];
contentBorderView.userInteractionEnabled = NO;
contentBorderView.backgroundColor = nil;
CAShapeLayer *contentBorderLayer = [CAShapeLayer new];
contentBorderLayer.strokeColor = Theme.secondaryColor.CGColor;
contentBorderLayer.fillColor = nil;
contentBorderLayer.lineWidth = CGHairlineWidth();
[contentBorderView.layer addSublayer:contentBorderLayer];
contentBorderView.layoutCallback = ^(UIView *layerView) {
contentBorderLayer.frame = layerView.bounds;
contentBorderLayer.path = [UIBezierPath bezierPathWithRoundedRect:layerView.bounds cornerRadius:18.f].CGPath;
};
[self.vStack addSubview:contentBorderView];
[contentBorderView autoPinEdgesToSuperviewEdges];
[contentBorderView setCompressionResistanceLow];
[contentBorderView setContentHuggingLow];
[self ensureShouldShowVoiceMemoButtonAnimated:NO doLayout:NO]; [self ensureShouldShowVoiceMemoButtonAnimated:NO doLayout:NO];
} }
@ -268,15 +288,11 @@ const CGFloat kMaxTextViewHeight = 98;
return; return;
} }
if (self.quotedMessagePreview) { [self clearQuotedMessagePreview];
[self clearQuotedMessagePreview];
}
OWSAssertDebug(self.quotedMessagePreview == nil);
_quotedReply = quotedReply; _quotedReply = quotedReply;
if (!quotedReply) { if (!quotedReply) {
[self clearQuotedMessagePreview];
return; return;
} }
@ -284,14 +300,10 @@ const CGFloat kMaxTextViewHeight = 98;
[[QuotedReplyPreview alloc] initWithQuotedReply:quotedReply conversationStyle:self.conversationStyle]; [[QuotedReplyPreview alloc] initWithQuotedReply:quotedReply conversationStyle:self.conversationStyle];
quotedMessagePreview.delegate = self; quotedMessagePreview.delegate = self;
UIView *wrapper = [UIView containerView]; self.quotedReplyWrapper.hidden = NO;
wrapper.layoutMargins = UIEdgeInsetsMake(self.quotedMessageTopMargin, 0, 0, 0); self.quotedReplyWrapper.layoutMargins = UIEdgeInsetsMake(self.quotedMessageTopMargin, 0, 0, 0);
[wrapper addSubview:quotedMessagePreview]; [self.quotedReplyWrapper addSubview:quotedMessagePreview];
[quotedMessagePreview ows_autoPinToSuperviewMargins]; [quotedMessagePreview ows_autoPinToSuperviewMargins];
[self.contentRows insertArrangedSubview:wrapper atIndex:0];
self.quotedMessagePreview = wrapper;
} }
- (CGFloat)quotedMessageTopMargin - (CGFloat)quotedMessageTopMargin
@ -301,10 +313,9 @@ const CGFloat kMaxTextViewHeight = 98;
- (void)clearQuotedMessagePreview - (void)clearQuotedMessagePreview
{ {
if (self.quotedMessagePreview) { self.quotedReplyWrapper.hidden = YES;
[self.contentRows removeArrangedSubview:self.quotedMessagePreview]; for (UIView *subview in self.quotedReplyWrapper.subviews) {
[self.quotedMessagePreview removeFromSuperview]; [subview removeFromSuperview];
self.quotedMessagePreview = nil;
} }
} }
@ -367,8 +378,8 @@ const CGFloat kMaxTextViewHeight = 98;
} }
self.layoutContraints = @[ self.layoutContraints = @[
[self.contentRows autoPinEdgeToSuperviewEdge:ALEdgeLeft withInset:self.receivedSafeAreaInsets.left], [self.hStack autoPinEdgeToSuperviewEdge:ALEdgeLeft withInset:self.receivedSafeAreaInsets.left],
[self.contentRows autoPinEdgeToSuperviewEdge:ALEdgeRight withInset:self.receivedSafeAreaInsets.right], [self.hStack autoPinEdgeToSuperviewEdge:ALEdgeRight withInset:self.receivedSafeAreaInsets.right],
]; ];
} }
@ -774,9 +785,10 @@ const CGFloat kMaxTextViewHeight = 98;
LinkPreviewView *linkPreviewView = [[LinkPreviewView alloc] initWithDelegate:self]; LinkPreviewView *linkPreviewView = [[LinkPreviewView alloc] initWithDelegate:self];
linkPreviewView.state = state; linkPreviewView.state = state;
self.linkPreviewView = linkPreviewView;
// TODO: Revisit once we have a separate quoted reply view. self.linkPreviewWrapper.hidden = NO;
[self.contentRows insertArrangedSubview:linkPreviewView atIndex:0]; [self.linkPreviewWrapper addSubview:linkPreviewView];
[linkPreviewView ows_autoPinToSuperviewMargins];
} }
- (void)clearLinkPreviewView - (void)clearLinkPreviewView
@ -784,8 +796,10 @@ const CGFloat kMaxTextViewHeight = 98;
OWSAssertIsOnMainThread(); OWSAssertIsOnMainThread();
// Clear old link preview state. // Clear old link preview state.
[self.linkPreviewView removeFromSuperview]; for (UIView *subview in self.linkPreviewWrapper.subviews) {
self.linkPreviewView = nil; [subview removeFromSuperview];
}
self.linkPreviewWrapper.hidden = YES;
} }
- (nullable OWSLinkPreviewDraft *)linkPreviewDraft - (nullable OWSLinkPreviewDraft *)linkPreviewDraft

@ -3991,7 +3991,6 @@ typedef enum : NSUInteger {
[self messageWasSent:message]; [self messageWasSent:message];
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
[BenchManager benchWithTitle:@"toggleDefaultKeyboard" [BenchManager benchWithTitle:@"toggleDefaultKeyboard"
block:^{ block:^{

@ -2,6 +2,18 @@
// Copyright (c) 2019 Open Whisper Systems. All rights reserved. // Copyright (c) 2019 Open Whisper Systems. All rights reserved.
// //
public extension CGPoint {
public func plusX(_ value: CGFloat) -> CGPoint {
return CGPoint(x: x + value, y: y)
}
public func plusY(_ value: CGFloat) -> CGPoint {
return CGPoint(x: x, y: y + value)
}
}
// MARK: -
@objc @objc
public enum LinkPreviewImageState: Int { public enum LinkPreviewImageState: Int {
case none case none
@ -213,6 +225,90 @@ public protocol LinkPreviewViewDelegate {
// MARK: - // MARK: -
@objc
public class LinkPreviewImageView: UIImageView {
private let maskLayer = CAShapeLayer()
@objc
public init() {
super.init(frame: .zero)
self.layer.mask = maskLayer
}
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
public override var bounds: CGRect {
didSet {
updateMaskLayer()
}
}
public override var frame: CGRect {
didSet {
updateMaskLayer()
}
}
public override var center: CGPoint {
didSet {
updateMaskLayer()
}
}
private func updateMaskLayer() {
let layerBounds = self.bounds
// One of the corners has assymetrical rounding to match the input toolbar border.
// This is somewhat inconvenient.
let upperLeft = CGPoint(x: 0, y: 0)
let upperRight = CGPoint(x: layerBounds.size.width, y: 0)
let lowerRight = CGPoint(x: layerBounds.size.width, y: layerBounds.size.height)
let lowerLeft = CGPoint(x: 0, y: layerBounds.size.height)
let bigRounding: CGFloat = 14
let smallRounding: CGFloat = 4
let upperLeftRounding = CurrentAppContext().isRTL ? smallRounding : bigRounding
let upperRightRounding = CurrentAppContext().isRTL ? bigRounding : smallRounding
let lowerRightRounding = smallRounding
let lowerLeftRounding = smallRounding
let path = UIBezierPath()
// It's sufficient to "draw" the rounded corners and not the edges that connect them.
path.addArc(withCenter: upperLeft.plusX(+upperLeftRounding).plusY(+upperLeftRounding),
radius: upperLeftRounding,
startAngle: CGFloat.pi * 1.0,
endAngle: CGFloat.pi * 1.5,
clockwise: true)
path.addArc(withCenter: upperRight.plusX(-upperRightRounding).plusY(+upperRightRounding),
radius: upperRightRounding,
startAngle: CGFloat.pi * 1.5,
endAngle: CGFloat.pi * 0.0,
clockwise: true)
path.addArc(withCenter: lowerRight.plusX(-lowerRightRounding).plusY(-lowerRightRounding),
radius: lowerRightRounding,
startAngle: CGFloat.pi * 0.0,
endAngle: CGFloat.pi * 0.5,
clockwise: true)
path.addArc(withCenter: lowerLeft.plusX(+lowerLeftRounding).plusY(-lowerLeftRounding),
radius: lowerLeftRounding,
startAngle: CGFloat.pi * 0.5,
endAngle: CGFloat.pi * 1.0,
clockwise: true)
maskLayer.path = path.cgPath
}
}
// MARK: -
@objc @objc
public class LinkPreviewView: UIStackView { public class LinkPreviewView: UIStackView {
private weak var delegate: LinkPreviewViewDelegate? private weak var delegate: LinkPreviewViewDelegate?
@ -436,21 +532,27 @@ public class LinkPreviewView: UIStackView {
return label return label
} }
private let approvalHeight: CGFloat = 76 private let approvalHeight: CGFloat = 72
private let approvalMarginTop: CGFloat = 6
private func createApprovalContents(state: LinkPreviewState) { private func createApprovalContents(state: LinkPreviewState) {
self.axis = .horizontal self.axis = .horizontal
self.alignment = .fill self.alignment = .fill
self.distribution = .fill self.distribution = .fill
self.spacing = 8 self.spacing = 8
self.isLayoutMarginsRelativeArrangement = true
let hMarginLeading: CGFloat = 6
let hMarginTrailing: CGFloat = 12
self.layoutMargins = UIEdgeInsets(top: approvalMarginTop,
left: CurrentAppContext().isRTL ? hMarginTrailing : hMarginLeading,
bottom: 0,
right: CurrentAppContext().isRTL ? hMarginLeading : hMarginTrailing)
NSLayoutConstraint.autoSetPriority(UILayoutPriority.defaultHigh) { self.layoutConstraints.append(self.autoSetDimension(.height, toSize: approvalHeight + approvalMarginTop))
self.layoutConstraints.append(self.autoSetDimension(.height, toSize: approvalHeight))
}
// Image // Image
if let imageView = createImageView(state: state) { if let imageView = createApprovalImageView(state: state) {
imageView.contentMode = .scaleAspectFill imageView.contentMode = .scaleAspectFill
imageView.autoPinToSquareAspectRatio() imageView.autoPinToSquareAspectRatio()
let imageSize = approvalHeight let imageSize = approvalHeight
@ -458,7 +560,6 @@ public class LinkPreviewView: UIStackView {
imageView.setContentHuggingHigh() imageView.setContentHuggingHigh()
imageView.setCompressionResistanceHigh() imageView.setCompressionResistanceHigh()
imageView.clipsToBounds = true imageView.clipsToBounds = true
// TODO: Cropping, stroke.
addArrangedSubview(imageView) addArrangedSubview(imageView)
} }
@ -554,13 +655,29 @@ public class LinkPreviewView: UIStackView {
return imageView return imageView
} }
private func createApprovalImageView(state: LinkPreviewState) -> UIImageView? {
guard state.isLoaded() else {
owsFailDebug("State not loaded.")
return nil
}
guard state.imageState() == .loaded else {
return nil
}
guard let image = state.image() else {
owsFailDebug("Could not load image.")
return nil
}
let imageView = LinkPreviewImageView()
imageView.image = image
return imageView
}
private func createLoadingContents() { private func createLoadingContents() {
self.axis = .vertical self.axis = .vertical
self.alignment = .center self.alignment = .center
NSLayoutConstraint.autoSetPriority(UILayoutPriority.defaultHigh) { self.layoutConstraints.append(self.autoSetDimension(.height, toSize: approvalHeight + approvalMarginTop))
self.layoutConstraints.append(self.autoSetDimension(.height, toSize: approvalHeight))
}
let activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .gray) let activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .gray)
activityIndicator.startAnimating() activityIndicator.startAnimating()

@ -1,11 +1,12 @@
// //
// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // Copyright (c) 2019 Open Whisper Systems. All rights reserved.
// //
import Foundation import Foundation
@objc @objc
public class OWSLayerView: UIView { public class OWSLayerView: UIView {
@objc
public var layoutCallback: ((UIView) -> Void) public var layoutCallback: ((UIView) -> Void)
@objc @objc

Loading…
Cancel
Save