September 26, 2011

Slide to Unlock Control in MonoTouch

Unable to display content. Adobe Flash is required.
In a recent MonoTouch project I needed to make something similar to the slide-to-unlock control at the bottom of the iOS lock screen.

The goal was to derive from a UIImageView and trigger an Activate event when a specified slider image got within range of the right end of the control. I found this was fairly straightforward to implement using a pan gesture. Below is the class I came up with and the associated images that were used:

Image


Slider
public class UISlideToActivateImageView : UIImageView
{
  #region Fields and Properties

  public event EventHandler<EventArgs> Activate;

  protected const float DEFAULT_ACTIVATION_RANGE = 20;

  public float ActivationRange { get; set; }

  public static Selector PanSelector
  {
    get
    {
      return new Selector("HandlePan");
    }
  }

  private UIImageView _sliderView = null;
  public UIImage Slider
  {
    get
    {
      return _sliderView.Image;
    }
    set
    {
      if (value != null)
      {
        if (_sliderView != null)
        {
          _sliderView.RemoveFromSuperview();
        }
        _sliderView = new UIImageView(
          new RectangleF(new PointF(0, 0), value.Size));
        _sliderView.Image = value;
        AddSubview(_sliderView);
      }
    }
  }

  protected PointF InitialLocation { get; set; }

  #endregion

  #region Constructors

  public UISlideToActivateImageView(PointF location, UIImage image)
    : base(image)
  {
    ActivationRange = DEFAULT_ACTIVATION_RANGE;
    Frame = new RectangleF(location, image.Size);
    RegisterPanGesture();
  }

  public UISlideToActivateImageView(PointF location, UIImage image,
    UIImage slider) : base(image)
  {
    ActivationRange = DEFAULT_ACTIVATION_RANGE;
    Frame = new RectangleF(location, image.Size);
    Slider = slider;
    RegisterPanGesture();
  }

  #endregion

  #region Events, Overrides and Delegates
        
  [Export("HandlePan")]
  public void HandlePan(UIPanGestureRecognizer panGesture)
  {
    const double EndedAnimationDuration = 0.2d;
    PointF newLocation;
    float adjX;
    if (panGesture != null)
    {
      newLocation = panGesture.LocationInView(this);
      switch (panGesture.State)
      {
        case UIGestureRecognizerState.Began:
          //User first taps the slider
          if ((newLocation.X <= (Frame.X + Slider.Size.Width))
            && (newLocation.X >= 0))
          {
            InitialLocation = newLocation;
          }
          break;
        case UIGestureRecognizerState.Changed:
          //Moved their finger - make slider follow horizontal movements
          adjX = Frame.X + (newLocation.X - InitialLocation.X);
          if ((InitialLocation != PointF.Empty) && (adjX >= 0)
            && (adjX <= (Frame.Width - Slider.Size.Width)))
          {
            UIView.Animate(0d, delegate() {
              _sliderView.Frame = new RectangleF(new PointF(adjX, 0),
                                             _sliderView.Frame.Size);
            });
            //If the Slider comes within ActivationRange of end of this
            //control, fire the Activate event
            if ((Activate != null)
              && (adjX >= 
                (Frame.Width - Slider.Size.Width - ActivationRange)))
            {
              //Moved the slider all the way across the image view
              Activate(this, EventArgs.Empty);
            }
          }
          break;
        case UIGestureRecognizerState.Cancelled:
        case UIGestureRecognizerState.Failed:
        case UIGestureRecognizerState.Ended:
          //Lifted up finger - return slider to original position
          InitialLocation = PointF.Empty;
          UIView.Animate(EndedAnimationDuration, delegate() {
            _sliderView.Frame = new RectangleF(new PointF(0, 0), 
                                             _sliderView.Frame.Size);
          });
          break;
      }
    }
  }

  //Delegate for allowing the pan gesture recognizer to receive touch.
  public class ReceiveTouchGestureRecognizerDelegate
    : UIGestureRecognizerDelegate
  {
    public override bool ShouldReceiveTouch (
      UIGestureRecognizer recognizer,
      UITouch touch)
    {
      return true;
    }
  }

  #endregion

  #region Helper Methods

  protected void RegisterPanGesture()
  {
    UserInteractionEnabled = true;
    UIPanGestureRecognizer pan = new UIPanGestureRecognizer();
    pan.AddTarget(this, PanSelector);
    pan.Delegate = new ReceiveTouchGestureRecognizerDelegate();
    AddGestureRecognizer(pan);
  }

  #endregion
}
To use this control it's simply a matter of assigning images (Image and Slider properties) and adding the class as a sub-view:
UISlideToActivateImageView slideToActivate = 
  new UISlideToActivateImageView(new PointF(31, 214), 
    UIImage.FromFile("slidetoactivate.png"), UIImage.FromFile("slider.png"));
slideToActivate.Activate += delegate(object sender, EventArgs e) {
  UIAlertView alert = new UIAlertView("Congratulations!", 
    "You've engaged the UISlideToActivateImageView!", null, "Okay");
  alert.Show();
};			
View.AddSubview(slideToActivate);

No comments: