Home > Delphi, OP4JS, Smart Mobile Studio > IScroll for Smart Mobile Studio – Part 2

IScroll for Smart Mobile Studio – Part 2

In my previous post I shared a simple class for scrolling the content of a control using iScroll (remember that you need iScroll 5.x for this code).

I have since isolated iScroll in its own controller class and added all the methods from iScroll to it. A controller class is, as the name implies, a class you use to control something else. In this case you can attach the iScroll controller to any TW3CustomControl in order to make the content touch and mouse-wheel sensitive with bounce and momentum scroll features.

Late creation, chicken or the egg?

Before we dig into the new scroll controller there is a topic we need to visit, namely that of “late creation”. When you create a HTML element by code, the element is not injected into the DOM until after the method has executed. In other words, the custom-control constructor executes before the element is visible and ready to be manipulated.

The DOM is a complex place

The DOM is a complex place

To make matters worse, event handling on the element is active immediately – meaning that events like resize() can fire before the constructor is complete. This is why we must always check that our sub-controls are valid before we use them inside the resize() method TW3CustomControl.

What is sorely needed is some form of callback which can be used to inform the browser, that it should come back immediately after an element is injected and ready – and perform extra work. In most cases this “extra work” is just a call to resize in order to layout child objects, but when dealing with native javascript libraries – it can also mean initialization of a system.

Well, this is exactly what I have solved. In the source below you will find a method called “w3_ExecuteOnElementReady()” which you can use inside your constructors (InitializeObject). What it does is simply to check if the element handle has been injected into the DOM, and when it’s ready – it executes a callback function under the same context as where it was called.

Here is the syntax for it:

procedure w3_ExecuteOnElementReady(const aElement:THandle;
          Const aFunc:TProcedureRef);

This is a great solution because it means you can issue “work” to be done on your control “immediately” after the HTML element is ready to be manipulated. For complex composite controls who needs a resize nudge immediately, here is how you would solve it:

Procedure TMyControl.InitializeObject;
Begin
  Inherited;
  //Setup child-controls etc
  //
  //Execute this when the TMyControl HTML element is ready
  W3_ExecuteOnElementReady(Handle, Procedure ()
  Begin
    self.Resize;
  end);
end;

Pretty cool eh? Perfect for controls with many sub-elements on it – which needs a nudge or resize straight away.

The reason I started with this, is because I use this technique to ensure that iScroll is not initialized before the handle is ready, otherwise iScroll would fail depending on how fast your computer runs. With the above code, iScroll is ensured to attach itself correctly.

Updated iScroll unit

Like mentioned in my previous article, you want to create sub-controls and place them on the Content sub-control of your own TW3ScrollWindow (please, derive your own class from this). When you are done populating, you have to call “refresh” to signal iScroll to update it’s scroll-scope.

Procedure TForm1.PopulateScrollList;
var
  x,dy: Integer;
  mLabel: TW3Label;
Begin
  for x:=1 to 100 do
  Begin
    mLabel:=TW3Label.Create(mMyControl.Content);
    mLabel.setBounds(0,dy,400,dy+16);
    inc(dy,16);
  end;
  inc(dy,16);
  mMyControl.Content.Height:=dy; //Size content to all labels
  mMyControl.ScrollAPI.Refresh; //Update IScroll
end;

You may also want to use w3_callback() in the for/next loop to give the browser some time to adjust itself between calls.

Right, here is the new unit — enjoy!

unit iScroll;

//#############################################################################
//
//  Unit:       iScroll.pas
//  Author:     Jon Lennart Aasenden
//  Company:    Jon Lennart Aasenden LTD
//  Copyright:  Copyright Jon Lennart Aasenden, all rights reserved
//
//  About:      This unit introduces wrapper classes for iScroll, and
//              provides a baseclass for scrolling content
//              (TScrollingWinControl under Delphi).
//
//  Note:       Save iScroll.js (built for version 5.1.2) to SMS/Libs
//              See iScroll documentation for more information.
//
//              IScroll homepage: http://cubiq.org/iscroll-5
//
//#############################################################################

interface

