Improved StackLayout push/pop animation

This commit is contained in:
Alexei Anoshenko 2024-11-24 08:52:43 +02:00
parent 31c07ced98
commit ed639c94c6
9 changed files with 480 additions and 280 deletions

View File

@ -1,9 +1,13 @@
# v0.18.0
* Property name type changed to PropertyName.
* Property name type changed from string to PropertyName.
* Transform interface renamed to TransformProperty. NewTransform function renamed to NewTransformProperty. TransformTag constant renamed to Transform.
* OriginX, OriginY, and OriginZ properties renamed to TransformOriginX, TransformOriginY, and TransformOriginZ
* GetOrigin function renamed to GetTransformOrigin
* GetOrigin function renamed to GetTransformOrigin.
* Changed Push and Pop method of StackLayout interface.
* Removed DefaultAnimation, StartToEndAnimation, EndToStartAnimation, TopDownAnimation, and BottomUpAnimation constants.
* Added "push-transform", "push-duration", "push-timing", and "move-to-front-animation" properties.
* Added GetPushDuration, GetPushTiming, and IsMoveToFrontAnimation functions.
* Added LineJoin type. Type of constants MiterJoin, RoundJoin, and BevelJoin changed to LineJoin. Type of Canvas.SetLineJoin function argument changed to LineJoin.
* Added LineCap type. Type of constants ButtCap, RoundCap, and SquareCap changed to LineCap. Type of Canvas.SetLineCap function argument changed to LineCap.

View File

