Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I create a bar area that slowly fills from left to right over 5, 10 or ?? seconds?

I have an application with a timer that can time anything from 1 - 300 seconds. Currently I show time remaining as a number that counts down every one second. It's done through the binding label vm.Timer

        App.Timer1Seconds = 10; // the value is set in code
                                // just using 10 as an example here

        while (App.Timer1Seconds > 0)
        {
            vm.Timer = App.Timer1Seconds.ToString();
            try
            {
                await Task.Delay(1000, App.tokenSource1.Token);
            }
            catch (TaskCanceledException)
            {
                App.Timer1Seconds = 0;
            }
            App.Timer1Seconds--;
        }

What I would like to do is to replace this with a bar area at the top of the screen and inside a grid so that it looks like this:

*********************

then

********************

then

*******************

then

******************

With the % of the bar filled in going from fully filled to nothing in a linear manner over the course of the time which could be anything from 300 to 3 seconds. Something a bit similar to the progress bar in a web browser when it retrieves a page but in this case I know the exact time of the activity.

Does anyone have any ideas as to how this could be created? Note that if it needs a custom renderer than I am okay with that solution.

Update:

I have implemented the solution by Sharada but there are two small questions I have:

Here's the code I have for my XAML:

<Grid Grid.Row="3" Grid.Column="0" VerticalOptions="FillAndExpand" HorizontalOptions="FillAndExpand" BackgroundColor="Aqua" RowSpacing="0" Padding="0">
   <Grid.RowDefinitions>
      <RowDefinition Height="2" />
   </Grid.RowDefinitions>
   <Grid.ColumnDefinitions>
      <ColumnDefinition Width="*" />
   </Grid.ColumnDefinitions>
   <local:TimerView Grid.Row="0" Grid.Column="0" StartTimerCommand="{Binding TimerStartCommand}" RemainingTime="{Binding TimeLeft}" HorizontalOptions="FillAndExpand" />
</Grid>

When it runs I would like the progress bar to occupy the full screen from right to left. However it seems to only use about 10% of the space like this:

|******                                                         |
|*****                                                          |
|****                                                           |
|***                                                            |
|**                                                             |
|*                                                              |

Can you suggest how I might be able to make the animation go from one side of the screen to another and then reduce in size like this:

|***************************************************************|
|************************************************************** |
|*************************************************************  |

...

|**                                                             |
|*                                                              |
|                                                               |

I notice there is no definition for timerView in the code behind. Here's what I added:

var timerView = new TimerView();

Let me know if that's okay.

Update 2:

Here's the full code for the page. I am still confused as to why the label does not fill from one side of the page to the other:

<?xml version="1.0" encoding="UTF-8"?>
<Frame xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:local="clr-namespace:Japanese;assembly=Japanese" x:Class="Japanese.PhrasesFrame" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand" BackgroundColor="Transparent" Padding="0" HasShadow="false">
    <StackLayout x:Name="phrasesFrameStackLayout" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand" IsVisible="false">
        <Grid x:Name="phraseGrid" BackgroundColor="Transparent" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand" Margin="0,20,0,0" RowSpacing="0">
            <Grid.RowDefinitions>
                <RowDefinition Height="8*" />
                <RowDefinition Height="70*" />
                <RowDefinition Height="8*" />
                <RowDefinition Height="4*" />
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*" />
            </Grid.ColumnDefinitions>
            <Grid Grid.Row="0" RowSpacing="5" Grid.Column="0" BackgroundColor="#EEEEEE" Padding="10,10,10,10" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand">
                <Grid IsVisible="{Binding InfoGridVisible, Converter={StaticResource InverseBoolConverter} }" VerticalOptions="FillAndExpand">
                </Grid>
                <Grid IsVisible="{Binding InfoGridVisible}">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="10*" />
                        <ColumnDefinition Width="10*" />
                        <ColumnDefinition Width="60*" />
                        <ColumnDefinition Width="5*" />
                        <ColumnDefinition Width="10*" />
                        <ColumnDefinition Width="5*" />
                    </Grid.ColumnDefinitions>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="50*" />
                        <RowDefinition Height="50*" />
                    </Grid.RowDefinitions>
                    <Label Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" />
                    <Label Grid.Row="0" Grid.Column="2" Grid.ColumnSpan="2" x:Name="statLabel" Style="{StaticResource smallLabel}" VerticalOptions="FillAndExpand" VerticalTextAlignment="Center" Text="{Binding StatLabel}" />
                    <Label Grid.Row="0" Grid.Column="3" Grid.ColumnSpan="3" x:Name="cvmLabel" Style="{StaticResource smallLabel}" VerticalOptions="FillAndExpand" VerticalTextAlignment="Center" Text="{Binding CvmLabel}" />
                    <Label Grid.Row="1" Grid.Column="0" x:Name="faveLabel" Style="{StaticResource smallIcon}" FontFamily="FontAwesome" VerticalOptions="FillAndExpand" VerticalTextAlignment="Center" />
                    <Label Grid.Row="1" Grid.Column="1" x:Name="hiddenLabel" Style="{StaticResource smallIcon}" FontFamily="FontAwesome" VerticalOptions="FillAndExpand" VerticalTextAlignment="Center" />
                    <Label Grid.Row="1" Grid.Column="2" x:Name="wordTypeLabel" Style="{StaticResource smallLeftLabel}" HorizontalTextAlignment="Start" Text="{Binding WordType}" />
                    <Label Grid.Row="1" Grid.Column="3" x:Name="points1" Style="{StaticResource smallLabel}" Text="{Binding Points1}" HorizontalTextAlignment="Start" />
                    <Label Grid.Row="1" Grid.Column="4" x:Name="points2" Style="{StaticResource smallLabel}" Text="{Binding Points2}" HorizontalTextAlignment="Start" />
                    <Label Grid.Row="1" Grid.Column="5" x:Name="timer" Style="{StaticResource smallLabel}" Text="{Binding Timer}" HorizontalTextAlignment="Start" />
                </Grid>
            </Grid>
            <Grid Grid.Row="1" Grid.Column="0" Padding="10,0,10,0" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand" BackgroundColor="#EEEEEE">
                <Frame CornerRadius="10" HasShadow="false">
                    <Grid>
                        <Grid IsVisible="{Binding WordGridVisible}" Padding="10" BackgroundColor="White">
                            <Grid.GestureRecognizers>
                                <TapGestureRecognizer Command="{Binding WordGridClickedCommand}" />
                            </Grid.GestureRecognizers>
                            <Grid.RowDefinitions>
                                <RowDefinition Height="45*" />
                                <RowDefinition Height="55*" />
                            </Grid.RowDefinitions>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="*" />
                            </Grid.ColumnDefinitions>
                            <Grid Grid.Row="0" Grid.Column="0" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand">
                                <Label x:Name="textLabel" Style="{StaticResource bigLabel}" XAlign="Center" VerticalOptions="Center" LineBreakMode="WordWrap" Text="{Binding TextLabel}" />
                            </Grid>
                            <Grid Grid.Row="1" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand" Padding="10,0,10,0" IsVisible="{Binding DetailGridVisible}">
                                <Grid.RowDefinitions>
                                    <RowDefinition Height="Auto" />
                                    <RowDefinition Height="Auto" />
                                    <RowDefinition Height="Auto" />
                                </Grid.RowDefinitions>
                                <Label x:Name="detail1" Grid.Row="0" Style="{StaticResource bigLabel}" Text="{Binding Detail1}" />
                                <Label x:Name="detail2" Grid.Row="1" Style="{StaticResource bigLabel}" Text="{Binding Detail2}" />
                                <Label x:Name="detail3" Grid.Row="2" Style="{StaticResource bigLabel}" Text="{Binding Detail3}" />
                            </Grid>
                        </Grid>
                        <Grid IsVisible="{Binding EmptyGridVisible}" Padding="10" BackgroundColor="White">
                            <Grid.RowDefinitions>
                                <RowDefinition Height="50*" />
                                <RowDefinition Height="50*" />
                            </Grid.RowDefinitions>
                            <Grid Grid.Row="0">
                                <Label FontSize="15" XAlign="Start" TextColor="Gray" VerticalOptions="Center" HorizontalOptions="FillAndExpand" Text="{Binding EmptyLabel1}" />
                            </Grid>
                            <Grid Grid.Row="1">
                                <Label FontSize="15" XAlign="Start" TextColor="Gray" VerticalOptions="Center" HorizontalOptions="FillAndExpand" Text="{Binding EmptyLabel2}" />
                            </Grid>
                        </Grid>
                    </Grid>
                </Frame>
            </Grid>
            <Grid Grid.Row="2" x:Name="buttonGrid" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand" BackgroundColor="#EEEEEE">
                <Grid IsVisible="{Binding EmptyFooterGridVisible }" Padding="10, 0" VerticalOptions="FillAndExpand" BackgroundColor="#EEEEEE">
                </Grid>
                <Grid IsVisible="{Binding ButtonGridVisible}" Padding="0" BackgroundColor="#EEEEEE" VerticalOptions="FillAndExpand">
                    <Grid IsVisible="{Binding CustomPointsSwitch}" VerticalOptions="FillAndExpand" Padding="10">
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="*" />
                            <ColumnDefinition Width="*" />
                            <ColumnDefinition Width="*" />
                            <ColumnDefinition Width="*" />
                        </Grid.ColumnDefinitions>
                        <Button Grid.Column="0" x:Name="aButton" Style="{StaticResource pointButton}" Text="Don't Know" BackgroundColor="#ff3b30" BorderColor="#ff3b30" TextColor="White" Command="{Binding AButtonClickedCommand}" />
                        <Button Grid.Column="1" x:Name="bButton" Style="{StaticResource pointButton}" Text="Very Hard" BackgroundColor="#FF9500" BorderColor="#FF9500" TextColor="White" Command="{Binding BButtonClickedCommand}" />
                        <Button Grid.Column="2" x:Name="cButton" Style="{StaticResource pointButton}" Text="Hard" BackgroundColor="#FFCC00" BorderColor="#FFCC00" TextColor="White" Command="{Binding CButtonClickedCommand}" />
                        <Button Grid.Column="3" x:Name="dButton" Style="{StaticResource pointButton}" Text="Easy" BackgroundColor="#4cd964" BorderColor="#4cd964" TextColor="White" Command="{Binding DButtonClickedCommand}" />
                    </Grid>
                    <Grid IsVisible="{Binding CustomPointsSwitch, Converter={StaticResource InverseBoolConverter} }" VerticalOptions="FillAndExpand" Padding="10">
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="*" />
                            <ColumnDefinition Width="*" />
                        </Grid.ColumnDefinitions>
                        <Button Grid.Column="0" VerticalOptions="FillAndExpand" Style="{StaticResource pointButton}" Text="Don't Know" BackgroundColor="#ff3b30" BorderColor="#ff3b30" TextColor="White" Command="{Binding NButtonClickedCommand}" />
                        <Button Grid.Column="1" VerticalOptions="FillAndExpand" Style="{StaticResource pointButton}" Text="Easy" BackgroundColor="#4cd964" BorderColor="#4cd964" TextColor="White" Command="{Binding YButtonClickedCommand}" />
                    </Grid>
                </Grid>
                <Grid IsVisible="{Binding ResetGridVisible}" Padding="10">
                    <Button Text="Reset All Points to Zero" TextColor="White" BackgroundColor="#4cd964" HorizontalOptions="FillAndExpand" VerticalOptions="StartAndExpand" Command="{Binding ResetButtonClickedCommand}" />
                </Grid>
            </Grid>
            <!--            <Grid x:Name="tapGrid" Grid.Row="3" Grid.Column="0" Padding="5,0,0,0" HorizontalOptions="FillAndExpand" VerticalOptions="Center">
                <Label x:Name="tapScreenLabel" Style="{StaticResource smallLabel}" />
            </Grid>-->
            <Grid Grid.Row="3" Grid.Column="0" VerticalOptions="FillAndExpand" HorizontalOptions="FillAndExpand" BackgroundColor="Aqua" RowSpacing="0" Padding="0">
                <Grid.RowDefinitions>
                    <RowDefinition Height="5" />
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>
                <local:TimerView Grid.Row="0" Grid.Column="0" StartTimerCommand="{Binding TimerStartCommand}" RemainingTime="{Binding TimeLeft}" HorizontalOptions="FillAndExpand" />
            </Grid>
        </Grid>
    </StackLayout>