uses 
  W3System, W3Components, w3graphics, W3Touch;

  type

  (* These are the constructor parameters for iScroll.
     Note: this object is managed by the controller.
     This structure represents the constructor parameter for iScroll. *)
  TW3IScrollOptions = Class(TObject)
  public
    property    MouseWheel:Boolean;
    Property    VerticalScrollbar:Boolean;
    Property    HorizontalScrollbar:Boolean;
    Property    FixedScrollbar:Boolean;
    Property    FadeScrollbar:Boolean;
    Property    HideScrollbar:Boolean;
    Property    Bounce:Boolean;
    Property    Momentum:Boolean;
    Property    LockDirection:Boolean;
    Property    Zoom:Boolean;
    Property    ZoomMax:Integer;
    Property    ScrollBarClass:String;
    function    toJSON:Variant;virtual;
    constructor Create;virtual;
  End;

  (* This is the IScroll controller/wrapper.
     Elements who wants to use IScroll should create an instance
     of this class, and then attach it to the element handle.
     Remember to detach and release the controller in
     the destructor. *)
  TW3IScrollController = Class(TObject)
  private
    FControl:   THandle;
    FHandle:    THandle;
    FEnabled:   Boolean;
    FOptions:   TW3IScrollOptions;
    FOnAttach:  TNotifyEvent;
    FOnDetach:  TNotifyEvent;
  protected
    procedure   setEnabled(const aValue:Boolean);
    function    getReady:Boolean;
  public
    Property    Enabled:Boolean read FEnabled write setEnabled;
    Property    ControlHandle:THandle read FControl;
    Property    Options:TW3IScrollOptions read FOptions write FOptions;
    Property    Handle:THandle read FHandle;
    Property    Ready:Boolean read getReady;
    Procedure   Attach;
    Procedure   Detach;
    Procedure   Refresh;
    Procedure   Stop;
    Procedure   ScrollTo(x,y:Integer;time:Integer;relative:Boolean);
    Procedure   ScrollToElement(const aElement:String;time:Integer);overload;
    Procedure   ScrollToElement(const aElement:THandle;time:Integer);overload;
    Procedure   ScrollToPage(PageX,PageY:Integer;time:Integer);
    Constructor Create(Const aHandle:THandle);virtual;
    Destructor  Destroy;Override;
  published
    Property    OnAttached:TNotifyEvent read FOnAttach write FOnAttach;
    Property    OnDetached:TNotifyEvent read FOnDetach write FOnDetach;
  End;

  (* Scrollable content placeholder.
     An instance of this class is created automatically by TW3ScrollWindow.
     To introduce another content class, derive class from
     TW3ScrollWindowContentClass - then override getScrollContentClass
     in TW3ScrollWindow and return your own derived classtype *)
  TW3ScrollWindowContent = Class(TW3CustomControl)
  end;
  TW3ScrollWindowContentClass = Class of TW3ScrollWindowContent;

  (* Scrollable content custom-control.
     If you need smooth, momentum based scrolling of content -- derive
     your custom control from this baseclass. *)
  TW3ScrollWindow = Class(TW3CustomControl)
  private
    FObj:     THandle;
    FContent: TW3ScrollWindowContent;
    FScroller:TW3IScrollController;
  protected
    function  getScrollContentClass:TW3ScrollWindowContentClass;virtual;
    procedure InitializeObject; override;
    procedure FinalizeObject;Override;
  public
    Property  Content:TW3ScrollWindowContent read FContent;
    Property  ScrollApi:TW3IScrollController read FScroller;
  End;


(* Helper functions *)
function  w3_FindElementRootAncestor(const aElement:THandle):THandle;
function  W3_ElementInDOM(const aElement:THandle):Boolean;
procedure w3_ExecuteOnElementReady(const aElement:THandle;
          Const aFunc:TProcedureRef);

implementation

{$R 'iScroll.js'}

uses w3sprite3d;

function w3_FindElementRootAncestor(const aElement:THandle):THandle;
var
  mAncestor:  THandle;
Begin
  if (aElement) then
  Begin
    mAncestor:=aElement;
    while (mAncestor.parentNode) do
    mAncestor:=mAncestor.parentNode;
    result:=mAncestor;
  end;
end;

function W3_ElementInDOM(const aElement:THandle):Boolean;
var
  mRef: THandle;
begin
  if (aElement) then
  Begin
    (* Check that top-level ancestor is window->document->body *)
    mRef:=w3_FindElementRootAncestor(aElement);
    result:=(mRef.body);
  end;
end;

procedure w3_ExecuteOnElementReady(const aElement:THandle;
          Const aFunc:TProcedureRef);
