PyQt (PySide) 使用 QML 仿制一个密码框动画

动画效果

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

下图是我仿制的动画:

PyQt (PySide) 使用 QML 仿制一个密码框动画_第1张图片

PyQt (PySide) 使用 QML 仿制一个密码框动画_第2张图片

实现思路

动画的实现

  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 个星号.
     *     激活状态: 明文显示, 这里用 "" 简单代替.
     * 动画:
     *     密码文字由不透明转为透明, 再由透明转为不透明. 当完全透明的那一刻, 迅
     *     速将密码文字由密文切换为明文.
     *     简单来说就是密文隐去, 随之明文渐现.
     */
    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 = ""
        }
    }

    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

你可能感兴趣的:(Python,GUI,界面开发)