mirror of
https://github.com/keymanapp/keyman.git
synced 2026-08-06 00:45:32 +00:00
440 lines
18 KiB
Swift
440 lines
18 KiB
Swift
/*
|
|
* Keyman is copyright (C) SIL International. MIT License.
|
|
*
|
|
* Created by Shawn Schantz on 2024-10-25.
|
|
*
|
|
* View controller for adjusting the height of the keyboard
|
|
*/
|
|
|
|
import Foundation
|
|
import UIKit
|
|
import os.log
|
|
|
|
class KeyboardHeightViewController: UIViewController {
|
|
let contentView: UIView = UIView()
|
|
let defaultButton = UIButton(type: .roundedRect)
|
|
let label: UILabel = UILabel()
|
|
let keyboardImage: UIImageView = UIImageView()
|
|
let keyboardResizer: UIImageView = UIImageView()
|
|
var keyboardConstraintsArray: [NSLayoutConstraint] = []
|
|
var resizerConstraintsArray: [NSLayoutConstraint] = []
|
|
var isPortrait = true
|
|
var keyboardHeight = 0.0
|
|
var defaultPortraitHeight = 0.0
|
|
var defaultLandscapeHeight = 0.0
|
|
var minKeyboardHeight = 10.0
|
|
var maxKeyboardHeight = 10.0
|
|
|
|
public init() {
|
|
super.init(nibName: nil, bundle: nil)
|
|
_ = view
|
|
}
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
|
|
os_log("KeyboardHeightViewController viewDidLoad", log:KeymanEngineLogger.ui, type: .info)
|
|
|
|
self.determineOrientation()
|
|
self.determineDefaultKeyboardHeights()
|
|
self.applyKeyboardHeight()
|
|
|
|
title = NSLocalizedString("adjust-keyboard-height-title", bundle: engineBundle, comment: "")
|
|
navigationItem.setHidesBackButton(false, animated: true)
|
|
navigationItem.leftBarButtonItem?.isEnabled = true
|
|
|
|
navigationController?.toolbar?.barTintColor = UIColor.orange
|
|
|
|
self.configureContentView()
|
|
self.configureDefaultHeightButton()
|
|
self.configureLabel()
|
|
self.configureKeyboardImage()
|
|
self.updateKeyboardImage()
|
|
self.configureKeyboardConstraints()
|
|
self.configureKeyboardResizer()
|
|
}
|
|
|
|
override func viewDidAppear(_ animated: Bool) {
|
|
super.viewDidAppear(animated)
|
|
|
|
self.calculateKeyboardHeightLimits()
|
|
}
|
|
|
|
private func determineOrientation() {
|
|
self.isPortrait = UIScreen.main.bounds.height > UIScreen.main.bounds.width
|
|
let message = "determineOrientation, isPortrait: \(self.isPortrait)"
|
|
os_log("%{public}s", log:KeymanEngineLogger.ui, type: .debug, message)
|
|
}
|
|
|
|
private func determineDefaultKeyboardHeights() {
|
|
// if no KeyboardScaleMap found for device, then default to Defaults.defaultUnknownKeyboardHeight
|
|
let portraitKeyboardScale = KeyboardScaleMap.getDeviceDefaultKeyboardScale(forPortrait: true)
|
|
self.defaultPortraitHeight = Double(portraitKeyboardScale?.keyboardHeight ?? Defaults.unknownDeviceKeyboardHeight)
|
|
|
|
let landscapeKeyboardScale = KeyboardScaleMap.getDeviceDefaultKeyboardScale(forPortrait: false)
|
|
self.defaultLandscapeHeight = Double(landscapeKeyboardScale?.keyboardHeight ?? Defaults.unknownDeviceKeyboardHeight)
|
|
}
|
|
|
|
/**
|
|
* Read the previously set keyboard heights from UserDefaults.
|
|
* Running the first time, these values would be defaults set in KeymanWebviewController.initKeyboardSize()
|
|
* These values should be set prior to now; if not, log an error and use default values
|
|
*/
|
|
private func applyKeyboardHeight() {
|
|
if (self.isPortrait) {
|
|
if (Storage.active.userDefaults.object(forKey: Key.portraitKeyboardHeight) != nil) {
|
|
self.keyboardHeight = Storage.active.userDefaults.portraitKeyboardHeight
|
|
let message = "applyKeyboardHeight, from UserDefaults loaded portrait value \(self.keyboardHeight)"
|
|
os_log("%{public}s", log:KeymanEngineLogger.ui, type: .debug, message)
|
|
} else {
|
|
self.keyboardHeight = self.defaultPortraitHeight
|
|
let message = "applyKeyboardHeight, portraitHeight not found in UserDefaults, using default value \(self.keyboardHeight)"
|
|
os_log("%{public}s", log:KeymanEngineLogger.ui, type: .error, message)
|
|
}
|
|
} else {
|
|
if (Storage.active.userDefaults.object(forKey: Key.portraitKeyboardHeight) != nil) {
|
|
self.keyboardHeight = Storage.active.userDefaults.landscapeKeyboardHeight
|
|
let message = "applyKeyboardHeight, from UserDefaults loaded landscape value \(self.keyboardHeight)"
|
|
os_log("%{public}s", log:KeymanEngineLogger.ui, type: .debug, message)
|
|
} else {
|
|
self.keyboardHeight = self.defaultLandscapeHeight
|
|
let message = "applyKeyboardHeight, landscapeHeight not found in UserDefaults, using default value \(self.keyboardHeight)"
|
|
os_log("%{public}s", log:KeymanEngineLogger.ui, type: .error, message)
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Set some reasonable limits on how small or large a keyboard can be set to during resizing.
|
|
*/
|
|
private func calculateKeyboardHeightLimits() {
|
|
// minimum height
|
|
self.minKeyboardHeight = 100.0
|
|
|
|
// set maxKeyboardHeight to 100 pts smaller than the contentView height
|
|
self.maxKeyboardHeight = contentView.frame.height - 100.0
|
|
|
|
let message = "minKeyboardHeight: \(minKeyboardHeight) maxKeyboardHeight: \(maxKeyboardHeight)"
|
|
os_log("%{public}s", log:KeymanEngineLogger.ui, type: .debug, message)
|
|
}
|
|
|
|
/**
|
|
* Given the vertical attemptedTranslation in points due to the user dragging the keyboard resizer view,
|
|
* return the amount that the user is allowed to grow or shrink the keyboard without exceeding its size limits.
|
|
*/
|
|
private func applyKeyboardSizeLimits(attemptedTranslation: Double) -> Double {
|
|
guard attemptedTranslation != 0 else {
|
|
return 0
|
|
}
|
|
|
|
// Are we trying to reduce or enlarge the keyboard?
|
|
// An increase in keyboardResizer .y translation moves it down and reduces the keyboard height.
|
|
let reducingKeyboardHeight = attemptedTranslation > 0
|
|
|
|
// by default, unless we exceed keyboard height limitations, we will approve all of the translation
|
|
var approvedTranslation = attemptedTranslation
|
|
|
|
// Calculate new proposed height by subtracting the translation.
|
|
let proposedKeyboardHeight = self.keyboardHeight - attemptedTranslation
|
|
|
|
// calculations vary depending on reducing or enlarging the keyboard
|
|
if (reducingKeyboardHeight) {
|
|
// if we are reducing too much, then calculate how much translation is allowed
|
|
if (proposedKeyboardHeight < self.minKeyboardHeight) {
|
|
// if we are already at the minimum size, return 0
|
|
if(self.keyboardHeight <= self.minKeyboardHeight) {
|
|
approvedTranslation = 0;
|
|
} else {
|
|
// how much space to translate the resizer downward, a positive number
|
|
let availableToReduce = self.keyboardHeight - self.minKeyboardHeight
|
|
approvedTranslation = availableToReduce
|
|
}
|
|
}
|
|
} else { // enlarging the keyboard height
|
|
if (proposedKeyboardHeight > self.maxKeyboardHeight) {
|
|
// if we are already at the maximum size, return 0
|
|
if(self.keyboardHeight >= self.maxKeyboardHeight) {
|
|
approvedTranslation = 0;
|
|
} else {
|
|
// how much space to translate the resizer upward, a negative number
|
|
let availableToEnlarge = self.keyboardHeight - self.maxKeyboardHeight
|
|
approvedTranslation = availableToEnlarge
|
|
}
|
|
}
|
|
}
|
|
|
|
// round to nearest whole number
|
|
approvedTranslation = approvedTranslation.rounded(.toNearestOrAwayFromZero)
|
|
let messageBegan = "approvedTranslation: \(approvedTranslation)"
|
|
os_log("%{public}s", log:KeymanEngineLogger.ui, type: .debug, messageBegan)
|
|
return approvedTranslation
|
|
}
|
|
|
|
/**
|
|
* Called when the keyboardResizer view is being moved to adjust the keyboard height.
|
|
*/
|
|
@objc func handleDrag(_ dragRecognizer: UIPanGestureRecognizer) {
|
|
switch dragRecognizer.state {
|
|
case .began:
|
|
os_log("keyboardResizer handleDrag began", log:KeymanEngineLogger.ui, type: .info)
|
|
|
|
case .changed:
|
|
self.provideResizeFeedback(drag: dragRecognizer)
|
|
|
|
case .ended:
|
|
// reset transforms before actual keyboard height change
|
|
self.keyboardResizer.transform = .identity
|
|
self.keyboardImage.transform = .identity
|
|
|
|
let verticalTranslation = dragRecognizer.translation(in: self.contentView).y
|
|
let approvedTranslation = self.applyKeyboardSizeLimits(attemptedTranslation: verticalTranslation)
|
|
if (approvedTranslation != 0) {
|
|
let newKeyboardHeight = keyboardHeight - approvedTranslation;
|
|
self.writeNewKeyboardHeight(newHeight: newKeyboardHeight)
|
|
} else {
|
|
os_log("handleDrag .ended with no resizing", log:KeymanEngineLogger.ui, type: .info)
|
|
}
|
|
default: break
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stretch the keyboard image and move the keyboardResizier object as it is being resized.
|
|
*/
|
|
private func provideResizeFeedback(drag: UIPanGestureRecognizer) {
|
|
// how much did we drag vertically
|
|
let verticalTranslation = drag.translation(in: self.contentView).y
|
|
|
|
// enforce keyboard size limits on translation
|
|
let approvedTranslation = self.applyKeyboardSizeLimits(attemptedTranslation: verticalTranslation)
|
|
|
|
if (approvedTranslation != 0) {
|
|
let keyboardTranslation = CGAffineTransform(translationX: 0, y: approvedTranslation)
|
|
self.keyboardResizer.transform = keyboardTranslation
|
|
|
|
let newKeyboardHeight = self.keyboardHeight - approvedTranslation;
|
|
let yScaleFactor = newKeyboardHeight/self.keyboardHeight
|
|
|
|
let keyboardScale = CGAffineTransform(scaleX: 1, y: yScaleFactor)
|
|
|
|
// concatenate both the translate and the scale for the keyboard image
|
|
let keyboardHeightTranslation = CGAffineTransform(translationX: 0, y: approvedTranslation/2)
|
|
let keyboardImageTransform = keyboardScale.concatenating(keyboardHeightTranslation)
|
|
self.keyboardImage.transform = keyboardImageTransform
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Write the new height of the keyboard to to UserDefaults.
|
|
*/
|
|
private func writeNewKeyboardHeight (newHeight: Double) {
|
|
let message = "writeNewKeyboardHeight, newHeight :\(newHeight) isPortrait: \(isPortrait)"
|
|
os_log("%{public}s", log:KeymanEngineLogger.ui, type: .info, message)
|
|
|
|
keyboardHeight = newHeight
|
|
self.updateKeyboardConstraints()
|
|
|
|
if (isPortrait) {
|
|
Storage.active.userDefaults.portraitKeyboardHeight = newHeight
|
|
} else {
|
|
Storage.active.userDefaults.landscapeKeyboardHeight = newHeight
|
|
}
|
|
}
|
|
|
|
private func configureKeyboardConstraints() {
|
|
// create constraints for each orientation and apply when device is rotated
|
|
self.keyboardConstraintsArray = [
|
|
keyboardImage.leftAnchor.constraint(equalTo: contentView.leftAnchor),
|
|
keyboardImage.rightAnchor.constraint(equalTo: contentView.rightAnchor),
|
|
keyboardImage.topAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -keyboardHeight),
|
|
keyboardImage.heightAnchor.constraint(equalToConstant: keyboardHeight)
|
|
]
|
|
NSLayoutConstraint.activate(self.keyboardConstraintsArray)
|
|
}
|
|
|
|
/**
|
|
* recalculate constraints for new portrait keyboard height
|
|
*/
|
|
private func updateKeyboardConstraints() {
|
|
if (!self.keyboardConstraintsArray.isEmpty) {
|
|
NSLayoutConstraint.deactivate(self.keyboardConstraintsArray)
|
|
}
|
|
self.keyboardConstraintsArray = [
|
|
keyboardImage.leftAnchor.constraint(equalTo: contentView.leftAnchor),
|
|
keyboardImage.rightAnchor.constraint(equalTo: contentView.rightAnchor),
|
|
keyboardImage.topAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -self.keyboardHeight),
|
|
keyboardImage.heightAnchor.constraint(equalToConstant: self.keyboardHeight)
|
|
]
|
|
NSLayoutConstraint.activate(self.keyboardConstraintsArray)
|
|
|
|
self.updateKeyboardResizerConstraints()
|
|
}
|
|
|
|
private func configureContentView() {
|
|
contentView.isUserInteractionEnabled = true
|
|
contentView.backgroundColor = .white
|
|
view.addSubview(contentView)
|
|
|
|
contentView.translatesAutoresizingMaskIntoConstraints = false
|
|
NSLayoutConstraint.activate([
|
|
contentView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
|
|
contentView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor),
|
|
contentView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor),
|
|
contentView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
|
|
])
|
|
}
|
|
|
|
func configureDefaultHeightButton() {
|
|
defaultButton.titleLabel?.font = .systemFont(ofSize: 20, weight: .bold)
|
|
let buttonTitle = NSLocalizedString("button-label-reset-default-keyboard-height", bundle: engineBundle, comment: "")
|
|
defaultButton.setTitle(buttonTitle, for: .normal)
|
|
defaultButton.sizeToFit()
|
|
defaultButton.layer.cornerRadius = 8.0
|
|
defaultButton.addTarget(self, action: #selector(self.restoreDefaultKeyboardHeight), for: .touchUpInside)
|
|
contentView.addSubview(defaultButton)
|
|
|
|
defaultButton.translatesAutoresizingMaskIntoConstraints = false
|
|
NSLayoutConstraint.activate([
|
|
defaultButton.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10.0),
|
|
defaultButton.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10.0)
|
|
])
|
|
}
|
|
|
|
@objc private func restoreDefaultKeyboardHeight() {
|
|
os_log("restored to default", log:KeymanEngineLogger.ui, type: .info)
|
|
let defaultHeight = self.isPortrait ? self.defaultPortraitHeight : self.defaultLandscapeHeight
|
|
|
|
UIView.animate(withDuration: 1.1, delay: 0.1, usingSpringWithDamping: 0.35, initialSpringVelocity: 4.5, options: .curveEaseOut, animations: {
|
|
self.writeNewKeyboardHeight(newHeight: defaultHeight)
|
|
self.view.layoutIfNeeded()
|
|
}, completion: nil)
|
|
}
|
|
|
|
private func configureLabel() {
|
|
label.backgroundColor = .white
|
|
label.textColor = .black
|
|
label.textAlignment = NSTextAlignment.left
|
|
label.font = UIFont.systemFont(ofSize: 16.0)
|
|
self.setLabelTextForOrientation()
|
|
label.numberOfLines = 0
|
|
contentView.addSubview(label)
|
|
|
|
label.translatesAutoresizingMaskIntoConstraints = false
|
|
NSLayoutConstraint.activate([
|
|
label.topAnchor.constraint(equalTo: self.defaultButton.bottomAnchor, constant: 5.0),
|
|
label.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10.0),
|
|
label.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant:-25.0),
|
|
label.heightAnchor.constraint(greaterThanOrEqualToConstant: 50.0)
|
|
])
|
|
}
|
|
|
|
private func setLabelTextForOrientation() {
|
|
let dragText = NSLocalizedString("keyboard-drag-instructions", bundle: engineBundle, comment: "")
|
|
var rotateText: String? = nil;
|
|
|
|
if (self.isPortrait) {
|
|
rotateText = NSLocalizedString("portrait-keyboard-rotate-instructions", bundle: engineBundle, comment: "")
|
|
} else {
|
|
rotateText = NSLocalizedString("landscape-keyboard-rotate-instructions", bundle: engineBundle, comment: "")
|
|
}
|
|
|
|
label.text = "\(dragText)\n\(rotateText!)"
|
|
}
|
|
|
|
private func configureKeyboardImage() {
|
|
keyboardImage.isUserInteractionEnabled = true
|
|
keyboardImage.contentMode = UIView.ContentMode.scaleToFill
|
|
keyboardImage.backgroundColor=UIColor.cyan
|
|
contentView.addSubview(keyboardImage)
|
|
|
|
keyboardImage.translatesAutoresizingMaskIntoConstraints = false
|
|
}
|
|
|
|
private func configureKeyboardResizer() {
|
|
var arrowSymbolImage: UIImage? = nil
|
|
keyboardResizer.isUserInteractionEnabled = true
|
|
|
|
// if using iOS earlier than 13.0, use bitmap image of arrow and non-dynamic gray
|
|
if #available(iOSApplicationExtension 13.0, *) {
|
|
arrowSymbolImage = UIImage(systemName: "arrow.up.and.down.square.fill")
|
|
keyboardResizer.backgroundColor = UIColor.systemGray3 // adapts for light/dark mode
|
|
} else {
|
|
arrowSymbolImage = UIImage(named:"arrow.up.and.down", in:engineBundle, compatibleWith:nil)
|
|
keyboardResizer.backgroundColor = UIColor.systemGray
|
|
}
|
|
keyboardResizer.image = arrowSymbolImage
|
|
keyboardResizer.layer.cornerRadius = 5
|
|
contentView.addSubview(keyboardResizer)
|
|
keyboardResizer.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
self.updateKeyboardResizerConstraints()
|
|
|
|
let drag = UIPanGestureRecognizer(target: self, action: #selector(handleDrag))
|
|
drag.maximumNumberOfTouches = 1
|
|
drag.minimumNumberOfTouches = 1
|
|
keyboardResizer.addGestureRecognizer(drag)
|
|
}
|
|
|
|
private func updateKeyboardResizerConstraints() {
|
|
// first, clear existing constraints
|
|
if (!resizerConstraintsArray.isEmpty) {
|
|
NSLayoutConstraint.deactivate(resizerConstraintsArray)
|
|
}
|
|
|
|
// add new constraints using the current value of the keyboard height
|
|
resizerConstraintsArray = [
|
|
keyboardResizer.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
|
|
keyboardResizer.topAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -keyboardHeight-30),
|
|
keyboardResizer.heightAnchor.constraint(equalToConstant: 60),
|
|
keyboardResizer.widthAnchor.constraint(equalToConstant: 60)
|
|
]
|
|
NSLayoutConstraint.activate(resizerConstraintsArray)
|
|
}
|
|
|
|
private func updateKeyboardImage() {
|
|
var kbImage: UIImage? = nil
|
|
if (self.isPortrait) {
|
|
kbImage = UIImage(named:"keyboard.compact", in:engineBundle, compatibleWith:nil)
|
|
} else {
|
|
kbImage = UIImage(named:"keyboard.regular", in:engineBundle, compatibleWith:nil)
|
|
}
|
|
keyboardImage.image = kbImage
|
|
|
|
let kbImageMessage = "updateKeyboardImage, kbImage: \(String(describing: kbImage))"
|
|
os_log("%{public}s", log:KeymanEngineLogger.ui, type: .debug, kbImageMessage)
|
|
}
|
|
|
|
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
|
let rotateMessage = "viewWillTransition to size \(size) with current UIScreen.main.bounds.size size = \(UIScreen.main.bounds.size)"
|
|
os_log("%{public}s", log:KeymanEngineLogger.ui, type: .info, rotateMessage)
|
|
super.viewWillTransition(to: size, with: coordinator)
|
|
|
|
/**
|
|
determine which orientation the device had rotated to and adjust content as necessary
|
|
*/
|
|
self.determineOrientation()
|
|
|
|
coordinator.animate(alongsideTransition: { _ in
|
|
self.applyKeyboardHeight()
|
|
self.calculateKeyboardHeightLimits()
|
|
self.setLabelTextForOrientation()
|
|
self.updateKeyboardImage()
|
|
self.updateKeyboardConstraints()
|
|
}, completion: nil)
|
|
}
|
|
|
|
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
|
super.traitCollectionDidChange(previousTraitCollection)
|
|
if (traitCollection.userInterfaceStyle == .dark) {
|
|
os_log("keyboardHeight traitCollectionDidChange to .dark", log:KeymanEngineLogger.ui, type: .debug)
|
|
} else {
|
|
os_log("keyboardHeight traitCollectionDidChange to .light", log:KeymanEngineLogger.ui, type: .debug)
|
|
}
|
|
}
|
|
|
|
required public init?(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
}
|
|
|