Begin
  if (aElement) then
  begin
    if assigned(aFunc) then
    Begin
      if W3_ElementInDOM(aElement) then
      aFunc() else
      w3_callback(
        procedure ()
        begin
          w3_ExecuteOnElementReady(aElement,aFunc);
        end,
        100);
    end;
  end;
end;

//############################################################################
// TW3IScrollController
//############################################################################

Constructor TW3IScrollController.Create(Const aHandle:THandle);
Begin
  inherited Create;
  FOptions:=TW3IScrollOptions.Create;
  FControl:=aHandle;
end;

Destructor TW3IScrollController.Destroy;
Begin
  if (FHandle) then
  Begin
    FHandle.destroy();
    FHandle:=null;
  end;
  FOptions.free;
  inherited;
end;

function TW3IScrollController.getReady:Boolean;
Begin
  if (FHandle) then
  Begin
    try
      result:=FHandle.isReady();
    except
      on e: exception do
      raise EW3Exception.CreateFmt
      ('IScroll->isReady() threw exception: [%s]',[e.message]);
    end;
  end;
end;

Procedure TW3IScrollController.Stop;
Begin
  if (FHandle) then
  Begin
    try
      FHandle.stop();
    except
      on e: exception do
      raise EW3Exception.CreateFmt
      ('IScroll->Stop() threw exception: [%s]',[e.message]);
    end;
  end;
end;

procedure  TW3IScrollController.setEnabled(const aValue:Boolean);
Begin
  if (FHandle) then
  Begin
    if aValue<>FEnabled then
    begin
      try
        case aValue of
        true:   FHandle.enable();
        false:  FHandle.disable();
        end;
        FEnabled:=aValue;
      except
        on e: exception do
        raise EW3Exception.CreateFmt
        ('IScroll->setEnabled([bool]) threw exception: [%s]',[e.message]);
      end;
    end;
  end;
end;

Procedure TW3IScrollController.Refresh;
begin
  if (FHandle) then
  Begin
    try
      FHandle.refresh();
    except
      on e: exception do
      raise EW3Exception.CreateFmt
      ('IScroll->Refresh() threw exception: [%s]',[e.message]);
    end;
  end;
end;

Procedure TW3IScrollController.ScrollTo(x,y:Integer;
          time:Integer;relative:Boolean);
Begin
  if (FHandle) then
  Begin
    try
      FHandle.scrollTo(x,y,time,relative);
    except
      on e: exception do
      raise EW3Exception.CreateFmt
      ('IScroll->ScrollTo([int,int]) threw exception: [%s]',[e.message]);
    end;
  end;
end;

Procedure TW3IScrollController.ScrollToPage(PageX,PageY:Integer;time:Integer);
Begin
  if (FHandle) then
  Begin
    try
      FHandle.scrollToPage(pageX,PageY,time);
    except
      on e: exception do
      raise EW3Exception.CreateFmt
      ('IScroll->ScrollToPage([int,int,int]) threw exception: [%s]',[e.message]);
    end;
  end;
end;

Procedure TW3IScrollController.ScrollToElement
         (const aElement:String;time:Integer);
Begin
  if (FHandle) then
  Begin
    try
      FHandle.scrollToElement(aElement,time);
    except
      on e: exception do
      raise EW3Exception.CreateFmt
      ('IScroll->ScrollToElement([str,int]) threw exception: [%s]',[e.message]);
    end;
  end;
end;

Procedure TW3IScrollController.ScrollToElement
          (const aElement:THandle;time:Integer);
Begin
  if (FHandle) then
  Begin
    try
      FHandle.scrollToElement(aElement,time);
    except
      on e: exception do
      raise EW3Exception.CreateFmt
      ('IScroll->ScrollToElement([handle,int]) threw exception: [%s]',[e.message]);
    end;
  end;
end;

Procedure TW3IScrollController.Attach;
var
  mHandle:  THandle;
  mTemp:    THandle;
  mOptions: Variant;
Begin
  (* Shut down current instance *)
  if (FHandle) then
  Detach;

  (* setup iScroll options *)
  mOptions:=FOptions.toJSON;

  (* Create iScroll controller for our viewport *)
  try
    mHandle:=FControl;
    asm
      @mTemp = new IScroll(@mHandle,@mOptions);
    end;
    FHandle:=mTemp;
  except
    on e: exception do
    raise EW3Exception.CreateFmt
    ('Failed to create IScroll instance: %s',[e.message]);
  end;

  FEnabled:=True;

  if assigned(FOnAttach) then
  FOnAttach(Self);

