LOTAnimatedSwitch.m 6.3 KB
//
//  LOTAnimatedSwitch.m
//  Lottie
//
//  Created by brandon_withrow on 8/25/17.
//  Copyright © 2017 Airbnb. All rights reserved.
//

#import "LOTAnimatedSwitch.h"
#import "LOTAnimationView.h"
#import "CGGeometry+LOTAdditions.h"

@implementation LOTAnimatedSwitch {
  CGFloat _onStartProgress;
  CGFloat _onEndProgress;
  CGFloat _offStartProgress;
  CGFloat _offEndProgress;
  CGPoint _touchTrackingStart;
  BOOL _on;
  BOOL _suppressToggle;
  BOOL _toggleToState;
}

/// Convenience method to initialize a control from the Main Bundle by name
+ (instancetype _Nonnull)switchNamed:(NSString * _Nonnull)toggleName {
  return [self switchNamed:toggleName inBundle:[NSBundle mainBundle]];
}

/// Convenience method to initialize a control from the specified bundle by name
+ (instancetype _Nonnull)switchNamed:(NSString * _Nonnull)toggleName inBundle:(NSBundle * _Nonnull)bundle {
  LOTComposition *composition = [LOTComposition animationNamed:toggleName inBundle:bundle];
  LOTAnimatedSwitch *animatedControl = [[self alloc] initWithFrame:CGRectZero];
  if (composition) {
    [animatedControl setAnimationComp:composition];
    animatedControl.bounds = composition.compBounds;
  }
  return animatedControl;
}

- (instancetype)initWithFrame:(CGRect)frame {
  self = [super initWithFrame:frame];
  if (self) {
    self.accessibilityHint = NSLocalizedString(@"Double tap to toggle setting.", @"Double tap to toggle setting.");
    _onStartProgress = 0;
    _onEndProgress = 1;
    _offStartProgress = 1;
    _offEndProgress = 0;
    _on = NO;
    [self addTarget:self action:@selector(_toggle) forControlEvents:UIControlEventTouchUpInside];
  }
  return self;
}

- (void)setAnimationComp:(LOTComposition *)animationComp {
  [super setAnimationComp:animationComp];
  [self setOn:_on animated:NO];
}

#pragma mark - External Methods

- (void)setProgressRangeForOnState:(CGFloat)fromProgress toProgress:(CGFloat)toProgress {
  _onStartProgress = fromProgress;
  _onEndProgress = toProgress;
  [self setOn:_on animated:NO];
}

- (void)setProgressRangeForOffState:(CGFloat)fromProgress toProgress:(CGFloat)toProgress {
  _offStartProgress = fromProgress;
  _offEndProgress = toProgress;
  [self setOn:_on animated:NO];
}

- (void)setOn:(BOOL)on {
  [self setOn:on animated:NO];
}

- (void)setOn:(BOOL)on animated:(BOOL)animated {
  _on = on;
  
  CGFloat startProgress = on ? _onStartProgress : _offStartProgress;
  CGFloat endProgress = on ? _onEndProgress : _offEndProgress;
  CGFloat finalProgress = endProgress;
  if (self.animationView.animationProgress < MIN(startProgress, endProgress) ||
      self.animationView.animationProgress > MAX(startProgress, endProgress)) {
    if (self.animationView.animationProgress != (!_on ? _onEndProgress : _offEndProgress)) {
      // Current progress is in the wrong timeline. Switch.
      endProgress = on ? _offStartProgress : _onStartProgress;
      startProgress = on ? _offEndProgress : _onEndProgress;
    }
  }
  
  if (finalProgress == self.animationView.animationProgress) {
    return;
  }
  
  if (animated) {
    [self.animationView pause];
    [self.animationView playFromProgress:startProgress toProgress:endProgress withCompletion:^(BOOL animationFinished) {
      if (animationFinished) {
        self.animationView.animationProgress = finalProgress;
      }
    }];
  } else {
    self.animationView.animationProgress = endProgress;
  }
}

- (NSString *)accessibilityValue {
  return self.isOn ? NSLocalizedString(@"On", @"On")  : NSLocalizedString(@"Off", @"Off");
}

#pragma mark - Internal Methods

- (void)_toggle {
  if (!_suppressToggle) {
    [self _toggleAndSendActions];
  }
}

- (void)_toggleAndSendActions {
  if (self.isEnabled) {
    #ifndef TARGET_OS_TV
    if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 10.0) {
      UIImpactFeedbackGenerator *generator = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleLight];
      [generator impactOccurred];
    }
    #endif
    [self setOn:!_on animated:YES];
    [self sendActionsForControlEvents:UIControlEventValueChanged];
  }
}

- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
  [super beginTrackingWithTouch:touch withEvent:event];
  _suppressToggle = NO;
  _touchTrackingStart = [touch locationInView:self];
  return YES;
}

- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
  BOOL superContinue = [super continueTrackingWithTouch:touch withEvent:event];
  if (!_interactiveGesture) {
    return superContinue;
  }
  CGPoint location = [touch locationInView:self];
  CGFloat diff = location.x - _touchTrackingStart.x;
  if (LOT_PointDistanceFromPoint(_touchTrackingStart, location) > self.bounds.size.width * 0.25) {
    // The touch has moved enough to register as its own gesture. Suppress the touch up toggle.
    _suppressToggle = YES;
  }
#ifdef __IPHONE_11_0
  // Xcode 9+
  if (@available(iOS 9.0, *)) {
#else
    // Xcode 8-
    if ([UIView respondsToSelector:@selector(userInterfaceLayoutDirectionForSemanticContentAttribute:)]) {
#endif
      if ([UIView userInterfaceLayoutDirectionForSemanticContentAttribute:self.semanticContentAttribute] == UIUserInterfaceLayoutDirectionRightToLeft) {
          diff = diff * -1;
      }
  }
  if (_on) {
    diff = diff * -1;
    if (diff <= 0) {
      self.animationView.animationProgress = _onEndProgress;
      _toggleToState = YES;
    } else {
      diff = MAX(MIN(self.bounds.size.width, diff), 0);
      self.animationView.animationProgress = LOT_RemapValue(diff, 0, self.bounds.size.width, _offStartProgress, _offEndProgress);
      _toggleToState = (diff / self.bounds.size.width) > 0.5 ? NO : YES;
    }
  } else {
    if (diff <= 0) {
      self.animationView.animationProgress = _offEndProgress;
      _toggleToState = NO;
    } else {
      diff = MAX(MIN(self.bounds.size.width, diff), 0);
      self.animationView.animationProgress = LOT_RemapValue(diff, 0, self.bounds.size.width, _onStartProgress, _onEndProgress);
      _toggleToState = (diff / self.bounds.size.width) > 0.5 ? YES : NO;
    }
  }
  return YES;
}

- (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
  [super endTrackingWithTouch:touch withEvent:event];
  if (!_interactiveGesture) {
    return;
  }
  if (_suppressToggle) {
    if (_toggleToState != _on) {
      [self _toggleAndSendActions];
    } else {
      [self setOn:_toggleToState animated:YES];
    }
  }
}

@end