KBSlider.m 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558
  1. //
  2. // KBSlider.m
  3. // KBSlider
  4. //
  5. // Created by Kevin Bradley on 12/25/20.
  6. // Copyright © 2020 nito. All rights reserved.
  7. //
  8. #import "KBSlider.h"
  9. #import <GameController/GameController.h>
  10. @interface KBSlider() {
  11. CGFloat _minimumValue;
  12. CGFloat _maximumValue;
  13. UIColor *_maximumTrackTintColor;
  14. UIColor *_minimumTrackTintColor;
  15. UIColor *_thumbTintColor;
  16. CGFloat _focusScaleFactor;
  17. BOOL _continuous;
  18. BOOL _isEnabled;
  19. BOOL _isSelected;
  20. BOOL _isHighlighted;
  21. }
  22. @property CGFloat trackViewHeight;
  23. @property CGFloat thumbSize;
  24. @property NSTimeInterval animationDuration;
  25. @property CGFloat defaultValue;
  26. @property CGFloat defaultMinimumValue;
  27. @property CGFloat defaultMaximumValue;
  28. @property BOOL defaultIsContinuous;
  29. @property UIColor *defaultThumbTintColor;
  30. @property UIColor *defaultTrackColor;
  31. @property UIColor *defaultMininumTrackTintColor;
  32. @property CGFloat defaultFocusScaleFactor;
  33. @property CGFloat defaultStepValue;
  34. @property CGFloat decelerationRate;
  35. @property CGFloat decelerationMaxVelocity;
  36. @property CGFloat fineTunningVelocityThreshold;
  37. @property NSMutableDictionary *thumbViewImages; //[UInt: UIImage] - not an allowed dict type in obj-c
  38. @property UIImageView *thumbView;
  39. @property NSMutableDictionary *trackViewImages; //[UInt: UIImage] - not an allowed dict type in obj-c
  40. @property UIImageView *trackView;
  41. @property NSMutableDictionary *minimumTrackViewImages; //[UInt: UIImage] - not an allowed dict type in obj-c
  42. @property UIImageView *minimumTrackView;
  43. @property NSMutableDictionary *maximumTrackViewImages; //[UInt: UIImage] - not an allowed dict type in obj-c
  44. @property UIImageView *maximumTrackView;
  45. @property UIPanGestureRecognizer *panGestureRecognizer;
  46. @property UITapGestureRecognizer *leftTapGestureRecognizer;
  47. @property UITapGestureRecognizer *rightTapGestureRecognizer;
  48. @property NSLayoutConstraint *thumbViewCenterXConstraint;
  49. @property DPadState dPadState; //.select
  50. @property NSTimer *deceleratingTimer;
  51. @property CGFloat deceleratingVelocity;
  52. @property CGFloat thumbViewCenterXConstraintConstant;
  53. @end
  54. @implementation KBSlider
  55. - (void)initializeDefaults {
  56. _trackViewHeight = 5;
  57. _thumbSize = 30;
  58. _animationDuration = 0.3;
  59. _defaultValue = 0;
  60. _defaultMinimumValue = 0;
  61. _defaultMaximumValue = 1;
  62. _defaultIsContinuous = true;
  63. _defaultThumbTintColor = [UIColor whiteColor];
  64. _defaultTrackColor = [UIColor grayColor];
  65. _defaultMininumTrackTintColor = [UIColor blueColor];
  66. _defaultFocusScaleFactor = 1.05;
  67. _defaultStepValue = 0.1;
  68. _decelerationRate = 0.92;
  69. _decelerationMaxVelocity = 1000;
  70. _fineTunningVelocityThreshold = 600;
  71. _storedValue = _defaultValue;
  72. _dPadState = DPadStateSelect;
  73. _continuous = _defaultIsContinuous;
  74. _minimumTrackViewImages = [NSMutableDictionary new];
  75. _maximumTrackViewImages = [NSMutableDictionary new];
  76. _trackViewImages = [NSMutableDictionary new];
  77. _thumbViewImages = [NSMutableDictionary new];
  78. _thumbTintColor = _defaultThumbTintColor;
  79. _minimumTrackTintColor = _defaultMininumTrackTintColor;
  80. _focusScaleFactor = _defaultFocusScaleFactor;
  81. _minimumValue = _defaultMinimumValue;
  82. _maximumValue = _defaultMaximumValue;
  83. _stepValue = _defaultStepValue;
  84. [self setEnabled:true];
  85. }
  86. - (BOOL)isContinuous {
  87. return _continuous;
  88. }
  89. - (void)setContinuous:(BOOL)continuous {
  90. _continuous = continuous;
  91. }
  92. - (void)setSelected:(BOOL)selected {
  93. _isSelected = selected;
  94. [self updateStateDependantViews];
  95. }
  96. - (BOOL)isSelected {
  97. return _isSelected;
  98. }
  99. - (void)setHighlighted:(BOOL)highlighted {
  100. _isHighlighted = highlighted;
  101. [self updateStateDependantViews];
  102. }
  103. - (BOOL)isHighlighted {
  104. return _isHighlighted;
  105. }
  106. - (void)setEnabled:(BOOL)enabled {
  107. _isEnabled = enabled;
  108. _panGestureRecognizer.enabled = enabled;
  109. [self updateStateDependantViews];
  110. }
  111. - (BOOL)isEnabled {
  112. return _isEnabled;
  113. }
  114. - (CGFloat)value {
  115. return _storedValue;
  116. }
  117. - (void)setValue:(CGFloat)value afterDelay:(NSInteger)delay {
  118. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
  119. [self setValue:value];
  120. });
  121. }
  122. - (void)setValue:(CGFloat)newValue {
  123. _storedValue = MIN(_maximumValue, newValue);
  124. _storedValue = MAX(_minimumValue, _storedValue);
  125. if (_trackView.bounds.size.width == 0){
  126. [self setValue:newValue afterDelay:0.1];
  127. }
  128. CGFloat offset = _trackView.bounds.size.width * (_storedValue - _minimumValue) / (_maximumValue - _minimumValue);
  129. offset = MIN(_trackView.bounds.size.width, offset);
  130. if(isnan(offset)){
  131. return;
  132. }
  133. _thumbViewCenterXConstraint.constant = offset;
  134. }
  135. - (CGFloat)maximumValue {
  136. return _maximumValue;
  137. }
  138. - (void)setMaximumValue:(CGFloat)maximumValue {
  139. _maximumValue = maximumValue;
  140. [self setValue:MIN(self.value, maximumValue)];
  141. }
  142. - (CGFloat)minimumValue {
  143. return _minimumValue;
  144. }
  145. - (void)setMinimumValue:(CGFloat)minimumValue {
  146. _minimumValue = minimumValue;
  147. [self setValue:MAX(self.value, minimumValue)];
  148. }
  149. - (UIColor *)maximumTrackTintColor {
  150. return _maximumTrackTintColor;
  151. }
  152. - (void)setMaximumTrackTintColor:(UIColor *)maximumTrackTintColor {
  153. _maximumTrackTintColor = maximumTrackTintColor;
  154. _maximumTrackView.backgroundColor = maximumTrackTintColor;
  155. }
  156. - (void)setMinimumTrackTintColor:(UIColor *)minimumTrackTintColor {
  157. _minimumTrackTintColor = minimumTrackTintColor;
  158. _minimumTrackView.backgroundColor = minimumTrackTintColor;
  159. }
  160. - (UIColor *)thumbTintColor {
  161. return _thumbTintColor;
  162. }
  163. - (void)setThumbTintColor:(UIColor *)thumbTintColor {
  164. _thumbTintColor = thumbTintColor;
  165. _thumbView.backgroundColor = thumbTintColor;
  166. }
  167. - (UIColor *)minimumTrackTintColor {
  168. return _minimumTrackTintColor;
  169. }
  170. - (CGFloat)focusScaleFactor {
  171. return _focusScaleFactor;
  172. }
  173. - (void)setFocusScaleFactor:(CGFloat)focusScaleFactor {
  174. _focusScaleFactor = focusScaleFactor;
  175. [self updateStateDependantViews];
  176. }
  177. - (void)setupView {
  178. [self initializeDefaults];
  179. [self setUpTrackView];
  180. [self setUpMinimumTrackView];
  181. [self setUpMaximumTrackView];
  182. [self setUpThumbView];
  183. [self setUpTrackViewConstraints];
  184. [self setUpMinimumTrackViewConstraints];
  185. [self setUpMaximumTrackViewConstraints];
  186. [self setUpThumbViewConstraints];
  187. [self setUpGestures];
  188. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(controllerConnected:) name:GCControllerDidConnectNotification object:nil];
  189. [self updateStateDependantViews];
  190. }
  191. - (void)setValue:(CGFloat)value animated:(BOOL)animated {
  192. [self setValue:value];
  193. [self stopDeceleratingTimer];
  194. if (animated){
  195. [UIView animateWithDuration:self.animationDuration animations:^{
  196. [self setNeedsLayout];
  197. [self layoutIfNeeded];
  198. }];
  199. }
  200. }
  201. - (void)setMinimumTrackImage:(UIImage *)image forState:(UIControlState)state {
  202. _minimumTrackViewImages[[NSNumber numberWithUnsignedInteger:state]] = image;
  203. [self updateStateDependantViews];
  204. }
  205. - (void)setMaximumTrackImage:(UIImage *)image forState:(UIControlState)state {
  206. _maximumTrackViewImages[[NSNumber numberWithUnsignedInteger:state]] = image;
  207. [self updateStateDependantViews];
  208. }
  209. - (void)setThumbImage:(UIImage *)image forState:(UIControlState)state {
  210. _thumbViewImages[[NSNumber numberWithUnsignedInteger:state]] = image;
  211. [self updateStateDependantViews];
  212. }
  213. - (UIImage *)currentThumbImage {
  214. return _thumbView.image;
  215. }
  216. - (UIImage *)minimumTrackImageForState:(UIControlState)state {
  217. NSNumber *key = [NSNumber numberWithUnsignedInteger:state];
  218. return _minimumTrackViewImages[key];
  219. }
  220. - (UIImage *)maximumTrackImageForState:(UIControlState)state {
  221. NSNumber *key = [NSNumber numberWithUnsignedInteger:state];
  222. return _maximumTrackViewImages[key];
  223. }
  224. - (UIImage *)thumbImageForState:(UIControlState)state {
  225. NSNumber *key = [NSNumber numberWithUnsignedInteger:state];
  226. return _thumbViewImages[key];
  227. }
  228. - (void)setUpThumbView {
  229. _thumbView = [UIImageView new];
  230. _thumbView.layer.cornerRadius = _thumbSize/2;
  231. _thumbView.backgroundColor = _thumbTintColor;
  232. [self addSubview:_thumbView];
  233. }
  234. - (void)setUpTrackView {
  235. _trackView = [UIImageView new];
  236. _trackView.layer.cornerRadius = _trackViewHeight/2;
  237. _trackView.backgroundColor = _defaultTrackColor;
  238. [self addSubview:_trackView];
  239. }
  240. - (void)setUpMinimumTrackView {
  241. _minimumTrackView = [UIImageView new];
  242. _minimumTrackView.layer.cornerRadius = _trackViewHeight/2;
  243. _minimumTrackView.backgroundColor = _minimumTrackTintColor;
  244. [self addSubview:_minimumTrackView];
  245. }
  246. - (void)setUpMaximumTrackView {
  247. _maximumTrackView = [UIImageView new];
  248. _maximumTrackView.layer.cornerRadius = _trackViewHeight/2;
  249. _maximumTrackView.backgroundColor = _maximumTrackTintColor;
  250. [self addSubview:_maximumTrackView];
  251. }
  252. - (void)setUpTrackViewConstraints {
  253. _trackView.translatesAutoresizingMaskIntoConstraints = false;
  254. [_trackView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor].active = true;
  255. [_trackView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor].active = true;
  256. [_trackView.centerYAnchor constraintEqualToAnchor:self.centerYAnchor].active = true;
  257. [_trackView.heightAnchor constraintEqualToConstant:_trackViewHeight].active = true;
  258. }
  259. - (void)setUpMinimumTrackViewConstraints {
  260. _minimumTrackView.translatesAutoresizingMaskIntoConstraints = false;
  261. [_minimumTrackView.leadingAnchor constraintEqualToAnchor:_trackView.leadingAnchor].active = true;
  262. [_minimumTrackView.trailingAnchor constraintEqualToAnchor:_thumbView.centerXAnchor].active = true;
  263. [_minimumTrackView.centerYAnchor constraintEqualToAnchor:_trackView.centerYAnchor].active = true;
  264. [_minimumTrackView.heightAnchor constraintEqualToConstant:_trackViewHeight].active = true;
  265. }
  266. - (void)setUpMaximumTrackViewConstraints {
  267. _maximumTrackView.translatesAutoresizingMaskIntoConstraints = false;
  268. [_maximumTrackView.leadingAnchor constraintEqualToAnchor:_thumbView.centerXAnchor].active = true;
  269. [_maximumTrackView.trailingAnchor constraintEqualToAnchor:_trackView.trailingAnchor].active = true;
  270. [_maximumTrackView.centerYAnchor constraintEqualToAnchor:_trackView.centerYAnchor].active = true;
  271. [_maximumTrackView.heightAnchor constraintEqualToConstant:_trackViewHeight].active = true;
  272. }
  273. - (void)setUpThumbViewConstraints {
  274. _thumbView.translatesAutoresizingMaskIntoConstraints = false;
  275. [_thumbView.centerYAnchor constraintEqualToAnchor:self.centerYAnchor].active = true;
  276. [_thumbView.heightAnchor constraintEqualToConstant:_thumbSize].active = true;
  277. [_thumbView.widthAnchor constraintEqualToConstant:_thumbSize].active = true;
  278. _thumbViewCenterXConstraint = [_thumbView.centerXAnchor constraintEqualToAnchor:_trackView.leadingAnchor constant:self.value];
  279. _thumbViewCenterXConstraint.active = true;
  280. }
  281. - (void)setUpGestures {
  282. _panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panGestureWasTriggered:)];
  283. [self addGestureRecognizer:_panGestureRecognizer];
  284. _leftTapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(leftTapWasTriggered)];
  285. _leftTapGestureRecognizer.allowedPressTypes = @[@(UIPressTypeLeftArrow)];
  286. _leftTapGestureRecognizer.allowedTouchTypes = @[@(UITouchTypeIndirect)];
  287. [self addGestureRecognizer:_leftTapGestureRecognizer];
  288. _rightTapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(rightTapWasTriggered)];
  289. _rightTapGestureRecognizer.allowedPressTypes = @[@(UIPressTypeRightArrow)];
  290. _rightTapGestureRecognizer.allowedTouchTypes = @[@(UITouchTypeIndirect)];
  291. [self addGestureRecognizer:_rightTapGestureRecognizer];
  292. }
  293. - (void)updateStateDependantViews {
  294. UIImage *currentMinImage = _minimumTrackViewImages[[NSNumber numberWithUnsignedInteger:self.state]];
  295. if (currentMinImage){
  296. _minimumTrackView.image = currentMinImage;
  297. } else {
  298. _minimumTrackView.image = _minimumTrackViewImages[[NSNumber numberWithUnsignedInteger:UIControlStateNormal]];
  299. }
  300. UIImage *currentMaxImage = _maximumTrackViewImages[[NSNumber numberWithUnsignedInteger:self.state]];
  301. if (currentMaxImage){
  302. _maximumTrackView.image = currentMaxImage;
  303. } else {
  304. _maximumTrackView.image = _maximumTrackViewImages[[NSNumber numberWithUnsignedInteger:UIControlStateNormal]];
  305. }
  306. UIImage *currentThumbImage = _thumbViewImages[[NSNumber numberWithUnsignedInteger:self.state]];
  307. if (currentThumbImage){
  308. _thumbView.image = currentThumbImage;
  309. } else {
  310. _thumbView.image = _thumbViewImages[[NSNumber numberWithUnsignedInteger:UIControlStateNormal]];
  311. }
  312. if ([self isFocused]){
  313. self.transform = CGAffineTransformMakeScale(_focusScaleFactor, _focusScaleFactor);
  314. } else {
  315. self.transform = CGAffineTransformIdentity;
  316. }
  317. }
  318. - (void)controllerConnected:(NSNotification *)n {
  319. GCController *controller = [n object];
  320. GCMicroGamepad *micro = [controller microGamepad];
  321. if (!micro)return;
  322. CGFloat threshold = 0.7;
  323. micro.reportsAbsoluteDpadValues = true;
  324. micro.dpad.valueChangedHandler = ^(GCControllerDirectionPad * _Nonnull dpad, float xValue, float yValue) {
  325. if (xValue < -threshold){
  326. self.dPadState = DPadStateLeft;
  327. } else if (xValue > threshold){
  328. self.dPadState = DPadStateRight;
  329. } else {
  330. self.dPadState = DPadStateSelect;
  331. }
  332. };
  333. }
  334. - (void)handleDeceleratingTimer:(NSTimer *)timer {
  335. CGFloat centerX = _thumbViewCenterXConstraintConstant + _deceleratingVelocity * 0.01;
  336. CGFloat percent = centerX / (_trackView.frame.size.width);
  337. CGFloat newValue = _minimumValue + ((_maximumValue - _minimumValue) * percent);
  338. [self setValue:newValue];
  339. if ([self isContinuous]){
  340. [self sendActionsForControlEvents:UIControlEventValueChanged];
  341. }
  342. _thumbViewCenterXConstraintConstant = _thumbViewCenterXConstraint.constant;
  343. _deceleratingVelocity *= _decelerationRate;
  344. if (![self isFocused] || fabs(_deceleratingVelocity) < 1){
  345. [self stopDeceleratingTimer];
  346. }
  347. }
  348. - (void)stopDeceleratingTimer {
  349. [_deceleratingTimer invalidate];
  350. _deceleratingTimer = nil;
  351. _deceleratingVelocity = 0;
  352. [self sendActionsForControlEvents:UIControlEventValueChanged];
  353. }
  354. - (BOOL)isVerticalGesture:(UIPanGestureRecognizer *)recognizer {
  355. CGPoint translation = [recognizer translationInView:self];
  356. if (fabs(translation.y) > fabs(translation.x)) {
  357. return true;
  358. }
  359. return false;
  360. }
  361. #pragma mark - Actions
  362. - (void)panGestureWasTriggered:(UIPanGestureRecognizer *)panGestureRecognizer {
  363. if ([self isVerticalGesture:panGestureRecognizer]){
  364. return;
  365. }
  366. CGFloat translation = [panGestureRecognizer translationInView:self].x;
  367. CGFloat velocity = [panGestureRecognizer velocityInView:self].x;
  368. switch(panGestureRecognizer.state){
  369. case UIGestureRecognizerStateBegan:
  370. [self stopDeceleratingTimer];
  371. _thumbViewCenterXConstraintConstant = _thumbViewCenterXConstraint.constant;
  372. break;
  373. case UIGestureRecognizerStateChanged:{
  374. CGFloat centerX = _thumbViewCenterXConstraintConstant + translation / 5;
  375. CGFloat percent = centerX / _trackView.frame.size.width;
  376. CGFloat newValue = _minimumValue + ((_maximumValue - _minimumValue) * percent);
  377. [self setValue:newValue];
  378. if ([self isContinuous]){
  379. [self sendActionsForControlEvents:UIControlEventValueChanged];
  380. }
  381. }
  382. break;
  383. case UIGestureRecognizerStateEnded:
  384. case UIGestureRecognizerStateCancelled:
  385. _thumbViewCenterXConstraintConstant = _thumbViewCenterXConstraint.constant;
  386. if (fabs(velocity) > _fineTunningVelocityThreshold){
  387. CGFloat direction = velocity > 0 ? 1 : -1;
  388. _deceleratingVelocity = fabs(velocity) > _decelerationMaxVelocity ? _decelerationMaxVelocity * direction : velocity;
  389. _deceleratingTimer = [NSTimer scheduledTimerWithTimeInterval:0.01 target:self selector:@selector(handleDeceleratingTimer:) userInfo:nil repeats:true];
  390. } else {
  391. [self stopDeceleratingTimer];
  392. }
  393. break;
  394. default:
  395. break;
  396. }
  397. }
  398. - (void)leftTapWasTriggered {
  399. CGFloat newValue = [self value]-_stepValue;
  400. [self setValue:newValue];
  401. }
  402. - (void)rightTapWasTriggered {
  403. CGFloat newValue = [self value]+_stepValue;
  404. [self setValue:newValue];
  405. }
  406. - (void)pressesBegan:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event {
  407. for (UIPress *press in presses){
  408. switch (press.type) {
  409. case UIPressTypeSelect:
  410. if(_dPadState == DPadStateLeft){
  411. _panGestureRecognizer.enabled = false;
  412. [self leftTapWasTriggered];
  413. } else if (_dPadState == DPadStateRight){
  414. _panGestureRecognizer.enabled = false;
  415. [self rightTapWasTriggered];
  416. } else {
  417. _panGestureRecognizer.enabled = false;
  418. }
  419. break;
  420. default:
  421. break;
  422. }
  423. }
  424. _panGestureRecognizer.enabled = true;
  425. [super pressesBegan:presses withEvent:event];
  426. }
  427. - (void)didUpdateFocusInContext:(UIFocusUpdateContext *)context withAnimationCoordinator:(UIFocusAnimationCoordinator *)coordinator {
  428. [coordinator addCoordinatedAnimations:^{
  429. [self updateStateDependantViews];
  430. } completion:nil];
  431. }
  432. #pragma mark - Initializers
  433. - (id)initWithCoder:(NSCoder *)coder {
  434. self = [super initWithCoder:coder];
  435. [self setupView];
  436. return self;
  437. }
  438. - (id)initWithFrame:(CGRect)frame {
  439. self = [super initWithFrame:frame];
  440. [self setupView];
  441. return self;
  442. }
  443. - (id)init {
  444. self = [super init];
  445. [self setupView];
  446. return self;
  447. }
  448. - (void)dealloc {
  449. [[NSNotificationCenter defaultCenter] removeObserver:self];
  450. }
  451. @end