</Frame>
like image 928
Alan2 Avatar asked Sep 14 '17 13:09

Alan2


2 Answers

As @hvaughan3 suggested, ProgressBar control can be used.

But if there is a need to create a control with customized look and feel, and custom animations then you can create your own custom progress-bar.

First step would be to create a custom animation that handles the Width animation.

public static class ViewExtensions
{
    public static Task<bool> WidthTo(this VisualElement self, double toWidth, uint length = 250, Easing easing = null)
    {
        easing = easing ?? Easing.Linear;
        var taskCompletionSource = new TaskCompletionSource<bool>();

        var animation = new Animation(
            callback: d => AbsoluteLayout.SetLayoutBounds(self, new Rectangle(0, 0, d, self.Height)),
            start: self.Width,
            end: toWidth,
            easing: easing);

        var offset = 1000;
        animation.Commit(self, "WidthTo", 
                         rate: Convert.ToUInt32(offset), 
                         length: length,
                         finished: (v, c) => taskCompletionSource.SetResult(c)
                        );

        return taskCompletionSource.Task;
    }
}

Next step would be to create a custom control that extends AbsoluteLayout and has following child controls:

  1. Progress-bar view: Represents time remaining progress as a bar

  2. Track-bar view: Represents full length

  3. Timer-label view: Represents time remaining progress as text label

