A game framework written with osu! in mind.
at master 254 lines 10 kB view raw
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2// See the LICENCE file in the repository root for full licence text. 3 4using System; 5using System.Collections.Generic; 6using JetBrains.Annotations; 7using osu.Framework.Graphics.Transforms; 8using osu.Framework.Lists; 9 10namespace osu.Framework.Graphics.Containers 11{ 12 /// <summary> 13 /// Manages dynamically displaying a custom <see cref="Drawable"/> based on a model object. 14 /// Useful for replacing <see cref="Drawable"/>s on the fly. 15 /// </summary> 16 public abstract class ModelBackedDrawable<T> : CompositeDrawable 17 { 18 /// <summary> 19 /// The currently displayed <see cref="Drawable"/>. Null if no drawable is displayed. 20 /// </summary> 21 protected Drawable DisplayedDrawable => displayedWrapper?.Content; 22 23 /// <summary> 24 /// The <see cref="IEqualityComparer{T}"/> used to compare models to ensure that <see cref="Drawable"/>s are not updated unnecessarily. 25 /// </summary> 26 protected readonly IEqualityComparer<T> Comparer; 27 28 private T model; 29 30 /// <summary> 31 /// Gets or sets the model, potentially triggering the current <see cref="Drawable"/> to update. 32 /// Subclasses should expose this via a nicer property name to better represent the data being set. 33 /// </summary> 34 protected T Model 35 { 36 get => model; 37 set 38 { 39 if (model == null && value == null) 40 return; 41 42 if (Comparer.Equals(model, value)) 43 return; 44 45 model = value; 46 47 if (IsLoaded) 48 updateDrawable(); 49 } 50 } 51 52 /// <summary> 53 /// The wrapper which has the current displayed content. 54 /// </summary> 55 private DelayedLoadWrapper displayedWrapper; 56 57 /// <summary> 58 /// The wrapper which is currently loading, or has finished loading (i.e <see cref="displayedWrapper"/>). 59 /// </summary> 60 private DelayedLoadWrapper currentWrapper; 61 62 /// <summary> 63 /// Constructs a new <see cref="ModelBackedDrawable{T}"/> with the default <typeparamref name="T"/> equality comparer. 64 /// </summary> 65 protected ModelBackedDrawable() 66 : this(EqualityComparer<T>.Default) 67 { 68 } 69 70 /// <summary> 71 /// Constructs a new <see cref="ModelBackedDrawable{T}"/> with a custom equality function. 72 /// </summary> 73 /// <param name="func">The equality function.</param> 74 protected ModelBackedDrawable(Func<T, T, bool> func) 75 : this(new FuncEqualityComparer<T>(func)) 76 { 77 } 78 79 /// <summary> 80 /// Constructs a new <see cref="ModelBackedDrawable{T}"/> with a custom <see cref="IEqualityComparer{T}"/>. 81 /// </summary> 82 /// <param name="comparer">The comparer to use.</param> 83 protected ModelBackedDrawable(IEqualityComparer<T> comparer) 84 { 85 Comparer = comparer; 86 } 87 88 protected override void LoadComplete() 89 { 90 base.LoadComplete(); 91 updateDrawable(); 92 } 93 94 private void updateDrawable() 95 { 96 if (TransformImmediately) 97 { 98 // If loading to a new model and we've requested to transform immediately, load a null model to allow such transforms to occur 99 loadDrawable(null); 100 } 101 102 loadDrawable(() => CreateDrawable(model)); 103 } 104 105 private void loadDrawable(Func<Drawable> createDrawableFunc) 106 { 107 // Remove the previous wrapper if the inner drawable hasn't finished loading. 108 if (currentWrapper?.DelayedLoadCompleted == false) 109 { 110 RemoveInternal(currentWrapper); 111 DisposeChildAsync(currentWrapper); 112 } 113 114 currentWrapper = createWrapper(createDrawableFunc, LoadDelay); 115 116 if (currentWrapper == null) 117 { 118 OnLoadStarted(); 119 finishLoad(currentWrapper); 120 OnLoadFinished(); 121 } 122 else 123 { 124 AddInternal(currentWrapper); 125 currentWrapper.DelayedLoadStarted += _ => OnLoadStarted(); 126 currentWrapper.DelayedLoadComplete += _ => 127 { 128 finishLoad(currentWrapper); 129 OnLoadFinished(); 130 }; 131 } 132 } 133 134 /// <summary> 135 /// Invoked when a <see cref="DelayedLoadWrapper"/> has finished loading its contents. 136 /// May be invoked multiple times for each <see cref="DelayedLoadWrapper"/>. 137 /// </summary> 138 /// <param name="wrapper">The <see cref="DelayedLoadWrapper"/>.</param> 139 private void finishLoad(DelayedLoadWrapper wrapper) 140 { 141 // Make the wrapper initially hidden. 142 ApplyHideTransforms(wrapper); 143 wrapper?.FinishTransforms(); 144 145 var showTransforms = ApplyShowTransforms(wrapper); 146 147 // If the wrapper hasn't changed then this invocation must be a result of a reload (e.g. DelayedLoadUnloadWrapper) 148 // In that case, we do not want to apply hide transforms and expire the last wrapper. 149 if (displayedWrapper != null && displayedWrapper != wrapper) 150 { 151 var lastWrapper = displayedWrapper; 152 153 // If the new wrapper is non-null, we need to wait for the show transformation to complete before hiding the old wrapper, 154 // otherwise, we can hide the old wrapper instantaneously and leave a blank display 155 var hideTransforms = wrapper == null 156 ? ApplyHideTransforms(lastWrapper) 157 : ((Drawable)lastWrapper)?.Delay(TransformDuration)?.Append(ApplyHideTransforms); 158 159 // Expire the last wrapper after the front-most transform has completed (the last wrapper is assumed to be invisible by that point) 160 (showTransforms ?? hideTransforms)?.OnComplete(_ => lastWrapper?.Expire()); 161 } 162 163 displayedWrapper = wrapper; 164 } 165 166 /// <summary> 167 /// Creates a <see cref="DelayedLoadWrapper"/> which supports reloading. 168 /// </summary> 169 /// <param name="createContentFunc">A function that creates the wrapped <see cref="Drawable"/>.</param> 170 /// <param name="timeBeforeLoad">The time before loading should begin.</param> 171 /// <returns>A <see cref="DelayedLoadWrapper"/> or null if <paramref name="createContentFunc"/> returns null.</returns> 172 private DelayedLoadWrapper createWrapper(Func<Drawable> createContentFunc, double timeBeforeLoad) 173 { 174 var content = createContentFunc?.Invoke(); 175 176 if (content == null) 177 return null; 178 179 return CreateDelayedLoadWrapper(() => 180 { 181 try 182 { 183 // optimisation to use already constructed object (used above for null check). 184 return content ?? createContentFunc(); 185 } 186 finally 187 { 188 // consume initial object if not already. 189 content = null; 190 } 191 }, timeBeforeLoad); 192 } 193 194 /// <summary> 195 /// Invoked when the <see cref="Drawable"/> representation of a model begins loading. 196 /// </summary> 197 protected virtual void OnLoadStarted() 198 { 199 } 200 201 /// <summary> 202 /// Invoked when the <see cref="Drawable"/> representation of a model has finished loading. 203 /// </summary> 204 protected virtual void OnLoadFinished() 205 { 206 } 207 208 /// <summary> 209 /// Determines whether <see cref="ApplyHideTransforms"/> should be invoked immediately on the currently-displayed drawable when switching to a new model. 210 /// </summary> 211 protected virtual bool TransformImmediately => false; 212 213 /// <summary> 214 /// The default time in milliseconds for transforms applied through <see cref="ApplyHideTransforms"/> and <see cref="ApplyShowTransforms"/>. 215 /// </summary> 216 protected virtual double TransformDuration => 1000; 217 218 /// <summary> 219 /// The delay in milliseconds before <see cref="Drawable"/>s will begin loading. 220 /// </summary> 221 protected virtual double LoadDelay => 0; 222 223 /// <summary> 224 /// Allows subclasses to customise the <see cref="DelayedLoadWrapper"/>. 225 /// </summary> 226 [NotNull] 227 protected virtual DelayedLoadWrapper CreateDelayedLoadWrapper([NotNull] Func<Drawable> createContentFunc, double timeBeforeLoad) => 228 new DelayedLoadWrapper(createContentFunc(), timeBeforeLoad); 229 230 /// <summary> 231 /// Creates a custom <see cref="Drawable"/> to display a model. 232 /// </summary> 233 /// <param name="model">The model that the <see cref="Drawable"/> should represent.</param> 234 /// <returns>A <see cref="Drawable"/> that represents <paramref name="model"/>, or null if no <see cref="Drawable"/> should be displayed.</returns> 235 [CanBeNull] 236 protected abstract Drawable CreateDrawable([CanBeNull] T model); 237 238 /// <summary> 239 /// Hides a drawable. 240 /// </summary> 241 /// <param name="drawable">The drawable that is to be hidden.</param> 242 /// <returns>The transform sequence.</returns> 243 protected virtual TransformSequence<Drawable> ApplyHideTransforms([CanBeNull] Drawable drawable) 244 => drawable?.FadeOut(TransformDuration, Easing.OutQuint); 245 246 /// <summary> 247 /// Shows a drawable. 248 /// </summary> 249 /// <param name="drawable">The drawable that is to be shown.</param> 250 /// <returns>The transform sequence.</returns> 251 protected virtual TransformSequence<Drawable> ApplyShowTransforms([CanBeNull] Drawable drawable) 252 => drawable?.FadeIn(TransformDuration, Easing.OutQuint); 253 } 254}