So I was implementing a simple image viewer and wanted to implement pinch to zoom inside a Flickable. After looking at several example, I came across this and thought I could do something similar. However, when I tested the code (Qt 5.13 on Android) it doesn't work as expected. The problem is that when the image is zoomed, the PinchArea stops working and the user is unable to zoom out while the Flickable still allows to "flick" the image (at that zoom level). Here is the code, which I couldn't find any mistake with:
import QtQuick 2.7
import QtQuick.Controls 2.0
import QtQuick.Layouts 1.0
ApplicationWindow {
visible: true
width: 640
height: 480
title: qsTr("Flickable Image Zoom Example")
Page {
id: imagePage
property string imgUrl: "qrc:/messi.jpg"
property string strHpTitle: ""
property string strThumbnailUrl: ""
anchors.fill: parent
Flickable {
id: imageFlickable
anchors.fill: parent
contentWidth: imageContainer.width; contentHeight: imageContainer.height
clip: true
onHeightChanged: if (imagePreview.status === Image.Ready) imagePreview.fitToScreen();
Item {
id: imageContainer
width: Math.max(imagePreview.width * imagePreview.scale, imageFlickable.width)
height: Math.max(imagePreview.height * imagePreview.scale, imageFlickable.height)
Image {
id: imagePreview
property real prevScale
function fitToScreen() {
scale = Math.min(imageFlickable.width / width, imageFlickable.height / height, 1)
pinchArea.minScale = scale
prevScale = scale
}
anchors.centerIn: parent
fillMode: Image.PreserveAspectFit
cache: false
asynchronous: true
source: imagePage.imgUrl
sourceSize.height: 1000;
smooth: !imageFlickable.moving
onStatusChanged: {
if (status == Image.Ready) {
fitToScreen()
}
}
onScaleChanged: {
if ((width * scale) > imageFlickable.width) {
var xoff = (imageFlickable.width / 2 + imageFlickable.contentX) * scale / prevScale;
imageFlickable.contentX = xoff - imageFlickable.width / 2
}
if ((height * scale) > imageFlickable.height) {
var yoff = (imageFlickable.height / 2 + imageFlickable.contentY) * scale / prevScale;
imageFlickable.contentY = yoff - imageFlickable.height / 2
}
prevScale = scale
}
}
}
PinchArea {
id: pinchArea
property real minScale: 1.0
property real maxScale: 3.0
anchors.fill: parent
pinch.target: imagePreview
pinch.minimumScale: minScale
pinch.maximumScale: maxScale
pinch.dragAxis: Pinch.XandYAxis
onPinchFinished: {
imageFlickable.returnToBounds()
}
}
}
}
}
Ok so I found the solution to this problem. The issue (I think) is due to a bug. For some strange reason, the Flickable in my code steals touch events from the PinchArea so the pinching touch events are ignored and passed onto the Flickable. In order to solve the problem, I found a workaround which consists in creating a MouseArea inside the PinchArea, this allows the PinchArea to receive the touch events so that the Flickable doesn't steal them. So with the workaround my code has the following structure:
Flickable{
PinchArea{
MouseArea{ anchors.fill: parent} // this is the workaround
}
Item{}
}
I have reported the bug here and uploaded a small video showing the cause.
Related
I am using Camera2 API for my app to communicate to camera device of android device, Then I'm trying to make a touch to focus feature unfortunately the focus area is not accurate. I was tried different work around and suggestions in google but I can't get the right focus area when I touch the screen. Is there anyone here make this feature working smoothly using kotlin?
here's the snippet of my code the calculate the focus area
val sensorArraySize: Rect? =
mCameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE)
var y = focus_point_x
var x = focus_point_y
if (sensorArraySize != null) {
y = (focus_point_x.toFloat() / previewSize.width * sensorArraySize.height()
.toFloat()).toInt()
x = (focus_point_y.toFloat() / previewSize.height * sensorArraySize.width()
.toFloat()).toInt()
}
val halfTouchLength = 150
focusArea = MeteringRectangle(
Math.max(x - halfTouchLength, 0),
Math.max(y - halfTouchLength, 0),
halfTouchLength * 2,
halfTouchLength * 2,
MeteringRectangle.METERING_WEIGHT_MAX - 1
)
I'm designing a video call screen where i have to manage selfViewContainer between hole screen.
On this screen, I already managed most of the parts for horizontal alignments but for vertical it's not working properly for me in devices with a notch.
I want to make minimum padding of 50 from the top while dragging
Here is a code for orange container which is draggable
Widget _selfView() {
return Positioned(
top: _selfViewTop,
left: _selfViewLeft,
child: Draggable(
feedback: _draggableView(),
childWhenDragging: Container(),
child: _draggableView(),
onDragEnd: (dragDetail) {
var screenWidth = AspectSize.getScreenWidth(context: context);
var screenHeight = AspectSize.getScreenHeight(context: context);
_selfViewLeft = dragDetail.offset.dx;
_selfViewTop = dragDetail.offset.dy;
if (_selfViewLeft < (screenWidth / 2)) {
_selfViewLeft = 16.0;
} else if (_selfViewLeft > (screenWidth / 2)) {
_selfViewLeft = (screenWidth) - (_selfViewWidth + 16);
}
if (_selfViewLeft < 1.0) {
_selfViewLeft = 16.0;
} else if ((_selfViewLeft + _selfViewWidth) > screenWidth) {
_selfViewLeft = (screenWidth) - (_selfViewWidth + 16);
}
if (_selfViewTop < 1.0) {
_selfViewTop = 50;
} else if ((_selfViewTop + _selfViewHeight) > screenHeight) {
_selfViewTop = (screenHeight) - (_selfViewWidth + 50);
}
setState(() {});
},
),
);
}
For this UI i used, the Stack widgets for managing containers the orange container is used to show the caller's picture, and the blue container will show pictures of another person.
Please give me some solution for this.
Thank you.
You can use
MediaQuery.of(context).viewPadding
and check whether the padding is greater than zero ,then you will need a safe area as there is a notch
.
And the best way to handle notch behaviour is to use the flutter_device_type package.
the following code in the above package can determine the notch:
if( Device.get().hasNotch ){
//Do some notch business
}
Thank You.
Here is a minimal version of a code revealing the problem which is:
Moving the racket when playing the game on the Desktop kit (Windows) doesn't affect the speed of ball's movement but when run on an Android device, moving the racket affects the speed of ball's movement as though their movements have been tied together.
What solution is there for that, please?
main.qml:
import QtQuick 2.9
import QtQuick.Window 2.2
Window {
visible: true
width: 720
height: 620
title: qsTr("Movement Test")
Rectangle {
id: table
anchors.fill: parent
color: "gray"
Rectangle {
id: ball
property double xincrement: Math.random() + 0.5
property double yincrement: Math.random() + 0.5
width: 15
height: width
radius: width / 2
color: "white"
x: 300; y: 300
}
Racket {
id: myRacket
x: table.width - 50
y: table.height/3
color: "blue"
}
Timer {
interval: 5; repeat: true; running: true
function collision() {
if((ball.x + ball.width >= myRacket.x &&
ball.x < myRacket.x + myRacket.width) &&
(ball.y + ball.height >= myRacket.y &&
ball.y <= myRacket.y + myRacket.height))
return true
return false
}
onTriggered: {
if(ball.x + ball.width >= table.width)
running = false
else if(ball.x <= 0)
ball.xincrement *= -1
else if (collision())
ball.xincrement *= -1
ball.x = ball.x + (ball.xincrement * 1.5);
ball.y = ball.y + (ball.yincrement * 1.5);
if(ball.y <= 0 || ball.y + ball.height >= table.height)
ball.yincrement *= -1
}
}
}
}
Racket.qml:
import QtQuick 2.9
Rectangle {
id: root
width: 15; height: 65
property int oldY: y
property bool yUwards: false
property bool yDwards: false
onYChanged: {
if(y > oldY) yDwards = true
else if (y < oldY) yUwards = true
oldY = y
}
MouseArea {
anchors.fill: parent
anchors.margins: -root.height
drag.target: root
focus: true
hoverEnabled: true
pressAndHoldInterval: 0
drag.axis: Drag.YAxis
drag.minimumY: table.y
drag.maximumY: table.height - root.height - 10
}
}
Qt tries to render every ~16-17ms. If you set your timer to 5ms, it will try to trigger it 3 - 4 times per frame.
Other things that are happening might hinder it from keeping that pace. If the device is less powerfull this effect might be more visible than on other devices.
To see, whether the Timer achievs the set rate, you can print the current ms part of the time with:
console.log(Qt.formatTime(new Date(), "zzz"))
The logged values shall be 5 appart, if the Timer achives full speed. It will be different from 5 if it doesn't.
The easiest way would be to set a target location where the ball will move to, and animate that movement (using a Animation-Type). The Animation shall take care, that the movement speed will be kept, even in cases where the frame rate might drop.
If you want to do it manually, instead of using the timer, you should use the onAfterRendering-slot (I think of Window). This will be triggered when ever something has moved on the screen and triggered rendering. This is also the ideal moment to check collisions.
You then need to calculate the new position depnding on its velocity, the current position and the elapsed time. Latter you can get either from the JS Date()-object, or you expose it somehow form C++ using a QElapsedTimer
A picture speaks better than words (black rectangle in red ApplicationWindow):
Notice the red unfilled areas on top and right side. The right side red color may be hard to notice but it is there! I want the rectangle which I have colored in black to fill the entire Application Window. See the code:
main.qml
import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
ApplicationWindow {
id: window
visible: true
/* Developing mobile apps you don’t need to set width
and height, because the ApplicationWindow always grabs
the total available space.
*/
//width: 640
//height: 480
color: "#ff0000" // Red color
/* For some reasons i want this Rectangle here
* and it MUST fill the entire window but I notice
* a pixel or two line on top and right of the
* screen.
*/
Rectangle {
id: page
width: window.width; height: window.height
//anchors.fill: parent // same output
color: "#000000" // Black color
}
}
main.cpp
#include <QGuiApplication>
#include <QQmlApplicationEngine>
int main(int argc, char *argv[])
{
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
engine.load(QUrl(QLatin1String("qrc:/main.qml")));
if (engine.rootObjects().isEmpty())
return -1;
return app.exec();
}
I don't know what I'm missing here :(
Please share a solution/workaround to this problem.
I also tried this but still the same output:
import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import QtQuick.Window 2.2
Window {
id: window
visible: true
height: Screen.height
width: Screen.width
/* Developing mobile apps you don’t need to set width
and height, because the ApplicationWindow always grabs
the total available space.
*/
//width: 640
//height: 480
color: "#ff0000" // Red color
/* For some reasons i want this Rectangle here
* and it MUST fill the entire window but I notice
* a pixel or two line on top and right of the
* screen.
*/
Rectangle {
id: page
width: window.width; height: window.height
//anchors.fill: parent // same output
color: "#000000" // Black color
}
}
I noticed that commenting following line in main.cpp resolves the issue BUT now all the widgets which I want to show in the UI look really small..! They look fine in the small-screen devices while small in large-screen devices. :(
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
Any solution for this?
EDIT:
This is a confirmed bug which has been reported HERE since almost an year! . If you are facing the same issue then please login to https://bugreports.qt.io/ and Vote THIS bug.
Well, you could use your "own" dp calculation.
main.cpp
int density = 0;
float logicalDensity = 0;
float yDpi = 0; float xDpi = 0;
#if defined(ANDROID)
QAndroidJniObject qtActivity = QAndroidJniObject::callStaticObjectMethod("org/qtproject/qt5/android/QtNative", "activity", "()Landroid/app/Activity;");
QAndroidJniObject resources = qtActivity.callObjectMethod("getResources", "()Landroid/content/res/Resources;");
QAndroidJniObject displayMetrics = resources.callObjectMethod("getDisplayMetrics", "()Landroid/util/DisplayMetrics;");
density = displayMetrics.getField<int>("densityDpi");
logicalDensity = displayMetrics.getField<float>("density");
yDpi = displayMetrics.getField<float>("ydpi");
xDpi = displayMetrics.getField<float>("xdpi");
qDebug() << "Native Android Call =>>> | Logical Density: " << logicalDensity << " | DensityDPI: " << density << " | " << "++++++++++++++++++++++++";
#endif
[...]
// Set Android pixel data for QML context
engine.rootContext()->setContextProperty("densityData", density);
engine.rootContext()->setContextProperty("logicalDensityData",logicalDensity);
engine.rootContext()->setContextProperty("xDpiData",xDpi);
engine.rootContext()->setContextProperty("yDpiData",yDpi);
For your ApplicationWindow component add this:
main.qml
Component.onCompleted: {
Units.pixelDensity = Qt.binding(function() {
if (Qt.platform.os === "android") {
return densityData / 25.4; // densityData is per inch but we need per mm
}
return Screen.pixelDensity
});
function calculateDiagonal() {
if (Qt.platform.os === "android") {
return Math.sqrt(Math.pow(Screen.width, 2) +
Math.pow(Screen.height, 2)) / densityData;
}
return Math.sqrt(Math.pow(Screen.width, 2) +
Math.pow(Screen.height, 2)) / (Screen.pixelDensity * 25.4);
}
Units.multiplier = Qt.binding(function() {
var diagonal = calculateDiagonal();
Device.diagonal = diagonal;
var baseMultiplier = 1;
if (diagonal >= 3.5 && diagonal < 5.1) { //iPhone 1st generation to phablet
return 0.8;
} else if (diagonal >= 5.1 && diagonal < 6.5) {
return 1;
} else if (diagonal >= 6.5 && diagonal < 15.1) {
return baseMultiplier;
} else if (diagonal >= 15.1 && diagonal < 29) {
return 1.4 * baseMultiplier;
} else if (diagonal >= 29 && diagonal < 92) {
return 1.4 * baseMultiplier;
} else {
return 1.4 * baseMultiplier;
}
});
Device.type = Qt.binding(function () {
var diagonal = calculateDiagonal();
Device.diagonal = diagonal;
if (diagonal >= 3.5 && diagonal < 5) { //iPhone 1st generation to phablet
return Device.phone;
} else if (diagonal >= 5 && diagonal < 7.2) {
return Device.phone;
} else if (diagonal >= 7.2 && diagonal < 15.1) {
return Device.tablet;
} else if (diagonal >= 15.1 && diagonal < 29) {
return Device.desktop;
} else if (diagonal >= 29 && diagonal < 92) {
return Device.tv;
} else {
return Device.unknown;
}
});
// Nasty hack because singletons cannot import the module they were declared in, so
// the grid unit cannot be defined in either Device or Units, because it requires both.
// See https://bugreports.qt.io/browse/QTBUG-39703
Units.gridUnit = Qt.binding(function() {
return Device.type === Device.phone || Device.type === Device.phablet
? Units.dp(48) : Device.type == Device.tablet ? Units.dp(56) : Units.dp(64)
});
}
Units.qml
Object {
id: units
/*!
\internal
This holds the pixel density used for converting millimeters into pixels. This is the exact
value from \l Screen:pixelDensity, but that property only works from within a \l Window type,
so this is hardcoded here and we update it from within \l ApplicationWindow
*/
property real pixelDensity: 4.46
property real multiplier: 1.4 //default multiplier, but can be changed by user
/*!
This is the standard function to use for accessing device-independent pixels. You should use
this anywhere you need to refer to distances on the screen.
*/
function dp(number) {
return Math.round(number*((pixelDensity*25.4)/160)*multiplier);
}
function gu(number) {
return number * gridUnit
}
property int gridUnit: dp(64)
}
Device.qml
import QtQuick 2.0
pragma Singleton
/*!
\qmltype Device
\inqmlmodule Material 0.1
\brief A singleton that provides information about the current device.
*/
Object {
id: device
//some kind of enum, by screen size
property int type: desktop
property int diagonal: -1
readonly property int phone: 0
readonly property int phablet: 1
readonly property int tablet: 2
readonly property int desktop: 3
readonly property int tv: 4
readonly property int unknown: 5 //it's either bigger than tv or smaller than phone
readonly property string name: {
switch (type) {
case 0:
return "phone";
case 1:
return "phablet";
case 2:
return "tablet";
case 3:
return "computer";
case 4:
return "TV";
case 5:
return "device";
}
}
readonly property string iconName: {
switch (type) {
case 0:
return "hardware/smartphone";
case 1:
return "hardware/tablet";
case 2:
return "hardware/tablet";
case 3:
return "hardware/desktop_windows";
case 4:
return "hardware/tv";
case 5:
return "hardware/computer";
}
}
readonly property bool isMobile: type == phone || type == phablet || type == tablet
}
Units and Device components are taken from here qml-material project "https://github.com/papyros/qml-material"
After adding this you should be able to wrap every pixel statement into width: Units.dp(30)
Just trying to get clarification on this issue. Is the scaling of fonts working for android using this attribute? Its working for iOS, I know, but it doesn't seem to want to play nice for android.
I'm trying to make a label that has a string of varying size in it. Here is a sample of the code that I'm using:
var name = Titanium.UI.createLabel({ text:response.name, color:'#000', minimumFontSize:10, font:{fontSize:24,fontFamily:'Helvetica Neue'}, width:120, height:45, top:0, left:0, right:2, wordWrap:false, textAlign:'left' });
Use 'sp' as measurement unit for font and 'dp' for sizes (example: fontSize: '24sp'). You can read more about density-specific resources on Android here.
If you are asking about whether 'minimumFontSize' on a label works on Android to automatically scale the size of the text to fit the label on a single line, according to the latest documentation for Titanium 3.X here:
http://docs.appcelerator.com/titanium/latest/#!/api/Titanium.UI.Label-property-minimumFontSize
no, it only works on iPhone and iPad
This might be a bit off topic, but this is the code we use to make sure some text fits a label. You call the function in a postLayout event handler or at some other moment when the dimensions of the label and the screen are known.
function fitTextInLabel(label,options)
{
/*
* Make the given text fit in the label by, well, just trying. Run this when the layout is complete
* IE in the onPostlayout of the view or the label. When using the cache-feature don't forget to
* check for orientation in creating the cache key: otherwise the font size will not be recalculated for the other orientation
* This is an alloy function: it requires underscore.js But rewriting it for plain titanium is not a big deal.
* Spin in het Web - www.spininhetweb.nl - Contact us for questions. Yes we build apps.
*
* Label: the Ti.UI.Label to fit the text in
* Options: an options object:
* text: the text to fit. When not given we will use the current text of the label. Use a lorum ipsum that's big enough.
* fitWidth: which width to fit the text in. Either the fixed width of the label ("current") or that of the parent ("parent"). When
* width is Ti.UI.SIZE use "parent". Default: current.
* fitHeight: which height to fit the text in. "current" or "parent". Default: current
* marginVertical: space to keep vertically. Will use marginVertical / 2 for top and bottom. Default: 0
* marginHorizontal: space to keep horizontally. Will use marginHorizontal / 2 for left and right. Default: 0
* cacheKey: string. When given, use caching. We will save the found fontsize as a persistant property. When called again with the same key
* we will not calculute, but just set the fontsize. The cache is only cleared when the user removes the app or its data
* We add the device orientation to the cacheKey, so we automatically differentiate between setting for portrait and landscape
* applyTo: array of labels. When given, we will set the same fontsize on the given labels.
* callback: function. When given, we will call this after setting the fontsize on the label. The prototype for the callback function is:
* fn(Ti.UI.Label lbl, int newFontSize)
*
* RETURNS boolean. False on some error, true when everything started out okay.
*
* This function runs on the event engine so it is basically async. After calling it, the font will not be changed until the callback runs
*/
//defaults
var o =
{
text: false,
fitWidth: "current",
fitHeight: "current",
marginVertical: 0,
marginHorizontal: 0,
cacheKey: false,
deleteCache: false, //special for development: set to true to recache without using the old value
callback: false,
applyTo: []
};
if (typeof(options) == "object")
{
_.each(options, function(v,k)
{
o[k] = v;
});
}
//o now contains all the chosen options plus defaults for the rest
//add orientation to the cachekey
if (o.cacheKey)
{
o.cacheKey = o.cacheKey + "_" + Ti.Gesture.orientation; //int
}
//log("*** fitTextInLabel label " + label.id + " tekst " + (o.text ? o.text : "(origineel)"),o);
var font = _.clone(label.font);
//cache?
if (o.cacheKey && (! o.deleteCache))
{
var cached = Ti.App.Properties.getInt(o.cacheKey,0);
if (cached)
{
font.fontSize = cached;
label.setFont(font);
//log("*** Cached op key " + o.cacheKey + " fontSize: " + cached);
_.each(o.applyTo,function(otherlabel)
{
//just set the font
var f = otherlabel.font;
f.fontSize = cached;
otherlabel.setFont(f);
});
//callback
if (o.callback)
{
o.callback(label,cached);
}
return; //done
}
}
//find the fontsize that fits in the label
//we use a different label outside of the view, to check it
var labelsize = label.getSize();
var parentsize = label.parent.getSize();
//which width and height to use?
var maxw = (o.fitWidth == "parent" ? parentsize : labelsize).width - (o.marginHorizontal / 2);
var maxh = (o.fitHeight == "parent" ? parentsize : labelsize).height - (o.marginVertical / 2);
//log("*** Moet passen in " + maxw + " X " + maxh);
font.fontSize = 40; //beginnen we mee, kan hoger en lager
var starting = true; //voor als we omhoog moeten
//create the test label in the parent container, using a postLayout callback for checking the fit
var testl = Ti.UI.createLabel({
text: (o.text ? o.text : label.getText()),
width: label.wordWrap ? maxw : Ti.UI.SIZE, //when wrapping, use a fixed with, otherwise just see how big it becomes
height: Ti.UI.SIZE, //we want to measure the height after setting the font size
wordWrap: label.wordWrap, //copy the wordWrap from the real label
font: font,
top: -5000 //somewhere out of view please (does this create scrollbars?)
});
var done = false;
var onPostLayout =
function()
{
//called when the test label is relayout because of the font change, so let's see how it all fits now
if (done)
{
return;
}
var lsize = testl.getSize();
//log("*** Proberen " + font.fontSize,lsize);
//We declare it a fit when the font becomes to small, fits inside the height of a wrapping label
//or fits inside the height AND width of a nonwrapping label
if (font.fontSize == 5 || (lsize.height <= maxh && (label.wordWrap || lsize.width < maxw)))
{
//it fits!
//did we startup with a too small font?
if (starting)
{
//the fontsize we started with fits. So let's try something bigger
font.fontSize += 10;
testl.setFont(font);
}
else
{
//we found it: it fits the space or is so small we stop trying
//log("*** Past!");
done = true; //stop the postLayout eventloop
label.setFont(font); //set the font
testl.parent.remove(testl);
testl = null; //garbace collect
if (o.cacheKey)
{
//let's cache this value
//log("*** Cachen naar " + o.cacheKey + ": " + font.fontSize);
Ti.App.Properties.setInt(o.cacheKey,font.fontSize);
}
//set the font for the applyTo array
_.each(o.applyTo,function(otherlabel)
{
//just set the font
var f = otherlabel.font;
f.fontSize = font.fontSize;
otherlabel.setFont(f);
});
//and callback
if (o.callback)
{
o.callback(label,font.fontSize);
}
}
}
else
{
//no fit yet. Let's try a pixel smaller
font.fontSize--;
testl.setFont(font); //this will fire a new postLayout event, running this function again
starting = false; //we are no longer starting up. When we find a fit, it's what we'll use
}
};
//let's go
testl.addEventListener("postlayout",onPostLayout);
label.parent.add(testl);
return true;
}
Hope this helps someone.
This parity-feature (TIMOB-1618) is currently in review and will be available in Titanium SDK 6.1.0 and later. The parity-review took some time, but you can already grab the changes from this Pull Request and apply them to your SDK today.