The custom control can use DeviceTimer to keep the label updated, while using above animation (as we defined) to animate progress-bar to nothing.

The last step is to create a command that will trigger the timer. We use a command property so that it is MVVM friendly (i.e. can be triggered through view model too).

public class TimerView : AbsoluteLayout
{
    public TimerView()
    {
        //Load view when size has been allocated
        SizeChanged += (sender, e) =>
        {
            if (Width == 0 || Height == 0)
                return;

            if (TrackBar == null || ProgressBar == null)
                return;


            Children.Clear();

            //ensure track-bar gets full width and height as parent
            SetLayoutFlags(TrackBar, AbsoluteLayoutFlags.SizeProportional);
            SetLayoutBounds(TrackBar, new Rectangle(0, 0, 1, 1));
            Children.Add(TrackBar);

            //ensure progress-bar gets full height, but width can be changed
            SetLayoutFlags(ProgressBar, AbsoluteLayoutFlags.None);
            SetLayoutBounds(ProgressBar, new Rectangle(0, 0, Width, Height));
            Children.Add(ProgressBar);

            //if timer-label available, ensure it gets full width and height
            if (TimerLabel != null)
            {
                SetLayoutFlags(TimerLabel, AbsoluteLayoutFlags.SizeProportional);
                SetLayoutBounds(TimerLabel, new Rectangle(0, 0, 1, 1));
                Children.Add(TimerLabel);

                TimerLabel.SetBinding(BindingContextProperty, new Binding(nameof(RemainingTime), source: this));
            }

            if (AutoStart != default(TimeSpan))
            {
                RemainingTime = AutoStart;
                StartTimerCommand.Execute(RemainingTime);
            }    
        };

        StartTimerCommand = new Command(async (timer) =>
        {
            if (!IsEnabled)
                return;

            //reset progress-bar width
            SetLayoutBounds(ProgressBar, new Rectangle(0, 0, Width, Height));
            if (timer != null && timer is TimeSpan)
                RemainingTime = (TimeSpan)timer;

            IsEnabled = false;

            //Start timer for label update
            var ctrlTobeUpdated = this;
            Device.StartTimer(TimeSpan.FromSeconds(1), () =>
            {
                var oneSecond = TimeSpan.FromSeconds(1);

                ctrlTobeUpdated.RemainingTime -= oneSecond;
                if (ctrlTobeUpdated.RemainingTime < oneSecond)
                {
                    ctrlTobeUpdated = null;
                    return false;
                }
                else
                    return true;
            });

            //Start animation
            await ProgressBar.WidthTo(0, Convert.ToUInt32(RemainingTime.TotalMilliseconds));
            IsEnabled = true;
        });

    }