@ -556,7 +556,7 @@ func (animation *animationData) animationCSS(session Session) string {
buffer.WriteString(" 1s ")
}
buffer.WriteString(animation.timingFunctionCSS(session))
buffer.WriteString(timingFunctionCSS(animation, TimingFunction, session))
if delay, ok := floatProperty(animation, Delay, session, 0); ok && delay > 0 {
buffer.WriteString(fmt.Sprintf(" %gs", delay))
@ -594,7 +594,7 @@ func (animation *animationData) transitionCSS(buffer *strings.Builder, session S
buffer.WriteString(" 1s ")
}
buffer.WriteString(animation.timingFunctionCSS(session))
buffer.WriteString(timingFunctionCSS(animation, TimingFunction, session))
if delay, ok := floatProperty(animation, Delay, session, 0); ok && delay > 0 {
buffer.WriteString(fmt.Sprintf(" %gs", delay))
@ -643,8 +643,8 @@ func (animation *animationData) writeTransitionString(tag PropertyName, buffer *
buffer.WriteString(" }")
}
func (animation *animationData) timingFunctionCSS(session Session) string {
if timingFunction, ok := stringProperty(animation, TimingFunction, session); ok {
func timingFunctionCSS(properties Properties, tag PropertyName, session Session) string {
if timingFunction, ok := stringProperty(properties, tag, session); ok {
if timingFunction, ok = session.resolveConstants(timingFunction); ok && isTimingFunctionValid(timingFunction) {
return timingFunction
}

View File

@ -62,6 +62,7 @@ var boolProperties = []PropertyName{
Repeating,
UserSelect,
ColumnSpanAll,
MoveToFrontAnimation,
}
var intProperties = []PropertyName{
@ -88,6 +89,7 @@ var floatProperties = map[PropertyName]struct{ min, max float64 }{
ProgressBarValue: {min: 0, max: math.MaxFloat64},
VideoWidth: {min: 0, max: 10000},
VideoHeight: {min: 0, max: 10000},
PushDuration: {min: 0, max: math.MaxFloat64},
}
var sizeProperties = map[PropertyName]string{

View File

@ -2,22 +2,62 @@ package rui
import (
"fmt"
"strconv"
"strings"
)
// Constants which represent [StackLayout] animation type during pushing or popping views
const (
// DefaultAnimation - default animation of StackLayout push
DefaultAnimation = 0
// StartToEndAnimation - start to end animation of StackLayout push
StartToEndAnimation = 1
// EndToStartAnimation - end to start animation of StackLayout push
EndToStartAnimation = 2
// TopDownAnimation - top down animation of StackLayout push
TopDownAnimation = 3
// BottomUpAnimation - bottom up animation of StackLayout push
BottomUpAnimation = 4
// PushTransform is the constant for "push-transform" property tag.
//
// Used by `StackLayout`.
// Specify start translation, scale and rotation over x, y and z axes as well as a distortion
// for an animated pushing of a child view.
//
// Supported types: `TransformProperty`, `string`.
//
// See `TransformProperty` description for more details.
//
// Conversion rules:
// `TransformProperty` - stored as is, no conversion performed.
// `string` - string representation of `Transform` interface. Example: "_{translate-x = 10px, scale-y = 1.1}".
PushTransform = "push-transform"
// PushDuration is the constant for "push-duration" property tag.
//
// Used by `StackLayout`.
// Sets the length of time in seconds that an push/pop animation takes to complete.
//
// Supported types: `float`, `int`, `string`.
//
// Internal type is `float`, other types converted to it during assignment.
PushDuration = "push-duration"
// PushTiming is the constant for "push-timing" property tag.
//
// Used by `StackLayout`.
// Set how an push/pop animation progresses through the duration of each cycle.
//
// Supported types: `string`.
//
// Values:
// "ease"(`EaseTiming`) - Speed increases towards the middle and slows down at the end.
// "ease-in"(`EaseInTiming`) - Speed is slow at first, but increases in the end.
// "ease-out"(`EaseOutTiming`) - Speed is fast at first, but decreases in the end.
// "ease-in-out"(`EaseInOutTiming`) - Speed is slow at first, but quickly increases and at the end it decreases again.
// "linear"(`LinearTiming`) - Constant speed.
PushTiming = "push-timing"
// MoveToFrontAnimation is the constant for "move-to-front-animation" property tag.
//
// Used by `StackLayout`.
// Specifies whether animation is used when calling the MoveToFront/MoveToFrontByID method of StackLayout interface.
//
// Supported types: `bool`, `int`, `string`.
//
// Values:
// `true` or `1` or "true", "yes", "on", "1" - animation is used (default value).
// `false` or `0` or "false", "no", "off", "0" - animation is not used.
MoveToFrontAnimation = "move-to-front-animation"
)
// StackLayout represents a StackLayout view
@ -45,21 +85,28 @@ type StackLayout interface {
// * TopDownAnimation (3) - Top-down animation;
// * BottomUpAnimation (4) - Bottom up animation.
// The third argument `onPushFinished` is the function to be called when the animation ends. It may be nil.
Push(view View, animation int, onPushFinished func())
Push(view View, onPushFinished func())
// Pop removes the current View from the container using animation.
// The second argument `onPopFinished`` is the function to be called when the animation ends. It may be nil.
// The function will return false if the StackLayout is empty and true if the current item has been removed.
Pop(animation int, onPopFinished func(View)) bool
Pop(onPopFinished func(View)) bool
}
type pushFinished struct {
peekID string
listener func()
}
type popFinished struct {
view View
listener func(View)
}
type stackLayoutData struct {
viewsContainerData
peek, prevPeek int
pushView, popView View
animationType int
onPushFinished func()
onPopFinished func(View)
onPushFinished map[string]pushFinished
onPopFinished map[string]popFinished
}
// NewStackLayout create new StackLayout object and return it
@ -80,44 +127,88 @@ func (layout *stackLayoutData) init(session Session) {
layout.viewsContainerData.init(session)
layout.tag = "StackLayout"
layout.systemClass = "ruiStackLayout"
layout.properties[TransitionEndEvent] = []func(View, PropertyName){layout.pushFinished, layout.popFinished}
layout.get = layout.getFunc
layout.onPushFinished = map[string]pushFinished{}
layout.onPopFinished = map[string]popFinished{}
layout.set = layout.setFunc
layout.remove = layout.removeFunc
layout.changed = layout.propertyChanged
}
func (layout *stackLayoutData) pushFinished(view View, tag PropertyName) {
if tag == "ruiPush" {
if layout.pushView != nil {
layout.pushView = nil
count := len(layout.views)
if count > 0 {
layout.peek = count - 1
layout.setRaw(TransitionEndEvent, []func(View, PropertyName){layout.transitionFinished})
if session.TextDirection() == RightToLeftDirection {
layout.setRaw(PushTransform, NewTransformProperty(Params{TranslateX: Percent(-100)}))
} else {
layout.peek = 0
}
updateInnerHTML(layout.htmlID(), layout.session)
layout.currentChanged()
}
if layout.onPushFinished != nil {
onPushFinished := layout.onPushFinished
layout.onPushFinished = nil
onPushFinished()
}
layout.setRaw(PushTransform, NewTransformProperty(Params{TranslateX: Percent(100)}))
}
}
func (layout *stackLayoutData) popFinished(view View, tag PropertyName) {
if tag == "ruiPop" {
popView := layout.popView
layout.popView = nil
updateInnerHTML(layout.htmlID(), layout.session)
if layout.onPopFinished != nil {
onPopFinished := layout.onPopFinished
layout.onPopFinished = nil
onPopFinished(popView)
func (layout *stackLayoutData) transitionFinished(view View, tag PropertyName) {
if tags := strings.Split(string(tag), "-"); len(tags) >= 2 {
session := layout.Session()
viewID := tags[1]
switch tags[0] {
case "push":
if finished, ok := layout.onPushFinished[viewID]; ok {
if finished.peekID != "" {
pageID := finished.peekID + "page"
session.startUpdateScript(pageID)
session.updateCSSProperty(pageID, "visibility", "hidden")
session.updateCSSProperty(pageID, "transition", "")
session.updateCSSProperty(pageID, "transform", "")
session.removeProperty(pageID, "ontransitionend")
session.removeProperty(pageID, "ontransitioncancel")
session.finishUpdateScript(pageID)
}
pageID := viewID + "page"
session.startUpdateScript(pageID)
session.updateCSSProperty(pageID, "z-index", "auto")
session.updateCSSProperty(pageID, "transition", "")
session.removeProperty(pageID, "ontransitionend")
session.removeProperty(pageID, "ontransitioncancel")
session.finishUpdateScript(pageID)
if finished.listener != nil {
finished.listener()
}
delete(layout.onPushFinished, viewID)
layout.contentChanged()
}
case "pop":
if finished, ok := layout.onPopFinished[viewID]; ok {
session.updateCSSProperty(viewID+"page", "display", "none")
if finished.listener != nil {
finished.listener(finished.view)
}
delete(layout.onPopFinished, viewID)
if count := len(layout.views); count > 0 {
peekID := layout.views[count-1].htmlID() + "page"
session.startUpdateScript(peekID)
session.removeProperty(peekID, "ontransitionend")
session.removeProperty(peekID, "ontransitioncancel")
session.finishUpdateScript(peekID)
}
}
case "move":
if count := len(layout.views); count > 1 {
pageID := layout.views[count-2].htmlID() + "page"
session.startUpdateScript(pageID)
session.updateCSSProperty(pageID, "visibility", "hidden")
session.updateCSSProperty(pageID, "transition", "")
session.updateCSSProperty(pageID, "transform", "")
session.finishUpdateScript(pageID)
}
pageID := viewID + "page"
session.startUpdateScript(pageID)
session.updateCSSProperty(pageID, "z-index", "auto")
session.updateCSSProperty(pageID, "transition", "")
session.removeProperty(pageID, "ontransitionend")
session.removeProperty(pageID, "ontransitioncancel")
session.finishUpdateScript(pageID)
layout.contentChanged()
}
}
}
@ -125,199 +216,225 @@ func (layout *stackLayoutData) popFinished(view View, tag PropertyName) {
func (layout *stackLayoutData) setFunc(tag PropertyName, value any) []PropertyName {
switch tag {
case TransitionEndEvent:
// TODO
listeners, ok := valueToOneArgEventListeners[View, PropertyName](value)
if ok && listeners != nil {
listeners = append(listeners, layout.pushFinished)
listeners = append(listeners, layout.popFinished)
listeners = append(listeners, layout.transitionFinished)
layout.setRaw(TransitionEndEvent, listeners)
return []PropertyName{tag}
}
return nil
case Current:
newCurrent := 0
switch value := value.(type) {
case string:
text, ok := layout.session.resolveConstants(value)
if !ok {
invalidPropertyValue(tag, value)
return nil
}
n, err := strconv.Atoi(strings.Trim(text, " \t"))
if err != nil {
invalidPropertyValue(tag, value)
ErrorLog(err.Error())
return nil
}
newCurrent = n
default:
n, ok := isInt(value)
if !ok {
notCompatibleType(tag, value)
return nil
} else if n < 0 || n >= len(layout.views) {
ErrorLogF(`The view index "%d" of "%s" property is out of range`, n, tag)
return nil
}
newCurrent = n
}
layout.prevPeek = layout.peek
if newCurrent == layout.peek {
return []PropertyName{}
}
layout.peek = newCurrent
case PushTiming:
if text, ok := value.(string); ok {
layout.setRaw(tag, text)
return []PropertyName{tag}
}
}
return layout.viewsContainerData.setFunc(tag, value)
}
func (layout *stackLayoutData) propertyChanged(tag PropertyName) {
switch tag {
case Current:
if layout.prevPeek != layout.peek {
if layout.prevPeek < len(layout.views) {
layout.Session().updateCSSProperty(layout.htmlID()+"page"+strconv.Itoa(layout.prevPeek), "visibility", "hidden")
}
layout.Session().updateCSSProperty(layout.htmlID()+"page"+strconv.Itoa(layout.peek), "visibility", "visible")
layout.prevPeek = layout.peek
}
default:
layout.viewsContainerData.propertyChanged(tag)
}
}
func (layout *stackLayoutData) removeFunc(tag PropertyName) []PropertyName {
switch tag {
case TransitionEndEvent:
layout.setRaw(TransitionEndEvent, []func(View, PropertyName){layout.pushFinished, layout.popFinished})
return []PropertyName{tag}
case Current:
layout.setRaw(Current, 0)
layout.setRaw(TransitionEndEvent, []func(View, PropertyName){layout.transitionFinished})
return []PropertyName{tag}
}
return layout.viewsContainerData.removeFunc(tag)
}
func (layout *stackLayoutData) getFunc(tag PropertyName) any {
if tag == Current {
return layout.peek
}
return layout.viewsContainerData.getFunc(tag)
}
func (layout *stackLayoutData) Peek() View {
if int(layout.peek) < len(layout.views) {
return layout.views[layout.peek]
if count := len(layout.views); count > 0 {
return layout.views[count-1]
}
return nil
}
func (layout *stackLayoutData) MoveToFront(view View) bool {
peek := int(layout.peek)
htmlID := view.htmlID()
for i, view2 := range layout.views {
if view2.htmlID() == htmlID {
if i != peek {
if peek < len(layout.views) {
layout.Session().updateCSSProperty(layout.htmlID()+"page"+strconv.Itoa(peek), "visibility", "hidden")
}
layout.peek = i
layout.Session().updateCSSProperty(layout.htmlID()+"page"+strconv.Itoa(i), "visibility", "visible")
layout.currentChanged()
}
return true
}
}
ErrorLog(`MoveToFront() fail. Subview not found."`)
if view == nil {
ErrorLog(`MoveToFront(nil) forbidden`)
return false
}
func (layout *stackLayoutData) currentChanged() {
if listener, ok := layout.changeListener[Current]; ok {
listener(layout, Current)
htmlID := view.htmlID()
switch count := len(layout.views); count {
case 0:
// do nothing
case 1:
if layout.views[0].htmlID() == htmlID {
return true
}
default:
for i, view := range layout.views {
if view.htmlID() == htmlID {
layout.moveToFrontByIndex(i)
return true
}
}
}
ErrorLog(`MoveToFront() fail. Subview not found.`)
return false
}
func (layout *stackLayoutData) MoveToFrontByID(viewID string) bool {
peek := int(layout.peek)
for i, view := range layout.views {
if view.ID() == viewID {
if i != peek {
if peek < len(layout.views) {
layout.Session().updateCSSProperty(layout.htmlID()+"page"+strconv.Itoa(peek), "visibility", "hidden")
switch count := len(layout.views); count {
case 0:
// do nothing
case 1:
if layout.views[0].ID() == viewID {
return true
}
layout.peek = i
layout.Session().updateCSSProperty(layout.htmlID()+"page"+strconv.Itoa(i), "visibility", "visible")
layout.currentChanged()
}
default:
for i, view := range layout.views {
if view.ID() == viewID {
layout.moveToFrontByIndex(i)
return true
}
}
}
ErrorLogF(`MoveToFront("%s") fail. Subview with "%s" not found."`, viewID, viewID)
ErrorLogF(`MoveToFront("%s") fail. Subview with "%s" not found.`, viewID, viewID)
return false
}
func (layout *stackLayoutData) Append(view View) {
if view != nil {
layout.peek = len(layout.views)
layout.viewsContainerData.Append(view)
layout.currentChanged()
} else {
ErrorLog("StackLayout.Append(nil, ....) is forbidden")
}
}
func (layout *stackLayoutData) moveToFrontByIndex(index int) {
func (layout *stackLayoutData) Insert(view View, index int) {
if view != nil {
count := len(layout.views)
if index < count {
layout.peek = int(index)
} else {
layout.peek = count
}
layout.viewsContainerData.Insert(view, index)
layout.currentChanged()
} else {
ErrorLog("StackLayout.Insert(nil, ....) is forbidden")
}
if index == count-1 {
return
}
func (layout *stackLayoutData) RemoveView(index int) View {
if index < 0 || index >= len(layout.views) {
return nil
view := layout.views[index]
peekID := layout.views[count-1].htmlID()
if index == 0 {
layout.views = append(layout.views[1:], view)
} else {
layout.views = append(append(layout.views[:index], layout.views[index+1:]...), view)
}
if layout.peek > 0 {
layout.peek--
session := layout.Session()
pageID := view.htmlID() + "page"
peekPageID := peekID + "page"
animated := IsMoveToFrontAnimation(layout)
var transform TransformProperty = nil
if animated {
transform = GetPushTransform(layout)
}
if transform == nil {
session.updateCSSProperty(peekPageID, "visibility", "hidden")
session.updateCSSProperty(pageID, "visibility", "visible")
layout.contentChanged()
return
}
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
buffer.WriteString(`stackTransitionEndEvent('`)
buffer.WriteString(layout.htmlID())
buffer.WriteString(`', 'move-`)
buffer.WriteString(view.htmlID())
buffer.WriteString(`', event)`)
listener := buffer.String()
transformCSS := transformMirror(transform, session).transformCSS(session)
transitionCSS := layout.pushTransitionCSS()
session.updateCSSProperty(peekPageID, "transition", transitionCSS)
session.startUpdateScript(pageID)
session.updateProperty(pageID, "ontransitionend", listener)
session.updateProperty(pageID, "ontransitioncancel", listener)
session.updateCSSProperty(pageID, "transform", transformCSS)
session.updateCSSProperty(pageID, "z-index", "100")
session.updateCSSProperty(pageID, "visibility", "visible")
session.finishUpdateScript(pageID)
session.updateCSSProperty(pageID, "transition", transitionCSS)
session.updateCSSProperty(pageID, "transform", "")
session.updateCSSProperty(peekPageID, "transform", transformCSS)
}
func (layout *stackLayoutData) contentChanged() {
if listener, ok := layout.changeListener[Content]; ok {
listener(layout, Content)
}
defer layout.currentChanged()
return layout.viewsContainerData.RemoveView(index)
}
func (layout *stackLayoutData) RemovePeek() View {
return layout.RemoveView(len(layout.views) - 1)
}
func (layout *stackLayoutData) Push(view View, animation int, onPushFinished func()) {
func (layout *stackLayoutData) pushTransitionCSS() string {
return fmt.Sprintf("transform %.2fs %s", GetPushDuration(layout), GetPushTiming(layout))
}
func transformMirror(transform TransformProperty, session Session) TransformProperty {
result := NewTransformProperty(nil)
for _, tag := range []PropertyName{Perspective, RotateX, RotateY, RotateZ, ScaleX, ScaleY, ScaleZ, TranslateZ} {
if value := transform.getRaw(tag); value != nil {
result.Set(tag, value)
}
}
for _, tag := range []PropertyName{Rotate, SkewX, SkewY} {
if angle, ok := angleProperty(transform, tag, session); ok {
angle.Value = -angle.Value
result.Set(tag, angle)
}
}
for _, tag := range []PropertyName{TranslateX, TranslateY} {
if size, ok := sizeProperty(transform, tag, session); ok {
size.Value = -size.Value
result.Set(tag, size)
}
}
return result
}
func (layout *stackLayoutData) Push(view View, onPushFinished func()) {
if view == nil {
ErrorLog("StackLayout.Push(nil, ....) is forbidden")
return
}
layout.pushView = view
layout.animationType = animation
//layout.animation["ruiPush"] = Animation{FinishListener: layout}
layout.onPushFinished = onPushFinished
transform := GetPushTransform(layout)
if transform == nil {
layout.Append(view)
if onPushFinished != nil {
onPushFinished()
}
return
}
prevPeek := ""
finished := pushFinished{
listener: onPushFinished,
}
if count := len(layout.views); count > 0 {
finished.peekID = layout.views[count-1].htmlID()
prevPeek = finished.peekID + "page"
}
htmlID := view.htmlID()
layout.onPushFinished[htmlID] = finished
view.setParentID(layout.htmlID())
layout.views = append(layout.views, view)
htmlID := layout.htmlID()
session := layout.Session()
buffer := allocStringBuilder()
@ -325,105 +442,110 @@ func (layout *stackLayoutData) Push(view View, animation int, onPushFinished fun
buffer.WriteString(`<div id="`)
buffer.WriteString(htmlID)
buffer.WriteString(`push" class="ruiStackPageLayout" ontransitionend="stackTransitionEndEvent('`)
buffer.WriteString(`page" class="ruiStackPageLayout" ontransitionend="stackTransitionEndEvent('`)
buffer.WriteString(layout.htmlID())
buffer.WriteString(`', 'push-`)
buffer.WriteString(htmlID)
buffer.WriteString(`', 'ruiPush', event)" style="`)
buffer.WriteString(`', event)" style="z-index: 100; transform: `)
buffer.WriteString(transform.transformCSS(layout.session))
buffer.WriteRune(';')
switch layout.animationType {
case StartToEndAnimation:
buffer.WriteString(fmt.Sprintf("transform: translate(-%gpx, 0px); transition: transform ", layout.frame.Width))
transitionCSS := layout.pushTransitionCSS()
buffer.WriteString(" transition: ")
buffer.WriteString(transitionCSS)
buffer.WriteString(`;">`)
case TopDownAnimation:
buffer.WriteString(fmt.Sprintf("transform: translate(0px, -%gpx); transition: transform ", layout.frame.Height))
case BottomUpAnimation:
buffer.WriteString(fmt.Sprintf("transform: translate(0px, %gpx); transition: transform ", layout.frame.Height))
default:
buffer.WriteString(fmt.Sprintf("transform: translate(%gpx, 0px); transition: transform ", layout.frame.Width))
}
buffer.WriteString(`1s ease;">`)
viewHTML(layout.pushView, buffer, "")
viewHTML(view, buffer, "")
buffer.WriteString(`</div>`)
session.appendToInnerHTML(htmlID, buffer.String())
layout.session.updateCSSProperty(htmlID+"push", "transform", "translate(0px, 0px)")
session.appendToInnerHTML(layout.htmlID(), buffer.String())
layout.views = append(layout.views, view)
view.setParentID(htmlID)
if listener, ok := layout.changeListener[Content]; ok {
listener(layout, Content)
}
if prevPeek != "" {
mirror := transformMirror(transform, session)
layout.session.updateCSSProperty(prevPeek, "transition", transitionCSS)
layout.session.updateCSSProperty(prevPeek, "transform", mirror.transformCSS(session))
}
func (layout *stackLayoutData) Pop(animation int, onPopFinished func(View)) bool {
layout.session.updateCSSProperty(htmlID+"page", "transform", "")
}
func (layout *stackLayoutData) Pop(onPopFinished func(View)) bool {
count := len(layout.views)
if count == 0 || layout.peek >= count {
if count == 0 {
ErrorLog("StackLayout is empty")
return false
}
layout.popView = layout.views[layout.peek]
layout.RemoveView(layout.peek)
transform := GetPushTransform(layout)
if transform == nil {
if view := layout.RemovePeek(); view != nil {
if onPopFinished != nil {
onPopFinished(view)
}
return true
}
return false
}
layout.animationType = animation
//layout.animation["ruiPop"] = Animation{FinishListener: layout}
layout.onPopFinished = onPopFinished
peek := count - 1
view := layout.views[peek]
view.setParentID("")
htmlID := layout.htmlID()
layout.views = layout.views[:peek]
layout.contentChanged()
layout.onPopFinished[view.htmlID()] = popFinished{
view: view,
listener: onPopFinished,
}
htmlID := view.htmlID()
session := layout.Session()
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
buffer.WriteString(`<div id="`)
buffer.WriteString(`stackTransitionEndEvent('`)
buffer.WriteString(layout.htmlID())
buffer.WriteString(`', 'pop-`)
buffer.WriteString(htmlID)
buffer.WriteString(`pop" class="ruiStackPageLayout" ontransitionend="stackTransitionEndEvent('`)
buffer.WriteString(htmlID)
buffer.WriteString(`', 'ruiPop', event)" ontransitioncancel="stackTransitionEndEvent('`)
buffer.WriteString(htmlID)
buffer.WriteString(`', 'ruiPop', event)" style="transition: transform 1s ease;">`)
viewHTML(layout.popView, buffer, "")
buffer.WriteString(`</div>`)
buffer.WriteString(`', event)`)
session.appendToInnerHTML(htmlID, buffer.String())
listener := buffer.String()
pageID := htmlID + "page"
var value string
switch layout.animationType {
case TopDownAnimation:
value = fmt.Sprintf("translate(0px, -%gpx)", layout.frame.Height)
transitionCSS := layout.pushTransitionCSS()
case BottomUpAnimation:
value = fmt.Sprintf("translate(0px, %gpx)", layout.frame.Height)
session.startUpdateScript(pageID)
session.updateProperty(pageID, "ontransitionend", listener)
session.updateProperty(pageID, "ontransitioncancel", listener)
session.updateCSSProperty(pageID, "z-index", "100")
session.updateCSSProperty(pageID, "transition", transitionCSS)
session.finishUpdateScript(pageID)
case StartToEndAnimation:
value = fmt.Sprintf("translate(-%gpx, 0px)", layout.frame.Width)
default:
value = fmt.Sprintf("translate(%gpx, 0px)", layout.frame.Width)
peek--
if peek >= 0 {
peekID := layout.views[peek].htmlID() + "page"
session.updateCSSProperty(peekID, "transition", "")
session.startUpdateScript(peekID)
session.updateCSSProperty(peekID, "transform", transformMirror(transform, session).transformCSS(session))
session.updateCSSProperty(peekID, "visibility", "visible")
session.finishUpdateScript(peekID)
session.updateCSSProperty(peekID, "transition", transitionCSS)
session.updateCSSProperty(peekID, "transform", "")
}
session.updateCSSProperty(pageID, "transform", transform.transformCSS(session))
layout.session.updateCSSProperty(htmlID+"pop", "transform", value)
return true
}
func (layout *stackLayoutData) htmlSubviews(self View, buffer *strings.Builder) {
count := len(layout.views)
if count > 0 {
htmlID := layout.htmlID()
peek := int(layout.peek)
if peek >= count {
peek = count - 1
}
if count := len(layout.views); count > 0 {
peek := count - 1
for i, view := range layout.views {
buffer.WriteString(`<div id="`)
buffer.WriteString(htmlID)
buffer.WriteString(view.htmlID())
buffer.WriteString(`page`)
buffer.WriteString(strconv.Itoa(i))
buffer.WriteString(`" class="ruiStackPageLayout"`)
if i != peek {
buffer.WriteString(` style="visibility: hidden;"`)
@ -434,3 +556,49 @@ func (layout *stackLayoutData) htmlSubviews(self View, buffer *strings.Builder)
}
}
}
// IsMoveToFrontAnimation returns "true" if an animation is used when calling the MoveToFront/MoveToFrontByID method of StackLayout interface.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func IsMoveToFrontAnimation(view View, subviewID ...string) bool {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if value, ok := boolProperty(view, MoveToFrontAnimation, view.Session()); ok {
return value
}
if value := valueFromStyle(view, MoveToFrontAnimation); value != nil {
if b, ok := valueToBool(value, view.Session()); ok {
return b
}
}
}
return true
}
// GetPushDuration returns the length of time in seconds that an push/pop StackLayout animation takes to complete.
// If the second argument (subviewID) is not specified or it is "" then a width of the first argument (view) is returned
func GetPushDuration(view View, subviewID ...string) float64 {
return floatStyledProperty(view, subviewID, PushDuration, 1)
}
// GetPushTiming returns the function which sets how an push/pop animation progresses.
// If the second argument (subviewID) is not specified or it is "" then a width of the first argument (view) is returned
func GetPushTiming(view View, subviewID ...string) string {
result := stringStyledProperty(view, subviewID, PushTiming, false)
if isTimingFunctionValid(result) {
return result
}
return "easy"
}
// GetTransform returns the start transform (translation, scale and rotation over x, y and z axes as well as a distortion)
// for an animated pushing of a child view.
// The default value is nil (no transform).
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetPushTransform(view View, subviewID ...string) TransformProperty {
return transformStyledProperty(view, subviewID, PushTransform)
}

View File

@ -11,14 +11,14 @@ const (
// Transform is the constant for "transform" property tag.
//
// Used by `View`.
// Specify translation, scale and rotation over x, y and z axes as well as a distorsion of a view along x and y axes.
// Specify translation, scale and rotation over x, y and z axes as well as a distortion of a view along x and y axes.
//
// Supported types: `TransformProperty`, `string`.
//
// See `Transform` description for more details.
// See `TransformProperty` description for more details.
//
// Conversion rules:
// `Transform` - stored as is, no conversion performed.
// `TransformProperty` - stored as is, no conversion performed.
// `string` - string representation of `Transform` interface. Example: "_{translate-x = 10px, scale-y = 1.1}".
Transform PropertyName = "transform"
@ -466,9 +466,9 @@ func transformSet(properties Properties, tag PropertyName, value any) []Property
return nil
}
func setTransformProperty(properties Properties, value any) bool {
func valueToTransformProperty(value any) TransformProperty {
setObject := func(obj DataObject) bool {
parseObject := func(obj DataObject) TransformProperty {
transform := NewTransformProperty(nil)
ok := true
for i := 0; i < obj.PropertyCount(); i++ {
@ -482,41 +482,44 @@ func setTransformProperty(properties Properties, value any) bool {
}
if !ok && transform.empty() {
return false
return nil
}
properties.setRaw(Transform, transform)
return true
return transform
}
switch value := value.(type) {
case TransformProperty:
properties.setRaw(Transform, value)
return true
return value
case DataObject:
return setObject(value)
return parseObject(value)
case DataNode:
if obj := value.Object(); obj != nil {
return setObject(obj)
return parseObject(obj)
}
notCompatibleType(Transform, value)
return false
case string:
if obj := ParseDataText(value); obj != nil {
return setObject(obj)
return parseObject(obj)
}
notCompatibleType(Transform, value)
}
return nil
}
func setTransformProperty(properties Properties, tag PropertyName, value any) bool {
if transform := valueToTransformProperty(value); transform != nil {
properties.setRaw(tag, transform)
return true
}
notCompatibleType(tag, value)
return false
}
return false
}
func getTransformProperty(properties Properties) TransformProperty {
if val := properties.getRaw(Transform); val != nil {
func getTransformProperty(properties Properties, tag PropertyName) TransformProperty {
if val := properties.getRaw(tag); val != nil {
if transform, ok := val.(TransformProperty); ok {
return transform
}
@ -528,7 +531,7 @@ func setTransformPropertyElement(properties Properties, tag PropertyName, value
switch tag {
case Perspective, RotateX, RotateY, RotateZ, Rotate, SkewX, SkewY, ScaleX, ScaleY, ScaleZ, TranslateX, TranslateY, TranslateZ:
var result []PropertyName = nil
if transform := getTransformProperty(properties); transform != nil {
if transform := getTransformProperty(properties, Transform); transform != nil {
if result = transformSet(transform, tag, value); result != nil {
result = append(result, Transform)
}
@ -694,7 +697,7 @@ func (style *viewStyle) writeViewTransformCSS(builder cssBuilder, session Sessio
builder.add(`transform-origin`, css)
}
if transform := getTransformProperty(style); transform != nil {
if transform := getTransformProperty(style, Transform); transform != nil {
builder.add(`transform`, transform.transformCSS(session))
}
}

View File

@ -708,7 +708,7 @@ func (view *viewData) propertyChanged(tag PropertyName) {
case Transform, Perspective, SkewX, SkewY, TranslateX, TranslateY, TranslateZ,
ScaleX, ScaleY, ScaleZ, Rotate, RotateX, RotateY, RotateZ:
css := ""
if transform := getTransformProperty(view); transform != nil {
if transform := getTransformProperty(view, Transform); transform != nil {
css = transform.transformCSS(session)
}
session.updateCSSProperty(htmlID, "transform", css)

View File

@ -562,7 +562,7 @@ func viewStyleGet(style Properties, tag PropertyName) any {
case RotateX, RotateY, RotateZ, Rotate, SkewX, SkewY, ScaleX, ScaleY, ScaleZ,
TranslateX, TranslateY, TranslateZ:
if transform := getTransformProperty(style); transform != nil {
if transform := getTransformProperty(style, Transform); transform != nil {
return transform.Get(tag)
}
return nil

View File

@ -345,8 +345,8 @@ func viewStyleSet(style Properties, tag PropertyName, value any) []PropertyName
}
return nil
case Transform:
if setTransformProperty(style, value) {
case Transform, PushTransform:
if setTransformProperty(style, tag, value) {
return []PropertyName{Transform}
} else {
return nil

View File

@ -567,6 +567,13 @@ func GetColumn(view View, subviewID ...string) Range {
return Range{}
}
// GetTransform returns a view transform: translation, scale and rotation over x, y and z axes as well as a distortion of a view along x and y axes.
// The default value is nil (no transform)
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetTransform(view View, subviewID ...string) TransformProperty {
return transformStyledProperty(view, subviewID, Transform)
}
// GetPerspective returns a distance between the z = 0 plane and the user in order to give a 3D-positioned
// element some perspective. Each 3D element with z > 0 becomes larger; each 3D-element with z < 0 becomes smaller.
// The default value is 0 (no 3D effects).
@ -860,6 +867,22 @@ func colorStyledProperty(view View, subviewID []string, tag PropertyName, inheri
return Color(0)
}
func transformStyledProperty(view View, subviewID []string, tag PropertyName) TransformProperty {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if transform := getTransformProperty(view, tag); transform != nil {
return transform
}
if value := valueFromStyle(view, tag); value != nil {
return valueToTransformProperty(value)
}
}
return nil
}
// FocusView sets focus on the specified subview, if it can be focused.
// The focused View is the View which will receive keyboard events by default.
// If the second argument (subviewID) is not specified or it is "" then focus is set on the first argument (view)