Flixnet theme, Part 5: Game details

The upper half of the screen contains the metadata and preview image of the currently selected game. The components here will consist of simple elements, like Image and Text, which will make adding them way easier.

You can place all these elements directly under the main FocusScope, or you could create a containing Item if you wish. I'll do the former to keep the guide shorter. I'll also create a property to hold the currently selected game called currentGame (see the previous part), which will be used in these new elements. The actual fields of a Game are listed in the API reference.

Title

A simple Text item in the upper left corner, with the left margin set to the same 100px we used at the game rows, and some additional margin at the top.

Text {
    id: title

    text: currentGame.title
    color: "white"

    font.pixelSize: vpx(32)
    font.family: globalFonts.sans
    font.bold: true

    anchors.top: parent.top
    anchors.topMargin: vpx(42)
    anchors.left: parent.left
    anchors.leftMargin: vpx(100)
}

Rating

The rating will be displayed as a five-star bar, with some percentage of it colored according to the actual rating. This can be done with two simple, overlapping QML Images: draw five empty stars first, then over them, draw filled ones according to the rating. Kind of like a progress bar, except we're using stars for filling.

But first of all, I actually draw two images for the stars, an empty one and a filled. Both have square size and transparent background. I create a new directory (eg. assets) in my theme folder and put them there.

star_empty.svgstar_filled.svg

Tip

I've used Inkscape for drawing the vector art; it has a built-in tool for drawing stars and other polygons.

Then I create the following Item. As the star image is a square, I make its width 5 times the height to hold the five stars horizontally. I make the empty-star Image fill this whole item, and set fillMode: Image.TileHorizontally to make the star repeat horizontally. For the filled-star image, I place it over the other one, and modify its width by the rating, which is provided as a number between 0.0 and 1.0 (0% and 100%).

Item {
    id: rating

    // set the item's dimensions
    height: vpx(16)
    width: height * 5

    // put it under the title
    anchors.top: title.bottom
    anchors.left: title.left


    // the empty stars
    Image {
        anchors.fill: parent

        source: "assets/star_empty.svg"
        sourceSize { width: parent.height; height: parent.height }

        // the most important bits!
        fillMode: Image.TileHorizontally
        horizontalAlignment: Image.AlignLeft
    }


    // the filled stars
    Image {
        anchors.top: parent.top
        anchors.left: parent.left

        width: parent.width * currentGame.rating // !!!
        height: parent.height

        source: "assets/star_filled.svg"
        sourceSize { width: parent.height; height: parent.height }

        fillMode: Image.TileHorizontally
        horizontalAlignment: Image.AlignLeft
    }

}

Note

Without horizontalAlignment the stars might not line up perfectly (the repeat will start from the center).

When a game has no rating defined, game.rating is 0.0. Showing five empty stars for an otherwise good game might be a bit misleading, so I'll make the rating bar only appear when the rating is over 0%:

Item {
    id: rating

    visible: currentGame.rating > 0.0

    // ...
}

Release year

Yet another simple Text element:

Text {
    id: year

    // if not defined, the release year is 0
    visible: game.year > 0

    text: game.year
    color: "white"
    font.pixelSize: vpx(16)
    font.family: globalFonts.sans

    anchors.left: rating.right
    anchors.top: rating.top
}

Row

Currently the year element is manually anchored right next to the rating. Doing this for each item every time is quite annoying, let's just put them in a Row:

Row {
    id: detailsRow

    // anchor the whole row
    anchors.top: title.bottom
    anchors.topMargin: vpx(5)
    anchors.left: title.left

    spacing: vpx(10)


    Item {
        id: rating

        // remove anchor items!
        // anchors.top: title.bottom
        // anchors.left: title.left

        // ...
    }

    Text {
        id: year

        // remove anchor items!
        // anchors.left: rating.right
        // anchors.top: rating.top

        // ...
    }
}

Player count

This one will be a rounded rectangle with smiley faces in it indicating the number of players. The player count defaults to one; similarly to the rating, I'll show the component only if the player count is more than one.

