Home > OP4JS > Smart Mobile Studio, creating a scrollbar

Smart Mobile Studio, creating a scrollbar

February 20, 2013 Leave a comment Go to comments

What HTML5 really needs is a proper, native scrollbar that doesnt suck. I’ve had a look around the internet but the native javascript versions are either dependent on some hyped up library, rendering it almost useless (i dont need 40k of javascript just to get things scrolling) – or a part of a large framework that i have no time or patience to wrap.

Since we are adding more and more user controls to smart these days, why not add good o’l TScrollBar to the mix?

It took me less than one hour to knock this one together. That included making breakfast, having a latte and brushing my teeth. If you are looking for a serious toolbox for your HTML5 development kit — then no other development studio will save you as much time as Smart Mobile Studio (he said blatantly promoting his own creation). Ok, here goes:

Analyzing the problem

So, what is a scrollbar? In short it consists of 4 parts: an up arrow, a region to move a handle, the handle – and last but not least: a down arrow. The hard part is not to draw or setup the actual control, but rather to translate between screen coordinates and the values the scrollbar represents. The scrollbar might represent 100000 items of “something”, but since you have (for instance) only 400 pixels to represent that sum – you have to translate between the visual and the abstract.

I started by isolating the math in 3 methods:

  function TW3CustomScrollBar.calcSizeOfHandle:Integer;
  var
    mTemp:  Integer;
  Begin
    mTemp:=TInteger.PercentOfValue(PageSize,Total);
    result:=round( mTemp * getArea / 100);
  end;

  function TW3CustomScrollBar.PositionToPixelOffset(aPosition:Integer):Integer;
  var
    mTemp:  Integer;
  Begin
    mTemp:=TInteger.PercentOfValue(aPosition,Total);
    result:=round( mTemp * getArea / 100);
  end;

  function TW3CustomScrollBar.PixelOffsetToPosition(aPxPos:Integer):Integer;
  var
    mTemp: Integer;
  Begin
    mTemp:=TInteger.PercentOfValue(aPxPos,getarea);
    result:=trunc( (mTemp * Total) / 100 );
  end;

I know, I know – this could have been made so much simpler. But it was 06:45 in the morning and I had just woken up and I was out of redbull. So just go with it. Here is the baseclass in all it’s humble glory:

type

  TW3ScrollbarLowerBtn  = Class(TW3CustomControl);
  TW3ScrollbarHigherBtn = class(TW3CustomControl);
  TW3ScrollbarHandle    = class(TW3CustomControl);

  TW3CustomScrollBar = Class(TW3CustomControl)
  private
    FUpBtn:     TW3ScrollbarLowerBtn;
    FDownBtn:   TW3ScrollbarHigherBtn;
    FHandle:    TW3ScrollbarHandle;
    FTotal:     Integer;
    FPageSize:  Integer;
    FPosition:  Integer;
  protected
    Procedure   setTotal(aValue:Integer);virtual;
    procedure   setPageSize(aValue:Integer);virtual;
    Procedure   setPosition(aValue:Integer);virtual;
    Procedure   setPositionNoCalc(aValue:Integer);virtual;
    procedure   InitializeObject; override;
    procedure   FinalizeObject; override;
    function    calcSizeOfHandle:Integer;
    function    PositionToPixelOffset(aPosition:Integer):Integer;
    function    PixelOffsetToPosition(aPxPos:Integer):Integer;
    Procedure   Recalculate;virtual;abstract;
    function    getArea:Integer;virtual;abstract;
  public
    class function supportAdjustment:Boolean;override;
    Property    MinButton:TW3ScrollbarLowerBtn read FUpBtn;
    property    MaxButton:TW3ScrollbarHigherBtn read FDownBtn;
    Property    DragHandle:TW3ScrollbarHandle read FHandle;
  published
    Property    Total:Integer read FTotal write setTotal;
    Property    PageSize:Integer read FPageSize write setPageSize;
    Property    Position:Integer read FPosition write setPosition;
  End;

Now you might be wondering, why do i bother declaring customcontrols that will, after all, be hidden inside the new control? Because that means that you can style them. Smart automatically maps your classnames to the styles in your stylesheet. So if you define a CSS class called TW3ScrollbarLowerBtn, it will be automatically applied to that control. This means you can alter the scrollbars with your own global theme if you have some css skills.

