Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

How to implement an opening and closing animation on a frame?



  • In my project, I have several user messages to show, depending on what happens in the application. To achieve that, I created a frame where my messages may appear, which acts as a view for my messages.

    The message component itself is just another frame, containing a glyph, a text and a close button. Each message is created dynamically and added to the message view described above when needed to be shown, and is destroyed when closed.

    Here is how my message view looks actually:
    f90c39aa-b88c-4012-ae06-cb1557ebe9e1-image.png

    All works as I expected, however I want now to add an opening and a closing animation. I want to animate the height in a such manner the message gives the effect to scroll down when opening, and scroll up when closing.

    However I don't know how to achieve this animation. I tried by myself, but my code just doesn't work, and I don't know how to implement a such animation in the above described dynamic context.

    Here is my actual code:

    1. Message view, where message items are created and deleted
    import QtQuick 2.15
    import QtQuick.Window 2.15
    import QtQuick.Controls 2.15
    import QtQuick.Shapes 1.15
    import QtGraphicalEffects 1.15
    import QtQuick.Templates 2.15 as T
    
    /**
    * Message view
    *@author JMR
    */
    T.Frame
    {
        // advanced properties
        property int m_MaxMsgOnView: 3
        property int m_MsgOffset:    4
    
        // aliases
        property var model: null
    
        // common properties
        id: frMessageView
        visible: true
    
        /**
        * Frame background
        */
        background: Rectangle
        {
            color: "transparent"
            anchors.fill: parent
        }
    
        /**
        * Bind several signals from the c++ model to the message view
        */
        Connections
        {
            // common properties
            target: model
    
            /**
            * Called when a message was added
            */
            function onMessageAdded()
            {
                if (g_Debug)
                    console.log("Notificaion message added signal");
    
                // recreate the view
                clearView();
                populateView();
    
                // show it (as message was added, view should at least contain one)
                frMessageView.visible = true;
            }
    
            /**
            * Called when a message was removed
            */
            function onMessageRemoved()
            {
                if (g_Debug)
                    console.log("Notificaion message removed signal");
    
                // clear the current view content
                clearView();
    
                // is view empty?
                if (!model.rowCount())
                {
                    frMessageView.visible = false;
                    return;
                }
    
                // populate the view with remaining content
                populateView();
            }
        }
    
        /**
        * Clear message view
        */
        function clearView()
        {
            // get frame children count
            var childrenCount = frMessageView.children.length;
    
            // iterate through frame children components
            for (var i = childrenCount - 1; i >= 0; --i)
                // is component a message item?
                if (frMessageView.children[i].m_ItemType === "WQTMessageItem")
                    // delete it (be careful, component will be moved in garbage collector and not deleted immediately)
                    frMessageView.children[i].destroy();
        }
    
        /**
        * Populate message view
        */
        function populateView()
        {
            // load the message item component
            var component = Qt.createComponent('WQTMessageItem.qml');
    
            // succeeded?
            if (component.status !== Component.Ready)
            {
                console.error("Message view - populateView - an error occurred while message item template was created - "
                        + component.errorString());
                return;
            }
    
            // get message count to show (no more than 3) and first visible message
            var rowCount     = model.rowCount();
            var messageCount = rowCount >= m_MaxMsgOnView ? m_MaxMsgOnView : rowCount;
            var startIndex   = rowCount - messageCount;
            var xOffset      = 0;
            var yOffset      = m_MaxMsgOnView * m_MsgOffset;
    
            // iterate through messages to create
            for (var i = 0; i < messageCount; ++i)
            {
                // calculate message index to show
                var curIndex = startIndex + i;
    
                // out of bounds?
                if (curIndex >= rowCount)
                    break;
    
                // add message item component to view
                component.createObject(frMessageView, {"id":          "miMessageItem" + i,
                                                       "m_Target":    frMessageView,
                                                       "x":           xOffset,
                                                       "y":           yOffset,
                                                       "width":       frMessageView.width - 10,
                                                       "height":      48,
                                                       "m_MaxHeight": 48,
                                                       "state":       "EXTENDED",
                                                       "m_Index":     curIndex});
    
                // calculate next offset
                xOffset += m_MsgOffset;
                yOffset -= m_MsgOffset;
            }
        }
    }
    
    1. Message item
    import QtQuick 2.15
    import QtQuick.Window 2.15
    import QtQuick.Controls 2.15
    import QtQuick.Shapes 1.15
    import QtGraphicalEffects 1.15
    import QtQuick.Templates 2.15 as T
    
    /**
    * Message item
    *@author JMR
    */
    T.Frame
    {
        // advanced properties
        property var    m_Target:           null
        property string m_ItemType:         "WQTMessageItem"
        property string m_NotificationIcon: "qrc:/Resources/Images/main/icon_notification.svg"
        property string m_WarningIcon:      "qrc:/Resources/Images/main/icon_warning.svg"
        property string m_ErrorIcon:        "qrc:/Resources/Images/main/icon_error.svg"
        property string m_CloseBtnGlyph:    "qrc:/Resources/Images/main/icon_small_close.svg"
        property int    m_Index:            0
        property int    m_MaxHeight:        48
    
        // state dependent properties
        property int    m_Type
        property string m_BgColor
        property string m_OutlineColor
        property string m_TextColor
        property string m_Glyph
    
        /**
        * Equivalent to WQTMessageViewModel::IEMessageType
        */
        enum IEMessageType
        {
            Notification,
            Warning,
            Error
        }
    
        // common properties
        id: frFrame
        height: 0
        state: "COLLAPSED"
    
        /**
        * Item background
        */
        background: Rectangle
        {
            // common properties
            anchors.fill: parent
            color: m_BgColor
            radius: 5
            border.width: 1
            border.color: m_OutlineColor
        }
    
        /**
        * Message icon
        */
        Image
        {
            // common properties
            id: imIcon
            anchors.left: parent.left
            anchors.leftMargin: 12
            anchors.top: parent.top
            anchors.topMargin: 10
            width: 16
            height: 16
            source: m_Glyph
            sourceSize: Qt.size(width, height)
            fillMode: Image.PreserveAspectFit
            smooth: true
        }
    
        /**
        * Close button
        */
        Button
        {
            // common properties
            id: btClose
            display: AbstractButton.IconOnly
            anchors.right: parent.right
            anchors.rightMargin: 6
            anchors.top: parent.top
            anchors.topMargin: 7
            width: 8
            height: 8
    
            background: null
    
            /**
            * Close button glyph
            */
            Image
            {
                // common properties
                anchors.fill: parent
                source: m_CloseBtnGlyph
                sourceSize: Qt.size(width, height)
                fillMode: Image.PreserveAspectFit
                smooth: false
            }
    
            /**
            * Called when close button is clicked
            */
            onClicked:
            {
                frFrame.state = "COLLAPSED";
    
                if (m_Target.model)
                    m_Target.model.removeMessageAt(m_Target.model.rowCount() - 1);
            }
        }
    
        /**
        * Message title
        */
        Text
        {
            // common properties
            id: laTitle
            anchors.top: parent.top
            anchors.topMargin: 8
            anchors.left: imIcon.right
            anchors.leftMargin: 7
            anchors.right: btClose.left
            anchors.rightMargin: 7
            font.bold: true
            font.pixelSize: 12
            horizontalAlignment: Text.AlignLeft
            verticalAlignment: Text.AlignVCenter
            renderType: Text.NativeRendering
            text: m_Target.model ? m_Target.model.getMessageTitle(m_Index) : ""
            color: m_TextColor
        }
    
        /**
        * Message caption
        */
        Text
        {
            // common properties
            id: laDescription
            anchors.top: laTitle.bottom
            anchors.topMargin: 0
            anchors.left: imIcon.right
            anchors.leftMargin: 7
            anchors.right: btClose.left
            anchors.rightMargin: 7
            anchors.bottom: parent.bottom
            anchors.bottomMargin: 5
            font.pixelSize: 14
            horizontalAlignment: Text.AlignLeft
            verticalAlignment: Text.AlignVCenter
            renderType: Text.NativeRendering
            text: m_Target.model ? m_Target.model.getMessageCaption(m_Index) : ""
            wrapMode: Text.WordWrap
            elide: Text.ElideRight
            color: m_TextColor
        }
    
        /**
        * Component state array
        */
        states:
        [
            State
            {
                name: "EXTENDED"
                PropertyChanges {target: frFrame; height: m_MaxHeight}
            },
            State
            {
                name: "COLLAPSED"
                PropertyChanges {target: frFrame; height: 0}
            }
        ]
    
        /**
        * Transitions between states
        */
        transitions:
        [
            Transition
            {
                from: "*"; to: "EXTENDED"
                NumberAnimation {property: "height"; easing.type: Easing.Linear; duration: 5000}
            },
            Transition
            {
                from: "*"; to: "COLLAPSED"
                NumberAnimation {property: "height"; easing.type: Easing.Linear; duration: 5000}
            }
        ]
    }
    

    The code is just a try to animate the message, but it not working. Can someone explain me how to correct it to reach my objective? I would be very greatful.

    NOTE I shown my code as is, forgive me, it depends on many other classes in my project, which I cannot show here. I hope this will be enough to understand the situation and help me to solve it.


  • Moderators

    hi @jeanmilost there are many ways to do this in QML the easiest way currently, because you set your height explicitly to 0 and the target value is probably via a Property animation or a Behavior animation

    something simple like

    Behavior on height { NumberAnimation{duration: 200} }
    

    could already be enough



  • @J-Hilk said in How to implement an opening and closing animation on a frame?:

    something simple like
    Behavior on height { NumberAnimation{duration: 200} }

    Good suggestion, I modified my code, this works now for the opening animation.

    However there is a remaining issue, which is how to delete my message item AFTER the closing animation ends. My first idea is to add a timer with the same interval as the animation duration, and which will send the close command after the elapsed time, but I think that solution isn't elegant.

    Is there a better way to do that, e.g is there an emitted signal which I can listen on animation ends?



  • Maybe use the finished signal from NumberAnimation?



  • hi
    @jeanmilost said in How to implement an opening and closing animation on a frame?:

    property var model: null

    You can use a ListModel instead.
    And ListView to show the messages


Log in to reply