mercredi 14 décembre 2011

Diaporama (ou slideshow) QML

Un pattern graphique souvent utilisé dans les applis mobiles est le diaporama d'images : l'expérience doit être intuitive, fluide et immersive. Pour le dernier point, il est recommandé de forcer l'orientation en mode paysage (voir cet article pour plus d'infos). Pour l'aspect intuitif il est classique d'utiliser son doigt pour balayer l'écran (swipe en anglais) de droite à gauche (resp gauche à droit) afin de faire défiler les images. Voici le rendu de l'appli que je propose aujourd'hui :



Les flèches latérales indiquent la possibilité de glisser son doigt vers la droite ou vers la gauche :



J'ai créé un projet avec deux fichiers QML : main.qml et Diaporama.qml ; le fichier main.qml contient à peu de chose près un code similaire à celui décrit dans l'article de lecteur video ; intéressons-nous au second fichier en charge d'afficher le diaporama : les images sont stockées localement dans le fichier binaire mais rien ne vous empêche de récupérer les images depuis le réseau par exemple :

Rectangle {
    id: diaporama

    signal leaveDiaporama()

    width: 640
    height: 360
    color: "black"


Le diaporama est un écran en mode paysage avec un fond noir et un signal émis lorsque l'utilisateur clique sur la croix. Nous définissons ensuite un modèle statique de type ListModel:

ListModel {
  id: myListModel
  ListElement { file: "./images/0.jpg"; name: "Picture n°0"; desc:"Lorem ipsum dolor sit amet"}
  ListElement { file: "./images/1.jpg"; name: "Picture n°1"; desc:"Lorem ipsum dolor sit amet"}
  ListElement { file: "./images/2.jpg"; name: "Picture n°2"; desc:"Lorem ipsum dolor sit amet"}
  ListElement { file: "./images/3.jpg"; name: "Picture n°3"; desc:"Lorem ipsum dolor sit amet"}
  ListElement { file: "./images/4.jpg"; name: "Picture n°4"; desc:"Lorem ipsum dolor sit amet"}
  ListElement { file: "./images/5.jpg"; name: "Picture n°5"; desc:"Lorem ipsum dolor sit amet"}
  ListElement { file: "./images/6.jpg"; name: "Picture n°6"; desc:"Lorem ipsum dolor sit amet"}
  ListElement { file: "./images/7.jpg"; name: "Picture n°7"; desc:"Lorem ipsum dolor sit amet"}
  ListElement { file: "./images/8.jpg"; name: "Picture n°8"; desc:"Lorem ipsum dolor sit amet"}
  ListElement { file: "./images/9.jpg"; name: "Picture n°9"; desc:"Lorem ipsum dolor sit amet"}
  ListElement { file: "./images/10.jpg"; name: "Picture n°10"; desc:"Lorem ipsum dolor sit amet"}
  ...
}


Chaque élément représente un slide symbolisé par la triplette suivante :
  • "file" : chemin pointant sur l'image
  • "name" : titre du slide
  • "desc" : légende du slide
Dans une application live, les images sont récupérées du réseau et gérées dans un modèle de type ListModel, XmlListModel ou autre. Visuellement le diaporama sera réalisé avec une liste horizontale, elle-même agencée dans une mise en page de type "Row" :

Row {
    anchors.fill: parent

    Rectangle{
        width: 64
        height: parent.height
        color: "black"
        z:1
        Image {
            id: leftArrow
            source: "images/left_arrow.png"
            width: 26
            anchors.centerIn: parent
        }
    }

    ListView{
        id:listView
        orientation:ListView.Horizontal
        width:parent.width-closeButton.width*2
        height: parent.height
        model:myListModel
        delegate:myDelegate
        maximumFlickVelocity:700
        snapMode: ListView.SnapToItem
                preferredHighlightBegin: 0; preferredHighlightEnd: 0
        highlightRangeMode: ListView.StrictlyEnforceRange
        highlightFollowsCurrentItem: true

        onCurrentIndexChanged: {
            if(listView.currentIndex == 0) leftArrow.visible = false;
            else leftArrow.visible = true;

            if(listView.currentIndex == listView.count-1) rightArrow.visible = false;
            else rightArrow.visible = true;
        }
    }

    Rectangle{
        width: 64
        height: parent.height
        color: "black"
        Button {
            id: closeButton
            iconSource: "images/close.png"
            width: 64

            MouseArea{
                anchors.fill: parent
                onClicked: {
                    leaveDiaporama()
                }
            }
        }

        Image {
            id: rightArrow
            source: "images/right_arrow.png"
            width: 26
            anchors.centerIn: parent
        }
    }
}


Les rectangles de droite et de gauche permettent d'afficher des petites flêches indiquant si des photos sont disponibles en slidant dans chacune des directions. La propriété orientation est donc positionnée à la valeur ListView.Horizontal pour le défilement latéral. La propriété delegate pointe sur le composant en charge de dessiner un slide identifié myDelegate (nous le décrirons juste après). La prorpriété maximumFlickVelocity indique la vitesse maximale de défilement en pixels/seconde. La propriété snapMode permet de définir où s'arrête le défilement : la valeur par défaut ListView.NoSnap indique que le défilement s'arrête n'importe où ; dans notre cas nous souhaitons l'arrêter sur un item (en l'occurence un slide) : nous pouvons donc utiliser soit ListView.SnapToItem soit ListView.SnapOneItem. ensuite il nous faut pister la valeur courante de l'index afin d'afficher ou de masquer les flêches latérales. Pour cela il faut fixer la propriété highlightRangeMode à ListView.StrictlyEnforceRange. Ainsi le signal onCurrentIndexChanged sera émis à chaque sélection/défilement de slide : nous pouvons alors jouer sur la visibilité des flêches droite/gauche.

Pour finir, je définis le composant delegate en charge de dessiner le slide composé d'un titre, d'une photo et d'une description :

Component{
    id:myDelegate

    Rectangle {
        width:ListView.view.width
        height:ListView.view.height
        color: "black"

        // Title
        Text{
            id:myTitle
            anchors.horizontalCenter: parent.horizontalCenter
            anchors.bottom: myImage.top
            anchors.bottomMargin: 10
            color:"white"
            font.bold: true
            font.pointSize: 9
            elide:Text.ElideRight
            text: name
        }

        // Image
        Image{
            id:myImage
            anchors.centerIn: parent
            source: file
        }

        // Description
        Text{
            id:txt
            width: parent.width
            color:"white"
            text:desc
            font.pointSize: 7
            wrapMode : Text.WordWrap
            horizontalAlignment: Text.AlignHCenter
            anchors.top:myImage.bottom
            anchors.topMargin:10
            anchors.left: myImage.left
            anchors.right: myImage.right
        }
    }
}


Le titre du slide est bindé avec la propriété "name" du modèle myListModel. L'image est initialisée à partir de la source et le descriptif à partir du champ "desc".

Et voilà.
Le code complet est disponible ici

Aucun commentaire:

Enregistrer un commentaire