end;

Procedure TW3IScrollController.Detach;
Begin
  if (FHandle) then
  begin
    try
      try
        FHandle.destroy();
        FHandle:=Null;
      except
        on e: exception do
        raise EW3Exception.CreateFmt
        ('Failed to destroy IScroll instance: %s',[e.message]);
      end;
    finally
      FEnabled:=False;
    end;

    if assigned(FOnDetach) then
    FOnDetach(self);

  end;
end;

//############################################################################
// TW3IScrollOptions
//############################################################################

constructor TW3IScrollOptions.Create;
Begin
  inherited Create;
  mouseWheel:=True;
  Bounce:=True;
  Momentum:=True;
  ZoomMax:=4;
  VerticalScrollbar:=True;
  HorizontalScrollbar:=True;
  HideScrollbar:=false;
end;

function TW3IScrollOptions.toJSON:Variant;
Begin
  (* Push IScroll options into JSON object *)
  result:=TVariant.createObject;
  result.mouseWheel:=mouseWheel;
  result.hScrollbar:=HorizontalScrollbar;
  result.vScrollbar:=verticalScrollbar;;
  result.fixedScrollbar:=fixedScrollbar;
  result.fadeScrollbar:=fadeScrollbar;
  result.hideScrollbar:=hideScrollbar;
  result.bounce:=bounce;
  result.momentum:=momentum;
  result.zoom:=Zoom;
  result.zoomMax:=ZoomMax;
  result.scrollbarClass:=ScrollBarClass;
  result.lockDirection:=lockDirection;
end;

//############################################################################
// TW3ScrollWindow
//############################################################################

procedure TW3ScrollWindow.InitializeObject;
Begin
  inherited;
  FContent:=getScrollContentClass.Create(self);
  FContent.Height:=0;

  w3_setStyle(FContent.Handle,'postion','relative');
  w3_setStyle(FContent.Handle,'min-width','100%');
  w3_setStyle(FContent.Handle,'min-height','0px');

  FScroller:=TW3IScrollController.Create(self.Handle);

  (* Attach IScroll when DOM element ready *)
  w3_ExecuteOnElementReady(Handle,
    procedure ()
    begin
      FScroller.Attach;
    end );
end;

procedure TW3ScrollWindow.FinalizeObject;
Begin
  FScroller.Detach;
  FScroller.free;
  FContent.free;
  inherited;
end;

function TW3ScrollWindow.getScrollContentClass:TW3ScrollWindowContentClass;
Begin
  result:=TW3ScrollWindowContent;
end;

end.
Advertisements
  1. robert lopz
    January 1, 2015 at 11:13 am

    Hi,

    I’m playing with this unit today..And i must say it’s scrolls very smootly.
    But when i’m trying to place an button on the scrollwindow..and i click the button i get two event clicks..and when i place an image on the scrollwindow the event never get fired..
    tested with a IPad..
    In a browser the event clicks behave nomally

    How can i options.tap to true descriped in https://github.com/cubiq/iscroll?

    Robert

  2. robert lopz
    January 1, 2015 at 11:20 am

    Hi,

    I’m playing with this unit today..And i must say it’s scrolls very smootly.
    But when i’m trying to place an button on the scrollwindow..and i click the button i get two event clicks..and when i place an image on the scrollwindow the event

    never get fired..
    tested with a IPad..
    In a browser the event clicks behave nomally

    How can i options.tap to true descriped in https://github.com/cubiq/iscroll?

    Robert

    • Jon Lennart Aasenden
      January 1, 2015 at 2:31 pm

      Check the initialization section, the options for iscroll are set in a record. Simply add to that record, I know there were a couple of options that i did not add at the time, but those should be easy to fix 🙂

  3. January 1, 2015 at 9:10 pm

    Thanks for the hint. i manually add the tap en the click option..
    The double event clicks seems to be a problem of iscroll.
    https://github.com/cubiq/iscroll/issues/742

    thanks

    • Jon Lennart Aasenden
      January 2, 2015 at 8:22 am

      There was another guy that also had this problem, but there was an option for this he said in the jScroll code. I never had a problem with it on iOS to be honest.

    • Jon Lennart Aasenden
      January 2, 2015 at 8:23 am

      You could also try to fetch the latest iScroll.js file from github and replace the one I have used. There may be newer versions of it with more functions 🙂

  1. No trackbacks yet.

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: