ListView.onRemove animation vs childrenRect.height - qml

I noticed strange behaviour in ListView childrenRect.height when I am removing elements from its model with ListView.onRemove animation. When I remove all elements except the last one, childrenRect.height property is wrong, but contentHeight property is ok. Removing ListView.onRemove animation results the problem disappears. Why childrenRect.height is wrong?
Using this code You will see that after You remove all elements except the last one, You cannot click in some area.
import QtQuick 2.0
import QtQuick.Controls 1.0
MouseArea
{
id: container
height: 400; width: 600
Rectangle { id: point; visible: false; width: 6; height: 6; radius: 3; color: "black" }
onPressed: { point.visible = true; point.x = mouse.x - 3; point.y = mouse.y - 3; }
ListModel {
id: listModel
ListElement { lorem: "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Proin nibh augue, suscipit a, scelerisque sed, lacinia in, mi. Cras vel lorem." }
ListElement { lorem: "Etiam pellentesque aliquet tellus. Phasellus pharetra nulla ac diam." }
ListElement { lorem: "Quisque semper justo at risus. Donec venenatis, turpis vel hendrerit interdum, dui ligula ultricies purus, sed posuere libero dui id orci." }
ListElement { lorem: "Nam congue, pede vitae dapibus aliquet, elit magna vulputate arcu, vel tempus metus leo non est." }
}
ListView {
id: messageListView
model: listModel
anchors.top: container.top; anchors.left: container.left; anchors.right: container.right
height: childrenRect.height
onCountChanged: console.log("count: " + count)
onHeightChanged: console.log("height: " + height) // sometimes wrong
onContentHeightChanged: console.log("contentHeight: " + contentHeight) // rather ok
delegate: Item {
id: messageItem
anchors.left: parent.left; anchors.right: parent.right
height: Math.max(50, messageText.height + 12)
Rectangle { anchors.fill: parent; anchors.margins: 1; opacity: 0.75; color: "green"; radius: 5 }
Text {
id: messageText
anchors.verticalCenter: parent.verticalCenter; anchors.left: parent.left; anchors.right: removeButton.left; anchors.margins: 10
text: lorem
color: "white"; horizontalAlignment: Text.AlignHCenter; wrapMode: Text.WordWrap
font.pixelSize: 16; font.weight: Font.Bold
style: Text.Outline; styleColor: "black"
maximumLineCount: 6; elide: Text.ElideRight
}
Button {
id: removeButton
enabled: (index !== -1) && (messageItem.opacity === 1.0)
anchors.right: parent.right; anchors.margins: 5
anchors.verticalCenter: parent.verticalCenter; implicitHeight: 40; implicitWidth: 40
onClicked: {
console.log("remove: " + index);
listModel.remove(index);
}
}
ListView.onRemove: SequentialAnimation {
PropertyAction { target: messageItem; property: "ListView.delayRemove"; value: true }
/// PROBLEM BEGIN
NumberAnimation { target: messageItem; properties: "opacity"; from: 1.0; to: 0.0; duration: 500 }
/// PROBLEM END
PropertyAction { target: messageItem; property: "ListView.delayRemove"; value: false }
}
}
}
}

Because the ListView.childrenRect may be dynamically changed by itself. For example, try to drag the view when your example code launched. As all four delegates disappeared from the top of the view, childrenRect.height increased (even if you comment out the NumberAnimation).
That means ListView uses it's childrenRect to something internally, like drag animation, thus this property is not reliable to ListView users.
Use contentHeight instead of childrenRect.height so you can always obtain the correct value . Or use contentItem.childrenRect.height that does the same thing according to Flickable.

Related

Removing element after sliding up animation and contrary, mounting the element before sliding down in Vue 3.X

The vue-slide-up-down library working with pre-mounted elements only.
Sliding down case: It will not mount the element before animation starts
Sliding up case: It will not unmount the element after animation ends
It just manipulating with the element's height and hidden attribute.
Now what if I don't what the target element be mounted when it does not displaying?
Sliding down case: Before animation starts, I want the element be mounted
Sliding up case: Once animation complete, I want the element be unmounted, not just hidden
If we try
<slide-up-down
v-if="active"
:active="active"
>
Only show this if "activeā€¯ is true
</slide-up-down>
it will not be the animation because:
Sliding down case: animation starts before element mounted
Sliding up case: the element will be unmounted before animation starts
You need a separate variable controlling whether the component is rendered (I named it isRendered below).
And a setter + getter computed (named rendered below) which sets both active and isRendered to current value, but in different order:
when setting to true: turn isRendered on first, then set active to true in $nextTick, so the animation is played
when setting to false: turn active to false first, wait for animation to finish and then set isRendered to false.
Vue2 demo:
Vue.component("slide-up-down", VueSlideUpDown)
new Vue({
el: "#app",
data: () => ({
isRendered: false,
active: false,
duration: 500
}),
computed: {
rendered: {
get() {
return this.isRendered
},
set(val) {
if (val) {
this.isRendered = val
this.$nextTick(() => {
this.active = val
})
} else {
this.active = val
setTimeout(() => {
this.isRendered = val
}, this.duration)
}
}
}
}
})
.wrap {
padding: 1rem;
border: 1px solid #ccc;
background-color: #f5f5f5;
}
button {
margin-bottom: 1rem;
}
<script src="https://unpkg.com/vue#2.7.14/dist/vue.min.js"></script>
<script src="https://unpkg.com/vue-slide-up-down#2.0.0/dist/vue-slide-up-down.umd.js"></script>
<div id="app">
<button #click="rendered = !rendered">Toggle</button>
<slide-up-down v-if="isRendered" v-bind="{ active, duration }">
<div class="wrap">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aut, consequatur
ut magnam, quos possimus, velit quam mollitia voluptate adipisci
reiciendis sapiente accusamus ullam ab voluptatem laborum non! Accusamus,
ullam, voluptatum.
</div>
</slide-up-down>
</div>
Vue3 demo:
const { createApp, reactive, computed, nextTick, toRefs } = Vue
const app = createApp({
setup() {
const state = reactive({
isRendered: false,
active: false,
duration: 500,
rendered: computed({
get() { return state.isRendered },
set(val) {
if (val) {
state.isRendered = val
nextTick(() => {
state.active = val
})
} else {
state.active = val
setTimeout(() => {
state.isRendered = val
}, state.duration)
}
}
})
})
return toRefs(state)
}
})
app.component("slide-up-down", Vue3SlideUpDown)
app.mount('#app')
.wrap {
padding: 1rem;
border: 1px solid #ccc;
background-color: #f5f5f5;
}
button {
margin-bottom: 1rem;
}
<script src="https://unpkg.com/vue#3.2.47/dist/vue.global.prod.js"></script>
<script src="https://unpkg.com/vue3-slide-up-down#1.2.5/dist/vue3-slide-up-down.umd.js"></script>
<div id="app">
<button #click="rendered = !rendered">Toggle</button>
<slide-up-down v-if="isRendered" v-model="active" v-bind="{ duration }">
<div class="wrap">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aut, consequatur
ut magnam, quos possimus, velit quam mollitia voluptate adipisci
reiciendis sapiente accusamus ullam ab voluptatem laborum non! Accusamus,
ullam, voluptatum.
</div>
</slide-up-down>
</div>
If you're gonna do this multiple times, you might want to extract it as a stand-alone component. Usage example:
<conditional-slide :rendered="condition" :duration="1000">
<div>content you want rendered based on `this.condition` (boolean)</div>
</conditional-slide>
Since the change now comes from the rendered prop, you can move the computed setter code into a watch:
In Vue 2:
<template>
<div>
<slide-up-down
v-if="isRendered"
v-bind="{ active, duration }"
>
<slot />
</slide-up-down>
</div>
</template>
<script>
import SlideUpDown from 'vue-slide-up-down'
export default {
components: { SlideUpDown },
props: {
rendered: {
type: Boolean,
default: false
},
duration: {
type: Number,
default: 500
}
},
data: () => ({
active: false,
isRendered: false
}),
watch: {
rendered: {
handler(val) {
if (val) {
this.isRendered = val
this.$nextTick(() => {
this.active = val
})
} else {
this.active = val
setTimeout(() => {
this.isRendered = val
}, this.duration)
}
},
immediate: true
}
}
}
</script>
Vue 3 example. Sandbox here.
feel free to add more props and pass them down to the inner <slide-up-down />.
note the vue2 and vue3 versions use different forks of the same plugin (pay attention to the imports) and have slightly different template syntax.
If I got you right, there is the solution.
Use v-show instead of v-if
I have used the original example from the vue-slide-up-down site to demonstrate the behavior.
If you use 'v-show' and also toggle the v-slide- component together, then you have the slide effect when your hidden content will be show again.
Vue.component("slide-up-down", VueSlideUpDown);
var vm = new Vue({
el: '#app',
data: () => ({
active: false,
show: true,
contentLength: 2,
showOptions: false,
useHidden: true,
timing: "",
duration: 500
}),
methods: {
toggle() {
this.active = !this.active;
},
toggleShow() {
this.show = !this.show;
this.active = this.show;
}
}
})
* {
margin: 0;
padding: 0;
font-size: 1em;
box-sizing: border-box;
}
body {
font-family: sans-serif;
padding: 20px;
line-height: 1.5em;
}
#app {
margin: 0 auto;
max-width: 700px;
}
.Button {
display: block;
width: 100%;
margin-top: 10px;
margin-left: auto;
margin-right: auto;
padding: 1em;
border: 0;
background-color: gray;
cursor: pointer;
}
.Button.small {
width: auto;
font-size: 0.8em;
padding: 0.5em 1em;
background-color: lightgray;
margin: 0;
}
.AccordionWrapper {
background-color: darkred;
}
.AccordionWrapper.easeInOutCirc {
transition-timing-function: cubic-bezier(0.785, 0.135, 0.15, 0.86);
}
.AccordionWrapper.customTiming {
transition-timing-function: cubic-bezier(0.195, 1.65, 0.435, -0.6);
}
.Accordion {
display: block;
width: 96%;
margin-left: auto;
margin-right: auto;
padding: 1em;
border: 0;
background-color: lightgray;
}
.Accordion p,
.Accordion hr {
margin-bottom: 10px;
}
.Accordion p:last-child {
margin-bottom: 0;
}
<div id="app">
<div v-show="show">
<button #click="toggle" class="Button">Toggle Content</button>
<slide-up-down class="AccordionWrapper" :active="active" :use-hidden="this.useHidden" :class="this.timing" :duration="this.duration">
<div class="Accordion">
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aut, consequatur ut magnam, quos possimus, velit quam mollitia voluptate adipisci reiciendis sapiente accusamus ullam ab voluptatem laborum non! Accusamus, ullam, voluptatum.</p>
</div>
</slide-up-down>
</div>
<button class="Button" #click="toggleShow">Toggle if elements exists</button>
</div>
<script src="https://unpkg.com/vue#2.7.14/dist/vue.js"></script>
<script src="https://unpkg.com/vue-slide-up-down#2.0.0/dist/vue-slide-up-down.umd.js"></script>

QML ScrollView does not scroll with TextArea

I'm trying to make a widget that has an image at the bottom, and text that populates from bottom-to-top. Here are three ASCII-art examples of the widget in action:
Text Line 3 ^
Text Line 4 |
Text Line 1 Text Line 5 |
Text Line 1 Text Line 2 Text Line 6 V
| ----------- | | ----------- | | ----------- |
| ----------- | | ----------- | | ----------- |
+-----------------+ +-----------------+ +-----------------+
| printer.svg | | printer.svg | | printer.svg |
+-----------------+ +-----------------+ +-----------------+
I'm having problems with the vertical spacing. It should be:
Image at bottom
TextArea should fill the rest of the space.
If there is too much text, a scroll bar should appear.
The objects needed are pretty clear:
ColumnLayout
+-- ScrollView
| +-- TextArea
+-- Image
But what combinations Layout alignments/heights/implicitHeights/contentHeights/anchors/position/policy can make this happen?
The closest I have gotten is below, and the commented-out stuff is what I've been brute-forcing.
import QtQuick 2.0
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.5
ColumnLayout {
width: parent.width
height: parent.height
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
ScrollView {
Layout.alignment: Qt.AlignBottom
Layout.maximumHeight: parent.height - rect.height
TextArea {
text:
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla eleifend non leo
a iaculis. Nam at tortor mollis, iaculis justo vel, dictum sapien. Vivamus sed
feugiat tortor, nec tempor nisi. Orci varius natoque penatibus et magnis dis
parturient montes, nascetur ridiculus mus. Quisque vestibulum, eros id vehicula
eleifend, eros tellus iaculis lectus, sit amet molestie justo ex in nulla.
Maecenas at ultrices velit. Vestibulum eu libero tortor. Morbi ipsum lorem,
interdum sed aliquam quis, semper pretium ante. Duis et nibh ac tortor tincidunt
commodo. Vestibulum commodo nibh nisi, in lacinia lectus imperdiet vel. Class
aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos
himenaeos.
Aliquam erat volutpat. Praesent eget erat a nisi vulputate euismod. Donec in
vulputate tellus, eget dapibus eros. Quisque nec lacus iaculis, venenatis ante
quis, tincidunt ligula. Donec aliquet diam sit amet nisl sollicitudin, et varius
arcu lobortis. Aliquam erat volutpat. Duis enim justo, fringilla egestas
volutpat et, efficitur id erat. Duis et ullamcorper leo. Maecenas ornare orci
purus, ac commodo sapien malesuada eget. In dapibus ex nec risus laoreet
fringilla. Donec maximus elit in elit aliquam, eget imperdiet mauris congue.
Nunc libero quam, fringilla et mauris sit amet, consectetur convallis augue. Sed
vitae hendrerit ex, ac sollicitudin nibh. Donec diam mi, placerat vitae
venenatis vitae, lacinia at urna."
}
}
Rectangle { // Image placeholder
id: rect
Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom
width: 300
height: 100
color: "red"
}
}
In this case, I am using anchors to force the ColumnLayout to fill its container. Then, things like Layout.alignment: Qt.AlignBottom behave well. I found that if I don't specify ScrollView{ Layout.maximumHeight: }, then the scroll bar doesn't appear.
It works well in the online sandbox I'm playing in, but in practice it fails miserably That's because this widget is contained in a StackLayout. When I try to include my widget in the project, I get:
QML ColumnLayout: Detected anchors on an item that is managed by a layout. This is undefined behaviour; use Layout.alignment instead.
So I've tried combinations of these lines:
ColumnLayout {
Layout.alignment: Qt.AlignBottom
height: parent.height
But neither line had any effect. I think that's because the parent deduces its size from its children.
I'd prefer not to change the rest of the application, but if I reduce the rest of the application to parent components and properties, I get something that looks like this. Is it even possible for me to write a bottom-to-top widget in an application like this?
ApplicationWindow {
readonly property int base_width: 1920
readonly property int base_height: 1080
id: mainApplication
width: base_width*4/5
height: base_height*4/5
Rectangle {
id: iosMain
anchors.fill: parent
StackLayout
{
id: stackLayout
anchors.left: menuButtonsLayout.right
anchors.right: iosMain.right
anchors.top: iosMain.top
anchors.bottom: iosMain.bottom
Item {
id: pageRoot
Layout.fillWidth: false
Layout.fillHeight: false
transformOrigin: Item.TopLeft
width: childrenRect.width
height: childrenRect.height
RowLayout {
x: Style.pageLeftMargin
y: Style.pageTopMargin
StackLayout {
id: stackLayout
width: parent.width
currentIndex: 0
Layout.fillHeight: true
MyWidget{} // <-- This is where my widget lives
}
}
}
}
}
}
This is the solution I came up with, I hope it fits your needs. I basically wrapped the ScrollView in an Item in order to fill the hole upper ColumnLayout cell via the attached property Layout.fillHeight. Then I use the height of the Item to place the ScrollView inside of it. First it is placed at the bottom until it grows bigger than the Item.
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
Window {
id: root
width: 480
height: 320
visible: true
color: "white"
Timer {
interval: 500
running: true
repeat: true
onTriggered: {
textArea.text += "\nTip Tip Tip " + Date().toString()
// scroll to bottom when text was added
scrollView.ScrollBar.vertical.position = 1.0 - scrollView.ScrollBar.vertical.size
}
}
StackLayout {
anchors.fill: parent
currentIndex: 0
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
Item {
id: frame
Layout.minimumWidth: 100
Layout.maximumWidth: 800
Layout.preferredWidth: image.paintedWidth
Layout.alignment: Qt.AlignHCenter
Layout.fillHeight: true
ScrollView {
id: scrollView
anchors.bottom: parent.bottom
width: parent.width
height: frame.height < scrollView.contentHeight ? frame.height : scrollView.contentHeight
TextArea {
id: textArea
color: "black"
text: "Typewriter Tip Tip Tip"
background: Rectangle {
border.color: "black"
}
}
}
}
Image {
id: image
Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom
Layout.fillWidth: true
Layout.minimumHeight: 100
Layout.maximumHeight: 400
source: "https://picsum.photos/300/100"
fillMode: Image.PreserveAspectFit
}
}
}
}

