Archive
Smart Mobile Studio, creating a scrollbar
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!
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 🙂
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!
You must be logged in to post a comment.