Archive

Archive for June, 2016

Scroll controllers, ah, i love ’em!

June 18, 2016 2 comments

Just a quick follow up on the scrolling controllers. What I have done is essentially to rip out the code that performs scrolling, and isolated it in separate non-visual components. This allows you to pick the scrolling you want for a particular TW3ScrollControl. From momentum to vanilla, to fully fledged bouncing iScroll5!

Watch the video here!: https://www.youtube.com/watch?v=JcUCV-8WKiU

Plain, vanilla scrolling is reduced to:

unit SmartCL.Scroll.Plain;

interface

{.$DEFINE USE_LATENCY_CACHE}

uses
  System.Types,
  System.Types.Convert,
  System.Time,
  System.Events,
  SmartCl.Events,
  SmartCL.System,
  SmartCL.Components,
  SmartCL.Effects,
  SmartCL.Scroll;

type

  TW3PlainScrollOptions = set of (psHorizontal,psVertical);

  TW3PlainScrollController = class(TW3ScrollController)
  protected
    procedure MoveBegins(const XPos, YPos: integer); override;
    procedure MoveUpdate(const XPos, YPos: integer); override;
    procedure MoveEnds(const XPos, YPos: integer); override;
  protected
    procedure InitializeObject; override;
  published
    property  Options: TW3PlainScrollOptions;
  end;

implementation

//#############################################################################
// TW3PlainScrollController
//#############################################################################

procedure TW3PlainScrollController.InitializeObject;
begin
  inherited;
  Options := [psHorizontal, psVertical];
end;

procedure TW3PlainScrollController.MoveBegins(const XPos, YPos: integer);
begin
end;

procedure TW3PlainScrollController.MoveUpdate(const XPos, YPos: integer);

  function GetBottomMax: integer;
  begin
    result := ScrollContent.height - ScrollControl.ClientHeight;
  end;

var
  LVDiff: integer;
  dy: integer;
begin
  LVDiff := TInteger.Diff(YPos, Startpos.y);

  if (Ypos < Startpos.y) then
  begin
    (* Move downwards *)
    dy := ScrollContent.top - LVDiff;
    if dy < (-GetBottomMax) then       dy := -GetBottomMax;     ScrollContent.top := dy;   end else   begin     (* Move upwards *)     dy := ScrollContent.top + LVDiff;     if dy > 0 then
      dy := 0;
    ScrollContent.top :=dy;
  end;
end;

procedure TW3PlainScrollController.MoveEnds(const XPos, YPos: integer);
begin
end;

end.

And momentum is as simple as:

momentum

unit SmartCL.Scroll.Momentum;

interface

uses
  System.Types, System.Types.Convert, System.Time, System.Events,
  SmartCl.Events, SmartCL.System, SmartCL.Components, SmartCL.Effects,
  SmartCL.Scroll;

type

  TW3MomentumData = record
    mdStart: integer;
    mdOffset: integer;
    mdTarget: integer;
    mdTimestamp: integer;
    mdFrame: double;
    mdVelocity: double;
    mdAmplitude: double;
    mdTimeConstant: double;
  end;

  TW3MomentumScrollController = class(TW3ScrollController)
  private
    FVRange:  TW3Range;
    FHRange:  TW3Range;
    FVData:   TW3MomentumData;
    FTicker:  TW3DispatchHandle;
    procedure AutoScroll;
    procedure ScrollY(const NewTop: integer);
    procedure Track;
  protected
    procedure SessionBegins; override;
    procedure SessionEnds; override;
  protected
    procedure MoveBegins(const XPos, YPos: integer); override;
    procedure MoveUpdate(const XPos, YPos: integer); override;
    procedure MoveEnds(const XPos, YPos: integer); override;
  public
    procedure Attach(const ScrollControl: TW3ScrollControl); override;
  end;

implementation


//#############################################################################
// TW3MomentumScrollController
//#############################################################################

procedure TW3MomentumScrollController.Attach(const ScrollControl: TW3ScrollControl);
begin
  inherited Attach(ScrollControl);
  FVData.mdTimeConstant := 325;
end;

procedure TW3MomentumScrollController.Track;
var
  LNow: integer;
  Elapsed: integer;
  Delta: double;
begin
  LNow := TW3Dispatch.JsNow.now();
  Elapsed := LNow - FVData.mdTimestamp;
  FVData.mdTimestamp := LNow;
  Delta := FVData.mdOffset - FVData.mdFrame;
  FVData.mdFrame := FVData.mdTarget;
  FVData.mdVelocity := 0.8 * (1000 * Delta / (1 + Elapsed)) + 0.2 * FVData.mdVelocity;
end;

procedure TW3MomentumScrollController.AutoScroll;
var
  Elapsed: integer;
  Delta: double;