WebView does not render the correct width and does not render at all without a height property on React Native 0.63

I use React Native 0.63 on Android with TypeScript, no expo and, worth mentionning, react-native-orientation-locker.
I made an Embed component like this:
const Embed: FC<Props> = ({ embed }: Props) => {
const { width, height } = useDimensions().window
console.log(`WIDTH = ${width}`)
// Return 411
const getSource = (embedCodeOrURI: string): (WebViewSourceUri & string) | (WebViewSourceHtml & string) => {
if (embed.startsWith('http')) {
console.log(`DEBUG > Embed > returned { uri: ${embed}}`)
return { uri: embedCodeOrURI } as WebViewSourceUri & string
}
console.log(`DEBUG > Embed > returned { html: ${embed}}`)
// If I use the test content, I still need to specify the height in the style prop but the width adjust correctly
const test =
'<div style="background-color:red"><p style="font-size:32pt">"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."</p></div>'
// Using the wrapper or not has zero impact on the bug
const withWrapper = `
<!DOCTYPE html>
<html>
<head></head>
<body>
<div id="baseDiv" style={max-width: 100%;max-height: 100%}>${embedCodeOrURI}</div>
</body>
</html>
`
const html = withWrapper
return { html } as WebViewSourceHtml & string
}
if (!embed) {
console.log(`ERROR > Embed > embed = ${embed}`)
return null
}
return (
<WebView
originWhitelist={['*']}
source={getSource(embed)}
scalesPageToFit={true}
containerStyle={{ backgroundColor: 'blue', height: 800 }}
allowsFullscreenVideo={true}
onError={(syntheticEvent): void => {
const { nativeEvent } = syntheticEvent
console.warn('ERROR > Embed > WebView error: ', nativeEvent)
}}
onHttpError={(syntheticEvent): void => {
const { nativeEvent } = syntheticEvent
console.warn('ERROR > Embed > WebView http error: ', nativeEvent.statusCode)
}}
onRenderProcessGone={(syntheticEvent): void => {
const { nativeEvent } = syntheticEvent
console.warn('WebView Crashed: ', nativeEvent.didCrash)
}}
onContentProcessDidTerminate={(syntheticEvent): void => {
const { nativeEvent } = syntheticEvent
console.warn('ERROR > Embed > Content process terminated: ', nativeEvent)
}}
startInLoadingState={true}
renderLoading={(): ReactElement => <ActivityIndicator />}
/>
)
}
export default Embed
First strange thing, the Twitter or Instagram embeds I tested displays in 411 width:
Second strange thing, If I use useDimensions().window.width (=411) as a the containerStyle width, this does no change.
...But if I add width=600, the view scale.
Third stange thing, the test html does displays full width if I don't specify a width.
...and will scale out of the screen if I specify 600:
Forth strange thing, if I do not add a height in the container view, the html content don't displays at all.
I solved the issue of the different scales by adding this in the head:
<head>
<meta charset="utf-8">\
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
I still need to specify a height so I currently adjust this manually for each post in the database.

