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.
IE 10, 11 and Edge patch for Smart Mobile Studio
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
Turns 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;
You must be logged in to post a comment.