Archive

Archive for March 28, 2016

Dealing with swipe gestures

March 28, 2016 5 comments

Someone asked me earlier about handling swipe gestures under Smart Pascal, so I thought I could deal with that in this post. I’m on sickleave with a bad back so I’m scribling this on my iPad (so heads up regarding typos). Cant say I’m to impressed with the Bluetooth keyboard, but it kinda works.

Describe a swipe

As luck would have it I already wrote a swipe controller way back in 2014, so I’ll be posting that here. But before we get into the code , let’s take a few seconds to think about what a swipe really is, because it’s not as simple as you may think.

Just stop and think about it: what is the difference between a normal touch-move and a swipe? Same thing right? Well yes and no (or kinda). What it all boils down to is time (!). So the difference between a normal touch-move operation and  a swipe, is that a swipe happens very quickly.

Latency and ranges

The time it takes from your finger touches the display until your finger leaves, is the latency of the swipe (see code below). Depending on the device this latency can vary. On an iPad you can have a latency as low as 10, while on iPhones you generally end up with 35 or 40. Anything lower and the swipe wont register.

The Microsoft Phone header, fully animated, swipe, mouse and keyboard controlled. Works great on iOS and Android as well

The Microsoft Phone swipe header

Next there are ranges. A swipe involves your finger moving quickly in some direction, and the controller has some default ranges for measuring these things. These ranges are simple minimum and maximum values for the distance. If you have hardly moved your finger then it’s not really a swipe; and if you move your finger slowly over a long distance, that’s not a swipe either (well, it can be. Just adjust the latency factor to suit your needs). At least not for components designed to be flicked back and forth.

The swipe controller class was actually written for the Microsoft Mobile component package (will be included with Smart Mobile Studio in the future), which implements the most popular MS controls used on Windows 10 touch devices. Fully GPU powered, animated, skinned and working brilliantly.

They also look good under iOS. Everyone is skinning their app’s these days, so mixing and matching between platforms is a great benefit.

Controller vs. inheritance

A controller class does not need to be inherited from. Instead, you create an instance and attach it to an already existing visual control. This is pretty cool because it doesn’t mess with your event handlers, it just extends your control over the existing behavior. Controllers are handy in situations where inheritance will be tricky, require to much refactoring or where the source is off-limits.

Using the controller is super simple:

  FSwiper := TSwipeController.Create(nil);
  FSwiper.Attach(self);
  FSwiper.OnSwipe := procedure (sender:TObject;const Direction:TSwipeControllerDirection)
    begin
       showmessage("Swipe detected @ " + ord(direction).toString);
    end;

To detach the controller just call the Detach() method, or free the object. It will dispose of everything automatically. Note: If you pass along the control in the constructor, it attaches automatically.

The code

Well here is the code. Enjoy! Also remember that you can check this out from GitHub.

unit swipecontroller;

interface

uses
  system.types,
  system.dateutils,
  SmartCL.System,
  SmartCL.Components;

type

  TSwipeControllerDirection = (
    sdNone=0,
    sdLeft,
    sdRight,
    sdDown,
    sdUp
    );

  TSwipeControllerEvent = Procedure (sender:TObject;
    const Direction:TSwipeControllerDirection);

  TSwipeControllerInfo = class
  public
    begins: TDateTime;
    sX: Integer;
    sY: Integer;
    eX: Integer;
    eY: Integer;
  end;

  TSwipeRange = class
  private
    FMin: Integer;
    FMax: Integer;
  protected
    procedure SetMin(Value:Integer);virtual;
    procedure SetMax(Value:Integer);virtual;
  public
    property  Minimum:Integer read FMin write SetMin;
    property  Maximum:Integer read FMax write SetMax;
    constructor Create(AMin,AMax:Integer);
  end;

  TSwipeController = class(TObject)
  private
    FAttached:  Boolean;
    FControl:   TW3TagObj;
    FHRange:    TSwipeRange;
    FVRange:    TSwipeRange;
    FInfo:      TSwipeControllerInfo;
    FDirection: TSwipeControllerDirection;
  protected
    FTouchHandleStart:  THandle;
    FTouchHandleMove:   THandle;
    FTouchHandleUp:     THandle;
    procedure SetupGestures;
    procedure RemoveGestures;
  public
    Property    Latency:Integer;
    property    HRange:TSwipeRange read FHRange;
    property    VRange:TSwipeRange read FVRange;
    Property    Owner:TW3TagObj read FControl;
    property    Attached:Boolean read FAttached;
    Property    OnSwipe:TSwipeControllerEvent;

    procedure   Attach(const AOwner:TW3TagObj);
    procedure   Detach;
    constructor Create(const AOwner:TW3TagObj);virtual;
    destructor  Destroy;Override;
  end;

