// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import UIKit import SessionUIKit final class InputViewButton: UIView { private let icon: UIImage? private let isSendButton: Bool private weak var delegate: InputViewButtonDelegate? private let hasOpaqueBackground: Bool private let onTap: (() -> Void)? private lazy var widthConstraint = set(.width, to: InputViewButton.size) private lazy var heightConstraint = set(.height, to: InputViewButton.size) private var longPressTimer: Timer? private var isLongPress = false // MARK: - UI Components private lazy var backgroundView: UIView = UIView() private lazy var iconImageView: UIImageView = UIImageView() // MARK: - Settings static let size: CGFloat = 40 static let expandedSize: CGFloat = 48 static let iconSize: CGFloat = 20 // MARK: - Lifecycle init(icon: UIImage?, isSendButton: Bool = false, delegate: InputViewButtonDelegate? = nil, hasOpaqueBackground: Bool = false, onTap: (() -> Void)? = nil) { self.icon = icon self.isSendButton = isSendButton self.delegate = delegate self.hasOpaqueBackground = hasOpaqueBackground self.onTap = onTap super.init(frame: CGRect.zero) setUpViewHierarchy() self.isAccessibilityElement = true } override init(frame: CGRect) { preconditionFailure("Use init(icon:delegate:) instead.") } required init?(coder: NSCoder) { preconditionFailure("Use init(icon:delegate:) instead.") } private func setUpViewHierarchy() { themeBackgroundColor = .clear if hasOpaqueBackground { let backgroundView: UIView = UIView() backgroundView.themeBackgroundColor = .inputButton_background backgroundView.alpha = Values.lowOpacity addSubview(backgroundView) backgroundView.pin(to: self) let blurView: UIVisualEffectView = UIVisualEffectView() addSubview(blurView) blurView.pin(to: self) ThemeManager.onThemeChange(observer: blurView) { [weak blurView] theme, _ in switch theme.interfaceStyle { case .light: blurView?.effect = UIBlurEffect(style: .light) default: blurView?.effect = UIBlurEffect(style: .dark) } } themeBorderColor = .borderSeparator layer.borderWidth = Values.separatorThickness } backgroundView.themeBackgroundColor = (isSendButton ? .primary : .inputButton_background) backgroundView.alpha = (isSendButton ? 1 : Values.lowOpacity) addSubview(backgroundView) backgroundView.pin(to: self) layer.cornerRadius = (InputViewButton.size / 2) layer.masksToBounds = true isUserInteractionEnabled = true widthConstraint.isActive = true heightConstraint.isActive = true iconImageView.image = icon?.withRenderingMode(.alwaysTemplate) iconImageView.themeTintColor = (isSendButton ? .black : .textPrimary) iconImageView.contentMode = .scaleAspectFit addSubview(iconImageView) iconImageView.center(in: self) iconImageView.set(.width, to: InputViewButton.iconSize) iconImageView.set(.height, to: InputViewButton.iconSize) } // MARK: - Animation private func animate( to size: CGFloat, themeBackgroundColor: ThemeValue, themeTintColor: ThemeValue, alpha: CGFloat ) { let frame = CGRect(center: center, size: CGSize(width: size, height: size)) widthConstraint.constant = size heightConstraint.constant = size UIView.animate(withDuration: 0.25) { self.layoutIfNeeded() self.frame = frame self.layer.cornerRadius = (size / 2) self.iconImageView.themeTintColor = themeTintColor self.backgroundView.themeBackgroundColor = themeBackgroundColor self.backgroundView.alpha = alpha } } private func expand() { animate( to: InputViewButton.expandedSize, themeBackgroundColor: .primary, themeTintColor: .black, alpha: 1 ) } private func collapse() { animate( to: InputViewButton.size, themeBackgroundColor: (isSendButton ? .primary : .inputButton_background), themeTintColor: (isSendButton ? .black : .textPrimary), alpha: (isSendButton ? 1 : Values.lowOpacity) ) } // MARK: - Interaction // We want to detect both taps and long presses override func touchesBegan(_ touches: Set, with event: UIEvent?) { guard isUserInteractionEnabled else { return } UIImpactFeedbackGenerator(style: .heavy).impactOccurred() expand() invalidateLongPressIfNeeded() longPressTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false, block: { [weak self] _ in self?.isLongPress = true self?.delegate?.handleInputViewButtonLongPressBegan(self) }) } override func touchesMoved(_ touches: Set, with event: UIEvent?) { guard isUserInteractionEnabled else { return } if isLongPress { delegate?.handleInputViewButtonLongPressMoved(self, with: touches.first) } } override func touchesEnded(_ touches: Set, with event: UIEvent?) { guard isUserInteractionEnabled else { return } collapse() if !isLongPress { delegate?.handleInputViewButtonTapped(self) onTap?() } else { delegate?.handleInputViewButtonLongPressEnded(self, with: touches.first) } invalidateLongPressIfNeeded() } override func touchesCancelled(_ touches: Set, with event: UIEvent?) { collapse() invalidateLongPressIfNeeded() } private func invalidateLongPressIfNeeded() { longPressTimer?.invalidate() isLongPress = false } } // MARK: - Delegate protocol InputViewButtonDelegate: AnyObject { func handleInputViewButtonTapped(_ inputViewButton: InputViewButton) func handleInputViewButtonLongPressBegan(_ inputViewButton: InputViewButton?) func handleInputViewButtonLongPressMoved(_ inputViewButton: InputViewButton, with touch: UITouch?) func handleInputViewButtonLongPressEnded(_ inputViewButton: InputViewButton, with touch: UITouch?) } extension InputViewButtonDelegate { func handleInputViewButtonLongPressBegan(_ inputViewButton: InputViewButton?) { } func handleInputViewButtonLongPressMoved(_ inputViewButton: InputViewButton, with touch: UITouch?) { } func handleInputViewButtonLongPressEnded(_ inputViewButton: InputViewButton, with touch: UITouch?) { } }