mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-04-18 23:48:29 +02:00
#4090 - New voice recording toolbar ui and swipe to cancel interaction.
This commit is contained in:
committed by
Stefan Ceriu
parent
53b2b05b15
commit
fbb451f33c
@@ -68,9 +68,6 @@ typedef enum : NSUInteger
|
||||
@property (weak, nonatomic) IBOutlet UIButton *inputContextButton;
|
||||
@property (weak, nonatomic) IBOutlet RoomActionsBar *actionsBar;
|
||||
|
||||
@property (weak, nonatomic) IBOutlet UIView *voiceRecorderContainerView;
|
||||
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *voiceRecorderContainerWidthConstraint;
|
||||
|
||||
/**
|
||||
Tell whether the filled data will be sent encrypted. NO by default.
|
||||
*/
|
||||
|
||||
@@ -35,14 +35,15 @@ const CGFloat kActionMenuAttachButtonSpringDamping = .45;
|
||||
const NSTimeInterval kActionMenuContentAlphaAnimationDuration = .2;
|
||||
const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3;
|
||||
|
||||
@interface RoomInputToolbarView() <VoiceRecordViewDelegate>
|
||||
@interface RoomInputToolbarView()
|
||||
{
|
||||
// The intermediate action sheet
|
||||
UIAlertController *actionSheet;
|
||||
|
||||
VoiceRecordView *voiceRecordView;
|
||||
}
|
||||
|
||||
@property (nonatomic, weak) IBOutlet UIView *voiceMessageToolbarContainerView;
|
||||
@property (nonatomic, strong) VoiceMessageToolbarView *voiceMessageToolbarView;
|
||||
|
||||
@end
|
||||
|
||||
@implementation RoomInputToolbarView
|
||||
@@ -78,12 +79,12 @@ const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3;
|
||||
|
||||
self.isEncryptionEnabled = _isEncryptionEnabled;
|
||||
|
||||
voiceRecordView = [VoiceRecordView instanceFromNib];
|
||||
voiceRecordView.delegate = self;
|
||||
self.voiceMessageToolbarView = [VoiceMessageToolbarView instanceFromNib];
|
||||
self.voiceMessageToolbarView.frame = self.voiceMessageToolbarContainerView.bounds;
|
||||
self.voiceMessageToolbarView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
||||
[self.voiceMessageToolbarContainerView addSubview:self.voiceMessageToolbarView];
|
||||
|
||||
voiceRecordView.frame = self.voiceRecorderContainerView.bounds;
|
||||
voiceRecordView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
||||
[self.voiceRecorderContainerView addSubview:voiceRecordView];
|
||||
[self _updateUIWithTextMessage:nil animated:NO];
|
||||
}
|
||||
|
||||
#pragma mark - Override MXKView
|
||||
@@ -137,15 +138,14 @@ const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3;
|
||||
self.inputContextButton.tintColor = ThemeService.shared.theme.textSecondaryColor;
|
||||
[self.actionsBar updateWithTheme:ThemeService.shared.theme];
|
||||
|
||||
self.voiceRecorderContainerView.backgroundColor = ThemeService.shared.theme.backgroundColor;
|
||||
[voiceRecordView updateWithTheme:ThemeService.shared.theme];
|
||||
[self.voiceMessageToolbarView updateWithTheme:ThemeService.shared.theme];
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
- (void)setTextMessage:(NSString *)textMessage
|
||||
{
|
||||
[self updateSendButtonWithMessage:textMessage];
|
||||
[self _updateUIWithTextMessage:textMessage animated:YES];
|
||||
[super setTextMessage:textMessage];
|
||||
}
|
||||
|
||||
@@ -302,7 +302,7 @@ const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3;
|
||||
- (BOOL)growingTextView:(HPGrowingTextView *)growingTextView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
|
||||
{
|
||||
NSString *newText = [growingTextView.text stringByReplacingCharactersInRange:range withString:text];
|
||||
[self updateSendButtonWithMessage:newText];
|
||||
[self _updateUIWithTextMessage:newText animated:YES];
|
||||
|
||||
return YES;
|
||||
}
|
||||
@@ -366,16 +366,6 @@ const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3;
|
||||
[super destroy];
|
||||
}
|
||||
|
||||
- (void)updateSendButtonWithMessage:(NSString *)textMessage
|
||||
{
|
||||
self.actionMenuOpened = NO;
|
||||
|
||||
[UIView animateWithDuration:.15 animations:^{
|
||||
self.rightInputToolbarButton.alpha = textMessage.length ? 1 : 0;
|
||||
self.voiceRecorderContainerView.alpha = textMessage.length ? 0 : 1;
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark - properties
|
||||
|
||||
- (void)setActionMenuOpened:(BOOL)actionMenuOpened
|
||||
@@ -436,12 +426,15 @@ const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3;
|
||||
[super paste:sender];
|
||||
}
|
||||
|
||||
#pragma mark - VoiceRecordViewDelegate
|
||||
#pragma mark - Private
|
||||
|
||||
- (void)voiceRecordViewExpandedStateDidChange:(VoiceRecordView * _Nonnull)voiceRecordView {
|
||||
[UIView animateWithDuration:voiceRecordView.expandAnimationDuration animations:^{
|
||||
self.voiceRecorderContainerWidthConstraint.constant = voiceRecordView.isExpanded ? self.bounds.size.width : 48;
|
||||
[self layoutIfNeeded];
|
||||
- (void)_updateUIWithTextMessage:(NSString *)textMessage animated:(BOOL)animated
|
||||
{
|
||||
self.actionMenuOpened = NO;
|
||||
|
||||
[UIView animateWithDuration:(animated ? 0.15f : 0.0f) animations:^{
|
||||
self.rightInputToolbarButton.alpha = textMessage.length ? 1.0f : 0.0f;
|
||||
self.voiceMessageToolbarContainerView.alpha = textMessage.length ? 0.0f : 1.0;
|
||||
}];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="18122" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17703"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
@@ -17,7 +16,7 @@
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="a84-Vc-6ud" userLabel="MainToolBar View">
|
||||
<rect key="frame" x="0.0" y="2" width="600" height="58"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Hga-l8-Wua" userLabel="attach Button">
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Hga-l8-Wua">
|
||||
<rect key="frame" x="12" y="10" width="36" height="36"/>
|
||||
<accessibility key="accessibilityConfiguration" identifier="AttachButton"/>
|
||||
<constraints>
|
||||
@@ -36,7 +35,7 @@
|
||||
<viewLayoutGuide key="contentLayoutGuide" id="F6O-76-cZl"/>
|
||||
<viewLayoutGuide key="frameLayoutGuide" id="rZR-Bv-AqG"/>
|
||||
</scrollView>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="QWp-NV-uh5" userLabel="Message Composer Container">
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="QWp-NV-uh5">
|
||||
<rect key="frame" x="60" y="9" width="484" height="36"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="input_text_background" translatesAutoresizingMaskIntoConstraints="NO" id="uH7-Q7-hpZ">
|
||||
@@ -99,7 +98,7 @@
|
||||
<constraint firstAttribute="trailing" secondItem="uH7-Q7-hpZ" secondAttribute="trailing" id="wS9-oU-alv"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<button opaque="NO" alpha="0.0" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="G8Z-CM-tGs" userLabel="send Button">
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="G8Z-CM-tGs">
|
||||
<rect key="frame" x="552" y="10" width="36" height="36"/>
|
||||
<accessibility key="accessibilityConfiguration" identifier="SendButton"/>
|
||||
<state key="normal" image="send_icon"/>
|
||||
@@ -107,12 +106,9 @@
|
||||
<action selector="onTouchUpInside:" destination="iN0-l3-epB" eventType="touchUpInside" id="Y9g-uz-rAz"/>
|
||||
</connections>
|
||||
</button>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="zp9-EN-PLT">
|
||||
<rect key="frame" x="552" y="0.0" width="48" height="58"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="48" id="paQ-xM-7ko"/>
|
||||
</constraints>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="zp9-EN-PLT" customClass="PassthroughView" customModule="Riot" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="58"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</view>
|
||||
</subviews>
|
||||
<constraints>
|
||||
@@ -128,6 +124,7 @@
|
||||
<constraint firstAttribute="height" constant="58" id="Yjj-ua-rbe"/>
|
||||
<constraint firstAttribute="bottom" secondItem="Hga-l8-Wua" secondAttribute="bottom" constant="12" id="b0G-CY-AmP"/>
|
||||
<constraint firstAttribute="trailing" secondItem="QWp-NV-uh5" secondAttribute="trailing" constant="56" id="hXO-cY-Jgz"/>
|
||||
<constraint firstItem="zp9-EN-PLT" firstAttribute="leading" secondItem="a84-Vc-6ud" secondAttribute="leading" id="iDd-At-1f7"/>
|
||||
<constraint firstAttribute="trailing" secondItem="ESv-9w-KJF" secondAttribute="trailing" id="jCS-Tf-vxr"/>
|
||||
<constraint firstAttribute="bottom" secondItem="zp9-EN-PLT" secondAttribute="bottom" id="o7N-Dj-dCH"/>
|
||||
<constraint firstAttribute="bottom" secondItem="ESv-9w-KJF" secondAttribute="bottom" constant="12" id="v8r-ac-MKn"/>
|
||||
@@ -162,8 +159,7 @@
|
||||
<outlet property="messageComposerContainerBottomConstraint" destination="NGr-2o-sOP" id="oez-6D-IKA"/>
|
||||
<outlet property="messageComposerContainerTopConstraint" destination="WyZ-3i-OHi" id="OcO-1f-bNA"/>
|
||||
<outlet property="rightInputToolbarButton" destination="G8Z-CM-tGs" id="NCk-5m-aNF"/>
|
||||
<outlet property="voiceRecorderContainerView" destination="zp9-EN-PLT" id="ybB-DB-qA4"/>
|
||||
<outlet property="voiceRecorderContainerWidthConstraint" destination="paQ-xM-7ko" id="17V-6a-ddW"/>
|
||||
<outlet property="voiceMessageToolbarContainerView" destination="zp9-EN-PLT" id="St9-oc-beM"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="137.59999999999999" y="151.12443778110946"/>
|
||||
</view>
|
||||
@@ -174,8 +170,5 @@
|
||||
<image name="input_text_background" width="30" height="20"/>
|
||||
<image name="send_icon" width="36" height="36"/>
|
||||
<image name="upload_icon" width="36" height="36"/>
|
||||
<systemColor name="systemBackgroundColor">
|
||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
private enum VoiceMessageToolbarViewState {
|
||||
case idle
|
||||
case recording
|
||||
}
|
||||
|
||||
class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDelegate {
|
||||
|
||||
@IBOutlet private var backgroundView: UIView!
|
||||
|
||||
@IBOutlet private var recordButtonsContainerView: UIView!
|
||||
@IBOutlet private var primaryRecordButton: UIButton!
|
||||
@IBOutlet private var secondaryRecordButton: UIButton!
|
||||
|
||||
@IBOutlet private var slideToCancelContainerView: UIView!
|
||||
@IBOutlet private var slideToCancelLabel: UILabel!
|
||||
@IBOutlet private var slideToCancelChevron: UIImageView!
|
||||
@IBOutlet private var slideToCancelFade: UIImageView!
|
||||
|
||||
private var cancelLabelToRecordButtonDistance: CGFloat = 0.0
|
||||
|
||||
private var state: VoiceMessageToolbarViewState = .idle {
|
||||
didSet {
|
||||
updateUIAnimated(true)
|
||||
}
|
||||
}
|
||||
|
||||
private var currentTheme: Theme? {
|
||||
didSet {
|
||||
updateUIAnimated(true)
|
||||
}
|
||||
}
|
||||
|
||||
@objc static func instanceFromNib() -> VoiceMessageToolbarView {
|
||||
let nib = UINib(nibName: "VoiceMessageToolbarView", bundle: nil)
|
||||
guard let view = nib.instantiate(withOwner: nil, options: nil).first as? Self else {
|
||||
fatalError("The nib \(nib) expected its root view to be of type \(self)")
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
|
||||
let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress))
|
||||
longPressGesture.delegate = self
|
||||
longPressGesture.minimumPressDuration = 0.1
|
||||
recordButtonsContainerView.addGestureRecognizer(longPressGesture)
|
||||
|
||||
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
|
||||
longPressGesture.delegate = self
|
||||
recordButtonsContainerView.addGestureRecognizer(panGesture)
|
||||
|
||||
updateUIAnimated(false)
|
||||
}
|
||||
|
||||
// MARK: - Themable
|
||||
|
||||
func update(theme: Theme) {
|
||||
currentTheme = theme
|
||||
}
|
||||
|
||||
// MARK: - UIGestureRecognizerDelegate
|
||||
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
@objc private func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) {
|
||||
switch gestureRecognizer.state {
|
||||
case UIGestureRecognizer.State.began:
|
||||
state = .recording
|
||||
|
||||
let convertedFrame = self.convert(slideToCancelLabel.frame, from: slideToCancelContainerView)
|
||||
cancelLabelToRecordButtonDistance = recordButtonsContainerView.frame.minX - convertedFrame.maxX
|
||||
|
||||
case UIGestureRecognizer.State.ended:
|
||||
state = .idle
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
|
||||
guard self.state == .recording && gestureRecognizer.state == .changed else {
|
||||
return
|
||||
}
|
||||
|
||||
let translation = gestureRecognizer.translation(in: self)
|
||||
|
||||
recordButtonsContainerView.transform = CGAffineTransform(translationX: min(translation.x, 0.0), y: 0.0)
|
||||
slideToCancelContainerView.transform = CGAffineTransform(translationX: min(translation.x + cancelLabelToRecordButtonDistance, 0.0), y: 0.0)
|
||||
}
|
||||
|
||||
private func updateUIAnimated(_ animated: Bool) {
|
||||
UIView.animate(withDuration: (animated ? 0.25 : 0.0)) {
|
||||
switch self.state {
|
||||
case .idle:
|
||||
self.slideToCancelContainerView.alpha = 0.0
|
||||
self.backgroundView.alpha = 0.0
|
||||
self.slideToCancelFade.alpha = 0.0
|
||||
self.recordButtonsContainerView.transform = .identity
|
||||
self.slideToCancelContainerView.transform = .identity
|
||||
self.primaryRecordButton.alpha = 1.0
|
||||
self.secondaryRecordButton.alpha = 0.0
|
||||
case .recording:
|
||||
self.slideToCancelContainerView.alpha = 1.0
|
||||
self.backgroundView.alpha = 1.0
|
||||
self.slideToCancelFade.alpha = 1.0
|
||||
self.primaryRecordButton.alpha = 0.0
|
||||
self.secondaryRecordButton.alpha = 1.0
|
||||
}
|
||||
|
||||
guard let theme = self.currentTheme else {
|
||||
return
|
||||
}
|
||||
|
||||
self.backgroundView.backgroundColor = theme.backgroundColor
|
||||
self.primaryRecordButton.tintColor = theme.textSecondaryColor
|
||||
self.slideToCancelLabel.textColor = theme.textSecondaryColor
|
||||
self.slideToCancelChevron.tintColor = theme.textSecondaryColor
|
||||
}
|
||||
}
|
||||
}
|
||||
113
Riot/Modules/Room/Views/InputToolbar/VoiceMessageToolbarView.xib
Normal file
113
Riot/Modules/Room/Views/InputToolbar/VoiceMessageToolbarView.xib
Normal file
@@ -0,0 +1,113 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="18122" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<view contentMode="scaleToFill" id="iN0-l3-epB" customClass="VoiceMessageToolbarView" customModule="Riot" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="40"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="FqE-3x-NQ9">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="40"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="dyu-ha-046">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="40"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="6FH-4Q-Z5e">
|
||||
<rect key="frame" x="141" y="10" width="132" height="20.5"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="chevron.left" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="82A-vC-KEp">
|
||||
<rect key="frame" x="0.0" y="2" width="12.5" height="17"/>
|
||||
</imageView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Slide to cancel" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Ydw-Nb-zP6">
|
||||
<rect key="frame" x="20.5" y="0.0" width="111.5" height="20.5"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="voice_message_cancel_fade" translatesAutoresizingMaskIntoConstraints="NO" id="BYJ-HN-opT">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="40"/>
|
||||
</imageView>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="7OQ-1F-5qT">
|
||||
<rect key="frame" x="358" y="-6" width="52" height="52"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="BDj-Sw-VQ5">
|
||||
<rect key="frame" x="0.0" y="0.0" width="52" height="52"/>
|
||||
<state key="normal" image="voice_message_record_button_default"/>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="rel-Fo-ROL">
|
||||
<rect key="frame" x="0.0" y="0.0" width="52" height="52"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<state key="normal" image="voice_message_record_button_recording"/>
|
||||
</button>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstItem="BDj-Sw-VQ5" firstAttribute="leading" secondItem="7OQ-1F-5qT" secondAttribute="leading" id="2Xv-EI-etf"/>
|
||||
<constraint firstAttribute="trailing" secondItem="BDj-Sw-VQ5" secondAttribute="trailing" id="2ZQ-3v-0W7"/>
|
||||
<constraint firstAttribute="height" constant="52" id="4XA-Gb-5NO"/>
|
||||
<constraint firstAttribute="trailing" secondItem="BDj-Sw-VQ5" secondAttribute="trailing" id="Dki-cT-7xX"/>
|
||||
<constraint firstAttribute="bottom" secondItem="BDj-Sw-VQ5" secondAttribute="bottom" id="fzv-iX-c1Y"/>
|
||||
<constraint firstItem="BDj-Sw-VQ5" firstAttribute="top" secondItem="7OQ-1F-5qT" secondAttribute="top" id="mNa-EU-ZKQ"/>
|
||||
<constraint firstAttribute="bottom" secondItem="BDj-Sw-VQ5" secondAttribute="bottom" id="phX-gD-B2H"/>
|
||||
<constraint firstItem="BDj-Sw-VQ5" firstAttribute="top" secondItem="7OQ-1F-5qT" secondAttribute="top" id="pv8-li-wP8"/>
|
||||
<constraint firstItem="BDj-Sw-VQ5" firstAttribute="leading" secondItem="7OQ-1F-5qT" secondAttribute="leading" id="ynJ-4x-1jv"/>
|
||||
<constraint firstAttribute="width" constant="52" id="zPb-1B-JyA"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstItem="BYJ-HN-opT" firstAttribute="top" secondItem="dyu-ha-046" secondAttribute="top" id="3Mq-1a-iKc"/>
|
||||
<constraint firstAttribute="trailing" secondItem="7OQ-1F-5qT" secondAttribute="trailing" constant="4" id="7eK-W2-VJy"/>
|
||||
<constraint firstItem="6FH-4Q-Z5e" firstAttribute="centerX" secondItem="dyu-ha-046" secondAttribute="centerX" id="IZ1-Dr-yrw"/>
|
||||
<constraint firstItem="6FH-4Q-Z5e" firstAttribute="centerY" secondItem="dyu-ha-046" secondAttribute="centerY" id="NAu-5j-4Yg"/>
|
||||
<constraint firstAttribute="trailing" secondItem="BYJ-HN-opT" secondAttribute="trailing" id="NWq-wG-sAe"/>
|
||||
<constraint firstItem="7OQ-1F-5qT" firstAttribute="centerY" secondItem="dyu-ha-046" secondAttribute="centerY" id="cUx-pa-VEf"/>
|
||||
<constraint firstItem="BYJ-HN-opT" firstAttribute="leading" secondItem="dyu-ha-046" secondAttribute="leading" id="lXc-5e-Ssj"/>
|
||||
<constraint firstAttribute="bottom" secondItem="BYJ-HN-opT" secondAttribute="bottom" id="yNQ-wC-4iD"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="dyu-ha-046" secondAttribute="trailing" id="4OH-8A-tMK"/>
|
||||
<constraint firstAttribute="bottom" secondItem="dyu-ha-046" secondAttribute="bottom" id="L4o-hW-kta"/>
|
||||
<constraint firstItem="dyu-ha-046" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" id="Qmv-hs-X3c"/>
|
||||
<constraint firstItem="dyu-ha-046" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" id="Tdp-61-WP9"/>
|
||||
</constraints>
|
||||
<nil key="simulatedTopBarMetrics"/>
|
||||
<nil key="simulatedBottomBarMetrics"/>
|
||||
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
|
||||
<connections>
|
||||
<outlet property="backgroundView" destination="FqE-3x-NQ9" id="RFR-SQ-s21"/>
|
||||
<outlet property="primaryRecordButton" destination="BDj-Sw-VQ5" id="dg3-fG-Bym"/>
|
||||
<outlet property="recordButtonsContainerView" destination="7OQ-1F-5qT" id="HDQ-r9-2Tu"/>
|
||||
<outlet property="secondaryRecordButton" destination="rel-Fo-ROL" id="KXM-gt-9hS"/>
|
||||
<outlet property="slideToCancelChevron" destination="82A-vC-KEp" id="Chg-EH-UBv"/>
|
||||
<outlet property="slideToCancelContainerView" destination="6FH-4Q-Z5e" id="qCc-rl-vQX"/>
|
||||
<outlet property="slideToCancelFade" destination="BYJ-HN-opT" id="qbb-Q9-xSo"/>
|
||||
<outlet property="slideToCancelLabel" destination="Ydw-Nb-zP6" id="l4Y-Eg-Qwc"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="-84" y="234"/>
|
||||
</view>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="chevron.left" catalog="system" width="96" height="128"/>
|
||||
<image name="voice_message_cancel_fade" width="104" height="47"/>
|
||||
<image name="voice_message_record_button_default" width="22" height="26.5"/>
|
||||
<image name="voice_message_record_button_recording" width="52" height="52"/>
|
||||
</resources>
|
||||
</document>
|
||||
@@ -1,107 +0,0 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
@objc protocol VoiceRecordViewDelegate: NSObjectProtocol {
|
||||
func voiceRecordViewExpandedStateDidChange(_ voiceRecordView: VoiceRecordView)
|
||||
}
|
||||
|
||||
@objcMembers
|
||||
class VoiceRecordView: UIView, Themable {
|
||||
|
||||
@IBOutlet var voiceMessageButton: UIImageView!
|
||||
@IBOutlet var voiceMessageButtonTrailingConstraint: NSLayoutConstraint!
|
||||
|
||||
weak var delegate: VoiceRecordViewDelegate?
|
||||
var isExpanded = false {
|
||||
didSet {
|
||||
delegate?.voiceRecordViewExpandedStateDidChange(self)
|
||||
}
|
||||
}
|
||||
let expandAnimationDuration = 0.3
|
||||
|
||||
private var firstTouchPoint: CGPoint = CGPoint.zero
|
||||
private var initialVoiceMessageButtonPadding: CGFloat = 0
|
||||
|
||||
// MARK: - Themable
|
||||
|
||||
func update(theme: Theme) {
|
||||
voiceMessageButton.tintColor = theme.tintColor
|
||||
}
|
||||
|
||||
// MARK: - Instanciation
|
||||
|
||||
class func instanceFromNib() -> VoiceRecordView {
|
||||
let nib = UINib(nibName: "VoiceRecordView", bundle: nil)
|
||||
guard let view = nib.instantiate(withOwner: nil, options: nil).first as? Self else {
|
||||
fatalError("The nib \(nib) expected its root view to be of type \(self)")
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
|
||||
initialVoiceMessageButtonPadding = voiceMessageButtonTrailingConstraint.constant
|
||||
}
|
||||
|
||||
// MARK: - Touch management
|
||||
|
||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
super.touchesBegan(touches, with: event)
|
||||
|
||||
let point = touches.first?.location(in: self) ?? CGPoint.zero
|
||||
firstTouchPoint = CGPoint(x: self.bounds.width - point.x, y: point.y)
|
||||
isExpanded = true
|
||||
}
|
||||
|
||||
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
super.touchesBegan(touches, with: event)
|
||||
|
||||
guard let point = touches.first?.location(in: self) else {
|
||||
return
|
||||
}
|
||||
|
||||
let xDelta = min(firstTouchPoint.x - (self.bounds.width - point.x), 0)
|
||||
UIView.animate(withDuration: 0.001) {
|
||||
self.voiceMessageButtonTrailingConstraint.constant = self.initialVoiceMessageButtonPadding - xDelta
|
||||
self.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
super.touchesEnded(touches, with: event)
|
||||
|
||||
isExpanded = false
|
||||
|
||||
UIView.animate(withDuration: expandAnimationDuration) {
|
||||
self.voiceMessageButtonTrailingConstraint.constant = self.initialVoiceMessageButtonPadding
|
||||
self.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
super.touchesCancelled(touches, with: event)
|
||||
|
||||
isExpanded = false
|
||||
|
||||
UIView.animate(withDuration: expandAnimationDuration) {
|
||||
self.voiceMessageButtonTrailingConstraint.constant = self.initialVoiceMessageButtonPadding
|
||||
self.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17703"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<view contentMode="scaleToFill" id="iN0-l3-epB" customClass="VoiceRecordView" customModule="Riot" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="40"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="center" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="action_voice_message" translatesAutoresizingMaskIntoConstraints="NO" id="vxl-Nq-SMz">
|
||||
<rect key="frame" x="366" y="2" width="36" height="36"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="36" id="HDl-A8-XOv"/>
|
||||
<constraint firstAttribute="height" constant="36" id="TTQ-3n-whY"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstItem="vxl-Nq-SMz" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="centerY" id="RSo-K7-fpr"/>
|
||||
<constraint firstAttribute="trailing" secondItem="vxl-Nq-SMz" secondAttribute="trailing" constant="12" id="USM-2w-hK0"/>
|
||||
</constraints>
|
||||
<nil key="simulatedTopBarMetrics"/>
|
||||
<nil key="simulatedBottomBarMetrics"/>
|
||||
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
|
||||
<connections>
|
||||
<outlet property="voiceMessageButton" destination="vxl-Nq-SMz" id="ie8-jc-Ylg"/>
|
||||
<outlet property="voiceMessageButtonTrailingConstraint" destination="USM-2w-hK0" id="Tbp-PW-1eK"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="-84" y="234"/>
|
||||
</view>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="action_voice_message" width="22" height="26.5"/>
|
||||
</resources>
|
||||
</document>
|
||||
Reference in New Issue
Block a user