Unity-UI-Extensions / com.unity.uiextensions

https://unity-ui-extensions.github.io/
1.2k stars 129 forks source link

Feature Request: set starting page in runtime for ScrollSnap components #451

Closed teh1archon closed 9 months ago

teh1archon commented 1 year ago


I need for the horizontal scroll snap component to set a different starting screen than 0. There is a property for that but it always resets to zero due to failing validation because the holder is not populated. I need to populate the horizontal screens during runtime. The fix is easy, just set the _currentPage to the desired value and call UpdateLayout. But _currentPage access modifier is internal so I can't change it from my code. Please add a method to force the change upon _currentPage so I can teleport to the correct page without animation during runtime without failing validation.

I even made a component for that already:
/#
namespace UnityEngine.UI.Extensions
{
    /// <summary>
    /// ScrollSnapBase and its inheritors do not support to change the initial screen for dynamically created screens.
    /// This extension forces it to allow the change the initial screen even for a not pre-populated holder.
    /// </summary>
    [RequireComponent(typeof(ScrollSnapBase))]
    public class ScrollSnapDynamicScreenSetter : MonoBehaviour
    {
        /// <summary>
        /// force set the initial page index for base, horizontal and vertical scroll snappers  
        /// </summary>
        /// <param name="screenIndex"></param>
        public void SetInitialPage(int screenIndex)
        {
            ScrollSnapBase targetScroller = GetComponent<ScrollSnapBase>();
            targetScroller._currentPage = screenIndex;
            targetScroller.UpdateLayout();
        }
    }
}
```c#
SimonDarksideJ commented 11 months ago

Sorry @teh1archon my notifications seem to not work, so I only just saw this.

Although I'm curious, why not just use the ChangePage method, or does this not work for you? (as it does the same code as above)

        public void ChangePage(int page)
        {
            GoToScreen(page);
        }

Edit - although I did just check and this method is missing in the docs, I'll get that updated asap

teh1archon commented 11 months ago

Hi, I don't have the code in front of me right now but the situation was something like this: Using GoToScreen before the page's instance exists is invalid and goes back to 0 (I think). All the pages in my project are created dynamically from the addressables. That's why I can't use the property Starting Page since it looks for instantiated objects in its OnValidate. I don't remember why the behavior is like this but after I create my pages and use ChangePage the scroller didn't move to it. I think because my pages had like pop-in animation so in that frame Unity's UI determines their width is 0. Either way, calling ChangePage after I instantiate my pages didn't move the scroller (or at least didn't jump but slid and it's not what I need) but my pages did load correctly. I admit that my code has a flaw in that it can take any value since it has no way to validate it and the end result may be wrong but at least it goes to the page I want without animation (I don't recall having a public method that I can pass an argument no change page with no animation).

SimonDarksideJ commented 11 months ago

OK, I think I understand your use case a bit better, but I feel we should try and update the existing functionality to work better rather than create a new function.

So I can accurately test your implementation, can you break down the steps and points for how you instantiate the control, then I can build a test to replicate it and validate any fix.

For instance, are you:

IS this what you are doing in your code? (p.s. the animations you use "shouldn't" have any effect, but if need be, I can test the same if you give me a guide for how you have set it up. Thanks

teh1archon commented 11 months ago

OK so the flow is like this: I login to the game and get all the setup data (everything is dynamic data). Next step, using the data, a Prefab that holds other nested prefabs is instantiated and it has the ScrollSnapHorizonal script. The script is set to have the first page index as 0 since it doesn't hold any Transforms as pages yet. Now I instantiate the actual pages under the correct holder. At this time I try to set the starting page index to 2 (this is a vertical mobile app with 5 pages/tabs and it starts at the middle one but it can also setup only 3 or 7 so the starting page is also a dynamic data).

I expect to be focused on the middle page but I'm still on the first one. I wonder if the UI doesn't finish processing all the UI layout building but Idouno, even though while at the code above no Awake/Start is happening on any of the pages (not that it matters) they are instantiated and the child count of the holder should be correct and the pages are created in the right size in the first place.

SimonDarksideJ commented 9 months ago

Hi @teh1archon , did some testing today and I could not replicate your issue, as ChangePage, even with dynamic content still changes to the selected page.

THe only thing I can think of is that you do not want the Lerp with the change page, as the control still swipes/lerps to the changed page. If you want it to just move to the page (rather than animate), I could see an issue.

I will update the ChangePage with an override to MOVE rather than lerp the current page after adding content.

Here is the test script I used:

public class CreateDynamicScrollSnap : MonoBehaviour
{
    [SerializeField]
    private GameObject ScrollSnapPrefab;

    private HorizontalScrollSnap hss;

    [SerializeField]
    private GameObject ScrollSnapContent;

    [SerializeField]
    private int startingPage = 0;

    private bool isInitialized = false;

    // Start is called before the first frame update
    void Start()
    {
        hss = Instantiate(ScrollSnapPrefab, this.transform).GetComponent<HorizontalScrollSnap>();
        hss.ChangePage(0);
    }

    // Update is called once per frame
    void Update()
    {
        if (!isInitialized && hss != null && Input.GetKeyDown(KeyCode.K))
        {
            AddHSSChildren();
            isInitialized = true;
        }
    }

    private void AddHSSChildren()
    {
        var contentGO = hss.transform.Find("Content");
        if (contentGO != null)
        {
            for (int i = 0; i < 10; i++)
            {
                GameObject item = Instantiate(ScrollSnapContent);
                SetHSSItemTest(item, $"Page {i}");
                hss.AddChild(item);
            }
            hss.ChangePage(startingPage);
        }
        else
        {
            Debug.LogError("Content not found");
        }
    }

    private void SetHSSItemTest(GameObject prefab, string value)
    {
        prefab.gameObject.name = value;
        prefab.GetComponentInChildren<UnityEngine.UI.Text>().text = value;
    }
}
SimonDarksideJ commented 9 months ago

OK, as an option (in the next release 2.3.2), I have added an argument to the "UpdateLayout" method, which allows resetting the position of the Scroll Snap to the starting position, which if you have changed the StartingScreen property, will force move the view to that item.

I'm also adding another example with your use case, which initializes the content on the press of a key

teh1archon commented 9 months ago

Yeah that's what I needed, to move without lerping. Thank you very much :)

SimonDarksideJ commented 9 months ago

Yup, so if you check the examples, that is now included. Essentially the flow is as follows:

Hope that helps. 2.3.2 is out now, both on the store and openupm