begin
  (* Scrolled passed end-of-document ? *)
  if (FVData.mdOffset >= FVRange.Maximum) then
  begin
    TW3Dispatch.ClearInterval(FTicker);
    FTicker := unassigned;
    ScrollY(ScrollContent.Height-ScrollControl.ClientHeight);
    exit;
  end;

  (* Scrolling breaches beginning of document? *)
  if (FVData.mdOffset < 0) then
  begin
    TW3Dispatch.ClearInterval(FTicker);
    FTicker := unassigned;
    ScrollY(0);
    exit;
  end;

  if (FVData.mdAmplitude <> 0) then
  begin
    Elapsed := TW3Dispatch.JsNow.now() - FVData.mdTimestamp;
    Delta := -FVData.mdAmplitude * Exp(-Elapsed / FVData.mdTimeConstant);
  end;

  if (delta > 5) or (delta < -5) then
  begin
    ScrollY(FVData.mdTarget + Delta);
    TW3Dispatch.RequestAnimationFrame(AutoScroll);
  end else
  begin
    ScrollY(FVData.mdTarget);
  end;
end;

procedure TW3MomentumScrollController.ScrollY(const NewTop: integer);
var
  LGPU: string;
begin
  if not (csDestroying in ScrollControl.ComponentState) then
  begin
    if (csReady in ScrollControl.ComponentState) then
    begin
      (* Use GPU scrolling to position the content *)
      FVData.mdOffset := FVRange.ClipTo(NewTop);
      LGPU := "translateY(" + (-FVData.mdOffset).ToString() + "px)";
      ScrollContent.Handle.style[BrowserAPI.Prefix("Transform")] := LGPU;
    end;
  end;
end;


procedure TW3MomentumScrollController.SessionBegins;
begin
  ScrollControl.UpdateContent;
end;

procedure TW3MomentumScrollController.SessionEnds;
begin
end;

procedure TW3MomentumScrollController.MoveBegins(const XPos, YPos: integer);
begin
  TW3Dispatch.ClearInterval(FTicker);

  FVRange := TW3Range.Create(0, ScrollContent.Height - ScrollControl.ClientHeight);
  FHRange := TW3Range.Create(0, ScrollContent.Width - ScrollControl.ClientWidth);

  FVData.mdStart := YPos;
  FVData.mdVelocity := 0;
  FVData.mdAmplitude := 0;
  FVData.mdFrame := FVData.mdOffset;// FOffset;
  FVData.mdTimeStamp := TW3Dispatch.JsNow.now();
  FTicker := TW3Dispatch.SetInterval(Track,100);
end;

procedure TW3MomentumScrollController.MoveUpdate(const XPos, YPos: integer);
var
  delta: integer;
begin
  delta := (FVData.mdStart - YPos);
  if (Delta > 2) or (Delta < -2) then
  begin
    FVData.mdStart := YPos;
    ScrollY(FVData.mdOffset + Delta);
  end;
end;

procedure TW3MomentumScrollController.MoveEnds(const XPos, YPos: integer);
begin
  TW3Dispatch.ClearInterval(FTicker);
  if (FVData.mdVelocity > 10) or (FVData.mdVelocity < -10) then
  begin
    FVData.mdAmplitude := 0.8 * FVData.mdVelocity;
    FVData.mdTarget := FVData.mdOffset + Round(FVData.mdAmplitude);
    FVData.mdTimestamp := TW3Dispatch.JsNow.Now();
    TW3Dispatch.RequestAnimationFrame(Autoscroll);
  end;
end;

end.

Neat, simple, easy to use and more important: easy to expand!

For those about to scroll, we salute you

June 10, 2016 1 comment

Came up with a better solution to the scrolling problem for Smart Mobile Studio. While I hate having to write code for Internet Explorer, it is nice to have a system that works everywhere. But that means more abstraction and “drivers” type classes.

The scrolling model

TW3Scrollbox and it’s variation(s) implement different types of scrolling. TW3Scrollbox has a very fast non-momentum scroll, making it perfect for displaying detailed information. But it would be nice if we could choose right?

The model I have come up with is super simple, especially in the upcoming RTL where we finally have non-visual components. I give you – TW3ScrollController

scroll_model

In the present model, TW3Scrollbox deals with scrolling directly. Actually the scrolling is implemented in the TW3ScrollContent control, mapping touch and mouse events directly. This turned out to be a remarkably fast way of moving things around. I really did not expect IE to keep up, but it’s perfectly pristine!

In the momentum-scroll example I posted, control of scrolling is handled by the container rather than content. This is very fast on webkit, and I also tried it on Microsoft mobile and Windows 10 mobile – and it’s very fast there. But for some reason the desktop Internet Explorer is slow and the content jitters a bit.