implementation

//############################################################################
// TSwipeRange
//############################################################################

constructor TSwipeRange.Create(AMin,AMax:Integer);
begin
  inherited Create;
  FMin:=AMin;
  FMax:=AMax;
end;

procedure TSwipeRange.SetMin(Value:Integer);
begin
  FMin := TInteger.EnsureRange(Value,10,1000);
end;

procedure TSwipeRange.SetMax(Value:Integer);
begin
  FMax := TInteger.EnsureRange(Value,10,1000);
end;

//############################################################################
// TSwipeController
//############################################################################

constructor TSwipeController.Create(const AOwner:TW3TagObj);
begin
  inherited Create;
  FHRange:=TSwipeRange.Create(20,40); // [min]---X---[max]
  FVRange:=TSwipeRange.Create(20,40); // [min]---Y---[max]
  FInfo := TSwipeControllerInfo.Create;
  Latency := 35;

  if assigned(AOwner) then
    Attach(Aowner);
end;

destructor TSwipeController.Destroy;
begin
  if FAttached then
  Detach;
  FHRange.free;
  FVRange.free;
  FInfo.free;
  inherited;
end;

procedure TSwipeController.Attach(const AOwner:TW3TagObj);
begin
  if FAttached then
  Detach;

  if AOwner<>NIl then
  begin
    FControl := AOwner;
    FAttached := true;
    FControl.Handle.readyExecute( procedure ()
      begin
        SetupGestures;
      end);
  end;
end;

procedure TSwipeController.Detach;
begin
  if FAttached then
  begin
    try
      RemoveGestures;
    finally
      FAttached := false;
    end;
  end;
end;

procedure TSwipeController.RemoveGestures;
begin
  FControl.Handle.removeEventListener(FTouchHandleStart);
  FControl.Handle.removeEventListener(FTouchHandleMove);
  FControl.Handle.removeEventListener(FTouchHandleUp);
  FTouchHandleStart := unassigned;
  FTouchHandleMove := unassigned;
  FTouchHandleUp := unassigned;
end;

procedure TSwipeController.SetupGestures;
begin
  FTouchHandleStart:=FControl.Handle.addEventListener
    ('touchstart', procedure (e:variant)
    begin
      e.preventDefault;
      var t := e.touches[0];
      FInfo.sx:=t.screenX;
      FInfo.sy:=t.screenY;
    end);

  FTouchHandleMove:=FControl.Handle.addEventListener
    ('touchmove', procedure (e:variant)
    begin
      e.preventDefault;
      if (e.touches) then
      Begin
        if e.touches.length>0 then
        begin
          var t := e.touches[0];
          FInfo.eX:=t.screenX;
          FInfo.eY:=t.screenY;
          FInfo.begins:=now;
        end;
      end;
    end);

  FTouchHandleUp:=FControl.Handle.addEventListener
    ('touchend', procedure (e:variant)
    var
      mTicks: Integer;
    begin
      e.preventDefault;
      FDirection:=sdNone;

      (* How many Ms since touch and release? *)
      mTicks:=MillisecondsBetween(FInfo.begins,now);

      if (mTicks <= Latency) then       begin         if (FInfo.ex - FHRange.Minimum > FInfo.sx)
        or (FInfo.ex + FHRange.Minimum < FInfo.sx) then
        begin
          if (FInfo.ey < (FInfo.sy + FVRange.Maximum))           and (FInfo.sy > (FInfo.ey - FVRange.Maximum)) then
          begin
            if (FInfo.ex >  FInfo.sx) then
            FDirection:=sdRight else
            FDirection:=sdLeft;
          end;
        end;

        if ((FInfo.ey - FVRange.Minimum) > FInfo.sy)
        or ((FInfo.ey + FVRange.Minimum) < FInfo.sY) then
        begin
          if  (FInfo.ex < (FInfo.sx + FHRange.Maximum))           and (FInfo.sx > (FInfo.ex - FHRange.Maximum)) then
          begin
            if FInfo.ey > FInfo.sY then
            FDirection :=sdDown else
            FDirection :=sdUp;
          end;
        end;

        if assigned(OnSwipe) then
        begin
          if FDirection<>sdNone then
          OnSwipe(self,FDirection);
        end;
      end;
    end);
end;

end.