Restyle unified search skeleton items animation and simplify their code
authorClaudio Cambra <claudio.cambra@gmail.com>
Mon, 11 Jul 2022 15:52:31 +0000 (17:52 +0200)
committerClaudio Cambra <claudio.cambra@gmail.com>
Fri, 5 Aug 2022 16:40:13 +0000 (18:40 +0200)
Signed-off-by: Claudio Cambra <claudio.cambra@gmail.com>
src/gui/tray/UnifiedSearchResultItemSkeleton.qml
src/gui/tray/UnifiedSearchResultItemSkeletonContainer.qml
src/gui/tray/Window.qml
theme/Style/Style.qml

index a7a1a6f95369a7ecfb9d60db7940ee2527b5dd2b..32e19cca3aada3a0e9c685f63b74bdc1ae63aa51 100644 (file)
@@ -1,6 +1,7 @@
 import QtQml 2.15
 import QtQuick 2.15
 import QtQuick.Layouts 1.2
+import QtGraphicalEffects 1.15
 import Style 1.0
 
 RowLayout {
@@ -14,47 +15,181 @@ RowLayout {
     property int titleFontSize: Style.unifiedSearchResultTitleFontSize
     property int sublineFontSize: Style.unifiedSearchResultSublineFontSize
 
-    property color titleColor: Style.ncTextColor
-    property color sublineColor: Style.ncSecondaryTextColor
+    Accessible.role: Accessible.ListItem
+    Accessible.name: qsTr("Search result skeleton.").arg(model.index)
 
-    property string iconColor: "#afafaf"
+    height: Style.trayWindowHeaderHeight
 
-    property int index: 0
+    /*
+    * An overview of what goes on here:
+    *
+    * We want to provide the user with a loading animation of a unified search result list item "skeleton".
+    * This skeleton has a square and two rectangles that are meant to represent the icon and the text of a
+    * real search result.
+    *
+    * We also want to provide a nice animation that has a dark gradient moving over these shapes. To do so,
+    * we very simply create rectangles that have a gradient going transparent->dark->dark->transparent, and
+    * overlay them over the shapes beneath.
+    *
+    * We then use NumberAnimations to move these gradients left-to-right over the base shapes below. Since
+    * the gradient rectangles are child elements of the base-color rectangles which they move over, as long
+    * as we ensure that the parent rectangle has the "clip" property enabled this will look nice and won't
+    * spill over onto other elements.
+    *
+    * We also want to make sure that, even though these gradient rectangles are separate, it looks as it is
+    * one single gradient sweeping over the base color components
+    */
 
-    Accessible.role: Accessible.ListItem
-    Accessible.name: qsTr("Search result skeleton.").arg(index)
+    property color baseGradientColor: Style.lightHover
+    property color progressGradientColor: Style.darkMode ? Qt.lighter(baseGradientColor, 1.2) : Qt.darker(baseGradientColor, 1.1)
 
-    height: Style.trayWindowHeaderHeight
+    property int animationRectangleWidth: Style.trayWindowWidth
+    property int animationStartX: -animationRectangleWidth
+    property int animationEndX: animationRectangleWidth
 
-    Rectangle {
-        id: unifiedSearchResultSkeletonThumbnail
-        color: unifiedSearchResultSkeletonItemDetails.iconColor
+    Component {
+        id: gradientAnimationRectangle
+        Rectangle {
+            width: unifiedSearchResultSkeletonItemDetails.animationRectangleWidth
+            height: parent.height
+            gradient: Gradient {
+                orientation: Gradient.Horizontal
+                GradientStop {
+                    position: 0
+                    color: "transparent"
+                }
+                GradientStop {
+                    position: 0.4
+                    color: unifiedSearchResultSkeletonItemDetails.progressGradientColor
+                }
+                GradientStop {
+                    position: 0.6
+                    color: unifiedSearchResultSkeletonItemDetails.progressGradientColor
+                }
+                GradientStop {
+                    position: 1.0
+                    color: "transparent"
+                }
+            }
+
+            NumberAnimation on x {
+                from: unifiedSearchResultSkeletonItemDetails.animationStartX
+                to: unifiedSearchResultSkeletonItemDetails.animationEndX
+                duration: 1000
+                loops: Animation.Infinite
+                running: true
+            }
+        }
+    }
+
+    Item {
         Layout.preferredWidth: unifiedSearchResultSkeletonItemDetails.iconWidth
         Layout.preferredHeight: unifiedSearchResultSkeletonItemDetails.iconWidth
         Layout.leftMargin: unifiedSearchResultSkeletonItemDetails.iconLeftMargin
         Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
+
+        Rectangle {
+            id: unifiedSearchResultSkeletonThumbnail
+            anchors.fill: parent
+            color: unifiedSearchResultSkeletonItemDetails.baseGradientColor
+            clip: true
+            visible: false
+
+            Loader {
+                x: mapFromItem(unifiedSearchResultSkeletonItemDetails, 0, 0).x
+                height: parent.height
+                sourceComponent: gradientAnimationRectangle
+            }
+        }
+
+        Rectangle {
+            id: unifiedSearchResultSkeletonThumbnailMask
+            anchors.fill: unifiedSearchResultSkeletonThumbnail
+            color: "white"
+            radius: 100
+            visible: false
+        }
+
+        OpacityMask {
+            anchors.fill: unifiedSearchResultSkeletonThumbnail
+            source: unifiedSearchResultSkeletonThumbnail
+            maskSource: unifiedSearchResultSkeletonThumbnailMask
+        }
     }
 
-    ColumnLayout {
+    Column {
         id: unifiedSearchResultSkeletonTextContainer
+
         Layout.fillWidth: true
+        Layout.leftMargin: unifiedSearchResultSkeletonItemDetails.textLeftMargin
+        Layout.rightMargin: unifiedSearchResultSkeletonItemDetails.textRightMargin
 
-        Rectangle {
-            id: unifiedSearchResultSkeletonTitleText
-            color: unifiedSearchResultSkeletonItemDetails.titleColor
-            Layout.preferredHeight: unifiedSearchResultSkeletonItemDetails.titleFontSize
-            Layout.leftMargin: unifiedSearchResultSkeletonItemDetails.textLeftMargin
-            Layout.rightMargin: unifiedSearchResultSkeletonItemDetails.textRightMargin
-            Layout.fillWidth: true
+        spacing: Style.standardSpacing
+
+        Item {
+            height: unifiedSearchResultSkeletonItemDetails.titleFontSize
+            width: parent.width
+
+            Rectangle {
+                id: unifiedSearchResultSkeletonTitleText
+                anchors.fill: parent
+                color: unifiedSearchResultSkeletonItemDetails.baseGradientColor
+                clip: true
+                visible: false
+
+                Loader {
+                    x: mapFromItem(unifiedSearchResultSkeletonItemDetails, 0, 0).x
+                    height: parent.height
+                    sourceComponent: gradientAnimationRectangle
+                }
+            }
+
+            Rectangle {
+                id: unifiedSearchResultSkeletonTitleTextMask
+                anchors.fill: unifiedSearchResultSkeletonTitleText
+                color: "white"
+                radius: Style.veryRoundedButtonRadius
+                visible: false
+            }
+
+            OpacityMask {
+                anchors.fill: unifiedSearchResultSkeletonTitleText
+                source: unifiedSearchResultSkeletonTitleText
+                maskSource: unifiedSearchResultSkeletonTitleTextMask
+            }
         }
 
-        Rectangle {
-            id: unifiedSearchResultSkeletonTextSubline
-            color: unifiedSearchResultSkeletonItemDetails.sublineColor
-            Layout.preferredHeight: unifiedSearchResultSkeletonItemDetails.sublineFontSize
-            Layout.leftMargin: unifiedSearchResultSkeletonItemDetails.textLeftMargin
-            Layout.rightMargin: unifiedSearchResultSkeletonItemDetails.textRightMargin
-            Layout.fillWidth: true
+        Item {
+            height: unifiedSearchResultSkeletonItemDetails.sublineFontSize
+            width: parent.width
+
+            Rectangle {
+                id: unifiedSearchResultSkeletonTextSubline
+                anchors.fill: parent
+                color: unifiedSearchResultSkeletonItemDetails.baseGradientColor
+                clip: true
+                visible: false
+
+                Loader {
+                    x: mapFromItem(unifiedSearchResultSkeletonItemDetails, 0, 0).x
+                    height: parent.height
+                    sourceComponent: gradientAnimationRectangle
+                }
+            }
+
+            Rectangle {
+                id: unifiedSearchResultSkeletonTextSublineMask
+                anchors.fill: unifiedSearchResultSkeletonTextSubline
+                color: "white"
+                radius: Style.veryRoundedButtonRadius
+                visible: false
+            }
+
+            OpacityMask {
+                anchors.fill:unifiedSearchResultSkeletonTextSubline
+                source: unifiedSearchResultSkeletonTextSubline
+                maskSource: unifiedSearchResultSkeletonTextSublineMask
+            }
         }
     }
 }
index 0ed23482cc316ef0dff5fea266045b5ac5397bc2..354835d51e8b742904571230b83a0af809978777 100644 (file)
@@ -5,22 +5,14 @@ import Style 1.0
 Column {
     id: unifiedSearchResultsListViewSkeletonColumn
 
+    property int animationRectangleWidth: Style.trayWindowWidth
+
     Repeater {
-        model: 10
+        model: Math.ceil(unifiedSearchResultsListViewSkeletonColumn.height / Style.trayWindowHeaderHeight)
         UnifiedSearchResultItemSkeleton {
             width: unifiedSearchResultsListViewSkeletonColumn.width
-        }
-    }
-
-    OpacityAnimator {
-        target: unifiedSearchResultsListViewSkeletonColumn
-        from: 0.5
-        to: 1
-        duration: 800
-        running: unifiedSearchResultsListViewSkeletonColumn.visible
-        loops: Animation.Infinite
-        easing {
-            type: Easing.InOutBounce
+            height: Style.trayWindowHeaderHeight
+            animationRectangleWidth: unifiedSearchResultsListViewSkeletonColumn.animationRectangleWidth
         }
     }
 }
index 34873a4d0813c6d9d2f4c65b458cb2f86e271669..22c92f64887b25504a77d2e6c064e1d56073f560 100644 (file)
@@ -111,7 +111,7 @@ Window {
     Rectangle {\r
         id: trayWindowBackground\r
 \r
-        property bool isUnifiedSearchActive: unifiedSearchResultsListViewSkeleton.visible\r
+        property bool isUnifiedSearchActive: unifiedSearchResultsListViewSkeletonLoader.active\r
                                              || unifiedSearchResultNothingFound.visible\r
                                              || unifiedSearchResultsErrorLabel.visible\r
                                              || unifiedSearchResultsListView.visible\r
@@ -721,13 +721,22 @@ Window {
             }\r
         }\r
 \r
-        UnifiedSearchResultItemSkeletonContainer {\r
-            id: unifiedSearchResultsListViewSkeleton\r
-            visible: !unifiedSearchResultNothingFound.visible && !unifiedSearchResultsListView.visible && ! UserModel.currentUser.unifiedSearchResultsListModel.errorString &&  UserModel.currentUser.unifiedSearchResultsListModel.searchTerm\r
+        Loader {\r
+            id: unifiedSearchResultsListViewSkeletonLoader\r
             anchors.top: trayWindowUnifiedSearchInputContainer.bottom\r
             anchors.left: trayWindowBackground.left\r
             anchors.right: trayWindowBackground.right\r
             anchors.bottom: trayWindowBackground.bottom\r
+\r
+            active: !unifiedSearchResultNothingFound.visible &&\r
+                    !unifiedSearchResultsListView.visible &&\r
+                    !UserModel.currentUser.unifiedSearchResultsListModel.errorString &&\r
+                    UserModel.currentUser.unifiedSearchResultsListModel.searchTerm\r
+\r
+            sourceComponent: UnifiedSearchResultItemSkeletonContainer {\r
+                anchors.fill: parent\r
+                animationRectangleWidth: trayWindow.width\r
+            }\r
         }\r
 \r
         ScrollView {\r
index 1d8b200e3852fdbdc58bdeb259fe5452047c2467..06970bae098de798bdf3372c31a3a692ddab0df3 100644 (file)
@@ -6,6 +6,7 @@ import com.nextcloud.desktopclient 1.0
 \r
 QtObject {\r
     readonly property int pixelSize: fontMetrics.font.pixelSize\r
+    readonly property bool darkMode: Theme.darkMode\r
 \r
     // Colors\r
     readonly property color ncBlue:      Theme.wizardHeaderBackgroundColor\r