├── .gitattributes ├── .gitignore ├── Images ├── CallingApp.mp4 ├── android_callingapp.mp4 ├── android_callingapp.webp ├── android_callingapp_call.gif ├── android_callingapp_calling.gif ├── callingapp.gif ├── callingapp.png ├── callingapp2.gif ├── ios_callingapp.mp4 ├── ios_callingapp.webp ├── ios_callingapp_call.png ├── ios_callingapp_calling.png ├── ios_callingapp_home.png └── original.gif ├── LICENSE ├── README.md └── src ├── CallingApp.Core ├── CallingApp.Core.csproj ├── Models │ ├── Contact.cs │ └── ContactGroup.cs ├── RelayCommand.cs └── ViewModels │ ├── BasePageViewModel.cs │ ├── IBasePageViewModel.cs │ └── MainPageViewModel.cs ├── CallingApp.Maui ├── App.xaml ├── App.xaml.cs ├── CallingApp.Maui.csproj ├── MauiProgram.cs ├── PathGeometryExtension.cs ├── Platforms │ ├── Android │ │ ├── AndroidManifest.xml │ │ ├── MainActivity.cs │ │ ├── MainApplication.cs │ │ └── Resources │ │ │ └── values │ │ │ └── colors.xml │ ├── MacCatalyst │ │ ├── AppDelegate.cs │ │ ├── Info.plist │ │ └── Program.cs │ ├── Tizen │ │ ├── Main.cs │ │ └── tizen-manifest.xml │ ├── Windows │ │ ├── App.xaml │ │ ├── App.xaml.cs │ │ ├── Package.appxmanifest │ │ └── app.manifest │ └── iOS │ │ ├── AppDelegate.cs │ │ ├── Info.plist │ │ └── Program.cs ├── Properties │ └── launchSettings.json ├── Resources │ ├── AppIcon │ │ ├── appicon.svg │ │ └── appiconfg.svg │ ├── Fonts │ │ ├── Anton.ttf │ │ ├── Gabarito-Bold.ttf │ │ ├── Gabarito-Medium.ttf │ │ └── Gabarito-SemiBold.ttf │ ├── Images │ │ ├── ariel.jpg │ │ ├── arthur.jpg │ │ ├── blank.jpg │ │ ├── cory.jpg │ │ ├── dotnet_bot.svg │ │ ├── elsa.jpg │ │ ├── jemma.jpg │ │ ├── john.jpg │ │ ├── kevin.jpg │ │ ├── rick.jpg │ │ ├── skyler.jpg │ │ └── vianey.jpg │ ├── Raw │ │ └── AboutAssets.txt │ ├── Splash │ │ └── splash.svg │ └── Styles │ │ └── Colors.xaml └── Views │ ├── Controls │ ├── AvatarView.xaml │ ├── AvatarView.xaml.cs │ ├── CallView.xaml │ ├── CallView.xaml.cs │ ├── ContactView.xaml │ ├── ContactView.xaml.cs │ ├── HangUpView.xaml │ ├── HangUpView.xaml.cs │ └── HidableContentView.cs │ └── Pages │ ├── MainPage.xaml │ └── MainPage.xaml.cs └── CallingApp.sln /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | [Aa][Rr][Mm]/ 24 | [Aa][Rr][Mm]64/ 25 | bld/ 26 | [Bb]in/ 27 | [Oo]bj/ 28 | [Ll]og/ 29 | 30 | # Visual Studio 2015/2017 cache/options directory 31 | .vs/ 32 | # Uncomment if you have tasks that create the project's static files in wwwroot 33 | #wwwroot/ 34 | 35 | # Visual Studio 2017 auto generated files 36 | Generated\ Files/ 37 | 38 | # MSTest test Results 39 | [Tt]est[Rr]esult*/ 40 | [Bb]uild[Ll]og.* 41 | 42 | # NUNIT 43 | *.VisualState.xml 44 | TestResult.xml 45 | 46 | # Build Results of an ATL Project 47 | [Dd]ebugPS/ 48 | [Rr]eleasePS/ 49 | dlldata.c 50 | 51 | # Benchmark Results 52 | BenchmarkDotNet.Artifacts/ 53 | 54 | # .NET Core 55 | project.lock.json 56 | project.fragment.lock.json 57 | artifacts/ 58 | 59 | # StyleCop 60 | StyleCopReport.xml 61 | 62 | # Files built by Visual Studio 63 | *_i.c 64 | *_p.c 65 | *_h.h 66 | *.ilk 67 | *.meta 68 | *.obj 69 | *.iobj 70 | *.pch 71 | *.pdb 72 | *.ipdb 73 | *.pgc 74 | *.pgd 75 | *.rsp 76 | *.sbr 77 | *.tlb 78 | *.tli 79 | *.tlh 80 | *.tmp 81 | *.tmp_proj 82 | *_wpftmp.csproj 83 | *.log 84 | *.vspscc 85 | *.vssscc 86 | .builds 87 | *.pidb 88 | *.svclog 89 | *.scc 90 | 91 | # Chutzpah Test files 92 | _Chutzpah* 93 | 94 | # Visual C++ cache files 95 | ipch/ 96 | *.aps 97 | *.ncb 98 | *.opendb 99 | *.opensdf 100 | *.sdf 101 | *.cachefile 102 | *.VC.db 103 | *.VC.VC.opendb 104 | 105 | # Visual Studio profiler 106 | *.psess 107 | *.vsp 108 | *.vspx 109 | *.sap 110 | 111 | # Visual Studio Trace Files 112 | *.e2e 113 | 114 | # TFS 2012 Local Workspace 115 | $tf/ 116 | 117 | # Guidance Automation Toolkit 118 | *.gpState 119 | 120 | # ReSharper is a .NET coding add-in 121 | _ReSharper*/ 122 | *.[Rr]e[Ss]harper 123 | *.DotSettings.user 124 | 125 | # JustCode is a .NET coding add-in 126 | .JustCode 127 | 128 | # TeamCity is a build add-in 129 | _TeamCity* 130 | 131 | # DotCover is a Code Coverage Tool 132 | *.dotCover 133 | 134 | # AxoCover is a Code Coverage Tool 135 | .axoCover/* 136 | !.axoCover/settings.json 137 | 138 | # Visual Studio code coverage results 139 | *.coverage 140 | *.coveragexml 141 | 142 | # NCrunch 143 | _NCrunch_* 144 | .*crunch*.local.xml 145 | nCrunchTemp_* 146 | 147 | # MightyMoose 148 | *.mm.* 149 | AutoTest.Net/ 150 | 151 | # Web workbench (sass) 152 | .sass-cache/ 153 | 154 | # Installshield output folder 155 | [Ee]xpress/ 156 | 157 | # DocProject is a documentation generator add-in 158 | DocProject/buildhelp/ 159 | DocProject/Help/*.HxT 160 | DocProject/Help/*.HxC 161 | DocProject/Help/*.hhc 162 | DocProject/Help/*.hhk 163 | DocProject/Help/*.hhp 164 | DocProject/Help/Html2 165 | DocProject/Help/html 166 | 167 | # Click-Once directory 168 | publish/ 169 | 170 | # Publish Web Output 171 | *.[Pp]ublish.xml 172 | *.azurePubxml 173 | # Note: Comment the next line if you want to checkin your web deploy settings, 174 | # but database connection strings (with potential passwords) will be unencrypted 175 | *.pubxml 176 | *.publishproj 177 | 178 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 179 | # checkin your Azure Web App publish settings, but sensitive information contained 180 | # in these scripts will be unencrypted 181 | PublishScripts/ 182 | 183 | # NuGet Packages 184 | *.nupkg 185 | # The packages folder can be ignored because of Package Restore 186 | **/[Pp]ackages/* 187 | # except build/, which is used as an MSBuild target. 188 | !**/[Pp]ackages/build/ 189 | # Uncomment if necessary however generally it will be regenerated when needed 190 | #!**/[Pp]ackages/repositories.config 191 | # NuGet v3's project.json files produces more ignorable files 192 | *.nuget.props 193 | *.nuget.targets 194 | 195 | # Microsoft Azure Build Output 196 | csx/ 197 | *.build.csdef 198 | 199 | # Microsoft Azure Emulator 200 | ecf/ 201 | rcf/ 202 | 203 | # Windows Store app package directories and files 204 | AppPackages/ 205 | BundleArtifacts/ 206 | Package.StoreAssociation.xml 207 | _pkginfo.txt 208 | *.appx 209 | 210 | # Visual Studio cache files 211 | # files ending in .cache can be ignored 212 | *.[Cc]ache 213 | # but keep track of directories ending in .cache 214 | !?*.[Cc]ache/ 215 | 216 | # Others 217 | ClientBin/ 218 | ~$* 219 | *~ 220 | *.dbmdl 221 | *.dbproj.schemaview 222 | *.jfm 223 | *.pfx 224 | *.publishsettings 225 | orleans.codegen.cs 226 | 227 | # Including strong name files can present a security risk 228 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 229 | #*.snk 230 | 231 | # Since there are multiple workflows, uncomment next line to ignore bower_components 232 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 233 | #bower_components/ 234 | 235 | # RIA/Silverlight projects 236 | Generated_Code/ 237 | 238 | # Backup & report files from converting an old project file 239 | # to a newer Visual Studio version. Backup files are not needed, 240 | # because we have git ;-) 241 | _UpgradeReport_Files/ 242 | Backup*/ 243 | UpgradeLog*.XML 244 | UpgradeLog*.htm 245 | ServiceFabricBackup/ 246 | *.rptproj.bak 247 | 248 | # SQL Server files 249 | *.mdf 250 | *.ldf 251 | *.ndf 252 | 253 | # Business Intelligence projects 254 | *.rdl.data 255 | *.bim.layout 256 | *.bim_*.settings 257 | *.rptproj.rsuser 258 | *- Backup*.rdl 259 | 260 | # Microsoft Fakes 261 | FakesAssemblies/ 262 | 263 | # GhostDoc plugin setting file 264 | *.GhostDoc.xml 265 | 266 | # Node.js Tools for Visual Studio 267 | .ntvs_analysis.dat 268 | node_modules/ 269 | 270 | # Visual Studio 6 build log 271 | *.plg 272 | 273 | # Visual Studio 6 workspace options file 274 | *.opt 275 | 276 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 277 | *.vbw 278 | 279 | # Visual Studio LightSwitch build output 280 | **/*.HTMLClient/GeneratedArtifacts 281 | **/*.DesktopClient/GeneratedArtifacts 282 | **/*.DesktopClient/ModelManifest.xml 283 | **/*.Server/GeneratedArtifacts 284 | **/*.Server/ModelManifest.xml 285 | _Pvt_Extensions 286 | 287 | # Paket dependency manager 288 | .paket/paket.exe 289 | paket-files/ 290 | 291 | # FAKE - F# Make 292 | .fake/ 293 | 294 | # JetBrains Rider 295 | .idea/ 296 | *.sln.iml 297 | 298 | # CodeRush personal settings 299 | .cr/personal 300 | 301 | # Python Tools for Visual Studio (PTVS) 302 | __pycache__/ 303 | *.pyc 304 | 305 | # Cake - Uncomment if you are using it 306 | # tools/** 307 | # !tools/packages.config 308 | 309 | # Tabs Studio 310 | *.tss 311 | 312 | # Telerik's JustMock configuration file 313 | *.jmconfig 314 | 315 | # BizTalk build output 316 | *.btp.cs 317 | *.btm.cs 318 | *.odx.cs 319 | *.xsd.cs 320 | 321 | # OpenCover UI analysis results 322 | OpenCover/ 323 | 324 | # Azure Stream Analytics local run output 325 | ASALocalRun/ 326 | 327 | # MSBuild Binary and Structured Log 328 | *.binlog 329 | 330 | # NVidia Nsight GPU debugger configuration file 331 | *.nvuser 332 | 333 | # MFractors (Xamarin productivity tool) working folder 334 | .mfractor/ 335 | 336 | # Local History for Visual Studio 337 | .localhistory/ 338 | 339 | # BeatPulse healthcheck temp database 340 | healthchecksdb 341 | 342 | .DS_Store -------------------------------------------------------------------------------- /Images/CallingApp.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RadekVyM/Calling-App/720b7afcd6b504690715f199ee504a200967ad3d/Images/CallingApp.mp4 -------------------------------------------------------------------------------- /Images/android_callingapp.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RadekVyM/Calling-App/720b7afcd6b504690715f199ee504a200967ad3d/Images/android_callingapp.mp4 -------------------------------------------------------------------------------- /Images/android_callingapp.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RadekVyM/Calling-App/720b7afcd6b504690715f199ee504a200967ad3d/Images/android_callingapp.webp -------------------------------------------------------------------------------- /Images/android_callingapp_call.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RadekVyM/Calling-App/720b7afcd6b504690715f199ee504a200967ad3d/Images/android_callingapp_call.gif -------------------------------------------------------------------------------- /Images/android_callingapp_calling.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RadekVyM/Calling-App/720b7afcd6b504690715f199ee504a200967ad3d/Images/android_callingapp_calling.gif -------------------------------------------------------------------------------- /Images/callingapp.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RadekVyM/Calling-App/720b7afcd6b504690715f199ee504a200967ad3d/Images/callingapp.gif -------------------------------------------------------------------------------- /Images/callingapp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RadekVyM/Calling-App/720b7afcd6b504690715f199ee504a200967ad3d/Images/callingapp.png -------------------------------------------------------------------------------- /Images/callingapp2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RadekVyM/Calling-App/720b7afcd6b504690715f199ee504a200967ad3d/Images/callingapp2.gif -------------------------------------------------------------------------------- /Images/ios_callingapp.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RadekVyM/Calling-App/720b7afcd6b504690715f199ee504a200967ad3d/Images/ios_callingapp.mp4 -------------------------------------------------------------------------------- /Images/ios_callingapp.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RadekVyM/Calling-App/720b7afcd6b504690715f199ee504a200967ad3d/Images/ios_callingapp.webp -------------------------------------------------------------------------------- /Images/ios_callingapp_call.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RadekVyM/Calling-App/720b7afcd6b504690715f199ee504a200967ad3d/Images/ios_callingapp_call.png -------------------------------------------------------------------------------- /Images/ios_callingapp_calling.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RadekVyM/Calling-App/720b7afcd6b504690715f199ee504a200967ad3d/Images/ios_callingapp_calling.png -------------------------------------------------------------------------------- /Images/ios_callingapp_home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RadekVyM/Calling-App/720b7afcd6b504690715f199ee504a200967ad3d/Images/ios_callingapp_home.png -------------------------------------------------------------------------------- /Images/original.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RadekVyM/Calling-App/720b7afcd6b504690715f199ee504a200967ad3d/Images/original.gif -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Radek Vymětalík 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Calling App 2 | 3 | **.NET MAUI** prototype of a phone app inspired by the [majority wallet & calling](https://dribbble.com/shots/14796428-majority-wallet-calling) design by [Hampus Öhman](https://dribbble.com/HampusOhman). 4 | 5 | 6 | https://github.com/user-attachments/assets/0a7eda83-6347-4273-bd10-d3f7790213c4 7 | 8 | 9 | This project demonstrates how to create .NET MAUI apps with custom graphics effects and many animations. 10 | 11 | ## Original design 12 | 13 | Many thanks to [Hampus Öhman](https://dribbble.com/HampusOhman) for their beautiful original work. 14 | 15 | [![Dribbble Design](./Images/original.gif)](https://dribbble.com/shots/14796428-majority-wallet-calling) 16 | 17 | ## Installation 18 | 19 | First, make sure you have your Visual Studio and .NET 8 environment set up for .NET MAUI development. If not, follow the [setup instructions](https://learn.microsoft.com/dotnet/maui/get-started/installation). Then make sure you have your [Android](https://learn.microsoft.com/dotnet/maui/get-started/first-app?pivots=devices-android) or [iOS](https://learn.microsoft.com/dotnet/maui/get-started/first-app?pivots=devices-ios) platform set up for deployment of the application. 20 | 21 | Once everything is set up, you can clone the repo and run the application via Visual Studio or Visual Studio Code. 22 | 23 | Here are some resources to learn more about .NET MAUI: 24 | 25 | - [Official website](https://dotnet.microsoft.com/apps/maui) 26 | - [Microsoft Learn](https://learn.microsoft.com/dotnet/maui/what-is-maui) 27 | - [.NET MAUI GitHub repository](https://github.com/dotnet/maui) 28 | 29 | ## Features 30 | 31 | Only .NET MAUI APIs and my [SimpleToolkit](https://github.com/RadekVyM/SimpleToolkit) library were used to create this sample. 32 | 33 |