    public static readonly BindableProperty TrackBarProperty =
        BindableProperty.Create(
            "TrackBar", typeof(View), typeof(TimerView),
            defaultValue: null);

    public View TrackBar
    {
        get { return (View)GetValue(TrackBarProperty); }
        set { SetValue(TrackBarProperty, value); }
    }

    public static readonly BindableProperty ProgressBarProperty =
        BindableProperty.Create(
            "ProgressBar", typeof(View), typeof(TimerView),
            defaultValue: null);

    public View ProgressBar
    {
        get { return (View)GetValue(ProgressBarProperty); }
        set { SetValue(ProgressBarProperty, value); }
    }

    public static readonly BindableProperty TimerLabelProperty =
        BindableProperty.Create(
            "TimerLabel", typeof(Label), typeof(TimerView),
            defaultValue: default(Label));

    public Label TimerLabel
    {
        get { return (Label)GetValue(TimerLabelProperty); }
        set { SetValue(TimerLabelProperty, value); }
    }

    public static readonly BindableProperty StartTimerCommandProperty =
        BindableProperty.Create(
            "StartTimerCommand", typeof(ICommand), typeof(TimerView),
            defaultBindingMode: BindingMode.OneWayToSource,
            defaultValue: default(ICommand));

    public ICommand StartTimerCommand
    {
        get { return (ICommand)GetValue(StartTimerCommandProperty); }
        set { SetValue(StartTimerCommandProperty, value); }
    }

    public static readonly BindableProperty RemainingTimeProperty =
        BindableProperty.Create(
            "RemainingTime", typeof(TimeSpan), typeof(TimerView),
            defaultBindingMode: BindingMode.OneWayToSource,
            defaultValue: default(TimeSpan));

    public TimeSpan RemainingTime
    {
        get { return (TimeSpan)GetValue(RemainingTimeProperty); }
        set { SetValue(RemainingTimeProperty, value); }
    }

    public static readonly BindableProperty AutoStartProperty =
        BindableProperty.Create(
            "AutoStart", typeof(TimeSpan), typeof(TimerView),
            defaultValue: default(TimeSpan));

    public TimeSpan AutoStart
    {
        get { return (TimeSpan)GetValue(AutoStartProperty); }
        set { SetValue(AutoStartProperty, value); }
    }
}

Sample Usage 1

XAML

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="5" />
        <RowDefinition Height="Auto" />
        <RowDefinition Height="*" />
    </Grid.RowDefinitions>

    <local:TimerView Grid.Column="0" Grid.Row="0" x:Name="timerView">
        <local:TimerView.ProgressBar>
            <BoxView BackgroundColor="Maroon" />
        </local:TimerView.ProgressBar>
        <local:TimerView.TrackBar>
            <BoxView BackgroundColor="Gray" />
        </local:TimerView.TrackBar>
    </local:TimerView>

    <Label Grid.Row="1" Text="{Binding Path=RemainingTime, StringFormat='{0:%s} seconds left', Source={x:Reference timerView}}" HorizontalOptions="Center" /> 
    <Button VerticalOptions="Start" Grid.Row="2" Text="Start Timer" x:Name="startBtn" Clicked="Handle_Clicked" />

</Grid>

Code-behind

void Handle_Clicked(object sender, System.EventArgs e)
{
    timerView.StartTimerCommand.Execute(TimeSpan.FromSeconds(10));
}

enter image description here

Sample Usage 2 - Custom look

