Archive

Archive for March, 2016

TTween and css bindings

March 29, 2016 6 comments
Tweens like clockwork

Tweens like clockwork

One thing that’s been missing from Smart’s TTween library is bindings. Well, fret ye no more because the next time you update Smart Mobile Studio you will get two shiny new units. Actually they are old units, they have been on Github for ages – but they will now be included with the RTL.

  • SmartCL.Tween.pas
  • SmartCL.Tween.Effects.pas

Bindings? What’s that?

If you follow the latest JavaScript developments you will know that – most modern tweening libraries can work directly with CSS styles. Meaning, that when you execute a tween – instead of you getting the values back through an event, the binding actually writes the data directly to the CSS style. It also uses translation objects to make sure values are written correctly depending on the datatype.

Well, this is now a part of Smart Mobile Studio’s RTL as well. So now you can do sexy stuff like:

procedure TForm1.W3Button1Click(Sender: TObject);
begin
  // Create a widget tween effect object
  // and target a panel we have on the form
  var effect:=TWidgetEffect.Create(w3panel1);

  // Set some default values. These are copied into each
  // binding, but you can overwrite them (see below)
  // Also note that bindings read from the CSS on start (!)
  effect.Distance:=100;
  effect.Duration:=200;
  effect.Easing := ttQuartInOut;

  // Use requestAnimationFrame API
  effect.TweenEngine.SyncRefresh := true;

  // Bind the "left" css property using a pixel translator
  var LLeftBinding := effect.Bind('left',TCSSBindingType.sbPixels);
  LLeftBinding.Tween.Distance := 100;
  LLeftBinding.Tween.Easing := ttCubeInOut;
  LLeftBinding.Tween.Duration := 600;
  LLeftBinding.Tween.Behavior:=TTweenBehavior.tbOscillate;

  // bind the "top" css property as well
  var LTopBinding := effect.Bind('top',TCSSBindingType.sbPixels);
  LTopBinding.Tween.Distance := 50;
  LTopBinding.Tween.Easing := ttSineInOut;
  LTopBinding.Tween.Duration := 400;
  LTopBinding.Tween.Behavior:=TTweenBehavior.tbOscillate;

  // and execute both at once
  effect.Execute( procedure (sender:TObject)
    begin
      // All done? Destroy the effect instance in 200ms
      TW3Dispatch.DelayedDispatch( procedure ()
        begin
          effect.free;
          effect := nil;
        end, 200);
    end);
end;

This is really super cool and opens up for some awesome effects!
Performing silky smooth rotation is now a matter of just binding to a single CSS property and tweening from 0 to 365.

Color tweening is also fun 🙂

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.