How to use transition with image opacity? #447
-
Hello, I'm trying to create a dissolve effect between two images by using a transition with the opacity property of each image. However, while one image replaces the other as expected, there is no transition. The change happens at once. AI (o1) tells me this is because my controls are re-rendered each time my state changes. It suggested various ways to have each Image control retain the same instance across renders, but I can't get any of them to work.
Here is my code (adapted from the counter example). I've been looking at code examples for hours, including discussions here about controlling rendering behavior, but haven't found anything that seems analogous to this (or that I can hack together in a way to make this work). Any help much appreciated. namespace CounterApp
// TimeSpan
open System
open Avalonia
// DoubleTransition
open Avalonia.Animation
open Avalonia.Controls
open Avalonia.Controls.ApplicationLifetimes
open Avalonia.FuncUI
open Avalonia.FuncUI.DSL
open Avalonia.FuncUI.Hosts
open Avalonia.Layout
// Bitmap
open Avalonia.Media.Imaging
open Avalonia.Themes.Fluent
type State =
{
image_1_opacity : float
image_2_opacity : float
}
module Main =
let view () =
Component(fun ctx ->
let state = ctx.useState { image_1_opacity = 1 ; image_2_opacity = 0 }
DockPanel.create [
DockPanel.children [
Button.create [
Button.dock Dock.Bottom
Button.onClick (fun _ -> state.Set { image_1_opacity = 1; image_2_opacity = 0 })
Button.content "Day"
Button.horizontalAlignment HorizontalAlignment.Stretch
Button.horizontalContentAlignment HorizontalAlignment.Center
]
Button.create [
Button.dock Dock.Bottom
Button.onClick (fun _ -> state.Set { image_1_opacity = 0; image_2_opacity = 1 })
Button.content "Night"
Button.horizontalAlignment HorizontalAlignment.Stretch
Button.horizontalContentAlignment HorizontalAlignment.Center
]
Grid.create [
Grid.children [
Image.create [
Image.height 1920
Image.width 1080
Image.opacity state.Current.image_1_opacity
Image.source <| new Bitmap ("/media/data/backup/avalonia/avalonia_20250124/images/golden-larch-forest-and-prusik-peak-dx-1920x1080.jpg")
Image.transitions (
let transitions = new Transitions ()
transitions.Add <| new DoubleTransition (Property = Image.OpacityProperty, Duration = TimeSpan.FromSeconds 2.0)
transitions
)
]
Image.create [
Image.height 1920
Image.width 1080
Image.opacity state.Current.image_2_opacity
Image.source <| new Bitmap ("/media/data/backup/avalonia/avalonia_20250124/images/dark-night-forest-view-5k-io-1920x1080.jpg")
Image.transitions (
let transitions = new Transitions ()
transitions.Add <| new DoubleTransition (Property = Image.OpacityProperty, Duration = TimeSpan.FromSeconds 2.0)
transitions
)
]
]
]
]
]
)
type MainWindow() =
inherit HostWindow()
do
base.Title <- "Counter Example"
base.Content <- Main.view ()
type App() =
inherit Application()
override this.Initialize() =
this.Styles.Add (FluentTheme())
this.RequestedThemeVariant <- Styling.ThemeVariant.Dark
override this.OnFrameworkInitializationCompleted() =
match this.ApplicationLifetime with
| :? IClassicDesktopStyleApplicationLifetime as desktopLifetime ->
desktopLifetime.MainWindow <- MainWindow()
| _ -> ()
module Program =
[<EntryPoint>]
let main(args: string[]) =
AppBuilder
.Configure<App>()
.UsePlatformDetect()
.UseSkia()
.StartWithClassicDesktopLifetime(args)
Update: I managed to get it working in Elm, but instead of using the transition attribute, I had to subscribe to a timer and incrementally update the image opacity on each update. I imagine a similar approach would work in Avalonia, so I'll try that next. Is it just that transitions/animations don't play well with MVU because in MVU the page keeps being re-rendered whereas transitions/animations expect the page to be more "stable"? It seems odd that I essentially have to reinvent the wheel this way, so maybe I'm missing something? module Main exposing (main)
import Browser
import Browser.Dom exposing (Viewport, getViewport)
import Browser.Events exposing (onResize)
import Html exposing (Html, button, div, img, text)
import Html.Attributes exposing (style, src)
import Html.Events exposing (onClick)
import String
import Time exposing (every)
import Task
import Maybe exposing (withDefault)
import String exposing (fromInt)
-- Constants
transition_duration : number
transition_duration = 5000
transition_frequency : number
transition_frequency = 30
transition_progress_rate : Float
transition_progress_rate = transition_frequency / transition_duration
-- MODEL
type alias Background =
{
width : Int,
height : Int,
source : String,
is_transitioning : Bool,
transition_target : String,
transition_progress : Float
}
type alias Model =
{
background : Background
}
init : () -> ( Model, Cmd Msg )
init _ =
(
{
background =
{
width = 0,
height = 0,
source = "../assets/day.jpg",
is_transitioning = False,
transition_target = "",
transition_progress = 0
}
},
Task.attempt GotViewport Browser.Dom.getViewport
)
-- UPDATE
type Msg
= Tick Time.Posix
| GotViewport (Result Browser.Dom.Error Viewport)
| Resize Int Int
| Day
| Night
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
Tick _ ->
if False == model.background.is_transitioning then ( model, Cmd.none )
else
let
transition_progress_1 = model.background.transition_progress + transition_progress_rate
transition_progress_2 = if transition_progress_1 > 1 then 1 else transition_progress_1
is_transitioning = if transition_progress_2 == 1 then False else True
source = if is_transitioning == False then model.background.transition_target else model.background.source
background_1 = model.background
background_2 =
{ background_1 |
source = source,
is_transitioning = is_transitioning,
transition_progress = transition_progress_2
}
in
(
{ model | background = background_2 }, Cmd.none
)
Resize width height ->
let
background_1 = model.background
background_2 =
{ background_1 |
width = width,
height = height
}
in
( { model | background = background_2 }, Cmd.none)
GotViewport (Ok viewport) ->
( model, Task.succeed (Resize (floor viewport.viewport.width) (floor viewport.viewport.height)) |> Task.perform identity )
GotViewport (Err _) ->
( model, Cmd.none )
Day ->
let
background_1 = model.background
background_2 =
{ background_1 |
is_transitioning = True,
transition_target = "../assets/day.jpg",
transition_progress = 0.0
}
in
( { model | background = background_2 }, Cmd.none )
Night ->
let
background_1 = model.background
background_2 =
{
background_1 |
is_transitioning = True,
transition_target = "../assets/night.jpg",
transition_progress = 0.0
}
in
( { model | background = background_2 }, Cmd.none )
-- VIEW
view : Model -> Html Msg
view model =
let
styleImage =
[
style "position" "absolute",
style "top" "0",
style "left" "0",
style "width" (String.fromInt model.background.width ++ "px"),
style "height" (String.fromInt model.background.height ++ "px")
]
styleButtons =
[
style "position" "absolute",
style "left" "10px",
style "bottom" "10px",
style "z-index" "1"
]
image_div =
if False == model.background.is_transitioning then
[
img ([
src model.background.source
] |> List.append styleImage) []
]
else
[
img
([
style "opacity" (String.fromFloat (1 - model.background.transition_progress)),
src model.background.source
] |> List.append styleImage) [],
img
([
style "opacity" (String.fromFloat model.background.transition_progress),
src model.background.transition_target
] |> List.append styleImage) []
]
in
div
[]
([
div styleButtons
[
button [ onClick Day ] [ text "Day" ],
button [ onClick Night ] [ text "Night" ]
]
] |> List.append image_div)
-- SUBSCRIPTIONS
subscriptions : Model -> Sub Msg
subscriptions _ =
Sub.batch [
every transition_frequency Tick,
Browser.Events.onResize Resize
]
-- MAIN
main : Program () Model Msg
main =
Browser.element
{
init = init,
update = update,
view = view,
subscriptions = subscriptions
} |
Beta Was this translation helpful? Give feedback.
Replies: 5 comments 7 replies
-
I believe the problem is that your transitions are being reinitialized on each render update. Honestly I find this kind of thing much easier to do in a Create <Styles
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style Selector="Image.opacity-fader">
<Setter Property="Transitions" >
<Transitions>
<DoubleTransition Duration="0:0:2.0" Property="Opacity" />
</Transitions>
</Setter>
</Style>
</Styles> Get rid of your current transitions code and add the Image.create
[
Image.classes ["opacity-fader"]
Image.height 1920
Image.width 1080
Image.opacity state.Current.image_1_opacity
Image.source <| new Bitmap ("/media/data/backup/avalonia/avalonia_20250124/images/golden-larch-forest-and-prusik-peak-dx-1920x1080.jpg")
]
Image.create [
Image.classes ["opacity-fader"]
Image.height 1920
Image.width 1080
Image.opacity state.Current.image_2_opacity
Image.source <| new Bitmap ("/media/data/backup/avalonia/avalonia_20250124/images/dark-night-forest-view-5k-io-1920x1080.jpg")
] Reference your override this.Initialize() =
this.Styles.Add (FluentTheme())
this.RequestedThemeVariant <- Styling.ThemeVariant.Dark
this.Styles.Load "avares://MyProjectName/Styles.xaml" (Substitute "MyProjectName" to whatever your assembly is called) |
Beta Was this translation helpful? Give feedback.
-
@IanWitham Thank you for your reply! As it happens, I arrived at a similar solution around the same time. However, with a transition, I couldn't find a way to set the "to" opacity value. I could set the "from" value in Anyway, I tried using animations in styles.xaml
<Styles
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style Selector="Image.fadeout">
<Style.Animations>
<Animation IterationCount="1" Duration="0:0:3">
<KeyFrame Cue="0%">
<Setter Property="Opacity" Value="1" />
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="Opacity" Value="0" />
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
<Style Selector="Image.fadein">
<Style.Animations>
<Animation IterationCount="1" Duration="0:0:3">
<KeyFrame Cue="0%">
<Setter Property="Opacity" Value="0" />
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="Opacity" Value="1" />
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
</Styles> That almost worked, but each animation only played once. After that, each time I changed the image and re-rendered the view, the image changed instantly with no animation. It's as if Avalonia remembered it had already played the animation once and didn't want to do it again. I haven't dug into the source yet to see if this is the case. Here's the working version I have currently. I'm specifying the animation in code because, I presume, it's disposed of after it plays. About the only remaining issue is that interrupting the animation (e.g. by clicking "Night", then "Day", then "Night" again) causes the final animation to play twice (e.g. the day-to-night animation plays twice.) namespace DayNightTransition
open System
open Elmish
open Avalonia
open Avalonia.Animation
open Avalonia.Controls
open Avalonia.Controls.ApplicationLifetimes
open Avalonia.FuncUI
open Avalonia.FuncUI.DSL
open Avalonia.FuncUI.Elmish
open Avalonia.FuncUI.Hosts
open Avalonia.FuncUI.Types
open Avalonia.Interactivity
open Avalonia.Layout
// Bitmap
open Avalonia.Media.Imaging
open Avalonia.Styling
open Avalonia.Themes.Fluent
// DispatcherTimer
open Avalonia.Threading
[<Measure>] type milliseconds
type Background_Transition_Info =
| Initial of string
| In_Transition of {| old : string; new_ : string |}
type Background =
{
width : int
height : int
transition : Background_Transition_Info
}
type State =
{
background : Background
}
type Msg =
| Resize of int * int
| Transition_Message of string
module Main =
let day_url = "/media/data/backup/gui/avalonia_20250208/avalonia_20250208/assets/day.jpg"
let night_url = "/media/data/backup/gui/avalonia_20250208/avalonia_20250208/assets/night.jpg"
let transition_duration = 3000.0<milliseconds>
let init () =
{
background =
{
width = 0
height = 0
transition = Initial day_url
}
}
let update msg state =
match msg with
| Resize (w, h) ->
{ state with
background =
{ state.background with
width = w
height = h
}
}
| Transition_Message new_ ->
match state.background.transition with
| Initial old when 0 = String.Compare (old, new_) -> state
| In_Transition transition_info when 0 = String.Compare (transition_info.new_, new_) -> state
| _ ->
{ state with
background =
{ state.background with
transition =
match state.background.transition with
| Initial old -> In_Transition {| old = old; new_ = new_ |}
| In_Transition transition_info -> In_Transition {| old = transition_info.new_; new_ = new_ |}
}
}
let get_image_attributes (state : State) = [
Image.width (float state.background.width)
Image.height (float state.background.height)
Canvas.left 0.0
Canvas.top 0.0
]
let get_fade_in_or_out_styles (fade_in : bool) : Styles =
let start_opacity = if fade_in then 0.0 else 1.0
let end_opacity = if fade_in then 1.0 else 0.0
let key_frame_0 = new KeyFrame ()
do
key_frame_0.Cue <- new Cue 0
key_frame_0.Setters.Add (new Setter (Component.OpacityProperty, start_opacity))
let key_frame_1 = new KeyFrame ()
do
key_frame_1.Cue <- new Cue 1
key_frame_1.Setters.Add (new Setter (Component.OpacityProperty, end_opacity))
let animation = new Animation()
animation.IterationCount <- new IterationCount (uint64 1)
animation.Duration <- transition_duration |> float |> TimeSpan.FromMilliseconds
do
animation.Children.Add key_frame_0
animation.Children.Add key_frame_1
let style = Style ()
do style.Animations.Add animation
let styles = new Styles ()
do styles.Add style
styles
let view (state: State) (dispatch: Msg -> unit) =
DockPanel.create [
DockPanel.children [
StackPanel.create [
StackPanel.dock Dock.Bottom
StackPanel.margin (10.0, 10.0, 10.0, 10.0)
StackPanel.orientation Orientation.Horizontal
StackPanel.children [
Button.create [
Button.content "Day"
Button.onClick (fun _ -> dispatch <| Transition_Message day_url)
]
Button.create [
Button.content "Night"
Button.onClick (fun _ -> dispatch <| Transition_Message night_url)
]
]
]
Canvas.create [
Canvas.dock Dock.Top
Canvas.children (
match state.background.transition with
| Initial transition_info ->
[
Image.create <|
(Image.source <| new Bitmap (transition_info))
:: get_image_attributes state
]
| In_Transition transition_info ->
[
Image.create <| [
Image.styles (get_fade_in_or_out_styles false)
Image.source <| new Bitmap (transition_info.old)
] @ get_image_attributes state
Image.create <| [
Image.styles (get_fade_in_or_out_styles true)
Image.source <| new Bitmap (transition_info.new_)
] @ get_image_attributes state
]
)
]
]
]
type MainWindow() as this =
inherit HostWindow()
let subscriptions (state: State) =
let resize (dispatch: Msg -> unit) =
let invoke (size : Size) =
Resize (int size.Width, int size.Height) |> dispatch
(this.GetObservable MainWindow.ClientSizeProperty).Subscribe invoke
[ [ nameof resize ], resize ]
do
base.Title <- "Day/Night Transition"
// base.Icon <- WindowIcon(System.IO.Path.Combine("Assets","Icons", "icon.ico"))
//this.VisualRoot.VisualRoot.Renderer.DrawFps <- true
//this.VisualRoot.VisualRoot.Renderer.DrawDirtyRects <- true
Elmish.Program.mkSimple Main.init Main.update Main.view
|> Program.withHost this
|> Program.withSubscription subscriptions
// |> Program.withConsoleTrace
|> Program.run
type App() =
inherit Application()
override this.Initialize() =
this.Styles.Add (FluentTheme())
this.RequestedThemeVariant <- Styling.ThemeVariant.Dark
override this.OnFrameworkInitializationCompleted() =
match this.ApplicationLifetime with
| :? IClassicDesktopStyleApplicationLifetime as desktopLifetime ->
let mainWindow = MainWindow()
desktopLifetime.MainWindow <- mainWindow
| _ -> ()
module Program =
[<EntryPoint>]
let main(args: string[]) =
AppBuilder
.Configure<App>()
.UsePlatformDetect()
.UseSkia()
.StartWithClassicDesktopLifetime(args) |
Beta Was this translation helpful? Give feedback.
-
Closing this as resolved since it seems to be working, but if anyone knows a simpler way to do this I'd definitely be interested. Thank you again IanWitham for your reply. |
Beta Was this translation helpful? Give feedback.
-
I tried to generalize my code (below) so I can cross-fade between two arbitrary images, and I'm back to the image changing immediately instead of transitioning. Per the component lifetime doc: "If the location of a component (for example in a list of children) changes it is considered a new component." So I suspect my generating the list of images in Canvas.children dynamically (old image, then new image) is causing the transition to not work because each re-render creates a new component for each image and the previous opacity is lost. My instinct is to:
More generally, I feel like I'm having these issues because I lack an understanding of:
For instance, when IanWitham said to "initialize the transitions within the (Image.)init function, which ensures they are only created once when the Image is created," I wouldn't have known to do that. To dig into issues like this, is there a way to log component lifetimes? Related, is there a component model equivalent of the Elmish Program.withConsoleTrace? I haven't done any UI programming since a desktop WPF app years ago, so I'm probably just lacking some foundational understanding here. I feel the docs are aimed at people who already have an idea of how things like component lifetimes, patching, etc. work. Is this something I could better understand by reading up on ReactJS (which the docs say the component model is inspired by) or just MVU generally? Any recommendations are appreciated. Thank you again. namespace DayNightTransition
open System
open Elmish
open Avalonia
open Avalonia.Animation
open Avalonia.Controls
open Avalonia.Controls.ApplicationLifetimes
open Avalonia.FuncUI
open Avalonia.FuncUI.DSL
open Avalonia.FuncUI.Elmish
open Avalonia.FuncUI.Elmish.ElmishHook
open Avalonia.FuncUI.Hosts
open Avalonia.FuncUI.Types
open Avalonia.Interactivity
open Avalonia.Layout
// Bitmap
open Avalonia.Media.Imaging
open Avalonia.Styling
open Avalonia.Themes.Fluent
// DispatcherTimer
open Avalonia.Threading
[<Measure>] type milliseconds
type Background_Transition_Info =
| Initial of string
| In_Transition of {|
old : {| url : string ; opacity : int |}
new_ : {| url : string ; opacity : int |}
|}
type Background =
{
width : int
height : int
transition : Background_Transition_Info
}
type State =
{
background : Background
}
type Msg =
| Resize of int * int
| Transition_Message of string
module Main =
let day_url = "/media/data/backup/gui/avalonia_20250208/avalonia_20250208/assets/day.jpg"
let night_url = "/media/data/backup/gui/avalonia_20250208/avalonia_20250208/assets/night.jpg"
let transition_duration = 3000.0<milliseconds>
let initial_state =
{
background =
{
width = 0
height = 0
transition = Initial day_url
}
}
let subscriptions (state: State) : Sub<Msg> =
// TODO1 Fix subscriptions
(*
let resize (dispatch: Msg -> unit) =
let invoke (size : Size) =
Resize (int size.Width, int size.Height) |> dispatch
(this.GetObservable MainWindow.ClientSizeProperty).Subscribe invoke
[ [ nameof resize ], resize ]
*)
[]
let update (msg : Msg) (state : State) : State * Cmd<Msg> =
match msg with
| Resize (w, h) ->
{ state with
background =
{ state.background with
width = w
height = h
}
}, Cmd.none
| Transition_Message new_ ->
match state.background.transition with
| Initial old when 0 = String.Compare (old, new_) -> state, Cmd.none
| In_Transition transition_info when 0 = String.Compare (transition_info.new_.url, new_) -> state, Cmd.none
| _ ->
{ state with
background =
{ state.background with
transition =
match state.background.transition with
| Initial old -> In_Transition {| old = {| url = old ; opacity = 0 |}; new_ = {| url = new_ ; opacity = 1 |} |}
| In_Transition transition_info -> In_Transition {| old = {| url = transition_info.new_.url; opacity = 0 |}; new_ = {| url = new_ ; opacity = 1 |} |}
}
}, Cmd.none
let get_image_attributes (state : State) = [
Canvas.left 0.0
Canvas.top 0.0
// TODO2 Fix auto-sizing
Image.width 1280.0
Image.height 780.0
// Image.width (float state.background.width)
// Image.height (float state.background.height)
]
let view () =
Component (fun ctx ->
let state = ctx.useState initial_state
DockPanel.create [
DockPanel.children [
StackPanel.create [
StackPanel.dock Dock.Bottom
StackPanel.margin (10.0, 10.0, 10.0, 10.0)
StackPanel.orientation Orientation.Horizontal
StackPanel.children [
Button.create [
Button.content "Day"
Button.onClick (fun _ ->
let state_new, _ = update (Transition_Message day_url) state.Current
state.Set state_new
)
]
Button.create [
Button.content "Night"
Button.onClick (fun _ ->
let state_new, _ = update (Transition_Message night_url) state.Current
state.Set state_new
)
]
]
]
Canvas.create [
Canvas.dock Dock.Top
Canvas.children (
match state.Current.background.transition with
| Initial transition_info ->
[
Image.create <|
(Image.source <| new Bitmap (transition_info))
:: get_image_attributes state.Current
]
| In_Transition transition_info ->
[
Image.create <| [
Image.init (fun (image : Image) ->
image.Transitions <- Transitions ()
image.Transitions.Add (DoubleTransition (Duration = (TimeSpan.FromMilliseconds <| float transition_duration), Property = Image.OpacityProperty))
)
Image.source <| new Bitmap (transition_info.old.url)
Image.opacity transition_info.old.opacity
] @ get_image_attributes state.Current
Image.create <| [
Image.init (fun (image : Image) ->
image.Transitions <- Transitions ()
image.Transitions.Add (DoubleTransition (Duration = (TimeSpan.FromMilliseconds <| float transition_duration), Property = Image.OpacityProperty))
)
Image.source <| new Bitmap (transition_info.new_.url)
Image.opacity transition_info.new_.opacity
] @ get_image_attributes state.Current
]
)
]
]
]
)
type MainWindow () =
inherit HostWindow ()
do
base.Title <- "Day/Night Transition"
"icon.ico"))
base.Content <- Main.view ()
type App () =
inherit Application ()
override this.Initialize () =
this.Styles.Add (FluentTheme ())
this.RequestedThemeVariant <- Styling.ThemeVariant.Dark
override this.OnFrameworkInitializationCompleted () =
match this.ApplicationLifetime with
| :? IClassicDesktopStyleApplicationLifetime as desktopLifetime ->
let mainWindow = MainWindow ()
desktopLifetime.MainWindow <- mainWindow
| _ -> ()
module Program =
[<EntryPoint>]
let main (args: string []) =
AppBuilder
.Configure<App>()
.UsePlatformDetect()
.UseSkia()
.StartWithClassicDesktopLifetime (args)
|
Beta Was this translation helpful? Give feedback.
-
@fsharpn00b here is some working code you can use as inspiration.
Components keep their state (
FuncUI tries to reuse existing controls. If a control is not there yet it will create it. Because there is no hard connection between a control that is specified via the DSL and actual controls in the Visual Tree FuncUI needs to figure out which control represents which DSL control. In some cases that's not possible -> order of controls changes, controls are added removed in a list. When this happens new controls are created for certain DSL controls.
It is important to understand that a control has state outside of the DSL. A TextBox for example has a cursor position, ... that you usually don't care about in the DSL, but you want to keep that state during re-renders. Your image has an Opacity that you want to animate. The actual opacity value state defined on the underlaying control. In the DSL we only patch what we think changed. So if you don't modify the opacity via the DSL but via an animation we don't know that it changed. This is by design, and useful for a lot of things. Like text editing, animations, ... https://funcui.avaloniaui.net/components/component-lifetime namespace Examples.ControlFade
open Avalonia.Animation
open Avalonia.Animation.Easings
open Avalonia.FuncUI.Experimental
open Avalonia.FuncUI.Types
open Avalonia.Media
module Main =
open Avalonia.Controls
open Avalonia.Controls.Primitives
open Avalonia.FuncUI
open Avalonia.FuncUI.DSL
open Avalonia.Layout
[<RequireQualifiedAccess>]
type ActiveControl =
| ControlA
| ControlB
let fadingControls (active: IReadable<ActiveControl>, controlA: IView, controlB: IView) =
Component.create ("fading-controls", fun ctx ->
let controlBRef = ctx.useState<Control option>(None)
let active = ctx.usePassedRead(active)
ctx.useEffect (
handler = (fun _ ->
match controlBRef.Current, active.Current with
| Some controlBRef, ActiveControl.ControlA ->
ignore (
task {
let animation =
Animation()
.WithDuration(1.0)
.WithEasing(CubicEaseIn())
.WithFillMode(FillMode.Forward)
.WithKeyFrames [
KeyFrame()
.WithCue(0)
.WithSetter(ContentControl.OpacityProperty, 1.0)
KeyFrame()
.WithCue(1)
.WithSetter(ContentControl.OpacityProperty, 0.0)
]
do! animation.RunAsync controlBRef
}
)
()
| Some controlBRef, ActiveControl.ControlB ->
ignore (
task {
let animation =
Animation()
.WithDuration(1.0)
.WithEasing(CubicEaseIn())
.WithFillMode(FillMode.Forward)
.WithKeyFrames [
KeyFrame()
.WithCue(0)
.WithSetter(ContentControl.OpacityProperty, 0.0)
KeyFrame()
.WithCue(1)
.WithSetter(ContentControl.OpacityProperty, 1.0)
]
do! animation.RunAsync controlBRef
}
)
| _ ->
()
),
triggers = [ EffectTrigger.AfterChange active ]
)
Panel.create [
Panel.children [
ContentControl.create [
ContentControl.verticalAlignment VerticalAlignment.Stretch
ContentControl.horizontalAlignment HorizontalAlignment.Stretch
ContentControl.verticalContentAlignment VerticalAlignment.Stretch
ContentControl.horizontalContentAlignment HorizontalAlignment.Stretch
ContentControl.content controlA
ContentControl.opacity 1.0
]
ContentControl.create [
ContentControl.init (fun c -> controlBRef.Set (Some c))
ContentControl.verticalAlignment VerticalAlignment.Stretch
ContentControl.horizontalAlignment HorizontalAlignment.Stretch
ContentControl.verticalContentAlignment VerticalAlignment.Stretch
ContentControl.horizontalContentAlignment HorizontalAlignment.Stretch
ContentControl.content controlB
ContentControl.opacity 1.0
]
]
]
)
let view =
Component (fun ctx ->
let state = ctx.useState ActiveControl.ControlA
DockPanel.create [
DockPanel.children [
UniformGrid.create [
UniformGrid.dock Dock.Bottom
UniformGrid.rows 1
UniformGrid.columns 2
UniformGrid.children [
Button.create [
Button.dock Dock.Bottom
Button.onClick (fun _ -> state.Set ActiveControl.ControlA)
Button.content "Show Control A"
Button.horizontalAlignment HorizontalAlignment.Stretch
]
Button.create [
Button.dock Dock.Bottom
Button.onClick (fun _ -> state.Set ActiveControl.ControlB)
Button.content "Show Control B"
Button.horizontalAlignment HorizontalAlignment.Stretch
]
]
]
TextBlock.create [
TextBlock.dock Dock.Top
TextBlock.verticalAlignment VerticalAlignment.Center
TextBlock.horizontalAlignment HorizontalAlignment.Center
TextBlock.text (string state.Current)
]
ContentControl.create [
ContentControl.dock Dock.Top
ContentControl.content (
fadingControls (
state,
TextBlock.create [
TextBlock.text "Control A"
TextBlock.background Brushes.Red
TextBlock.fontSize 48.0
TextBlock.verticalAlignment VerticalAlignment.Stretch
TextBlock.horizontalAlignment HorizontalAlignment.Stretch
],
TextBlock.create [
TextBlock.text "Control B"
TextBlock.background Brushes.Green
TextBlock.fontSize 48.0
TextBlock.verticalAlignment VerticalAlignment.Stretch
TextBlock.horizontalAlignment HorizontalAlignment.Stretch
]
)
)
]
]
]
)
|
Beta Was this translation helpful? Give feedback.
That's a bit odd because I tested my solution with your code and it worked. It's also the solution I'm using for a project I'm working on. Perhaps you missed a step?
I also came up with another solution which does not require
Styles.xaml
. Simply initialize the transitions within theinit
function, which ensures they are only created once when theImage
is created: