I am writing a chat application using Qt/QML. However, I found an issue while testing the application on my Android device: the virtual keyboard "moves" the window upward and does not allow me to see many of the displayed messages, only the bottom part of my app.
Ideally, I would like to resize the window so that both the message controls (such as the text box and attach files button) and the title bar to be shown. For a graphical example, you can take a look at this:
.
Is it possible to do this in QML?
You can tell Android to do this for you.
Android will resize your application window whenever the virtual keyboard shows up after you adjust the <activity> tag of your AndroidManifest.xml like this:
<activity ... android:windowSoftInputMode="adjustResize">
Source: This was discussed as a workaround in two comments on a Qt bug that prevented manually resizing the window for some time until the end of 2015.
Deploying to Android 10 from Qt 5.12 (C++, no QML required). There don't seem to be any non-QML C++ examples out there of resizing an application in response to on-screen keyboard visibility changes. The ones I did find require interfacing w/ Java from Qt4.
It's necessary to create a container, separate from the QMainWindow, for all of your visible UI. QMainWindow normally occupies the entire screen and will be overlapped by the on-screen keyboard. The container QWidget is what can be resized and must contain every UI element you expect not to be under the keyboard.
The example uses QFrame as being a very minimal (lightweight) container.
YourApp.cpp:
YourApp::YourApp ( QWidget *parent ) : QMainWindow ( parent ) {
// With Android, an application running normally ...
// ... occupies the whole screen. Plan accordingly.
QSize availableSize = qApp->desktop()->availableGeometry().size();
Application_Width = availableSize.width();
Application_Height = availableSize.height();
App_Frame = new QFrame(this);
// Build your UI inside this QFrame
setCentralWidget(App_Frame);
Virtual_Keyboard_Enabled = true;
App_Input_Method = QApplication::inputMethod();
connect(App_Input_Method, SIGNAL(keyboardRectangleChanged()),
this, SLOT(onKeyboardRectangleChanged()));
this->show();
}
void
YourApp::onKeyboardRectangleChanged ( ) {
#if defined(Q_OS_ANDROID)
bool keyboard_visible = App_Input_Method->isVisible();
QRectF keyboard_rectangle = App_Input_Method->keyboardRectangle();
if (not keyboard_visible) {
App_Frame->resize(Application_Width, Application_Height);
}
else {
int keyboard_height = int(keyboard_rectangle.height());
App_Frame->resize(Application_Width,
(Application_Height - keyboard_height));
}
#endif
}
void
YourApp::Toggle_Virtual_Keyboard_Enabled ( ) {
#if defined(Q_OS_ANDROID)
Virtual_Keyboard_Enabled = not Virtual_Keyboard_Enabled;
App_Input_Method->setVisible(Virtual_Keyboard_Enabled);
qApp->setAutoSipEnabled(Virtual_Keyboard_Enabled);
#endif
}
YourApp.h:
class YourApp : public QMainWindow {
Q_OBJECT
public:
YourApp ( QWidget *parent = nullptr );
~YourApp ( );
private:
bool Virtual_Keyboard_Enabled;
QInputMethod *App_Input_Method;
QFrame *App_Frame;
void
Toggle_Virtual_Keyboard_Enabled ( );
private slots:
void
onKeyboardRectangleChanged ( );
}
This post explain the way to resize the QML controls when Android virtual keyboard come up. It involve the use of some java code but you can copy and past directly the code provided with the project example linked:
QML: Resize controls when Android virtual keyboard come up
There is a QML-only way of reacting by resizing your window contents when the virtual keyboard is shown or hidden by the user.
First, make sure that the window resizing is not already done by Android for you (which is also possible). So you would tell Android that the keyboard should overlap the window, by adjusting the <activity> tag in AndroidManifest.xml as follows:
<activity ... android:windowSoftInputMode="adjustPan">
Then, you would place the following into a QML file where you have access to the window or window contents you want to resize and / or reposition:
Connections {
target: Qt.inputMethod
onKeyboardRectangleChanged: {
var newRect = Qt.inputMethod.keyboardRectangle
console.log(
"New keyboard rectangle size:" +
" x: " + newRect.x +
" y: " + newRect.y +
" width: " + newRect.width +
" height: " + newRect.height
)
// Your UI resizing / repositioning code goes here.
}
}
Explanations and details:
The Qt QML Type is not instantiable (source), so you cannot write Qt { inputMethod.onKeyboardRectangleChanged: { }}.
The Connections QML type is made for these cases, allowing to implement a signal handler outside of the object emitting it (details).
Another alternative is to connect the signal to a JavaScript function with connect(), as demonstrated here.
The QRectF type used in the underlaying C++ class QInputMethod is available in QML as QML Basic Type rect. This is documented here:
When integrating with C++, note that any QRect or QRectF value passed into QML from C++ is automatically converted into a rect value, and vice-versa.
You should not implement this in a onVisibleChanged signal handler because that event is not fired on Android when the user clicks the "hide keyboard" button. (Tested with Android 6.0 and Qt 5.12.) This seems to be a bug in Qt, since a keyboard of height 0 is definitely not visible.
Related
I took the source I found here:
https://github.com/xamarin/Xamarin.Forms/blob/master/Xamarin.Forms.Platform.Android/Renderers/MasterDetailRenderer.cs
And created a custom renderer in a Xamarin Forms Android project. I got the project to build and the menu to open / close as expected as well as display the starting detail page.
The detail page is a "NavigaionPage" in Xamarin.Forms which is actually just a ViewGroup with a bunch of code to make it work with Push / Pop functions.
When I push a new Page, one that has a custom renderer and native RelativeLayout as a subview, the Page appears blank white until I rotate orientation.
After some research in the code, I realized both OnMeasure and OnLayout was not being called in the Page which was being pushed to the NavigationPage / detail page (with the pushed page's size stuck at 0x0 until orientation change).
I created another custom renderer but this time for the NavigationPage which looks like this:
public class MasterDetailContentNavigationPageRenderer : NavigationPageRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<NavigationPage> e)
{
base.OnElementChanged(e);
if(e.OldElement != null)
{
e.OldElement.Pushed -= handleOnPushed;
}
if(e.NewElement != null)
{
e.NewElement.Pushed += handleOnPushed;
}
}
private void handleOnPushed(object sender, EventArgs e)
{
var w = MeasureSpec.MakeMeasureSpec(MeasuredWidth, MeasureSpecMode.Exactly);
var h = MeasureSpec.MakeMeasureSpec(MeasuredHeight, MeasureSpecMode.Exactly);
Measure(w, h);
Layout(Left, Top, Right, Bottom);
}
}
This custom renderer for the detail page / NavigaionPage worked for 2 out of 3 of my pushed Pages.
The main / most important page did not fully add all of the UI elements.
The Page that was pushed and missing UI has SOME of the UI. The following is a list of how the page is displayed:
--- SongViewerLayout : RelativeLayout
------ RelativeLayout
--------- ViewerTopBar (draws all buttons except one)
--------- DocumentView (blank white spot between top bar and pagination)
--------- ViewerPagination (draws background color but not page buttons)
If I change orientations the missing button appears in the top bar, the page numbers show up and the document view sort of draws but it's off.
If I go back to using Xamarin.Forms provided MasterDetailPage the UI just loads as expected.
I have studied the code in the Xamarin.Forms repo but none of it really applies to the inner workings of the elements that are added. In other words, the detail page (which in my case is a NavigationPage) gets added on SetElement(...) and some handlers are assigned ... but nothing I could tell that looks for Pages being pushed and then responding. I would assume / expect the NavigationPage just to work as expected now that it's a subview of the MasterDetailPage / DrawerLayout.
Note: the views that seems to have the most issues are added programmatically and given "LayoutParamaters". The views found in the AXML seem to just show up.
Is there something I am missing to "initialize" a layout in Android? I have made native apps in Android and not had to do anything special.
Why would the UI above the NavigationPage cause the NavigationPage not to do standard initialization?
What am I missing???
Edit:
I created a working example of this issue on my GitHub account which you can clone here:
https://github.com/XamarinMonkey/CustomMasterDetail
Build and run CustomMasterDetail.Droid (Notice the UI is a MasterDetailPage)
Tap any item
The bottom blue bar has no page numbers.
Rotate orientation the page numbers display.
Stop the build
Comment out the entire class "MainMasterDetailPageRenderer.cs"
Rebuild / run the app
Tap any item
Page numbers are shown right away as expected just because it is now using the default MasterDetailPage!
Give suggestions please!
Edit #2:
Thanks to some feedback, it does not seem to be an issue in 6.0 devices. My testing device is a "Samsung Galaxy Tab Pro" with v5.1.1. And unfortunately I need this app to go back to 4!
Edit #3:
The issue has to be adding a view programmatically in Android 5.1.1 to a native RelativeLayout (which is pushed to a NavigationPage detail page ) after it is inflated in Xamarin.Forms. Calling Measure / Layout manually seems to solve the issue. But I need a more automated solution.
I came up with a solution that works. It might be expensive but it seems to layout fast on devices I have tested.
Basicly, I read up on Android and found a way to listen for layout changes using the ViewTreeObserver.GlobalLayout event combined with calling Measure / Layout on the detail and master ViewGroups.
public void MeasureAndLayoutNative()
{
if(_childView != null)
{
IVisualElementRenderer renderer = Platform.GetRenderer(_childView);
if(renderer.ViewGroup != null)
{
var nativeView = renderer.ViewGroup;
var w = MeasureSpec.MakeMeasureSpec(nativeView.MeasuredWidth, MeasureSpecMode.Exactly);
var h = MeasureSpec.MakeMeasureSpec(nativeView.MeasuredHeight, MeasureSpecMode.Exactly);
nativeView.Measure(w, h);
nativeView.Layout(nativeView.Left, nativeView.Top, nativeView.Right, nativeView.Bottom);
}
}
}
I updated my code example above with the working changes!
I have some content/input fields that are covered when the android keyboard is shown in my cordova app. I have
android:windowSoftInputMode="adjustPan" and <preference name="fullscreen" value="false" />
I tried android:windowSoftInputMode="adjustResize but it kept shrinking my content because it was resizing the window (My content is sized based on viewport width and viewport height). Thank you for any suggestions!
So I had a work around myself that may or may not work for everyone, but I figured I could post this to hopefully help someone who comes across this!
I found a lot of answers but none really helped me. So in my AndroidManinfest.xml file I set android:windowSoftInputMode="adjustPan|stateHidden". Yes, this will still cover the content below the keyboard when it's opened.
To avoid that, I gave all of my scroll views that would be affected by the keyboard being shown a class of inputScrollContainer. Name them whatever you would like.
Since every container (for me) was the same height as was the top bar for each page, I did the following: (you will have to install the device plugin and the keyboard plugin from cordova
Got window.innerHeight at the beginning of my js (if you do this inside of your native.keyboardshow function, iOS will give you the resized view based on the keyboard's height)
Then, inside my native.keyboardShow function, I did the following:
- Then got the height of the top bar (I chose one as they were all the same)
- Added the added the keyboard height and top bar height together
- Then I subtracted those from the window height
Doing this now gave me the height "leftover" for the scroll view to have. After that I:
Got all elements by class name inputScrollContainer
Looped through them and assigned the new height to each (you can assign it to the only scroll view currently in view, but I only had three affected views so I wasn't worried about it)
Now the scroll view was resized to whatever was left between the top bar and the keyboard. Then on my native.keyboardhide function, I just restored the height to what the original height for all of the scroll views was before.
I'm sure there are other ways to do this, but doing it this way gave me flexibility and consistency across iOS and Android. I hope this helps someone!
To move the layout up when the keyboard is visible/shown add the following activity.
<activity android:windowSoftInputMode="adjustPan|adjustResize"> </activity>
adjustResize : The activity's main window is always resized to make room for the soft keyboard on screen.
adjustPan : The activity's main window is not resized to make room for the soft keyboard. Rather, the contents of the window are automatically panned so that the current focus is never obscured by the keyboard and users can always see what they are typing. This is generally less desirable than resizing, because the user may need to close the soft keyboard to get at and interact with obscured parts of the window.
In your scenario you can make use of adjust pan
However it works based on the android versions. It may not work in particular versions. please be find and use.
Please have look at this answer you will come to know a lot.
Viewport height is the problem here.
There is some way to correct the problem with mediaqueries, or with javascript (modifying all of your dom element with the correct height).
But in my case, I had lots of dom elements, and really didn't want to change all of this with javascript.
My trick is :
- Change all of your vh with rem and divide your value by 4
- use this little javascript in all of your page :
$("html").css({"font-size": ($(window).height()/25)+"px"});
Here we go, in this example, font-size is 4% of window height (cause font-size has a minimum value on mobile app), so :
1rem=4% of widow height=4vh
0.25rem = 1vh etc...
In my case, I use a SASS function to divide with 4 all of my vh, so it was easier to change all css. (1h = rem(1) = 0.25rem)
Hope this will help someday.
This JS option delivers a UX similar to iOS:
let events = {
android: {
keyboard: {
threshold: 300, //px
transition: 300, //ms
visible: false,
last_el: null
}
}
}
onAndroidKeyboard() {
if(is_android) {
let threshold = events.android.keyboard.threshold;
let transition = events.android.keyboard.transition;
function onIn(e) {
let target = e.target;
if(target.nodeName.toLowerCase() !== 'input') {
return false
}
let visible = events.android.keyboard.visible;
let h = window.innerHeight;
try {
let bottom = target.getBoundingClientRect().bottom;
if(bottom) {
let diff = h - bottom;
if(diff < threshold) {
if(!visible) {
let animate_amount = threshold - diff;
events.android.keyboard.visible = true;
document.body.style.transform = 'translateY(0)';
document.body.style.webkitTransition = `all ${transition}ms`;
document.body.style.transition = `all ${transition}ms`;
events.android.keyboard.visible = true;
events.android.keyboard.last_el = target;
requestAnimationFrame(function () {
document.body.style.transform = `translateY(-${animate_amount}px)`;
});
}
}
}
} catch (e) {
console.error(e);
}
}
function onOut(e) {
let visible = events.android.keyboard.visible;
if(visible) {
document.body.style.transform = 'translateY(0)';
setTimeout(function () {
requestAnimationFrame(function () {
document.body.style.removeProperty('transform');
document.body.style.removeProperty('transition');
document.body.style.removeProperty('webkitTransition');
events.android.keyboard.visible = false;
events.android.keyboard.last_el = null;
});
}, transition)
}
}
document.addEventListener('focusin', onIn, false);
document.addEventListener('focusout', onOut, false);
}
}
I'm porting an existing QML/C++ application to the Android system. The application is already running on the Android tablet, but I have issues with Android keyboard.
Since my QML/C++ application has implemented its own keyboard, I would like to disable an Android one.
I've tried to add android:windowSoftInputMode="stateAlwaysHidden" line in AndroidManifest.xml file, but keyboard still appears when I press an edit box.
Since I'm porting an existing application, I do not want to edit the code of application itself. The only things I can edit are AndroidManifest.xml, QtApplication.java and QtActivity.java files. QtApplication and QtActivity are derived from Application and Activity Android classes.
Is it possible to disable the Android keyboard globally for whole app at the startup of application(with editing manifest file or overriding onCreate, onStart or similar functions)?
Are there any functions in Application and Activity classes that I can override them and consequently disable native keyboard?
After some time I found a solution, actually workaround for this problem. The idea was to consume an event that requests Software Input Panel (QEvent::RequestSoftwareInputPanel). This event is sent by QML/C++ application to the host Android system.
So, I implemented an event filter called SIPRequestEater.
class SIPRequestEater: public QObject
{
Q_OBJECT
protected:
bool eventFilter(QObject *obj, QEvent *event)
{
if(event->type() == QEvent::RequestSoftwareInputPanel)
{
// filter out RequestSoftwareInputPanel event
return true;
}
else
{
// standard event processing
return QObject::eventFilter(obj, event);
}
}
};
This filter has to be installed to QCoreApplication berfore the QCoreApplication::run is called.
QCoreApplication *coreApp = QCoreApplication::instance();
SIPRequestEater *sipRequestEater = new SIPRequestEater();
coreApp->installEventFilter(sipRequestEater);
It can be installed also on QApllication.
The problem is, that this filter does not catch QEvent::RequestSoftwareInputPanel event. My explanation for this is that filters, which are installed with QCoreApplication::installEventFilter(<filter>) are filters only for input events, from Android to QML application. QEvent::RequestSoftwareInputPanel is actually going in other direction, from QML application to the Android system. I didn't find out if it is possible to filter/disable output events. Because of this I decided to filter out the focus in event QEvent::FocusIn which actually causes QEvent::RequestSoftwareInputPanel.For our application this works as it should. The Android keyboard is not appearing anymore and our edit text fields still get focus, because we have our own implementation of focus and keyboard. I believe that this is not the perfect solution for everyone, that's why I called it workaround.
If someone knows, how to filter out output events, specially QEvent::RequestSoftwareInputPanel, please post it here.
The final implementation of filter is:
class SIPRequestEater: public QObject
{
Q_OBJECT
protected:
bool eventFilter(QObject *obj, QEvent *event)
{
if(event->type() == QEvent::FocusIn)
{
// filter out FocusIn event
return true;
}
else
{
// standard event processing
return QObject::eventFilter(obj, event);
}
}
};
QApplication::setAutoSipEnabled(false) disables the software virtual keyboard from popping up automatically. You can use the "Q_OS_ANDROID" preprocessor directive to avoid modifying behavior on your other target platforms:
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
#ifdef Q_OS_ANDROID
a.setAutoSipEnabled(false);
#endif
(...)
}
You can programmatically display or hide the virtual keyboard using this code:
QInputMethod* input;
input = QGuiApplication::inputMethod();
if(input->isVisible())
{
input->setVisible(false);
}
else
{
input->setVisible(true);
}
Here is another approach using Qt.inputMethod - hide virtual keyboard immediately when it get visible for example by adding visibleChanged handler in qml root item
Component.onCompleted: {
Qt.inputMethod.visibleChanged.connect(function () {
if (Qt.inputMethod.visible)
Qt.inputMethod.hide()
})
}
With this approach it is also possible to add user interface setting to choose what keyboard to use (system or built in app).
Update:
Have figured out that sometimes there can be a blink of system virtual keyboard before hiding, especially when tapping to text input field to much. Handling visibleChanged in C++ don't solve this issue but seems to make it less frequently.
QObject::connect(QGuiApplication::inputMethod(), &QInputMethod::visibleChanged, [] {
QGuiApplication::inputMethod()->hide();
});
I am having issues with a PhoneGap application that I'm working in. My app has lots of forms, since the objective of the app is mostly to provide a nice user interface to a database. However, whenever the user tries to edit an input field that is close to the bottom, the Android keyboard will pop up and cover the field, so that the user cannot see what he/she is writing.
Do you know if there is a workaround for this? Has anyone come across this issue on their apps?
What you can do in this case (what I did when I had this problem...): add on-focus event on fields, and scroll up document. So you will see input field on the top of page :)
I agree with Paulius, for Android I found this to be the cleanest solution.
I know this is an old question but I will share my solution for other people if any body is still facing this issue.
// fix keyboard hiding focused input texts
// using native keyboard plugin and move.min.js
// https://github.com/vitohe/ionic-plugins-keyboard/tree/f94842fec1bacf72107083d2e44735e417e8439d
// http://visionmedia.github.io/move.js/
// not tested on iOS so implementation is for Android only
if ($cordovaDevice.getPlatform() === "Android") {
// device is running Android
// attach showkeyboard event listener
// which is triggered when the native keyboard is opened
window.addEventListener('native.showkeyboard', keyboardShowHandler);
// native.showkeyboard callback
// e contains keyboard height
function keyboardShowHandler(e) {
// get viewport height
var viewportHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
// get the maximum allowed height without the need to scroll the page up/down
var scrollLimit = viewportHeight - (document.activeElement.offsetHeight + document.activeElement.offsetTop);
// if the keyboard height is bigger than the maximum allowed height
if (e.keyboardHeight > scrollLimit) {
// calculate the Y distance
var scrollYDistance = document.activeElement.offsetHeight + (e.keyboardHeight - scrollLimit);
// animate using move.min.js (CSS3 animations)
move(document.body).to(0, -scrollYDistance).duration('.2s').ease('in-out').end();
}
}
window.addEventListener('native.hidekeyboard', keyboardHideHandler);
// native.hidekeyboard callback
function keyboardHideHandler() {
// remove focus from activeElement
// which is naturally an input since the nativekeyboard is hiding
document.activeElement.blur();
// animate using move.min.js (CSS3 animations)
move(document.body).to(0, 0).duration('.2s').ease('in-out').end();
}
}
The end result is unbelievably smooth.
I am using titanium appecelerator to build an app in both ios and android.
I use the following code to create a tab group and add a tab to it.
var localTabGroup = Ti.UI.createTabGroup();
var planTab = Ti.UI.createTab({
title : NYC.Common.StringConstant.TAB_TITLE_PLAN,
icon : NYC.Common.ResourcePathConstant.IMG_TAB_PLAN,
window : planTabWin
});
localTabGroup.open();
And call the following function to create a window and add it to the tab
addWindowToTabGroup : function(window) {
tabGroup.activeTab.open(window, {
animated : true
});
},
Now, I often have to remove window from the stack of the tab ( eg: on android back button or ios navigation bar back)
Till now, I use window.close() to remove the window from the stack . But, it always shows warnings like.
[ERROR][TiBaseActivity( 378)] (main) [3320,4640528] Layout cleanup.
[WARN][InputManagerService( 62)] Window already focused, ignoring focus gain of: com.android.internal.view.IInputMethodClient$Stub$Proxy#406e4258
I was just wondering if I am following the correct approach? Or is there a better way to remove a window from the tab?
Thanks.
Tabs behave a lot differently on iOS and Android, On Android, the tab does not maintain a stack of windows. Calling open opens a new, heavyweight window, which by default covers the tab group entirely.This is very different from iOS, but it is for Android applications. Users always use the Back button to close the window and return to the tab group.
This may be happening because you are trying to remove the window even though natively Android already removes it. Check out the Android Implementation Notes of the docs here
To completely eliminate this problem, I would just open up a modal window without using the TabGroup, this would be more cross platform:
addWindowToTabGroup : function(window) {
window.open({
modal : true,
animated : true
});
}
This will open a modal window which behaves the same on both platforms, and can be handled easily by the native back button functionality.