Catch quick swipes in Smart Mobile Studio
Smart Mobile Studio has full support for gesture events, but there are some scenarios where wrapping the whole shabam is just pure overkill. One such case is my latest component: A Windows Mobile 8 category header. It’s a full implementation of the Windows 8.1 mobile category header, complete with CSS3 transformation and GPU powered scrolling.
The Windows 8.1 mobile category header
If you own a Windows 8.1 mobile device you have probably seen this header in action many times. It’s basically just a black horizontal panel. Inside is X number of categories, where the current selected category is highlighted with a white color, while non-selected categories are in a dark grey color.
In the picture to the right you can see it at the top of the display below the “select players” caption. Under the header is a normal pick-list as it’s called in Visual Studio SDK. The category header is de-coupled from the list below it, which gives a nice effect since they move and scroll at different speeds.
Behavior
When you swipe to the right or left the category on either side, depending on what way you swiped, the content scrolls smoothly and centers the selected item.
It’s a very simple little thing but visually very effective, since it tells you where you are and also where you can navigate to next. It’s also very intuitive since you immediately realize that you can move through the different categories by swiping to the left or right.
But do we really need to handle the full gesture API just for this? I think not. It’s overkill for such a small task.
Secondly, since this is a component designed to be re-used and dropped into a project, we dont want to use the gesture events. Component developers traditionally leave the published events alone so that their customers can enjoy the full spectrum of events in their development cycle.
Well, to catch quick swipes in the general directions (left, right, up, down) I decided to implement this in a base-class. And here it is.
NOTE: Please note that you need the recent beta to compile this on your version of Smart Mobile Studio.
unit SmartCL.GestureDetect; interface uses System.types, system.colors, system.dateutils, SmartCL.System, SmartCL.graphics, SmartCL.Components; type TW3QuickSwipeInfo = Record begins: TDateTime; sX: Integer; sY: Integer; eX: Integer; eY: Integer; end; TW3SwipeDirection = (sdNone=0,sdLeft,sdRight,sdDown,sdUp); TSwipeDetectedEvent = Procedure (sender:TObject;Direction:TW3SwipeDirection); TW3QuickSwipeBase = class(TW3CustomControl) private FSwipeDet: TW3QuickSwipeInfo; FMinX: Integer = 20; // [20]--X--[>>] FMaxX: Integer = 40; // [<<]--X--[40] FMinY: Integer = 40; FMaxY: Integer = 50; FDirection: TW3SwipeDirection; FOnSwipe: TSwipeDetectedEvent; FTouchHandleStart: THandle; FTouchHandleMove: THandle; FTouchHandleUp: THandle; public procedure setupGestures; procedure removeGestureDetection; function gestureDetection:Boolean; published Property OnSwipe:TSwipeDetectedEvent read FOnSwipe write FOnSwipe; end; implementation function TW3QuickSwipeBase.gestureDetection:Boolean; begin result:=(FTouchHandleStart) and (FTouchHandleMove) and (FTouchHandleUp); end; Procedure TW3QuickSwipeBase.setupGestures; begin FTouchHandleStart:=handle.addEventListener('touchstart', procedure (e:variant) begin e.preventDefault; var t := e.touches[0]; FSwipeDet.sx:=t.screenX; FSwipeDet.sy:=t.screenY; end); FTouchHandleMove:=handle.addEventListener('touchmove', procedure (e:variant) begin e.preventDefault; if (e.touches) then Begin if e.touches.length>0 then begin var t := e.touches[0]; FSwipeDet.eX:=t.screenX; FSwipeDet.eY:=t.screenY; FSwipeDet.begins:=now; end; end; end); FTouchHandleUp:=handle.addEventListener('touchend', procedure (e:variant) var mTicks: Integer; begin e.preventDefault; FDirection:=sdNone; (* How many Ms since touch and release? *) mTicks:=MillisecondsBetween(FSwipeDet.begins,now); if mTicks < 10 then begin if (FSwipedet.ex - FMinx > FSwipeDet.sx) or (FSwipeDet.ex + FMinx < FSwipeDet.sx) then begin if (FSwipeDet.ey < (FSwipedet.sy + FMaxY)) and (FSwipeDet.sy > (FSwipeDet.ey - FMaxY)) then begin if (FSwipeDet.ex > FSwipeDet.sx) then FDirection:=sdRight else FDirection:=sdLeft; end; end; if ((FSwipeDet.ey - FMinY) > FSwipeDet.sy) or ((FSwipeDet.ey + FMiny) < FSwipeDet.sY) then begin if (FSwipeDet.ex < (FSwipeDet.sx + FMaxX)) and (FSwipeDet.sx > (FSwipeDet.ex - FMaxX)) then begin if FSwipeDet.ey > FSwipeDet.sY then FDirection :=sdDown else FDirection :=sdUp; end; end; if FDirection<>sdNone then w3_requestAnimationFrame( procedure () begin if assigned(OnSwipe) then OnSwipe(self,FDirection); end); end; end); end; procedure TW3QuickSwipeBase.removeGestureDetection; begin if Handle.valid and handle.ready and not (csDestroying in ComponentState) then begin if (FTouchHandleStart) then Handle.removeEventListener(FTouchHandleStart); if (FTouchHandleMove) then Handle.removeEventListener(FTouchHandleMove); if (FTouchHandleUp) then Handle.removeEventListener(FTouchHandleUp); end; end; end.
Right, when you derive your controls from this baseclass, all you have to do is to call setupGestures() in the constructor (initializeObject) and finally removeGestureDetection() in the destructor. Then just catch the OnSwipe event in your ancestor.
Voila, you will now be able to detect quick swipes to the right,left,upwards and downwards.
Below is how I used it to write the Smart Mobile version of the Windows 8 category header.
The final product
No article like this would be complete without a sneak peak at the final product. I realize the irony of not being able to display it live here, but sadly WordPress doesnt allow embedding JS/HTML5 which is a real shame. But here is a picture of the final version. I also adapted the CSS to pick the right fonts for MS-phones, iOS, Android and desktop.
uses System.Types, system.typecon, System.colors, SmartCL.GestureDetect, SmartCL.System, SmartCL.Graphics, SmartCL.Components, SmartCL.Forms, SmartCL.Fonts, SmartCL.Borders, SmartCL.Application, SmartCL.Controls.Label, SmartCL.Controls.ListMenu, SmartCL.Controls.Memo; type TWin8CategoryItem = Class; IItemHost = Interface procedure ItemAltered(Item:TWin8CategoryItem); end; TCategoryChange = (ccAdded,ccDeleted,ccClear,ccItemChange); ICategoryHost = Interface procedure CategoriesAltered(Item:TWin8CategoryItem;const Change:TCategoryChange); end; TWin8textItem = Class(TW3CustomControl) protected procedure StyleTagObject;override; public Property CategoryItem: TWin8CategoryItem; end; ISubItem = Interface procedure setCtrl(value:TWin8textItem); function getCtrl:TWin8textItem; end; TWin8SlideMenu = partial class(TW3QuickSwipeBase); TWin8CategoryItem = Class(TW3OwnedObject,ISubItem) private FOnClick: TNotifyEvent; FWidth: Integer; FHeight: Integer; FCaption: String; FControl: TWin8textItem; Procedure setCaption(value:String); protected procedure setCtrl(value:TWin8textItem); function getCtrl:TWin8textItem; public Procedure ReCalc; Public property Caption:String read FCaption write setCaption; Property Width:Integer read FWidth; Property Height:Integer read FHeight; Property OnClick:TNotifyEvent read FOnClick write FOnClick; end; TWin8Categories = Class(TW3OwnedObject,IItemHost) private FItems: Array of TWin8CategoryItem; procedure ItemAltered(Item:TWin8CategoryItem); public Property Owner:TWin8SlideMenu read ( TWin8SlideMenu( inherited Owner ) ); property Count:Integer read ( FItems.Count ); property Items[index:Integer]:TWin8CategoryItem read ( FItems[index] ); function Add:TWin8CategoryItem; function IndexOf(const Item:TWin8CategoryItem):Integer; procedure Delete(index:Integer); Procedure Clear; end; TWin8Slider = Class(TW3CustomControl); TWin8ItemSelectedEvent = Procedure (sender:TObject;Category:TWin8CategoryItem); TWin8SlideMenu = Class(TW3QuickSwipeBase,ICategoryHost) private FObjects: TWin8Categories; FSlider: TWin8Slider; FSelected: TWin8CategoryItem; FUnSelectedColor: TColor; FSelectedColor: TColor; FOnSelected: TWin8ItemSelectedEvent; procedure setSelectedColor(const Value:TColor); procedure setUnSelectedColor(const value:TColor); protected procedure setSelected(value:TWin8CategoryItem); procedure CategoriesAltered(Item:TWin8CategoryItem; const Change:TCategoryChange); procedure LayoutItems; Procedure CenterSelected; protected procedure InitializeObject;Override; procedure FinalizeObject;Override; Procedure ObjectReady;Override; procedure Resize;Override; Procedure StyleTagObject;override; public Property SelectedItem: TWin8CategoryItem read FSelected write setSelected; property Categories:TWin8Categories read FObjects; Procedure First; procedure Last; Procedure Next; procedure Previous; published Property OnCategorySelected:TWin8ItemSelectedEvent read FOnSelected write FOnSelected; property SelectedColor:TColor read FSelectedColor write setSelectedColor; property UnSelectedColor:TColor read FUnSelectedColor write setUnSelectedColor; end; //############################################################################ // TWin8textItem //############################################################################ procedure TWin8textItem.StyleTagObject; begin inherited; self.font.size:=22; end; //############################################################################ // TWin8SlideMenu //############################################################################ procedure TWin8SlideMenu.InitializeObject; begin inherited; FObjects:=TWin8Categories.Create(self); Font.Size:=22; FSlider:=TWin8Slider.Create(self); self.FUnSelectedColor:=clGrey; self.FSelectedColor:=clWhite; FSlider.Background.FromColor(clBlack); self.OnSwipe:=Procedure (Sender:TObject;Direction:TW3SwipeDirection) begin case Direction of sdNone: background.fromColor(clNone); sdLeft: Next; sdRight:Previous; sdUp: background.fromColor(clCyan); sdDown: background.fromColor(clblue); end; end; self.OnKeyDown:=procedure (sender:TObject;aKeyCode: Integer) begin case aKeyCode of 37: Previous; // right 38: First; // up 39: Next; // left 40: Last; // down end; end; Handle.ReadyExecute( procedure () begin w3_setAttrib(handle,'tabindex',0); end); self.setupGestures; end; procedure TWin8SlideMenu.FinalizeObject; begin FObjects.free; FSlider.free; inherited; end; Procedure TWin8SlideMenu.First; begin if not (csDestroying in ComponentState) then begin if (csReady in ComponentState) then begin if Categories.count>0 then SelectedItem:=Categories.Items[0] else if SelectedItem<>NIL then SelectedItem:=NIL; end; end; end; procedure TWin8SlideMenu.Last; Begin if not (csDestroying in ComponentState) then begin if (csReady in ComponentState) then begin if Categories.count>0 then begin if SelectedItem<>Categories.Items[Categories.count-1] then SelectedItem:=Categories.Items[Categories.count-1]; end; end; end; end; Procedure TWin8SlideMenu.Next; var mIndex: Integer; begin if not (csDestroying in ComponentState) then begin if (csReady in ComponentState) then begin if SelectedItem<>NIL then begin mIndex:=Categories.IndexOf(SelectedItem); if mIndex<Categories.Count-1 then SelectedItem:=Categories.items[mIndex+1] else selectedItem:=Categories.items[0]; end else Begin if categories.Count>0 then SelectedItem:=Categories.Items[0]; end; end; end; end; procedure TWin8SlideMenu.Previous; var mIndex: Integer; begin if not (csDestroying in ComponentState) then begin if (csReady in ComponentState) then begin if SelectedItem<>NIL then begin mIndex:=Categories.indexOf(SelectedItem); if mIndex>0 then SelectedItem:=Categories.items[mIndex-1] else selectedItem:=Categories.Items[Categories.count-1]; end else begin if categories.count>0 then selectedItem:=Categories.Items[Categories.count-1]; end; end; end; end; procedure TWin8SlideMenu.setSelectedColor(const Value:TColor); begin if Value<>FSelectedColor then begin if (csReady in ComponentState) then Begin Beginupdate; FSelectedColor:=Value; addToComponentState([csMoved]); EndUpdate; end else FSelectedColor:=Value; end; end; procedure TWin8SlideMenu.setUnSelectedColor(const value:TColor); begin if Value<>FUnSelectedColor then begin if (csReady in ComponentState) then begin beginUpdate; FUnSelectedColor:=Value; AddToComponentState([csMoved]); endUpdate; end else FUnSelectedColor:=Value; end; end; Procedure TWin8SlideMenu.styleTagObject; begin inherited; end; Procedure TWin8SlideMenu.ObjectReady; begin inherited; W3_RequestAnimationFrame(procedure () begin LayoutItems; if (SelectedItem=NIL) and (Categories.Count>0) then SelectedItem:=Categories.items[0]; end); end; Procedure TWin8SlideMenu.CenterSelected; var dx: Integer; mCtrl: TW3CustomControl; begin if (FSelected=NIL) and (Categories.Count>0) then FSelected:=Categories.items[0]; if FSelected<>NIL then Begin mCtrl:=(FSelected as ISubItem).getCtrl; if mCtrl<>NIL then begin dx:=-(mCtrl.left + (mCtrl.width div 2)); inc(dx, (clientwidth div 2) ); FSlider.fxMoveTo(dx,2,0.3, procedure () var mCtrl: TWin8textItem; begin mCtrl:=(FSelected as ISubItem).getctrl; mCtrl.Font.Color:=FSelectedColor; if assigned(FOnSelected) then FOnSelected(Self,FSelected); end); end; end; end; procedure TWin8SlideMenu.setSelected(value:TWin8CategoryItem); var mCtrl: TW3CustomControl; begin if Value<>FSelected then begin (* Change color or currently selected item *) if FSelected<>NIl then begin try mCtrl:=(FSelected as ISubItem).getctrl; if mCtrl<>NIL then mCtrl.Font.Color:=FUnSelectedColor; except on e: exception do; end; end; (* set new selection *) FSelected:=Value; (* Center selected element *) If (csReady in ComponentState) and (FSelected<>NIL) then CenterSelected; end; end; procedure TWin8SlideMenu.LayoutItems; var x: integer; mTotalWidth: Integer; dx: Integer; mCtrl: TW3CustomControl; begin for x:=0 to self.Categories.Count-1 do Begin categories.items[x].ReCalc; inc(mTotalWidth,categories.items[x].Width + 4); end; if mTotalWidth<>FSlider.width then FSlider.width:=mTotalWidth; dx:=0; for x:=0 to self.Categories.Count-1 do begin mCtrl:=(Categories.Items[x] as ISubItem).getctrl; if mCtrl<>NIL then begin mCtrl.SetBounds(dx,0,Categories.items[x].width,Categories.items[x].height); mCtrl.InnerHTML:=Categories.items[x].Caption; inc(dx,Categories.items[x].width); inc(dx,4); end; end; end; procedure TWin8SlideMenu.CategoriesAltered(Item:TWin8CategoryItem; const Change:TCategoryChange); var mDisp: TWin8textItem; mAccess: ISubItem; begin case Change of ccAdded: Begin (* Create text conatiner element *) mDisp:=TWin8textItem.Create(FSlider); (* Connect container to our item object *) mAccess:=(Item as ISubItem); mAccess.setCtrl(mDisp); (* Initialize the element *) mDisp.BeginUpdate; try mDisp.width:=22; mDisp.height:=22; mDisp.CategoryItem:=Item; mDisp.font.name:=self.Font.Name; mDisp.Font.Size:=self.Font.size; mDisp.font.color:=FUnSelectedColor; mDisp.Background.FromColor(clNone); finally mDisp.EndUpdate; end; (* Hook resize of text element *) mDisp.OnResize:=Procedure (sender:TObject) var mParent: TWin8Slider; mItem: TWin8textItem; Begin mItem:=TWin8textItem(sender); mParent:= TWin8Slider( mItem.Parent ); if mParent<>NIL then begin if mParent.height<>mItem.height then mItem.height:=mParent.Height; end; end; mDisp.OnClick:=procedure (sender:TObject) var mObj: TWin8textItem; begin mObj:=TWin8textItem(sender); SelectedItem:=mObj.CategoryItem; end; w3_requestAnimationFrame(LayoutItems); end; ccDeleted: Begin mDisp:=(item as ISubItem).getCtrl; mDisp.free; end; ccClear: begin end; ccItemChange: begin selectedItem:=NIL; if (csReady in ComponentState) and not (csDestroying in ComponentState) then LayoutItems; end; end; end; procedure TWin8SlideMenu.ReSize; var x: Integer; mItem: TWin8CategoryItem; mheight: Integer; begin inherited; if (csReady in ComponentState) then begin for x:=0 to Categories.Count-1 do Begin mItem:=Categories.items[x]; if mItem.Height>mHeight then mHeight:=mItem.height; end; if mHeight=0 then mHeight:=clientHeight-2; self.FSlider.top:=2; self.FSlider.height:=mHeight; //self.FSlider.Height:=clientHeight-4; end; end; //############################################################################ // TWin8Categories //############################################################################ Procedure TWin8Categories.Clear; var mItem:TWin8CategoryItem; begin try for mItem in FItems do mItem.free; finally FItems.clear; end; end; procedure TWin8Categories.ItemAltered(Item:TWin8CategoryItem); begin if assigned(Owner) then begin (Owner as ICategoryHost).CategoriesAltered (Item,TCategoryChange.ccItemChange); end; end; function TWin8Categories.Add:TWin8CategoryItem; begin result:=TWin8CategoryItem.Create(self); FItems.add(result); (Owner as ICategoryHost).CategoriesAltered(result,ccAdded); end; function TWin8Categories.IndexOf(const Item:TWin8CategoryItem):Integer; begin result:=FItems.indexOf(Item); end; procedure TWin8Categories.Delete(index:Integer); begin FItems.delete(index); end; //############################################################################ // TWin8CategoryItem //############################################################################ procedure TWin8CategoryItem.setCtrl(value:TWin8textItem); begin FControl:=Value; end; function TWin8CategoryItem.getCtrl:TWin8textItem; begin Result:=FControl; end; Procedure TWin8CategoryItem.setCaption(value:String); var mHost: TWin8SlideMenu; begin FCaption:=Value; ReCalc; if (owner<>NIL) then begin mHost:=TWin8Categories(Owner).Owner; if (csReady in mHost.ComponentState) then begin (mHost as ICategoryHost).CategoriesAltered (self,TCategoryChange.ccItemChange); end; end; end; Procedure TWin8CategoryItem.ReCalc; var mHost: TWin8SlideMenu; mSize: TW3TextMetric; begin if Owner<>NIl then begin mHost:=TWin8Categories(Owner).Owner; mSize:=mHost.MeasureText(StrReplace(FCaption," ","_")); FWidth:=mSize.tmWidth; FHeight:=mSize.tmHeight; end; end;
In the Smart Mobile Studio competition launched earlier, I would create, just for fun, a custom header menu component with a fixed slide menu button and a configurable (previous/next buttons) to be used between forms. This component should be designed to be re-used and dropped into a project. Sadly, I’ve been stuck implement this sliding menu and my smart have been expired.
The idea would be a semi-transparent slide menu overlay that hides the main content. When the user clicks the overlay, the menu will toggle back out of view. A push menu will slide in and “push” the main content aside when toggled, something like this:
Forms are created in Application->Display->View.
So by injecting a panel in Application->Display, and then moving view() to the right, you would get a menu like above, without screwing up form positions.