Voice Note Lock

This commit is contained in:
Michael Kirk 2019-02-05 22:36:03 -07:00
parent a566145d5b
commit d29ce740cb
8 changed files with 293 additions and 33 deletions

View File

@ -437,6 +437,7 @@
45FBC5C81DF8575700E9B410 /* CallKitCallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FBC59A1DF8575700E9B410 /* CallKitCallManager.swift */; };
45FBC5D11DF8592E00E9B410 /* SignalCall.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FBC5D01DF8592E00E9B410 /* SignalCall.swift */; };
4AC4EA13C8A444455DAB351F /* Pods_SignalMessaging.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 264242150E87D10A357DB07B /* Pods_SignalMessaging.framework */; };
4C04392A220A9EC800BAEA63 /* VoiceNoteLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C043929220A9EC800BAEA63 /* VoiceNoteLock.swift */; };
4C04F58421C860C50090D0BB /* MantlePerfTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C04F58321C860C50090D0BB /* MantlePerfTest.swift */; };
4C090A1B210FD9C7001FD7F9 /* HapticFeedback.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C090A1A210FD9C7001FD7F9 /* HapticFeedback.swift */; };
4C11AA5020FD59C700351FBD /* MessageStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C11AA4F20FD59C700351FBD /* MessageStatusView.swift */; };
@ -1149,6 +1150,7 @@
45F659811E1BE77000444429 /* NonCallKitCallUIAdaptee.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NonCallKitCallUIAdaptee.swift; sourceTree = "<group>"; };
45FBC59A1DF8575700E9B410 /* CallKitCallManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallKitCallManager.swift; sourceTree = "<group>"; };
45FBC5D01DF8592E00E9B410 /* SignalCall.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignalCall.swift; sourceTree = "<group>"; };
4C043929220A9EC800BAEA63 /* VoiceNoteLock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceNoteLock.swift; sourceTree = "<group>"; };
4C04F58321C860C50090D0BB /* MantlePerfTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MantlePerfTest.swift; path = Models/MantlePerfTest.swift; sourceTree = "<group>"; };
4C090A1A210FD9C7001FD7F9 /* HapticFeedback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = HapticFeedback.swift; path = UserInterface/HapticFeedback.swift; sourceTree = "<group>"; };
4C11AA4F20FD59C700351FBD /* MessageStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageStatusView.swift; sourceTree = "<group>"; };
@ -2330,6 +2332,7 @@
450D19111F85236600970622 /* RemoteVideoView.h */,
450D19121F85236600970622 /* RemoteVideoView.m */,
34B6A902218B3F62007C4606 /* TypingIndicatorView.swift */,
4C043929220A9EC800BAEA63 /* VoiceNoteLock.swift */,
);
name = Views;
path = views;
@ -3394,6 +3397,7 @@
34386A52207D0C01009F5D9C /* HomeViewCell.m in Sources */,
34DC9BD921543E0C00FDDCEC /* DebugContactsUtils.m in Sources */,
34DBF007206C3CB200025978 /* OWSBubbleShapeView.m in Sources */,
4C04392A220A9EC800BAEA63 /* VoiceNoteLock.swift in Sources */,
34D1F0BA1F8800D90066283D /* OWSAudioMessageView.m in Sources */,
34D8C02B1ED3685800188D7C /* DebugUIContacts.m in Sources */,
3496956E21A301A100DCFE74 /* OWSBackupExportJob.m in Sources */,

View File

@ -19,11 +19,13 @@ NS_ASSUME_NONNULL_BEGIN
- (void)voiceMemoGestureDidStart;
- (void)voiceMemoGestureDidEnd;
- (void)voiceMemoGestureDidLock;
- (void)voiceMemoGestureDidComplete;
- (void)voiceMemoGestureDidCancel;
- (void)voiceMemoGestureDidChange:(CGFloat)cancelAlpha;
- (void)voiceMemoGestureDidUpdateCancelWithRatioComplete:(CGFloat)cancelAlpha;
@end
@ -55,10 +57,12 @@ NS_ASSUME_NONNULL_BEGIN
- (void)updateFontSizes;
- (void)updateLayoutWithSafeAreaInsets:(UIEdgeInsets)safeAreaInsets;
- (void)ensureTextViewHeight;
#pragma mark - Voice Memo
- (void)ensureTextViewHeight;
- (void)lockVoiceMemoUI;
- (void)showVoiceMemoUI;
- (void)hideVoiceMemoUI:(BOOL)animated;
@ -67,6 +71,8 @@ NS_ASSUME_NONNULL_BEGIN
- (void)cancelVoiceMemoIfNecessary;
#pragma mark -
@property (nonatomic, nullable) OWSQuotedReplyModel *quotedReply;
@property (nonatomic, nullable, readonly) OWSLinkPreviewDraft *linkPreviewDraft;