node redis client: correct way to save JavaScript objects in lists

I need some help with the following scenario: I am using redis to store chat messages sent to various rooms:
for(var i = 0; i < 5; i++){
var message = {
"player": "player " + i,
"message": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque convallis finibus bibendum. Praesent quis ante vitae est porttitor scelerisque. Vestibulum blandit, urna ac placerat vehicula, orci nunc bibendum sem, vitae condimentum orci purus id nisi.",
"room": "room-1",
"time": new Date()
};
client.lpush(1room-1', JSON.stringify(message), redis.print);
}
I then fetch the last 100 messages:
var args = [ 'room-1', '0', '100' ];
client.lrange(args, function(err, results){
if(err){
console.log(err);
} else {
_.each(results, function(result){
var message = JSON.parse(result);
console.log(message);
});
}
});
I am new to redis and would like to make sure this is the best way of writing and reading for the scenario. I am concerned about the need to stringify the object for write and then parse the reads.
Is this the optimal way of doing this?

Titanium Alloy - How to animate a View from 0 height to "auto"

In Titanium Alloy, I'm looking to animate a View from a height of 0, to a height of "auto" or Ti.UI.SIZE. The View contains a Label, which has a varying amount of text in it that can span over several lines, so I need the View to animate to the height that it needs just to show the Label within. The idea is that you click a button and it animates the View and text to slide open. Similar to the jQuery slideDown animation seen a lot on the web.
The problem I'm having is that if I try to animate to a height of "auto" or Ti.UI.SIZE, the animation doesn't seem to happen at all. Animating to a fixed height works, but I need it to be flexible to contents of the view.
My view
<Button id="toggleBrandInfoBtn" onClick="toggleBrandInfo" title="Show Text" />
<View id="brandInfo" layout="vertical" height="0">
<Label id="brandInfoLabel" text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor."></Label>
</View>
Controller
$.brandInfo.anchorPoint = {x:0, y:0};
$.toggleBrandInfoBtn.addEventListener("click", function () {
$.brandInfo.animate({
anchorPoint: {x:1, y:1},
top: 0,
height: Ti.UI.SIZE,
duration: 1000
});
});
Have you tried a percentage value? This is not necessarily documented but I have had some success with it. Also try a 1% starting value.
$.brandInfo.anchorPoint = {x:0, y:0};
$.toggleBrandInfoBtn.addEventListener("click", function () {
$.brandInfo.animate({
anchorPoint: {x:1, y:1},
top: 0,
height: '100%',
duration: 1000
});
});
As of SDK 3.3.0 this is not possible.
One way around the problem might be to start with brandInfo visible=false and height Ti.UI.SIZE
From that get the view height required for the content and store.
Then as part of your animation sequence set the height to zero, make visible = true and then do the animation to the pre-calculated height stored earlier.
I had the same issue and this module helped me out there:
https://github.com/animecyc/TitaniumAnimator