/* * Copyright (c) 2019 Samsung Electronics Co., Ltd. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ using Tizen.NUI.BaseComponents; using System.Runtime.InteropServices; using System.Collections.Generic; using System.Diagnostics; using System; namespace Tizen.NUI { /// /// [Draft] The class that initiates the Measuring and Layouting of the tree, /// It sets a callback that becomes the entry point into the C# Layouting. /// internal class LayoutController : global::System.IDisposable { static bool LayoutDebugController = false; // Debug flag private global::System.Runtime.InteropServices.HandleRef unmanagedLayoutController; [UnmanagedFunctionPointer(CallingConvention.StdCall)] internal delegate void Callback(int id); event Callback _instance; // A Flag to check if it is already disposed. protected bool disposed = false; private Window _window; Animation _coreAnimation; private List _layoutTransitionDataQueue; private List _itemRemovalQueue; /// /// Constructs a LayoutController which controls the measuring and layouting.
/// Window attach this LayoutController to. ///
public LayoutController(Window window) { global::System.IntPtr cPtr = Interop.LayoutController.LayoutController_New(); _window = window; // Wrap cPtr in a managed handle. unmanagedLayoutController = new global::System.Runtime.InteropServices.HandleRef(this, cPtr); _instance = new Callback(Process); Interop.LayoutController.LayoutController_SetCallback(unmanagedLayoutController, _instance); _layoutTransitionDataQueue = new List(); } /// /// Get the unique id of the LayoutController /// public int GetId() { return Interop.LayoutController.LayoutController_GetId(unmanagedLayoutController); } /// /// Request relayout of a LayoutItem and it's parent tree. /// /// LayoutItem that is required to be laid out again. public void RequestLayout(LayoutItem layoutItem) { // Go up the tree and mark all parents to relayout ILayoutParent layoutParent = layoutItem.GetParent(); if (layoutParent != null) { LayoutGroup layoutGroup = layoutParent as LayoutGroup; if(! layoutGroup.LayoutRequested) { layoutGroup.RequestLayout(); } } } /// /// Destructor which adds LayoutController to the Dispose queue. /// ~LayoutController() { } /// /// Explict Dispose. /// public void Dispose() { Dispose(DisposeTypes.Explicit); } /// /// Add transition data for a LayoutItem to the transition stack. /// /// Transition data for a LayoutItem. internal void AddTransitionDataEntry( LayoutData transitionDataEntry) { _layoutTransitionDataQueue.Add(transitionDataEntry); } /// /// Add LayoutItem to a removal stack for removal after transitions finish. /// /// LayoutItem to remove. internal void AddToRemovalStack( LayoutItem itemToRemove) { if (_itemRemovalQueue == null) { _itemRemovalQueue = new List(); } _itemRemovalQueue.Add(itemToRemove); } /// /// Dispose Explict or Implicit /// protected virtual void Dispose(DisposeTypes type) { if (disposed) { return; } if (type == DisposeTypes.Explicit) { //Called by User code //Release your own managed resources here. //You should release all of your own disposable objects here. } //Release your own unmanaged resources here. //You should not access any managed member here except static instance. //because the execution order of Finalizes is non-deterministic. if (unmanagedLayoutController.Handle != global::System.IntPtr.Zero) { Interop.LayoutController.delete_LayoutController(unmanagedLayoutController); unmanagedLayoutController = new global::System.Runtime.InteropServices.HandleRef(null, global::System.IntPtr.Zero); } disposed = true; } // Traverse through children from the provided root. // If a child has no layout but is a pure View then assign a default layout and continue traversal. // If child has no layout and not a pure View then assign a LayoutItem that terminates the layouting (leaf), private void AutomaticallyAssignLayouts(View rootNode) { for (uint i = 0; i < rootNode.ChildCount; i++) { View view = rootNode.GetChildAt(i); if (rootNode.Layout == null ) { if (rootNode.GetType() == typeof(View)) { rootNode.Layout = new LayoutGroup(); AutomaticallyAssignLayouts(rootNode); } else { rootNode.Layout = new LayoutItem(); } } } } // Traverse the tree looking for a root node that is a layout. // Once found, it's children can be assigned Layouts and the Measure process started. private void FindRootLayouts(View rootNode) { if (rootNode.Layout != null) { // rootNode has a layout, ensure all children have default layouts or layout items. AutomaticallyAssignLayouts(rootNode); // rootNode has a layout, start measuring and layouting from here. MeasureAndLayout(rootNode); } else { // Search children of supplied node for a layout. for (uint i = 0; i < rootNode.ChildCount; i++) { View view = rootNode.GetChildAt(i); FindRootLayouts(view); } } } // Starts of the actual measuring and layouting from the given root node. // Can be called from multiple starting roots but in series. void MeasureAndLayout(View root) { if (root !=null) { // Get parent MeasureSpecification, this could be the Window or View with an exact size. Container parentNode = root.GetParent(); Size2D rootSize; Position2D rootPosition = root.Position2D; View parentView = parentNode as View; if (parentView) { // Get parent View's Size. If using Legacy size negotiation then should have been set already. rootSize = new Size2D(parentView.Size2D.Width, parentView.Size2D.Height); } else { // Parent not a View so assume it's a Layer which is the size of the window. rootSize = _window.WindowSize; } // Determine measure specification for root. // The root layout policy could be an exact size, be match parent or wrap children. // If wrap children then at most it can be the root parent size. // If match parent then should be root parent size. // If exact then should be that size limited by the root parent size. LayoutLength width = new LayoutLength(rootSize.Width); LayoutLength height = new LayoutLength(rootSize.Height); MeasureSpecification.ModeType widthMode = MeasureSpecification.ModeType.AtMost; MeasureSpecification.ModeType heightMode = MeasureSpecification.ModeType.AtMost; if ( root.WidthSpecification >= 0 ) { // exact size provided so match width exactly width = new LayoutLength(root.WidthSpecification); widthMode = MeasureSpecification.ModeType.Exactly; } else if (root.WidthSpecification == LayoutParamPolicies.MatchParent) { widthMode = MeasureSpecification.ModeType.Exactly; } if (root.HeightSpecification >= 0 ) { // exact size provided so match height exactly height = new LayoutLength(root.HeightSpecification); heightMode = MeasureSpecification.ModeType.Exactly; } else if (root.HeightSpecification == LayoutParamPolicies.MatchParent) { heightMode = MeasureSpecification.ModeType.Exactly; } MeasureSpecification parentWidthSpecification = new MeasureSpecification( width, widthMode); MeasureSpecification parentHeightSpecification = new MeasureSpecification( height, heightMode); // Start at root with it's parent's widthSpecification and heightSpecification MeasureHierarchy(root, parentWidthSpecification, parentHeightSpecification); // Start at root which was just measured. PerformLayout( root, new LayoutLength(rootPosition.X), new LayoutLength(rootPosition.Y), new LayoutLength(rootPosition.X) + root.Layout.MeasuredWidth.Size, new LayoutLength(rootPosition.Y) + root.Layout.MeasuredHeight.Size ); bool readyToPlay = SetupCoreAnimation(); if (readyToPlay) { PlayAnimation(); } } } /// /// Entry point into the C# Layouting that starts the Processing /// private void Process(int id) { // First layer in the Window should be the default layer (index 0 ) uint numberOfLayers = _window.LayerCount; for (uint layerIndex = 0; layerIndex < numberOfLayers; layerIndex++) { Layer layer = _window.GetLayer(layerIndex); for (uint i = 0; i < layer.ChildCount; i++) { View view = layer.GetChildAt(i); FindRootLayouts(view); } } } /// /// Starts measuring the tree, starting from the root layout. /// private void MeasureHierarchy(View root, MeasureSpecification widthSpec, MeasureSpecification heightSpec) { // Does this View have a layout? // Yes - measure the layout. It will call this method again for each of it's children. // No - reached leaf or no layouts set // // If in a leaf View with no layout, it's natural size is bubbled back up. if (root.Layout != null) { root.Layout.Measure(widthSpec, heightSpec); } } /// /// Performs the layouting positioning /// private void PerformLayout(View root, LayoutLength left, LayoutLength top, LayoutLength right, LayoutLength bottom) { if(root.Layout != null) { root.Layout.Layout(left, top, right, bottom); } } /// /// Play the animation. /// private void PlayAnimation() { _coreAnimation.Play(); Debug.WriteLineIf( LayoutDebugController, "LayoutController Core Duration:" + _coreAnimation.Duration); } private void AnimationFinished(object sender, EventArgs e) { // Iterate list of LayoutItem that were set for removal. // Now the core animation has finished their Views can be removed. if (_itemRemovalQueue != null) { foreach (LayoutItem item in _itemRemovalQueue) { // Check incase the parent was already removed and the Owner was // removed already. if (item.Owner) { // Check again incase the parent has already been removed. ILayoutParent layoutParent = item.GetParent(); LayoutGroup layoutGroup = layoutParent as LayoutGroup; if (layoutGroup !=null) { layoutGroup.Owner?.RemoveChild(item.Owner); } } } _itemRemovalQueue.Clear(); // If LayoutItems added to stack whilst the core animation is playing // they would have been cleared here. // Could have another stack to be added to whilst the animation is running. // After the running stack is cleared it can be populated with the content // of the other stack. Then the main removal stack iterated when AnimationFinished // occurs again. } } /// /// Set up the animation from each LayoutItems position data. /// Iterates the transition stack, adding an Animator to the core animation. /// private bool SetupCoreAnimation() { // Initialize animation for this layout run. bool animationPending = false; if (_layoutTransitionDataQueue.Count > 0 ) // Something to animate { _coreAnimation = new Animation(); _coreAnimation.EndAction = Animation.EndActions.StopFinal; _coreAnimation.Finished += AnimationFinished; // Iterate all items that have been queued for repositioning. foreach (LayoutData layoutPositionData in _layoutTransitionDataQueue) { AddAnimatorsToAnimation(layoutPositionData); } animationPending = true; // transitions have now been applied, clear stack, ready for new transitions on // next layout traversal. _layoutTransitionDataQueue.Clear(); } return animationPending; } private void SetupAnimationForPosition(LayoutData layoutPositionData, TransitionComponents positionTransitionComponents) { // A removed item does not have a valid target position within the layout so don't try to position. if( layoutPositionData.ConditionForAnimation != TransitionCondition.Remove ) { _coreAnimation.AnimateTo(layoutPositionData.Item.Owner, "Position", new Vector3(layoutPositionData.Left, layoutPositionData.Top, layoutPositionData.Item.Owner.Position.Z), positionTransitionComponents.Delay, positionTransitionComponents.Duration, positionTransitionComponents.AlphaFunction ); Debug.WriteLineIf( LayoutDebugController, "LayoutController SetupAnimationForPosition View:" + layoutPositionData.Item.Owner.Name + " left:" + layoutPositionData.Left + " top:" + layoutPositionData.Top + " delay:" + positionTransitionComponents.Delay + " duration:" + positionTransitionComponents.Duration ); } } private void SetupAnimationForSize(LayoutData layoutPositionData, TransitionComponents sizeTransitionComponents) { // Text size cant be animated so is set to it's final size. // It is due to the internals of the Text not being able to recalculate fast enough. if (layoutPositionData.Item.Owner is TextLabel || layoutPositionData.Item.Owner is TextField) { float itemWidth = layoutPositionData.Right-layoutPositionData.Left; float itemHeight = layoutPositionData.Bottom-layoutPositionData.Top; // Set size directly. layoutPositionData.Item.Owner.Size2D = new Size2D((int)itemWidth, (int)itemHeight); } else { _coreAnimation.AnimateTo(layoutPositionData.Item.Owner, "Size", new Vector3(layoutPositionData.Right-layoutPositionData.Left, layoutPositionData.Bottom-layoutPositionData.Top, layoutPositionData.Item.Owner.Position.Z), sizeTransitionComponents.Delay, sizeTransitionComponents.Duration, sizeTransitionComponents.AlphaFunction); } } void SetupAnimationForCustomTransitions(TransitionList transitionsToAnimate, View view ) { if (transitionsToAnimate?.Count > 0) { foreach (LayoutTransition transition in transitionsToAnimate) { if ( transition.AnimatableProperty != AnimatableProperties.Position && transition.AnimatableProperty != AnimatableProperties.Size ) { _coreAnimation.AnimateTo(view, transition.AnimatableProperty.ToString(), transition.TargetValue, transition.Animator.Delay, transition.Animator.Duration, transition.Animator.AlphaFunction ); Debug.WriteLineIf( LayoutDebugController, "LayoutController SetupAnimationForCustomTransitions View:" + view.Name + " Property:" + transition.AnimatableProperty.ToString() + " delay:" + transition.Animator.Delay + " duration:" + transition.Animator.Duration ); } } } } /// /// Iterate transitions and replace Position Components if replacements found in list. /// private void FindAndReplaceAnimatorComponentsForProperty(TransitionList sourceTransitionList, AnimatableProperties propertyToMatch, ref TransitionComponents transitionComponentToUpdate) { foreach( LayoutTransition transition in sourceTransitionList) { if (transition.AnimatableProperty == propertyToMatch) { // Matched property to animate is for the propertyToMatch so use provided Animator. transitionComponentToUpdate = transition.Animator; } } } private TransitionComponents CreateDefaultTransitionComponent( int delay, int duration ) { AlphaFunction alphaFunction = new AlphaFunction(AlphaFunction.BuiltinFunctions.Linear); return new TransitionComponents(delay, duration, alphaFunction); } /// /// Sets up the main animation with the animators for each item (each layoutPositionData structure) /// private void AddAnimatorsToAnimation( LayoutData layoutPositionData ) { LayoutTransition positionTransition = new LayoutTransition(); LayoutTransition sizeTransition = new LayoutTransition(); TransitionCondition conditionForAnimators = layoutPositionData.ConditionForAnimation; // LayoutChanged transitions overrides ChangeOnAdd and ChangeOnRemove as siblings will // reposition to the new layout not to the insertion/removal of a sibling. if (layoutPositionData.ConditionForAnimation.HasFlag(TransitionCondition.LayoutChanged)) { conditionForAnimators = TransitionCondition.LayoutChanged; } // Set up a default transitions, will be overwritten if inherited from parent or set explicitly. TransitionComponents positionTransitionComponents = CreateDefaultTransitionComponent(0, 100); TransitionComponents sizeTransitionComponents = CreateDefaultTransitionComponent(0, 0); bool matchedCustomTransitions = false; // Inherit parent transitions if none already set on View for the condition. // Transitions set on View rather than LayoutItem so if the Layout changes the transition persist. TransitionList transitionsForCurrentCondition = new TransitionList(); ILayoutParent layoutParent = layoutPositionData.Item.GetParent(); if (layoutParent !=null) { // Check if item to animate has it's own Transitions for this condition. // If a key exists then a List of atleast 1 transition exists. if ( layoutPositionData.Item.Owner.LayoutTransitions.ContainsKey(conditionForAnimators)) { // Child has transitions for the condition matchedCustomTransitions = layoutPositionData.Item.Owner.LayoutTransitions.TryGetValue(conditionForAnimators, out transitionsForCurrentCondition); } else { // Item doesn't have it's own transitions for this condition so copy parents if // has a parent with transitions. LayoutGroup layoutGroup = layoutParent as LayoutGroup; TransitionList parentTransitionList; // Note TryGetValue returns null if key not matched. if (layoutGroup !=null && layoutGroup.Owner.LayoutTransitions.TryGetValue(conditionForAnimators, out parentTransitionList)) { // Copy parent transitions to temporary TransitionList. List contains transitions for the current condition. LayoutTransitionsHelper.CopyTransitions(parentTransitionList, transitionsForCurrentCondition); matchedCustomTransitions = false; // Using parent transition as no custom match. } } } // Position/Size transitions can be for a layout changing to another layout or an item being added or removed. // There can only be one position transition and one size position, they will be replaced if set multiple times. // transitionsForCurrentCondition represent all non position (custom) properties that should be animated. // Search for Position property in the transitionsForCurrentCondition list of custom transitions, // and only use the particular parts of the animator as custom transitions should not effect all parameters of Position. // Typically Delay, Duration and Alphafunction can be custom. FindAndReplaceAnimatorComponentsForProperty(transitionsForCurrentCondition, AnimatableProperties.Position, ref positionTransitionComponents); // Size FindAndReplaceAnimatorComponentsForProperty(transitionsForCurrentCondition, AnimatableProperties.Size, ref sizeTransitionComponents); // Add animators to the core Animation, SetupAnimationForCustomTransitions(transitionsForCurrentCondition, layoutPositionData.Item.Owner); SetupAnimationForPosition(layoutPositionData, positionTransitionComponents); SetupAnimationForSize(layoutPositionData, sizeTransitionComponents); } } // class LayoutController } // namespace Tizen.NUI