The culprit is not my code or approach, it’s actually something else. Internet Explorer and Edge are the only browsers that implements OnReSize() events. No other browser has this. In the SMS RTL we have to manually figure out when to call ReSize (based on the BeginUpdate, AddControlState, EndUpdate methods).

In the momentum scroller I use SetBounds() to keep the content within the horizontal bounds of the control. This causes an extra call to Resize every time the content moves even a pixel (even though it shouldnt, because the size doesnt change). So yeah, fixing that will make all the difference. I’m going to nail this thing once and for all, just like I did with font measurements way back.

TW3ScrollController

But isolating the code that actually deals with scrolling in separate, non visual components that you can attach to the TW3ScrollBox makes sense. Rather than hardcoding everything into a huge, spaghetti monster unit — we can now isolate different scroll methods in their own units (keeping those bytes down).
Normal per-pixel scrolling, momentum scrolling, CSS3 animation based scrolling, tween based (cpu) scrolling. It gives us some options – and allows you to implement your own variations if you find mine lacking.

I’m also giving the browser driver a much deserved overhaul. Getting the browser type and version info should be easy (and humanly readable). And since you may want to pick different scroll methods depending on the browser type — being able to check if your running on Edge for the desktop, or IE on a mobile device… well, it should be there. End of story.

iScroll

I really want iScroll to be the standard scroll library for Smart Mobile Studio, but since people feel it’s hard to use and adapt to — I may end up doing the unthinkable and re-write it in object pascal from scratch. But iScroll5 really is so much better. It has been developed and tested on a plethora of devices for six years now.

It even does things the built-in browser scrolling (for the browsers that allows this, yeah im looking at you Safari!) doesn’t deal with.

But I have enough on my plate right now, so iScroll porting will have to wait.

IE 10, 11 and Edge patch for Smart Mobile Studio

June 10, 2016 2 comments

Last week I posted a momentum scrolling base-control for Smart Mobile Studio. It was meant as an example only, written for the new RTL which wont hit the userbase just yet.

It was a bit unfair perhaps, but in all honesty – adapting it to the present RTL is child’s play, and one of the forum members did just that and got it working more or less instantly.

Sadly, it didnt work at all under Internet Explorer. While I am shocked that people actually use Internet Explorer in this day and age – not to mention Edge (or any of Microsoft browsers), I took a few minutes to check out what the fuzz was about.

Microsoft IE strikes again

ie-devil_03Turns out Microsoft has altered the vendor information quite radically the past 4 years. This also means that the Smart Mobile Studio startup code was unable to determine the browser above IE version 10 – and in such a case we always fall back on Mozilla.

But ofcourse, IE wont accept “moz” prefixed CSS3 instructions, so while the visual controls does indeed work – they produced no visual scroll feedback due to the wrong driver.

Patching the RTL to recognize IE, Trident and Edge builds

All you have to do is to replace a function in SmartCL.System.pas called w3_getIsInternetExplorer(). With the new code in place, your SMS applications should start to behave as normal under Microsoft’s browsers:

  • Create a blank new visual project
  • Right-click on SmartCL.System in your main-form’s uses clause
  • Select “open file at cursor” from the pop-up menu
  • Press CTRL + H to open up the search dialog
  • Enter “w3_getIsInternetExplorer” to locate the function
  • Delete the old function and paste in this code
function  w3_getIsInternetExplorer: Boolean;
var
  ua: variant;

  function IE_10_Or_Older: boolean;
  var
    msie: variant;
  begin
    asm
      @msie = (@ua).indexOf('MSIE ');
      @result = (@msie > 0);
    end;
  end;

  function IE_11: boolean;
  var
    trident: variant;
  begin
    asm
      @trident = (@ua).indexOf('Trident/');
      @result = (@trident > 0);
    end;
  end;

  function IE_Edge: boolean;
  var
    edge: variant;
  begin
    asm
      @edge = (@ua).indexOf('Edge/');
      @result = (@edge > 0);
    end;
  end;

begin
  asm
    @ua = window.navigator.userAgent;
  end;
  result := IE_Edge or IE_11 or IE_10_Or_Older;
end;

A small shim for older IE

In some IE builds (8 and 9 I seem to remember) there is also a problem with “element.addEventListner()”. Where both Firefox, Chrome, Safari, Opera and the myriad of webkit clones expects the plain event-name, IE expects the event names to be prefixed with “on”. This flies straight in the face of what every other browser vendor supports (Microsoft always have to screw up like this, sorry but I just think they act like spoiled, retarded children at times).

You might want to add this shim to the Initialization section of your application unit, just to be on the safe side:

initialization
begin
  asm
  if (typeof Element.prototype.addEventListener === 'undefined') {
    Element.prototype.addEventListener = function (e, callback) {
      e = 'on' + e;
      return this.attachEvent(e, callback);
    };
  }
  end;
end;