TOPasscodeKeypadView.m 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432
  1. //
  2. // TOPasscodeKeypadView.h
  3. //
  4. // Copyright 2017 Timothy Oliver. All rights reserved.
  5. //
  6. // Permission is hereby granted, free of charge, to any person obtaining a copy
  7. // of this software and associated documentation files (the "Software"), to
  8. // deal in the Software without restriction, including without limitation the
  9. // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
  10. // sell copies of the Software, and to permit persons to whom the Software is
  11. // furnished to do so, subject to the following conditions:
  12. //
  13. // The above copyright notice and this permission notice shall be included in
  14. // all copies or substantial portions of the Software.
  15. //
  16. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
  17. // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
  20. // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
  21. // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  22. #import "TOPasscodeKeypadView.h"
  23. #import "TOPasscodeCircleImage.h"
  24. #import "TOPasscodeCircleButton.h"
  25. #import "TOPasscodeCircleView.h"
  26. #import "TOPasscodeButtonLabel.h"
  27. @interface TOPasscodeKeypadView()
  28. /* Passcode buttons */
  29. @property (nonatomic, strong, readwrite) NSArray<TOPasscodeCircleButton *> *keypadButtons;
  30. /* The '0' button for the different layouts */
  31. @property (nonatomic, strong) TOPasscodeCircleButton *verticalZeroButton;
  32. @property (nonatomic, strong) TOPasscodeCircleButton *horizontalZeroButton;
  33. /* Images */
  34. @property (nonatomic, strong) UIImage *buttonImage;
  35. @property (nonatomic, strong) UIImage *tappedButtonImage;
  36. @end
  37. @implementation TOPasscodeKeypadView
  38. - (instancetype)initWithFrame:(CGRect)frame
  39. {
  40. if (self = [super initWithFrame:frame]) {
  41. self.userInteractionEnabled = YES;
  42. _buttonDiameter = 81.0f;
  43. _buttonSpacing = (CGSize){25,15};
  44. _buttonStrokeWidth = 1.5f;
  45. _showLettering = YES;
  46. _buttonNumberFont = nil;
  47. _buttonLetteringFont = nil;
  48. _buttonLabelSpacing = FLT_MIN;
  49. _buttonLetteringSpacing = FLT_MIN;
  50. [self sizeToFit];
  51. }
  52. return self;
  53. }
  54. - (TOPasscodeCircleButton *)makeCircleButtonWithNumber:(NSInteger)number letteringString:(NSString *)letteringString
  55. {
  56. NSString *numberString = [NSString stringWithFormat:@"%ld", (long)number];
  57. TOPasscodeCircleButton *circleButton = [[TOPasscodeCircleButton alloc] initWithNumberString:numberString letteringString:letteringString];
  58. circleButton.backgroundImage = self.buttonImage;
  59. circleButton.hightlightedBackgroundImage = self.tappedButtonImage;
  60. circleButton.vibrancyEffect = self.vibrancyEffect;
  61. // Add handler for when button is tapped
  62. __weak typeof(self) weakSelf = self;
  63. circleButton.buttonTappedHandler = ^{
  64. if (weakSelf.buttonTappedHandler) {
  65. weakSelf.buttonTappedHandler(number);
  66. }
  67. };
  68. return circleButton;
  69. }
  70. - (void)setUpButtons
  71. {
  72. NSMutableArray *buttons = [NSMutableArray array];
  73. NSInteger numberOfButtons = 11; // 1-9 are normal, 10 is the vertical '0', 11 is the horizontal '0'
  74. NSArray *letteredTitles = @[@"ABC", @"DEF", @"GHI", @"JKL",
  75. @"MNO", @"PQRS", @"TUV", @"WXYZ"];
  76. for (NSInteger i = 0; i < numberOfButtons; i++) {
  77. // Work out the button number text
  78. NSInteger buttonNumber = i + 1;
  79. if (buttonNumber == 10 || buttonNumber == 11) { buttonNumber = 0; }
  80. // Work out the lettering text
  81. NSString *letteringString = nil;
  82. if (self.showLettering && i > 0 && i-1 < letteredTitles.count) { // (Skip 1 and 0)
  83. letteringString = letteredTitles[i-1];
  84. }
  85. // Create a new button and add it to this view
  86. TOPasscodeCircleButton *circleButton = [self makeCircleButtonWithNumber:buttonNumber letteringString:letteringString];
  87. [self addSubview:circleButton];
  88. [buttons addObject:circleButton];
  89. if (!self.showLettering) {
  90. circleButton.buttonLabel.verticallyCenterNumberLabel = YES; // Center the digit in the middle
  91. }
  92. // Hang onto the 0 button if it's the vertical one
  93. // And center the text
  94. if (i == 9) {
  95. self.verticalZeroButton = circleButton;
  96. // Hide the button if it's not vertically laid out
  97. if (self.horizontalLayout) {
  98. self.verticalZeroButton.contentAlpha = 0.0f;
  99. self.verticalZeroButton.hidden = YES;
  100. }
  101. }
  102. else if (i == 10) {
  103. self.horizontalZeroButton = circleButton;
  104. // Hide the button if it's not horizontally laid out
  105. if (!self.horizontalLayout) {
  106. self.horizontalZeroButton.contentAlpha = 0.0f;
  107. self.horizontalZeroButton.hidden = YES;
  108. }
  109. }
  110. }
  111. _keypadButtons = [NSArray arrayWithArray:buttons];
  112. }
  113. - (void)sizeToFit
  114. {
  115. CGFloat padding = 2.0f;
  116. CGRect frame = self.frame;
  117. if (self.horizontalLayout) {
  118. frame.size.width = ((self.buttonDiameter + padding) * 4) + (self.buttonSpacing.width * 3);
  119. frame.size.height = ((self.buttonDiameter + padding) * 3) + (self.buttonSpacing.height * 2);
  120. }
  121. else {
  122. frame.size.width = ((self.buttonDiameter + padding) * 3) + (self.buttonSpacing.width * 2);
  123. frame.size.height = ((self.buttonDiameter + padding) * 4) + (self.buttonSpacing.height * 3);
  124. }
  125. self.frame = CGRectIntegral(frame);
  126. }
  127. - (void)layoutSubviews
  128. {
  129. [super layoutSubviews];
  130. NSInteger i = 0;
  131. CGPoint origin = CGPointZero;
  132. for (TOPasscodeCircleButton *button in self.keypadButtons) {
  133. // Set the button frame
  134. CGRect frame = button.frame;
  135. frame.origin = origin;
  136. button.frame = frame;
  137. // Work out the next offset
  138. CGFloat horizontalOffset = frame.size.width + self.buttonSpacing.width;
  139. origin.x += horizontalOffset;
  140. i++;
  141. // If we're at the end of the row, move to the next one
  142. if (i % 3 == 0) {
  143. origin.x = 0.0f;
  144. origin.y = origin.y + frame.size.height + self.buttonSpacing.height;
  145. }
  146. }
  147. // Lay out the vertical button
  148. CGRect frame = self.verticalZeroButton.frame;
  149. frame.origin.x += (frame.size.width + self.buttonSpacing.width);
  150. self.verticalZeroButton.frame = frame;
  151. // Lay out the horizontal button
  152. frame = self.horizontalZeroButton.frame;
  153. frame.origin.x = (frame.size.width + self.buttonSpacing.width) * 3.0f;
  154. frame.origin.y = frame.size.height + self.buttonSpacing.height;
  155. self.horizontalZeroButton.frame = frame;
  156. // Layout the accessory buttons
  157. CGFloat midPointY = CGRectGetMidY(self.verticalZeroButton.frame);
  158. if (self.leftAccessoryView) {
  159. CGRect leftButtonFrame = self.keypadButtons.firstObject.frame;
  160. CGFloat midPointX = CGRectGetMidX(leftButtonFrame);
  161. [self.leftAccessoryView sizeToFit];
  162. self.leftAccessoryView.center = (CGPoint){midPointX, midPointY};
  163. }
  164. if (self.rightAccessoryView) {
  165. CGRect rightButtonFrame = self.keypadButtons[2].frame;
  166. CGFloat midPointX = CGRectGetMidX(rightButtonFrame);
  167. [self.rightAccessoryView sizeToFit];
  168. self.rightAccessoryView.center = (CGPoint){midPointX, midPointY};
  169. }
  170. }
  171. #pragma mark - Style Accessors -
  172. - (void)setVibrancyEffect:(UIVibrancyEffect *)vibrancyEffect
  173. {
  174. if (vibrancyEffect == _vibrancyEffect) { return; }
  175. _vibrancyEffect = vibrancyEffect;
  176. for (TOPasscodeCircleButton *button in self.keypadButtons) {
  177. button.vibrancyEffect = _vibrancyEffect;
  178. }
  179. }
  180. #pragma mark - Lazy Getters -
  181. - (UIImage *)buttonImage
  182. {
  183. if (!_buttonImage) {
  184. _buttonImage = [TOPasscodeCircleImage hollowCircleImageOfSize:self.buttonDiameter strokeWidth:self.buttonStrokeWidth padding:1.0f];
  185. }
  186. return _buttonImage;
  187. }
  188. - (UIImage *)tappedButtonImage
  189. {
  190. if (!_tappedButtonImage) {
  191. _tappedButtonImage = [TOPasscodeCircleImage circleImageOfSize:self.buttonDiameter inset:self.buttonStrokeWidth * 0.5f padding:1.0f antialias:YES];
  192. }
  193. return _tappedButtonImage;
  194. }
  195. - (NSArray<TOPasscodeCircleButton *> *)keypadButtons
  196. {
  197. if (_keypadButtons) { return _keypadButtons; }
  198. [self setUpButtons];
  199. return _keypadButtons;
  200. }
  201. #pragma mark - Audio Delegate Protocol -
  202. - (BOOL)enableInputClicksWhenVisible
  203. {
  204. return YES;
  205. }
  206. #pragma mark - Public Layout Setters -
  207. - (void)setHorizontalLayout:(BOOL)horizontalLayout
  208. {
  209. [self setHorizontalLayout:horizontalLayout animated:NO duration:0.0f];
  210. }
  211. - (void)setHorizontalLayout:(BOOL)horizontalLayout animated:(BOOL)animated duration:(CGFloat)duration
  212. {
  213. if (horizontalLayout== _horizontalLayout) {
  214. return;
  215. }
  216. _horizontalLayout = horizontalLayout;
  217. // Resize itself now so the frame value is up to date externally
  218. [self sizeToFit];
  219. // Set initial animation state
  220. self.verticalZeroButton.hidden = NO;
  221. self.horizontalZeroButton.hidden = NO;
  222. self.verticalZeroButton.contentAlpha = _horizontalLayout ? 1.0f : 0.0f;
  223. self.horizontalZeroButton.contentAlpha = _horizontalLayout ? 0.0f : 1.0f;
  224. void (^animationBlock)(void) = ^{
  225. self.verticalZeroButton.contentAlpha = self.horizontalLayout ? 0.0f : 1.0f;
  226. self.horizontalZeroButton.contentAlpha = self.horizontalLayout ? 1.0f : 0.0f;
  227. };
  228. void (^completionBlock)(BOOL) = ^(BOOL complete) {
  229. self.verticalZeroButton.hidden = self.horizontalLayout;
  230. self.horizontalZeroButton.hidden = self.horizontalLayout;
  231. };
  232. // Don't animate if not needed
  233. if (!animated) {
  234. animationBlock();
  235. completionBlock(YES);
  236. return;
  237. }
  238. // Perform animation
  239. [UIView animateWithDuration:duration animations:animationBlock completion:completionBlock];
  240. }
  241. - (void)updateButtonsForCurrentState
  242. {
  243. for (TOPasscodeCircleButton *circleButton in self.keypadButtons) {
  244. circleButton.backgroundImage = self.buttonImage;
  245. circleButton.hightlightedBackgroundImage = self.tappedButtonImage;
  246. circleButton.numberFont = self.buttonNumberFont;
  247. circleButton.letteringFont = self.buttonLetteringFont;
  248. circleButton.letteringVerticalSpacing = self.buttonLabelSpacing;
  249. circleButton.letteringCharacterSpacing = self.buttonLetteringSpacing;
  250. circleButton.tintColor = self.buttonBackgroundColor;
  251. circleButton.textColor = self.buttonTextColor;
  252. circleButton.highlightedTextColor = self.buttonHighlightedTextColor;
  253. if (!_showLettering) {
  254. circleButton.buttonLabel.letteringLabel.text = nil;
  255. circleButton.buttonLabel.verticallyCenterNumberLabel = YES;
  256. }
  257. }
  258. [self setNeedsLayout];
  259. }
  260. - (void)setButtonDiameter:(CGFloat)buttonDiameter
  261. {
  262. if (_buttonDiameter == buttonDiameter) { return; }
  263. _buttonDiameter = buttonDiameter;
  264. _tappedButtonImage = nil;
  265. _buttonImage = nil;
  266. [self updateButtonsForCurrentState];
  267. }
  268. - (void)setButtonSpacing:(CGSize)buttonSpacing
  269. {
  270. if (CGSizeEqualToSize(_buttonSpacing, buttonSpacing)) { return; }
  271. _buttonSpacing = buttonSpacing;
  272. [self updateButtonsForCurrentState];
  273. }
  274. - (void)setButtonStrokeWidth:(CGFloat)buttonStrokeWidth
  275. {
  276. if (_buttonStrokeWidth== buttonStrokeWidth) { return; }
  277. _buttonStrokeWidth = buttonStrokeWidth;
  278. _tappedButtonImage = nil;
  279. _buttonImage = nil;
  280. [self updateButtonsForCurrentState];
  281. }
  282. - (void)setShowLettering:(BOOL)showLettering
  283. {
  284. if (_showLettering == showLettering) { return; }
  285. _showLettering = showLettering;
  286. [self updateButtonsForCurrentState];
  287. }
  288. - (void)setButtonNumberFont:(UIFont *)buttonNumberFont
  289. {
  290. if (_buttonNumberFont == buttonNumberFont) { return; }
  291. _buttonNumberFont = buttonNumberFont;
  292. [self updateButtonsForCurrentState];
  293. }
  294. - (void)setButtonLetteringFont:(UIFont *)buttonLetteringFont
  295. {
  296. if (buttonLetteringFont == _buttonLetteringFont) { return; }
  297. _buttonLetteringFont = buttonLetteringFont;
  298. [self updateButtonsForCurrentState];
  299. }
  300. - (void)setButtonLabelSpacing:(CGFloat)buttonLabelSpacing
  301. {
  302. if (buttonLabelSpacing == _buttonLabelSpacing) { return; }
  303. _buttonLabelSpacing = buttonLabelSpacing;
  304. [self updateButtonsForCurrentState];
  305. }
  306. - (void)setButtonLetteringSpacing:(CGFloat)buttonLetteringSpacing
  307. {
  308. if (buttonLetteringSpacing == _buttonLetteringSpacing) { return; }
  309. _buttonLetteringSpacing = buttonLetteringSpacing;
  310. [self updateButtonsForCurrentState];
  311. }
  312. - (void)setButtonBackgroundColor:(UIColor *)buttonBackgroundColor
  313. {
  314. if (buttonBackgroundColor == _buttonBackgroundColor) { return; }
  315. _buttonBackgroundColor = buttonBackgroundColor;
  316. [self updateButtonsForCurrentState];
  317. }
  318. - (void)setButtonTextColor:(UIColor *)buttonTextColor
  319. {
  320. if (buttonTextColor == _buttonTextColor) { return; }
  321. _buttonTextColor = buttonTextColor;
  322. [self updateButtonsForCurrentState];
  323. }
  324. - (void)setButtonHighlightedTextColor:(UIColor *)buttonHighlightedTextColor
  325. {
  326. if (buttonHighlightedTextColor == _buttonHighlightedTextColor) { return; }
  327. _buttonHighlightedTextColor = buttonHighlightedTextColor;
  328. [self updateButtonsForCurrentState];
  329. }
  330. - (void)setLeftAccessoryView:(UIView *)leftAccessoryView
  331. {
  332. if (_leftAccessoryView == leftAccessoryView) { return; }
  333. _leftAccessoryView = leftAccessoryView;
  334. [self addSubview:_leftAccessoryView];
  335. [self setNeedsLayout];
  336. }
  337. - (void)setRightAccessoryView:(UIView *)rightAccessoryView
  338. {
  339. if (_rightAccessoryView == rightAccessoryView) { return; }
  340. _rightAccessoryView = rightAccessoryView;
  341. [self addSubview:_rightAccessoryView];
  342. [self setNeedsLayout];
  343. }
  344. - (void)setContentAlpha:(CGFloat)contentAlpha
  345. {
  346. _contentAlpha = contentAlpha;
  347. for (TOPasscodeCircleButton *button in self.keypadButtons) {
  348. // Skip whichever '0' button is not presently being used
  349. if ((self.horizontalLayout && button == self.verticalZeroButton) ||
  350. (!self.horizontalLayout && button == self.horizontalZeroButton))
  351. {
  352. continue;
  353. }
  354. button.contentAlpha = contentAlpha;
  355. }
  356. }
  357. @end