PyQt (PySide) 使用 QML 仿製一個密碼框動畫

動畫效果

來源地址: https://uimovement.com/media/resource_image/image_5213.gif.mp4

下圖是我仿製的動畫:

在這裏插入圖片描述

慢動作效果

實現思路

動畫的實現

  1. 鎖圖標由白色變成了黑色. 鎖的圖標我們可以通過 Image 對象加載. 白色變黑色則通過附加在 Image 上的 ColorOverlay 實現.
  2. 密碼由星號變成明文. 爲了讓變化自然, 我們對星號漸隱, 明文漸入轉換.
  3. 白色矩形覆蓋填充到整個按鈕. 用 QML 來實現是比較簡單的, 就是一個屬性動畫. 我們對白色矩形 (初始時的圓形) 的各個子屬性添加屬性動畫, 指定它末狀態的值就能做出來.
  4. 眨眼動畫. 這個仔細看不是簡單的兩張靜態圖的切換, 我沒有找到合適的素材, 所以就用 gif 錄製工具把原版的眨眼截取下來了 (如下圖所示), 再把 gif 套到一個白色圓形中, 遮掩一下它方正的邊緣.

在這裏插入圖片描述

QML 佈局結構

對 QML 的組織採用面向對象的思想. 把每一個組件看作是對象, 組件之間嵌套組合構成了完整的 UI.

Window
    Password
        LockIcon
        PwdText
        EyesBlink

代碼及註釋

說明

  • 詳細的說明以通過註釋的形式給出.
  • 代碼中的屬性和變量命名風格做了自定義, 相關閱讀見此 (TODO).
  • 深入瞭解 QML 對象的使用, 請查閱 Qt 助手工具.

目錄結構

demo
|- ui
    |- Main.qml  # 這裏是 ui 的主入口.
    |- Password
        |- Main.qml  # 這裏是 Password 組件的主入口, 將被加載到 ui/Main.qml 中.
        |            # 下面的 LockIcon.qml, PwdText.qml, EyesBlink.qml 則會被本
        |            # 文件引用.
        |- LockIcon.qml
        |- PwdText.qml
        |- EyesBlink.qml
|- icon
    |- lock-white.png
    |- eyes-blink.gif
|- main.py

代碼

// === ui/Main.qml ===
import QtQuick.Window 2.14
import "./Password" as Password

Window {
    color: "#EDEDED"
    visible: true
    width: 1200; height: 600

    Password.Main {
        id: _pwd
        anchors.centerIn: parent
    }
}

// === ui/Password/Main.qml ===
import Qt3D.Animation 2.14  // 動畫模塊
import QtQuick 2.14

