Pull-to-Reload Customizations

This is a continuation from this thread. I’ve created a basic pull-to-reload and wanted to customize some of the behavior (namely the threshold ‘pull amount’ required to start the PulledPastThreshold state && the ‘stickiness’ of the threshold boundary). Watching Mortoray’s excellent Twitch channel today, he answered most of my questions, indicating the threshold amount is not adjustable (hard-coded magic number).

I hope that explains the customization I would like to implement. Additionally, I do not fully understand all of the Range options in the ScrollingAnimation class, which I am using to customize/override the default ScrollView behavior. Can someone elaborate on the options? I have not found any available documentation other than the list of option values.

I have created a simple sandbox to test and discover as much about the workings of the PullToReload class as I can (perhaps it will benefit others).

<App>
    <JavaScript>
        var Observable = require("FuseJS/Observable");
        var isBusy = Observable(false); // indicates when 'busy' is occuring

        // This function will recieve the ReloadHandler call once the 'pull' threshold
        // has been reached.
        function reload() {
            isBusy.value = true;
            console.log("reload has been fired..." + isBusy.value);

            // simulate 5 seconds of busy
            setTimeout( () => {
                isBusy.value = false;
                console.log("reload is finished..." + isBusy.value);
            }, 5000);
        }

        module.exports = {
            isBusy,
            reload,
        }
    </JavaScript>

    <ClientPanel>
        <DockPanel>
            <StackPanel Orientation="Vertical">
                <!-- purely visual element -->
                <Panel Dock="Top" Height="50" Color="#2377B3">
                    <Text FontSize="32" Color="#ddd" Alignment="Center"
                        Value="Title Bar" />
                </Panel>

                <!-- visual display element for 'pulling' indicator -->
                <!-- NOTE: set MinHeight and Opacity default values (0) 
                for Busy and ScrollingAnimation animations to effectively 
                revert them back. -->
                <Panel ux:Name="pulledPanel" Height="0" Color="#D6F274" Opacity="0" MinHeight="0">
                    <Text ux:Name="pulledText" Padding="5"
                        Alignment="BottomCenter" FontSize="18" Color="#333" 
                        TextWrapping="Wrap" TextAlignment="Center"
                        Value="Nothing Happening" />
                </Panel>
 
                <!-- handle 'busy' animations/changes -->
                <Busy ux:Name="isBusy" IsActive="{isBusy}" />
                <WhileBusy>
                    <Rectangle Color="#aaa7" Layer="Overlay" Alignment="Center" Padding="15,5" CornerRadius="10">
                        <Text Value="Busy is active: {isBusy}" FontSize="32" />
                    </Rectangle>
                    <!-- NOTE: DurationBack values are purposefully set 'high' to 
                    demonstrate the activation of the Rest state once the IsLoading 
                    binding in PullToReload is set false. Set these back to something
                    more appealing, i.e. .4 and .2 respectively. -->
                    <Change pulledPanel.MinHeight="45" DurationBack="1.5" />
                    <Change pulledPanel.Opacity="1" DurationBack=".75" />
                </WhileBusy>

                <ScrollView SnapMinTransform="False">
                    <!-- For interesting effects, in place of Range, use From/To values 
                    to control how quickly the Change values are reached. i.e. From="0" To="-50",
                    will effectively reveal the full panel Height of 125 with only a -50 point
                    'pull'. The negative number indicates pulling down from the uppermost
                    bound of the StackPanel. Likewise, a high negative value requires more 'pull'
                    to affect the ScrollingAnimation actions and effectively reduces the
                    reveal of the pulledPanel in this example. -->
                    <!-- <ScrollingAnimation From="0" To="-50" > -->
                    <!-- <ScrollingAnimation From="0" To="-500" > -->
                    <ScrollingAnimation Range="SnapMin" >
                        <Change pulledPanel.Opacity="1" />
                        <Change pulledPanel.Height="125" />
                    </ScrollingAnimation>

                    <!-- NOTE: if the IsLoading binding is not set/reset, the PullToReload 
                    element will not be 'reset' after it has fired the ReloadHandler. 
                    To experiment, remove the IsLoading binding or set the property to 
                    something that does not exist (i.e. isBuzzzy vice isBusy) and pull 
                    the element to fire the reload, then try again - the threshold cannot 
                    be triggered.-->
                    <PullToReload IsLoading="{isBusy}" ReloadHandler="{reload}">
                            <State ux:Binding="Pulling">
                                <!-- actions occuring while pulling (not in relation to pull distance) -->
                                <Change pulledText.Value="Pulling state..." />
                            </State>
                            <State ux:Binding="PulledPastThreshold">
                                <!-- actions occuring once threshold has been triggered -->
                                <Change pulledPanel.Color="#2377B3" />
                                <Change pulledText.Value="Reached threshold (ReloadHandler will be fired when released/reversed)" />
                            </State>
                            <State ux:Binding="Loading">
                                <!-- actions occuring while IsLoading binding is true -->
                                <Change pulledText.Value="Loading state..." />
                                <Change pulledPanel.Color="#FC956F" />
                            </State>
                            <State ux:Binding="Rest">
                                <!-- actions occuring when the 'pull' action is released/reversed
                                AND IsLoading binding is false. -->
                                <Change pulledText.Value="Rest state..." />
                                <Change pulledPanel.Color="#74F2B7" />
                            </State>
                    </PullToReload>

                    <!-- stacked display elements "green" -->
                    <StackPanel Orientation="Vertical" ItemSpacing="3" Padding="3">
                        <Each Count="5">
                            <Rectangle Height="125" Color="#74F2B7" CornerRadius="10" />
                        </Each>
                    </StackPanel>
                </ScrollView>
            </StackPanel>
        </DockPanel>
    </ClientPanel>