Next, we have the implementation for our baseclass, which couldnt be simpler:

  //###########################################################################
  // TW3CustomScrollBar
  //###########################################################################

  procedure TW3CustomScrollBar.InitializeObject;
  Begin
    inherited;
    FUpBtn:=TW3ScrollbarLowerBtn.Create(self);
    FDownBtn:=TW3ScrollbarHigherBtn.Create(self);
    FHandle:=TW3ScrollbarHandle.Create(self);
  end;

  procedure TW3CustomScrollBar.FinalizeObject;
  Begin
    FUpBtn.free;
    FDownBtn.free;
    FHandle.free;
    inherited;
  end;

  class function TW3CustomScrollBar.supportAdjustment:Boolean;
  Begin
    result:=true;
  end;

  Procedure TW3CustomScrollBar.setTotal(aValue:Integer);
  Begin
    aValue:=TInteger.EnsureRange(aValue,0,MAX_INT);
    if aValue<>FTotal then
    Begin
      Ftotal:=aValue;

      if FPageSize>FTotal then
      FPageSize:=FTotal;

      if FPosition>FTotal-FPageSize then
      Begin
        if (FTotal-FPageSize)<1 then
        FPosition:=0 else
        FPosition:=FTotal-FPageSize;
      end;
      ReCalculate;
      LayoutChildren;
    end;
  end;

  procedure TW3CustomScrollBar.setPageSize(aValue:Integer);
  Begin
    aValue:=TInteger.EnsureRange(aValue,0,FTotal);
    if aValue<>FPageSize then
    Begin
      FPageSize:=aValue;
      if FTotal>0 then
      Begin
        ReCalculate;
        LayoutChildren;
      end;
    end;
  end;

  Procedure TW3CustomScrollBar.setPosition(aValue:Integer);
  Begin
    aValue:=TInteger.EnsureRange(aValue,0,FTotal-FPageSize);
    if aValue<>FPosition then
    Begin
      FPosition:=aValue;
      if FTotal>0 then
      Begin
        ReCalculate;
        LayoutChildren;
      end;
    end;
  end;

  Procedure TW3CustomScrollBar.setPositionNoCalc(aValue:Integer);
  Begin
    aValue:=TInteger.EnsureRange(aValue,0,FTotal-FPageSize);
    if aValue<>FPosition then
    FPosition:=aValue;
  end;

  function TW3CustomScrollBar.calcSizeOfHandle:Integer;
  var
    mTemp:  Integer;
  Begin
    mTemp:=TInteger.PercentOfValue(PageSize,Total);
    result:=round( mTemp * getArea / 100);
  end;

  function TW3CustomScrollBar.PositionToPixelOffset(aPosition:Integer):Integer;
  var
    mTemp:  Integer;
  Begin
    mTemp:=TInteger.PercentOfValue(aPosition,Total);
    result:=round( mTemp * getArea / 100);
  end;

  function TW3CustomScrollBar.PixelOffsetToPosition(aPxPos:Integer):Integer;
  var
    mTemp: Integer;
  Begin
    mTemp:=TInteger.PercentOfValue(aPxPos,getarea);
    result:=trunc( (mTemp * Total) / 100 );
  end;

The vertical bar

Deriving from this skelleton class we can now easily derive both horizontal and vertical scrollbars. Let’s start with the vertical. This one is a bit more complex since we need to dig into javascript events directly. I did this not to hijack the published events (like mousedown etc) making sure these can still be used. Here is the interface

  TW3VerticalScrollbar = Class(TW3CustomScrollBar)
  private
    FDragSize:  Integer;
    FDragPos:   Integer;
    FMoving:    Boolean;
    FEntry:     Integer;

    procedure   jsmousedown(eventObj:JMouseEvent);
    procedure   jsmousemove(eventObj:JMouseEvent);
    procedure   jsmouseup(eventObj:JMouseEvent);

    Procedure   doMouseDown(button:TMouseButton;
                shiftState:TShiftState;x,y:Integer);
    procedure   doMouseUp(button:TMouseButton;
                shiftState:TShiftState;x,y:Integer);
    procedure   doMouseMove(shiftState:TShiftState;x,y:Integer);
  protected
    procedure InitializeObject; override;
    procedure Resize;Override;
    function  getArea:Integer;override;
    Procedure Recalculate;override;
  End;

