123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558 |
- //
- // KBSlider.m
- // KBSlider
- //
- // Created by Kevin Bradley on 12/25/20.
- // Copyright © 2020 nito. All rights reserved.
- //
- #import "KBSlider.h"
- #import <GameController/GameController.h>
- @interface KBSlider() {
- CGFloat _minimumValue;
- CGFloat _maximumValue;
- UIColor *_maximumTrackTintColor;
- UIColor *_minimumTrackTintColor;
- UIColor *_thumbTintColor;
- CGFloat _focusScaleFactor;
- BOOL _continuous;
-
- BOOL _isEnabled;
- BOOL _isSelected;
- BOOL _isHighlighted;
- }
- @property CGFloat trackViewHeight;
- @property CGFloat thumbSize;
- @property NSTimeInterval animationDuration;
- @property CGFloat defaultValue;
- @property CGFloat defaultMinimumValue;
- @property CGFloat defaultMaximumValue;
- @property BOOL defaultIsContinuous;
- @property UIColor *defaultThumbTintColor;
- @property UIColor *defaultTrackColor;
- @property UIColor *defaultMininumTrackTintColor;
- @property CGFloat defaultFocusScaleFactor;
- @property CGFloat defaultStepValue;
- @property CGFloat decelerationRate;
- @property CGFloat decelerationMaxVelocity;
- @property CGFloat fineTunningVelocityThreshold;
- @property NSMutableDictionary *thumbViewImages; //[UInt: UIImage] - not an allowed dict type in obj-c
- @property UIImageView *thumbView;
- @property NSMutableDictionary *trackViewImages; //[UInt: UIImage] - not an allowed dict type in obj-c
- @property UIImageView *trackView;
- @property NSMutableDictionary *minimumTrackViewImages; //[UInt: UIImage] - not an allowed dict type in obj-c
- @property UIImageView *minimumTrackView;
- @property NSMutableDictionary *maximumTrackViewImages; //[UInt: UIImage] - not an allowed dict type in obj-c
- @property UIImageView *maximumTrackView;
- @property UIPanGestureRecognizer *panGestureRecognizer;
- @property UITapGestureRecognizer *leftTapGestureRecognizer;
- @property UITapGestureRecognizer *rightTapGestureRecognizer;
- @property NSLayoutConstraint *thumbViewCenterXConstraint;
- @property DPadState dPadState; //.select
- @property NSTimer *deceleratingTimer;
- @property CGFloat deceleratingVelocity;
- @property CGFloat thumbViewCenterXConstraintConstant;
- @end
- @implementation KBSlider
- - (void)initializeDefaults {
- _trackViewHeight = 5;
- _thumbSize = 30;
- _animationDuration = 0.3;
- _defaultValue = 0;
- _defaultMinimumValue = 0;
- _defaultMaximumValue = 1;
- _defaultIsContinuous = true;
- _defaultThumbTintColor = [UIColor whiteColor];
- _defaultTrackColor = [UIColor grayColor];
- _defaultMininumTrackTintColor = [UIColor blueColor];
- _defaultFocusScaleFactor = 1.05;
- _defaultStepValue = 0.1;
- _decelerationRate = 0.92;
- _decelerationMaxVelocity = 1000;
- _fineTunningVelocityThreshold = 600;
-
- _storedValue = _defaultValue;
- _dPadState = DPadStateSelect;
- _continuous = _defaultIsContinuous;
-
- _minimumTrackViewImages = [NSMutableDictionary new];
- _maximumTrackViewImages = [NSMutableDictionary new];
- _trackViewImages = [NSMutableDictionary new];
- _thumbViewImages = [NSMutableDictionary new];
-
- _thumbTintColor = _defaultThumbTintColor;
- _minimumTrackTintColor = _defaultMininumTrackTintColor;
- _focusScaleFactor = _defaultFocusScaleFactor;
- _minimumValue = _defaultMinimumValue;
- _maximumValue = _defaultMaximumValue;
- _stepValue = _defaultStepValue;
- [self setEnabled:true];
-
- }
- - (BOOL)isContinuous {
- return _continuous;
- }
- - (void)setContinuous:(BOOL)continuous {
- _continuous = continuous;
- }
- - (void)setSelected:(BOOL)selected {
- _isSelected = selected;
- [self updateStateDependantViews];
- }
- - (BOOL)isSelected {
- return _isSelected;
- }
- - (void)setHighlighted:(BOOL)highlighted {
- _isHighlighted = highlighted;
- [self updateStateDependantViews];
- }
- - (BOOL)isHighlighted {
- return _isHighlighted;
- }
- - (void)setEnabled:(BOOL)enabled {
- _isEnabled = enabled;
- _panGestureRecognizer.enabled = enabled;
- [self updateStateDependantViews];
- }
- - (BOOL)isEnabled {
- return _isEnabled;
- }
- - (CGFloat)value {
- return _storedValue;
- }
- - (void)setValue:(CGFloat)value afterDelay:(NSInteger)delay {
- dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
- [self setValue:value];
- });
- }
- - (void)setValue:(CGFloat)newValue {
- _storedValue = MIN(_maximumValue, newValue);
- _storedValue = MAX(_minimumValue, _storedValue);
- if (_trackView.bounds.size.width == 0){
- [self setValue:newValue afterDelay:0.1];
- }
- CGFloat offset = _trackView.bounds.size.width * (_storedValue - _minimumValue) / (_maximumValue - _minimumValue);
- offset = MIN(_trackView.bounds.size.width, offset);
- if(isnan(offset)){
- return;
- }
- _thumbViewCenterXConstraint.constant = offset;
- }
- - (CGFloat)maximumValue {
- return _maximumValue;
- }
- - (void)setMaximumValue:(CGFloat)maximumValue {
- _maximumValue = maximumValue;
- [self setValue:MIN(self.value, maximumValue)];
- }
- - (CGFloat)minimumValue {
- return _minimumValue;
- }
- - (void)setMinimumValue:(CGFloat)minimumValue {
- _minimumValue = minimumValue;
- [self setValue:MAX(self.value, minimumValue)];
- }
- - (UIColor *)maximumTrackTintColor {
- return _maximumTrackTintColor;
- }
- - (void)setMaximumTrackTintColor:(UIColor *)maximumTrackTintColor {
- _maximumTrackTintColor = maximumTrackTintColor;
- _maximumTrackView.backgroundColor = maximumTrackTintColor;
- }
- - (void)setMinimumTrackTintColor:(UIColor *)minimumTrackTintColor {
- _minimumTrackTintColor = minimumTrackTintColor;
- _minimumTrackView.backgroundColor = minimumTrackTintColor;
- }
- - (UIColor *)thumbTintColor {
- return _thumbTintColor;
- }
- - (void)setThumbTintColor:(UIColor *)thumbTintColor {
- _thumbTintColor = thumbTintColor;
- _thumbView.backgroundColor = thumbTintColor;
- }
- - (UIColor *)minimumTrackTintColor {
- return _minimumTrackTintColor;
- }
- - (CGFloat)focusScaleFactor {
- return _focusScaleFactor;
- }
- - (void)setFocusScaleFactor:(CGFloat)focusScaleFactor {
- _focusScaleFactor = focusScaleFactor;
- [self updateStateDependantViews];
- }
- - (void)setupView {
-
- [self initializeDefaults];
- [self setUpTrackView];
- [self setUpMinimumTrackView];
- [self setUpMaximumTrackView];
- [self setUpThumbView];
-
- [self setUpTrackViewConstraints];
- [self setUpMinimumTrackViewConstraints];
- [self setUpMaximumTrackViewConstraints];
- [self setUpThumbViewConstraints];
-
- [self setUpGestures];
-
- [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(controllerConnected:) name:GCControllerDidConnectNotification object:nil];
- [self updateStateDependantViews];
- }
- - (void)setValue:(CGFloat)value animated:(BOOL)animated {
- [self setValue:value];
- [self stopDeceleratingTimer];
- if (animated){
- [UIView animateWithDuration:self.animationDuration animations:^{
- [self setNeedsLayout];
- [self layoutIfNeeded];
- }];
- }
- }
- - (void)setMinimumTrackImage:(UIImage *)image forState:(UIControlState)state {
- _minimumTrackViewImages[[NSNumber numberWithUnsignedInteger:state]] = image;
- [self updateStateDependantViews];
- }
- - (void)setMaximumTrackImage:(UIImage *)image forState:(UIControlState)state {
- _maximumTrackViewImages[[NSNumber numberWithUnsignedInteger:state]] = image;
- [self updateStateDependantViews];
- }
- - (void)setThumbImage:(UIImage *)image forState:(UIControlState)state {
- _thumbViewImages[[NSNumber numberWithUnsignedInteger:state]] = image;
- [self updateStateDependantViews];
- }
- - (UIImage *)currentThumbImage {
- return _thumbView.image;
- }
- - (UIImage *)minimumTrackImageForState:(UIControlState)state {
- NSNumber *key = [NSNumber numberWithUnsignedInteger:state];
- return _minimumTrackViewImages[key];
-
- }
- - (UIImage *)maximumTrackImageForState:(UIControlState)state {
- NSNumber *key = [NSNumber numberWithUnsignedInteger:state];
- return _maximumTrackViewImages[key];
- }
- - (UIImage *)thumbImageForState:(UIControlState)state {
- NSNumber *key = [NSNumber numberWithUnsignedInteger:state];
- return _thumbViewImages[key];
- }
- - (void)setUpThumbView {
- _thumbView = [UIImageView new];
- _thumbView.layer.cornerRadius = _thumbSize/2;
- _thumbView.backgroundColor = _thumbTintColor;
- [self addSubview:_thumbView];
- }
- - (void)setUpTrackView {
- _trackView = [UIImageView new];
- _trackView.layer.cornerRadius = _trackViewHeight/2;
- _trackView.backgroundColor = _defaultTrackColor;
- [self addSubview:_trackView];
- }
- - (void)setUpMinimumTrackView {
- _minimumTrackView = [UIImageView new];
- _minimumTrackView.layer.cornerRadius = _trackViewHeight/2;
- _minimumTrackView.backgroundColor = _minimumTrackTintColor;
- [self addSubview:_minimumTrackView];
- }
- - (void)setUpMaximumTrackView {
- _maximumTrackView = [UIImageView new];
- _maximumTrackView.layer.cornerRadius = _trackViewHeight/2;
- _maximumTrackView.backgroundColor = _maximumTrackTintColor;
- [self addSubview:_maximumTrackView];
- }
- - (void)setUpTrackViewConstraints {
- _trackView.translatesAutoresizingMaskIntoConstraints = false;
- [_trackView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor].active = true;
- [_trackView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor].active = true;
- [_trackView.centerYAnchor constraintEqualToAnchor:self.centerYAnchor].active = true;
- [_trackView.heightAnchor constraintEqualToConstant:_trackViewHeight].active = true;
-
- }
- - (void)setUpMinimumTrackViewConstraints {
- _minimumTrackView.translatesAutoresizingMaskIntoConstraints = false;
- [_minimumTrackView.leadingAnchor constraintEqualToAnchor:_trackView.leadingAnchor].active = true;
- [_minimumTrackView.trailingAnchor constraintEqualToAnchor:_thumbView.centerXAnchor].active = true;
- [_minimumTrackView.centerYAnchor constraintEqualToAnchor:_trackView.centerYAnchor].active = true;
- [_minimumTrackView.heightAnchor constraintEqualToConstant:_trackViewHeight].active = true;
-
- }
- - (void)setUpMaximumTrackViewConstraints {
- _maximumTrackView.translatesAutoresizingMaskIntoConstraints = false;
- [_maximumTrackView.leadingAnchor constraintEqualToAnchor:_thumbView.centerXAnchor].active = true;
- [_maximumTrackView.trailingAnchor constraintEqualToAnchor:_trackView.trailingAnchor].active = true;
- [_maximumTrackView.centerYAnchor constraintEqualToAnchor:_trackView.centerYAnchor].active = true;
- [_maximumTrackView.heightAnchor constraintEqualToConstant:_trackViewHeight].active = true;
-
- }
- - (void)setUpThumbViewConstraints {
- _thumbView.translatesAutoresizingMaskIntoConstraints = false;
- [_thumbView.centerYAnchor constraintEqualToAnchor:self.centerYAnchor].active = true;
- [_thumbView.heightAnchor constraintEqualToConstant:_thumbSize].active = true;
- [_thumbView.widthAnchor constraintEqualToConstant:_thumbSize].active = true;
- _thumbViewCenterXConstraint = [_thumbView.centerXAnchor constraintEqualToAnchor:_trackView.leadingAnchor constant:self.value];
- _thumbViewCenterXConstraint.active = true;
- }
- - (void)setUpGestures {
-
- _panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panGestureWasTriggered:)];
- [self addGestureRecognizer:_panGestureRecognizer];
-
- _leftTapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(leftTapWasTriggered)];
- _leftTapGestureRecognizer.allowedPressTypes = @[@(UIPressTypeLeftArrow)];
- _leftTapGestureRecognizer.allowedTouchTypes = @[@(UITouchTypeIndirect)];
- [self addGestureRecognizer:_leftTapGestureRecognizer];
-
- _rightTapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(rightTapWasTriggered)];
- _rightTapGestureRecognizer.allowedPressTypes = @[@(UIPressTypeRightArrow)];
- _rightTapGestureRecognizer.allowedTouchTypes = @[@(UITouchTypeIndirect)];
- [self addGestureRecognizer:_rightTapGestureRecognizer];
- }
- - (void)updateStateDependantViews {
-
- UIImage *currentMinImage = _minimumTrackViewImages[[NSNumber numberWithUnsignedInteger:self.state]];
- if (currentMinImage){
- _minimumTrackView.image = currentMinImage;
- } else {
- _minimumTrackView.image = _minimumTrackViewImages[[NSNumber numberWithUnsignedInteger:UIControlStateNormal]];
- }
- UIImage *currentMaxImage = _maximumTrackViewImages[[NSNumber numberWithUnsignedInteger:self.state]];
- if (currentMaxImage){
- _maximumTrackView.image = currentMaxImage;
- } else {
- _maximumTrackView.image = _maximumTrackViewImages[[NSNumber numberWithUnsignedInteger:UIControlStateNormal]];
- }
- UIImage *currentThumbImage = _thumbViewImages[[NSNumber numberWithUnsignedInteger:self.state]];
- if (currentThumbImage){
- _thumbView.image = currentThumbImage;
- } else {
- _thumbView.image = _thumbViewImages[[NSNumber numberWithUnsignedInteger:UIControlStateNormal]];
- }
-
- if ([self isFocused]){
- self.transform = CGAffineTransformMakeScale(_focusScaleFactor, _focusScaleFactor);
- } else {
- self.transform = CGAffineTransformIdentity;
- }
-
- }
- - (void)controllerConnected:(NSNotification *)n {
- GCController *controller = [n object];
- GCMicroGamepad *micro = [controller microGamepad];
- if (!micro)return;
-
- CGFloat threshold = 0.7;
- micro.reportsAbsoluteDpadValues = true;
- micro.dpad.valueChangedHandler = ^(GCControllerDirectionPad * _Nonnull dpad, float xValue, float yValue) {
- if (xValue < -threshold){
- self.dPadState = DPadStateLeft;
- } else if (xValue > threshold){
- self.dPadState = DPadStateRight;
- } else {
- self.dPadState = DPadStateSelect;
- }
- };
- }
- - (void)handleDeceleratingTimer:(NSTimer *)timer {
-
- CGFloat centerX = _thumbViewCenterXConstraintConstant + _deceleratingVelocity * 0.01;
- CGFloat percent = centerX / (_trackView.frame.size.width);
- CGFloat newValue = _minimumValue + ((_maximumValue - _minimumValue) * percent);
- [self setValue:newValue];
- if ([self isContinuous]){
- [self sendActionsForControlEvents:UIControlEventValueChanged];
- }
- _thumbViewCenterXConstraintConstant = _thumbViewCenterXConstraint.constant;
-
- _deceleratingVelocity *= _decelerationRate;
- if (![self isFocused] || fabs(_deceleratingVelocity) < 1){
- [self stopDeceleratingTimer];
- }
- }
- - (void)stopDeceleratingTimer {
- [_deceleratingTimer invalidate];
- _deceleratingTimer = nil;
- _deceleratingVelocity = 0;
- [self sendActionsForControlEvents:UIControlEventValueChanged];
- }
- - (BOOL)isVerticalGesture:(UIPanGestureRecognizer *)recognizer {
-
- CGPoint translation = [recognizer translationInView:self];
- if (fabs(translation.y) > fabs(translation.x)) {
- return true;
- }
- return false;
- }
- #pragma mark - Actions
- - (void)panGestureWasTriggered:(UIPanGestureRecognizer *)panGestureRecognizer {
-
- if ([self isVerticalGesture:panGestureRecognizer]){
- return;
- }
- CGFloat translation = [panGestureRecognizer translationInView:self].x;
- CGFloat velocity = [panGestureRecognizer velocityInView:self].x;
- switch(panGestureRecognizer.state){
- case UIGestureRecognizerStateBegan:
- [self stopDeceleratingTimer];
- _thumbViewCenterXConstraintConstant = _thumbViewCenterXConstraint.constant;
- break;
-
- case UIGestureRecognizerStateChanged:{
- CGFloat centerX = _thumbViewCenterXConstraintConstant + translation / 5;
- CGFloat percent = centerX / _trackView.frame.size.width;
- CGFloat newValue = _minimumValue + ((_maximumValue - _minimumValue) * percent);
- [self setValue:newValue];
- if ([self isContinuous]){
- [self sendActionsForControlEvents:UIControlEventValueChanged];
- }
- }
- break;
-
- case UIGestureRecognizerStateEnded:
- case UIGestureRecognizerStateCancelled:
- _thumbViewCenterXConstraintConstant = _thumbViewCenterXConstraint.constant;
- if (fabs(velocity) > _fineTunningVelocityThreshold){
- CGFloat direction = velocity > 0 ? 1 : -1;
- _deceleratingVelocity = fabs(velocity) > _decelerationMaxVelocity ? _decelerationMaxVelocity * direction : velocity;
- _deceleratingTimer = [NSTimer scheduledTimerWithTimeInterval:0.01 target:self selector:@selector(handleDeceleratingTimer:) userInfo:nil repeats:true];
- } else {
- [self stopDeceleratingTimer];
- }
- break;
-
- default:
- break;
-
- }
- }
- - (void)leftTapWasTriggered {
-
- CGFloat newValue = [self value]-_stepValue;
- [self setValue:newValue];
- }
- - (void)rightTapWasTriggered {
- CGFloat newValue = [self value]+_stepValue;
- [self setValue:newValue];
- }
- - (void)pressesBegan:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event {
- for (UIPress *press in presses){
- switch (press.type) {
- case UIPressTypeSelect:
- if(_dPadState == DPadStateLeft){
- _panGestureRecognizer.enabled = false;
- [self leftTapWasTriggered];
- } else if (_dPadState == DPadStateRight){
- _panGestureRecognizer.enabled = false;
- [self rightTapWasTriggered];
- } else {
- _panGestureRecognizer.enabled = false;
- }
- break;
- default:
- break;
- }
- }
- _panGestureRecognizer.enabled = true;
- [super pressesBegan:presses withEvent:event];
- }
- - (void)didUpdateFocusInContext:(UIFocusUpdateContext *)context withAnimationCoordinator:(UIFocusAnimationCoordinator *)coordinator {
- [coordinator addCoordinatedAnimations:^{
- [self updateStateDependantViews];
- } completion:nil];
- }
- #pragma mark - Initializers
- - (id)initWithCoder:(NSCoder *)coder {
- self = [super initWithCoder:coder];
- [self setupView];
- return self;
- }
- - (id)initWithFrame:(CGRect)frame {
- self = [super initWithFrame:frame];
- [self setupView];
- return self;
- }
- - (id)init {
- self = [super init];
- [self setupView];
- return self;
- }
- - (void)dealloc {
- [[NSNotificationCenter defaultCenter] removeObserver:self];
- }
- @end
|