</App>

Hi Darrin,

thanks for posting an excellent reproduction, pleasure to work with!

I would like to first handle an issue that you were not asking about, and it’s the fact that you have put your ScrollView inside of a StackPanel. That is something we strongly discourage because children of StackPanels try to take up as little space as possible by default. The result usually is that a ScrollView will not be scrollable, which you can easily see if you increase the number of Rectangles in your list from 5 to 10.

Here, I fixed your code so that it works correctly:

<App>
    <JavaScript>
        var Observable = require("FuseJS/Observable");
        var isBusy = Observable(false); // indicates when 'busy' is occuring

        // This function will recieve the ReloadHandler call once the 'pull' threshold
        // has been reached.
        function reload() {
            isBusy.value = true;
            console.log("reload has been fired..." + isBusy.value);

            // simulate 5 seconds of busy
            setTimeout( () => {
                isBusy.value = false;
                console.log("reload is finished..." + isBusy.value);
            }, 5000);
        }

        module.exports = {
            isBusy,
            reload,
        }
    </JavaScript>

    <ClientPanel>
        <DockPanel>
            <StackPanel Dock="Top" Orientation="Vertical">
                <!-- purely visual element -->
                <Panel Height="50" Color="#2377B3">
                    <Text FontSize="32" Color="#ddd" Alignment="Center"
                        Value="Title Bar" />
                </Panel>

                <!-- visual display element for 'pulling' indicator -->
                <!-- NOTE: set MinHeight and Opacity default values (0) 
                for Busy and ScrollingAnimation animations to effectively 
                revert them back. -->
                <Panel ux:Name="pulledPanel" Height="0" Color="#D6F274" Opacity="0" MinHeight="0">
                    <Text ux:Name="pulledText" Padding="5"
                        Alignment="BottomCenter" FontSize="18" Color="#333" 
                        TextWrapping="Wrap" TextAlignment="Center"
                        Value="Nothing Happening" />
                </Panel>
 
                <!-- handle 'busy' animations/changes -->
                <Busy ux:Name="isBusy" IsActive="{isBusy}" />
                <WhileBusy>
                    <Rectangle Color="#aaa7" Layer="Overlay" Alignment="Center" Padding="15,5" CornerRadius="10">
                        <Text Value="Busy is active: {isBusy}" FontSize="32" />
                    </Rectangle>
                    <!-- NOTE: DurationBack values are purposefully set 'high' to 
                    demonstrate the activation of the Rest state once the IsLoading 
                    binding in PullToReload is set false. Set these back to something
                    more appealing, i.e. .4 and .2 respectively. -->
                    <Change pulledPanel.MinHeight="45" DurationBack="1.5" />
                    <Change pulledPanel.Opacity="1" DurationBack=".75" />
                </WhileBusy>

            </StackPanel>

            <ScrollView SnapMinTransform="False">
                <!-- For interesting effects, in place of Range, use From/To values 
                to control how quickly the Change values are reached. i.e. From="0" To="-50",
                will effectively reveal the full panel Height of 125 with only a -50 point
                'pull'. The negative number indicates pulling down from the uppermost
                bound of the StackPanel. Likewise, a high negative value requires more 'pull'
                to affect the ScrollingAnimation actions and effectively reduces the
                reveal of the pulledPanel in this example. -->
                <!-- <ScrollingAnimation From="0" To="-50" > -->
                <!-- <ScrollingAnimation From="0" To="-500" > -->
                <ScrollingAnimation Range="SnapMin" >
                    <Change pulledPanel.Opacity="1" />
                    <Change pulledPanel.Height="125" />
                </ScrollingAnimation>

                <!-- NOTE: if the IsLoading binding is not set/reset, the PullToReload 
                element will not be 'reset' after it has fired the ReloadHandler. 
                To experiment, remove the IsLoading binding or set the property to 
                something that does not exist (i.e. isBuzzzy vice isBusy) and pull 
                the element to fire the reload, then try again - the threshold cannot 
                be triggered.-->
                <PullToReload IsLoading="{isBusy}" ReloadHandler="{reload}">
                        <State ux:Binding="Pulling">
                            <!-- actions occuring while pulling (not in relation to pull distance) -->
                            <Change pulledText.Value="Pulling state..." />
                        </State>
                        <State ux:Binding="PulledPastThreshold">
                            <!-- actions occuring once threshold has been triggered -->
                            <Change pulledPanel.Color="#2377B3" />
                            <Change pulledText.Value="Reached threshold (ReloadHandler will be fired when released/reversed)" />
                        </State>
                        <State ux:Binding="Loading">
                            <!-- actions occuring while IsLoading binding is true -->
                            <Change pulledText.Value="Loading state..." />
                            <Change pulledPanel.Color="#FC956F" />
                        </State>
                        <State ux:Binding="Rest">
                            <!-- actions occuring when the 'pull' action is released/reversed
                            AND IsLoading binding is false. -->
                            <Change pulledText.Value="Rest state..." />
                            <Change pulledPanel.Color="#74F2B7" />
                        </State>
                </PullToReload>

                <!-- stacked display elements "green" -->
                <StackPanel Orientation="Vertical" ItemSpacing="3" Padding="3">
                    <Each Count="10">
                        <Rectangle Height="125" Color="#74F2B7" CornerRadius="10" />
                    </Each>
                </StackPanel>
            </ScrollView>
            
        </DockPanel>
    </ClientPanel>