34 | 35 |    36 | 37 |

38 | -------------------------------------------------------------------------------- /src/CallingApp.Core/CallingApp.Core.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/CallingApp.Core/Models/Contact.cs: -------------------------------------------------------------------------------- 1 | namespace CallingApp.Core.Models; 2 | 3 | public class Contact 4 | { 5 | public required string Image { get; init; } 6 | public required string Name { get; init; } 7 | } -------------------------------------------------------------------------------- /src/CallingApp.Core/Models/ContactGroup.cs: -------------------------------------------------------------------------------- 1 | namespace CallingApp.Core.Models; 2 | 3 | public class ContactGroup 4 | { 5 | public required string Title { get; init; } 6 | public required IReadOnlyList Contacts { get; init; } 7 | } -------------------------------------------------------------------------------- /src/CallingApp.Core/RelayCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Input; 2 | 3 | namespace CallingApp.Core; 4 | 5 | public class RelayCommand : ICommand 6 | { 7 | private readonly Action? parameteredAction; 8 | private readonly Action? action; 9 | private readonly Predicate? canExecute; 10 | 11 | public event EventHandler? CanExecuteChanged; 12 | 13 | public RelayCommand(Action action, Predicate? canExecute) 14 | { 15 | this.action = action; 16 | this.canExecute = canExecute; 17 | } 18 | 19 | public RelayCommand(Action parameteredAction, Predicate? canExecute) 20 | { 21 | this.parameteredAction = parameteredAction; 22 | this.canExecute = canExecute; 23 | } 24 | 25 | public RelayCommand(Action action) : this(action, null) { } 26 | 27 | public RelayCommand(Action parameteredAction) : this(parameteredAction, null) { } 28 | 29 | public bool CanExecute(object? parameter) => 30 | canExecute is null || canExecute(parameter); 31 | 32 | public void Execute(object? parameter) 33 | { 34 | parameteredAction?.Invoke(parameter); 35 | action?.Invoke(); 36 | } 37 | 38 | public void OnCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty); 39 | } -------------------------------------------------------------------------------- /src/CallingApp.Core/ViewModels/BasePageViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace CallingApp.Core.ViewModels; 4 | 5 | public class BasePageViewModel : IBasePageViewModel, INotifyPropertyChanged 6 | { 7 | public event PropertyChangedEventHandler? PropertyChanged; 8 | 9 | protected void OnPropertyChanged(string propertyName) 10 | { 11 | try 12 | { 13 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 14 | } 15 | catch (Exception exception) 16 | { 17 | System.Diagnostics.Debug.WriteLine(exception.Message); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /src/CallingApp.Core/ViewModels/IBasePageViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace CallingApp.Core.ViewModels; 4 | 5 | public interface IBasePageViewModel : INotifyPropertyChanged 6 | { 7 | } -------------------------------------------------------------------------------- /src/CallingApp.Core/ViewModels/MainPageViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Input; 2 | using CallingApp.Core.Models; 3 | 4 | namespace CallingApp.Core.ViewModels; 5 | 6 | public class MainPageViewModel : BasePageViewModel 7 | { 8 | Contact? person; 9 | 10 | public Contact? CurrentPerson 11 | { 12 | get => person; 13 | set 14 | { 15 | person = value; 16 | OnPropertyChanged(nameof(CurrentPerson)); 17 | } 18 | } 19 | public IList CloseFriends => 20 | [ 21 | new Contact { Image = "vianey.jpg", Name = "Vianey" }, 22 | new Contact { Image = "skyler.jpg", Name = "Skyler" }, 23 | new Contact { Image = "rick.jpg", Name = "Rick" }, 24 | ]; 25 | public IList Contacts => 26 | [ 27 | new ContactGroup 28 | { 29 | Title = "A", 30 | Contacts = 31 | [ 32 | new Contact { Image = "blank.jpg", Name = "Adriel" }, 33 | new Contact { Image = "ariel.jpg", Name = "Ariel" }, 34 | new Contact { Image = "arthur.jpg", Name = "Arthur" }, 35 | ] 36 | }, 37 | new ContactGroup 38 | { 39 | Title = "C", 40 | Contacts = 41 | [ 42 | new Contact { Image = "cory.jpg", Name = "Cory" }, 43 | ] 44 | }, 45 | new ContactGroup 46 | { 47 | Title = "E", 48 | Contacts = 49 | [ 50 | new Contact { Image = "elsa.jpg", Name = "Elsa" }, 51 | ] 52 | }, 53 | new ContactGroup 54 | { 55 | Title = "J", 56 | Contacts = 57 | [ 58 | new Contact { Image = "jemma.jpg", Name = "Jemma" }, 59 | new Contact { Image = "john.jpg", Name = "John" }, 60 | ] 61 | }, 62 | new ContactGroup 63 | { 64 | Title = "K", 65 | Contacts = 66 | [ 67 | new Contact { Image = "kevin.jpg", Name = "Kevin" }, 68 | ] 69 | }, 70 | new ContactGroup 71 | { 72 | Title = "L", 73 | Contacts = 74 | [ 75 | new Contact { Image = "blank.jpg", Name = "Lola" }, 76 | ] 77 | }, 78 | new ContactGroup 79 | { 80 | Title = "M", 81 | Contacts = 82 | [ 83 | new Contact { Image = "blank.jpg", Name = "Murphy" }, 84 | ] 85 | }, 86 | new ContactGroup 87 | { 88 | Title = "R", 89 | Contacts = 90 | [ 91 | new Contact { Image = "rick.jpg", Name = "Rick" }, 92 | ] 93 | }, 94 | new ContactGroup 95 | { 96 | Title = "S", 97 | Contacts = 98 | [ 99 | new Contact { Image = "skyler.jpg", Name = "Skyler" }, 100 | ] 101 | }, 102 | new ContactGroup 103 | { 104 | Title = "V", 105 | Contacts = 106 | [ 107 | new Contact { Image = "vianey.jpg", Name = "Vianey" }, 108 | ] 109 | }, 110 | ]; 111 | 112 | public ICommand CallPersonCommand { get; private init; } 113 | 114 | public MainPageViewModel() 115 | { 116 | CallPersonCommand = new RelayCommand(parameter => CurrentPerson = parameter as Contact); 117 | } 118 | } -------------------------------------------------------------------------------- /src/CallingApp.Maui/App.xaml: -------------------------------------------------------------------------------- 1 |  2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352a144 144 0 1 0 0-288 144 144 0 1 0 0 288z 13 | M256 80c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 144L48 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l144 0 0 144c0 17.7 14.3 32 32 32s32-14.3 32-32l0-144 144 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-144 0 0-144z 14 | M64 360a56 56 0 1 0 0 112 56 56 0 1 0 0-112zm0-160a56 56 0 1 0 0 112 56 56 0 1 0 0-112zM120 96A56 56 0 1 0 8 96a56 56 0 1 0 112 0z 15 | M164.9 24.6c-7.7-18.6-28-28.5-47.4-23.2l-88 24C12.1 30.2 0 46 0 64C0 311.4 200.6 512 448 512c18 0 33.8-12.1 38.6-29.5l24-88c5.3-19.4-4.6-39.7-23.2-47.4l-96-40c-16.3-6.8-35.2-2.1-46.3 11.6L304.7 368C234.3 334.7 177.3 277.7 144 207.3L193.3 167c13.7-11.2 18.4-30 11.6-46.3l-40-96z 16 | m 176.27148,110.59375 c -2.21358,-0.0797 -4.00526,2.3285 -3.27861,4.42373 0.56269,2.14344 3.38865,3.15273 5.18367,1.85441 1.88934,-1.16026 2.03452,-4.15799 0.27096,-5.49967 -0.60884,-0.49611 -1.38998,-0.77876 -2.17602,-0.77847 z m 10.98243,0 c -2.21155,-0.0765 -4.00233,2.32918 -3.27719,4.42362 0.56036,2.14351 3.3869,3.15257 5.1807,1.85444 1.88783,-1.16055 2.03463,-4.15709 0.27259,-5.49956 -0.60889,-0.49609 -1.39012,-0.77836 -2.1761,-0.7785 z m 10.98437,0 c -2.21357,-0.0797 -4.00527,2.3285 -3.27861,4.42373 0.56269,2.14345 3.38864,3.15272 5.18366,1.85441 1.88935,-1.16026 2.03452,-4.15799 0.27096,-5.49967 -0.60883,-0.49611 -1.38998,-0.77875 -2.17601,-0.77847 z m -21.9668,10.64648 c -2.21434,-0.0796 -4.00425,2.32868 -3.27962,4.42441 0.56107,2.14597 3.38974,3.15627 5.18637,1.85792 1.88894,-1.16177 2.03247,-4.16157 0.26927,-5.50386 -0.60884,-0.49611 -1.38999,-0.77875 -2.17602,-0.77847 z m 10.98243,0 c -2.21204,-0.0765 -4.00206,2.3296 -3.27758,4.4243 0.5603,2.14463 3.38791,3.15774 5.18278,1.85631 1.88744,-1.16178 2.03241,-4.15929 0.2709,-5.50211 -0.6089,-0.49608 -1.39012,-0.77836 -2.1761,-0.7785 z m 10.98437,0 c -2.21434,-0.0796 -4.00426,2.32868 -3.27962,4.42441 0.56107,2.14597 3.38974,3.15627 5.18637,1.85792 1.88895,-1.16177 2.03246,-4.16157 0.26926,-5.50386 -0.60883,-0.49611 -1.38998,-0.77875 -2.17601,-0.77847 z m -10.98437,10.61524 c -2.21274,-0.0769 -4.00212,2.33087 -3.27739,4.4256 0.56135,2.14369 3.38727,3.15516 5.18257,1.8565 1.88758,-1.16134 2.03238,-4.15917 0.27092,-5.50183 -0.60874,-0.49654 -1.38981,-0.78014 -2.1761,-0.78027 z 17 | m 168.47647,142.02751 c -0.45058,-2e-5 -0.90189,0.17383 -1.24716,0.51907 l -0.69436,0.69436 c -0.6905,0.6905 -0.6905,1.80042 0,2.49095 l 35.03155,35.03152 c 0.69048,0.69048 1.80378,0.69048 2.49431,0 l 0.69097,-0.69439 c 0.69051,-0.6905 0.69048,-1.80041 0,-2.49092 l -16.27029,-16.27029 v -12.01312 l -6.00655,6.00319 -12.75132,-12.7513 c -0.34524,-0.34527 -0.79657,-0.51907 -1.24715,-0.51907 z m 0.82582,16.52646 v 11.98615 0.27301 h 9.31657 l 9.86263,9.85927 v -10.68171 l -11.43335,-11.43672 z 18 | m 184.97734,185.69771 c -3.8093,-0.0214 -7.29796,3.09238 -7.7131,6.87826 -0.56728,3.75702 1.99103,7.65132 5.6657,8.62017 3.61649,1.11098 7.8292,-0.81942 9.34514,-4.28704 1.6758,-3.44211 0.35678,-7.95902 -2.90712,-9.95945 -1.29888,-0.83756 -2.84589,-1.27505 -4.39062,-1.25194 z m 0.0594,17.82888 c -6.63489,-0.0514 -13.04954,4.11411 -15.70143,10.19621 -0.9825,2.18047 -1.50421,4.56676 -1.52129,6.9583 11.52505,0 23.0501,0 34.57516,0 0.0251,-6.63504 -4.16596,-13.03308 -10.25855,-15.66077 -2.22398,-0.99162 -4.65891,-1.50405 -7.09389,-1.49374 z 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 31 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/CallingApp.Maui/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using CallingApp.Maui.Views.Pages; 2 | 3 | namespace CallingApp.Maui 4 | { 5 | public partial class App : Application 6 | { 7 | public App() 8 | { 9 | InitializeComponent(); 10 | 11 | MainPage = new MainPage(); 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /src/CallingApp.Maui/CallingApp.Maui.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0-android;net8.0-ios;net8.0-maccatalyst 5 | $(TargetFrameworks);net8.0-windows10.0.19041.0 6 | 7 | 8 | Exe 9 | CallingApp.Maui 10 | true 11 | true 12 | enable 13 | 14 | 15 | CallingApp.Maui 16 | 17 | 18 | com.companyname.callingapp.maui 19 | 960CE054-B66C-42E1-AF9A-A29404A6B3E7 20 | 21 | 22 | 1.0 23 | 1 24 | 25 | 11.0 26 | 13.1 27 | 21.0 28 | 10.0.17763.0 29 | 10.0.17763.0 30 | 6.5 31 | 32 | 33 | 34 | false 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /src/CallingApp.Maui/MauiProgram.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using SimpleToolkit.Core; 3 | 4 | namespace CallingApp.Maui; 5 | 6 | public static class MauiProgram 7 | { 8 | public static MauiApp CreateMauiApp() 9 | { 10 | var builder = MauiApp.CreateBuilder(); 11 | builder 12 | .UseMauiApp() 13 | .ConfigureFonts(fonts => 14 | { 15 | fonts.AddFont("Gabarito-Medium.ttf", "RegularFont"); 16 | fonts.AddFont("Gabarito-SemiBold.ttf", "SemiBoldFont"); 17 | fonts.AddFont("Gabarito-Bold.ttf", "BoldFont"); 18 | fonts.AddFont("Anton.ttf", "TimeFont"); 19 | }); 20 | 21 | builder.UseSimpleToolkit(); 22 | 23 | #if DEBUG 24 | builder.Logging.AddDebug(); 25 | #endif 26 | 27 | #if ANDROID || IOS 28 | builder.DisplayContentBehindBars(); 29 | #endif 30 | 31 | #if ANDROID 32 | builder.SetDefaultNavigationBarAppearance(Colors.Transparent); 33 | builder.SetDefaultStatusBarAppearance(Colors.Transparent, false); 34 | #endif 35 | 36 | return builder.Build(); 37 | } 38 | } -------------------------------------------------------------------------------- /src/CallingApp.Maui/PathGeometryExtension.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Maui.Controls.Shapes; 2 | 3 | namespace CallingApp.Maui; 4 | 5 | public class PathGeometryExtension : IMarkupExtension 6 | { 7 | readonly PathGeometryConverter pathGeometryConverter = new(); 8 | 9 | public string Path { get; set; } 10 | 11 | public Geometry ProvideValue(IServiceProvider serviceProvider) => 12 | pathGeometryConverter.ConvertFromInvariantString(Path) as Geometry; 13 | 14 | object IMarkupExtension.ProvideValue(IServiceProvider serviceProvider) => 15 | (this as IMarkupExtension).ProvideValue(serviceProvider); 16 | } -------------------------------------------------------------------------------- /src/CallingApp.Maui/Platforms/Android/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/CallingApp.Maui/Platforms/Android/MainActivity.cs: -------------------------------------------------------------------------------- 1 | using Android.App; 2 | using Android.Content.PM; 3 | using Android.OS; 4 | 5 | namespace CallingApp.Maui 6 | { 7 | [Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)] 8 | public class MainActivity : MauiAppCompatActivity 9 | { 10 | } 11 | } -------------------------------------------------------------------------------- /src/CallingApp.Maui/Platforms/Android/MainApplication.cs: -------------------------------------------------------------------------------- 1 | using Android.App; 2 | using Android.Runtime; 3 | 4 | namespace CallingApp.Maui 5 | { 6 | [Application] 7 | public class MainApplication : MauiApplication 8 | { 9 | public MainApplication(IntPtr handle, JniHandleOwnership ownership) 10 | : base(handle, ownership) 11 | { 12 | } 13 | 14 | protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); 15 | } 16 | } -------------------------------------------------------------------------------- /src/CallingApp.Maui/Platforms/Android/Resources/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #2B2D42 4 | #5D618A 5 | #2B2D42 6 | -------------------------------------------------------------------------------- /src/CallingApp.Maui/Platforms/MacCatalyst/AppDelegate.cs: -------------------------------------------------------------------------------- 1 | using Foundation; 2 | 3 | namespace CallingApp.Maui 4 | { 5 | [Register("AppDelegate")] 6 | public class AppDelegate : MauiUIApplicationDelegate 7 | { 8 | protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); 9 | } 10 | } -------------------------------------------------------------------------------- /src/CallingApp.Maui/Platforms/MacCatalyst/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIDeviceFamily 6 | 7 | 1 8 | 2 9 | 10 | UIRequiredDeviceCapabilities 11 | 12 | arm64 13 | 14 | UISupportedInterfaceOrientations 15 | 16 | UIInterfaceOrientationPortrait 17 | UIInterfaceOrientationLandscapeLeft 18 | UIInterfaceOrientationLandscapeRight 19 | 20 | UISupportedInterfaceOrientations~ipad 21 | 22 | UIInterfaceOrientationPortrait 23 | UIInterfaceOrientationPortraitUpsideDown 24 | UIInterfaceOrientationLandscapeLeft 25 | UIInterfaceOrientationLandscapeRight 26 | 27 | XSAppIconAssets 28 | Assets.xcassets/appicon.appiconset 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/CallingApp.Maui/Platforms/MacCatalyst/Program.cs: -------------------------------------------------------------------------------- 1 | using ObjCRuntime; 2 | using UIKit; 3 | 4 | namespace CallingApp.Maui 5 | { 6 | public class Program 7 | { 8 | // This is the main entry point of the application. 9 | static void Main(string[] args) 10 | { 11 | // if you want to use a different Application Delegate class from "AppDelegate" 12 | // you can specify it here. 13 | UIApplication.Main(args, null, typeof(AppDelegate)); 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/CallingApp.Maui/Platforms/Tizen/Main.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Maui; 2 | using Microsoft.Maui.Hosting; 3 | using System; 4 | 5 | namespace CallingApp.Maui 6 | { 7 | internal class Program : MauiApplication 8 | { 9 | protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); 10 | 11 | static void Main(string[] args) 12 | { 13 | var app = new Program(); 14 | app.Run(args); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /src/CallingApp.Maui/Platforms/Tizen/tizen-manifest.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | appicon.xhigh.png 7 | 8 | 9 | 10 | 11 | http://tizen.org/privilege/internet 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/CallingApp.Maui/Platforms/Windows/App.xaml: -------------------------------------------------------------------------------- 1 |  7 | 8 | 9 | -------------------------------------------------------------------------------- /src/CallingApp.Maui/Platforms/Windows/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.UI.Xaml; 2 | 3 | // To learn more about WinUI, the WinUI project structure, 4 | // and more about our project templates, see: http://aka.ms/winui-project-info. 5 | 6 | namespace CallingApp.Maui.WinUI 7 | { 8 | /// 9 | /// Provides application-specific behavior to supplement the default Application class. 10 | /// 11 | public partial class App : MauiWinUIApplication 12 | { 13 | /// 14 | /// Initializes the singleton application object. This is the first line of authored code 15 | /// executed, and as such is the logical equivalent of main() or WinMain(). 16 | /// 17 | public App() 18 | { 19 | this.InitializeComponent(); 20 | } 21 | 22 | protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); 23 | } 24 | } -------------------------------------------------------------------------------- /src/CallingApp.Maui/Platforms/Windows/Package.appxmanifest: -------------------------------------------------------------------------------- 1 |  2 | 7 | 8 | 9 | 10 | 11 | $placeholder$ 12 | User Name 13 | $placeholder$.png 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/CallingApp.Maui/Platforms/Windows/app.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | true/PM 12 | PerMonitorV2, PerMonitor 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/CallingApp.Maui/Platforms/iOS/AppDelegate.cs: -------------------------------------------------------------------------------- 1 | using Foundation; 2 | 3 | namespace CallingApp.Maui 4 | { 5 | [Register("AppDelegate")] 6 | public class AppDelegate : MauiUIApplicationDelegate 7 | { 8 | protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); 9 | } 10 | } -------------------------------------------------------------------------------- /src/CallingApp.Maui/Platforms/iOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | LSRequiresIPhoneOS 6 | 7 | UIDeviceFamily 8 | 9 | 1 10 | 2 11 | 12 | UIRequiredDeviceCapabilities 13 | 14 | arm64 15 | 16 | UISupportedInterfaceOrientations 17 | 18 | UIInterfaceOrientationPortrait 19 | UIInterfaceOrientationLandscapeLeft 20 | UIInterfaceOrientationLandscapeRight 21 | 22 | UISupportedInterfaceOrientations~ipad 23 | 24 | UIInterfaceOrientationPortrait 25 | UIInterfaceOrientationPortraitUpsideDown 26 | UIInterfaceOrientationLandscapeLeft 27 | UIInterfaceOrientationLandscapeRight 28 | 29 | XSAppIconAssets 30 | Assets.xcassets/appicon.appiconset 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/CallingApp.Maui/Platforms/iOS/Program.cs: -------------------------------------------------------------------------------- 1 | using ObjCRuntime; 2 | using UIKit; 3 | 4 | namespace CallingApp.Maui 5 | { 6 | public class Program 7 | { 8 | // This is the main entry point of the application. 9 | static void Main(string[] args) 10 | { 11 | // if you want to use a different Application Delegate class from "AppDelegate" 12 | // you can specify it here. 13 | UIApplication.Main(args, null, typeof(AppDelegate)); 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/CallingApp.Maui/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Windows Machine": { 4 | "commandName": "MsixPackage", 5 | "nativeDebugging": false 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /src/CallingApp.Maui/Resources/AppIcon/appicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/CallingApp.Maui/Resources/AppIcon/appiconfg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/CallingApp.Maui/Resources/Fonts/Anton.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RadekVyM/Calling-App/720b7afcd6b504690715f199ee504a200967ad3d/src/CallingApp.Maui/Resources/Fonts/Anton.ttf -------------------------------------------------------------------------------- /src/CallingApp.Maui/Resources/Fonts/Gabarito-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RadekVyM/Calling-App/720b7afcd6b504690715f199ee504a200967ad3d/src/CallingApp.Maui/Resources/Fonts/Gabarito-Bold.ttf -------------------------------------------------------------------------------- /src/CallingApp.Maui/Resources/Fonts/Gabarito-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RadekVyM/Calling-App/720b7afcd6b504690715f199ee504a200967ad3d/src/CallingApp.Maui/Resources/Fonts/Gabarito-Medium.ttf -------------------------------------------------------------------------------- /src/CallingApp.Maui/Resources/Fonts/Gabarito-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RadekVyM/Calling-App/720b7afcd6b504690715f199ee504a200967ad3d/src/CallingApp.Maui/Resources/Fonts/Gabarito-SemiBold.ttf -------------------------------------------------------------------------------- /src/CallingApp.Maui/Resources/Images/ariel.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RadekVyM/Calling-App/720b7afcd6b504690715f199ee504a200967ad3d/src/CallingApp.Maui/Resources/Images/ariel.jpg -------------------------------------------------------------------------------- /src/CallingApp.Maui/Resources/Images/arthur.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RadekVyM/Calling-App/720b7afcd6b504690715f199ee504a200967ad3d/src/CallingApp.Maui/Resources/Images/arthur.jpg -------------------------------------------------------------------------------- /src/CallingApp.Maui/Resources/Images/blank.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RadekVyM/Calling-App/720b7afcd6b504690715f199ee504a200967ad3d/src/CallingApp.Maui/Resources/Images/blank.jpg -------------------------------------------------------------------------------- /src/CallingApp.Maui/Resources/Images/cory.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RadekVyM/Calling-App/720b7afcd6b504690715f199ee504a200967ad3d/src/CallingApp.Maui/Resources/Images/cory.jpg -------------------------------------------------------------------------------- /src/CallingApp.Maui/Resources/Images/dotnet_bot.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /src/CallingApp.Maui/Resources/Images/elsa.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RadekVyM/Calling-App/720b7afcd6b504690715f199ee504a200967ad3d/src/CallingApp.Maui/Resources/Images/elsa.jpg -------------------------------------------------------------------------------- /src/CallingApp.Maui/Resources/Images/jemma.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RadekVyM/Calling-App/720b7afcd6b504690715f199ee504a200967ad3d/src/CallingApp.Maui/Resources/Images/jemma.jpg -------------------------------------------------------------------------------- /src/CallingApp.Maui/Resources/Images/john.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RadekVyM/Calling-App/720b7afcd6b504690715f199ee504a200967ad3d/src/CallingApp.Maui/Resources/Images/john.jpg -------------------------------------------------------------------------------- /src/CallingApp.Maui/Resources/Images/kevin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RadekVyM/Calling-App/720b7afcd6b504690715f199ee504a200967ad3d/src/CallingApp.Maui/Resources/Images/kevin.jpg -------------------------------------------------------------------------------- /src/CallingApp.Maui/Resources/Images/rick.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RadekVyM/Calling-App/720b7afcd6b504690715f199ee504a200967ad3d/src/CallingApp.Maui/Resources/Images/rick.jpg -------------------------------------------------------------------------------- /src/CallingApp.Maui/Resources/Images/skyler.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RadekVyM/Calling-App/720b7afcd6b504690715f199ee504a200967ad3d/src/CallingApp.Maui/Resources/Images/skyler.jpg -------------------------------------------------------------------------------- /src/CallingApp.Maui/Resources/Images/vianey.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RadekVyM/Calling-App/720b7afcd6b504690715f199ee504a200967ad3d/src/CallingApp.Maui/Resources/Images/vianey.jpg -------------------------------------------------------------------------------- /src/CallingApp.Maui/Resources/Raw/AboutAssets.txt: -------------------------------------------------------------------------------- 1 | Any raw assets you want to be deployed with your application can be placed in 2 | this directory (and child directories). Deployment of the asset to your application 3 | is automatically handled by the following `MauiAsset` Build Action within your `.csproj`. 4 | 5 | 6 | 7 | These files will be deployed with you package and will be accessible using Essentials: 8 | 9 | async Task LoadMauiAsset() 10 | { 11 | using var stream = await FileSystem.OpenAppPackageFileAsync("AboutAssets.txt"); 12 | using var reader = new StreamReader(stream); 13 | 14 | var contents = reader.ReadToEnd(); 15 | } 16 | -------------------------------------------------------------------------------- /src/CallingApp.Maui/Resources/Splash/splash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/CallingApp.Maui/Resources/Styles/Colors.xaml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 6 | 7 | #2B2D42 8 | White 9 | #5D618A 10 | White 11 | #EF233C 12 | #FFFFFF 13 | #2B2D42 14 | #EDF2F4 15 | #2B2D42 16 | White 17 | Black 18 | #E1E1E1 19 | #C8C8C8 20 | #ACACAC 21 | #919191 22 | #6E6E6E 23 | #404040 24 | #212121 25 | #141414 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/CallingApp.Maui/Views/Controls/AvatarView.xaml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 22 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 43 | -------------------------------------------------------------------------------- /src/CallingApp.Maui/Views/Controls/AvatarView.xaml.cs: -------------------------------------------------------------------------------- 1 | using CallingApp.Core.Models; 2 | using Microsoft.Maui.Controls.Shapes; 3 | 4 | namespace CallingApp.Maui.Views.Controls; 5 | 6 | public partial class AvatarView : ContentView 7 | { 8 | bool initImageBorderSizeChanged = true; 9 | bool isPhonePickedUp = false; 10 | 11 | Point imageCenterPosition => new((Width - imageBorder.WidthRequest) / 2, (Height - imageBorder.HeightRequest) / 2); 12 | Point imageTopPosition = new(0, 0); 13 | Point topLabelPosition = new(0, 0); 14 | double topImageSize => 20; 15 | double topImageScale => topImageSize / imageBorder.Width; 16 | 17 | readonly TopLabelDrawable topLabelDrawable; 18 | 19 | 20 | public AvatarView() 21 | { 22 | InitializeComponent(); 23 | 24 | App.Current.Resources.TryGetValue("Primary", out object overlayColor); 25 | topLabelDrawable = new TopLabelDrawable(17, overlayColor as Color); 26 | 27 | topLabelDrawable.TextSizeUpdated += TopLabelDrawableTextSizeUpdated; 28 | 29 | topLabelGraphicsView.Drawable = topLabelDrawable; 30 | topLabelGraphicsView.Invalidate(); 31 | } 32 | 33 | 34 | private void TopLabelDrawableTextSizeUpdated() 35 | { 36 | if (BindingContext is Core.Models.Contact person) 37 | { 38 | topLabelDrawable.Text = person.Name; 39 | topLabelGraphicsView.Invalidate(); 40 | 41 | UpdateSizes(); 42 | } 43 | } 44 | 45 | protected override void OnBindingContextChanged() 46 | { 47 | base.OnBindingContextChanged(); 48 | 49 | if (BindingContext is Core.Models.Contact person) 50 | { 51 | topLabelDrawable.Text = person.Name; 52 | topLabelGraphicsView.Invalidate(); 53 | 54 | UpdateSizes(); 55 | } 56 | } 57 | 58 | private void ImageBorderSizeChanged(object sender, EventArgs e) 59 | { 60 | if (initImageBorderSizeChanged && imageBorder.Height != 0) 61 | { 62 | IsVisible = false; 63 | initImageBorderSizeChanged = false; 64 | } 65 | 66 | UpdateSizes(); 67 | } 68 | 69 | private void ContentViewSizeChanged(object sender, EventArgs e) 70 | { 71 | UpdateSizes(); 72 | 73 | if (isPhonePickedUp) 74 | imageBorder.TranslationY = imageTopPosition.Y - imageBorder.Y; 75 | } 76 | 77 | private void UpdateSizes() 78 | { 79 | double spacing = 8; 80 | double verticalCenter = 15; 81 | double totalWidth = topImageSize + topLabelDrawable.TextSize.Width + spacing; 82 | 83 | imageTopPosition = new Point(((Width - totalWidth) / 2) - ((imageBorder.Width - topImageSize) / 2), verticalCenter - (topImageSize / 2) - ((imageBorder.Height - topImageSize) / 2)); 84 | topLabelPosition = new Point(((Width - totalWidth) / 2) + topImageSize + spacing, verticalCenter - (topLabelDrawable.TextSize.Height / 2)); 85 | 86 | AbsoluteLayout.SetLayoutBounds(imageBorder, new Rect(imageCenterPosition, imageBorder.Bounds.Size)); // This works on Windows but does not work on Android 87 | imageBorder.Layout(new Rect(imageCenterPosition, imageBorder.Bounds.Size)); // This works on Android but does not work on Windows 88 | 89 | topLabelDrawable.TextPosition = topLabelPosition; 90 | if (!isPhonePickedUp) 91 | topLabelDrawable.TextTranslationX = -topLabelDrawable.TextSize.Width; 92 | topLabelGraphicsView.Invalidate(); 93 | } 94 | 95 | public async Task OnCalled() 96 | { 97 | IsVisible = true; 98 | 99 | await Task.Delay(100); 100 | 101 | var animation = new Animation 102 | { 103 | { 0, 0.6, new Animation(v => imageBorder.Opacity = v, 0, 1) }, 104 | { 0, 1, new Animation(v => imageBorder.Scale = v, 1.5, 1) }, 105 | { 0, 1, new Animation(v => centerLabel.Opacity = v, 0, 1) } 106 | }; 107 | 108 | animation.Commit(this, "ShowAnimation", length: 700); 109 | 110 | await Task.Delay(700); 111 | } 112 | 113 | public async Task OnPhonePickedUp() 114 | { 115 | topLabelDrawable.TextOpacity = 1; 116 | topLabelDrawable.TextTranslationX = -topLabelDrawable.TextSize.Width; 117 | topLabelGraphicsView.Invalidate(); 118 | 119 | var animation = new Animation 120 | { 121 | { 122 | 0.5, 123 | 1, 124 | new Animation(v => 125 | { 126 | topLabelDrawable.TextTranslationX = (float)v * topLabelDrawable.TextTranslationX; 127 | topLabelGraphicsView.Invalidate(); 128 | }, 1, 0) 129 | }, 130 | { 0, 0.8, new Animation(v => centerLabel.Opacity = v, 1, 0) }, 131 | { 0, 1, new Animation(v => imageBorder.Scale = v, imageBorder.Scale, topImageScale) }, 132 | { 0, 1, new Animation(v => imageBorder.TranslationX = v * (imageTopPosition.X - imageBorder.X), 0, 1) }, 133 | { 0, 1, new Animation(v => imageBorder.TranslationY = v * (imageTopPosition.Y - imageBorder.Y), 0, 1) } 134 | }; 135 | 136 | animation.Commit(this, "TranslationAnimation", length: 700); 137 | 138 | await Task.Delay(700); 139 | isPhonePickedUp = true; 140 | } 141 | 142 | public async Task OnPhoneHangedUp(bool isOnThePhone) 143 | { 144 | isPhonePickedUp = false; 145 | 146 | var animation = new Animation 147 | { 148 | { 0.4, 1, new Animation(v => imageBorder.Opacity = v, 1, 0) } 149 | }; 150 | 151 | if (isOnThePhone) 152 | { 153 | animation.Add(0.5, 1, new Animation(v => { 154 | topLabelDrawable.TextTranslationX = (float)v; 155 | topLabelGraphicsView.Invalidate(); 156 | }, 0, -topLabelDrawable.TextSize.Width)); 157 | } 158 | else 159 | { 160 | animation.Add(0, 0.8, new Animation(v => centerLabel.Opacity = v, 1, 0)); 161 | animation.Add(0, 1, new Animation(v => imageBorder.Scale = v * imageBorder.Scale, 1, 0, easing: Easing.SpringIn)); 162 | } 163 | 164 | animation.Commit(this, "HangedUpAnimation", length: 700); 165 | 166 | await Task.Delay(700); 167 | 168 | imageBorder.TranslationX = 0; 169 | imageBorder.TranslationY = 0; 170 | 171 | topLabelDrawable.TextOpacity = 0; 172 | IsVisible = true; 173 | 174 | topLabelGraphicsView.Invalidate(); 175 | } 176 | 177 | class TopLabelDrawable : IDrawable 178 | { 179 | readonly float fontSize; 180 | readonly Color color; 181 | 182 | public string Text { get; set; } 183 | public SizeF TextSize { get; private set; } 184 | public float TextTranslationX { get; set; } = 0; 185 | public float TextOpacity { get; set; } = 0; 186 | public PointF TextPosition { get; set; } 187 | 188 | public event Action TextSizeUpdated; 189 | 190 | public TopLabelDrawable(float fontSize, Color color) 191 | { 192 | this.fontSize = fontSize; 193 | this.color = color; 194 | } 195 | 196 | public void Draw(ICanvas canvas, RectF dirtyRect) 197 | { 198 | canvas.FontSize = fontSize; 199 | canvas.FontColor = new Color(color.Red, color.Green, color.Blue, TextOpacity); 200 | 201 | var newTextSize = canvas.GetStringSize(Text, Microsoft.Maui.Graphics.Font.Default, fontSize); 202 | if (TextSize != newTextSize) 203 | { 204 | TextSize = newTextSize; 205 | TextSizeUpdated?.Invoke(); 206 | } 207 | var textRect = new RectF(TextTranslationX + TextPosition.X, TextPosition.Y, TextSize.Width * 2, TextSize.Height); 208 | 209 | canvas.ClipRectangle(new RectF(TextPosition.X, 0, textRect.Width, textRect.Height * 2)); 210 | canvas.DrawString(Text, textRect, HorizontalAlignment.Left, VerticalAlignment.Center); 211 | 212 | canvas.ResetState(); // See: https://github.com/dotnet/Microsoft.Maui.Graphics/issues/405 213 | } 214 | } 215 | } -------------------------------------------------------------------------------- /src/CallingApp.Maui/Views/Controls/CallView.xaml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 17 | 18 | 25 | 26 | 35 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 62 | 63 | 70 | 71 | 72 | 76 | 77 | 81 | 82 | 92 | 93 | -------------------------------------------------------------------------------- /src/CallingApp.Maui/Views/Controls/CallView.xaml.cs: -------------------------------------------------------------------------------- 1 | using CallingApp.Maui.Views.Pages; 2 | 3 | namespace CallingApp.Maui.Views.Controls; 4 | 5 | public partial class CallView : ContentView 6 | { 7 | #region Private members 8 | 9 | const string MovingQuadrangleAndTimeAnimationName = "MovingQuadrangleAndTimeAnimation"; 10 | const string TriangleToQuadrangleAnimationName = "TriangleToQuadrangleAnimation"; 11 | const string TriangleAnimationName = "TriangleAnimation"; 12 | const string QuadrangleCollapseAnimationName = "QuadrangleCollapseAnimation"; 13 | const string IconsGridAnimationName = "IconsGridAnimation"; 14 | const string MovingWaveAnimationName = "MovingWaveAnimation"; 15 | const string WaveAnimationName = "WaveAnimation"; 16 | const string StopWaveAnimationName = "StopWaveAnimation"; 17 | 18 | bool initButtonsStackSizeChanged = true; 19 | bool initTimeGridSizeChanged = true; 20 | 21 | double initialButtonsStackTranslation => -buttonsStack.Height * 0.75; 22 | int numberOfPoints => 22; 23 | int numberOfHiddenPoints => 4; 24 | Point upVector; 25 | Point downVector; 26 | Point firstPointOffsetVector; 27 | Point firstPoint; 28 | Point pointToPointVector; 29 | float visibleLineLength => (float)Math.Sqrt(Math.Pow(overlayGraphicsView.Height, 2) + Math.Pow(overlayGraphicsView.Width, 2)); 30 | float pointsDistance => visibleLineLength / (numberOfPoints - numberOfHiddenPoints - 1); 31 | bool isWaveAnimating; 32 | bool isOnThePhone = false; 33 | bool isHangedUp = true; 34 | double waveVectorScale = 0; 35 | double maxWaveVectorScale => 0.4f; 36 | Point overlayTopLeftPoint = new(0, 0); 37 | Point minOverlayTopLeftPoint => new(0, Math.Min(0.7d * overlayGraphicsView.Height, overlayGraphicsView.Height - 200)); 38 | Point maxOverlayTopLeftPoint => new(0, minOverlayTopLeftPoint.Y + 20); 39 | Point overlayTopRightPoint = new(0, 0); 40 | Point minOverlayTopRightPoint => new(overlayGraphicsView.Width, (7d / 24d) * overlayGraphicsView.Height); 41 | Point maxOverlayTopRightPoint => new(overlayGraphicsView.Width, (2d / 5d) * overlayGraphicsView.Height); 42 | DateTime startTime = new(); 43 | 44 | OverlayDrawable overlayDrawable; 45 | 46 | #endregion 47 | 48 | public static readonly BindableProperty SafeAreaProperty = 49 | BindableProperty.Create(nameof(SafeArea), typeof(Thickness), typeof(CallView), propertyChanged: static (bindable, oldValue, newValue) => 50 | { 51 | var view = bindable as CallView; 52 | 53 | if (newValue is Thickness safeArea) 54 | { 55 | view.avatarView.Margin = safeArea; 56 | view.hangUpView.Margin = new Thickness(safeArea.Left, 0, safeArea.Right, safeArea.Bottom + 60); 57 | } 58 | }); 59 | 60 | public Thickness SafeArea 61 | { 62 | get => (Thickness)GetValue(SafeAreaProperty); 63 | set => SetValue(SafeAreaProperty, value); 64 | } 65 | 66 | #region Constructor 67 | 68 | public CallView() 69 | { 70 | InitializeComponent(); 71 | } 72 | 73 | #endregion 74 | 75 | #region Private methods 76 | 77 | private void OverlayGraphicsViewSizeChanged(object sender, EventArgs e) 78 | { 79 | float scale = pointsDistance / visibleLineLength; 80 | 81 | pointToPointVector = new Point(overlayGraphicsView.Width * scale, -overlayGraphicsView.Height * scale); 82 | firstPoint = new Point(-pointToPointVector.X * numberOfHiddenPoints, (-pointToPointVector.Y * numberOfHiddenPoints) + overlayGraphicsView.Height); 83 | firstPointOffsetVector = new Point(0, 0); 84 | upVector = new Point(0, 0); 85 | downVector = new Point(0, 0); 86 | 87 | App.Current.Resources.TryGetValue("Primary", out object overlayColor); 88 | 89 | overlayDrawable = new OverlayDrawable(overlayColor as Color) { OverlayPath = new PathF() }; 90 | 91 | overlayGraphicsView.Drawable = overlayDrawable; 92 | overlayGraphicsView.Invalidate(); 93 | } 94 | 95 | private void ButtonsStackSizeChanged(object sender, EventArgs e) 96 | { 97 | if (initButtonsStackSizeChanged) 98 | { 99 | buttonsStack.IsVisible = false; 100 | initButtonsStackSizeChanged = false; 101 | } 102 | } 103 | 104 | private void TimeGridSizeChanged(object sender, EventArgs e) 105 | { 106 | if (initTimeGridSizeChanged) 107 | { 108 | timeGrid.IsVisible = false; 109 | initTimeGridSizeChanged = false; 110 | } 111 | } 112 | 113 | private async void HangUpViewInteracted(object sender, EventArgs e) 114 | { 115 | isHangedUp = true; 116 | 117 | // Abort all running animations 118 | overlayGraphicsView.AbortAnimation(MovingQuadrangleAndTimeAnimationName); 119 | overlayGraphicsView.AbortAnimation(TriangleToQuadrangleAnimationName); 120 | await StopWaveAnimation(); 121 | 122 | await Task.Delay(200); 123 | 124 | this.InputTransparent = true; 125 | _ = hangUpView.Hide(); 126 | _ = avatarView.OnPhoneHangedUp(isOnThePhone); 127 | GetMainPage(this)?.ShowAllViews(); 128 | 129 | if (isOnThePhone) 130 | { 131 | await Task.WhenAll( 132 | StartCollapsingQuadrangleAnimation(800), 133 | StartDetailElementsAnimation(false)); 134 | await Task.Delay(100); 135 | } 136 | else 137 | { 138 | await StartTriangleAnimation(false); 139 | } 140 | 141 | overlayGraphicsView.IsVisible = false; 142 | buttonsStack.IsVisible = false; 143 | timeGrid.IsVisible = false; 144 | isOnThePhone = false; 145 | } 146 | 147 | private async Task PickUpPhone() 148 | { 149 | await StopWaveAnimation(); 150 | 151 | _ = avatarView.OnPhonePickedUp(); 152 | 153 | _ = StartTriangleToQuadrangleAnimation(); 154 | 155 | // Show all icons, buttons, time... 156 | _ = StartDetailElementsAnimation(true); 157 | 158 | await Task.Delay(900); 159 | 160 | StartMovingQuadrangleAndTimeAnimation(4000); 161 | } 162 | 163 | #region Start animation methods 164 | 165 | private void StartMovingWaveAnimation() 166 | { 167 | overlayGraphicsView.AbortAnimation(MovingWaveAnimationName); 168 | isWaveAnimating = true; 169 | 170 | Animation animation = new Animation(); 171 | 172 | animation.Add(0, 1, new Animation(v => 173 | { 174 | firstPointOffsetVector = new Point(pointToPointVector.X * v, pointToPointVector.Y * v); 175 | upVector = new Point(pointToPointVector.Y * waveVectorScale, -pointToPointVector.X * waveVectorScale); 176 | downVector = new Point(-pointToPointVector.Y * waveVectorScale, pointToPointVector.X * waveVectorScale); 177 | 178 | overlayDrawable.OverlayPath = GetOverlayWavePathF(); 179 | overlayGraphicsView.Invalidate(); 180 | }, 0, numberOfHiddenPoints)); 181 | 182 | animation.Commit(overlayGraphicsView, MovingWaveAnimationName, length: 400, repeat: () => isWaveAnimating); 183 | } 184 | 185 | private void StartWaveAnimation() 186 | { 187 | overlayGraphicsView.AbortAnimation(WaveAnimationName); 188 | 189 | Animation animation = new Animation(); 190 | 191 | animation.Add(0.1, 0.3, new Animation(v => 192 | { 193 | waveVectorScale = v; 194 | }, 0, maxWaveVectorScale)); 195 | 196 | animation.Add(0.7, 0.9, new Animation(v => 197 | { 198 | waveVectorScale = v; 199 | }, maxWaveVectorScale, 0)); 200 | 201 | animation.Commit(overlayGraphicsView, WaveAnimationName, length: 1200, repeat: () => isWaveAnimating); 202 | } 203 | 204 | private async Task StartCollapsingQuadrangleAnimation(uint lengthOfAnimation) 205 | { 206 | var bottomLeftPointOffsetY = 80; 207 | 208 | Animation animation = new Animation 209 | { 210 | { 211 | 0, 212 | 1, 213 | new Animation(v => 214 | { 215 | overlayTopLeftPoint = new Point(maxOverlayTopLeftPoint.X, v); 216 | }, overlayTopLeftPoint.Y, overlayGraphicsView.Height + bottomLeftPointOffsetY) 217 | }, 218 | { 219 | 0, 220 | 1, 221 | new Animation(v => 222 | { 223 | overlayTopRightPoint = new Point(maxOverlayTopRightPoint.X, v); 224 | }, overlayTopRightPoint.Y, overlayGraphicsView.Height + 20) 225 | }, 226 | { 227 | 0, 228 | 1, 229 | new Animation(v => 230 | { 231 | overlayDrawable.OverlayPath = GetQuadrAnglePathF(bottomLeftPointOffsetY); 232 | overlayGraphicsView.Invalidate(); 233 | }) 234 | } 235 | }; 236 | 237 | animation.Commit(overlayGraphicsView, QuadrangleCollapseAnimationName, length: lengthOfAnimation, easing: Easing.CubicOut); 238 | 239 | await Task.Delay((int)lengthOfAnimation); 240 | } 241 | 242 | private void StartMovingQuadrangleAndTimeAnimation(uint lengthOfAnimationCycle = 4000) 243 | { 244 | var animation = CreateMovingQuadrangleAnimation(); 245 | 246 | // Animation of the time element 247 | startTime = DateTime.Now; 248 | animation.Add(0, 1, new Animation(v => 249 | { 250 | var timeSpan = DateTime.Now - startTime; 251 | string seconds = timeSpan.ToString("ss"); 252 | string minutes = timeSpan.ToString("mm"); 253 | 254 | if (seconds != secondsLabel.Text) 255 | secondsLabel.Text = seconds; 256 | if (minutes != minutesLabel.Text) 257 | minutesLabel.Text = minutes; 258 | }, 0, 1)); 259 | 260 | animation.Commit(overlayGraphicsView, MovingQuadrangleAndTimeAnimationName, length: lengthOfAnimationCycle, repeat: () => isOnThePhone); 261 | } 262 | 263 | private async Task StartTriangleToQuadrangleAnimation(uint lengthOfAnimation = 800) 264 | { 265 | var animation = CreateTriangelToQuadrangleAnimation(); 266 | 267 | animation.Commit(overlayGraphicsView, TriangleToQuadrangleAnimationName, length: lengthOfAnimation, easing: Easing.CubicOut); 268 | 269 | await Task.Delay((int)lengthOfAnimation); 270 | } 271 | 272 | private async Task StartTriangleAnimation(bool show, uint lengthOfAnimation = 800) 273 | { 274 | overlayGraphicsView.IsVisible = true; 275 | 276 | // Show/hide the gray triangular overlay 277 | var animation = new Animation(v => 278 | { 279 | var path = new PathF(); 280 | 281 | path.MoveTo(new Point(overlayGraphicsView.Width, overlayGraphicsView.Height)); 282 | 283 | path.LineTo(new Point(overlayGraphicsView.Width, overlayGraphicsView.Height * (1 - (Math.Pow((1 - v), 2))))); 284 | path.LineTo(new Point(overlayGraphicsView.Width * v, overlayGraphicsView.Height)); 285 | 286 | path.Close(); 287 | 288 | overlayDrawable.OverlayPath = path; 289 | overlayGraphicsView.Invalidate(); 290 | }, show ? 1 : 0, show ? 0 : 1); 291 | 292 | animation.Commit(overlayGraphicsView, TriangleAnimationName, length: lengthOfAnimation, easing: show ? Easing.CubicOut : Easing.CubicIn); 293 | 294 | await Task.Delay((int)lengthOfAnimation + 100); 295 | } 296 | 297 | private async Task StartDetailElementsAnimation(bool show, uint lengthOfAnimation = 700) 298 | { 299 | if (show) 300 | { 301 | // Set default values 302 | buttonsStack.IsVisible = true; 303 | buttonsStack.Opacity = 0; 304 | buttonsStack.TranslationY = initialButtonsStackTranslation; 305 | timeGrid.IsVisible = true; 306 | timeGrid.Opacity = 0; 307 | timeGrid.TranslationY = timeGrid.Height; 308 | 309 | secondsLabel.Text = "00"; 310 | minutesLabel.Text = "00"; 311 | } 312 | 313 | var animation = new Animation(); 314 | 315 | // Show/hide buttons and the time element 316 | animation.Add(0, 1, new Animation(v => buttonsStack.Opacity = v, show ? 0 : 1, show ? 1 : 0)); 317 | if (show) 318 | animation.Add(0, 1, new Animation(v => buttonsStack.TranslationY = v, show ? initialButtonsStackTranslation : 0, show ? 0 : initialButtonsStackTranslation)); 319 | animation.Add(show ? 0.6 : 0, show ? 1 : 0.6, new Animation(v => timeGrid.Opacity = v, show ? 0 : 1, show ? 1 : 0)); 320 | animation.Add(show ? 0.6 : 0, show ? 1 : 0.6, new Animation(v => timeGrid.TranslationY = v, show ? timeGrid.Height / 2 : 0, show ? 0 : timeGrid.Height / 2)); 321 | 322 | animation.Commit(overlayGraphicsView, IconsGridAnimationName, length: lengthOfAnimation); 323 | 324 | await Task.Delay((int)lengthOfAnimation); 325 | 326 | if (!show) 327 | { 328 | buttonsStack.IsVisible = false; 329 | timeGrid.IsVisible = false; 330 | } 331 | } 332 | 333 | private async Task StopWaveAnimation() 334 | { 335 | overlayGraphicsView.AbortAnimation(WaveAnimationName); 336 | 337 | Animation animation = new Animation(v => 338 | { 339 | waveVectorScale = v; 340 | }, waveVectorScale, 0); 341 | 342 | animation.Commit(overlayGraphicsView, StopWaveAnimationName, length: 240); 343 | await Task.Delay(250); 344 | 345 | overlayGraphicsView.AbortAnimation(MovingWaveAnimationName); 346 | 347 | isWaveAnimating = false; 348 | 349 | overlayGraphicsView.IsVisible = true; 350 | } 351 | 352 | #endregion 353 | 354 | #region Animation creation methods 355 | 356 | private Animation CreateTriangelToQuadrangleAnimation() 357 | { 358 | // Animate the overlay from triangle to quadrangle 359 | var animation = new Animation(); 360 | 361 | animation.Add(0, 1, new Animation(v => 362 | { 363 | overlayTopLeftPoint = new Point(maxOverlayTopLeftPoint.X, v); 364 | }, overlayGraphicsView.Height, maxOverlayTopLeftPoint.Y)); 365 | animation.Add(0, 1, new Animation(v => 366 | { 367 | overlayTopRightPoint = new Point(maxOverlayTopRightPoint.X, v); 368 | }, 0, maxOverlayTopRightPoint.Y)); 369 | animation.Add(0, 1, new Animation(v => 370 | { 371 | overlayDrawable.OverlayPath = GetQuadrAnglePathF(); 372 | overlayGraphicsView.Invalidate(); 373 | })); 374 | 375 | return animation; 376 | } 377 | 378 | private Animation CreateMovingQuadrangleAnimation() 379 | { 380 | var animation = new Animation(); 381 | 382 | // Top left point up 383 | animation.Add(0.1, 0.5, new Animation(v => 384 | { 385 | overlayTopLeftPoint = new Point(minOverlayTopLeftPoint.X, maxOverlayTopLeftPoint.Y - (v * (maxOverlayTopLeftPoint.Y - minOverlayTopLeftPoint.Y))); 386 | }, 0, 1)); 387 | // Top right point up 388 | animation.Add(0, 0.4, new Animation(v => 389 | { 390 | overlayTopRightPoint = new Point(minOverlayTopRightPoint.X, maxOverlayTopRightPoint.Y - (v * (maxOverlayTopRightPoint.Y - minOverlayTopRightPoint.Y))); 391 | }, 0, 1, easing: Easing.SinInOut)); 392 | // Top left point down 393 | animation.Add(0.6, 1, new Animation(v => 394 | { 395 | overlayTopLeftPoint = new Point(maxOverlayTopLeftPoint.X, maxOverlayTopLeftPoint.Y - (v * (maxOverlayTopLeftPoint.Y - minOverlayTopLeftPoint.Y))); 396 | }, 1, 0)); 397 | // Top right point down 398 | animation.Add(0.5, 0.9, new Animation(v => 399 | { 400 | overlayTopRightPoint = new Point(maxOverlayTopRightPoint.X, maxOverlayTopRightPoint.Y - (v * (maxOverlayTopRightPoint.Y - minOverlayTopRightPoint.Y))); 401 | }, 1, 0, easing: Easing.SinInOut)); 402 | 403 | // Animation of the moving quadrangle - updating path 404 | animation.Add(0, 1, new Animation(v => 405 | { 406 | overlayDrawable.OverlayPath = GetQuadrAnglePathF(); 407 | overlayGraphicsView.Invalidate(); 408 | })); 409 | 410 | return animation; 411 | } 412 | 413 | #endregion 414 | 415 | #region PathF methods 416 | 417 | private PathF GetOverlayWavePathF() 418 | { 419 | List points = GetAllWavePoints(); 420 | 421 | var path = new PathF(); 422 | 423 | path.MoveTo(firstPoint); 424 | path.LineTo(points[0]); 425 | 426 | for (int i = 0; i < points.Count; i++) 427 | { 428 | if (i + 1 == points.Count) 429 | break; 430 | 431 | if (i % 2 == 0) 432 | continue; 433 | 434 | Point fPoint = points[i]; 435 | Point sPoint = points[i + 1]; 436 | 437 | path.QuadTo(fPoint, sPoint); 438 | } 439 | 440 | var lastPoint = new Point(overlayGraphicsView.Width + (pointToPointVector.X * numberOfHiddenPoints), pointToPointVector.X * numberOfHiddenPoints); 441 | 442 | path.LineTo(lastPoint); 443 | path.LineTo(new Point(lastPoint.X, firstPoint.Y)); 444 | 445 | path.Close(); 446 | 447 | return path; 448 | } 449 | 450 | private PathF GetQuadrAnglePathF(double bottomLeftPointOffsetY = 0) 451 | { 452 | var path = new PathF(); 453 | 454 | path.MoveTo(new Point(overlayGraphicsView.Width, overlayGraphicsView.Height)); 455 | 456 | path.LineTo(new Point(overlayTopRightPoint.X, overlayTopRightPoint.Y)); 457 | path.LineTo(new Point(overlayTopLeftPoint.X, overlayTopLeftPoint.Y)); 458 | path.LineTo(new Point(overlayTopLeftPoint.X, overlayGraphicsView.Height + bottomLeftPointOffsetY)); 459 | 460 | path.Close(); 461 | return path; 462 | } 463 | 464 | private List GetAllWavePoints() 465 | { 466 | List points = []; 467 | 468 | bool up = true; 469 | bool onLine = true; 470 | 471 | for (int i = 0; i < numberOfPoints; i++) 472 | { 473 | Point point = new Point(firstPoint.X + (pointToPointVector.X * i) + firstPointOffsetVector.X, firstPoint.Y + (pointToPointVector.Y * i) + firstPointOffsetVector.Y); 474 | 475 | if (onLine) 476 | { 477 | points.Add(point); 478 | onLine = false; 479 | } 480 | else 481 | { 482 | if (up) 483 | { 484 | points.Add(new Point(point.X + upVector.X, point.Y + upVector.Y)); 485 | up = false; 486 | onLine = true; 487 | } 488 | else 489 | { 490 | points.Add(new Point(point.X + downVector.X, point.Y + downVector.Y)); 491 | up = true; 492 | onLine = true; 493 | } 494 | 495 | if (i == numberOfPoints - 1) 496 | points.Add(new Point(firstPoint.X + (pointToPointVector.X * (i + 1)) + firstPointOffsetVector.X, firstPoint.Y + (pointToPointVector.Y * (i + 1)) + firstPointOffsetVector.Y)); 497 | } 498 | } 499 | 500 | return points; 501 | } 502 | 503 | #endregion 504 | 505 | private MainPage GetMainPage(View view) 506 | { 507 | if (view.Parent.GetType() != typeof(App)) 508 | { 509 | VisualElement parent = view; 510 | 511 | while (parent != null) 512 | { 513 | if (parent is MainPage mainPage) 514 | return mainPage; 515 | else if (parent.Parent is ShellGroupItem || parent.Parent is App) 516 | parent = null; 517 | else 518 | parent = (VisualElement)parent.Parent; 519 | } 520 | } 521 | 522 | return null; 523 | } 524 | 525 | #endregion 526 | 527 | #region Public methods 528 | 529 | public async Task Call() 530 | { 531 | this.InputTransparent = false; 532 | isHangedUp = false; 533 | 534 | _ = hangUpView.Show(); 535 | _ = avatarView.OnCalled(); 536 | await StartTriangleAnimation(true); 537 | 538 | overlayGraphicsView.IsVisible = true; 539 | 540 | StartMovingWaveAnimation(); 541 | StartWaveAnimation(); 542 | 543 | for (int i = 0; i < 100; i++) 544 | { 545 | await Task.Delay(40); 546 | if (isHangedUp) 547 | return; 548 | } 549 | 550 | if (!isHangedUp) 551 | { 552 | isOnThePhone = true; 553 | await PickUpPhone(); 554 | } 555 | } 556 | 557 | #endregion 558 | 559 | class OverlayDrawable(Color overlayColor) : IDrawable 560 | { 561 | readonly Color overlayColor = overlayColor; 562 | 563 | public PathF OverlayPath { get; set; } 564 | 565 | public void Draw(ICanvas canvas, RectF dirtyRect) 566 | { 567 | canvas.SetFillPaint(new SolidPaint(overlayColor), dirtyRect); 568 | 569 | canvas.FillPath(OverlayPath); 570 | } 571 | } 572 | } -------------------------------------------------------------------------------- /src/CallingApp.Maui/Views/Controls/ContactView.xaml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | 21 | 29 | 37 | 43 | 44 | 45 | 47 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/CallingApp.Maui/Views/Controls/ContactView.xaml.cs: -------------------------------------------------------------------------------- 1 | namespace CallingApp.Maui.Views.Controls; 2 | 3 | public partial class ContactView : Grid 4 | { 5 | public event EventHandler CallButtonClicked { add => callButton.Clicked += value; remove => callButton.Clicked -= value; } 6 | 7 | public ContactView() 8 | { 9 | InitializeComponent(); 10 | } 11 | } -------------------------------------------------------------------------------- /src/CallingApp.Maui/Views/Controls/HangUpView.xaml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/CallingApp.Maui/Views/Controls/HangUpView.xaml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Maui.Controls.Shapes; 2 | 3 | namespace CallingApp.Maui.Views.Controls; 4 | 5 | public partial class HangUpView : ContentView 6 | { 7 | #region Private members 8 | 9 | bool initSizeChange = true; 10 | 11 | double arcRadius => Width / 6.5; 12 | double arcThickness => arcRadius / 2; 13 | double lastTotalY = 0; 14 | 15 | HangupDrawable drawable; 16 | 17 | #endregion 18 | 19 | #region Public members 20 | 21 | public event Action Interacted; 22 | 23 | #endregion 24 | 25 | 26 | #region Constructor 27 | 28 | public HangUpView() 29 | { 30 | InitializeComponent(); 31 | } 32 | 33 | #endregion 34 | 35 | 36 | #region Private methods 37 | 38 | private void GraphicsViewSizeChanged(object sender, EventArgs e) 39 | { 40 | float triangleWidth = (float)((2 * arcRadius) - (2 * arcThickness)); 41 | float triangleHeight = triangleWidth / 2; 42 | 43 | var defaulArcPosition = new Point((Width / 2) - arcRadius, (Height / 2) - arcRadius); 44 | var defaultTopTrianglePosition = new Point(defaulArcPosition.X + arcThickness, defaulArcPosition.Y + arcRadius + 1); 45 | var defaultBottomTrianglePosition = new Point(defaultTopTrianglePosition.X, defaultTopTrianglePosition.Y + triangleHeight); 46 | 47 | App.Current.Resources.TryGetValue("Primary", out object overlayColor); 48 | 49 | drawable = new HangupDrawable( 50 | (float)arcRadius, 51 | (float)arcThickness, 52 | triangleWidth, 53 | triangleHeight, 54 | defaulArcPosition, 55 | defaultTopTrianglePosition, 56 | defaultBottomTrianglePosition, 57 | overlayColor as Color); 58 | 59 | graphicsView.Drawable = drawable; 60 | 61 | graphicsView.Invalidate(); 62 | 63 | if (initSizeChange) 64 | { 65 | IsVisible = false; 66 | initSizeChange = false; 67 | } 68 | } 69 | 70 | private async void PanUpdated(object sender, PanUpdatedEventArgs e) 71 | { 72 | #if IOS || ANDROID 73 | switch (e.StatusType) 74 | { 75 | case GestureStatus.Completed: 76 | case GestureStatus.Canceled: 77 | if (lastTotalY > ellipse.Height / 3) 78 | Interacted?.Invoke(sender, e); 79 | 80 | await Task.Delay(250); 81 | 82 | var animation = new Animation() 83 | { 84 | { 0, 1, new Animation(v => drawable.ArcTranslationY = (float)v, drawable.ArcTranslationY, 0) }, 85 | { 0, 1, new Animation(v => drawable.TopTriangleTranslationY = (float)v, drawable.TopTriangleTranslationY, 0) }, 86 | { 0, 1, new Animation(v => drawable.BottomTriangleTranslationY = (float)v, drawable.BottomTriangleTranslationY, 0) }, 87 | { 0, 1, new Animation(v => drawable.TopTriangleOpacity = (float)v, drawable.TopTriangleOpacity, 1) }, 88 | { 0, 1, new Animation(v => drawable.BottomTriangleOpacity = (float)v, drawable.BottomTriangleOpacity, 1) }, 89 | { 0, 1, new Animation(v => graphicsView.Invalidate(), 0, 1) } 90 | }; 91 | 92 | animation.Commit(this, "ToDefaultStateAnimation"); 93 | 94 | await Task.Delay(250); 95 | 96 | drawable.ArcTranslationY = 0; 97 | drawable.TopTriangleTranslationY = 0; 98 | drawable.BottomTriangleTranslationY = 0; 99 | drawable.TopTriangleOpacity = 1; 100 | drawable.BottomTriangleOpacity = 1; 101 | graphicsView.Invalidate(); 102 | break; 103 | case GestureStatus.Running: 104 | lastTotalY = e.TotalY; 105 | 106 | float y = e.TotalY < 0 ? 0f : (float)(e.TotalY > ellipse.Height / 2 ? ellipse.Height / 2 : e.TotalY); 107 | float scale = (float)(y / (ellipse.Height / 2)); 108 | 109 | float offset = (float)((2 * arcRadius) - (2 * arcThickness)) / 3; 110 | 111 | drawable.ArcTranslationY = -offset * scale; 112 | drawable.TopTriangleTranslationY = offset * scale; 113 | drawable.BottomTriangleTranslationY = (offset * 2) * scale; 114 | drawable.TopTriangleOpacity = 1 - (scale * 0.3f); 115 | drawable.BottomTriangleOpacity = 1 - (scale * 0.5f); 116 | 117 | graphicsView.Invalidate(); 118 | break; 119 | } 120 | #endif 121 | } 122 | 123 | private void Tapped(object sender, EventArgs e) 124 | { 125 | #if WINDOWS || MACCATALYST 126 | Interacted?.Invoke(sender, e); 127 | #endif 128 | } 129 | 130 | #endregion 131 | 132 | #region Public methods 133 | 134 | public async Task Show() 135 | { 136 | IsVisible = true; 137 | ellipse.StrokeThickness = 0; 138 | Scale = 1.5; 139 | 140 | drawable.ArcOpacity = 0; 141 | drawable.ArcRotation = 0; 142 | drawable.TopTriangleScale = 0; 143 | drawable.BottomTriangleScale = 0; 144 | graphicsView.Invalidate(); 145 | 146 | var animation = new Animation() 147 | { 148 | { 149 | 0, 150 | 0.7, 151 | new Animation(v => 152 | { 153 | Scale = v; 154 | }, 1.4, 1) 155 | }, 156 | { 0, 0.7, new Animation(v => ellipse.StrokeThickness = v, 0, HeightRequest / 2) }, 157 | { 0.3, 0.5, new Animation(v => drawable.ArcOpacity = (float)v, 0, 1) }, 158 | { 0, 1, new Animation(v => drawable.ArcRotation = (float)v, 0, 360) }, 159 | { 0.8, 1, new Animation(v => drawable.TopTriangleScale = (float)v, 0, 1) }, 160 | { 0.6, 1, new Animation(v => drawable.BottomTriangleScale = (float)v, 0, 1) }, 161 | { 0, 1, new Animation(v => graphicsView.Invalidate(), 0, 1) } 162 | }; 163 | 164 | animation.Commit(this, "ShowAnimation", length: 700); 165 | 166 | await Task.Delay(700); 167 | 168 | drawable.ArcOpacity = 1; 169 | drawable.ArcRotation = 0; 170 | drawable.TopTriangleScale = 1; 171 | drawable.BottomTriangleScale = 1; 172 | graphicsView.Invalidate(); 173 | } 174 | 175 | public async Task Hide() 176 | { 177 | var animation = new Animation(v => this.Scale = v, 1, 0, easing: Easing.SpringIn); 178 | 179 | animation.Commit(this, "HideAnimation", length: 600); 180 | 181 | await Task.Delay(600); 182 | 183 | IsVisible = false; 184 | } 185 | 186 | #endregion 187 | 188 | class HangupDrawable : IDrawable 189 | { 190 | readonly float arcRadius; 191 | readonly float arcThickness; 192 | readonly float triangleWidth; 193 | readonly float triangleHeight; 194 | readonly PointF defaultArcPosition; 195 | readonly PointF defaultTopTrianglePosition; 196 | readonly PointF defaultBottomTrianglePosition; 197 | readonly Color primaryColor; 198 | readonly PathF defaultArcPath; 199 | readonly PathF defaultTopTrianglePath; 200 | readonly PathF defaultBottomTrianglePath; 201 | 202 | public float ArcOpacity { get; set; } = 1; 203 | public float TopTriangleOpacity { get; set; } = 1; 204 | public float BottomTriangleOpacity { get; set; } = 1; 205 | public float ArcRotation { get; set; } = 0; 206 | public float ArcTranslationY { get; set; } = 0; 207 | public float TopTriangleTranslationY { get; set; } = 0; 208 | public float BottomTriangleTranslationY { get; set; } = 0; 209 | public float TopTriangleScale { get; set; } = 1; 210 | public float BottomTriangleScale { get; set; } = 1; 211 | 212 | 213 | public HangupDrawable(float arcRadius, float arcThickness, float triangleWidth, float triangleHeight, PointF defaultArcPosition, PointF defaultTopTrianglePosition, PointF defaultBottomTrianglePosition, Color primaryColor) 214 | { 215 | this.arcRadius = arcRadius; 216 | this.arcThickness = arcThickness; 217 | this.triangleWidth = triangleWidth; 218 | this.triangleHeight = triangleHeight; 219 | this.primaryColor = primaryColor; 220 | this.defaultArcPosition = defaultArcPosition; 221 | this.defaultTopTrianglePosition = defaultTopTrianglePosition; 222 | this.defaultBottomTrianglePosition = defaultBottomTrianglePosition; 223 | 224 | defaultArcPath = CreateDefaultArcPath(); 225 | defaultTopTrianglePath = CreateDefaultTopTrianglePath(); 226 | defaultBottomTrianglePath = CreateDefaultBottomTrianglePath(); 227 | } 228 | 229 | 230 | public void Draw(ICanvas canvas, RectF dirtyRect) 231 | { 232 | var arcPath = new PathF(defaultArcPath); 233 | arcPath.Move(0, ArcTranslationY); 234 | arcPath = arcPath.Rotate(ArcRotation, new Point(defaultArcPosition.X + (defaultArcPath.Bounds.Width / 2), defaultTopTrianglePosition.Y + ArcTranslationY)); 235 | 236 | var topTrianglePath = defaultTopTrianglePath.AsScaledPath(TopTriangleScale); 237 | topTrianglePath.Move( 238 | defaultTopTrianglePosition.X - topTrianglePath.Bounds.X - (topTrianglePath.Bounds.Width / 2) + (defaultTopTrianglePath.Bounds.Width / 2), 239 | defaultTopTrianglePosition.Y - topTrianglePath.Bounds.Y + TopTriangleTranslationY); 240 | 241 | var bottomTrianglePath = defaultBottomTrianglePath.AsScaledPath(BottomTriangleScale); 242 | bottomTrianglePath.Move( 243 | defaultBottomTrianglePosition.X - bottomTrianglePath.Bounds.X - (bottomTrianglePath.Bounds.Width / 2) + (defaultBottomTrianglePath.Bounds.Width / 2), 244 | defaultBottomTrianglePosition.Y - bottomTrianglePath.Bounds.Y + BottomTriangleTranslationY); 245 | 246 | canvas.SetFillPaint(new SolidColorBrush(Color.FromRgba(primaryColor.Red, primaryColor.Green, primaryColor.Blue, ArcOpacity)), dirtyRect); 247 | canvas.FillPath(arcPath); 248 | 249 | canvas.SetFillPaint(new SolidColorBrush(Color.FromRgba(primaryColor.Red, primaryColor.Green, primaryColor.Blue, TopTriangleOpacity)), dirtyRect); 250 | canvas.FillPath(topTrianglePath); 251 | 252 | canvas.SetFillPaint(new SolidColorBrush(Color.FromRgba(primaryColor.Red, primaryColor.Green, primaryColor.Blue, BottomTriangleOpacity)), dirtyRect); 253 | canvas.FillPath(bottomTrianglePath); 254 | } 255 | 256 | private PathF CreateDefaultArcPath() 257 | { 258 | var path = new PathF(); 259 | path.MoveTo(new Point(0, arcRadius)); 260 | path.QuadTo(new Point(0, 0), new Point(arcRadius, 0)); 261 | path.QuadTo(new Point(2 * arcRadius, 0), new Point(2 * arcRadius, arcRadius)); 262 | path.LineTo(new Point((2 * arcRadius) - arcThickness, arcRadius)); 263 | path.QuadTo(new Point((2 * arcRadius) - arcThickness, arcThickness), new Point(arcRadius, arcThickness)); 264 | path.QuadTo(new Point(arcThickness, arcThickness), new Point(arcThickness, arcRadius)); 265 | path.Close(); 266 | path.Move(defaultArcPosition.X, defaultArcPosition.Y); 267 | return path; 268 | } 269 | 270 | private PathF CreateDefaultTrianglePath() 271 | { 272 | var path = new PathF(); 273 | 274 | path.MoveTo(new Point(0, 0)); 275 | path.LineTo(new Point(triangleWidth, 0)); 276 | path.LineTo(new Point(arcRadius - arcThickness, triangleHeight)); 277 | path.Close(); 278 | 279 | return path; 280 | } 281 | 282 | private PathF CreateDefaultTopTrianglePath() 283 | { 284 | var path = CreateDefaultTrianglePath(); 285 | path.Move(defaultTopTrianglePosition.X, defaultTopTrianglePosition.Y); 286 | return path; 287 | } 288 | 289 | private PathF CreateDefaultBottomTrianglePath() 290 | { 291 | var path = CreateDefaultTrianglePath(); 292 | path.Move(defaultBottomTrianglePosition.X, defaultBottomTrianglePosition.Y); 293 | return path; 294 | } 295 | } 296 | } -------------------------------------------------------------------------------- /src/CallingApp.Maui/Views/Controls/HidableContentView.cs: -------------------------------------------------------------------------------- 1 | namespace CallingApp.Maui.Views.Controls; 2 | 3 | // TODO: When issue https://github.com/dotnet/maui/issues/8050 is fixed, use ContentView instead 4 | public class HidableContentView : Grid 5 | { 6 | private bool shown = true; 7 | 8 | public HidableContentView() 9 | { 10 | IsClippedToBounds = true; 11 | SizeChanged += HidableContentViewSizeChanged; 12 | } 13 | 14 | private void HidableContentViewSizeChanged(object sender, EventArgs e) 15 | { 16 | foreach (var child in Children) 17 | { 18 | if (child is View view) 19 | view.TranslationY = shown ? 0 : view.Height; 20 | } 21 | } 22 | 23 | public void HideContent() 24 | { 25 | var animation = new Animation(); 26 | foreach (var child in Children) 27 | { 28 | if (child is View view) 29 | animation.Add(0, 1, new Animation(v => view.TranslationY = v, 0, view.Height)); 30 | } 31 | animation.Commit(this, "HideContentAnimation", length: 500, easing: Easing.SinIn); 32 | 33 | shown = false; 34 | } 35 | 36 | public void ShowContent() 37 | { 38 | var animation = new Animation(); 39 | foreach (var child in Children) 40 | { 41 | if (child is View view) 42 | animation.Add(0, 1, new Animation(v => view.TranslationY = v, view.Height, 0)); 43 | } 44 | animation.Commit(this, "ShowContentAnimation", length: 500, easing: Easing.SinIn); 45 | 46 | shown = true; 47 | } 48 | } -------------------------------------------------------------------------------- /src/CallingApp.Maui/Views/Pages/MainPage.xaml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | 19 | 25 | 33 | 34 | 35 | 37 | 40 | 41 | 46 | 47 | 48 | 53 | 54 | 58 | 62 | 63 | 67 | 70 | 71 | 74 | 75 | 76 | 78 | 83 | 84 | 87 | 88 | 89 | 90 | 92 | 93 | 94 | 95 | 96 | 97 | 99 | 104 | 105 | 108 | 109 | 110 | 111 | 112 | 120 | 123 | 124 | 125 | 126 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 146 | 147 | 148 | 151 | 154 | 157 | 158 | 159 | 160 | 161 | 167 | 168 | 169 | 177 | 185 | 186 | 187 | 189 | 190 | 191 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 203 | 204 | 205 | -------------------------------------------------------------------------------- /src/CallingApp.Maui/Views/Pages/MainPage.xaml.cs: -------------------------------------------------------------------------------- 1 | using CallingApp.Core.ViewModels; 2 | using CallingApp.Maui.Views.Controls; 3 | using SimpleToolkit.Core; 4 | 5 | namespace CallingApp.Maui.Views.Pages; 6 | 7 | public partial class MainPage : ContentPage 8 | { 9 | const double SizePadding = 25; 10 | 11 | 12 | public MainPage() 13 | { 14 | BindingContext = new MainPageViewModel(); 15 | 16 | InitializeComponent(); 17 | 18 | Loaded += OnLoaded; 19 | Unloaded += OnUnloaded; 20 | } 21 | 22 | 23 | private void OnLoaded(object sender, EventArgs e) 24 | { 25 | Window.SubscribeToSafeAreaChanges(OnSafeAreaChanged); 26 | } 27 | 28 | private void OnUnloaded(object sender, EventArgs e) 29 | { 30 | Window.UnsubscribeFromSafeAreaChanges(OnSafeAreaChanged); 31 | } 32 | 33 | private void OnSafeAreaChanged(Thickness thickness) 34 | { 35 | var sideMargin = new Thickness(SizePadding + thickness.Left, 0, SizePadding + thickness.Right, 0); 36 | 37 | contentGrid.Padding = new Thickness(0, thickness.Top, 0, 0); 38 | listScrollView.Padding = new Thickness(thickness.Left, 0, thickness.Right, thickness.Bottom); 39 | callView.SafeArea = thickness; 40 | titleCotnainer.Margin = sideMargin; 41 | buttonsContainer.Margin = sideMargin; 42 | } 43 | 44 | private async void CallButtonClicked(object sender, EventArgs e) 45 | { 46 | await Task.Delay(100); 47 | HideAllViews(); 48 | 49 | await Task.Delay(200); 50 | await callView.Call(); 51 | } 52 | 53 | private void HideAllViews() 54 | { 55 | var hidableContentViews = GetAllHidableContentViews(contentGrid); 56 | 57 | var animation = new Animation 58 | { 59 | { 0, 1, new Animation(v => whiteBoxView.TranslationY = v, 0, Height) }, 60 | { 0, 1, new Animation(v => whiteGradientBoxView.TranslationY = v, 0, Height) } 61 | }; 62 | 63 | foreach (var hidableContentView in hidableContentViews) 64 | { 65 | hidableContentView.HideContent(); 66 | } 67 | 68 | animation.Commit(this, "HideContentAnimation", length: 500, easing: Easing.SinIn); 69 | } 70 | 71 | private IEnumerable GetAllHidableContentViews(IView view) 72 | { 73 | var hidableContentViews = new List(); 74 | 75 | if (view is null) 76 | return hidableContentViews; 77 | 78 | switch (view) 79 | { 80 | case HidableContentView hidableContentView: 81 | hidableContentViews.Add(hidableContentView); 82 | foreach (var child in hidableContentView.Children) 83 | hidableContentViews.AddRange(GetAllHidableContentViews(child)); 84 | break; 85 | case IBindableLayout layout: 86 | foreach (var child in layout.Children) 87 | hidableContentViews.AddRange(GetAllHidableContentViews(child as IView)); 88 | break; 89 | case IContentView contenView: 90 | hidableContentViews.AddRange(GetAllHidableContentViews(contenView.Content as IView)); 91 | break; 92 | } 93 | 94 | return hidableContentViews; 95 | } 96 | 97 | public void ShowAllViews() 98 | { 99 | var hidableContentViews = GetAllHidableContentViews(contentGrid); 100 | 101 | var animation = new Animation 102 | { 103 | { 0, 1, new Animation(v => whiteBoxView.TranslationY = v, Height, 0) }, 104 | { 0, 1, new Animation(v => whiteGradientBoxView.TranslationY = v, Height, 0) } 105 | }; 106 | 107 | foreach (var hidableContentView in hidableContentViews) 108 | { 109 | hidableContentView.ShowContent(); 110 | } 111 | 112 | animation.Commit(this, "ShowContentAnimation", length: 500, easing: Easing.SinIn); 113 | } 114 | } -------------------------------------------------------------------------------- /src/CallingApp.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.3.32611.2 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CallingApp.Core", "CallingApp.Core\CallingApp.Core.csproj", "{EF7FB7CB-DFE9-4463-9EB5-72220BDA7050}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CallingApp.Maui", "CallingApp.Maui\CallingApp.Maui.csproj", "{0B9220EE-1B99-452A-802F-5BB4F6D256A7}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Debug|iPhone = Debug|iPhone 14 | Debug|iPhoneSimulator = Debug|iPhoneSimulator 15 | Release|Any CPU = Release|Any CPU 16 | Release|iPhone = Release|iPhone 17 | Release|iPhoneSimulator = Release|iPhoneSimulator 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {EF7FB7CB-DFE9-4463-9EB5-72220BDA7050}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {EF7FB7CB-DFE9-4463-9EB5-72220BDA7050}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {EF7FB7CB-DFE9-4463-9EB5-72220BDA7050}.Debug|iPhone.ActiveCfg = Debug|Any CPU 23 | {EF7FB7CB-DFE9-4463-9EB5-72220BDA7050}.Debug|iPhone.Build.0 = Debug|Any CPU 24 | {EF7FB7CB-DFE9-4463-9EB5-72220BDA7050}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU 25 | {EF7FB7CB-DFE9-4463-9EB5-72220BDA7050}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU 26 | {EF7FB7CB-DFE9-4463-9EB5-72220BDA7050}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {EF7FB7CB-DFE9-4463-9EB5-72220BDA7050}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {EF7FB7CB-DFE9-4463-9EB5-72220BDA7050}.Release|iPhone.ActiveCfg = Release|Any CPU 29 | {EF7FB7CB-DFE9-4463-9EB5-72220BDA7050}.Release|iPhone.Build.0 = Release|Any CPU 30 | {EF7FB7CB-DFE9-4463-9EB5-72220BDA7050}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU 31 | {EF7FB7CB-DFE9-4463-9EB5-72220BDA7050}.Release|iPhoneSimulator.Build.0 = Release|Any CPU 32 | {0B9220EE-1B99-452A-802F-5BB4F6D256A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {0B9220EE-1B99-452A-802F-5BB4F6D256A7}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {0B9220EE-1B99-452A-802F-5BB4F6D256A7}.Debug|Any CPU.Deploy.0 = Debug|Any CPU 35 | {0B9220EE-1B99-452A-802F-5BB4F6D256A7}.Debug|iPhone.ActiveCfg = Debug|Any CPU 36 | {0B9220EE-1B99-452A-802F-5BB4F6D256A7}.Debug|iPhone.Build.0 = Debug|Any CPU 37 | {0B9220EE-1B99-452A-802F-5BB4F6D256A7}.Debug|iPhone.Deploy.0 = Debug|Any CPU 38 | {0B9220EE-1B99-452A-802F-5BB4F6D256A7}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU 39 | {0B9220EE-1B99-452A-802F-5BB4F6D256A7}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU 40 | {0B9220EE-1B99-452A-802F-5BB4F6D256A7}.Debug|iPhoneSimulator.Deploy.0 = Debug|Any CPU 41 | {0B9220EE-1B99-452A-802F-5BB4F6D256A7}.Release|Any CPU.ActiveCfg = Release|Any CPU 42 | {0B9220EE-1B99-452A-802F-5BB4F6D256A7}.Release|Any CPU.Build.0 = Release|Any CPU 43 | {0B9220EE-1B99-452A-802F-5BB4F6D256A7}.Release|Any CPU.Deploy.0 = Release|Any CPU 44 | {0B9220EE-1B99-452A-802F-5BB4F6D256A7}.Release|iPhone.ActiveCfg = Release|Any CPU 45 | {0B9220EE-1B99-452A-802F-5BB4F6D256A7}.Release|iPhone.Build.0 = Release|Any CPU 46 | {0B9220EE-1B99-452A-802F-5BB4F6D256A7}.Release|iPhone.Deploy.0 = Release|Any CPU 47 | {0B9220EE-1B99-452A-802F-5BB4F6D256A7}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU 48 | {0B9220EE-1B99-452A-802F-5BB4F6D256A7}.Release|iPhoneSimulator.Build.0 = Release|Any CPU 49 | {0B9220EE-1B99-452A-802F-5BB4F6D256A7}.Release|iPhoneSimulator.Deploy.0 = Release|Any CPU 50 | EndGlobalSection 51 | GlobalSection(SolutionProperties) = preSolution 52 | HideSolutionNode = FALSE 53 | EndGlobalSection 54 | GlobalSection(ExtensibilityGlobals) = postSolution 55 | SolutionGuid = {ED9497E4-2F08-44AC-9D4A-2DABB376515A} 56 | EndGlobalSection 57 | EndGlobal 58 | --------------------------------------------------------------------------------