UPDATE: The demo project attached has been updated due to a couple of “glitches”. One remaining known issue is that the “Config” page doesn’t move to its original position if changing orientation when the keyboard is already showing. I’ll revisit the article when I’ve come up with a solution.
I’m sure almost everyone who develops an app for mobile devices has, or will, come across this problem: an edit control is low enough on the screen to be covered by the virtual keyboard when the user taps on the edit control. When I first started using iOS, I figured that iOS was smart enough to shift the control automatically. Of course, iOS isn’t that smart, so developers need to determine exactly what is shifted.
For example, you might have a form with a toolbar at the top, and the rest of the form occupied with a tabcontrol. The following is an animation of what can happen when you don’t shift the control(s): (click each image to see the animation)
One issue is that you might want the toolbar to remain visible, but the parent of the control (or a parent of that) should be shifted up far enough so that the control is completely clear of the virtual keyboard. The following animations demonstrate this. First for an edit control:
..and for a memo control:
Note from this animation that the caret in the memo appears just above the virtual keyboard, rather than below the bottom of the memo control.
The link following contains a demo that can be used with any Firemonkey mobile project, that takes the drudgery out of having to work out how to move the control(s).
[…] […]
Thanks for this. I’ve been looking for a good solution! I just discovered your site. Very nice!
Hello,
thanks for your sample app for scrolling the focused control element.
Unfortunately, it does not work correctly when the device is rotated.
Then the footer disappears and the memo component is moved to far up.
Maybe you have an idea how to fix this?
Bye,
Jürgen
Thanks for your feedback! I’ll take a look at it and come up with a solution.
Hello,
I took a look at your source code and think that I have found the reason for the “oversized” scrolling.
In your function TControlMover.GetFocusedControlOffset(KeyboardRect: TRect) the calculation of the number of pixels to move is not correct and has to be like this:
Result := (ControlPos.Y + ControlHeight + 2) – (ARect.Bottom – Abs (ARect.Top)) + 21;
With this correction the scrolling does also function in landscape mode.
In this context, I came across a strange effect when changing the orientation after (!) editing one field. The whole layout is destroyed and it is not possible any more to edit anything.
Is this a known Firemonkey bug in XE5 SP1?
Bye,
Jürgen
I haven’t updated the code for XE5 Update 1, which does have known problems as you describe. I’ll take a look into it.
Nice example. But how can I write a method to move the control when having a date time picker or a custom picker? Your class should handle those controls too (in IOS).
I’ll have a look into it.. thanks for your feedback!
Thank you. By the way, I recognized that the controls aren’t moved when the device or the simulator change orientation to landscape.
Does not work if you open the keyboard. (Delphi XE5 UPD2)
What does “open the keyboard” mean, and on which platform(s)?
I meant to display the virtual keyboard.
If the virtual keyboard is displayed, then all elements of the application disappear.
Platform Android 4.1.2, the device Samsung Galaxy S2.
http://youtu.be/TwZY2volk7I
I think I found the problem:
in FormVirtualKeyboardShown it should be
FSaveProps.Align := FSaveProps.Control.Align;
In the original code the alignment of the base objects remained at alNone.
[…] Head over and get the full source to the TControlMover component and demo. […]
Hi everybody,
I have been working with your code, and with suggestions that people post here to correct the problems of your ControlMover class, and I get a definitive code for the class that works in all situations. I share with you for your enjoy.
I have used too a class TFMXUtil posted in this QC for the solution:
http://qc.embarcadero.com/wc/qcmain.aspx/qcmain.aspx?d=117598
The final code here (tested with Delphi XE5 Update 2, IOS 7, iPad Mini):
unit ControlMover;
interface
uses
FMX.SomeUtil, // modified by Patrick 15/2/2014
FMX.Forms, FMX.Controls, System.Types, FMX.Types;
type
TSaveProperties = record
Control: TControl;
Align: TAlignLayout;
Position: TPointF;
end;
TGetMoveControlEvent = procedure(Sender: TObject; FocusedControl: TControl; var MoveControl: TControl) of object;
TControlMover = class(TObject)
private
FForm: TCommonCustomForm;
FSaveProps: TSaveProperties;
FVKBounds: TRectF; // modified by Patrick
FVKVisible: Boolean;
FOnGetMoveControl: TGetMoveControlEvent;
procedure DoGetMoveControl;
function GetFocusedControlOffset(KeyboardRect: TRectF): Single; // modified By Patrick 15/2/2014
function GetStatusBarHeight: Single;
function GetViewRect: TRectF;
function FocusedControl: TControl;
procedure FormVirtualKeyboardHidden(Sender: TObject; KeyboardVisible: Boolean; const Bounds: TRect);
procedure FormVirtualKeyboardShown(Sender: TObject; KeyboardVisible: Boolean; const Bounds: TRect);
public
constructor Create(AForm: TCommonCustomForm);
procedure SlideControl;
property OnGetMoveControl: TGetMoveControlEvent read FOnGetMoveControl write FOnGetMoveControl;
end;
implementation
uses
{$IFDEF IOS}
iOSApi.Foundation, iOSApi.UIKit, FMX.Platform.iOS,
{$ENDIF}
System.SysUtils, FMX.Memo;
{ TControlMover }
constructor TControlMover.Create(AForm: TCommonCustomForm);
begin
inherited Create;
FForm := AForm;
FForm.OnVirtualKeyboardShown := FormVirtualKeyboardShown;
FForm.OnVirtualKeyboardHidden := FormVirtualKeyboardHidden;
end;
procedure TControlMover.FormVirtualKeyboardHidden(Sender: TObject; KeyboardVisible: Boolean; const Bounds: TRect);
begin
FVKVisible := False;
if Assigned(FSaveProps.Control) then
begin
FSaveProps.Control.AnimateFloat(‘Position.Y’, FSaveProps.Position.Y, 0.1);
FSaveProps.Control.Align := FSaveProps.Align;
end;
end;
procedure TControlMover.FormVirtualKeyboardShown(Sender: TObject; KeyboardVisible: Boolean; const Bounds: TRect);
begin
FVKVisible := True;
// FVKBounds := Bounds; // commented By Patrick 15/2/2014
FVKBounds := TFMXUtil.VirtualKeyboardRectToAbsoluteRect(TRectF.Create(Bounds), TForm(FForm)); // modified By Patrick 15/2/2014
if FocusedControl = nil then
Exit;
DoGetMoveControl;
if Assigned(FSaveProps.Control) then
begin
FSaveProps.Align := FSaveProps.Control.Align; // modified by Patrick 15/2/2014
FSaveProps.Control.Align := TAlignLayout.alNone;
FSaveProps.Position.Y := FSaveProps.Control.Position.Y;
SlideControl;
end;
end;
procedure TControlMover.DoGetMoveControl;
var
MoveControl: TControl;
begin
MoveControl := nil;
if Assigned(FOnGetMoveControl) then
FOnGetMoveControl(Self, FocusedControl, MoveControl);
FSaveProps.Control := MoveControl;
end;
function TControlMover.FocusedControl: TControl;
begin
Result := nil;
if Assigned(FForm.Focused) and (FForm.Focused.GetObject is TControl) then
Result := TControl(FForm.Focused.GetObject);
end;
function TControlMover.GetFocusedControlOffset(KeyboardRect: TRectF): Single;
var
Control: TControl;
ControlPos: TPointF;
ControlHeight: Single;
Memo: TMemo;
Caret: TCaret;
ViewRect: TRectF;
begin
Result := 0;
Control := FocusedControl;
if Assigned(Control) then
begin
ControlPos := Control.LocalToAbsolute(PointF(0, 0));
ControlHeight := Control.Height;
if Control is TMemo then
begin
Memo := TMemo(Control);
Caret := Memo.Caret;
ControlPos.Y := ControlPos.Y + (Caret.Pos.Y – Memo.ViewportPosition.Y);
ControlHeight := Caret.Size.Height + 4;
end;
ViewRect := GetViewRect;
Result := (ControlPos.Y + ControlHeight + 2)
// Subtract the keyboard height from the view height to obtain the actual top of the keyboard
– ((ViewRect.Bottom – ViewRect.Top) – (KeyboardRect.Bottom – KeyboardRect.Top))
// Add the status bar height
+ GetStatusBarHeight;
if Result < 0 then
Result := 0;
end;
end;
function TControlMover.GetStatusBarHeight: Single;
{$IFDEF IOS}
begin
Result := 30; // modified by Patrick 14/02/2014
{$IFDEF CPUARM} // i.e. on an iOS device
// if TOSVersion.Check(7, 0) then
// Result := TUIApplication.wrap(TUIApplication.OCClass.SharedApplication).statusBarFrame.size.height; // commented by Patrick 14/02/2014
{$ENDIF}
end;
{$ELSE}
begin
Result := 0;
end;
{$ENDIF}
function TControlMover.GetViewRect: TRectF;
{$IFDEF IOS}
var
ARect: NSRect;
begin
ARect := WindowHandleToPlatform(FForm.Handle).View.bounds;
Result := RectF(ARect.origin.x, ARect.origin.y, ARect.size.width – ARect.origin.x, ARect.size.height – ARect.origin.y);
end;
{$ELSE}
begin
// TODO – Android
end;
{$ENDIF}
procedure TControlMover.SlideControl;
begin
if FVKVisible and Assigned(FSaveProps.Control) then
FSaveProps.Control.AnimateFloat('Position.Y', FSaveProps.Control.Position.Y – GetFocusedControlOffset(FVKBounds), 0.1);
end;
end.
Some tweaks for Android, it can also now be registered as component and dropped onto a form. It will then automatically assign the form.
If the form already has any keyboard Shown or Hidden events assigned these are remembered and still triggered.
Still think GetViewRect for Android could be improved.
unit ControlMover;
interface
uses
FMX.Forms, FMX.Controls, System.Types, FMX.Types, System.Classes;
type
TSaveProperties = record
Control: TControl;
Align: TAlignLayout;
Position: TPointF;
end;
TGetMoveControlEvent = procedure(Sender: TObject; FocusedControl: TControl;
var MoveControl: TControl) of object;
TControlMover = class(TControl)
private
FForm: TCommonCustomForm;
FSaveProps: TSaveProperties;
FVKBounds: TRectF;
FVKVisible: Boolean;
FOnGetMoveControl: TGetMoveControlEvent;
FFormVirtualKeyboardShownEvent: TVirtualKeyboardEvent;
FFormVirtualKeyboardHiddenEvent: TVirtualKeyboardEvent;
procedure DoGetMoveControl;
function GetFocusedControlOffset(KeyboardRect: TRectF): Single;
function GetStatusBarHeight: Single;
function GetViewRect: TRectF;
function FocusedControl: TControl;
procedure FormVirtualKeyboardHidden(Sender: TObject;
KeyboardVisible: Boolean; const Bounds: TRect);
procedure FormVirtualKeyboardShown(Sender: TObject;
KeyboardVisible: Boolean; const Bounds: TRect);
function GetScreenOrientation: TScreenOrientation;
function VirtualKeyboardRectToAbsoluteRect(const Rect: TRectF;
const Scene: IScene): TRectF;
protected
procedure SetForm(AForm: TCommonCustomForm);
public
constructor Create(AOwner: TComponent); override;
procedure SlideControl;
published
property Form: TCommonCustomForm read FForm;
property OnGetMoveControl: TGetMoveControlEvent read FOnGetMoveControl
write FOnGetMoveControl;
end;
procedure Register;
implementation
uses
{$IFDEF MSWINDOWS}
ShellApi, Winapi.Windows, FMX.Forms,
{$ENDIF}
{$IFDEF ANDROID}
FMX.Platform.Android,
{$ENDIF}
{$IFDEF IOS}
iOSapi.UIKit, iOSapi.Foundation, FMX.Platform.iOS,
{$ENDIF}
FMX.Platform, System.Math, FMX.Memo;
{ TControlMover }
function TControlMover.GetScreenOrientation: TScreenOrientation;
var
{$IFDEF IOS}
App: UIApplication;
{$ELSE}
ScreenService: IFMXScreenService;
{$ENDIF}
begin
{$IFDEF IOS}
App := TUIApplication.Wrap(TUIApplication.OCClass.sharedApplication);
case App.statusBarOrientation of
UIInterfaceOrientationPortrait:
Result := TScreenOrientation.soPortrait;
UIInterfaceOrientationPortraitUpsideDown:
Result := TScreenOrientation.soInvertedPortrait;
UIInterfaceOrientationLandscapeLeft:
Result := TScreenOrientation.soLandscape;
UIInterfaceOrientationLandscapeRight:
Result := TScreenOrientation.soInvertedLandscape;
else
{$IFDEF DEBUG}
raise Exception.Create(‘Screen Orientation unknown’);
{$ELSE}
Result := TScreenOrientation.soPortrait;
{$ENDIF}
end;
{$ELSE}
if TPlatformServices.Current.SupportsPlatformService(IFMXScreenService,
IInterface(ScreenService)) then
Result := ScreenService.GetScreenOrientation()
else
Result := TScreenOrientation.soPortrait;
{$ENDIF}
end;
function TControlMover.VirtualKeyboardRectToAbsoluteRect
(const Rect: TRectF; const Scene: IScene): TRectF;
{$IFDEF IOS}
var
ScreenService: IFMXScreenService;
KeybdHeight: Single;
RealActualOrientation: TScreenOrientation;
UIApp: UIApplication;
function GetRealHeightOfTheKeyboard(const R: TRectF): Single;
var
ScreenSize: TPointF;
CertainlyFound: Boolean;
begin
ScreenSize := ScreenService.GetScreenSize();
CertainlyFound := False;
Result := 0;
if RealActualOrientation in [TScreenOrientation.soPortrait,
TScreenOrientation.soInvertedPortrait] then
begin
if ScreenSize.Y = R.Height then
begin
Result := R.Width;
CertainlyFound := True;
end
else if ScreenSize.Y = R.Width then
begin
Result := R.Height;
CertainlyFound := True;
end;
end
else
begin
if ScreenSize.X = R.Height then
begin
Result := R.Width;
CertainlyFound := True;
end
else if ScreenSize.X = R.Width then
begin
Result := R.Height;
CertainlyFound := True;
end;
end;
if not CertainlyFound then
Result := Min(R.Height, R.Width);
end;
{$ENDIF}
begin
{$IFDEF IOS}
Result := Rect;
RealActualOrientation := GetScreenOrientation();
if TPlatformServices.Current.SupportsPlatformService(IFMXScreenService,
IInterface(ScreenService)) then
begin
// Bug no iOS? Quando o dispositivo está em landscape, ele não informa a
// posição vertical do teclado virtual corretamente;
KeybdHeight := GetRealHeightOfTheKeyboard(Result);
if RealActualOrientation in [TScreenOrientation.soPortrait,
TScreenOrientation.soInvertedPortrait] then
Result.Bottom := ScreenService.GetScreenSize.Y
else
Result.Bottom := ScreenService.GetScreenSize.X;
Result.Top := Result.Bottom – KeybdHeight;
end;
UIApp := TUIApplication.Wrap(TUIApplication.OCClass.sharedApplication);
if RealActualOrientation in [TScreenOrientation.soLandscape,
TScreenOrientation.soInvertedLandscape] then
Result.Offset(0, -UIApp.statusBarFrame.size.Width)
else
Result.Offset(0, -UIApp.statusBarFrame.size.Height);
{$ELSE}
Result.TopLeft := Scene.ScreenToLocal(Rect.TopLeft);
Result.BottomRight := Scene.ScreenToLocal(Rect.BottomRight);
{$ENDIF}
end;
constructor TControlMover.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FFormVirtualKeyboardShownEvent := nil;
FFormVirtualKeyboardHiddenEvent := nil;
if (AOwner is TCommonCustomForm) then
begin
SetForm((AOwner as TCommonCustomForm));
end;
end;
procedure TControlMover.SetForm(AForm: TCommonCustomForm);
begin
if Assigned(FForm) then
begin
FForm.OnVirtualKeyboardShown := FFormVirtualKeyboardShownEvent;
FForm.OnVirtualKeyboardHidden := FFormVirtualKeyboardHiddenEvent;
end;
if Assigned(AForm) then
begin
FForm := AForm;
FFormVirtualKeyboardShownEvent := FForm.OnVirtualKeyboardShown;
FFormVirtualKeyboardHiddenEvent := FForm.OnVirtualKeyboardHidden;
FForm.OnVirtualKeyboardShown := FormVirtualKeyboardShown;
FForm.OnVirtualKeyboardHidden := FormVirtualKeyboardHidden;
end
else
begin
FForm := nil;
end;
end;
procedure TControlMover.FormVirtualKeyboardHidden(Sender: TObject;
KeyboardVisible: Boolean; const Bounds: TRect);
begin
FVKVisible := False;
if Assigned(FSaveProps.Control) then
begin
FSaveProps.Control.AnimateFloat(‘Position.Y’, FSaveProps.Position.Y, 0.1);
FSaveProps.Control.Align := FSaveProps.Align;
end;
if Assigned(FFormVirtualKeyboardHiddenEvent) then
begin
FFormVirtualKeyboardHiddenEvent(Sender, KeyboardVisible, Bounds);
end;
end;
procedure TControlMover.FormVirtualKeyboardShown(Sender: TObject;
KeyboardVisible: Boolean; const Bounds: TRect);
begin
FVKVisible := True;
FVKBounds := VirtualKeyboardRectToAbsoluteRect(TRectF.Create(Bounds),
TForm(FForm));
if FocusedControl = nil then
Exit;
DoGetMoveControl;
if Assigned(FSaveProps.Control) then
begin
FSaveProps.Align := FSaveProps.Control.Align;
FSaveProps.Control.Align := TAlignLayout.None;
FSaveProps.Position.Y := FSaveProps.Control.Position.Y;
SlideControl;
end;
if Assigned(FFormVirtualKeyboardShownEvent) then
begin
FFormVirtualKeyboardShownEvent(Sender, KeyboardVisible, Bounds);
end;
end;
procedure TControlMover.DoGetMoveControl;
var
MoveControl: TControl;
begin
MoveControl := nil;
if Assigned(FOnGetMoveControl) then
FOnGetMoveControl(Self, FocusedControl, MoveControl);
FSaveProps.Control := MoveControl;
end;
function TControlMover.FocusedControl: TControl;
begin
Result := nil;
if Assigned(FForm.Focused) and (FForm.Focused.GetObject is TControl) then
Result := TControl(FForm.Focused.GetObject);
end;
function TControlMover.GetFocusedControlOffset
(KeyboardRect: TRectF): Single;
var
Control: TControl;
ControlPos: TPointF;
ControlHeight: Single;
Memo: TMemo;
Caret: TCaret;
ViewRect: TRectF;
begin
Result := 0;
Control := FocusedControl;
if Assigned(Control) then
begin
ControlPos := Control.LocalToAbsolute(PointF(0, 0));
ControlHeight := Control.Height;
if Control is TMemo then
begin
Memo := TMemo(Control);
Caret := Memo.Caret;
ControlPos.Y := ControlPos.Y + (Caret.Pos.Y – Memo.ViewportPosition.Y);
ControlHeight := Caret.size.Height + 4;
end;
ViewRect := GetViewRect;
Result := (ControlPos.Y + ControlHeight + 2)
// Subtract the keyboard height from the view height to obtain the actual top of the keyboard
– ((ViewRect.Bottom – ViewRect.Top) – (KeyboardRect.Bottom –
KeyboardRect.Top))
// Add the status bar height
+ GetStatusBarHeight;
if Result < 0 then
Result := 0;
end;
end;
function TControlMover.GetStatusBarHeight: Single;
{$IFDEF IOS}
begin
Result := 30; // modified by Patrick 14/02/2014
{$IFDEF CPUARM} // i.e. on an iOS device
// if TOSVersion.Check(7, 0) then
// Result := TUIApplication.wrap(TUIApplication.OCClass.SharedApplication).statusBarFrame.size.height; // commented by Patrick 14/02/2014
{$ENDIF}
end;
{$ELSE}
begin
Result := 0;
end;
{$ENDIF}
function TControlMover.GetViewRect: TRectF;
{$IFDEF IOS}
var
ARect: NSRect;
begin
ARect := WindowHandleToPlatform(FForm.Handle).View.Bounds;
Result := RectF(ARect.origin.X, ARect.origin.Y,
ARect.size.Width – ARect.origin.X, ARect.size.Height – ARect.origin.Y);
end;
{$ELSE}
{$IFDEF ANDROID}
begin
Result := WindowHandleToPlatform(FForm.Handle).Bounds;
end;
{$ELSE}
begin
// Nothing
end;
{$ENDIF ANDROID}
{$ENDIF IOS}
procedure TControlMover.SlideControl;
begin
if FVKVisible and Assigned(FSaveProps.Control) then
FSaveProps.Control.AnimateFloat('Position.Y', FSaveProps.Control.Position.Y
– GetFocusedControlOffset(FVKBounds), 0.1);
end;
procedure Register;
begin
RegisterComponents('Form Helpers', [TControlMover]);
end;
end.
Hi Tristan,
Before I jump headlong into debugging this for XE7, have you tried your code with it? In the simulator, when I select the edit in the config page in my demo, the controls slide up, then slide down again (obscured by the VK)
Thanks!
No, I am yet to try XE7, current apps work ok in XE6 waiting for next round of modification before I upgrade.
OK, I’ll have a play around with it, soon
Hi
I have used your code, and your project in Delphi XE6, but the problem is in config page when you click on the the bottom editbox, the list will go up for a moment and then it will get back down under the keyboard.
I’ll have to re-check this in XE6.
It seems to be an issue with TVertScrollbox; it isn’t allowing child objects to have a negative value for Position.Y (at least in XE7, and I guess XE6). I’m looking into it, however anyone shedding any light on this would be welcome.
On my iPad “Patrick Mira Pedrol” source code, but and for ANDROID? I need that and i can´t find anywhere! Please, can somebody help me?
Thanks!
I’ll have a look into the Android issue; thanks.
has anyone made a component for this issue?
Those seeking a solution for Android should check out the Android related comments. I’m about to check out the new features in XE7, and revisit some articles, so there might be some updates to the solutions I’ve provided so far
Hi and thanks for this work. Have you got now a final solution for both Android & IOS for Delphi XE7 ?
I’ll check the status of the bug that is preventing it from working in XE6 and XE7. It’s one of those that is a bit difficult to track down.
There is this code somewhere pro xe8?
I need to revisit this project for XE8. There’s also a long outstanding bug in TVertScrollBox (possibly since XE6) that I need to resolve, i.e. it does not allow child objects to have a negative value for Position.Y (which worked in XE5)
I’ll get to it within the next day or so, because it will affect a couple of current projects.
Can I take it that there is no progress on this yet?
It is affecting an Android app of mine and has forced me to restrict input fields, to the upper half of the display.
Does EMB know about the matter? I fear it will still be unfixed in XE9.
No progress on what, exactly?
I was referring to your May 8, 2015 post above.
I was having problems with a TVertScrollBox containing a TEdit which could not be seen without scrolling. In that case the TEdit was not ‘correctly’ scrolled up when the VKbd showed.
However I have since discovered the undernoted vkdbhelper unit which seems to meet all my needs.
http://www.fmxexpress.com/keep-controls-visible-when-virtual-keyboard-opens-in-delphi-xe8-firemonkey-on-android/
[…] Malcolm on Moving controls into view when the virtual keyboard is shown […]