XAML

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="5" />
        <RowDefinition Height="25" />
        <RowDefinition Height="*" />
    </Grid.RowDefinitions>
     <local:TimerView Grid.Row="1" x:Name="timerView">
        <local:TimerView.ProgressBar>
            <Frame HasShadow="false" Padding="0" Margin="0" BackgroundColor="Aqua" />
        </local:TimerView.ProgressBar>
        <local:TimerView.TrackBar>
            <Frame HasShadow="true" Padding="0" Margin="0" />
        </local:TimerView.TrackBar>
        <local:TimerView.TimerLabel>
            <Label Text="{Binding Path=., StringFormat='{0:%m}:{0:%s}'}" HorizontalTextAlignment="Center" VerticalTextAlignment="Center" /> 
        </local:TimerView.TimerLabel>
    </local:TimerView>

    <Button VerticalOptions="Start" Grid.Row="2" Text="Start Timer" x:Name="startBtn" Clicked="Handle_Clicked" />
</Grid>

enter image description here

EDIT - 1

Sample Usage 3 - View Model

XAML

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="5" />
        <RowDefinition Height="Auto" />
        <RowDefinition Height="*" />
    </Grid.RowDefinitions>

    <local:TimerView Grid.Row="0" StartTimerCommand="{Binding TimerStartCommand}" RemainingTime="{Binding TimeLeft}">
        <local:TimerView.ProgressBar>
            <BoxView BackgroundColor="Maroon" />
        </local:TimerView.ProgressBar>
        <local:TimerView.TrackBar>
            <BoxView BackgroundColor="Gray" />
        </local:TimerView.TrackBar>
    </local:TimerView>
    <Label Grid.Row="1" Text="{Binding Path=TimeLeft, StringFormat='{0:%s}'}" HorizontalOptions="Center" /> 
    <Button VerticalOptions="Start" Grid.Row="2" Text="Start Timer" Command="{Binding ButtonClickCommand}" />

</Grid>

View model

public class ProgressVM : BaseViewModel
{
    public Command TimerStartCommand { get; set; }
    public Command ButtonClickCommand => new Command(() => TimerStartCommand?.Execute(TimeSpan.FromSeconds(20)));

    private TimeSpan _timeLeft;
    public TimeSpan TimeLeft
    {
        get { return _timeLeft; }
        set
        {
            _timeLeft = value;
            OnPropertyChanged();
        }
    }
}

EDIT - 2 : As per question update

If you need an animation that first goes from left to right in an instant and then goes back to left; that is also possible through a minor update in control.

enter image description here

Then, you will need to update SizeChanged handler, and StartTimer handler as following:

public class TimerView : AbsoluteLayout
{
    public TimerView()
    {
        SizeChanged += (sender, e) =>
        {
            if (Width == 0 || Height == 0)
                return;

            if (TrackBar == null || ProgressBar == null)
                return;


            Children.Clear();

            SetLayoutFlags(TrackBar, AbsoluteLayoutFlags.SizeProportional);
            SetLayoutBounds(TrackBar, new Rectangle(0, 0, 1, 1));
            Children.Add(TrackBar);

            SetLayoutFlags(ProgressBar, AbsoluteLayoutFlags.None);
            SetLayoutBounds(ProgressBar, new Rectangle(0, 0, 0, Height));
            Children.Add(ProgressBar);

            if(TimerLabel != null)
            {
                SetLayoutFlags(TimerLabel, AbsoluteLayoutFlags.SizeProportional);
                SetLayoutBounds(TimerLabel, new Rectangle(0, 0, 1, 1));
                Children.Add(TimerLabel);

                TimerLabel.SetBinding(BindingContextProperty, new Binding(nameof(RemainingTime), source: this));
            }
        };

        StartTimerCommand = new Command(async (timer) =>
        {
            if (!IsEnabled)
                return;

            if (timer != null && timer is TimeSpan)
                RemainingTime = (TimeSpan)timer;

            IsEnabled = false;

            var ctrlTobeUpdated = this;
            Device.StartTimer(TimeSpan.FromSeconds(1), () =>
            {
                var oneSecond = TimeSpan.FromSeconds(1);

                ctrlTobeUpdated.RemainingTime -= oneSecond;
                if (ctrlTobeUpdated.RemainingTime < oneSecond)
                {
                    ctrlTobeUpdated = null;
                    return false;
                }
                else
                    return true;
            });

            await ProgressBar.WidthTo(Width, Convert.ToUInt32(150));
            await ProgressBar.WidthTo(0, Convert.ToUInt32(RemainingTime.TotalMilliseconds - 150));
            IsEnabled = true;
        });

    }

EDIT - 3: Removed code/support for default values in bindable property definition for ProgressBar, and TimerBar.

EDIT - 4: Add support for auto-start timer on load.

Sample Usage 4: Auto start timer

XAML

<local:TimerView AutoStart="0:0:20"> <!-- timespan for 20 seconds or VM based AutoStart="{Binding SetTime}" -->
    <local:TimerView.ProgressBar>
        <BoxView BackgroundColor="Maroon" />
    </local:TimerView.ProgressBar>
    <local:TimerView.TrackBar>
        <BoxView BackgroundColor="Gray" />
    </local:TimerView.TrackBar>
</local:TimerView>

EDIT - 5: Add support for pause and stop commands for timer. Also updated timer view to be more resilient in terms of device-orientation changes.