Rectangle {  // ui/Password/Main.qml 是密碼框的主體. 在其內引用其他子組件文件.
    id: _root

    // 設置一個深藍色的長條狀的矩形作爲密碼框的主體.
    color: "#172336"
    radius: 24
    width: 480; height: 80

    // 聲明兩個自定義的變量.
    property bool p_active: false  // 是否處於激活狀態. 默認爲 false. 只有當密碼框被點擊時纔會變成 true.
    property int p_duration: 5000  // 動畫的時長. 5000ms 的慢動作是爲了便於調試時觀察; 正式結果將改爲 500ms.

    // 定義激活時的狀態. 在這裏我們只定義了白色遮罩的激活狀態 (也就是末狀態). 其他組件 (鎖, 密碼文字, 眼睛) 則在各自的 qml 文件中定義, 不在這裏寫.
    states: [
        State {
            when: p_active  // 監聽 p_active 變量, 當值爲 true 時此狀態被激發.
            PropertyChanges {  // 定義末狀態的屬性和值.
                target: _rect_mask  // 目標對象是白色遮罩的 id.
                anchors.margins: 0  // 邊距調爲 0.
                width: _root.width; height: _root.height; radius: _root.radius  // 寬, 高, 弧度變爲根對象的值.
                x: _root.x; y: _root.y  // 座標 (左上角頂點的座標) 也變爲根對象的值.
            }
        }
    ]

    // 當 states 列表的任意一個狀態被激發時, transitions 就會因此產生動畫效果.
    transitions: [
        Transition {
            // 我們定義一個數字類型的屬性動畫. 因爲寬, 高, 弧度等值都是數字類型的.
            NumberAnimation {  
                target: _rect_mask
                duration: p_duration  // 動畫時長. 就是我們剛纔定義的 5000ms.
                easing.type: Easing.OutQuart  // 爲了讓動畫看起來自然, 我們使用非線性插值器. Easing.OutQuart 的效果是開始時快, 結束時非常緩慢, 適合表現飛入視界並獲取焦點的效果.
                properties: "anchors.margins,height,radius,width,x,y"  // 指定白色遮罩對象的這些屬性發生變化.
            }
        }
    ]

    // 白色遮罩. 始狀態是一個圓形, 位於密碼框的右側.
    Rectangle {
        id: _rect_mask

        // 對齊: 對齊到根對象右側, 邊距爲 24px, 與根對象垂直居中.
        anchors.margins: 24
        anchors.right: _root.right
        anchors.verticalCenter: _root.verticalCenter
        
        color: "white"
        width: 48; height: 48; radius: 24  // 注意看這裏, 當 width == height 且 radius == 1/2 width 時, 矩形就是一個圓形.

        // 綁定點擊區域.
        MouseArea {
            anchors.fill: _rect_mask
            onClicked: {
                p_active = true  // 當白色遮罩被點擊時, p_active 變爲 true. 這時候我們再去看 states. State 的 when 屬性會自動監聽到這個變化, 並激發這個狀態, 從而引起 transitions 動畫生效, 整個動畫開始發生.
            }
        }
    }

    // 右側的眨眼動畫. 因爲這個 gif 是方形的, 所以和白色遮罩疊在一起, 把方形邊緣遮住.
    EyesBlink {
        id: _eye
        
        // 這個對齊值是反覆調整出來的. 最終要的效果是: 看起來要比白色遮罩小, 不能把方形邊緣漏出來, 還要看起來位於其中心.
        anchors.right: _root.right
        anchors.rightMargin: 34
        anchors.top: _lock.top
        anchors.topMargin: 4
        width: 28; height: 28

        // 把根對象的 p_active 綁定到動畫播放屬性上. 這樣點擊時纔會播放眨眼動作.
        p_active: _root.p_active
        speed: 4  // 注意 EyesBlink 的動畫時長不遵循 p_duration, 而是其 gif 文件的時長除以 speed. speed 默認爲 1, 這裏被我設置成了 4, 爲了看起來更快一點.
    }

    // 鎖圖標的組件. 這裏只覆寫了錨點, 尺寸和變量屬性. 詳見 ui/Password/LockIcon.qml.
    LockIcon {
        id: _lock
        anchors.left: _root.left
        anchors.margins: 24
        anchors.verticalCenter: _root.verticalCenter

        p_active: _root.p_active
        p_duration: _root.p_duration

        obj_Image {
            width: 32; height: 32
        }
    }

    // 密碼文字的組件. 這裏只覆寫了錨點和變量屬性. 詳見 ui/Password/PwdText.qml.
    PwdText {
        anchors.left: _lock.right
        anchors.leftMargin: 12
        anchors.verticalCenter: _root.verticalCenter

        p_active: _root.p_active
        p_duration: _root.p_duration
    }
}

// === ui/Password/LockIcon.qml ===
import QtGraphicalEffects 1.14  // 用於製作 ColorOverlay
import QtQuick 2.14

Item {
    width: _icon.width; height: _icon.height

    property alias obj_Image: _icon  // 將子對象圖標暴露給外部. 從而使父級可以引用 (因爲我們想在父級定義它的寬度和高度).
    property bool p_active: false  // 激活狀態. 默認爲 false. 同樣被父級定義, 此屬性會被綁定到父級的 p_active 屬性上.
    property int p_duration: 0  // 動畫時長. 同樣被父級定義.

    // 透明背景的鎖形圖標.
    Image {
        id: _icon
        source: "../../icon/lock-white.png"
    }

    // 由於鎖圖標是白色的, 我們需要在 p_active = true 狀態將它變成黑色, 所以使用 ColorOverlay 實現.
    // ColorOverlay 可以覆蓋目標對象的顏色, 並且我們還可以對 ColorOverlay 的 color 屬性綁定一個過渡動畫.
    ColorOverlay {
        id: _overlay
        source: _icon
        anchors.fill: _icon
        //color: "white"
    }

    // 定義默認狀態和激活狀態的 ColorOverlay.
    states: [
        State {
            name: "defaultState"
            when: !p_active
            PropertyChanges {
                target: _overlay
                color: "white"
            }
        },
        State {
            name: "activeState"
            when: p_active
            PropertyChanges {
                target: _overlay
                color: "black"
            }
        }
    ]

    // 當狀態發生變化時, Transition 會被自動觸發, 實現動畫過程.
    transitions: [
        Transition {
            ColorAnimation {
                target: _overlay
                duration: p_duration
            }
        }
    ]
}

// === ui/Password/PwdText.qml ===
import QtQuick 2.14

Text {
    /* 密碼文字存在兩種狀態:
     *     默認狀態: 密文顯示. 以星號 (*) 顯示, 密文的長度是 12 個星號.
     *     激活狀態: 明文顯示, 這裏用 "<this is the real password>" 簡單代替.
     * 動畫:
     *     密碼文字由不透明轉爲透明, 再由透明轉爲不透明. 當完全透明的那一刻, 迅
     *     速將密碼文字由密文切換爲明文.
     *     簡單來說就是密文隱去, 隨之明文漸現.
     */
    id: _root

    color: "#585DC5"
    font.pixelSize: 24
    opacity: 1  // 不透明度. 數值從 0.0 到 1.0. (1.0 爲完全顯示.)
    text: "* ".repeat(p_pwdLength)  // 爲了讓星號之間空隙大一點, 我夾了空格.

    property bool p_active: false  // 激活狀態. 默認爲 false. 同樣被父級定義, 此屬性會被綁定到父級的 p_active 屬性上.
    property int p_duration: 0  // 動畫時長. 同樣被父級定義.
    property int p_pwdLength: 12

    // 我們監聽 opacity 屬性的變化, 當 opacity 變化時, 此信號會被自動觸發.
    onOpacityChanged: {
        // 進入一個判斷邏輯: 當完全透明且處於 p_active 狀態, 則將 text 變成明文.
        if (opacity == 0 && p_active) {
            text = "<this is the real password>"
        }
    }

    states: [
        State {
            name: "showPwdInPlainText"
            when: p_active
            PropertyChanges {
                target: _root
                opacity: 1
            }
        }
    ]

    transitions: [
        Transition {  // 使用序列動畫. 前 20% 時間是漸隱動畫, 後 80% 時間是漸入動畫.
            SequentialAnimation {
                NumberAnimation {
                    duration: p_duration * 0.2
                    easing.type: Easing.OutQuart
                    properties: "opacity"
                    from: 1; to: 0
                }  // 注意這裏不要有逗號.
                NumberAnimation {
                    duration: p_duration * 0.8
                    easing.type: Easing.OutQuart
                    properties: "opacity"
                    from: 0; to: 1
                }
            }
        }
    ]
}

// === ui/Password/EyesBlink.qml ===
import QtQuick 2.14

AnimatedImage {  // AnimatedImage 對象專用於加載可動的圖像. 詳見 Qt 助手 QML > AnimatedImage.
    id: _img
    playing: p_active  // 初始化載入時, gif 動畫設爲暫停狀態.
    source: "../../icon/eyes-blink.gif"
    
    property bool p_active: false  // 激活狀態. 默認爲 false. 同樣被父級定義, 此屬性會被綁定到父級的 p_active 屬性上.

    // 當 AnimatedImage 播放時, 其 currentFrame 會發生變化, 此信號的內置監聽方法 onCurrentFrameChanged 會被自動觸發.
    // 我們判斷當 currentFrame 播放到最後一幀時, 停止動畫, 以免陷入循環播放.
    onCurrentFrameChanged: {
        if (currentFrame == frameCount - 1) {  // 因爲 currentFrame 是從 0 開始數的, 所以這裏要減一.
            playing = false
            // paused = true
        }
    }
}

最後是 main.py 代碼:

# === main.py ===
from PySide2.QtQml import QQmlApplicationEngine
from PySide2.QtWidgets import QApplication

app = QApplication()
engine = QQmlApplicationEngine('./ui/Main.qml')
app.exec_()

源碼及附件

源碼及圖標文件以打包, 下載鏈接見此: https://lanzous.com/ica5cla

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章