First I create the smiley face image (based on the Unicode "filled smiling face" symbol (U+263B). Again, it's square sized with a transparent background.

Then create a background rounded Rectangle and the smiles Image in it, putting the whole thing in the Row created in the previous step:

Rectangle {
    id: multiplayer

    // the Rectangle's size depends on the Image,
    // with some additional padding
    width: smileys.width + vpx(8)
    height: smileys.height + vpx(5)

    color: "#555"
    radius: vpx(3)

    visible: currentGame.players > 1


    Image {
        id: smileys

        // 13px looked good for me
        width: vpx(13) * currentGame.players
        height: vpx(13)

        anchors.centerIn: parent

        source: "assets/smiley.svg"
        sourceSize { width: smileys.height; height: smileys.height }

        fillMode: Image.TileHorizontally
        horizontalAlignment: Image.AlignLeft
    }
}

Developer

Yet another simple Text in the Row:

Text {
    id: developer

    text: currentGame.developer
    color: "white"
    font.pixelSize: vpx(16)
    font.family: globalFonts.sans
}

Tip

A game may have multiple developers: if you just want to show them as a Text, you can use <Game>.developer, a string that simply lists them all. There's also <Game>.developerList, a JavaScript Array, if you wish to use them individually.

Description

A bigger text with set boundaries for alignment. If there is a short summary, I'll use that, otherwise the beginning of the full description.

Text {
    id: description

    text: currentGame.description
    color: "white"
    font.pixelSize: vpx(18)
    font.family: globalFonts.sans

    // allow word wrapping, justify horizontally
    wrapMode: Text.WordWrap
    horizontalAlignment: Text.AlignJustify
    // if the text is too long, end it with an ellipsis (...)
    elide: Text.ElideRight

    anchors {
        left: detailsRow.left
        right: parent.horizontalCenter
        top: detailsRow.bottom; topMargin: vpx(20)
        bottom: parent.verticalCenter; bottomMargin: vpx(32)
    }
}

Screenshot

This should be below everything else on the screen -- in fact, if you look at the image at the beginning of this guide, it's actually going into the bottom-half region of the screen, reaching the row of images.

As it's under everything else, I'll put its implementation at the top of the theme file, even before the collection PathView. I'll anchor the top and left edges of the image to the top right corner of the screen. To make it go slightly into the bottom half, I'll anchor the bottom edge to the vertical center of the screen, then add a small amount of negative margin to the bottom (a positive margin reduces the size of the element, while a negative one increases it).

Image {
    id: screenshot

    asynchronous: true
    fillMode: Image.PreserveAspectFit

    // set the first screenshot as source, or nothing
    source: currentGame.assets.screenshots[0] || ""
    sourceSize { width: 512; height: 512 }

    anchors.top: parent.top
    anchors.right: parent.right
    anchors.bottom: parent.verticalCenter
    anchors.bottomMargin: vpx(-45) // the height of the collection label
}

Note

Using negative margins kind of feels like a hack though, so depending on the situation you might prefer to use simple width/height properties.

Help

The screenshots are stored under assets.screenshots, which is a regular JavaScript Array. If it's empty, screenshots[0] will be undefined, and setting an undefined value as the source of an Image will produce a warning in the log. Setting it to an empty string, however, will not, so appending || "" as a fallback will silence the warning.

An alternative solution could be is to use screenshots as a model in eg. a ListView, and the Image as delegate. You could then further extend it to periodically change the current visible screenshot.

Tip

You can also use the z property of the components to set their relative "height".

Gradients

There are two linear gradients ("fade-ins"), one from the left and one from the bottom of the image. Such effect can be added just like regular components, can be positioned, sized, animated, etc. But first of all, to use gradients you'll need the QtGraphicalEffects QML module:

import QtQuick 2.0
import QtGraphicalEffects 1.0

FocusScope {
    // ...
}

Then, create the horizontal linear gradient inside our Image component:

Image {
    id: screenshot

    // ...


    LinearGradient {
        width: parent.width * 0.25
        height: parent.height

        anchors.left: parent.left

        // since it goes straight horizontally from the left,
        // the Y of the point doesn't really matter
        start: Qt.point(0, 0)
        end: Qt.point(width, 0)
        // at the left side (0%), it starts with a fully visible black
        // at the right side (100%), it blends into transparency
        gradient: Gradient {
            GradientStop { position: 0.0; color: "black" }
            GradientStop { position: 1.0; color: "transparent" }
        }
    }

}

And another for the bottom:

LinearGradient {
    width: parent.width
    height: vpx(50)

    anchors.bottom: parent.bottom

    // goes straight up, so the X of the point doesn't really matter
    start: Qt.point(0, height)
    end: Qt.point(0, 0)
    gradient: Gradient {
        GradientStop { position: 0.0; color: "black" }
        GradientStop { position: 1.0; color: "transparent" }
    }
}

And we're done!

Selection marker

Perhaps not easy to notice on the example images, but actually there's a white rectangular border around the current item's place on the topmost horizontal axis. It's position is fixed and does not move even during scrolling.

I'll create an empty, border-only Rectangle for it. Since it's over everything else in the theme, I'll put it to the bottom of the whole file, after the gameAxisDelegate's definition.

Rectangle {
    id: selectionMarker

    width: vpx(240)
    height: vpx(135)

    color: "transparent"
    border { width: 3; color: "white" }

    anchors.left: parent.left
    anchors.leftMargin: vpx(100)
    anchors.top: parent.verticalCenter
    anchors.topMargin: vpx(45)
}

Opacity

The currently active horizontal row is fully visible, while the rest are a bit darker. I'll set the opacity of the non-active rows to 60%. In addition, I'll add a light animation, so instead of a sudden change in the visibility, the rows gradually raise their opacity during scrolling.

Simply add these two lines to the collectionAxisDelegate:

Component {
    id: collectionAxisDelegate

    Item {
        // JS functions

        // width, height

        opacity: PathView.isCurrentItem ? 1.0 : 0.6
        Behavior on opacity { NumberAnimation { duration: 150 } }

        // ...
    }
}

Done!

With all these components added, it seems we're actually done! Here's the end result:

preview