  • src: TimerView.cs
  • src: TimerView.Properties.cs
like image 180
Sharada Gururaj Avatar answered Oct 18 '22 18:10

Sharada Gururaj


As already mentioned in the comments, a Xamarin.Forms.ProgressBar would work well in this scenario.

To assist with creating this functionality the following classes were used. Similar to what you were doing with the label.

public class CountDownTimer {
    TimeSpan delay = TimeSpan.FromSeconds(1);

    public Task Wait(int seconds, IProgress<CountdownReport> progress = null) {
        return Wait(seconds, CancellationToken.None, progress);
    }

    public async Task Wait(int seconds, CancellationToken cancellationToken = default (CancellationToken), IProgress<CountdownReport> progress = null) {
        var total = TimeSpan.FromSeconds(seconds);
        var current = total.TotalSeconds;
        while (current > 0) {
            reportProgress(progress, total, current);
            try {
                //simulate wait
                await Task.Delay(delay, cancellationToken);
            } catch (TaskCanceledException e) {
                current = 0;
            }
            current--;
        }
        if (current > -1) {
            reportProgress(progress, total, current);
        }
    }

    private void reportProgress(IProgress<CountdownReport> progress, TimeSpan total, double current) {
        if (progress != null) {
            var percentageRemaining = (int)((current / total.TotalSeconds) * 100);
            var progressPercentage = 100 - percentageRemaining;
            var timeRemaining = TimeSpan.FromSeconds(current);
            var report = new CountdownReport {
                InitialTime = total,
                RemainingTime = timeRemaining,
                ProgressPercentage = progressPercentage,
                RemainingPercentage = percentageRemaining
            };
            //report progress
            progress.Report(report);
        }
    }
}

public class CountdownReport {
    public TimeSpan InitialTime { get; set; }
    public int ProgressPercentage { get; set; }        
    public int RemainingPercentage { get; set; }
    public TimeSpan RemainingTime { get; set; }        
}

Assuming you have a view with a ProgressBar called progressBar

public async void btnStartTimer_Click(object sender, EventArgs args) {
    var time = App.Timer1Seconds; //Just using as an example;    
    var timer = new CountDownTimer();
    var progress = new CountDownPercentage(e => {        
        var remainingTime = e.RemainingTime;
        App.Timer1Seconds = remainingTime.TotalSeconds;
        var progressPercentage = e.ProgressPercentage;
        progressBar.Progress = progressPercentage / 100;
    });
    // start countdown and wait
    await timer.Wait(time, App.tokenSource1.Token, progress);

    //...do anything else needed after the countdown completes.
}

Where CountDownPercentage was defined as

public class CountDownPercentage : IProgress<CountdownReport> {
    private Action<CountdownReport> handler;
    public CountDownPercentage(Action<CountdownReport> handler) {
        this.handler = handler;
    }
    public void Report(CountdownReport value) {
        if (handler != null) {
            handler(value);
        }
    }
}

While the above example initiates the countdown in button click, the same can be done on any other event or command that suits your scenario. There is room to modify the above strategy. This serves as a good foundation to build upon.

Example .Net fiddle of the code above

like image 4
Nkosi Avatar answered Oct 18 '22 18:10

Nkosi