And here is the implementation. Never mind the colors, I havent done the CSS yet 🙂


  procedure TW3VerticalScrollbar.InitializeObject;
  Begin
    inherited;
    (* minbutton.color:=clGreen;
    maxButton.color:=clCyan;
    draghandle.Color:=clRed; *)

    (* Avoid using up the exposed event handlers *)
    handle.addEventListener('mousedown',@jsmousedown,false);
    handle.addEventListener('mousemove',@jsmousemove,false);
    handle.addEventListener('mouseup',@jsmouseup,false);
  end;

  procedure TW3VerticalScrollbar.jsmousedown(eventObj:JMouseEvent);
  Begin
    eventObj.preventDefault();
    var sr := ScreenRect;
    var shiftState := TShiftState.Current;
    shiftState.MouseButtons := shiftState.MouseButtons or (1 shl eventObj.button);
    shiftState.MouseEvent := eventObj;
    doMouseDown(eventObj.button, shiftState,
             eventObj.clientX-sr.Left, eventObj.clientY-sr.Top);
  end;

  procedure TW3VerticalScrollbar.jsmousemove(eventObj:JMouseEvent);
  Begin
    eventObj.preventDefault();
    var sr := ScreenRect;
    var shiftState := TShiftState.Current;
    shiftState.MouseEvent := eventObj;
    doMouseMove(shiftState, eventObj.clientX-sr.Left, eventObj.clientY-sr.Top);
  end;

  procedure TW3VerticalScrollbar.jsmouseup(eventObj:JMouseEvent);
  Begin
    eventObj.preventDefault();
    var sr := ScreenRect;
    var shiftState := TShiftState.Current;
    shiftState.MouseButtons := shiftState.MouseButtons and not (1 shl eventObj.button);
    shiftState.MouseEvent := eventObj;
    doMouseUp(eventObj.button, shiftState,
           eventObj.clientX-sr.Left, eventObj.clientY-sr.Top);
  end;

  function TW3VerticalScrollbar.getArea:Integer;
  Begin
    result:=Height;

    result:=MaxButton.top - minbutton.boundsRect.bottom;
    exit;

    if minButton.Visible then
    dec(result,MinButton.Height);

    if maxButton.Visible then
    dec(result,MaxButton.height);
  end;

  Procedure TW3VerticalScrollbar.doMouseDown(button:TMouseButton;
            shiftState:TShiftState;x,y:Integer);
  Begin
    if button=mbLeft then
    begin
      if  dragHandle.BoundsRect.ContainsPos(x,y) then
      begin
        FEntry:=y - dragHandle.top;
        FMoving:=True;
      end;
    end;
  end;

  procedure TW3VerticalScrollbar.doMouseMove(shiftState:TShiftState;
            x,y:Integer);
  var
    mNewPos:  Integer;
    dy: Integer;
  Begin
    if FMoving then
    Begin
      (* take offset on draghandle into account *)
      dy:=y - FEntry;

      (* position draghandle *)
      draghandle.top:=TInteger.EnsureRange(dy,
        minButton.top + minButton.height,
        maxButton.top - FDragSize);

      (* Update position based on draghandle *)
      mNewPos:=PixelOffsetToPosition(draghandle.top-MinButton.BoundsRect.Bottom);
      setpositionNoCalc(mNewPos);
    end;
  end;

  procedure TW3VerticalScrollbar.doMouseUp(button:TMouseButton;
            shiftState:TShiftState;x,y:Integer);
  Begin
    if FMoving then
    Begin
      FMoving:=False;
      setPosition(PixelOffsetToPosition
      (draghandle.top-MinButton.BoundsRect.Bottom));
    end;
  end;

  procedure TW3VerticalScrollbar.Resize;
  var
    mTop: Integer;
  Begin
    inherited;
    MinButton.SetBounds(0,0,width,width);
    MaxButton.setBounds(0,(height-width),width,width);

    Recalculate;

    mTop:=MinButton.top + minButton.Height + FDragPos;
    DragHandle.SetBounds(2,mTop,width-4,FDragSize + Border.getVSpace);
  end;

  Procedure TW3VerticalScrollbar.Recalculate;
  Begin
    FDragSize:=calcSizeOfHandle;
    FDragPos:=PositionToPixelOffset(Position);
  end;

And voila — we have a scrollbar!

Scrolltastic

Scrolltastic

Not much to look at, so let’s put some skin on this puppy! Paste this into the application CSS file:


.TW3ScrollbarLowerBtn, .TW3ScrollbarHigherBtn {
  border: 2px ridge rgba(0,0,0,0.3);
  background-color: #FFFFFF;
  overflow: hidden;
  border-radius: 25px;
  -webkit-border-radius: 25px;
     -moz-border-radius: 25px;
      -ms-border-radius: 25px;
       -o-border-radius: 25px;
}

.TW3ScrollbarHandle {
  border: 2px ridge rgba(0,0,0,0.3);
  background-color: #FFFFFF;
  overflow: hidden;
  border-radius: 25px;
  -webkit-border-radius: 25px;
     -moz-border-radius: 25px;
      -ms-border-radius: 25px;
       -o-border-radius: 25px;
}

.TW3VerticalScrollbar {
  border: 2px ridge rgba(0,0,0,0.3);
  background-color: #FFFFFF;
  overflow: hidden;
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  border-radius: 25px;
  -webkit-border-radius: 25px;
     -moz-border-radius: 25px;
      -ms-border-radius: 25px;
       -o-border-radius: 25px;
}

Now let’s run the example again and we have a much better looking scrollbar 🙂

One skinned puppy

One skinned puppy

Well I’m sure you see where this is going – so I wont bother skinning it any more on this blog, but making user controls that are 100% javascript, no dependencies on the OS or any other tricks, is pretty darn easy. The scrollbars (properly skinned) will be in the next Smart Mobile Studio update — enjoy!

Advertisements
  1. No comments yet.
  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: