feat: FluCarousel支持纵向轮播

This commit is contained in:
Polaris-Night 2025-03-12 23:38:26 +08:00
parent 9ac58a8ca7
commit 8377fb5227
6 changed files with 355 additions and 98 deletions

View File

@ -1123,6 +1123,11 @@ Updated content:
<source>Carousel map, support infinite carousel, infinite swipe, and components implemented with ListView</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="qml/page/T_Carousel.qml" line="203"/>
<source>Auto play</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>T_CheckBox</name>

View File

@ -1142,12 +1142,17 @@ Updated content:
<message>
<location filename="qml/page/T_Carousel.qml" line="10"/>
<source>Carousel</source>
<translation type="unfinished"></translation>
<translation></translation>
</message>
<message>
<location filename="qml/page/T_Carousel.qml" line="36"/>
<source>Carousel map, support infinite carousel, infinite swipe, and components implemented with ListView</source>
<translation type="unfinished">ListView实现的组件</translation>
<translation>ListView实现的组件</translation>
</message>
<message>
<location filename="qml/page/T_Carousel.qml" line="203"/>
<source>Auto play</source>
<translation></translation>
</message>
</context>
<context>

View File

@ -121,7 +121,6 @@ FluScrollablePage{
}
}
CodeExpander{
Layout.fillWidth: true
Layout.topMargin: -6
@ -140,6 +139,90 @@ FluScrollablePage{
Component.onCompleted: {
carousel.model = [{url:"qrc:/example/res/image/banner_1.jpg"},{url:"qrc:/example/res/image/banner_2.jpg"},{url:"qrc:/example/res/image/banner_3.jpg"}]
}
}'
}
FluFrame{
Layout.fillWidth: true
Layout.preferredHeight: 300 + topPadding + bottomPadding
padding: 10
Layout.topMargin: 10
RowLayout{
anchors.fill: parent
Item{
Layout.preferredWidth: 400
Layout.fillHeight: true
FluShadow{
radius: 8
}
FluCarousel{
anchors.fill: parent
orientation: Qt.Vertical
autoPlay: auto_play_switch.checked
loopTime:1500
indicatorGravity: Qt.AlignVCenter | Qt.AlignRight
indicatorMarginTop:15
delegate: Component{
Item{
anchors.fill: parent
Image {
anchors.fill: parent
source: model.url
asynchronous: true
fillMode:Image.PreserveAspectCrop
}
Rectangle{
height: 40
width: parent.width
anchors.bottom: parent.bottom
color: "#33000000"
FluText{
anchors.fill: parent
verticalAlignment: Qt.AlignVCenter
horizontalAlignment: Qt.AlignHCenter
text:model.title
color: FluColors.Grey10
}
}
}
}
Layout.topMargin: 20
Layout.leftMargin: 5
Component.onCompleted: {
var arr = []
arr.push({url:"qrc:/example/res/image/banner_1.jpg",title:"共同应对全球性问题"})
arr.push({url:"qrc:/example/res/image/banner_2.jpg",title:"三小只全程没互动"})
arr.push({url:"qrc:/example/res/image/banner_3.jpg",title:"有效投资扩大 激发增长动能"})
model = arr
}
}
}
FluToggleSwitch{
id: auto_play_switch
Layout.alignment: Qt.AlignRight
text: qsTr("Auto play")
}
}
}
CodeExpander{
Layout.fillWidth: true
Layout.topMargin: -6
code:'FluCarousel{
id:carousel
width: 400
height: 300
orientation: Qt.Vertical
delegate: Component{
Image {
anchors.fill: parent
source: model.url
asynchronous: true
fillMode:Image.PreserveAspectCrop
}
}
Component.onCompleted: {
carousel.model = [{url:"qrc:/example/res/image/banner_1.jpg"},{url:"qrc:/example/res/image/banner_2.jpg"},{url:"qrc:/example/res/image/banner_3.jpg"}]
}
}'
}
}

View File

@ -4,6 +4,7 @@ import FluentUI 1.0
Item {
property bool autoPlay: true
property int orientation: Qt.Horizontal
property int loopTime: 2000
property var model
property Component delegate
@ -14,7 +15,7 @@ Item {
property int indicatorMarginTop: 0
property int indicatorMarginBottom: 20
property int indicatorSpacing: 10
property alias indicatorAnchors: layout_indicator.anchors
property alias indicatorAnchors: indicator_loader.anchors
property Component indicatorDelegate : com_indicator
id:control
width: 400
@ -24,13 +25,24 @@ Item {
}
QtObject{
id:d
property bool flagXChanged: false
property bool isManualMoving: false
property bool isAnimEnable: control.autoPlay && list_view.count>3
onIsAnimEnableChanged: {
if(isAnimEnable){
timer_run.restart()
}else{
timer_run.stop()
}
}
function setData(data){
if(!data){
if(!data || !Array.isArray(data)){
return
}
content_model.clear()
list_view.resetPos()
if(data.length === 0){
return
}
content_model.append(data[data.length-1])
content_model.append(data)
content_model.append(data[0])
@ -49,7 +61,7 @@ Item {
clip: true
boundsBehavior: ListView.StopAtBounds
model:content_model
maximumFlickVelocity: 4 * (list_view.orientation === Qt.Horizontal ? width : height)
maximumFlickVelocity: 4 * (control.orientation === Qt.Vertical ? height : width)
preferredHighlightBegin: 0
preferredHighlightEnd: 0
highlightMoveDuration: 0
@ -63,7 +75,7 @@ Item {
d.setData(control.model)
}
}
orientation : ListView.Horizontal
orientation : control.orientation
delegate: Item{
id:item_control
width: ListView.view.width
@ -88,34 +100,63 @@ Item {
}
}
onMovementEnded:{
d.flagXChanged = false
d.isManualMoving = false
list_view.highlightMoveDuration = 0
currentIndex = list_view.contentX/list_view.width
if(currentIndex === 0){
currentIndex = list_view.count-2
}else if(currentIndex === list_view.count-1){
currentIndex = 1
if(control.orientation === Qt.Vertical){
currentIndex = (list_view.contentY - list_view.originY) / list_view.height
if(currentIndex === 0){
currentIndex = list_view.count - 2
}else if(currentIndex === list_view.count - 1) {
currentIndex = 1
}
} else {
currentIndex = (list_view.contentX - list_view.originX) / list_view.width
if(currentIndex === 0){
currentIndex = list_view.count - 2
}else if(currentIndex === list_view.count - 1){
currentIndex = 1
}
}
if(d.isAnimEnable){
timer_run.restart()
}
}
onMovementStarted: {
d.flagXChanged = true
d.isManualMoving = true
timer_run.stop()
}
onContentXChanged: {
if(d.flagXChanged){
var maxX = Math.min(list_view.width*(currentIndex+1),list_view.count*list_view.width)
var minX = Math.max(0,(list_view.width*(currentIndex-1)))
if(contentX>=maxX){
contentX = maxX
if(d.isManualMoving && control.orientation === Qt.Horizontal){
const range = getPosRange(list_view.width, currentIndex)
if(contentX >= range.max){
contentX = range.max
}
if(contentX<=minX){
contentX = minX
if(contentX <= range.min){
contentX = range.min
}
}
}
onContentYChanged: {
if(d.isManualMoving && control.orientation === Qt.Vertical){
const range = getPosRange(list_view.height, currentIndex)
if(contentY >= range.max){
contentY = range.max
}
if(contentY <= range.min){
contentY = range.min
}
}
}
function resetPos() {
contentX = 0
contentY = 0
}
function getPosRange(size, index) {
return {
"min": Math.max(0, size * (index - 1)),
"max": Math.min(size * (index + 1), list_view.count * size)
}
}
}
Component{
id:com_indicator
@ -140,9 +181,9 @@ Item {
}
}
}
Row{
id:layout_indicator
spacing: control.indicatorSpacing
Loader{
id: indicator_loader
anchors{
horizontalCenter:(indicatorGravity & Qt.AlignHCenter) ? parent.horizontalCenter : undefined
verticalCenter: (indicatorGravity & Qt.AlignVCenter) ? parent.verticalCenter : undefined
@ -155,28 +196,66 @@ Item {
rightMargin: control.indicatorMarginBottom
topMargin: control.indicatorMarginBottom
}
visible: showIndicator
Repeater{
id:repeater_indicator
model: list_view.count
FluLoader{
property int displayIndex: {
if(index === 0)
return list_view.count-3
if(index === list_view.count-1)
return 0
return index-1
}
property int realIndex: index
property bool checked: list_view.currentIndex === index
sourceComponent: {
if(index===0 || index===list_view.count-1)
return undefined
return control.indicatorDelegate
active: showIndicator
sourceComponent: control.orientation === Qt.Vertical ? column_indicator : row_indicator
}
Component{
id: row_indicator
Row{
id:layout_indicator
spacing: control.indicatorSpacing
Repeater{
id:repeater_indicator
model: list_view.count
FluLoader{
property int displayIndex: {
if(index === 0)
return list_view.count-3
if(index === list_view.count-1)
return 0
return index-1
}
property int realIndex: index
property bool checked: list_view.currentIndex === index
sourceComponent: {
if(index===0 || index===list_view.count-1)
return undefined
return control.indicatorDelegate
}
}
}
}
}
Component{
id: column_indicator
Column{
id:layout_indicator
spacing: control.indicatorSpacing
Repeater{
id:repeater_indicator
model: list_view.count
FluLoader{
property int displayIndex: {
if(index === 0)
return list_view.count-3
if(index === list_view.count-1)
return 0
return index-1
}
property int realIndex: index
property bool checked: list_view.currentIndex === index
sourceComponent: {
if(index===0 || index===list_view.count-1)
return undefined
return control.indicatorDelegate
}
}
}
}
}
Timer{
id:timer_anim
interval: 250
@ -198,10 +277,10 @@ Item {
}
}
function changedIndex(index){
d.flagXChanged = true
d.isManualMoving = true
timer_run.stop()
list_view.currentIndex = index
d.flagXChanged = false
d.isManualMoving = false
if(d.isAnimEnable){
timer_run.restart()
}

View File

@ -4,7 +4,7 @@ import QtQuick.tooling 1.2
// It is used for QML tooling purposes only.
//
// This file was auto-generated by:
// 'qmlplugindump -nonrelocatable -noinstantiate FluentUI 1.0 F:/FluentUI/build/Desktop_Qt_5_15_2_MSVC2019_32bit-Release/src'
// 'qmlplugindump -nonrelocatable -noinstantiate FluentUI 1.0 E:/develop/QtCode/opensource/FluentUI/build/Desktop_Qt_5_15_2_MinGW_64_bit-Release/src'
Module {
dependencies: ["QtQuick 2.0"]
@ -2776,7 +2776,7 @@ Module {
}
Property {
name: "layoutMacosButtons"
type: "FluLoader_QMLTYPE_11"
type: "FluLoader_QMLTYPE_14"
isReadonly: true
isPointer: true
}
@ -2898,6 +2898,7 @@ Module {
isComposite: true
defaultProperty: "data"
Property { name: "autoPlay"; type: "bool" }
Property { name: "orientation"; type: "int" }
Property { name: "loopTime"; type: "int" }
Property { name: "model"; type: "QVariant" }
Property { name: "delegate"; type: "QQmlComponent"; isPointer: true }
@ -2955,12 +2956,12 @@ Module {
Property { name: "checkedDisableColor"; type: "QColor" }
Property { name: "disableColor"; type: "QColor" }
Property { name: "size"; type: "double" }
Property { name: "textColor"; type: "QColor" }
Property { name: "textRight"; type: "bool" }
Property { name: "textSpacing"; type: "double" }
Property { name: "animationEnabled"; type: "bool" }
Property { name: "clickListener"; type: "QVariant" }
Property { name: "indeterminate"; type: "bool" }
Property { name: "textColor"; type: "QColor" }
}
Component {
prototype: "FluRectangle"
@ -3189,6 +3190,8 @@ Module {
isComposite: true
defaultProperty: "content"
Property { name: "headerText"; type: "string" }
Property { name: "headerHeight"; type: "int" }
Property { name: "headerDelegate"; type: "QQmlComponent"; isPointer: true }
Property { name: "expand"; type: "bool" }
Property { name: "contentHeight"; type: "int" }
Property { name: "content"; type: "QObject"; isList: true; isReadonly: true }
@ -3469,15 +3472,15 @@ Module {
defaultProperty: "data"
Property { name: "logo"; type: "QUrl" }
Property { name: "title"; type: "string" }
Property { name: "items"; type: "FluObject_QMLTYPE_176"; isPointer: true }
Property { name: "footerItems"; type: "FluObject_QMLTYPE_176"; isPointer: true }
Property { name: "items"; type: "FluObject_QMLTYPE_182"; isPointer: true }
Property { name: "footerItems"; type: "FluObject_QMLTYPE_182"; isPointer: true }
Property { name: "displayMode"; type: "int" }
Property { name: "autoSuggestBox"; type: "QQmlComponent"; isPointer: true }
Property { name: "actionItem"; type: "QQmlComponent"; isPointer: true }
Property { name: "topPadding"; type: "int" }
Property { name: "pageMode"; type: "int" }
Property { name: "navItemRightMenu"; type: "FluMenu_QMLTYPE_48"; isPointer: true }
Property { name: "navItemExpanderRightMenu"; type: "FluMenu_QMLTYPE_48"; isPointer: true }
Property { name: "navItemRightMenu"; type: "FluMenu_QMLTYPE_47"; isPointer: true }
Property { name: "navItemExpanderRightMenu"; type: "FluMenu_QMLTYPE_47"; isPointer: true }
Property { name: "navCompactWidth"; type: "int" }
Property { name: "navTopMargin"; type: "int" }
Property { name: "cellHeight"; type: "int" }
@ -3676,6 +3679,7 @@ Module {
exportMetaObjectRevisions: [0]
isComposite: true
defaultProperty: "content"
Property { name: "textHighlightColor"; type: "QColor" }
Property { name: "textNormalColor"; type: "QColor" }
Property { name: "textHoverColor"; type: "QColor" }
Property { name: "textSpacing"; type: "int" }
@ -3772,11 +3776,11 @@ Module {
Property { name: "normalColor"; type: "QColor" }
Property { name: "hoverColor"; type: "QColor" }
Property { name: "disableColor"; type: "QColor" }
Property { name: "textColor"; type: "QColor" }
Property { name: "size"; type: "double" }
Property { name: "textRight"; type: "bool" }
Property { name: "textSpacing"; type: "double" }
Property { name: "clickListener"; type: "QVariant" }
Property { name: "textColor"; type: "QColor" }
}
Component {
prototype: "QQuickItem"
@ -3899,7 +3903,9 @@ Module {
exportMetaObjectRevisions: [0]
isComposite: true
defaultProperty: "content"
Property { name: "autoResetScroll"; type: "bool" }
Property { name: "content"; type: "QObject"; isList: true; isReadonly: true }
Method { name: "resetScroll"; type: "QVariant" }
Property { name: "launchMode"; type: "int" }
Property { name: "animationEnabled"; type: "bool" }
Property { name: "url"; type: "string" }
@ -4294,8 +4300,8 @@ Module {
Property { name: "dotDisableColor"; type: "QColor" }
Property { name: "textSpacing"; type: "double" }
Property { name: "textRight"; type: "bool" }
Property { name: "clickListener"; type: "QVariant" }
Property { name: "textColor"; type: "QColor" }
Property { name: "clickListener"; type: "QVariant" }
}
Component {
prototype: "QQuickToolTip"
@ -4379,7 +4385,6 @@ Module {
Property { name: "fitsAppBarWindows"; type: "bool" }
Property { name: "tintOpacity"; type: "QVariant" }
Property { name: "blurRadius"; type: "int" }
Property { name: "availableEffects"; type: "QVariant"; isReadonly: true }
Property { name: "appBar"; type: "QQuickItem"; isPointer: true }
Property { name: "backgroundColor"; type: "QColor" }
Property { name: "stayTop"; type: "bool" }
@ -4403,6 +4408,7 @@ Module {
Property { name: "contentData"; type: "QObject"; isList: true; isReadonly: true }
Property { name: "effect"; type: "string" }
Property { name: "effective"; type: "bool"; isReadonly: true }
Property { name: "availableEffects"; type: "QStringList"; isReadonly: true }
Signal {
name: "initArgument"
Parameter { name: "argument"; type: "QVariant" }
@ -4485,7 +4491,6 @@ Module {
Property { name: "fitsAppBarWindows"; type: "bool" }
Property { name: "tintOpacity"; type: "QVariant" }
Property { name: "blurRadius"; type: "int" }
Property { name: "availableEffects"; type: "QVariant"; isReadonly: true }
Property { name: "appBar"; type: "QQuickItem"; isPointer: true }
Property { name: "backgroundColor"; type: "QColor" }
Property { name: "stayTop"; type: "bool" }
@ -4509,6 +4514,7 @@ Module {
Property { name: "contentData"; type: "QObject"; isList: true; isReadonly: true }
Property { name: "effect"; type: "string" }
Property { name: "effective"; type: "bool"; isReadonly: true }
Property { name: "availableEffects"; type: "QStringList"; isReadonly: true }
Signal {
name: "initArgument"
Parameter { name: "argument"; type: "QVariant" }

View File

@ -4,6 +4,7 @@ import FluentUI
Item {
property bool autoPlay: true
property int orientation: Qt.Horizontal
property int loopTime: 2000
property var model
property Component delegate
@ -14,7 +15,7 @@ Item {
property int indicatorMarginTop: 0
property int indicatorMarginBottom: 20
property int indicatorSpacing: 10
property alias indicatorAnchors: layout_indicator.anchors
property alias indicatorAnchors: indicator_loader.anchors
property Component indicatorDelegate : com_indicator
id:control
width: 400
@ -24,13 +25,24 @@ Item {
}
QtObject{
id:d
property bool flagXChanged: false
property bool isManualMoving: false
property bool isAnimEnable: control.autoPlay && list_view.count>3
onIsAnimEnableChanged: {
if(isAnimEnable){
timer_run.restart()
}else{
timer_run.stop()
}
}
function setData(data){
if(!data){
if(!data || !Array.isArray(data)){
return
}
content_model.clear()
list_view.resetPos()
if(data.length === 0){
return
}
content_model.append(data[data.length-1])
content_model.append(data)
content_model.append(data[0])
@ -49,7 +61,7 @@ Item {
clip: true
boundsBehavior: ListView.StopAtBounds
model:content_model
maximumFlickVelocity: 4 * (list_view.orientation === Qt.Horizontal ? width : height)
maximumFlickVelocity: 4 * (control.orientation === Qt.Vertical ? height : width)
preferredHighlightBegin: 0
preferredHighlightEnd: 0
highlightMoveDuration: 0
@ -63,7 +75,7 @@ Item {
d.setData(control.model)
}
}
orientation : ListView.Horizontal
orientation : control.orientation
delegate: Item{
id:item_control
width: ListView.view.width
@ -88,34 +100,63 @@ Item {
}
}
onMovementEnded:{
d.flagXChanged = false
d.isManualMoving = false
list_view.highlightMoveDuration = 0
currentIndex = list_view.contentX/list_view.width
if(currentIndex === 0){
currentIndex = list_view.count-2
}else if(currentIndex === list_view.count-1){
currentIndex = 1
if(control.orientation === Qt.Vertical){
currentIndex = (list_view.contentY - list_view.originY) / list_view.height
if(currentIndex === 0){
currentIndex = list_view.count - 2
}else if(currentIndex === list_view.count - 1) {
currentIndex = 1
}
} else {
currentIndex = (list_view.contentX - list_view.originX) / list_view.width
if(currentIndex === 0){
currentIndex = list_view.count - 2
}else if(currentIndex === list_view.count - 1){
currentIndex = 1
}
}
if(d.isAnimEnable){
timer_run.restart()
}
}
onMovementStarted: {
d.flagXChanged = true
d.isManualMoving = true
timer_run.stop()
}
onContentXChanged: {
if(d.flagXChanged){
var maxX = Math.min(list_view.width*(currentIndex+1),list_view.count*list_view.width)
var minX = Math.max(0,(list_view.width*(currentIndex-1)))
if(contentX>=maxX){
contentX = maxX
if(d.isManualMoving && control.orientation === Qt.Horizontal){
const range = getPosRange(list_view.width, currentIndex)
if(contentX >= range.max){
contentX = range.max
}
if(contentX<=minX){
contentX = minX
if(contentX <= range.min){
contentX = range.min
}
}
}
onContentYChanged: {
if(d.isManualMoving && control.orientation === Qt.Vertical){
const range = getPosRange(list_view.height, currentIndex)
if(contentY >= range.max){
contentY = range.max
}
if(contentY <= range.min){
contentY = range.min
}
}
}
function resetPos() {
contentX = 0
contentY = 0
}
function getPosRange(size, index) {
return {
"min": Math.max(0, size * (index - 1)),
"max": Math.min(size * (index + 1), list_view.count * size)
}
}
}
Component{
id:com_indicator
@ -140,9 +181,9 @@ Item {
}
}
}
Row{
id:layout_indicator
spacing: control.indicatorSpacing
Loader{
id: indicator_loader
anchors{
horizontalCenter:(indicatorGravity & Qt.AlignHCenter) ? parent.horizontalCenter : undefined
verticalCenter: (indicatorGravity & Qt.AlignVCenter) ? parent.verticalCenter : undefined
@ -155,28 +196,66 @@ Item {
rightMargin: control.indicatorMarginBottom
topMargin: control.indicatorMarginBottom
}
visible: showIndicator
Repeater{
id:repeater_indicator
model: list_view.count
FluLoader{
property int displayIndex: {
if(index === 0)
return list_view.count-3
if(index === list_view.count-1)
return 0
return index-1
}
property int realIndex: index
property bool checked: list_view.currentIndex === index
sourceComponent: {
if(index===0 || index===list_view.count-1)
return undefined
return control.indicatorDelegate
active: showIndicator
sourceComponent: control.orientation === Qt.Vertical ? column_indicator : row_indicator
}
Component{
id: row_indicator
Row{
id:layout_indicator
spacing: control.indicatorSpacing
Repeater{
id:repeater_indicator
model: list_view.count
FluLoader{
property int displayIndex: {
if(index === 0)
return list_view.count-3
if(index === list_view.count-1)
return 0
return index-1
}
property int realIndex: index
property bool checked: list_view.currentIndex === index
sourceComponent: {
if(index===0 || index===list_view.count-1)
return undefined
return control.indicatorDelegate
}
}
}
}
}
Component{
id: column_indicator
Column{
id:layout_indicator
spacing: control.indicatorSpacing
Repeater{
id:repeater_indicator
model: list_view.count
FluLoader{
property int displayIndex: {
if(index === 0)
return list_view.count-3
if(index === list_view.count-1)
return 0
return index-1
}
property int realIndex: index
property bool checked: list_view.currentIndex === index
sourceComponent: {
if(index===0 || index===list_view.count-1)
return undefined
return control.indicatorDelegate
}
}
}
}
}
Timer{
id:timer_anim
interval: 250
@ -198,10 +277,10 @@ Item {
}
}
function changedIndex(index){
d.flagXChanged = true
d.isManualMoving = true
timer_run.stop()
list_view.currentIndex = index
d.flagXChanged = false
d.isManualMoving = false
if(d.isAnimEnable){
timer_run.restart()
}