</App>

I’ll shortly post another answer in which I’ll try to cover the rest of your questions.

The PullToReload trigger is no magic. Actually, you see exactly how it is implemented in fuselibs-public.

The logic behind all the ScrollingAnimation.Range options resides in ScrollingAnimation.uno and are used for the math to calculate the scroll progress. As you’ll see from the code there, that method accesses a number of properties on the attached ScrollView, which makes sense - there’s a number of different directions a ScrollView can be scrolled, and an even greater number of properties you can set on one.

So, technically, you should be able to adjust that “pull amount” threshold, but I imagine it will not be an easy task.

Let us know if this helps and if we can be of further assistance.

@Uldis - awesome! Thanks for all of the feedback. And THANKS for the pointers on the ScrollView inside a StackPanel. I also moved the Busy and WhileBusy code to the top of the ClientPanel so the Busy ‘indicator’ display rectangle positions itself in the center of the screen vice riding at the top of the display on the ‘pulledPanel’ Panel element.

    <ClientPanel>
        <!-- handle 'busy' animations/changes -->
        <Busy ux:Name="isBusy" IsActive="{isBusy}" />
        <WhileBusy>
            <Rectangle Color="#aaa7" Alignment="Center" Padding="15,5" CornerRadius="10">
                <Text Value="Busy is active: {isBusy}" FontSize="32" />
            </Rectangle>
            <!-- NOTE: DurationBack values are purposefully set 'high' to 
            demonstrate the activation of the Rest state once the IsLoading 
            binding in PullToReload is set false. Set these back to something
            more appealing, i.e. .4 and .2 respectively. -->
            <Change pulledPanel.MinHeight="45" DurationBack=".4" Easing="SinusoidalIn" />
            <Change pulledPanel.Opacity="1" DurationBack=".2" />
        </WhileBusy>

Technically, it doesn’t need to have the Layer="Overlay" property either – so removed that as well.

I will dig through the source you mentioned. Thanks again!