Archive
Scroll controllers, ah, i love ’em!
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:
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
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
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.
Momentum Scrolling
Momentum scrolling is something we havent had as an option in the VJL directly. We excluded it initially because there were excellent JavaScript libraries especially for this (like iScroll), but in retrospect I guess it wouldnt hurt to have it in the VJL written in object pascal.
Here is a little something I slapped together the other day. Im going to make both TListbox and the ordinary content containers have an option for this.

Oooo.. sexy sexy scroller thingy!
Note: This supports both mouse and touch, and if you are confused about the event objects then head over to Github and snag a copy of that there. Just remove the references to units you dont have and include eventobjs.pas in your uses clause!
The call to SetInitialTransformationStyles() should be replaced with (this makes the browser mark the element for GPU, which is very fast):
FContent.Handle.style[BrowserAPI.Prefix('transformStyle')] := 'preserve-3d'; FContent.Handle.style[BrowserAPI.Prefix('Perspective')] := 800; FContent.Handle.style[BrowserAPI.Prefix('transformOrigin')] := '50% 50%'; FContent.Handle.style[BrowserAPI.Prefix('Transform')] := 'translateZ(0px)';
Oh and it fades out the indicator after a scroll session, quite nice if I say so myself 🙂
Enjoy!
unit Form1; interface uses System.types, System.Colors, System.Events, System.Time, System.Widget, System.Objects, W3C.Date, W3C.DOM, SmartCL.Effects, SmartCL.Events, SmartCL.MouseCapture, SmartCL.System, SmartCL.Graphics, SmartCL.Components, SmartCL.Forms, SmartCL.Fonts, SmartCL.Borders, SmartCL.Application, SmartCL.Controls.Listbox, SmartCL.Controls.Panel, SmartCL.Controls.CheckBox, SmartCL.Controls.Button; type TScrollContent = class(TW3CustomControl) end; TW3ScrollIndicator = class(TW3CustomControl) end; TW3VScrollControl = class(TW3CustomControl) private FYOffset: integer; FContent: TScrollContent; FVRange: TW3Range; FHRange: TW3Range; FPressed: boolean; FStartY: integer; FTarget: integer; FAmplitude: double; FTimestamp: integer; FVelocity: double; FFrame: double; FTicker: TW3DispatchHandle; FFader: TW3DispatchHandle; FTimeConstant: double; FMouseDownEvent: TW3DOMEvent; FMouseUpEvent: TW3DOMEvent; FMouseMoveEvent: TW3DOMEvent; FTouchDownEvent: TW3DOMEvent; FTouchMoveEvent: TW3DOMEvent; FTouchEndsEvent: TW3DOMEvent; FIndicator: TW3ScrollIndicator; function GetYPosition(const E: variant): integer; procedure MoveBegins(sender: TObject; EventObj: JEvent); procedure MoveEnds(sender: TObject; EventObj: JEvent); procedure MoveUpdate(sender: TObject; EventObj: JEvent); procedure HandleContentSizeChanged(sender: TObject); protected procedure Track;virtual; procedure AutoScroll;virtual; procedure ScrollBegins;virtual; procedure ScrollEnds;virtual; procedure Resize;override; procedure InitializeObject; override; procedure FinalizeObject; override; procedure ObjectReady;override; procedure ScrollY(const NewTop: integer); public Property Content:TScrollContent read FContent; end; TForm1 = class(TW3Form) procedure W3Button1Click(Sender: TObject); private {$I "Form1:intf"} FBox: TW3VScrollControl; protected procedure InitializeForm; override; procedure InitializeObject; override; procedure Resize; override; end; implementation //################################################################### // TW3VScrollControl //################################################################### procedure TW3VScrollControl.InitializeObject; begin inherited; FPressed:=false; FYOffset := 0; FStartY := 0; FTimeConstant := 325; Background.fromColor(clWhite); FContent := TScrollContent.Create(self); FIndicator:=TW3ScrollIndicator.Create(self); FIndicator.width:=8; FIndicator.height:=32; FIndicator.StyleClass:='TW3ScrollContentIndicator'; FIndicator.Transparent := true; FMouseDownEvent := TW3DOMEvent.Create(self); FMouseDownEvent.Attach("mousedown"); FMouseDownEvent.OnEvent := @MoveBegins; FMouseMoveEvent := TW3DOMEvent.Create(self); FMouseMoveEvent.Attach("mousemove"); FMouseMoveEvent.OnEvent := @MoveUpdate; FMouseUpEvent := TW3DOMEvent.Create(self); FMouseUpEvent.Attach("mouseup"); FMouseUpEvent.OnEvent := @MoveEnds; FTouchDownEvent := TW3DOMEvent.Create(self); FTouchDownEvent.Attach("touchstart"); FTouchDownEvent.OnEvent:= @MoveBegins; FTouchMoveEvent := TW3DOMEvent.Create(self); FTouchMoveEvent.Attach("touchmove"); FTouchMoveEvent.OnEvent := @MoveUpdate; FTouchEndsEvent := TW3DOMEvent.Create(self); FTouchEndsEvent.Attach("touchend"); FTouchEndsEvent.OnEvent := @MoveEnds; FContent.Handle.ReadyExecute( procedure () begin (* Mark content for GPU acceleration *) FContent.SetInitialTransformationStyles; end); end; procedure TW3VScrollControl.ObjectReady; begin inherited; FContent.OnReSize := HandleContentSizeChanged; FIndicator.left:=ClientWidth-FIndicator.width; FIndicator.bringToFront; FIndicator.Visible:=false; resize; end; procedure TW3VScrollControl.FinalizeObject; begin FContent.free; inherited; end; procedure TW3VScrollControl.HandleContentSizeChanged(sender: TObject); begin if not (csDestroying in ComponentState) then begin FVRange := TW3Range.Create(0, FContent.Height - ClientHeight); FHRange := TW3Range.Create(0, FContent.Width - ClientWidth); end; end; procedure TW3VScrollControl.Resize; var LClient: TRect; begin inherited; if (csReady in ComponentState) then begin LClient := ClientRect; FVRange := TW3Range.Create(0, FContent.Height - LClient.Height); FHRange := TW3Range.Create(0, FContent.Width - LClient.Width); FContent.SetBounds(0,FContent.top,LClient.Width,FContent.height); FIndicator.MoveTo(ClientWidth-FIndicator.Width,FIndicator.top); end; end; procedure TW3VScrollControl.ScrollY(const NewTop: integer); var LGPU: string; LIndicatorTarget: integer; function GetRelativePos:double; begin result := (ClientHeight - FIndicator.Height) / (FContent.Height - ClientHeight); end; begin if not (csDestroying in ComponentState) then begin if (csReady in ComponentState) then begin (* Use GPU scrolling to position the content *) FYOffset := FVRange.ClipTo(NewTop); LGPU := "translate3d(0px,"; LGPU += FloatToStr(-FYOffset) + "px, 0px)"; FContent.Handle.style[BrowserAPI.Prefix("Transform")] := LGPU; (* Use GPU scrolling to position the indicator *) LIndicatorTarget := FYOffset * GetRelativePos; FIndicator.left := clientwidth - FIndicator.width; LGPU :="translateY(" + TInteger.ToPxStr(LIndicatorTarget) + ")"; FIndicator.Handle.style[BrowserAPI.Prefix("Transform")]:= LGPU; end; end; end; procedure TW3VScrollControl.Track; var LNow: integer; Elapsed: integer; Delta: double; V: double; begin LNow := TW3Dispatch.JsNow.now(); Elapsed := LNow - FTimestamp; FTimestamp := TW3Dispatch.JsNow.now(); Delta := FYOffset - FFrame; FFrame := FYOffset; v := 1000 * Delta / (1 + Elapsed); FVelocity := 0.8 * v + 0.2 * FVelocity; end; procedure TW3VScrollControl.ScrollBegins; begin TW3Dispatch.ClearInterval(FFader); if not (csDestroying in ComponentState) then begin FIndicator.Visible := true; FIndicator.AlphaBlend := true; FIndicator.Opacity := 255; end; end; procedure TW3VScrollControl.ScrollEnds; begin TW3Dispatch.ClearInterval(FFader); if not (csDestroying in ComponentState) then begin FFader:=TW3Dispatch.SetInterval(procedure () begin FIndicator.AlphaBlend := true; FIndicator.Opacity := FIndicator.Opacity - 10; if FIndicator.Opacity=0 then begin TW3Dispatch.ClearInterval(FFader); end; end, 50); end; end; procedure TW3VScrollControl.AutoScroll; var Elapsed: integer; Delta: double; begin if FAmplitude<>0 then begin Elapsed := TW3Dispatch.JsNow.now() - FTimestamp; Delta := -FAmplitude * Exp(-Elapsed / FTimeConstant); end; (* Scrolled passed end-of-document ? *) if (FYOffset >= (FContent.Height - ClientHeight)) then begin TW3Dispatch.ClearInterval(FTicker); FTicker := unassigned; ScrollY(FContent.Height-ClientHeight); ScrollEnds; exit; end; (* Scrolling breaches beginning of document? *) if (FYOffset < 0) then begin TW3Dispatch.ClearInterval(FTicker); FTicker := unassigned; ScrollY(0); ScrollEnds; exit; end; if (delta > 5) or (delta < -5) then begin ScrollY(FTarget + Delta); W3_RequestAnimationFrame(AutoScroll); end else begin ScrollY(FTarget); ScrollEnds; end; end; function TW3VScrollControl.GetYPosition(const e: variant): integer; begin if ( (e.targetTouches) and (e.targetTouches.length >0)) then result := e.targetTouches[0].clientY else result := e.clientY; end; procedure TW3VScrollControl.MoveBegins(sender: TObject; EventObj: JEvent); begin FPressed := true; FStartY := GetYPosition(EventObj); FVelocity := 0; FAmplitude := 0; FFrame := FYOffset; FTimestamp := TW3Dispatch.JsNow.now(); TW3Dispatch.ClearInterval(FTicker); FTicker := TW3Dispatch.SetInterval(Track,100); EventObj.preventDefault(); EventObj.stopPropagation(); end; procedure TW3VScrollControl.MoveUpdate(sender: TObject; EventObj: JEvent); var y, delta: integer; begin if FPressed then begin y := GetYPosition(eventObj); delta := (FStartY - Y); if (Delta>2) or (Delta < -2) then begin FStartY := Y; ScrollY(FYOffset + Delta); end; end; EventObj.preventDefault(); EventObj.stopPropagation(); end; procedure TW3VScrollControl.MoveEnds(sender: TObject; EventObj: JEvent); begin FPressed := false; TW3Dispatch.ClearInterval(FTicker); if (FVelocity > 10) or (FVelocity < -10) then begin FAmplitude := 0.8 * FVelocity; FTarget := round(FYOffset + FAmplitude); FTimeStamp := TW3Dispatch.JsNow.Now(); ScrollBegins; w3_requestAnimationFrame(autoscroll); end; EventObj.preventDefault(); EventObj.stopPropagation(); end; { TForm1 } procedure TForm1.W3Button1Click(Sender: TObject); begin self.FBox.Content.height:=1000; end; procedure TForm1.InitializeForm; begin inherited; // this is a good place to initialize components FBox := TW3VScrollControl.Create(self); FBox.SetBounds(10,10,300,300); // var LText :=" <table cellpadding=|0px| style=|border-collapse: collapse| width=|100%|>"; for var x:=1 to 400 do begin if ((x div 2) * 2) = x then LText += " <tr padding=|0px| style=|border: 0px solid black; background:#ECECEC|>" else LText += " <tr style=|border: 0px solid black; background:#FFFFFF|>"; LText += " <td padding=|0px| height=|32px| style=|border-bottom: 1px solid #ddd|>" + x.toString + "</td> "; LText += " <td style=|border-bottom: 1px solid #ddd|>List item #" + x.toString + "</td> "; LText += "</tr> "; end; LText +="</table> "; LText := StrReplace(LText,'|',''''); FBox.Content.innerHTML := LText; FBox.Content.width:=1000; FBox.Content.height := FBox.Content.ScrollInfo.ScrollHeight; end; procedure TForm1.InitializeObject; begin inherited; {$I "Form1:impl"} end; procedure TForm1.Resize; begin inherited; if (csReady in ComponentState) then begin //FBox.setBounds(10,10,clientwidth div 2, clientHeight div 2); end; end; initialization begin Forms.RegisterForm({$I %FILE%}, TForm1); end; end.
Smart Mobile Studio For Business
This is a topic which surfaces from time to time. I completely understand that people whole are new to Smart Mobile Studio, people who havent had time to get into the whole HTML5 “shift” in technology that has occurred for the past 6 years have to ask. And I am very happy to answer such questions, it’s why we made the product to begin with – to help Delphi and Lazarus developers preserve their hard-earned knowledge and use their skillset on a new and exciting platform: namely HTML5 and the cloud.
So is Smart Mobile Studio up for the challenge of business apps? Let’s have a look.
The application
While we have several customers who use and work exclusively with Smart Mobile Studio as their primary development platform, the first and most obvious example for me has to be StarSoft. StarSoft OY is a company from Finland, one of Norway’s neighbours (The Smart Company is Norwegian). One of its developers is Jarto Tarpio which I have had the good fortune of talking with on several occasions. I must admit that Jarto is not an average programmer, his insight into both Delphi and Smart Mobile Studio and the speed at which he adopted the technology demonstrates that he has a solid grasp of both native and virtual environments. So as far as case studies goes, Jarto cannot be called a complete beginner.
StarSoft has produced their latest application, simply called Wilma, written from scratch in Smart Mobile Studio. It’s an application which targets the Finnish school system, and can be downloaded from Google Store and Apple Appstore. The application has around 100.000 downloads and is a good example of what can be achieved using Smart Mobile Studio alone.
You can view the application here (Google AppStore)
Wilma
In Jarto’s own words “When it comes to feedback, we’ve got a lot of good feedback, but there are also a lot of 1-ratings from kids who hate going to school” which is to be anticipated I guess for an app that empowers teachers and parents with access to the public school system, with real-time messaging if someone has skipped class among the features 🙂
Jarto was nice enough to write a few words in response to a Facebook debate, here is a verbatim copy of his reply about Smart Mobile Studio:
“let me tell my reasons for going with SMS instead of Delphi when it came to mobile apps: I didn’t like Delphi’s slow compiler in mobile development. I also fought a long time to get Delphi to recognize my Android device to be able to debug on it – and failed. I didn’t like the way everything needed to be set up for iOS development. SMS was a lot simpler and faster. I also noticed, that app speeds were about the same, so there was no clear advantage of using Delphi compared to SMS. I also did a test of filling a TListBox with a 1k lines on Delphi and noticed that it took a lot of time on Android, so I figured that I needed to write my own listboxes no matter which tool I used. And the last reason is my goal of using the same codebase for iOS, Android and Windows Phone. We already do use 100% same source code with SMS on iOS and Android, but haven’t started to use it on Windows Phone yet. The app itself has about 100k total downloads. There have been problems with really old Android phones, but otherwise it has worked well. When it comes to feedback, we’ve got a lot of good feedback, but there are also a lot of 1-ratings from kids who hate going to school” -Source: Jarto Tarpio, StarSoft OY
Snapshots
Below are some snapshots from Google Store. As you can see Jarto and the team at StarSoft have used Smart Mobile Studio exactly as it was supposed to be used. With a rich CSS style made for the app, custom controls written and adapted for the solution and focus on code which delivers on all platforms. The application is available for both Android and iOS from the same codebase with a Microsoft Phone version in the pipeline.
Casebook source
Updated
All code moved to the same repo. Casebook is now a part of QTXLibrary!
If you want to have a peek at the “casebook” skeleton app (http://lennartaasenden.magix.net/public/) then you can download the “R&D” source in zip format here.
Please be warned that this source will change drastically over the next weeks and that it’s undocumented. The first to be added will be a datasource so articles are truly dynamic, meaning the source of articles can either be a “live” webservice, a file stored on the server or a JSON service.
Also be sure you grab the QTX library here: https://code.google.com/p/qtxlibrary/
A few notes
Heavy use of callback’s and delays make for a more fluent and responsive interface. You will find plenty of this, especially in the event-handlers for buttons and form navigation.
Cloud effects turn themselves off when the present-form is not the login-form, but they can complete a cycle before that happens.
Dont be afraid to mess things up
You must be logged in to post a comment.