Development
Step 1: Create new project
-
Open Visual Studio IDE to create a new Tizen Wearable Xaml App.
- When the start window appears, choose Create a new project. Alternatively, you can do it via File > New > Project.
- In the new project window, set up the fields based on the following screen. Choose Tizen Wearable Xaml App and press the Next button.
- In the Configure your new project, set project name as SquatCounter and press the Create button.
-
Add/remove files
- Right click on
MainPage.xaml
and select delete from the context menu - Right click on
App.xaml
and delete it - Right click on SquatCounter project, select Add > New Item from the context menu, choose C# class and name it
App.cs
. After adding the file, we need to enter following code toApp.cs
using Xamarin.Forms; using Xamarin.Forms.Xaml; namespace SquatCounter { public class App : Application { public App() { } } }
- Right click on
-
Create folder structure
- Right click on SquatCounter project, select Add > New Folder from the context menu and name it Views
- Simalarly, add new folders named ViewModels and Services
After passing through all the steps, the structure of the project should look like the following image.
Learn more by watching the video
Step 2: Add the assets
-
Download and unpack the assets for the application.
It consists of two folders.
Inside, you may see these images:
- background image file for counting squats page (
res/squat_background.png
)
- three image files for guide pages (
res/step_1.png
,res/step_2.png
,res/step_3.png
)
- image file for clock step page (
res/clock.png
)
- image file of application icon (
shared/res/SquatCounter.png
)
- background image file for counting squats page (
-
Select the unpacked folders, that is res and shared, and copy them with context menu or press Ctrl + C. Next, make a left mouse click on the project name SquatCounter in the Solution Explorer and press Ctrl + V, to paste copied folders directly to the project.
-
Confirm the pop-ups to replace the content of the res and shared folders of the project.
Learn more by watching the video
Step 3: Implement services - part 1
First, we will develop the application logic.
Let's start with the implementation of the page navigation service based on singleton design pattern. This service will provide navigation between pages across the application.
-
Go to the Solution Explorer, right click on folder Services, select Add > New Item from the context menu, choose class and name it
PageNavigationService.cs
. -
Let's modify our class based on principles of the singleton design pattern.
namespace SquatCounter.Services { public sealed class PageNavigationService { private static PageNavigationService _instance; public static PageNavigationService Instance { get => _instance ?? (_instance = new PageNavigationService()); } } }
-
Right click on the Views folder, select Add > New from the context menu, choose Content page and name it
GuidePage.xaml
. -
Next, we will add
GoToGuidePage
method that will display GuidePage. We will also create a body ofGoToSquatCounterPage
method, which will be completed in later on.using SquatCounter.Views; using Xamarin.Forms; namespace SquatCounter.Services { public sealed class PageNavigationService { private static PageNavigationService _instance; public static PageNavigationService Instance { get => _instance ?? (_instance = new PageNavigationService()); } public void GoToGuidePage() { Application.Current.MainPage = new NavigationPage(new GuidePage()); } public void GoToSquatCounterPage() { } } }
-
The last thing we need to do is modify
App.cs
constructor to display the guide page. At the code below, we have added compile-time checking on all XAML files within namespace. It will help us to notice errors while compile-time, improves performance of the views and reduce size of our app.using SquatCounter.Services; using Xamarin.Forms; using Xamarin.Forms.Xaml; [assembly: XamlCompilation(XamlCompilationOptions.Compile)] namespace SquatCounter { public class App : Application { public App() { PageNavigationService.Instance.GoToGuidePage(); } } }
-
After build and run application we should see following screen.
Learn more by watching the video
Step 4: Implement services - part 2
Now, we will implement a service for the Pressure Sensor. It will allow us to obtain pressure data from instance of the pressure sensor and this data will be used later to count squats.
-
Go to the Solution Explorer, right click on Services folder, select Add > New Item from the context menu, choose class and name it
PressureSensorService.cs
. -
Let's modify our
PressureSensorService.cs
class. Before we start implementing the pressure sensor, we need to change class access modifier to public and create public constructor with empty body.namespace SquatCounter.Services { public class PressureSensorService { public PressureSensorService() { } } }
-
Next, we need to add
PressureSensor
instance which allows us to obtain pressure data.using Tizen.Sensor; namespace SquatCounter.Services { public class PressureSensorService { private readonly PressureSensor _pressureSensor; public PressureSensorService() { } } }
-
Before we create the instance of
PressureSensor
we need to add following properties and method.NotSupportedMsg
- this field have a string message which will be displayed ifPressureSensor
is not supportedInterval
- this field describes frequency in milliseconds of calling callbackPressureSensorUpdated
- this is the callback method that will be called everyInterval
frequency
using Tizen.Sensor; namespace SquatCounter.Services { public class PressureSensorService { private readonly PressureSensor _pressureSensor; private const string NotSupportedMsg = "Pressure sensor is not supported on your device."; private const int Interval = 10; public PressureSensorService() { } private void PressureSensorUpdated(object sender, PressureSensorDataUpdatedEventArgs e) { } } }
-
In this step, we will create and start instance of
PressureSensor
in the constructor body ofPressureSensorService
.using Tizen.Sensor; using System; namespace SquatCounter.Services { public class PressureSensorService { private PressureSensor _pressureSensor; private const string NotSupportedMsg = "Pressure sensor is not supported on your device."; private const int Interval = 10; public PressureSensorService() { if (!PressureSensor.IsSupported) { throw new NotSupportedException(NotSupportedMsg); } _pressureSensor = new PressureSensor(); _pressureSensor.DataUpdated += PressureSensorUpdated; _pressureSensor.Interval = Interval; _pressureSensor.Start(); } private void PressureSensorUpdated(object sender, PressureSensorDataUpdatedEventArgs e) { } } }
-
In this step, we will create an event handler, which is invoked whenever
PressueSensorUpdate
callback has been raised.using System; using Tizen.Sensor; namespace SquatCounter.Services { public class PressureSensorService { private PressureSensor _pressureSensor; private const string NotSupportedMsg = "Pressure sensor is not supported on your device."; public event EventHandler<float> ValueUpdated; private const int Interval = 10; public PressureSensorService() { if (!PressureSensor.IsSupported) { throw new NotSupportedException(NotSupportedMsg); } _pressureSensor = new PressureSensor(); _pressureSensor.DataUpdated += PressureSensorUpdated; _pressureSensor.Interval = Interval; _pressureSensor.Start(); } private void PressureSensorUpdated(object sender, PressureSensorDataUpdatedEventArgs e) { ValueUpdated?.Invoke(this, e.Pressure); } } }
-
The last thing that we need to do is release the resources obtained by
PressureSensor
. To achieve this we will use Dispose pattern. This step is very important for our application. If we do not release the resources, then the app may crash.namespace SquatCounter.Services { public class PressureSensorService : IDisposable { private PressureSensor _pressureSensor; private const string NotSupportedMsg = "Pressure sensor is not supported on your device."; public event EventHandler<float> ValueUpdated; private const int Interval = 10; public PressureSensorService() { if (!PressureSensor.IsSupported) { throw new NotSupportedException(NotSupportedMsg); } _pressureSensor = new PressureSensor(); _pressureSensor.DataUpdated += PressureSensorUpdated; _pressureSensor.Interval = Interval; _pressureSensor.Start(); } private void PressureSensorUpdated(object sender, PressureSensorDataUpdatedEventArgs e) { ValueUpdated?.Invoke(this, e.Pressure); } public void Dispose() { _pressureSensor?.Stop(); _pressureSensor.DataUpdated -= PressureSensorUpdated; _pressureSensor?.Dispose(); } } }
Learn more by watching the video
Step 5: Implement services - part 3
The data we receive from the sensor must be processed in order to obtain the necessary information from it. Now we can create our counting squats logic with the following points:
- Read the first 10 pressure readings from
PressureCounterService
. - Determine our calibration upper and lower threshold pressure at starting point.
- Every next pressure reading calculate a moving average of the 10 last readings.
- If the computed average is between upper and lower threshold, then count the squat.
-
Go to the Solution Explorer, right click on Services folder, select Add > New Item from the context menu, choose C# class and name it
SquatCounterService.cs
. -
To obtain data from
PressureSensorService
, we need to add following things:- private field
_pressureSensorService
- callback method
PressureSensorUpdated
for value update inPressureSensorService
- create instance of
PressureSensorService
in body ofSquatCounterService
constructor and subscribe updated event by our callback method
namespace SquatCounter.Services { public class SquatCounterService { private PressureSensorService _pressureService; public SquatCounterService() { _pressureService = new PressureSensorService(); _pressureService.ValueUpdated += PressureSensorUpdated; } private void PressureSensorUpdated(object sender, float pressure) { } } }
- private field
-
Next, we will add the methods and fields for service calibration and calculate stable average,
CalculateStableAverage
method.using System.Collections.Generic; using System.Linq; namespace SquatCounter.Services { public class SquatCounterService { private const float Accuracy = 0.030F; private const int WindowSize = 10; private PressureSensorService _pressureService; private Queue<float> _pressureWindow; private float _upperThreshold; private float _lowerThreshold; private bool _valueExceededUpperThreshold; private bool _isServiceCalibrated; public SquatCounterService() { _pressureWindow = new Queue<float>(); _pressureService = new PressureSensorService(); _pressureService.ValueUpdated += PressureSensorUpdated; } private void PressureSensorUpdated(object sender, float pressure) { } private void CalibrateService() { float average = CalculateStableAverage(); _upperThreshold = average + Accuracy; _lowerThreshold = average - Accuracy; _isServiceCalibrated = true; } private float CalculateStableAverage() { float minPressure = _pressureWindow.Min(); float maxPressure = _pressureWindow.Max(); float sum = _pressureWindow.Sum(); return (sum - minPressure - maxPressure) / (WindowSize - 2); } }
-
In this step, we will add previously created method to calibrate
SquatCounter
afterPressureWindow
reach the first 10 readings.using System.Collections.Generic; using System.Linq; namespace SquatCounter.Services { public class SquatCounterService { private const float Accuracy = 0.030F; private const int WindowSize = 10; private PressureSensorService _pressureService; private Queue<float> _pressureWindow; private float _upperThreshold; private float _lowerThreshold; private bool _valueExceededUpperThreshold; private bool _isServiceCalibrated; public SquatCounterService() { _pressureWindow = new Queue<float>(); _pressureService = new PressureSensorService(); _pressureService.ValueUpdated += PressureSensorUpdated; } private void PressureSensorUpdated(object sender, float pressure) { _pressureWindow.Enqueue(pressure); if (_pressureWindow.Count < WindowSize) { return; } else if (_pressureWindow.Count == WindowSize && !_isServiceCalibrated) { CalibrateService(); } _pressureWindow.Dequeue(); } private void CalibrateService() { float average = CalculateStableAverage(); _upperThreshold = average + Accuracy; _lowerThreshold = average - Accuracy; _isServiceCalibrated = true; } private float CalculateStableAverage() { float minPressure = _pressureWindow.Min(); float maxPressure = _pressureWindow.Max(); float sum = _pressureWindow.Sum(); return (sum - minPressure - maxPressure) / (WindowSize - 2); } } }
-
Next, we will create method that analyze our pressure window to detect hit to the starting calibration. This method will be a rising event and pass counted squats every time when squat is detected.
using System; using System.Collections.Generic; using System.Linq; namespace SquatCounter.Services { public class SquatCounterService { private const float Accuracy = 0.030F; private const int WindowSize = 10; private PressureSensorService _pressureService; private Queue<float> _pressureWindow; private float _upperThreshold; private float _lowerThreshold; private bool _valueExceededUpperThreshold; private bool _isServiceCalibrated; public event EventHandler<int> SquatsUpdated; public int SquatsCount { get; private set; } public SquatCounterService() { _pressureWindow = new Queue<float>(); _pressureService = new PressureSensorService(); _pressureService.ValueUpdated += PressureSensorUpdated; } private void PressureSensorUpdated(object sender, float pressure) { _pressureWindow.Enqueue(pressure); if (_pressureWindow.Count < WindowSize) { return; } else if (_pressureWindow.Count == WindowSize && !_isServiceCalibrated) { CalibrateService(); } AnalyzeNewWindow(); _pressureWindow.Dequeue(); } private void CalibrateService() { float average = CalculateStableAverage(); _upperThreshold = average + Accuracy; _lowerThreshold = average - Accuracy; _isServiceCalibrated = true; } private float CalculateStableAverage() { float minPressure = _pressureWindow.Min(); float maxPressure = _pressureWindow.Max(); float sum = _pressureWindow.Sum(); return (sum - minPressure - maxPressure) / (WindowSize - 2); } private void AnalyzeNewWindow() { float average = CalculateStableAverage(); if (average <= _upperThreshold && average >= _lowerThreshold && _valueExceededUpperThreshold) { _valueExceededUpperThreshold = false; SquatsCount++; SquatsUpdated.Invoke(this, SquatsCount); } else if (average > _upperThreshold) { _valueExceededUpperThreshold = true; } } } }
-
Add stop, start, reset methods and implement dispose pattern to release managed resources.
using System; using System.Collections.Generic; using System.Linq; namespace SquatCounter.Services { public class SquatCounterService : IDisposable { private const float Accuracy = 0.030F; private const int WindowSize = 10; private PressureSensorService _pressureService; private Queue<float> _pressureWindow; private float _upperThreshold; private float _lowerThreshold; private bool _valueExceededUpperThreshold; private bool _isServiceCalibrated; public event EventHandler<int> SquatsUpdated; public int SquatsCount { get; private set; } public SquatCounterService() { _pressureWindow = new Queue<float>(); _pressureService = new PressureSensorService(); _pressureService.ValueUpdated += PressureSensorUpdated; } public void Start() { _pressureService.ValueUpdated += PressureSensorUpdated; } public void Stop() { _pressureService.ValueUpdated -= PressureSensorUpdated; } public void Reset() { SquatsCount = 0; SquatsUpdated.Invoke(this, SquatsCount); } public void Dispose() { _pressureService.ValueUpdated -= PressureSensorUpdated; _pressureService?.Dispose(); } private void PressureSensorUpdated(object sender, float pressure) { _pressureWindow.Enqueue(pressure); if (_pressureWindow.Count < WindowSize) { return; } else if (_pressureWindow.Count == WindowSize && !_isServiceCalibrated) { CalibrateService(); } AnalyzeNewWindow(); _pressureWindow.Dequeue(); } private void CalibrateService() { float average = CalculateStableAverage(); _upperThreshold = average + Accuracy; _lowerThreshold = average - Accuracy; _isServiceCalibrated = true; } private float CalculateStableAverage() { float minPressure = _pressureWindow.Min(); float maxPressure = _pressureWindow.Max(); float sum = _pressureWindow.Sum(); return (sum - minPressure - maxPressure) / (WindowSize - 2); } private void AnalyzeNewWindow() { float average = CalculateStableAverage(); if (average <= _upperThreshold && average >= _lowerThreshold && _valueExceededUpperThreshold) { _valueExceededUpperThreshold = false; SquatsCount++; SquatsUpdated.Invoke(this, SquatsCount); } else if (average > _upperThreshold) { _valueExceededUpperThreshold = true; } } } }
Learn more by watching the video
Step 6: Implement viewmodels - part 1
In order to show all the data to the user we need to add a view model, which acts as a connection between the model and the view. It’s responsible for the presentation logic.
-
In this step, we will create our base view model. Go to the Solution Explorer, right click on ViewModels folder, select Add > New Item from the context menu, choose C# class and name it
ViewModelBase.cs
. -
Next, add following code to
ViewModelBase
class.using System.ComponentModel; using System.Runtime.CompilerServices; namespace SquatCounter.ViewModels { public class ViewModelBase : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null) { if (Equals(storage, value)) { return false; } storage = value; OnPropertyChanged(propertyName); return true; } protected void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } }
-
Now, we can create our squat counter view model. Go to the Solution Explorer, right click on folder ViewModels, select Add > New Item from the context menu, choose C# class and name it
SquatCounterPageViewModel.cs
. Created class must inherit from theViewModelBase
class.namespace SquatCounter.ViewModels { public class SquatCounterPageViewModel : ViewModelBase { } }
-
Let's create our instance of
SquatCounterService
, subscribe to service event and create property for counting squats.using SquatCounter.Services; namespace SquatCounter.ViewModels { public class SquatCounterPageViewModel : ViewModelBase { private SquatCounterService _squatService; private int _squatsCount; public int SquatsCount { get => _squatsCount; set => SetProperty(ref _squatsCount, value); } public SquatCounterPageViewModel() { _squatService = new SquatCounterService(); _squatService.SquatsUpdated += ExecuteSquatsUpdatedCallback; } private void ExecuteSquatsUpdatedCallback(object sender, int squatsCount) { SquatsCount = squatsCount; } } }
-
Next, we need to implement timer to count, create a method for subscribing timer tick and property for elapsed time.
using SquatCounter.Services; using System.Timers; using System; namespace SquatCounter.ViewModels { public class SquatCounterPageViewModel : ViewModelBase { private SquatCounterService _squatService; private int _squatsCount; private Timer _timer; private int _seconds; private string _time; public int SquatsCount { get => _squatsCount; set => SetProperty(ref _squatsCount, value); } public string Time { get => _time; set => SetProperty(ref _time, value); } public SquatCounterPageViewModel() { _seconds = 0; _time = "00:00"; _squatService = new SquatCounterService(); _squatService.SquatsUpdated += ExecuteSquatsUpdatedCallback; _timer = new Timer(1000); _timer.Elapsed += TimerTick; _timer.Start(); } private void ExecuteSquatsUpdatedCallback(object sender, int squatsCount) { SquatsCount = squatsCount; } private void TimerTick(object sender, EventArgs e) { _seconds++; Time = TimeSpan.FromSeconds(_seconds).ToString("mm\\:ss"); } } }
-
In this step, we will implement commands for reset and change state of
SquatCounterService
.using SquatCounter.Services; using System.Timers; using System; using System.Windows.Input; using Xamarin.Forms; namespace SquatCounter.ViewModels { public class SquatCounterPageViewModel : ViewModelBase { private SquatCounterService _squatService; private int _squatsCount; private Timer _timer; private int _seconds; private string _time; private bool _isCouting; public int SquatsCount { get => _squatsCount; set => SetProperty(ref _squatsCount, value); } public string Time { get => _time; set => SetProperty(ref _time, value); } public bool IsCounting { get => _isCouting; set => SetProperty(ref _isCouting, value); } public ICommand ChangeServiceStateCommand { get; } public ICommand ResetCommand { get; } public SquatCounterPageViewModel() { _seconds = 0; _time = "00:00"; _squatService = new SquatCounterService(); _squatService.SquatsUpdated += ExecuteSquatsUpdatedCallback; _timer = new Timer(1000); _timer.Elapsed += TimerTick; _timer.Start(); ChangeServiceStateCommand = new Command(ExecuteChangeServiceStateCommand); ResetCommand = new Command(ExecuteResetCommand); } private void ExecuteSquatsUpdatedCallback(object sender, int squatsCount) { SquatsCount = squatsCount; } private void TimerTick(object sender, EventArgs e) { _seconds++; Time = TimeSpan.FromSeconds(_seconds).ToString("mm\\:ss"); } private void ExecuteChangeServiceStateCommand() { if (IsCounting) { _timer.Stop(); _squatService.Stop(); } else { _timer.Start(); _squatService.Start(); } IsCounting = !IsCounting; } private void ExecuteResetCommand() { _squatService.Reset(); Time = "00:00"; _seconds = 0; } } }
-
Now, we need to release resources after page will be popped. Without this, the app could crash.
public SquatCounterPageViewModel() { _seconds = 0; _time = "00:00"; _squatService = new SquatCounterService(); _squatService.SquatsUpdated += ExecuteSquatsUpdatedCallback; _timer = new Timer(1000); _timer.Elapsed += TimerTick; _timer.Start(); ChangeServiceStateCommand = new Command(ExecuteChangeServiceStateCommand); ResetCommand = new Command(ExecuteResetCommand); if (Application.Current.MainPage is NavigationPage navigationPage) { navigationPage.Popped += OnPagePopped; } } private void OnPagePopped(object sender, NavigationEventArgs e) { _timer.Elapsed -= TimerTick; _timer.Close(); _squatService.SquatsUpdated -= ExecuteSquatsUpdatedCallback; _squatService.Dispose(); if (Application.Current.MainPage is NavigationPage navigationPage) { navigationPage.Popped -= OnPagePopped; } }
Learn more by watching the video
Step 7: Implement viewmodels - part 2
In this step, we will implement GuideViewModel
. It provides logic to present each step of squat.
-
Go to the Solution Explorer, right click on ViewModels folder, select Add > New Item from the context menu, choose C# class and name
GuidePageViewModel.cs
. Created class must inherit from theViewModelBase
class.namespace SquatCounter.ViewModels { public class GuidePageViewModel : ViewModelBase { } }
-
Here, we will implement command to move to the next page and property image path for the current page guide.
using SquatCounter.Services; using System.Windows.Input; using Xamarin.Forms; namespace SquatCounter.ViewModels { public class GuidePageViewModel : ViewModelBase { public string ImagePath { get; set; } public ICommand GoToSquatCounterPageCommand { get; set; } public GuidePageViewModel() { GoToSquatCounterPageCommand = new Command(ExecuteGoToSquatCounterPageCommand); } public void ExecuteGoToSquatCounterPageCommand() { PageNavigationService.Instance.GoToSquatCounterPage(); } } }
Learn more by watching the video
Step 8: UI - Implement guide pages
It is time to deal with the user interface. In this part, we will create a full UI of the application including Guide pages and Counting Squat page and fill them with data from the viewmodel
using binding.
The implementation will consist in modification of the GuidePage
, which will display the next pages of squats and clock page. To achieve this we will use Tizen extension to Xamarin.Forms - Cirucular UI.
-
Right click on the Views folder, select Add > New Item from the context menu, choose content page and name it
GuideStepPage.xaml
. Now, we can paste the following code to the created page.<?xml version="1.0" encoding="utf-8" ?> <cui:CirclePage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:cui ="clr-namespace:Tizen.Wearable.CircularUI.Forms;assembly=Tizen.Wearable.CircularUI.Forms" x:Class="SquatCounter.Views.GuideStepPage"> <cui:CirclePage.Content> </cui:CirclePage.Content> </cui:CirclePage>
-
Next, we need to bind the image path to image to set our step page.
<?xml version="1.0" encoding="utf-8" ?> <cui:CirclePage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:cui ="clr-namespace:Tizen.Wearable.CircularUI.Forms;assembly=Tizen.Wearable.CircularUI.Forms" x:Class="SquatCounter.Views.GuideStepPage"> <cui:CirclePage.Content> <AbsoluteLayout> <Image AbsoluteLayout.LayoutBounds="0, 0, 360, 360" Source="{Binding ImagePath}" /> </AbsoluteLayout> </cui:CirclePage.Content> </cui:CirclePage>
-
We also need to modify file
GuideStepPage.xaml.cs
with following code.using Tizen.Wearable.CircularUI.Forms; namespace SquatCounter.Views { public partial class GuideStepPage : CirclePage { public GuideStepPage() { InitializeComponent(); } } }
-
Right click on the Views folder, select Add > New Item from the context menu, choose content page and name it
GuideClockPage.xaml
. Now, we can paste the code below to the created page.<?xml version="1.0" encoding="utf-8" ?> <cui:CirclePage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:cui ="clr-namespace:Tizen.Wearable.CircularUI.Forms;assembly=Tizen.Wearable.CircularUI.Forms" x:Class="SquatCounter.Views.GuideClockPage"> <cui:CirclePage.Content> </cui:CirclePage.Content> </cui:CirclePage>
-
The only thing that makes
GuideStepPage
different fromGuideClockPage
is the command that calls up transition as a gesture to theSquatCounterPage
. Let's bind the command and image path to the page.<?xml version="1.0" encoding="utf-8" ?> <cui:CirclePage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:cui ="clr-namespace:Tizen.Wearable.CircularUI.Forms;assembly=Tizen.Wearable.CircularUI.Forms" x:Class="SquatCounter.Views.GuideClockPage"> <cui:CirclePage.Content> <AbsoluteLayout> <Image AbsoluteLayout.LayoutBounds="0, 0, 360, 360" Source="{Binding ImagePath}"> <Image.GestureRecognizers> <TapGestureRecognizer Command="{Binding GoToSquatCounterPageCommand}"> </TapGestureRecognizer> </Image.GestureRecognizers> </Image> </AbsoluteLayout> </cui:CirclePage.Content> </cui:CirclePage>
-
We need modify
GuideClockPage.xaml.cs
in similar way as like in Step 8-4.using Tizen.Wearable.CircularUI.Forms; namespace SquatCounter.Views { public partial class GuideClockPage : CirclePage { public GuideClockPage() { InitializeComponent(); } } }
-
Now, we have implemented
GuideStepPage
to introduce the user on how to do squats and theGuideClockPage
. In addition, we can modify theGuidePage.xaml
file with the setting pages.<?xml version="1.0" encoding="utf-8" ?> <cui:IndexPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:cui ="clr-namespace:Tizen.Wearable.CircularUI.Forms;assembly=Tizen.Wearable.CircularUI.Forms" xmlns:viewModels="clr-namespace:SquatCounter.ViewModels;assembly=SquatCounter" xmlns:views="clr-namespace:SquatCounter.Views;assembly=SquatCounter" x:Class="SquatCounter.Views.GuidePage" NavigationPage.HasNavigationBar="False"> <cui:IndexPage.Children> <views:GuideStepPage> <views:GuideStepPage.BindingContext> <viewModels:GuidePageViewModel ImagePath="step_1.png" /> </views:GuideStepPage.BindingContext> </views:GuideStepPage> <views:GuideStepPage> <views:GuideStepPage.BindingContext> <viewModels:GuidePageViewModel ImagePath="step_2.png" /> </views:GuideStepPage.BindingContext> </views:GuideStepPage> <views:GuideStepPage> <views:GuideStepPage.BindingContext> <viewModels:GuidePageViewModel ImagePath="step_3.png" /> </views:GuideStepPage.BindingContext> </views:GuideStepPage> <views:GuideClockPage> <views:GuideClockPage.BindingContext> <viewModels:GuidePageViewModel ImagePath="clock.png" /> </views:GuideClockPage.BindingContext> </views:GuideClockPage> </cui:IndexPage.Children> </cui:IndexPage>
-
Last thing to do is to modify
GuidePage.xaml.cs
file.using Tizen.Wearable.CircularUI.Forms; namespace SquatCounter.Views { public partial class GuidePage : IndexPage { public GuidePage() { InitializeComponent(); } } }
-
After build and run our app we will see four pages.
-
Step pages
-
Clock page
-
Learn more by watching the video
Step 9: UI - Implement squat counter page
Let's create SquatCounterPage
, which will be responsible for displaying counted time and squats.
-
Right click on the Views folder, select Add > New Item from the context menu, choose content page and name it
SquatCounterPage.xaml
. Now, we can paste the following code to the created page.<?xml version="1.0" encoding="utf-8" ?> <cui:CirclePage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:viewModels="clr-namespace:SquatCounter.ViewModels;assembly=SquatCounter" xmlns:cui ="clr-namespace:Tizen.Wearable.CircularUI.Forms;assembly=Tizen.Wearable.CircularUI.Forms" x:Class="SquatCounter.Views.SquatCounterPage" NavigationPage.HasNavigationBar="False"> <cui:CirclePage.BindingContext> <viewModels:SquatCounterPageViewModel /> </cui:CirclePage.BindingContext> <cui:CirclePage.Content> <AbsoluteLayout> <Image AbsoluteLayout.LayoutBounds="0, 0, 360, 360" Source="squat_background.png" /> <Label AbsoluteLayout.LayoutBounds="130, 28, 100, 47" TextColor="#77A6D2" FontFamily="BreezeSans" FontSize="Large" HorizontalTextAlignment="Center" Text="Squats" /> <Label AbsoluteLayout.LayoutBounds="118, 90, 123, 72" TextColor="#FFFEFE" FontSize="28.50" FontFamily="BreezeSans" HorizontalTextAlignment="Center" Text="{Binding SquatsCount}" /> <Label AbsoluteLayout.LayoutBounds="148, 184, 64, 27" FontFamily="BreezeSans" FontSize="Small" HorizontalTextAlignment="Center" TextColor="#77A6D2" Text="Time" /> <Label AbsoluteLayout.LayoutBounds="133, 228, 94, 29" TextColor="#FFFEFE" FontFamily="BreezeSans" FontSize="Large" HorizontalTextAlignment="Center" Text="{Binding Time}" /> <AbsoluteLayout.GestureRecognizers> <TapGestureRecognizer Command="{Binding ChangeServiceStateCommand}" /> </AbsoluteLayout.GestureRecognizers> </AbsoluteLayout> </cui:CirclePage.Content> </cui:CirclePage>
-
Here, we need to add action button from Circular UI to reset timer and counted squats.
<?xml version="1.0" encoding="utf-8" ?> <cui:CirclePage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:viewModels="clr-namespace:SquatCounter.ViewModels;assembly=SquatCounter" xmlns:cui ="clr-namespace:Tizen.Wearable.CircularUI.Forms;assembly=Tizen.Wearable.CircularUI.Forms" x:Class="SquatCounter.Views.SquatCounterPage" NavigationPage.HasNavigationBar="False"> <cui:CirclePage.BindingContext> <viewModels:SquatCounterPageViewModel /> </cui:CirclePage.BindingContext> <cui:CirclePage.Content> <AbsoluteLayout> <Image AbsoluteLayout.LayoutBounds="0, 0, 360, 360" Source="squat_background.png" /> <Label AbsoluteLayout.LayoutBounds="130, 28, 100, 47" TextColor="#77A6D2" FontFamily="BreezeSans" FontSize="Large" HorizontalTextAlignment="Center" Text="Squats" /> <Label AbsoluteLayout.LayoutBounds="118, 90, 123, 72" TextColor="#FFFEFE" FontSize="28.50" FontFamily="BreezeSans" HorizontalTextAlignment="Center" Text="{Binding SquatsCount}" /> <Label AbsoluteLayout.LayoutBounds="148, 184, 64, 27" FontFamily="BreezeSans" FontSize="Small" HorizontalTextAlignment="Center" TextColor="#77A6D2" Text="Time" /> <Label AbsoluteLayout.LayoutBounds="133, 228, 94, 29" TextColor="#FFFEFE" FontFamily="BreezeSans" FontSize="Large" HorizontalTextAlignment="Center" Text="{Binding Time}" /> <AbsoluteLayout.GestureRecognizers> <TapGestureRecognizer Command="{Binding ChangeServiceStateCommand}" /> </AbsoluteLayout.GestureRecognizers> </AbsoluteLayout> </cui:CirclePage.Content> <cui:CirclePage.ActionButton> <cui:ActionButtonItem Text="RESET" BackgroundColor="#2a537d" Command="{Binding ResetCommand}"/> </cui:CirclePage.ActionButton> </cui:CirclePage>
-
Let's implement trigger to disable the action button while time is counted.
<?xml version="1.0" encoding="utf-8" ?> <cui:CirclePage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:viewModels="clr-namespace:SquatCounter.ViewModels;assembly=SquatCounter" xmlns:cui ="clr-namespace:Tizen.Wearable.CircularUI.Forms;assembly=Tizen.Wearable.CircularUI.Forms" x:Class="SquatCounter.Views.SquatCounterPage" NavigationPage.HasNavigationBar="False"> <cui:CirclePage.BindingContext> <viewModels:SquatCounterPageViewModel /> </cui:CirclePage.BindingContext> <cui:CirclePage.Content> <AbsoluteLayout> <Image AbsoluteLayout.LayoutBounds="0, 0, 360, 360" Source="squat_background.png" /> <Label AbsoluteLayout.LayoutBounds="130, 28, 100, 47" TextColor="#77A6D2" FontFamily="BreezeSans" FontSize="Large" HorizontalTextAlignment="Center" Text="Squats" /> <Label AbsoluteLayout.LayoutBounds="118, 90, 123, 72" TextColor="#FFFEFE" FontSize="28.50" FontFamily="BreezeSans" HorizontalTextAlignment="Center" Text="{Binding SquatsCount}" /> <Label AbsoluteLayout.LayoutBounds="148, 184, 64, 27" FontFamily="BreezeSans" FontSize="Small" HorizontalTextAlignment="Center" TextColor="#77A6D2" Text="Time" /> <Label AbsoluteLayout.LayoutBounds="133, 228, 94, 29" TextColor="#FFFEFE" FontFamily="BreezeSans" FontSize="Large" HorizontalTextAlignment="Center" Text="{Binding Time}" /> <AbsoluteLayout.GestureRecognizers> <TapGestureRecognizer Command="{Binding ChangeServiceStateCommand}" /> </AbsoluteLayout.GestureRecognizers> </AbsoluteLayout> </cui:CirclePage.Content> <cui:CirclePage.ActionButton> <cui:ActionButtonItem Text="RESET" BackgroundColor="#2a537d" Command="{Binding ResetCommand}"/> </cui:CirclePage.ActionButton> <cui:CirclePage.Triggers> <DataTrigger TargetType="cui:CirclePage" Binding="{Binding IsCounting}" Value="True"> <Setter Property="ActionButton"> <Setter.Value> <cui:ActionButtonItem IsVisible="False" /> </Setter.Value> </Setter> </DataTrigger> </cui:CirclePage.Triggers> </cui:CirclePage>
-
We also need to modify
SquatCounterPage.xaml.cs
.using Tizen.Wearable.CircularUI.Forms; namespace SquatCounter.Views { public partial class SquatCounterPage : CirclePage { public SquatCounterPage() { InitializeComponent(); } } }
-
At the last step, we need to implement
GoToSquatCounterPage
method, which will push our page.using SquatCounter.Views; using Xamarin.Forms; namespace SquatCounter.Services { public sealed class PageNavigationService { private static PageNavigationService _instance; public static PageNavigationService Instance { get => _instance ?? (_instance = new PageNavigationService()); } public void GoToGuidePage() { Application.Current.MainPage = new NavigationPage(new GuidePage()); } public async void GoToSquatCounterPage() { await Application.Current.MainPage.Navigation.PushAsync(new SquatCounterPage()); } } }
Learn more by watching the video
We have finished our app! Now, after you build and run app, you can go to SquatCounterPage
from GuideClockPage
.
To test your app in an emulator:
- Build your project (Ctrl + Shift + B)
- Run the your wearable emulator first (Tizen > Tizen Emulator Manager > Launch an emulator)
- Install the app (Ctrl + F5)
- When the app appears, right click the emulator and open Control Panel to test it using emulated data.
You're done!
Congratulations! You have successfully achieved the goal of this Code Lab activity. Now, you can develop your own Squat Counter app by yourself! But, if you're having trouble, you may check out the link below.