A better IPhone Header for Smart Mobile Studio
With the introduction of the QTX effects units, we can start to make some interesting components that feel more responsive and alive (for the lack of a better word). If the world of HTML5 apps is about anything – it’s flamboyant and exciting user interfaces which not only rival, but surpass the classical components of native languages.
Since writing your own components is often regarded as a black art, something you really dont want to do which is time consuming and boring – I have decided to show just how little it takes to make a completely new IOS header component for Smart Mobile Studio. The goals for this new header is simple, namely to mimic some of the native effects introduced in iOS 5.x – which in no particular order are:
- When either back or next buttons are set to invisible, they slide out of view
- When a button slides out, the caption follows suit – taking up the newly available space
- When you alter the title of the header, the text fades out before setting the new text, then fades back in
- The title font is not longer weight-bold, but plain and shaded
Sounds very complicated right? Well, under Delphi or C++ builder it would have required more than a fair share of black magic, but under Smart Mobile Studio – which were built for this very purpose, it’s a snap.
Isolating the parts
First, let’s look at what an IOS header consists of. It’s actually a very simple piece of engineering, with only two possible buttons visible at once, a gradient background (and buttons styled to match) and a caption. Depending on your IOS version the title is either centered or left oriented. We are going to opt for a centered caption.
So, what we end up with are just 3 custom controls. That’s all it’s going to take to make our new and exciting IOS header, which will look oh-so-clever in combination with sliding forms whizzing about the place. So let’s get to work.
First, we isolate the button classes:
TQTXBackButton = Class(TW3CustomControl) private FOnVisible: TQTXButtonVisibleEvent; protected procedure setVisible(const aValue:Boolean);reintroduce; public property OnVisibleChange:TQTXButtonVisibleEvent read FOnVisible write FOnVisible; published Property Visible:Boolean read getVisible write setVisible; End; TQTXNextButton = Class(TW3CustomControl) protected procedure setVisible(const aValue:Boolean);reintroduce; End;
Since we want our buttons to be more responsive when they either appear or go-away, we target the visible property on our button components. We also need to inform our toolbar that a button has appeared or disappeared, so we add an event to the mix – simply called OnVisibleChange. This way, we can create a response which deals with re-positioning the title whenever a button is moving.
Next, we need the actual header control. This will be extremely simple since all it’s gonna do is to house 3 child components (two buttons and a label). So that looks like this:
TQTXHeaderBar = Class(TW3CustomControl) private FBackButton: TQTXBackButton; FNextButton: TQTXNextButton; FCaption: TQTXHeaderTitle; Procedure HandleBackButtonVisibleChange(sender:TObject;aVisible:Boolean); protected Procedure Resize;override; Procedure InitializeObject;override; Procedure FinalizeObject;Override; public Property Title:TQTXHeaderTitle read FCaption; Property BackButton:TQTXBackButton read FBackButton; property NextButton:TQTXNextButton read FNextButton; End;
Using the force
With the classes clearly defined, we can start to add some meat to our spanking new component. But first, let’s talk a bit about timing.
When you change the title of what is ultimately a label, you want it execute the change in sequence. First, you want the label to quickly fade out using a smooth effect, then you want the actual text to be altered (invisible at this point), until we quickly fade the label back into view.
This probably sounds very complicated, but it’s really the most simple thing to deal with in this task. In fact, the entire label class is no larger than this:
Procedure TQTXHeaderTitle.SetInheritedCaption(const aValue:String); Begin inherited setCaption(aValue); end; Procedure TQTXHeaderTitle.setCaption(const aValue:String); begin if ObjectReady and TQTXTools.getElementInDOM(Handle) then Begin self.fxZoomOut(0.3, procedure () Begin setInheritedCaption(aValue); self.fxZoomIn(0.3); end); end else inherited setCaption(aValue); end;
You may be wondering why there is a function called “setInheritedCaption” defined. To make a long story short, you can only call inherited methods inside a normal procedure, hence we cant call “inherited setCaption()” to change the caption inside a callback procedure. The only way to solve this is to isolate it outside the setCaption() method.
Now in the setCaption() method we first check if the object is ready to be used, that the handle has become a part of the DOM (document object model). If we omit this, you stand at risk of triggering an effect before the DOM is loaded – and your app wont work. You notice that if things are not ready, we call the ancestor directly, because the text being applied is set from the constructor – so no effect can be applied.
If everything is ready and the component is visible, then we use the QTX effects to zoom the label out in 0.3 seconds. We also provide a callback event (anonymous procedure) which is executed when the effect is done. Here we change the text via setInheritedCaption() and fade the component back in. Easy as apple pie.
Next is the button(s). Like mentioned we wanted an event to let us know whenever a button has it’s visible property changed. The event handler for that presently looks like this:
Procedure TQTXHeaderBar.HandleBackButtonVisibleChange (sender:TObject;aVisible:Boolean); Begin case aVisible of false: Begin FCaption.fxMoveTo(2, (clientHeight div 2) - (FCaption.height div 2), 0.3); end; true: Begin FCaption.fxMoveTo((clientwidth div 2) - (FCaption.width div 2), (clientHeight div 2) - (FCaption.height div 2), 0.3); end; end; end;
So, whenever you alter the visibility of the back-button (button on the left of the header), we either slide the header-label center, or backwards, to occupy the space BackButton used to have.
And lest we forget, here is the code for the actual header. All it does at the moment is to house the other controls, so it’s not that advanced. But it’s a lot more responsive than the default IOS header which ship with Smart Mobile Studio.
Procedure TQTXHeaderBar.InitializeObject; Begin inherited; FBackButton:=TQTXBackButton.Create(self); FBackbutton.InnerHTML:='<b>Back</b>'; FBackbutton.Background.FromColor(clRed); FBackButton.OnVisibleChange:=HandleBackButtonVisibleChange; FNextButton:=TQTXNextButton.Create(self); FCaption:=TQTXHeaderTitle.Create(self); FCaption.Background.FromColor(clRed); end; Procedure TQTXHeaderBar.FinalizeObject; Begin FBackbutton.free; FNextButton.free; FCaption.free; inherited; end; Procedure TQTXHeaderBar.HandleBackButtonVisibleChange (sender:TObject;aVisible:Boolean); Begin case aVisible of false: Begin FCaption.fxMoveTo(2, (clientHeight div 2) - (FCaption.height div 2), 0.3); end; true: Begin FCaption.fxMoveTo((clientwidth div 2) - (FCaption.width div 2), (clientHeight div 2) - (FCaption.height div 2), 0.3); end; end; end; Procedure TQTXHeaderBar.Resize; Begin inherited; FBackbutton.setbounds(2,2,100,24); FNextButton.setBounds((clientwidth-2)-100,2,100,24); if FBackbutton.Visible then FCaption.MoveTo((clientwidth div 2) - (FCaption.width div 2), (clientHeight div 2) - (FCaption.height div 2)) else Begin FCaption.moveto(2, (clientHeight div 2) - (FCaption.height div 2) ); end; end;
Adding some bling
Before we start working on the NextButton (right on the header), let’s have a peek at what we got so far, and let’s add some dummy events to the mix to make sure the effects are working. First, add the following to InitializeComponent:
FCaption.OnClick:=Procedure (sender:TObject) Begin FBackButton.Visible:=not FBackbutton.Visible; end;
So whenever you click on the title-label, this code will toggle the visibility of our back-button. Next, let’s create an instance of our header and add another event, just for fun. So in the initializeObject on your form, create the IOS header like this:
mTemp:=TQTXHeaderBar.Create(display); mTemp.height:=40; mTemp.BackButton.OnClick:=Procedure (sender:TObject) Begin mTemp.Title.Caption:='Testing changes'; w3_callback( procedure () Begin mTemp.title.caption:='And this is cool stuff'; end, 1000); end;
This means that whenever we click the back-button, we alter the caption twice after each other. Now let’s have a peek at what it looks like.
Meeeh.. let’s try that again, and this time with the CSS used by TW3HeaderControl
Final touches
As you probably anticipate already, the Next-Button is more or less a clone of what we did with the first, so I wont cover that. The difference is that the label wont move so much – as resize itself to the new available size.
What we must do however, is to change the ancestor for the button classes to TW3ToolButton (as opposed to TW3CustomControl) – and basically, that’s it! Less than 100 lines of code and you have a fully effect driven clone of the IOS form header.
Now when we navigate our forms the GUI feels so much more alive and responsive. It’s just an optical effect really, but it gives that special “feel” to it which we recognize from native, Objective C components under IOS.
After thought
This example of writing components was extremely simple, but still the topics involved, like using GPU powered CSS effects is right up there with the best of code. Under native Delphi or C++ we would have to code even that, so naturally the amount of source-code we have to write under those languages would be much, much larger.
But that is also the point, namely to point out how simple, elegant and even enjoying it is to write your own HTML5 components. I know it sounds like i’m just basking in my own ideas, but Smart Mobile Studio is the only product which made me feel like I was 15 years old again, hacking away on my Amiga. Nothing is nicer than waking up, grabbing a fresh pot of coffey and sit down with SMS to code something you love working on.
As always, remember to download the Quartex library (which is needed for the effects), this can be found here: https://code.google.com/p/qtxlibrary/ (and check it out into the SMS/Libraries folder).
Here is the full source so far (the finished version will be on QTX tomorrow):
unit qtxheader; //############################################################################# // // Unit: qtxheader.pas // Author: Jon Lennart Aasenden [Cipher Diaz of Quartex] // Company: Jon Lennart Aasenden LTD // Copyright: Copyright Jon Lennart Aasenden, all rights reserved // // About: This unit introduces a replacement for TW3HeaderControl. // It uses CSS3 animation effects to slide and fade header // elements out of view, which makes for a more responsive // and living UI experience. // // // _______ _______ _______ _________ _______ // ( ___ )|\ /|( ___ )( ____ )\__ __/( ____ \|\ /| // | ( ) || ) ( || ( ) || ( )| ) ( | ( \/( \ / ) // | | | || | | || (___) || (____)| | | | (__ \ (_) / // | | | || | | || ___ || __) | | | __) ) _ ( // | | /\| || | | || ( ) || (\ ( | | | ( / ( ) \ // | (_\ \ || (___) || ) ( || ) \ \__ | | | (____/\( / \ ) // (____\/_)(_______)|/ \||/ \__/ )_( (_______/|/ \| // // // //############################################################################# interface uses W3System, w3components, w3graphics, w3ToolButton, w3borders, qtxutils, qtxeffects, qtxlabel; {.$DEFINE USE_ANIMFRAME_SYNC} const CNT_ANIM_DELAY = 0.22; type TQTXButtonVisibleEvent = Procedure (sender:TObject;aVisible:Boolean); (* Isolate commonalities for Back/Next buttons in ancestor class *) TQTXHeaderButton = Class(TW3ToolButton) private FOnVisible: TQTXButtonVisibleEvent; public property OnVisibleChange:TQTXButtonVisibleEvent read FOnVisible write FOnVisible; Protected Procedure setInheritedVisible(const aValue:Boolean); End; (* Back-button, slides to the left out of view *) TQTXBackButton = Class(TQTXHeaderButton) protected procedure setVisible(const aValue:Boolean);reintroduce; published Property Visible:Boolean read getVisible write setVisible; End; (* Next-button, slides to the right out of view *) TQTXNextButton = Class(TQTXHeaderButton) protected procedure setVisible(const aValue:Boolean);reintroduce; published Property Visible:Boolean read getVisible write setVisible; End; (* Header title label, uses fx to change text *) TQTXHeaderTitle = Class(TQTXLabel) private Procedure SetInheritedCaption(const aValue:String); protected procedure setCaption(const aValue:String);override; End; (* Header control, dynamically resizes and positions caption and button based on visibility. Otherwise identical to TW3HeaderControl *) TQTXHeaderBar = Class(TW3CustomControl) private FBackButton: TQTXBackButton; FNextButton: TQTXNextButton; FCaption: TQTXHeaderTitle; FMargin: Integer = 4; FFader: Boolean = false; Procedure HandleBackButtonVisibleChange(sender:TObject;aVisible:Boolean); Procedure HandleNextButtonVisibleChange(sender:TObject;aVisible:Boolean); protected Procedure setMargin(const aValue:Integer); Procedure Resize;override; Procedure InitializeObject;override; Procedure FinalizeObject;Override; public Property FadeTitle:Boolean read FFader write FFader; Property Margin:Integer read FMargin write setMargin; Property Title:TQTXHeaderTitle read FCaption; Property BackButton:TQTXBackButton read FBackButton; property NextButton:TQTXNextButton read FNextButton; End; implementation //############################################################################# // TQTXHeaderButton //############################################################################# (* This method simply exposes access to the inherited version of setVisible. Since inherited method cannot be called from anonymous event-handlers, we expose it here. *) Procedure TQTXHeaderButton.setInheritedVisible(const aValue:Boolean); Begin inherited setVisible(aValue); end; //############################################################################# // TQTXBackButton //############################################################################# procedure TQTXBackButton.setVisible(const aValue:Boolean); var mParent: TQTXHeaderBar; dx: Integer; Begin (* Make sure object is ready and that the button is injected into the DOM *) if ObjectReady and Handle.Ready and TQTXTools.getDocumentReady then Begin (* Make sure parent is valid *) if Parent<>NIL then Begin (* get parent by ref *) mParent:=TQTXHeaderBar(Parent); if aValue<>getVisible then begin case aValue of false: Begin if mParent.ObjectReady and mParent.Handle.Ready then Begin dx:=-Width; {$IFDEF USE_ANIMFRAME_SYNC} w3_requestAnimationFrame( procedure () begin {$ENDIF} self.fxMoveTo(dx,top,CNT_ANIM_DELAY, procedure () begin setInheritedVisible(false); end); {$IFDEF USE_ANIMFRAME_SYNC} end); {$ENDIF} end else setInheritedVisible(false); end; True: Begin setInheritedVisible(true); self.MoveTo(-Width, (mParent.ClientHeight div 2) - self.height div 2); if mParent.ObjectReady and mParent.Handle.Ready then {$IFDEF USE_ANIMFRAME_SYNC} w3_requestAnimationFrame( procedure () {$ENDIF} Begin self.fxMoveTo(mParent.margin, (mParent.ClientHeight div 2) - self.height div 2,CNT_ANIM_DELAY); {$IFDEF USE_ANIMFRAME_SYNC} end); {$ELSE} end; {$ENDIF} end; end; if assigned(OnVisibleChange) and mParent.Handle.Ready then OnVisibleChange(self,aValue); end; end; end else inherited setVisible(aValue); end; //############################################################################# // TQTXNextButton //############################################################################# procedure TQTXNextButton.setVisible(const aValue:Boolean); var dy: Integer; dx: Integer; mParent: TQTXHeaderBar; Begin (* Make sure element is ready and inserted into the DOM *) if ObjectReady and TQTXTools.getDocumentReady and Handle.Ready then Begin (* make sure parent is valid *) if parent<>NIL then begin (* Make sure this represents a change in state *) if aValue<>getVisible then Begin (* cast parent to local variable *) mParent:=TQTXHeaderBar(Parent); case aValue of false: begin (* move button out to the right *) dy:=top; dx:=mParent.Width; {$IFDEF USE_ANIMFRAME_SYNC} w3_requestAnimationFrame( procedure () begin {$ENDIF} self.fxMoveTo(dx,dy,CNT_ANIM_DELAY, procedure () begin setInheritedVisible(false); end); {$IFDEF USE_ANIMFRAME_SYNC} end); {$ENDIF} end; true: begin (* move button in to the left *) setInheritedVisible(true); dy:=top; dx:=(mParent.ClientWidth - mparent.margin) - self.Width; {$IFDEF USE_ANIMFRAME_SYNC} w3_requestAnimationFrame( procedure () begin {$ENDIF} self.fxMoveTo(dx,dy,CNT_ANIM_DELAY); {$IFDEF USE_ANIMFRAME_SYNC} end); {$ENDIF} end; end; if assigned(OnVisibleChange) then OnVisibleChange(self,aValue); end; end; end else inherited setVisible(aValue); end; //############################################################################# // TQTXHeaderTitle //############################################################################# Procedure TQTXHeaderTitle.SetInheritedCaption(const aValue:String); Begin inherited setCaption(aValue); end; Procedure TQTXHeaderTitle.setCaption(const aValue:String); begin (* Make sure we can do this *) if ObjectReady and TQTXTools.getDocumentReady and Handle.Ready then Begin (* Check valid parent *) if Parent<>NIL then Begin (* Use fading at all? *) if TQTXHeaderBar(Parent).FadeTitle then Begin {$IFDEF USE_ANIMFRAME_SYNC} w3_requestAnimationFrame( procedure () begin {$ENDIF} self.fxFadeOut(CNT_ANIM_DELAY, procedure () Begin setInheritedCaption(aValue); self.fxFadeIn(CNT_ANIM_DELAY); end); {$IFDEF USE_ANIMFRAME_SYNC} end); {$ENDIF} end else setInheritedCaption(aValue); end else inherited setCaption(aValue); end else inherited setCaption(aValue); end; //############################################################################# // TQTXHeaderBar //############################################################################# Procedure TQTXHeaderBar.InitializeObject; Begin inherited; StyleClass:='TW3HeaderControl'; FBackButton:=TQTXBackButton.Create(self); FBackButton.setInheritedVisible(false); FBackbutton.styleClass:='TW3ToolButton'; FBackbutton.Caption:='Back'; FBackbutton.Height:=28; FNextButton:=TQTXNextButton.Create(self); FNextButton.setInheritedVisible(false); FNextButton.styleClass:='TW3ToolButton'; FNextButton.Caption:='Next'; FNextButton.height:=28; FCaption:=TQTXHeaderTitle.Create(self); FCaption.Autosize:=False; FCaption.Caption:='Welcome'; //FCaption.handle.style['border']:='1px solid #444444'; //FCaption.handle.style['background-color']:='rgba(255,255,255,0.3)'; (* hook up events when element is injected in the DOM *) TQTXTools.ExecuteOnElementReady(Handle, procedure () Begin (* Use update mechanism, which forces an internal resize when sized flag is set *) beginUpdate; try FBackButton.OnVisibleChange:=HandleBackButtonVisibleChange; FNextButton.OnVisibleChange:=HandleNextButtonVisibleChange; setWasMoved; setWasSized; finally EndUpdate; end; end); end; Procedure TQTXHeaderBar.FinalizeObject; Begin FBackbutton.free; FNextButton.free; FCaption.free; inherited; end; Procedure TQTXHeaderBar.setMargin(const aValue:Integer); Begin if aValue<>FMargin then begin (* If the element is not ready, try again in 100 ms *) if ObjectReady and TQTXTools.getDocumentReady and Handle.Ready then Begin BeginUpdate; FMargin:=TInteger.EnsureRange(aValue,1,MAX_INT); setWasSized; endUpdate; end else w3_callback(procedure () begin setMargin(aValue); end,100); end; end; Procedure TQTXHeaderBar.HandleNextButtonVisibleChange (sender:TObject;aVisible:Boolean); var wd,dx: Integer; Begin case aVisible of false: begin wd:=clientwidth; dec(wd,FMargin); if FBackButton.Visible then dec(wd,FBackButton.width + FMargin); dx:=FMargin; if FBackButton.visible then inc(dx,FBackButton.Width + FMargin); if ObjectReady and Handle.Ready then Begin wd:=wd - FMargin; {$IFDEF USE_ANIMFRAME_SYNC} w3_requestAnimationFrame( procedure () begin {$ENDIF} FCaption.fxScaleTo(dx, (clientHeight div 2) - FCaption.Height div 2, wd, FCaption.height, CNT_ANIM_DELAY, NIL); {$IFDEF USE_ANIMFRAME_SYNC} end); {$ENDIF} end; end; true: Begin dx:=FMargin; if FBackButton.visible then inc(dx,FBackButton.Width + FMargin); wd:=ClientWidth - (2 * FMargin); if FBackButton.Visible then dec(wd,FBackButton.width); dec(wd,FNextButton.Width); dec(wd,FMargin * 2); {$IFDEF USE_ANIMFRAME_SYNC} w3_requestAnimationFrame( procedure () begin {$ENDIF} FCaption.fxSizeTo(wd,FCaption.Height,CNT_ANIM_DELAY, procedure () Begin FCaption.fxMoveTo(dx, (clientHeight div 2) - FCaption.Height div 2, CNT_ANIM_DELAY); end); {$IFDEF USE_ANIMFRAME_SYNC} end); {$ENDIF} end; end; Resize; end; Procedure TQTXHeaderBar.HandleBackButtonVisibleChange (sender:TObject;aVisible:Boolean); var dx: Integer; wd: Integer; Begin case aVisible of false: begin {$IFDEF USE_ANIMFRAME_SYNC} w3_requestAnimationFrame( procedure () begin {$ENDIF} FBackButton.fxMoveTo(-FBackButton.width, (clientheight div 2) - FBackButton.height div 2, CNT_ANIM_DELAY); {$IFDEF USE_ANIMFRAME_SYNC} end); {$ENDIF} end; true: Begin {$IFDEF USE_ANIMFRAME_SYNC} w3_requestAnimationFrame( procedure () begin {$ENDIF} FBackButton.fxMoveTo(FMargin, (clientheight div 2) - FBackButton.height div 2, CNT_ANIM_DELAY); {$IFDEF USE_ANIMFRAME_SYNC} end); {$ENDIF} end; end; case aVisible of false: Begin wd:=ClientWidth - (FMargin * 2); if FNextButton.Visible then Begin dec(wd,FNextButton.Width); dec(wd,FMargin); end; {$IFDEF USE_ANIMFRAME_SYNC} w3_requestAnimationFrame( procedure () begin {$ENDIF} FCaption.fxScaleTo(Fmargin, (clientHeight div 2) - (FCaption.height div 2), wd,FCaption.Height,CNT_ANIM_DELAY,NIL); {$IFDEF USE_ANIMFRAME_SYNC} end); {$ENDIF} end; true: Begin dx:=FMargin + BackButton.Width + FMargin; wd:=ClientWidth - (FMargin * 2); dec(wd,FBackButton.width); if FNextButton.visible then Begin dec(wd,FNextButton.Width); dec(wd,FMargin * 2); end else dec(wd,FMargin); {$IFDEF USE_ANIMFRAME_SYNC} w3_requestAnimationFrame( procedure () begin {$ENDIF} FCaption.fxScaleTo(dx, (clientHeight div 2) - (FCaption.height div 2), wd,FCaption.Height, CNT_ANIM_DELAY, NIL); {$IFDEF USE_ANIMFRAME_SYNC} end); {$ENDIF} end; end; end; Procedure TQTXHeaderBar.Resize; var dx: Integer; wd: Integer; Begin inherited; if FBackbutton.visible then FBackbutton.setbounds(FMargin, (clientheight div 2) - FBackButton.height div 2, FBackButton.width, FBackbutton.height); if FNextButton.visible then FNextButton.setBounds((clientwidth-FMargin)-FNextButton.width, (clientHeight div 2) - FNextButton.height div 2, FNextButton.width, FNextButton.Height); dx:=FMargin; if FBackButton.visible then inc(dx,FBackButton.Width + FMargin); wd:=ClientWidth - FMargin; if FBackButton.visible then dec(wd,FBackButton.Width + FMargin); if FNextButton.visible then begin dec(wd,FNextButton.width + FMargin); dec(wd,FMargin); end else dec(wd,FMargin); FCaption.SetBounds(dx, (clientHeight div 2) - (FCaption.height div 2), wd,FCaption.Height); end; end.
You must be logged in to post a comment.