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"
@ -26,12 +26,7 @@ NS_ASSUME_NONNULL_BEGIN
[self setTranslatesAutoresizingMaskIntoConstraints:NO];
self.delegate = self;
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.backgroundColor = nil;
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.
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 updatePlaceholderVisibility];

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

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

@ -2,6 +2,18 @@
// 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
public enum LinkPreviewImageState: Int {
case none
@ -213,6 +225,90 @@ public protocol LinkPreviewViewDelegate {
// 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
public class LinkPreviewView: UIStackView {
private weak var delegate: LinkPreviewViewDelegate?
@ -436,21 +532,27 @@ public class LinkPreviewView: UIStackView {
return label
}
private let approvalHeight: CGFloat = 76
private let approvalHeight: CGFloat = 72
private let approvalMarginTop: CGFloat = 6
private func createApprovalContents(state: LinkPreviewState) {
self.axis = .horizontal
self.alignment = .fill
self.distribution = .fill
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))
}
self.layoutConstraints.append(self.autoSetDimension(.height, toSize: approvalHeight + approvalMarginTop))
// Image
if let imageView = createImageView(state: state) {
if let imageView = createApprovalImageView(state: state) {
imageView.contentMode = .scaleAspectFill
imageView.autoPinToSquareAspectRatio()
let imageSize = approvalHeight
@ -458,7 +560,6 @@ public class LinkPreviewView: UIStackView {
imageView.setContentHuggingHigh()
imageView.setCompressionResistanceHigh()
imageView.clipsToBounds = true
// TODO: Cropping, stroke.
addArrangedSubview(imageView)
}
@ -554,13 +655,29 @@ public class LinkPreviewView: UIStackView {
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() {
self.axis = .vertical
self.alignment = .center
NSLayoutConstraint.autoSetPriority(UILayoutPriority.defaultHigh) {
self.layoutConstraints.append(self.autoSetDimension(.height, toSize: approvalHeight))
}
self.layoutConstraints.append(self.autoSetDimension(.height, toSize: approvalHeight + approvalMarginTop))
let activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .gray)
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
@objc
public class OWSLayerView: UIView {
@objc
public var layoutCallback: ((UIView) -> Void)
@objc

Loading…
Cancel
Save