Home > CSS, JavaScript, Object Pascal, OP4JS, Smart Mobile Studio > Catch quick swipes in Smart Mobile Studio

Catch quick swipes in Smart Mobile Studio

Windows 8.1 mobile category header

Windows 8.1 mobile category header

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.

The black category menu in action

Work in progress

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.

Snappy animations and easy navigation

Snappy animations and easy navigation


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;

  1. April 5, 2015 at 10:24 pm

    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:

  2. Jon Lennart Aasenden
    April 7, 2015 at 7:31 am

    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.

  1. No trackbacks yet.

Leave a comment