View File

@ -20,6 +20,12 @@
NS_ASSUME_NONNULL_BEGIN
typedef NS_CLOSED_ENUM(NSUInteger, VoiceMemoRecordingState){
VoiceMemoRecordingState_Idle,
VoiceMemoRecordingState_RecordingHeld,
VoiceMemoRecordingState_RecordingLocked
};
static void *kConversationInputTextViewObservingContext = &kConversationInputTextViewObservingContext;
const CGFloat kMinTextViewHeight = 36;
@ -62,11 +68,16 @@ const CGFloat kMaxTextViewHeight = 98;
#pragma mark - Voice Memo Recording UI
@property (nonatomic, nullable) UIView *voiceMemoUI;
@property (nonatomic, nullable) VoiceMemoLockView *voiceMemoLockView;
@property (nonatomic, nullable) UIView *voiceMemoContentView;
@property (nonatomic) NSDate *voiceMemoStartTime;
@property (nonatomic, nullable) NSTimer *voiceMemoUpdateTimer;
@property (nonatomic) UIGestureRecognizer *voiceMemoGestureRecognizer;
@property (nonatomic, nullable) UILabel *voiceMemoCancelLabel;
@property (nonatomic, nullable) UIView *voiceMemoRedRecordingCircle;
@property (nonatomic, nullable) UILabel *recordingLabel;
@property (nonatomic) BOOL isRecordingVoiceMemo;
@property (nonatomic, readonly) BOOL isRecordingVoiceMemo;
@property (nonatomic) VoiceMemoRecordingState voiceMemoRecordingState;
@property (nonatomic) CGPoint voiceMemoGestureStartLocation;
@property (nonatomic, nullable) NSArray<NSLayoutConstraint *> *layoutContraints;
@property (nonatomic) UIEdgeInsets receivedSafeAreaInsets;
@ -164,6 +175,7 @@ const CGFloat kMaxTextViewHeight = 98;
UILongPressGestureRecognizer *longPressGestureRecognizer =
[[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)];
longPressGestureRecognizer.minimumPressDuration = 0;
self.voiceMemoGestureRecognizer = longPressGestureRecognizer;
[self.voiceMemoButton addGestureRecognizer:longPressGestureRecognizer];
self.userInteractionEnabled = YES;
@ -435,18 +447,25 @@ const CGFloat kMaxTextViewHeight = 98;
case UIGestureRecognizerStateFailed:
if (self.isRecordingVoiceMemo) {
// Cancel voice message if necessary.
self.isRecordingVoiceMemo = NO;
self.voiceMemoRecordingState = VoiceMemoRecordingState_Idle;
[self.inputToolbarDelegate voiceMemoGestureDidCancel];
}
break;
case UIGestureRecognizerStateBegan:
if (self.isRecordingVoiceMemo) {
// Cancel voice message if necessary.
self.isRecordingVoiceMemo = NO;
[self.inputToolbarDelegate voiceMemoGestureDidCancel];
switch (self.voiceMemoRecordingState) {
case VoiceMemoRecordingState_Idle:
break;
case VoiceMemoRecordingState_RecordingHeld:
OWSFailDebug(@"while recording held, shouldn't be possible to restart gesture.");
[self.inputToolbarDelegate voiceMemoGestureDidCancel];
break;
case VoiceMemoRecordingState_RecordingLocked:
OWSFailDebug(@"once locked, shouldn't be possible to interact with gesture.");
[self.inputToolbarDelegate voiceMemoGestureDidCancel];
break;
}
// Start voice message.
self.isRecordingVoiceMemo = YES;
self.voiceMemoRecordingState = VoiceMemoRecordingState_RecordingHeld;
self.voiceMemoGestureStartLocation = [sender locationInView:self];
[self.inputToolbarDelegate voiceMemoGestureDidStart];
break;
@ -457,25 +476,62 @@ const CGFloat kMaxTextViewHeight = 98;
// For LTR/RTL, swiping in either direction will cancel.
// This is okay because there's only space on screen to perform the
// gesture in one direction.
CGFloat offset = fabs(self.voiceMemoGestureStartLocation.x - location.x);
CGFloat xOffset = fabs(self.voiceMemoGestureStartLocation.x - location.x);
// The lower this value, the easier it is to cancel by accident.
// The higher this value, the harder it is to cancel.
const CGFloat kCancelOffsetPoints = 100.f;
CGFloat cancelAlpha = offset / kCancelOffsetPoints;
CGFloat cancelAlpha = xOffset / kCancelOffsetPoints;
BOOL isCancelled = cancelAlpha >= 1.f;
if (isCancelled) {
self.isRecordingVoiceMemo = NO;
self.voiceMemoRecordingState = VoiceMemoRecordingState_Idle;
[self.inputToolbarDelegate voiceMemoGestureDidCancel];
break;
} else {
[self.inputToolbarDelegate voiceMemoGestureDidChange:cancelAlpha];
[self.inputToolbarDelegate voiceMemoGestureDidUpdateCancelWithRatioComplete:cancelAlpha];
}
CGFloat yOffset = fabs(self.voiceMemoGestureStartLocation.y - location.y);
// require a certain threshold before we consider the user to be
// interacting with the lock ui, otherwise there's perceptible wobble
// of the lock slider even when the user isn't intended to interact with it.
const CGFloat kLockThresholdPoints = 20.f;
const CGFloat kLockOffsetPoints = 80.f;
CGFloat yOffsetBeyondThreshold = MAX(yOffset - kLockThresholdPoints, 0);
CGFloat lockAlpha = yOffsetBeyondThreshold / kLockOffsetPoints;
BOOL isLocked = lockAlpha >= 1.f;
if (isLocked) {
switch (self.voiceMemoRecordingState) {
case VoiceMemoRecordingState_RecordingHeld:
self.voiceMemoRecordingState = VoiceMemoRecordingState_RecordingLocked;
[self.inputToolbarDelegate voiceMemoGestureDidLock];
[self.inputToolbarDelegate voiceMemoGestureDidUpdateCancelWithRatioComplete:0];
break;
case VoiceMemoRecordingState_RecordingLocked:
// already locked
break;
case VoiceMemoRecordingState_Idle:
OWSFailDebug(@"failure: unexpeceted idle state");
[self.inputToolbarDelegate voiceMemoGestureDidCancel];
break;
}
} else {
[self.voiceMemoLockView updateWithRatioComplete:lockAlpha];
}
}
break;
case UIGestureRecognizerStateEnded:
if (self.isRecordingVoiceMemo) {
// End voice message.
self.isRecordingVoiceMemo = NO;
[self.inputToolbarDelegate voiceMemoGestureDidEnd];
switch (self.voiceMemoRecordingState) {
case VoiceMemoRecordingState_Idle:
break;
case VoiceMemoRecordingState_RecordingHeld:
// End voice message.
self.voiceMemoRecordingState = VoiceMemoRecordingState_Idle;
[self.inputToolbarDelegate voiceMemoGestureDidComplete];
break;
case VoiceMemoRecordingState_RecordingLocked:
// Continue recording.
break;
}
break;
}
@ -483,6 +539,17 @@ const CGFloat kMaxTextViewHeight = 98;
#pragma mark - Voice Memo
- (BOOL)isRecordingVoiceMemo
{
switch (self.voiceMemoRecordingState) {
case VoiceMemoRecordingState_Idle:
return NO;
case VoiceMemoRecordingState_RecordingHeld:
case VoiceMemoRecordingState_RecordingLocked:
return YES;
}
}
- (void)showVoiceMemoUI
{
OWSAssertIsOnMainThread();
@ -490,9 +557,9 @@ const CGFloat kMaxTextViewHeight = 98;
self.voiceMemoStartTime = [NSDate date];
[self.voiceMemoUI removeFromSuperview];
[self.voiceMemoLockView removeFromSuperview];
self.voiceMemoUI = [UIView new];
self.voiceMemoUI.userInteractionEnabled = NO;
self.voiceMemoUI.backgroundColor = Theme.toolbarBackgroundColor;
[self addSubview:self.voiceMemoUI];
self.voiceMemoUI.frame = CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height);
@ -505,6 +572,14 @@ const CGFloat kMaxTextViewHeight = 98;
self.recordingLabel.textColor = [UIColor ows_destructiveRedColor];
self.recordingLabel.font = [UIFont ows_mediumFontWithSize:14.f];
[self.voiceMemoContentView addSubview:self.recordingLabel];
VoiceMemoLockView *voiceMemoLockView = [VoiceMemoLockView new];
self.voiceMemoLockView = voiceMemoLockView;
[self addSubview:voiceMemoLockView];
[voiceMemoLockView autoPinTrailingToSuperviewMargin];
[voiceMemoLockView autoPinEdge:ALEdgeBottom toEdge:ALEdgeTop ofView:self.voiceMemoContentView];
[voiceMemoLockView setCompressionResistanceHigh];
[self updateVoiceMemo];
UIImage *icon = [UIImage imageNamed:@"voice-memo-button"];
@ -512,6 +587,7 @@ const CGFloat kMaxTextViewHeight = 98;
UIImageView *imageView =
[[UIImageView alloc] initWithImage:[icon imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]];
imageView.tintColor = [UIColor ows_destructiveRedColor];
[imageView setContentHuggingHigh];
[self.voiceMemoContentView addSubview:imageView];
NSMutableAttributedString *cancelString = [NSMutableAttributedString new];
@ -559,11 +635,13 @@ const CGFloat kMaxTextViewHeight = 98;
NSBaselineOffsetAttributeName : @(-1.f),
}]];
UILabel *cancelLabel = [UILabel new];
self.voiceMemoCancelLabel = cancelLabel;
cancelLabel.attributedText = cancelString;
[self.voiceMemoContentView addSubview:cancelLabel];
const CGFloat kRedCircleSize = 100.f;
UIView *redCircleView = [UIView new];
self.voiceMemoRedRecordingCircle = redCircleView;
redCircleView.backgroundColor = [UIColor ows_destructiveRedColor];
redCircleView.layer.cornerRadius = kRedCircleSize * 0.5f;
[redCircleView autoSetDimension:ALDimensionWidth toSize:kRedCircleSize];
@ -593,6 +671,17 @@ const CGFloat kMaxTextViewHeight = 98;
cancelLabelStartFrame.origin.x
= (CurrentAppContext().isRTL ? -self.voiceMemoUI.bounds.size.width : self.voiceMemoUI.bounds.size.width);
cancelLabel.frame = cancelLabelStartFrame;
voiceMemoLockView.transform = CGAffineTransformMakeScale(0.0, 0.0);
[voiceMemoLockView layoutIfNeeded];
[UIView animateWithDuration:0.2f
delay:1.f
options:0
animations:^{
voiceMemoLockView.transform = CGAffineTransformIdentity;
}
completion:nil];
[UIView animateWithDuration:0.35f
delay:0.f
options:UIViewAnimationOptionCurveEaseOut
@ -636,11 +725,19 @@ const CGFloat kMaxTextViewHeight = 98;
{
OWSAssertIsOnMainThread();
self.voiceMemoRecordingState = VoiceMemoRecordingState_Idle;
UIView *oldVoiceMemoUI = self.voiceMemoUI;
UIView *oldVoiceMemoLockView = self.voiceMemoLockView;
self.voiceMemoUI = nil;
self.voiceMemoCancelLabel = nil;
self.voiceMemoRedRecordingCircle = nil;
self.voiceMemoContentView = nil;
self.voiceMemoLockView = nil;
self.recordingLabel = nil;
NSTimer *voiceMemoUpdateTimer = self.voiceMemoUpdateTimer;
[self.voiceMemoUpdateTimer invalidate];
self.voiceMemoUpdateTimer = nil;
[oldVoiceMemoUI.layer removeAllAnimations];
@ -649,17 +746,77 @@ const CGFloat kMaxTextViewHeight = 98;
[UIView animateWithDuration:0.35f
animations:^{
oldVoiceMemoUI.layer.opacity = 0.f;
oldVoiceMemoLockView.layer.opacity = 0.f;
}
completion:^(BOOL finished) {
[oldVoiceMemoUI removeFromSuperview];
[voiceMemoUpdateTimer invalidate];
[oldVoiceMemoLockView removeFromSuperview];
}];
} else {
[oldVoiceMemoUI removeFromSuperview];
[voiceMemoUpdateTimer invalidate];
[oldVoiceMemoLockView removeFromSuperview];
}
}
- (void)lockVoiceMemoUI
{
__weak __typeof(self) weakSelf = self;
UIButton *sendVoiceMemoButton = [[OWSButton alloc] initWithBlock:^{
[weakSelf.inputToolbarDelegate voiceMemoGestureDidComplete];
}];
[sendVoiceMemoButton setTitle:MessageStrings.sendButton forState:UIControlStateNormal];
[sendVoiceMemoButton setTitleColor:UIColor.ows_signalBlueColor forState:UIControlStateNormal];
sendVoiceMemoButton.alpha = 0;
[self.voiceMemoContentView addSubview:sendVoiceMemoButton];
[sendVoiceMemoButton autoPinEdgesToSuperviewMarginsExcludingEdge:ALEdgeLeading];
[sendVoiceMemoButton setCompressionResistanceHigh];
[sendVoiceMemoButton setContentHuggingHigh];
UIButton *cancelButton = [[OWSButton alloc] initWithBlock:^{
[weakSelf.inputToolbarDelegate voiceMemoGestureDidCancel];
}];
[cancelButton setTitle:CommonStrings.cancelButton forState:UIControlStateNormal];
[cancelButton setTitleColor:UIColor.ows_destructiveRedColor forState:UIControlStateNormal];
cancelButton.alpha = 0;
cancelButton.titleLabel.textAlignment = NSTextAlignmentCenter;
[self.voiceMemoContentView addSubview:cancelButton];
OWSAssert(self.recordingLabel != nil);
[self.recordingLabel setContentHuggingHigh];
[NSLayoutConstraint autoSetPriority:UILayoutPriorityDefaultLow
forConstraints:^{
[cancelButton autoHCenterInSuperview];
}];
[cancelButton autoPinEdge:ALEdgeLeading
toEdge:ALEdgeTrailing
ofView:self.recordingLabel
withOffset:4
relation:NSLayoutRelationGreaterThanOrEqual];
[cancelButton autoPinEdge:ALEdgeTrailing
toEdge:ALEdgeLeading
ofView:sendVoiceMemoButton
withOffset:-4
relation:NSLayoutRelationLessThanOrEqual];
[cancelButton autoVCenterInSuperview];
[self.voiceMemoContentView layoutIfNeeded];
[UIView animateWithDuration:0.35
animations:^{
self.voiceMemoCancelLabel.alpha = 0;
self.voiceMemoRedRecordingCircle.alpha = 0;
self.voiceMemoLockView.transform = CGAffineTransformMakeScale(0, 0);
cancelButton.alpha = 1.0;
sendVoiceMemoButton.alpha = 1.0;
}
completion:^(BOOL finished) {
[self.voiceMemoCancelLabel removeFromSuperview];
[self.voiceMemoRedRecordingCircle removeFromSuperview];
[self.voiceMemoLockView removeFromSuperview];
}];
}
- (void)setVoiceMemoUICancelAlpha:(CGFloat)cancelAlpha
{
OWSAssertIsOnMainThread();
@ -681,7 +838,7 @@ const CGFloat kMaxTextViewHeight = 98;
- (void)cancelVoiceMemoIfNecessary
{
if (self.isRecordingVoiceMemo) {
self.isRecordingVoiceMemo = NO;
self.voiceMemoRecordingState = VoiceMemoRecordingState_Idle;
}
}

View File

@ -1,5 +1,5 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
#import "ConversationScrollButton.h"
@ -86,12 +86,9 @@ NS_ASSUME_NONNULL_BEGIN
if (self.hasUnreadMessages) {
foregroundColor = UIColor.whiteColor;
backgroundColor = UIColor.ows_materialBlueColor;
} else if (Theme.isDarkThemeEnabled) {
foregroundColor = UIColor.ows_materialBlueColor;
backgroundColor = [UIColor colorWithWhite:0.25f alpha:1.f];
} else {
foregroundColor = UIColor.ows_materialBlueColor;
backgroundColor = [UIColor colorWithWhite:0.95f alpha:1.f];
backgroundColor = Theme.scrollButtonBackgroundColor;
}
const CGFloat circleSize = self.class.circleSize;

View File

@ -4033,17 +4033,25 @@ typedef enum : NSUInteger {
[self requestRecordingVoiceMemo];
}
- (void)voiceMemoGestureDidEnd
- (void)voiceMemoGestureDidComplete
{
OWSAssertIsOnMainThread();
OWSLogInfo(@"voiceMemoGestureDidEnd");
OWSLogInfo(@"");
[self.inputToolbar hideVoiceMemoUI:YES];
[self endRecordingVoiceMemo];
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);
}
- (void)voiceMemoGestureDidLock
{
OWSAssertIsOnMainThread();
OWSLogInfo(@"");
[self.inputToolbar lockVoiceMemoUI];
}
- (void)voiceMemoGestureDidCancel
{
OWSAssertIsOnMainThread();
@ -4055,7 +4063,7 @@ typedef enum : NSUInteger {
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);
}
- (void)voiceMemoGestureDidChange:(CGFloat)cancelAlpha
- (void)voiceMemoGestureDidUpdateCancelWithRatioComplete:(CGFloat)cancelAlpha
{
OWSAssertIsOnMainThread();

View File

@ -0,0 +1,80 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
import Foundation
@objc
public class VoiceMemoLockView: UIView {
private var offsetConstraint: NSLayoutConstraint!
private let offsetFromToolbar: CGFloat = 40
private let backgroundViewInitialHeight: CGFloat = 80
private var chevronTravel: CGFloat {
return -1 * (backgroundViewInitialHeight - 50)
}
@objc
public override init(frame: CGRect) {
super.init(frame: frame)
addSubview(backgroundView)
backgroundView.addSubview(lockIconView)
backgroundView.addSubview(chevronView)
layoutMargins = UIEdgeInsets(top: 0, leading: 0, bottom: offsetFromToolbar, trailing: 0)
backgroundView.autoPinEdges(toSuperviewMarginsExcludingEdge: .bottom)
self.offsetConstraint = backgroundView.autoPinEdge(toSuperviewMargin: .bottom)
// we anchor the top so that the bottom "slides up" to meet it as the user slides the lock
backgroundView.autoPinEdge(.top, to: .bottom, of: self, withOffset: -offsetFromToolbar - backgroundViewInitialHeight)
backgroundView.layoutMargins = UIEdgeInsets(top: 6, leading: 6, bottom: 6, trailing: 6)
lockIconView.autoPinEdges(toSuperviewMarginsExcludingEdge: .bottom)
chevronView.autoPinEdges(toSuperviewMarginsExcludingEdge: .top)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: -
@objc
public func update(ratioComplete: CGFloat) {
offsetConstraint.constant = CGFloatLerp(0, chevronTravel, ratioComplete)
}
// MARK: - Subviews
private lazy var lockIconView: UIImageView = {
let imageTemplate = #imageLiteral(resourceName: "ic_lock_outline").withRenderingMode(.alwaysTemplate)
let imageView = UIImageView(image: imageTemplate)
imageView.tintColor = .ows_destructiveRed
imageView.autoSetDimensions(to: CGSize(width: 24, height: 24))
return imageView
}()
private lazy var chevronView: UIView = {
let label = UILabel()
label.text = "\u{2303}"
label.textColor = .ows_destructiveRed
label.textAlignment = .center
return label
}()
private lazy var backgroundView: UIView = {
let view = UIView()
let width: CGFloat = 36
view.autoSetDimension(.width, toSize: width)
view.backgroundColor = Theme.scrollButtonBackgroundColor
view.layer.cornerRadius = width / 2
view.layer.borderColor = Theme.offBackgroundColor.cgColor
view.layer.borderWidth = CGHairlineWidth()
return view
}()
}

View File

@ -1,5 +1,5 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
#import "UIColor+OWS.h"
@ -61,6 +61,8 @@ extern NSString *const ThemeDidChangeNotification;
@property (class, readonly, nonatomic) UIColor *toastForegroundColor;
@property (class, readonly, nonatomic) UIColor *toastBackgroundColor;
@property (class, readonly, nonatomic) UIColor *scrollButtonBackgroundColor;
@end
NS_ASSUME_NONNULL_END

View File

@ -1,5 +1,5 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
#import "Theme.h"
@ -194,6 +194,12 @@ NSString *const ThemeKeyThemeEnabled = @"ThemeKeyThemeEnabled";
return (Theme.isDarkThemeEnabled ? UIColor.ows_gray75Color : UIColor.ows_gray60Color);
}
+ (UIColor *)scrollButtonBackgroundColor
{
return Theme.isDarkThemeEnabled ? [UIColor colorWithWhite:0.25f alpha:1.f]
: [UIColor colorWithWhite:0.95f alpha:1.f];
}
@end
NS_ASSUME_NONNULL_END