├── .gitattributes ├── .github └── workflows │ └── dotnet.yml ├── .gitignore ├── LICENSE ├── README.md ├── ScreenShots └── normal-distributions.png ├── Source ├── OxyPlot.Maui.Skia.sln ├── OxyPlot.Maui.Skia │ ├── AppHostBuilderExtensions.cs │ ├── Core │ │ ├── BaseTemplatedView.cs │ │ └── CompositeDelegateViewCommand.cs │ ├── Effects │ │ └── MyTouchEffect.cs │ ├── Fonts │ │ ├── EmbeddedResourceFontsResolver.cs │ │ ├── IMauiFontLoader.cs │ │ ├── IPlotFontsResolver.cs │ │ ├── LocalFileFontsResolver.cs │ │ └── SkFontsHelper.cs │ ├── GlobalUsings.cs │ ├── Manipulators │ │ ├── TouchManipulator.cs │ │ └── TouchTrackerManipulator.cs │ ├── MauiOxyTouchEventArgs.cs │ ├── OxyPlot.Maui.Skia.csproj │ ├── Platforms │ │ ├── Android │ │ │ ├── Effects │ │ │ │ └── PlatformTouchEffect.cs │ │ │ └── MauiFontLoader.cs │ │ ├── MacCatalyst │ │ │ ├── Effects │ │ │ │ └── PlatformTouchEffect.cs │ │ │ └── MauiFontLoader.cs │ │ ├── Tizen │ │ │ └── PlatformClass1.cs │ │ ├── Windows │ │ │ ├── Effects │ │ │ │ └── PlatformTouchEffect.cs │ │ │ ├── MauiFontLoader.cs │ │ │ └── ModifierKeyExt.cs │ │ └── iOS │ │ │ ├── Effects │ │ │ └── PlatformTouchEffect.cs │ │ │ └── MauiFontLoader.cs │ ├── PlotCommands.cs │ ├── PlotController.cs │ ├── PlotView.cs │ ├── PlotViewBase.Events.cs │ ├── PlotViewBase.Properties.cs │ ├── PlotViewBase.cs │ ├── RenderTarget.cs │ ├── SkiaExtensions.cs │ ├── SkiaRenderContext.cs │ └── Tracker │ │ ├── TrackerControl.xaml │ │ ├── TrackerControl.xaml.cs │ │ ├── TrackerDefinition.cs │ │ └── TrackerHelper.cs └── OxyplotMauiSample │ ├── App.xaml │ ├── App.xaml.cs │ ├── AppShell.xaml │ ├── AppShell.xaml.cs │ ├── MainPage.xaml │ ├── MainPage.xaml.cs │ ├── MauiProgram.cs │ ├── OxyplotMauiSample.csproj │ ├── Pages │ ├── CustomTrackerPage.xaml │ ├── CustomTrackerPage.xaml.cs │ ├── ExampleBrowser.xaml │ ├── ExampleBrowser.xaml.cs │ ├── IssueDemos │ │ ├── DemoInfo.cs │ │ ├── DemoPageAttribute.cs │ │ ├── ExtensionMethods.cs │ │ ├── IssueDemoPage.xaml │ │ ├── IssueDemoPage.xaml.cs │ │ └── Pages │ │ │ ├── AddPlotViews.xaml │ │ │ ├── AddPlotViews.xaml.cs │ │ │ ├── AllBackgroundColorsSet.xaml │ │ │ ├── AllBackgroundColorsSet.xaml.cs │ │ │ ├── BackgroundColorPickers.xaml │ │ │ ├── BackgroundColorPickers.xaml.cs │ │ │ ├── ChangeVisibility.xaml │ │ │ ├── ChangeVisibility.xaml.cs │ │ │ ├── NoBackgroundColor.xaml │ │ │ ├── NoBackgroundColor.xaml.cs │ │ │ ├── PageBackgroundColor.xaml │ │ │ ├── PageBackgroundColor.xaml.cs │ │ │ ├── PlotModelBackground.xaml │ │ │ ├── PlotModelBackground.xaml.cs │ │ │ ├── PlotViewBackgroundColor.xaml │ │ │ ├── PlotViewBackgroundColor.xaml.cs │ │ │ └── TabbedPageWithItemTemplate.cs │ ├── PanModePage.xaml │ ├── PanModePage.xaml.cs │ ├── PlotViewPage.xaml │ └── PlotViewPage.xaml.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 │ │ ├── NotoSansCJKsc-Regular.otf │ │ ├── OpenSans-Regular.ttf │ │ └── OpenSans-Semibold.ttf │ ├── Images │ │ └── dotnet_bot.svg │ ├── Raw │ │ └── AboutAssets.txt │ ├── Splash │ │ └── splash.svg │ └── Styles │ │ ├── Colors.xaml │ │ └── Styles.xaml │ └── ViewModelBase.cs └── icon.png /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | name: ci/github-actions 2 | 3 | on: 4 | push: 5 | branches: [ '**' ] 6 | pull_request: 7 | branches: [ '**' ] 8 | # Allows you to run this workflow manually from the Actions tab 9 | workflow_dispatch: 10 | 11 | jobs: 12 | 13 | winBuild: 14 | runs-on: windows-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | - uses: nuget/setup-nuget@v2 20 | - name: Setup .NET 21 | uses: actions/setup-dotnet@v4 22 | with: 23 | dotnet-version: 9.0.x 24 | - name: Set up JDK 11 25 | uses: actions/setup-java@v4 26 | with: 27 | distribution: 'temurin' 28 | java-version: '11' 29 | - name: Install .NET MAUI 30 | shell: pwsh 31 | run: | 32 | & dotnet nuget locals all --clear 33 | & dotnet workload install maui --source https://aka.ms/dotnet6/nuget/index.json --source https://api.nuget.org/v3/index.json 34 | & dotnet workload install android ios maccatalyst tvos macos maui wasm-tools maui-maccatalyst --source https://aka.ms/dotnet6/nuget/index.json --source https://api.nuget.org/v3/index.json 35 | - name: Build library (with nuget package) 36 | run: dotnet build ./Source/OxyPlot.Maui.Skia/OxyPlot.Maui.Skia.csproj /p:Configuration=Release /t:restore,build,pack /p:PackageOutputPath=./nuget /p:Version=$(git describe) /p:ContinuousIntegrationBuild=true /p:DeterministicSourcePaths=false 37 | - name: Build sample 38 | run: dotnet build ./Source/OxyplotMauiSample/OxyplotMauiSample.csproj /p:Configuration=Release /t:restore,build /p:Version=$(git describe) /p:ContinuousIntegrationBuild=true /p:DeterministicSourcePaths=false 39 | - name: Upload packages 40 | uses: actions/upload-artifact@v4 41 | with: 42 | name: nupkg 43 | path: ./Source/*/nuget/*.nupkg 44 | 45 | macBuild: 46 | runs-on: macos-15 47 | steps: 48 | - uses: actions/checkout@v4 49 | with: 50 | fetch-depth: 0 51 | - name: Setup .NET 52 | uses: actions/setup-dotnet@v4 53 | with: 54 | dotnet-version: 9.0.x 55 | - name: Setup XCode 56 | uses: maxim-lobanov/setup-xcode@v1 57 | with: 58 | xcode-version: latest-stable 59 | - name: Install .NET MAUI 60 | run: | 61 | dotnet nuget locals all --clear 62 | dotnet workload install maui --source https://aka.ms/dotnet6/nuget/index.json --source https://api.nuget.org/v3/index.json 63 | dotnet workload install android ios maccatalyst tvos macos maui wasm-tools maui-maccatalyst --source https://aka.ms/dotnet6/nuget/index.json --source https://api.nuget.org/v3/index.json 64 | - name: Install Android tools 65 | run: ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --sdk_root=$ANDROID_SDK_ROOT "platforms;android-34" "build-tools;34.0.0" "platform-tools" 66 | - name: Build library (with nuget package) 67 | run: dotnet build ./Source/OxyPlot.Maui.Skia/OxyPlot.Maui.Skia.csproj /p:Configuration=Release /t:restore,build,pack /p:Version=$(git describe) /p:ContinuousIntegrationBuild=true /p:DeterministicSourcePaths=false 68 | - name: Build sample 69 | run: dotnet build ./Source/OxyplotMauiSample/OxyplotMauiSample.csproj /p:Configuration=Release /t:restore,build /p:Version=$(git describe) /p:ContinuousIntegrationBuild=true /p:DeterministicSourcePaths=false 70 | 71 | linuxBuild: 72 | runs-on: ubuntu-latest 73 | steps: 74 | - uses: actions/checkout@v4 75 | with: 76 | fetch-depth: 0 77 | - name: Setup .NET 78 | uses: actions/setup-dotnet@v4 79 | with: 80 | dotnet-version: 9.0.x 81 | - name: Install workloads 82 | run: dotnet workload install android wasm-tools maui-android 83 | - name: Install Android tools 84 | run: ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --sdk_root=$ANDROID_SDK_ROOT "platform-tools" 85 | - name: Build library (with nuget package) 86 | run: dotnet build ./Source/OxyPlot.Maui.Skia/OxyPlot.Maui.Skia.csproj /p:Configuration=Release /t:restore,build,pack /p:Version=$(git describe) /p:ContinuousIntegrationBuild=true /p:DeterministicSourcePaths=false 87 | - name: Build sample 88 | run: dotnet build ./Source/OxyplotMauiSample/OxyplotMauiSample.csproj /p:Configuration=Release /t:restore,build /p:Version=$(git describe) /p:ContinuousIntegrationBuild=true /p:DeterministicSourcePaths=false 89 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | obj/ 3 | .vs/ 4 | .mono/ 5 | .DS_Store 6 | packages/ 7 | Resource.designer.cs 8 | *.user 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 OxyPlot contributors 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 | [OxyPlot](https://github.com/oxyplot/oxyplot) is a cross-platform plotting library for .NET 2 | 3 | This repository contains the Maui implementation with SkiaSharp base on [OxyPlot.SkiaSharp.Wpf](https://github.com/oxyplot/oxyplot/tree/develop/Source/OxyPlot.SkiaSharp.Wpf) 4 | 5 | [Here](https://github.com/iniceice88/OxyPlot.Xamarin.Forms.Skia) is the xamarin version 6 | 7 | ![License](https://img.shields.io/badge/license-MIT-red.svg) 8 | 9 | #### Features 10 | - Pan(single-finger,two-finger or by drag axis) and Zoom 11 | - Show tracker 12 | - Unicode support 13 | 14 | #### Examples 15 | 16 | You can find examples in the `/Source/Examples` folder in the code repository. 17 | 18 | ![Plot](ScreenShots/normal-distributions.png) -------------------------------------------------------------------------------- /ScreenShots/normal-distributions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxyplot/oxyplot-maui/802ed230ab1ea6666e296129cc3ec162a26b959e/ScreenShots/normal-distributions.png -------------------------------------------------------------------------------- /Source/OxyPlot.Maui.Skia.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.3.32929.385 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OxyPlot.Maui.Skia", "OxyPlot.Maui.Skia\OxyPlot.Maui.Skia.csproj", "{F5F109EC-6749-4D9B-B969-A45E472378E7}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OxyplotMauiSample", "OxyplotMauiSample\OxyplotMauiSample.csproj", "{F6531A13-1F13-4798-83E1-EC6C580F59DC}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {F5F109EC-6749-4D9B-B969-A45E472378E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {F5F109EC-6749-4D9B-B969-A45E472378E7}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {F5F109EC-6749-4D9B-B969-A45E472378E7}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {F5F109EC-6749-4D9B-B969-A45E472378E7}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {F6531A13-1F13-4798-83E1-EC6C580F59DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {F6531A13-1F13-4798-83E1-EC6C580F59DC}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {F6531A13-1F13-4798-83E1-EC6C580F59DC}.Debug|Any CPU.Deploy.0 = Debug|Any CPU 23 | {F6531A13-1F13-4798-83E1-EC6C580F59DC}.Release|Any CPU.ActiveCfg = Release|Any CPU 24 | {F6531A13-1F13-4798-83E1-EC6C580F59DC}.Release|Any CPU.Build.0 = Release|Any CPU 25 | {F6531A13-1F13-4798-83E1-EC6C580F59DC}.Release|Any CPU.Deploy.0 = Release|Any CPU 26 | EndGlobalSection 27 | GlobalSection(SolutionProperties) = preSolution 28 | HideSolutionNode = FALSE 29 | EndGlobalSection 30 | GlobalSection(ExtensibilityGlobals) = postSolution 31 | SolutionGuid = {BC90704B-E601-4CFB-A85E-4AD0272143AD} 32 | EndGlobalSection 33 | EndGlobal 34 | -------------------------------------------------------------------------------- /Source/OxyPlot.Maui.Skia/AppHostBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using OxyPlot.Maui.Skia.Effects; 2 | using OxyPlot.Maui.Skia.Fonts; 3 | 4 | namespace OxyPlot.Maui.Skia; 5 | 6 | public static class AppHostBuilderExtensions 7 | { 8 | public static MauiAppBuilder UseOxyPlotSkia(this MauiAppBuilder builder) 9 | { 10 | return builder.ConfigureEffects(effects => 11 | { 12 | #if __ANDROID__ 13 | effects.Add(); 14 | #elif WINDOWS 15 | effects.Add(); 16 | #elif MACCATALYST 17 | effects.Add(); 18 | #elif __IOS__ 19 | effects.Add(); 20 | #endif 21 | }); 22 | } 23 | 24 | public static MauiAppBuilder UseOxyPlotSkiaCustomFonts(this MauiAppBuilder builder, IPlotFontsResolver resolver) 25 | { 26 | SkFontsHelper.FontsResolver = resolver; 27 | return builder; 28 | } 29 | } -------------------------------------------------------------------------------- /Source/OxyPlot.Maui.Skia/Core/BaseTemplatedView.cs: -------------------------------------------------------------------------------- 1 | namespace OxyPlot.Maui.Skia.Core; 2 | 3 | public abstract class BaseTemplatedView : TemplatedView where TControl : View, new() 4 | { 5 | protected TControl Control { get; private set; } 6 | 7 | public BaseTemplatedView() 8 | => ControlTemplate = new ControlTemplate(typeof(TControl)); 9 | 10 | protected override void OnBindingContextChanged() 11 | { 12 | base.OnBindingContextChanged(); 13 | Control.BindingContext = BindingContext; 14 | } 15 | 16 | /// 17 | protected override void OnChildAdded(Microsoft.Maui.Controls.Element child) 18 | { 19 | if (Control == null && child is TControl content) 20 | { 21 | Control = content; 22 | OnControlInitialized(Control); 23 | } 24 | 25 | base.OnChildAdded(child); 26 | } 27 | 28 | protected abstract void OnControlInitialized(TControl control); 29 | } -------------------------------------------------------------------------------- /Source/OxyPlot.Maui.Skia/Core/CompositeDelegateViewCommand.cs: -------------------------------------------------------------------------------- 1 | namespace OxyPlot.Maui.Skia.Core; 2 | 3 | /// 4 | /// Combine multiple commands to one. 5 | /// 6 | /// 7 | public class CompositeDelegateViewCommand : IViewCommand 8 | where T : OxyInputEventArgs 9 | { 10 | private readonly IViewCommand[] commands; 11 | public CompositeDelegateViewCommand(params IViewCommand[] commands) 12 | { 13 | this.commands = commands; 14 | } 15 | 16 | public void Execute(IView view, IController controller, T args) 17 | { 18 | foreach (var cmd in commands) 19 | { 20 | cmd.Execute(view, controller, args); 21 | if (args.Handled) 22 | break; 23 | } 24 | } 25 | 26 | public void Execute(IView view, IController controller, OxyInputEventArgs args) 27 | { 28 | foreach (var cmd in commands) 29 | { 30 | cmd.Execute(view, controller, args); 31 | if (args.Handled) 32 | break; 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /Source/OxyPlot.Maui.Skia/Effects/MyTouchEffect.cs: -------------------------------------------------------------------------------- 1 | namespace OxyPlot.Maui.Skia.Effects; 2 | 3 | public class MyTouchEffect : RoutingEffect 4 | { 5 | public event TouchActionEventHandler TouchAction; 6 | 7 | public void OnTouchAction(Microsoft.Maui.Controls.Element element, TouchActionEventArgs args) 8 | { 9 | TouchAction?.Invoke(element, args); 10 | } 11 | } 12 | 13 | public delegate void TouchActionEventHandler(object sender, TouchActionEventArgs args); 14 | 15 | public enum TouchActionType 16 | { 17 | Pressed, 18 | Moved, 19 | Released, 20 | MouseWheel 21 | } 22 | 23 | public class TouchActionEventArgs : EventArgs 24 | { 25 | public TouchActionEventArgs(long id, TouchActionType type, Point[] locations, bool isInContact) 26 | { 27 | Id = id; 28 | Type = type; 29 | Locations = locations; 30 | IsInContact = isInContact; 31 | } 32 | 33 | public long Id { get; } 34 | 35 | public TouchActionType Type { get; } 36 | 37 | public Point Location => Locations == null || Locations.Length == 0 ? Point.Zero : Locations[0]; 38 | 39 | public Point[] Locations { get; } 40 | 41 | public bool IsInContact { get; } 42 | 43 | public OxyModifierKeys ModifierKeys { get; set; } 44 | 45 | public int MouseWheelDelta { get; set; } 46 | } -------------------------------------------------------------------------------- /Source/OxyPlot.Maui.Skia/Fonts/EmbeddedResourceFontsResolver.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace OxyPlot.Maui.Skia.Fonts; 4 | 5 | public class EmbeddedResourceFontsResolver : IPlotFontsResolver 6 | { 7 | private readonly Dictionary fontWeights = new() 8 | { 9 | [100] = "Thin", 10 | [200] = "ExtraLight", 11 | [300] = "Light", 12 | [400] = "Regular", 13 | [500] = "Medium", 14 | [600] = "SemiBold", 15 | [700] = "Bold", 16 | [800] = "ExtraBold", 17 | [900] = "Black" 18 | }; 19 | 20 | public Assembly Assembly { get; set; } 21 | 22 | public string[] SearchFontExtensions { get; set; } = { ".ttf", ".otf" }; 23 | 24 | public EmbeddedResourceFontsResolver(Assembly assembly) 25 | { 26 | this.Assembly = assembly; 27 | } 28 | 29 | /// 30 | public Stream ResolveFont(string fontFamily, int fontWeight) 31 | { 32 | if (Assembly == null) 33 | { 34 | return null; 35 | } 36 | 37 | var weight = (fontWeights.TryGetValue(fontWeight, out var w) ? w : "Regular"); 38 | var resourceNames = Assembly.GetManifestResourceNames(); 39 | foreach (var ext in SearchFontExtensions) 40 | { 41 | var fontFileName = $"{fontFamily}-{weight}{ext}"; 42 | var resourceName = 43 | resourceNames.FirstOrDefault(x => x.EndsWith(fontFileName, StringComparison.OrdinalIgnoreCase)); 44 | 45 | if (resourceName == null) 46 | { 47 | fontFileName = $"{fontFamily}{ext}"; 48 | resourceName = 49 | resourceNames.FirstOrDefault(x => x.EndsWith(fontFileName, StringComparison.OrdinalIgnoreCase)); 50 | } 51 | 52 | if (resourceName == null) 53 | continue; 54 | return Assembly.GetManifestResourceStream(resourceName); 55 | } 56 | 57 | return null; 58 | } 59 | } -------------------------------------------------------------------------------- /Source/OxyPlot.Maui.Skia/Fonts/IMauiFontLoader.cs: -------------------------------------------------------------------------------- 1 | namespace OxyPlot.Maui.Skia.Fonts; 2 | 3 | public interface IMauiFontLoader 4 | { 5 | Stream Load(string fontName); 6 | } -------------------------------------------------------------------------------- /Source/OxyPlot.Maui.Skia/Fonts/IPlotFontsResolver.cs: -------------------------------------------------------------------------------- 1 | namespace OxyPlot.Maui.Skia.Fonts; 2 | 3 | public interface IPlotFontsResolver 4 | { 5 | Stream ResolveFont(string fontFamily, int fontWeight); 6 | } -------------------------------------------------------------------------------- /Source/OxyPlot.Maui.Skia/Fonts/LocalFileFontsResolver.cs: -------------------------------------------------------------------------------- 1 | namespace OxyPlot.Maui.Skia.Fonts; 2 | 3 | public class LocalFileFontsResolver : IPlotFontsResolver 4 | { 5 | private readonly Dictionary fontWeights = new() 6 | { 7 | [100] = "Thin", 8 | [200] = "ExtraLight", 9 | [300] = "Light", 10 | [400] = "Regular", 11 | [500] = "Medium", 12 | [600] = "SemiBold", 13 | [700] = "Bold", 14 | [800] = "ExtraBold", 15 | [900] = "Black" 16 | }; 17 | 18 | public string FontsDirectory { get; set; } 19 | 20 | public string[] SearchFontExtensions { get; set; } = { ".ttf", ".otf" }; 21 | 22 | public LocalFileFontsResolver(string fontsDirectory) 23 | { 24 | this.FontsDirectory = fontsDirectory; 25 | } 26 | 27 | /// 28 | public Stream ResolveFont(string fontFamily, int fontWeight) 29 | { 30 | if (!Directory.Exists(FontsDirectory)) 31 | { 32 | return null; 33 | } 34 | 35 | var weight = (fontWeights.TryGetValue(fontWeight, out var w) ? w : "Regular"); 36 | foreach (var ext in SearchFontExtensions) 37 | { 38 | var fontFilePath = Path.Combine(FontsDirectory, $"{fontFamily}-{weight}{ext}"); 39 | if (!File.Exists(fontFilePath)) 40 | { 41 | fontFilePath = Path.Combine(FontsDirectory, fontFamily + ext); 42 | } 43 | 44 | if (!File.Exists(fontFilePath)) 45 | { 46 | continue; 47 | } 48 | 49 | return File.OpenRead(fontFilePath); 50 | } 51 | 52 | // can not find bold,try fallback to Regular 53 | if (weight != "Regular") 54 | { 55 | foreach (var ext in SearchFontExtensions) 56 | { 57 | var fontFilePath = Path.Combine(FontsDirectory, $"{fontFamily}-Regular{ext}"); 58 | if (!File.Exists(fontFilePath)) 59 | { 60 | continue; 61 | } 62 | 63 | return File.OpenRead(fontFilePath); 64 | } 65 | } 66 | 67 | return null; 68 | } 69 | } -------------------------------------------------------------------------------- /Source/OxyPlot.Maui.Skia/Fonts/SkFontsHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | 3 | namespace OxyPlot.Maui.Skia.Fonts; 4 | 5 | internal static class SkFontsHelper 6 | { 7 | public static IPlotFontsResolver FontsResolver { get; set; } 8 | 9 | public static IMauiFontLoader MauiFontLoader { get; set; } = 10 | #if __ANDROID__ 11 | new Platforms.Android.MauiFontLoader(); 12 | #elif WINDOWS 13 | new Windows.MauiFontLoader(); 14 | #elif MACCATALYST 15 | new mac.MauiFontLoader(); 16 | #elif __IOS__ 17 | new OxyPlot.Maui.Skia.ios.MauiFontLoader(); 18 | #else 19 | null; 20 | #endif 21 | 22 | private static readonly ConcurrentDictionary FontCache = new(); 23 | 24 | public static SKTypeface ResolveFont(string fontFamily, int fontWeight) 25 | { 26 | if (string.IsNullOrEmpty(fontFamily)) return SKTypeface.Default; 27 | 28 | var key = $"{fontFamily}\t{fontWeight}"; 29 | return FontCache.GetOrAdd(key, (_) => ResolveFontCore(fontFamily, fontWeight)); 30 | } 31 | 32 | private static SKTypeface ResolveFontCore(string fontFamily, int fontWeight) 33 | { 34 | var typeface = SKTypeface.FromFamilyName(fontFamily, new SKFontStyle(fontWeight, (int)SKFontStyleWidth.Normal, SKFontStyleSlant.Upright)); 35 | if (typeface != null && typeface.FamilyName == fontFamily) 36 | return typeface; 37 | 38 | var fontStream = MauiFontLoader?.Load(fontFamily); 39 | if (fontStream == null) 40 | { 41 | fontStream = FontsResolver?.ResolveFont(fontFamily, fontWeight); 42 | } 43 | 44 | if (fontStream != null) 45 | { 46 | typeface = SKTypeface.FromStream(fontStream); 47 | fontStream.Close(); 48 | return typeface; 49 | } 50 | 51 | return typeface ?? SKTypeface.Default; 52 | } 53 | } -------------------------------------------------------------------------------- /Source/OxyPlot.Maui.Skia/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using System.Collections.ObjectModel; 2 | global using SkiaSharp; 3 | global using SkiaSharp.Views.Maui; 4 | global using SkiaSharp.Views.Maui.Controls; 5 | -------------------------------------------------------------------------------- /Source/OxyPlot.Maui.Skia/Manipulators/TouchManipulator.cs: -------------------------------------------------------------------------------- 1 | namespace OxyPlot.Maui.Skia.Manipulators; 2 | 3 | /// 4 | /// Provides a manipulator for panning and scaling by touch events. 5 | /// 6 | public class TouchManipulator : PlotManipulator 7 | { 8 | /// 9 | /// Initializes a new instance of the class. 10 | /// 11 | /// The plot view. 12 | public TouchManipulator(IPlotView plotView) 13 | : base(plotView) 14 | { 15 | SetHandledForPanOrZoom = true; 16 | IsOnlyAcceptAxisPan = false; 17 | IsPanByTowFinger = false; 18 | } 19 | 20 | /// 21 | /// Only can pan by drag Axises if set to True 22 | /// 23 | public bool IsOnlyAcceptAxisPan { get; set; } 24 | 25 | /// 26 | /// Pan by tow-finger? 27 | /// https://github.com/oxyplot/oxyplot/issues/633 28 | /// 29 | public bool IsPanByTowFinger { get; set; } 30 | 31 | /// 32 | /// Gets or sets a value indicating whether e.Handled should be set to true 33 | /// in case pan or zoom is enabled. 34 | /// 35 | protected bool SetHandledForPanOrZoom { get; set; } 36 | 37 | /// 38 | /// Gets or sets a value indicating whether panning is enabled. 39 | /// 40 | private bool IsPanEnabled { get; set; } 41 | 42 | /// 43 | /// Gets or sets a value indicating whether zooming is enabled. 44 | /// 45 | private bool IsZoomEnabled { get; set; } 46 | 47 | /// 48 | /// Occurs when a manipulation is complete. 49 | /// 50 | /// The instance containing the event data. 51 | public override void Completed(OxyTouchEventArgs e) 52 | { 53 | base.Completed(e); 54 | 55 | if (SetHandledForPanOrZoom) 56 | { 57 | e.Handled |= IsPanEnabled || IsZoomEnabled; 58 | } 59 | } 60 | 61 | /// 62 | /// Occurs when a touch delta event is handled. 63 | /// 64 | /// The instance containing the event data. 65 | public override void Delta(OxyTouchEventArgs e) 66 | { 67 | base.Delta(e); 68 | 69 | if (!IsPanEnabled && !IsZoomEnabled) 70 | { 71 | return; 72 | } 73 | 74 | var newPosition = e.Position; 75 | var previousPosition = newPosition - e.DeltaTranslation; 76 | 77 | var ignorePan = IsOnlyAcceptAxisPan && XAxis != null && YAxis != null; 78 | if (!ignorePan && IsPanByTowFinger && e is MauiOxyTouchEventArgs e2) 79 | { 80 | ignorePan = e2.PointerCount == 1; 81 | } 82 | 83 | if (!ignorePan) 84 | { 85 | XAxis?.Pan(previousPosition, newPosition); 86 | 87 | YAxis?.Pan(previousPosition, newPosition); 88 | } 89 | 90 | var current = InverseTransform(newPosition.X, newPosition.Y); 91 | 92 | XAxis?.ZoomAt(e.DeltaScale.X, current.X); 93 | YAxis?.ZoomAt(e.DeltaScale.Y, current.Y); 94 | 95 | PlotView.InvalidatePlot(false); 96 | e.Handled = true; 97 | } 98 | 99 | /// 100 | /// Occurs when an input device begins a manipulation on the plot. 101 | /// 102 | /// The instance containing the event data. 103 | public override void Started(OxyTouchEventArgs e) 104 | { 105 | AssignAxes(e.Position); 106 | base.Started(e); 107 | 108 | if (SetHandledForPanOrZoom) 109 | { 110 | IsPanEnabled = XAxis is { IsPanEnabled: true } 111 | || YAxis is { IsPanEnabled: true }; 112 | 113 | IsZoomEnabled = XAxis is { IsZoomEnabled: true } 114 | || YAxis is { IsZoomEnabled: true }; 115 | 116 | e.Handled |= IsPanEnabled || IsZoomEnabled; 117 | } 118 | } 119 | } -------------------------------------------------------------------------------- /Source/OxyPlot.Maui.Skia/Manipulators/TouchTrackerManipulator.cs: -------------------------------------------------------------------------------- 1 | using OxyPlot.Maui.Skia.Tracker; 2 | using Timer = System.Timers.Timer; 3 | 4 | namespace OxyPlot.Maui.Skia.Manipulators; 5 | 6 | /// 7 | /// Provides a plot manipulator for tracker functionality. 8 | /// 9 | public class TouchTrackerManipulator : OxyPlot.TouchManipulator 10 | { 11 | /// 12 | /// The current series. 13 | /// 14 | private Series.Series currentSeries; 15 | 16 | /// 17 | /// Initializes a new instance of the class. 18 | /// 19 | /// The plot view. 20 | public TouchTrackerManipulator(IPlotView plotView) 21 | : base(plotView) 22 | { 23 | this.Snap = true; 24 | this.PointsOnly = false; 25 | this.LockToInitialSeries = true; 26 | this.FiresDistance = 20.0; 27 | this.CheckDistanceBetweenPoints = false; 28 | 29 | // Note: the tracker manipulator should not handle pan or zoom 30 | this.SetHandledForPanOrZoom = false; 31 | } 32 | 33 | /// 34 | /// Gets or sets a value indicating whether to show tracker on points only (not interpolating). 35 | /// 36 | public bool PointsOnly { get; set; } 37 | 38 | /// 39 | /// Gets or sets a value indicating whether to snap to the nearest point. 40 | /// 41 | public bool Snap { get; set; } 42 | 43 | /// 44 | /// Gets or sets a value indicating whether to lock the tracker to the initial series. 45 | /// 46 | /// true if the tracker should be locked; otherwise, false. 47 | public bool LockToInitialSeries { get; set; } 48 | 49 | /// 50 | /// Gets or sets the distance from the series at which the tracker fires. 51 | /// 52 | public double FiresDistance { get; set; } 53 | 54 | /// 55 | /// Gets or sets a value indicating whether to check distance when showing tracker between data points. 56 | /// 57 | /// This parameter is ignored if is equal to False. 58 | public bool CheckDistanceBetweenPoints { get; set; } 59 | 60 | /// 61 | /// Gets or sets a value indicating whether to track annotations. 62 | /// 63 | public bool IsTrackAnnotations { get; set; } = false; 64 | 65 | /// 66 | /// Occurs when a manipulation is complete. 67 | /// 68 | /// The instance containing the event data. 69 | public override void Completed(OxyTouchEventArgs e) 70 | { 71 | base.Completed(e); 72 | 73 | this.currentSeries = null; 74 | // this.PlotView.HideTracker(); 75 | if (this.PlotView.ActualModel != null) 76 | { 77 | this.PlotView.ActualModel.RaiseTrackerChanged(null); 78 | } 79 | } 80 | 81 | /// 82 | /// Occurs when a touch delta event is handled. 83 | /// 84 | /// The instance containing the event data. 85 | public override void Delta(OxyTouchEventArgs e) 86 | { 87 | base.Delta(e); 88 | 89 | var v = startPoint - e.Position; 90 | 91 | if (v is { Length: > 10 }) 92 | { 93 | if (updateTrackerTimer is { Enabled: true }) 94 | { 95 | updateTrackerTimer.Stop(); 96 | updateTrackerTimer = null; 97 | } 98 | 99 | // This is touch, we want to hide the tracker because the user is probably panning / zooming now 100 | this.PlotView.HideTracker(); 101 | } 102 | } 103 | 104 | /// 105 | /// Occurs when an input device begins a manipulation on the plot. 106 | /// 107 | /// The instance containing the event data. 108 | public override void Started(OxyTouchEventArgs e) 109 | { 110 | base.Started(e); 111 | startPoint = e.Position; 112 | 113 | this.currentSeries = this.PlotView.ActualModel?.GetSeriesFromPoint(e.Position); 114 | 115 | UpdateTrackerDelay(e.Position); 116 | } 117 | 118 | private ScreenPoint? startPoint; 119 | private Timer updateTrackerTimer; 120 | 121 | private void UpdateTrackerDelay(ScreenPoint position) 122 | { 123 | if (updateTrackerTimer == null) 124 | { 125 | updateTrackerTimer = new Timer(100); 126 | updateTrackerTimer.AutoReset = false; 127 | updateTrackerTimer.Elapsed += (s, e) => 128 | { 129 | updateTrackerTimer = null; 130 | (this.PlotView as BindableObject).Dispatcher.Dispatch(() => UpdateTracker(position)); 131 | }; 132 | } 133 | else 134 | { 135 | updateTrackerTimer.Stop(); 136 | } 137 | updateTrackerTimer.Start(); 138 | } 139 | 140 | /// 141 | /// Updates the tracker to the specified position. 142 | /// 143 | /// The position. 144 | private void UpdateTracker(ScreenPoint position) 145 | { 146 | if (this.currentSeries == null || !this.LockToInitialSeries) 147 | { 148 | // get the nearest 149 | this.currentSeries = this.PlotView.ActualModel?.GetSeriesFromPoint(position, this.FiresDistance); 150 | } 151 | 152 | if (this.currentSeries == null) 153 | { 154 | if (!this.LockToInitialSeries) 155 | { 156 | this.PlotView.HideTracker(); 157 | } 158 | 159 | if (IsTrackAnnotations) 160 | { 161 | TrackAnnotations(position); 162 | } 163 | return; 164 | } 165 | 166 | var actualModel = this.PlotView.ActualModel; 167 | if (actualModel == null) 168 | { 169 | return; 170 | } 171 | 172 | if (!actualModel.PlotArea.Contains(position.X, position.Y)) 173 | { 174 | return; 175 | } 176 | 177 | var result = TrackerHelper.GetNearestHit( 178 | this.currentSeries, position, this.Snap, this.PointsOnly, this.FiresDistance, this.CheckDistanceBetweenPoints); 179 | if (result != null) 180 | { 181 | result.PlotModel = actualModel; 182 | this.PlotView.ShowTracker(result); 183 | actualModel.RaiseTrackerChanged(result); 184 | } 185 | } 186 | 187 | /// 188 | /// Track Annotations 189 | /// 190 | /// 191 | private void TrackAnnotations(ScreenPoint sp) 192 | { 193 | foreach (var annotation in PlotView.ActualModel.Annotations 194 | .Where(x => !string.IsNullOrEmpty(x.ToolTip)) 195 | .Reverse()) 196 | { 197 | var args = new HitTestArguments(sp, FiresDistance); 198 | var res = annotation.HitTest(args); 199 | 200 | if (res == null) 201 | continue; 202 | 203 | var dp = annotation.InverseTransform(sp); 204 | var result = new TrackerHitResult 205 | { 206 | Position = sp, 207 | DataPoint = dp, 208 | Text = annotation.ToolTip, 209 | PlotModel = this.PlotView.ActualModel 210 | }; 211 | this.PlotView.ShowTracker(result); 212 | this.PlotView.ActualModel.RaiseTrackerChanged(result); 213 | break; 214 | } 215 | } 216 | } -------------------------------------------------------------------------------- /Source/OxyPlot.Maui.Skia/MauiOxyTouchEventArgs.cs: -------------------------------------------------------------------------------- 1 | namespace OxyPlot.Maui.Skia; 2 | 3 | public class MauiOxyTouchEventArgs : OxyTouchEventArgs 4 | { 5 | public int PointerCount { get; set; } 6 | 7 | public MauiOxyTouchEventArgs() 8 | { 9 | } 10 | 11 | /// 12 | /// Initializes a new instance of the class. 13 | /// 14 | /// The current touches. 15 | /// The previous touches. 16 | public MauiOxyTouchEventArgs(ScreenPoint[] currentTouches, ScreenPoint[] previousTouches) 17 | : base(currentTouches, previousTouches) 18 | { 19 | PointerCount = currentTouches.Length; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Source/OxyPlot.Maui.Skia/OxyPlot.Maui.Skia.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0;net8.0-android34.0;net9.0;net9.0-android 5 | $(TargetFrameworks);net8.0-ios;net8.0-maccatalyst;net9.0-ios;net9.0-maccatalyst 6 | $(TargetFrameworks);net8.0-windows10.0.19041.0;net9.0-windows10.0.19041.0 7 | 8 | 9 | true 10 | true 11 | enable 12 | 13 | 12.2 14 | 15.0 15 | 21.0 16 | 10.0.17763.0 17 | 10.0.17763.0 18 | 6.5 19 | 20 | OxyPlot.Maui.Skia 21 | 1.1.0 22 | OxyPlot for MAUI 23 | OxyPlot is a plotting library for .NET. This package targets MAUI. 24 | OxyPlot contributors 25 | OxyPlot contributors 26 | icon.png 27 | MIT 28 | https://oxyplot.github.io/ 29 | README.md 30 | https://github.com/oxyplot/oxyplot-maui 31 | 32 | 33 | 34 | false 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | MSBuild:Compile 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /Source/OxyPlot.Maui.Skia/Platforms/Android/Effects/PlatformTouchEffect.cs: -------------------------------------------------------------------------------- 1 | using Android.Views; 2 | using Microsoft.Maui.Controls.Platform; 3 | using Microsoft.Maui.Platform; 4 | using OxyPlot.Maui.Skia.Effects; 5 | using View = Android.Views.View; 6 | 7 | namespace OxyPlot.Maui.Skia.Droid.Effects; 8 | 9 | public class PlatformTouchEffect : PlatformEffect 10 | { 11 | private Microsoft.Maui.Controls.Element _formsElement; 12 | private Func _fromPixels; 13 | private MyTouchEffect libMyTouchEffect; 14 | private View _view; 15 | 16 | protected override void OnAttached() 17 | { 18 | // Get the Android View corresponding to the Element that the effect is attached to 19 | _view = Control == null ? Container : Control; 20 | 21 | // Get access to the TouchEffect class in the .NET Standard library 22 | var touchEffect = Element.Effects.OfType().FirstOrDefault(); 23 | 24 | if (touchEffect != null && _view != null) 25 | { 26 | ViewHolder.Add(_view, this); 27 | 28 | _formsElement = Element; 29 | 30 | libMyTouchEffect = touchEffect; 31 | 32 | // Save fromPixels function 33 | _fromPixels = _view.Context.FromPixels; 34 | 35 | // Set event handler on View 36 | _view.Touch += OnTouch; 37 | } 38 | } 39 | 40 | protected override void OnDetached() 41 | { 42 | if (ViewHolder.ContainsKey(_view)) 43 | { 44 | ViewHolder.Remove(_view); 45 | _view.Touch -= OnTouch; 46 | } 47 | } 48 | 49 | private void OnTouch(object sender, View.TouchEventArgs args) 50 | { 51 | // Two object common to all the events 52 | var senderView = sender as View; 53 | var motionEvent = args.Event; 54 | 55 | int[] twoIntArray = new int[2]; 56 | senderView.GetLocationOnScreen(twoIntArray); 57 | 58 | var list = new List(); 59 | for (var pointerIndex = 0; pointerIndex < motionEvent.PointerCount; pointerIndex++) 60 | { 61 | list.Add(new Point(twoIntArray[0] + motionEvent.GetX(pointerIndex), 62 | twoIntArray[1] + motionEvent.GetY(pointerIndex))); 63 | } 64 | 65 | var screenPointerCoords = list.ToArray(); 66 | var id = motionEvent.GetPointerId(motionEvent.ActionIndex); 67 | 68 | // Use ActionMasked here rather than Action to reduce the number of possibilities 69 | switch (args.Event.ActionMasked) 70 | { 71 | case MotionEventActions.Down: 72 | case MotionEventActions.PointerDown: 73 | FireEvent(this, id, TouchActionType.Pressed, screenPointerCoords, true); 74 | break; 75 | case MotionEventActions.Move: 76 | FireEvent(this, id, TouchActionType.Moved, screenPointerCoords, true); 77 | break; 78 | case MotionEventActions.Up: 79 | case MotionEventActions.Pointer1Up: 80 | FireEvent(this, id, TouchActionType.Released, screenPointerCoords, false); 81 | break; 82 | } 83 | } 84 | 85 | private void FireEvent(PlatformTouchEffect platformTouchEffect, int id, TouchActionType actionType, Point[] pointerLocations, 86 | bool isInContact) 87 | { 88 | // Get the method to call for firing events 89 | Action onTouchAction = 90 | platformTouchEffect.libMyTouchEffect.OnTouchAction; 91 | 92 | // Get the location of the pointer within the view 93 | int[] twoIntArray = new int[2]; 94 | platformTouchEffect._view.GetLocationOnScreen(twoIntArray); 95 | List locations = new List(); 96 | foreach (var loc in pointerLocations) 97 | { 98 | var x = loc.X - twoIntArray[0]; 99 | var y = loc.Y - twoIntArray[1]; 100 | var point = new Point(_fromPixels(x), _fromPixels(y)); 101 | locations.Add(point); 102 | } 103 | 104 | // Call the method 105 | onTouchAction(platformTouchEffect._formsElement, 106 | new TouchActionEventArgs(id, actionType, locations.ToArray(), isInContact)); 107 | } 108 | 109 | static class ViewHolder 110 | { 111 | private static readonly Dictionary _viewDic = new(); 112 | 113 | public static bool ContainsKey(View view) 114 | { 115 | Shake(); 116 | return _viewDic.ContainsKey(view.GetHashCode()); 117 | } 118 | 119 | public static void Add(View view, PlatformTouchEffect eff) 120 | { 121 | Shake(); 122 | _viewDic[view.GetHashCode()] = new WeakViewTouchEffectPair(view, eff); 123 | } 124 | 125 | public static void Remove(View view) 126 | { 127 | Shake(); 128 | 129 | _viewDic.Remove(view.GetHashCode()); 130 | } 131 | 132 | private static void Shake() 133 | { 134 | foreach (var key in _viewDic.Keys.ToArray()) 135 | { 136 | if (!_viewDic[key].IsAlive) 137 | _viewDic.Remove(key); 138 | } 139 | } 140 | } 141 | 142 | class WeakViewTouchEffectPair 143 | { 144 | private readonly WeakReference _weakView; 145 | public View View => _weakView.Target as View; 146 | 147 | public bool IsAlive => _weakTouchEffect.IsAlive && _weakView.IsAlive; 148 | 149 | private readonly WeakReference _weakTouchEffect; 150 | public PlatformTouchEffect TouchEffect => _weakTouchEffect.Target as PlatformTouchEffect; 151 | 152 | public WeakViewTouchEffectPair(View view, PlatformTouchEffect eff) 153 | { 154 | _weakView = new WeakReference(view); 155 | _weakTouchEffect = new WeakReference(eff); 156 | } 157 | 158 | } 159 | } -------------------------------------------------------------------------------- /Source/OxyPlot.Maui.Skia/Platforms/Android/MauiFontLoader.cs: -------------------------------------------------------------------------------- 1 | using OxyPlot.Maui.Skia.Fonts; 2 | using AApplication = Android.App.Application; 3 | 4 | namespace OxyPlot.Maui.Skia.Platforms.Android; 5 | 6 | public class MauiFontLoader : IMauiFontLoader 7 | { 8 | static readonly string[] FontFolders = 9 | { 10 | "Fonts/", 11 | "fonts/", 12 | }; 13 | 14 | private IFontRegistrar fontRegistrar = null; 15 | /// 16 | public Stream Load(string fontName) 17 | { 18 | if (fontRegistrar == null) 19 | fontRegistrar = IPlatformApplication.Current.Services.GetRequiredService(); 20 | 21 | return GetFromAssets(fontName); 22 | } 23 | 24 | Stream GetFromAssets(string fontName) 25 | { 26 | fontName = fontRegistrar.GetFont(fontName) ?? fontName; 27 | 28 | // First check Alias 29 | var asset = LoadFontFromAsset(fontName); 30 | if (asset != null) 31 | return asset; 32 | 33 | // The font might be a file, such as a temporary file extracted from EmbeddedResource 34 | if (File.Exists(fontName)) 35 | { 36 | return File.OpenRead(fontName); 37 | } 38 | 39 | var fontFile = FontFile.FromString(fontName); 40 | if (!string.IsNullOrWhiteSpace(fontFile.Extension)) 41 | { 42 | return FindFont(fontFile.FileNameWithExtension()); 43 | } 44 | 45 | foreach (var ext in FontFile.Extensions) 46 | { 47 | var font = FindFont(fontFile.FileNameWithExtension(ext)); 48 | if (font != null) 49 | return font; 50 | } 51 | 52 | return null; 53 | } 54 | 55 | Stream FindFont(string fileWithExtension) 56 | { 57 | var result = LoadFontFromAsset(fileWithExtension); 58 | if (result != null) 59 | return result; 60 | 61 | foreach (var folder in FontFolders) 62 | { 63 | result = LoadFontFromAsset(folder + fileWithExtension); 64 | if (result != null) 65 | return result; 66 | } 67 | 68 | return null; 69 | } 70 | 71 | Stream LoadFontFromAsset(string fontName) 72 | { 73 | try 74 | { 75 | var fontPath = FontNameToFontFile(fontName); 76 | if (!AApplication.Context.Assets.List("").Contains(fontPath)) 77 | return null; 78 | return AApplication.Context.Assets.Open(fontPath); 79 | } 80 | catch 81 | { 82 | // ignore 83 | } 84 | 85 | return null; 86 | } 87 | 88 | string FontNameToFontFile(string fontFamily) 89 | { 90 | fontFamily ??= string.Empty; 91 | 92 | int hashtagIndex = fontFamily.IndexOf("#", StringComparison.Ordinal); 93 | if (hashtagIndex >= 0) 94 | return fontFamily.Substring(0, hashtagIndex); 95 | 96 | return fontFamily; 97 | } 98 | } -------------------------------------------------------------------------------- /Source/OxyPlot.Maui.Skia/Platforms/MacCatalyst/Effects/PlatformTouchEffect.cs: -------------------------------------------------------------------------------- 1 | using CoreGraphics; 2 | using Foundation; 3 | using Microsoft.Maui.Controls.Platform; 4 | using OxyPlot.Maui.Skia.Effects; 5 | using UIKit; 6 | 7 | namespace OxyPlot.Maui.Skia.macOS.Effects; 8 | 9 | public class PlatformTouchEffect : PlatformEffect 10 | { 11 | private UIView _view; 12 | private TouchRecognizer _touchRecognizer; 13 | private MyTouchEffect _touchEffect; 14 | protected override void OnAttached() 15 | { 16 | _view = Control ?? Container; 17 | _touchEffect = Element.Effects.OfType().FirstOrDefault(); 18 | 19 | if (_touchEffect == null || _view == null) return; 20 | 21 | _touchRecognizer = new TouchRecognizer(Element, _touchEffect); 22 | _view.AddGestureRecognizer(_touchRecognizer); 23 | _view.AddGestureRecognizer(GetMouseWheelRecognizer(_view)); 24 | } 25 | 26 | protected override void OnDetached() 27 | { 28 | if (_touchRecognizer != null) 29 | { 30 | _touchRecognizer.Detach(); 31 | _view.RemoveGestureRecognizer(_touchRecognizer); 32 | } 33 | } 34 | 35 | // https://github.com/dotnet/maui/issues/16130 36 | private UIPanGestureRecognizer GetMouseWheelRecognizer(UIView v) 37 | { 38 | return new UIPanGestureRecognizer((e) => 39 | { 40 | if (e.State == UIGestureRecognizerState.Ended) 41 | return; 42 | 43 | var isZoom = e.NumberOfTouches == 0; 44 | if (!isZoom) return; 45 | 46 | var l = e.LocationInView(v); 47 | var t = e.TranslationInView(v); 48 | var deltaX = t.X / 2; 49 | var deltaY = t.Y / 2; 50 | var delta = deltaY != 0 ? deltaY : deltaX; 51 | 52 | var tolerance = 5; 53 | if (Math.Abs(delta) < tolerance) return; 54 | 55 | var pointerX = l.X - t.X; 56 | var pointerY = l.Y - t.Y; 57 | var locations = new[] { new Point(pointerX, pointerY) }; 58 | 59 | var eventArgs = new TouchActionEventArgs(0, TouchActionType.MouseWheel, locations, false) 60 | { 61 | MouseWheelDelta = (int)delta 62 | }; 63 | 64 | _touchEffect.OnTouchAction(Element, eventArgs); 65 | }) 66 | { 67 | AllowedScrollTypesMask = UIScrollTypeMask.Discrete | UIScrollTypeMask.Continuous, 68 | MinimumNumberOfTouches = 0, 69 | ShouldRecognizeSimultaneously = (_, _) => true 70 | }; 71 | } 72 | } 73 | 74 | internal class TouchRecognizer : UIGestureRecognizer 75 | { 76 | private readonly Microsoft.Maui.Controls.Element _element; 77 | private readonly MyTouchEffect _touchEffect; 78 | private uint _activeTouchesCount = 0; 79 | 80 | public TouchRecognizer(Microsoft.Maui.Controls.Element element, MyTouchEffect touchEffect) 81 | { 82 | this._element = element; 83 | this._touchEffect = touchEffect; 84 | ShouldRecognizeSimultaneously = (_, _) => true; 85 | } 86 | 87 | public void Detach() 88 | { 89 | ShouldRecognizeSimultaneously = null; 90 | } 91 | 92 | public override void TouchesBegan(NSSet touches, UIEvent evt) 93 | { 94 | base.TouchesBegan(touches, evt); 95 | _activeTouchesCount += touches.Count.ToUInt32(); 96 | FireEvent(touches, TouchActionType.Pressed, true); 97 | } 98 | 99 | public override void TouchesMoved(NSSet touches, UIEvent evt) 100 | { 101 | base.TouchesMoved(touches, evt); 102 | 103 | if (_activeTouchesCount == touches.Count.ToUInt32()) 104 | { 105 | FireEvent(touches, TouchActionType.Moved, true); 106 | } 107 | } 108 | 109 | public override void TouchesEnded(NSSet touches, UIEvent evt) 110 | { 111 | base.TouchesEnded(touches, evt); 112 | _activeTouchesCount -= touches.Count.ToUInt32(); 113 | FireEvent(touches, TouchActionType.Released, false); 114 | } 115 | 116 | private void FireEvent(NSSet touches, TouchActionType actionType, bool isInContact) 117 | { 118 | UITouch[] uiTouches = touches.Cast().ToArray(); 119 | long id = ((IntPtr)uiTouches.First().Handle).ToInt64(); 120 | Point[] points = new Point[uiTouches.Length]; 121 | 122 | for (int i = 0; i < uiTouches.Length; i++) 123 | { 124 | CGPoint cgPoint = uiTouches[i].LocationInView(View); 125 | points[i] = new(cgPoint.X, cgPoint.Y); 126 | } 127 | _touchEffect.OnTouchAction(_element, new(id, actionType, points, isInContact)); 128 | } 129 | } -------------------------------------------------------------------------------- /Source/OxyPlot.Maui.Skia/Platforms/MacCatalyst/MauiFontLoader.cs: -------------------------------------------------------------------------------- 1 | using OxyPlot.Maui.Skia.Fonts; 2 | 3 | namespace OxyPlot.Maui.Skia.mac; 4 | 5 | public class MauiFontLoader : IMauiFontLoader 6 | { 7 | private IFontRegistrar fontRegistrar = null; 8 | 9 | /// 10 | public Stream Load(string fontName) 11 | { 12 | if (fontRegistrar == null) 13 | fontRegistrar = IPlatformApplication.Current.Services.GetRequiredService(); 14 | 15 | fontName = fontRegistrar.GetFont(fontName); 16 | if (File.Exists(fontName)) 17 | { 18 | return File.OpenRead(fontName); 19 | } 20 | 21 | try 22 | { 23 | var resolvedFilename = ResolveFileSystemFont(fontName); 24 | if (!string.IsNullOrEmpty(resolvedFilename) && File.Exists(resolvedFilename)) 25 | { 26 | return File.OpenRead(resolvedFilename); 27 | } 28 | } 29 | catch (Exception e) 30 | { 31 | Console.WriteLine(e); 32 | return null; 33 | } 34 | 35 | return null; 36 | } 37 | 38 | string ResolveFileSystemFont(string filename) 39 | { 40 | var mainBundlePath = Foundation.NSBundle.MainBundle.BundlePath; 41 | 42 | #if MACCATALYST 43 | // macOS Apps have Contents folder in the bundle root, iOS does not 44 | mainBundlePath = Path.Combine(mainBundlePath, "Contents"); 45 | #endif 46 | 47 | var fontBundlePath = Path.Combine(mainBundlePath, filename); 48 | if (File.Exists(fontBundlePath)) 49 | return fontBundlePath; 50 | 51 | fontBundlePath = Path.Combine(mainBundlePath, "Resources", filename); 52 | if (File.Exists(fontBundlePath)) 53 | return fontBundlePath; 54 | 55 | fontBundlePath = Path.Combine(mainBundlePath, "Fonts", filename); 56 | if (File.Exists(fontBundlePath)) 57 | return fontBundlePath; 58 | 59 | fontBundlePath = Path.Combine(mainBundlePath, "Resources", "Fonts", filename); 60 | if (File.Exists(fontBundlePath)) 61 | return fontBundlePath; 62 | 63 | // TODO: check other folders as well 64 | 65 | return null; 66 | } 67 | 68 | } -------------------------------------------------------------------------------- /Source/OxyPlot.Maui.Skia/Platforms/Tizen/PlatformClass1.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace OxyPlot.Maui.Skia 4 | { 5 | // All the code in this file is only included on Tizen. 6 | public class PlatformClass1 7 | { 8 | } 9 | } -------------------------------------------------------------------------------- /Source/OxyPlot.Maui.Skia/Platforms/Windows/Effects/PlatformTouchEffect.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Maui.Controls.Platform; 2 | using Microsoft.UI.Xaml; 3 | using Microsoft.UI.Xaml.Input; 4 | using OxyPlot.Maui.Skia.Effects; 5 | 6 | namespace OxyPlot.Maui.Skia.Windows.Effects; 7 | 8 | public class PlatformTouchEffect : PlatformEffect 9 | { 10 | FrameworkElement frameworkElement; 11 | Action onTouchAction; 12 | 13 | protected override void OnAttached() 14 | { 15 | // Get the Windows FrameworkElement corresponding to the Element that the effect is attached to 16 | frameworkElement = Control == null ? Container : Control; 17 | 18 | // Get access to the TouchEffect class in the .NET Standard library 19 | var touchEffect = Element.Effects.OfType().FirstOrDefault(); 20 | 21 | if (touchEffect != null && frameworkElement != null) 22 | { 23 | // Save the method to call on touch events 24 | onTouchAction = touchEffect.OnTouchAction; 25 | 26 | // Set event handlers on FrameworkElement 27 | frameworkElement.PointerPressed += OnPointerPressed; 28 | frameworkElement.PointerMoved += OnPointerMoved; 29 | frameworkElement.PointerReleased += OnPointerReleased; 30 | frameworkElement.PointerWheelChanged += FrameworkElement_PointerWheelChanged; 31 | } 32 | } 33 | 34 | private void FrameworkElement_PointerWheelChanged(object sender, PointerRoutedEventArgs args) 35 | { 36 | CommonHandler(sender, TouchActionType.MouseWheel, args); 37 | } 38 | 39 | protected override void OnDetached() 40 | { 41 | if (onTouchAction != null) 42 | { 43 | frameworkElement.PointerPressed -= OnPointerPressed; 44 | frameworkElement.PointerMoved -= OnPointerMoved; 45 | frameworkElement.PointerReleased -= OnPointerReleased; 46 | frameworkElement.PointerWheelChanged -= FrameworkElement_PointerWheelChanged; 47 | } 48 | } 49 | 50 | private bool _pressed = false; 51 | void OnPointerPressed(object sender, PointerRoutedEventArgs args) 52 | { 53 | _pressed = true; 54 | CommonHandler(sender, TouchActionType.Pressed, args); 55 | } 56 | 57 | void OnPointerMoved(object sender, PointerRoutedEventArgs args) 58 | { 59 | if (_pressed) 60 | CommonHandler(sender, TouchActionType.Moved, args); 61 | } 62 | 63 | void OnPointerReleased(object sender, PointerRoutedEventArgs args) 64 | { 65 | _pressed = false; 66 | CommonHandler(sender, TouchActionType.Released, args); 67 | } 68 | 69 | void CommonHandler(object sender, TouchActionType touchActionType, PointerRoutedEventArgs args) 70 | { 71 | var pointerPoint = args.GetCurrentPoint(sender as UIElement); 72 | var windowsPoint = pointerPoint.Position; 73 | var touchArgs = new TouchActionEventArgs(args.Pointer.PointerId, 74 | touchActionType, 75 | new Point[] { new(windowsPoint.X, windowsPoint.Y) }, 76 | args.Pointer.IsInContact) 77 | { 78 | ModifierKeys = args.KeyModifiers.ToOxyModifierKeys() 79 | }; 80 | 81 | if (touchActionType == TouchActionType.MouseWheel) 82 | { 83 | touchArgs.MouseWheelDelta = pointerPoint.Properties.MouseWheelDelta; 84 | } 85 | 86 | onTouchAction(Element, touchArgs); 87 | } 88 | } -------------------------------------------------------------------------------- /Source/OxyPlot.Maui.Skia/Platforms/Windows/MauiFontLoader.cs: -------------------------------------------------------------------------------- 1 | using Windows.Storage; 2 | using OxyPlot.Maui.Skia.Fonts; 3 | 4 | namespace OxyPlot.Maui.Skia.Windows; 5 | 6 | public class MauiFontLoader : IMauiFontLoader 7 | { 8 | private IFontRegistrar _fontRegistrar = null; 9 | 10 | /// 11 | public Stream Load(string fontName) 12 | { 13 | if (_fontRegistrar == null) 14 | _fontRegistrar = IPlatformApplication.Current.Services.GetRequiredService(); 15 | 16 | fontName = _fontRegistrar.GetFont(fontName); 17 | if (File.Exists(fontName)) 18 | { 19 | return File.OpenRead(fontName); 20 | } 21 | 22 | try 23 | { 24 | var file = StorageFile.GetFileFromApplicationUriAsync(new Uri(fontName)).AsTask().Result; 25 | return file.OpenReadAsync().AsTask().Result.AsStream(); 26 | } 27 | catch (Exception e) 28 | { 29 | Console.WriteLine(e); 30 | return null; 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /Source/OxyPlot.Maui.Skia/Platforms/Windows/ModifierKeyExt.cs: -------------------------------------------------------------------------------- 1 | using Windows.System; 2 | 3 | namespace OxyPlot.Maui.Skia.Windows; 4 | 5 | public static class ModifierKeyExt 6 | { 7 | public static OxyModifierKeys ToOxyModifierKeys(this VirtualKeyModifiers vkm) 8 | { 9 | var modifiers = OxyModifierKeys.None; 10 | 11 | if (vkm.HasFlag(VirtualKeyModifiers.Shift)) 12 | { 13 | modifiers |= OxyModifierKeys.Shift; 14 | } 15 | 16 | if (vkm.HasFlag(VirtualKeyModifiers.Control)) 17 | { 18 | modifiers |= OxyModifierKeys.Control; 19 | } 20 | 21 | if (vkm.HasFlag(VirtualKeyModifiers.Menu)) 22 | { 23 | modifiers |= OxyModifierKeys.Alt; 24 | } 25 | 26 | if (vkm.HasFlag(VirtualKeyModifiers.Windows)) 27 | { 28 | modifiers |= OxyModifierKeys.Windows; 29 | } 30 | 31 | return modifiers; 32 | } 33 | } -------------------------------------------------------------------------------- /Source/OxyPlot.Maui.Skia/Platforms/iOS/Effects/PlatformTouchEffect.cs: -------------------------------------------------------------------------------- 1 | using CoreGraphics; 2 | using Foundation; 3 | using Microsoft.Maui.Controls.Platform; 4 | using OxyPlot.Maui.Skia.Effects; 5 | using UIKit; 6 | 7 | namespace OxyPlot.Maui.Skia.ios.Effects; 8 | 9 | public class PlatformTouchEffect : PlatformEffect 10 | { 11 | private UIView view; 12 | private TouchRecognizer touchRecognizer; 13 | 14 | protected override void OnAttached() 15 | { 16 | view = Control ?? Container; 17 | 18 | var touchEffect = Element.Effects.OfType().FirstOrDefault(); 19 | 20 | if (touchEffect != null && view != null) 21 | { 22 | touchRecognizer = new TouchRecognizer(Element, touchEffect); 23 | view.AddGestureRecognizer(touchRecognizer); 24 | } 25 | } 26 | 27 | protected override void OnDetached() 28 | { 29 | if (touchRecognizer != null) 30 | { 31 | touchRecognizer.Detach(); 32 | view.RemoveGestureRecognizer(touchRecognizer); 33 | } 34 | } 35 | } 36 | 37 | internal class TouchRecognizer : UIGestureRecognizer 38 | { 39 | private readonly Microsoft.Maui.Controls.Element element; 40 | private readonly MyTouchEffect touchEffect; 41 | private uint activeTouchesCount = 0; 42 | 43 | public TouchRecognizer(Microsoft.Maui.Controls.Element element, MyTouchEffect touchEffect) 44 | { 45 | this.element = element; 46 | this.touchEffect = touchEffect; 47 | 48 | ShouldRecognizeSimultaneously = new UIGesturesProbe((_, _) => true); 49 | } 50 | 51 | public void Detach() 52 | { 53 | ShouldRecognizeSimultaneously = null; 54 | } 55 | 56 | public override void TouchesBegan(NSSet touches, UIEvent evt) 57 | { 58 | base.TouchesBegan(touches, evt); 59 | activeTouchesCount += touches.Count.ToUInt32(); 60 | FireEvent(touches, TouchActionType.Pressed, true); 61 | } 62 | 63 | public override void TouchesMoved(NSSet touches, UIEvent evt) 64 | { 65 | base.TouchesMoved(touches, evt); 66 | 67 | if (activeTouchesCount == touches.Count.ToUInt32()) 68 | { 69 | FireEvent(touches, TouchActionType.Moved, true); 70 | } 71 | } 72 | 73 | public override void TouchesEnded(NSSet touches, UIEvent evt) 74 | { 75 | base.TouchesEnded(touches, evt); 76 | activeTouchesCount -= touches.Count.ToUInt32(); 77 | FireEvent(touches, TouchActionType.Released, false); 78 | } 79 | 80 | public override void TouchesCancelled(NSSet touches, UIEvent evt) 81 | { 82 | base.TouchesCancelled(touches, evt); 83 | } 84 | 85 | private void FireEvent(NSSet touches, TouchActionType actionType, bool isInContact) 86 | { 87 | UITouch[] uiTouches = touches.Cast().ToArray(); 88 | long id = ((IntPtr)uiTouches.First().Handle).ToInt64(); 89 | Point[] points = new Point[uiTouches.Length]; 90 | 91 | for (int i = 0; i < uiTouches.Length; i++) 92 | { 93 | CGPoint cgPoint = uiTouches[i].LocationInView(View); 94 | points[i] = new(cgPoint.X, cgPoint.Y); 95 | } 96 | touchEffect.OnTouchAction(element, new(id, actionType, points, isInContact)); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Source/OxyPlot.Maui.Skia/Platforms/iOS/MauiFontLoader.cs: -------------------------------------------------------------------------------- 1 | using OxyPlot.Maui.Skia.Fonts; 2 | 3 | namespace OxyPlot.Maui.Skia.ios; 4 | 5 | public class MauiFontLoader : IMauiFontLoader 6 | { 7 | private IFontRegistrar fontRegistrar = null; 8 | 9 | /// 10 | public Stream Load(string fontName) 11 | { 12 | if (fontRegistrar == null) 13 | fontRegistrar = IPlatformApplication.Current.Services.GetRequiredService(); 14 | 15 | fontName = fontRegistrar.GetFont(fontName); 16 | if (File.Exists(fontName)) 17 | { 18 | return File.OpenRead(fontName); 19 | } 20 | 21 | try 22 | { 23 | var resolvedFilename = ResolveFileSystemFont(fontName); 24 | if (!string.IsNullOrEmpty(resolvedFilename) && File.Exists(resolvedFilename)) 25 | { 26 | return File.OpenRead(resolvedFilename); 27 | } 28 | } 29 | catch (Exception e) 30 | { 31 | Console.WriteLine(e); 32 | return null; 33 | } 34 | 35 | return null; 36 | } 37 | 38 | string ResolveFileSystemFont(string filename) 39 | { 40 | var mainBundlePath = Foundation.NSBundle.MainBundle.BundlePath; 41 | 42 | #if MACCATALYST 43 | // macOS Apps have Contents folder in the bundle root, iOS does not 44 | mainBundlePath = Path.Combine(mainBundlePath, "Contents"); 45 | #endif 46 | 47 | var fontBundlePath = Path.Combine(mainBundlePath, filename); 48 | if (File.Exists(fontBundlePath)) 49 | return fontBundlePath; 50 | 51 | fontBundlePath = Path.Combine(mainBundlePath, "Resources", filename); 52 | if (File.Exists(fontBundlePath)) 53 | return fontBundlePath; 54 | 55 | fontBundlePath = Path.Combine(mainBundlePath, "Fonts", filename); 56 | if (File.Exists(fontBundlePath)) 57 | return fontBundlePath; 58 | 59 | fontBundlePath = Path.Combine(mainBundlePath, "Resources", "Fonts", filename); 60 | if (File.Exists(fontBundlePath)) 61 | return fontBundlePath; 62 | 63 | // TODO: check other folders as well 64 | 65 | return null; 66 | } 67 | 68 | } -------------------------------------------------------------------------------- /Source/OxyPlot.Maui.Skia/PlotCommands.cs: -------------------------------------------------------------------------------- 1 | namespace OxyPlot.Maui.Skia; 2 | 3 | public class PlotCommands 4 | { 5 | /// 6 | /// Gets the pan/zoom touch command. 7 | /// 8 | public static IViewCommand PanZoomByTouch { get; private set; } 9 | 10 | /// 11 | /// Gets the pan(axis only)/zoom touch command. 12 | /// 13 | public static IViewCommand PanZoomByTouchAxisOnly { get; private set; } 14 | 15 | /// 16 | /// Gets the pan(by two finger)/zoom touch command. 17 | /// 18 | public static IViewCommand PanZoomByTouchTwoFinger { get; private set; } 19 | 20 | /// 21 | /// Gets the snap tracker command. 22 | /// 23 | public static IViewCommand SnapTrackTouch { get; private set; } 24 | 25 | static PlotCommands() 26 | { 27 | PanZoomByTouch = new DelegatePlotCommand((view, controller, args) => controller.AddTouchManipulator(view, new Manipulators.TouchManipulator(view), args)); 28 | PanZoomByTouchAxisOnly = new DelegatePlotCommand((view, controller, args) => controller.AddTouchManipulator(view, new Manipulators.TouchManipulator(view) { IsOnlyAcceptAxisPan = true }, args)); 29 | PanZoomByTouchTwoFinger = new DelegatePlotCommand((view, controller, args) => controller.AddTouchManipulator(view, new Manipulators.TouchManipulator(view) { IsPanByTowFinger = true }, args)); 30 | SnapTrackTouch = new DelegatePlotCommand((view, controller, args) => controller.AddTouchManipulator(view, new Manipulators.TouchTrackerManipulator(view) { Snap = true, PointsOnly = true, LockToInitialSeries = false }, args)); 31 | } 32 | } -------------------------------------------------------------------------------- /Source/OxyPlot.Maui.Skia/PlotController.cs: -------------------------------------------------------------------------------- 1 | using OxyPlot.Maui.Skia.Core; 2 | 3 | namespace OxyPlot.Maui.Skia; 4 | 5 | public class PlotController : ControllerBase, IPlotController 6 | { 7 | /// 8 | /// Initializes a new instance of the class. 9 | /// 10 | public PlotController() 11 | { 12 | var cmd = new CompositeDelegateViewCommand( 13 | PlotCommands.SnapTrackTouch, 14 | PlotCommands.PanZoomByTouch 15 | ); 16 | 17 | this.BindTouchDown(cmd); 18 | 19 | #if WINDOWS 20 | this.BindMouseWheel(OxyPlot.PlotCommands.ZoomWheel); 21 | this.BindMouseWheel(OxyModifierKeys.Control, OxyPlot.PlotCommands.ZoomWheelFine); 22 | #endif 23 | #if MACCATALYST 24 | this.BindMouseWheel(OxyPlot.PlotCommands.ZoomWheel); 25 | #endif 26 | } 27 | } -------------------------------------------------------------------------------- /Source/OxyPlot.Maui.Skia/PlotView.cs: -------------------------------------------------------------------------------- 1 | namespace OxyPlot.Maui.Skia; 2 | 3 | /// 4 | /// Represents a control that displays a . This is based on . 5 | /// 6 | public class PlotView : PlotViewBase 7 | { 8 | public PlotView() 9 | { 10 | this.Controller = new PlotController(); 11 | } 12 | 13 | /// 14 | /// Gets the SkiaRenderContext. 15 | /// 16 | private SkiaRenderContext SkiaRenderContext => (SkiaRenderContext)this.renderContext; 17 | 18 | /// 19 | protected override void ClearBackground() 20 | { 21 | var color = this.ActualModel?.Background.IsVisible() == true 22 | ? this.ActualModel.Background.ToSKColor() 23 | : SKColors.Empty; 24 | 25 | this.SkiaRenderContext.SkCanvas.Clear(color); 26 | } 27 | 28 | private SKCanvasView _plotPresenter; 29 | 30 | /// 31 | protected override View CreatePlotPresenter() 32 | { 33 | _plotPresenter = new SKCanvasView(); 34 | _plotPresenter.PaintSurface += this.SkElement_PaintSurface; 35 | return _plotPresenter; 36 | } 37 | 38 | /// 39 | protected override IRenderContext CreateRenderContext() 40 | { 41 | return new SkiaRenderContext(); 42 | } 43 | 44 | /// 45 | protected override void RenderOverride() 46 | { 47 | // Instead of rendering directly, invalidate the plot presenter. 48 | // Actual rendering is done in SkElement_PaintSurface. 49 | try 50 | { 51 | _plotPresenter.InvalidateSurface(); 52 | } 53 | catch (Exception e) 54 | { 55 | Console.WriteLine(e); 56 | } 57 | } 58 | 59 | /// 60 | protected override double UpdateDpi() 61 | { 62 | var scale = base.UpdateDpi(); 63 | this.SkiaRenderContext.DpiScale = (float)scale; 64 | return scale; 65 | } 66 | 67 | /// 68 | /// This is called when the SKElement paints its surface. 69 | /// 70 | /// The sender. 71 | /// The surface paint event args. 72 | private void SkElement_PaintSurface(object sender, SKPaintSurfaceEventArgs e) 73 | { 74 | this.SkiaRenderContext.SkCanvas = e.Surface.Canvas; 75 | base.RenderOverride(); 76 | this.SkiaRenderContext.SkCanvas = null; 77 | } 78 | } -------------------------------------------------------------------------------- /Source/OxyPlot.Maui.Skia/PlotViewBase.Events.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // Copyright (c) 2020 OxyPlot contributors 4 | // 5 | // -------------------------------------------------------------------------------------------------------------------- 6 | 7 | using OxyPlot.Maui.Skia.Effects; 8 | 9 | namespace OxyPlot.Maui.Skia; 10 | 11 | /// 12 | /// Base class for MAUI PlotView implementations. 13 | /// 14 | public abstract partial class PlotViewBase 15 | { 16 | /// 17 | /// The touch points of the previous touch event. 18 | /// 19 | private ScreenPoint[] previousTouchPoints; 20 | 21 | private void AddTouchEffect() 22 | { 23 | var touchEffect = new MyTouchEffect(); 24 | touchEffect.TouchAction += TouchEffect_TouchAction; 25 | if (!InputTransparent) 26 | { 27 | this.Effects.Add(touchEffect); 28 | } 29 | this.PropertyChanged += (_, args) => 30 | { 31 | if (args.PropertyName is null) return; 32 | if (args.PropertyName != nameof(InputTransparent)) return; 33 | if (InputTransparent) 34 | { 35 | if (this.Effects.Contains(touchEffect)) 36 | { 37 | this.Effects.Remove(touchEffect); 38 | } 39 | } 40 | else 41 | { 42 | if (!this.Effects.Contains(touchEffect)) 43 | { 44 | this.Effects.Add(touchEffect); 45 | } 46 | 47 | } 48 | }; 49 | } 50 | 51 | private void TouchEffect_TouchAction(object sender, TouchActionEventArgs e) 52 | { 53 | switch (e.Type) 54 | { 55 | case TouchActionType.Pressed: 56 | OnTouchDownEvent(e); 57 | break; 58 | case TouchActionType.Moved: 59 | OnTouchMoveEvent(e); 60 | break; 61 | case TouchActionType.Released: 62 | OnTouchUpEvent(e); 63 | break; 64 | case TouchActionType.MouseWheel: 65 | OnMouseWheelEvent(e); 66 | break; 67 | default: 68 | throw new ArgumentOutOfRangeException(); 69 | } 70 | } 71 | 72 | /// 73 | /// Handles touch down events. 74 | /// 75 | /// The motion event arguments. 76 | /// true if the event was handled. 77 | private bool OnTouchDownEvent(TouchActionEventArgs e) 78 | { 79 | var args = ToTouchEventArgs(e, Scale); 80 | var handled = this.ActualController.HandleTouchStarted(this, args); 81 | this.previousTouchPoints = GetTouchPoints(e, Scale); 82 | return handled; 83 | } 84 | 85 | /// 86 | /// Handles touch move events. 87 | /// 88 | /// The motion event arguments. 89 | /// true if the event was handled. 90 | private bool OnTouchMoveEvent(TouchActionEventArgs e) 91 | { 92 | var currentTouchPoints = GetTouchPoints(e, Scale); 93 | var args = new MauiOxyTouchEventArgs(currentTouchPoints, this.previousTouchPoints); 94 | var handled = this.ActualController.HandleTouchDelta(this, args); 95 | this.previousTouchPoints = currentTouchPoints; 96 | return handled; 97 | } 98 | 99 | /// 100 | /// Handles touch released events. 101 | /// 102 | /// The motion event arguments. 103 | /// true if the event was handled. 104 | private bool OnTouchUpEvent(TouchActionEventArgs e) 105 | { 106 | return this.ActualController.HandleTouchCompleted(this, ToTouchEventArgs(e, Scale)); 107 | } 108 | 109 | /// 110 | /// Handles Mouse Wheel events. 111 | /// 112 | /// The motion event arguments. 113 | /// true if the event was handled. 114 | private bool OnMouseWheelEvent(TouchActionEventArgs e) 115 | { 116 | var args = new OxyMouseWheelEventArgs 117 | { 118 | Position = new ScreenPoint(e.Location.X / Scale, e.Location.Y / Scale), 119 | Delta = e.MouseWheelDelta, 120 | ModifierKeys = e.ModifierKeys 121 | }; 122 | return this.ActualController.HandleMouseWheel(this, args); 123 | } 124 | 125 | /// 126 | /// Converts an to a . 127 | /// 128 | /// The event arguments. 129 | /// The resolution scale factor. 130 | /// The converted event arguments. 131 | public static OxyTouchEventArgs ToTouchEventArgs(TouchActionEventArgs e, double scale) 132 | { 133 | return new MauiOxyTouchEventArgs 134 | { 135 | Position = new ScreenPoint(e.Location.X / scale, e.Location.Y / scale), 136 | DeltaTranslation = new ScreenVector(0, 0), 137 | DeltaScale = new ScreenVector(1, 1), 138 | PointerCount = e.Locations.Length 139 | }; 140 | } 141 | 142 | /// 143 | /// Gets the touch points from the specified argument. 144 | /// 145 | /// The event arguments. 146 | /// The resolution scale factor. 147 | /// The touch points. 148 | public static ScreenPoint[] GetTouchPoints(TouchActionEventArgs e, double scale) 149 | { 150 | var result = new ScreenPoint[e.Locations.Length]; 151 | for (int i = 0; i < e.Locations.Length; i++) 152 | { 153 | result[i] = new ScreenPoint(e.Locations[i].X / scale, e.Locations[i].Y / scale); 154 | } 155 | 156 | return result; 157 | } 158 | } -------------------------------------------------------------------------------- /Source/OxyPlot.Maui.Skia/PlotViewBase.Properties.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // Copyright (c) 2020 OxyPlot contributors 4 | // 5 | // -------------------------------------------------------------------------------------------------------------------- 6 | 7 | namespace OxyPlot.Maui.Skia; 8 | 9 | /// 10 | /// Base class for MAUI PlotView implementations. 11 | /// 12 | public abstract partial class PlotViewBase 13 | { 14 | /// 15 | /// Identifies the dependency property. 16 | /// 17 | public static readonly BindableProperty ControllerProperty = 18 | BindableProperty.Create(nameof(Controller), typeof(IPlotController), typeof(PlotViewBase)); 19 | 20 | /// 21 | /// Identifies the dependency property. 22 | /// 23 | public static readonly BindableProperty DefaultTrackerTemplateProperty = 24 | BindableProperty.Create( 25 | nameof(DefaultTrackerTemplate), typeof(ControlTemplate), typeof(PlotViewBase)); 26 | 27 | /// 28 | /// Identifies the dependency property. 29 | /// 30 | public static readonly BindableProperty IsMouseWheelEnabledProperty = 31 | BindableProperty.Create(nameof(IsMouseWheelEnabled), typeof(bool), typeof(PlotViewBase), true); 32 | 33 | /// 34 | /// Identifies the dependency property. 35 | /// 36 | public static readonly BindableProperty ModelProperty = 37 | BindableProperty.Create(nameof(Model), typeof(PlotModel), typeof(PlotViewBase), null, propertyChanged: ModelChanged); 38 | 39 | /// 40 | /// Identifies the dependency property. 41 | /// 42 | public static readonly BindableProperty ZoomRectangleTemplateProperty = 43 | BindableProperty.Create( 44 | nameof(ZoomRectangleTemplate), typeof(ControlTemplate), typeof(PlotViewBase)); 45 | 46 | /// 47 | /// Gets or sets the Plot controller. 48 | /// 49 | /// The Plot controller. 50 | public IPlotController Controller 51 | { 52 | get => (IPlotController)this.GetValue(ControllerProperty); 53 | set => this.SetValue(ControllerProperty, value); 54 | } 55 | 56 | /// 57 | /// Gets or sets the default tracker template. 58 | /// 59 | public ControlTemplate DefaultTrackerTemplate 60 | { 61 | get => (ControlTemplate)this.GetValue(DefaultTrackerTemplateProperty); 62 | set => this.SetValue(DefaultTrackerTemplateProperty, value); 63 | } 64 | 65 | /// 66 | /// Gets or sets a value indicating whether IsMouseWheelEnabled. 67 | /// 68 | public bool IsMouseWheelEnabled 69 | { 70 | get => (bool)this.GetValue(IsMouseWheelEnabledProperty); 71 | set => this.SetValue(IsMouseWheelEnabledProperty, value); 72 | } 73 | 74 | /// 75 | /// Gets or sets the model. 76 | /// 77 | /// The model. 78 | public PlotModel Model 79 | { 80 | get => (PlotModel)this.GetValue(ModelProperty); 81 | set => this.SetValue(ModelProperty, value); 82 | } 83 | 84 | /// 85 | /// Gets or sets the zoom rectangle template. 86 | /// 87 | /// The zoom rectangle template. 88 | public ControlTemplate ZoomRectangleTemplate 89 | { 90 | get => (ControlTemplate)this.GetValue(ZoomRectangleTemplateProperty); 91 | set => this.SetValue(ZoomRectangleTemplateProperty, value); 92 | } 93 | } -------------------------------------------------------------------------------- /Source/OxyPlot.Maui.Skia/PlotViewBase.cs: -------------------------------------------------------------------------------- 1 | using OxyPlot.Maui.Skia.Core; 2 | 3 | namespace OxyPlot.Maui.Skia; 4 | 5 | public abstract partial class PlotViewBase : BaseTemplatedView, IPlotView 6 | { 7 | public event Action UpdateStarted; 8 | public event Action UpdateFinished; 9 | public event Action RenderStarted; 10 | public event Action RenderFinished; 11 | 12 | private int mainThreadId = 1; 13 | 14 | protected override void OnControlInitialized(Grid control) 15 | { 16 | this.grid = control; 17 | mainThreadId = Thread.CurrentThread.ManagedThreadId; 18 | ApplyTemplate(); 19 | AddTouchEffect(); 20 | } 21 | 22 | /// 23 | /// The grid. 24 | /// 25 | protected Grid grid; 26 | 27 | /// 28 | /// The plot presenter. 29 | /// 30 | protected View plotPresenter; 31 | 32 | /// 33 | /// The render context 34 | /// 35 | protected IRenderContext renderContext; 36 | 37 | /// 38 | /// The model lock. 39 | /// 40 | private readonly object modelLock = new object(); 41 | 42 | /// 43 | /// The current tracker. 44 | /// 45 | private View currentTracker; 46 | 47 | /// 48 | /// The current tracker template. 49 | /// 50 | private ControlTemplate currentTrackerTemplate; 51 | 52 | /// 53 | /// The default plot controller. 54 | /// 55 | private IPlotController defaultController; 56 | 57 | /// 58 | /// Indicates whether the was in the visual tree the last time was called. 59 | /// 60 | private bool isInVisualTree; 61 | 62 | /// 63 | /// The overlays. 64 | /// 65 | private AbsoluteLayout overlays; 66 | 67 | /// 68 | /// The zoom control. 69 | /// 70 | private ContentView zoomControl; 71 | 72 | /// 73 | /// Initializes a new instance of the class. 74 | /// 75 | protected PlotViewBase() 76 | { 77 | TrackerDefinitions = new ObservableCollection(); 78 | 79 | DefaultTrackerTemplate = new ControlTemplate(() => 80 | { 81 | var tc = new TrackerControl(); 82 | tc.SetBinding(TrackerControl.PositionProperty, "Position"); 83 | tc.SetBinding(TrackerControl.LineExtentsProperty, "PlotModel.PlotArea"); 84 | tc.Content = TrackerControl.DefaultTrackerTemplateContentProvider(); 85 | return tc; 86 | }); 87 | SizeChanged += OnSizeUpdated; 88 | } 89 | 90 | /// 91 | /// Gets the actual PlotView controller. 92 | /// 93 | /// The actual PlotView controller. 94 | public IPlotController ActualController => this.Controller ?? (this.defaultController ??= new OxyPlot.PlotController()); 95 | 96 | /// 97 | IController IView.ActualController => this.ActualController; 98 | 99 | /// 100 | /// Gets the actual model. 101 | /// 102 | /// The actual model. 103 | public PlotModel ActualModel { get; private set; } 104 | 105 | /// 106 | Model IView.ActualModel => this.ActualModel; 107 | 108 | /// 109 | /// Gets the coordinates of the client area of the view. 110 | /// 111 | public OxyRect ClientArea => new OxyRect(0, 0, this.Width, this.Height); 112 | 113 | /// 114 | /// Gets the tracker definitions. 115 | /// 116 | /// The tracker definitions. 117 | public ObservableCollection TrackerDefinitions { get; } 118 | 119 | /// 120 | /// Hides the tracker. 121 | /// 122 | public void HideTracker() 123 | { 124 | if (this.currentTracker != null) 125 | { 126 | this.overlays.Children.Remove(this.currentTracker); 127 | this.currentTracker = null; 128 | this.currentTrackerTemplate = null; 129 | } 130 | } 131 | 132 | /// 133 | /// Hides the zoom rectangle. 134 | /// 135 | public void HideZoomRectangle() 136 | { 137 | this.zoomControl.IsVisible = false; 138 | } 139 | 140 | /// 141 | /// Invalidate the PlotView (not blocking the UI thread) 142 | /// 143 | /// The update Data. 144 | public void InvalidatePlot(bool updateData = true) 145 | { 146 | if (this.ActualModel == null) 147 | { 148 | return; 149 | } 150 | 151 | UpdateStarted?.Invoke(); 152 | 153 | lock (this.ActualModel.SyncRoot) 154 | { 155 | ((IPlotModel)this.ActualModel).Update(updateData); 156 | } 157 | 158 | UpdateFinished?.Invoke(); 159 | 160 | this.BeginInvoke(this.Render); 161 | } 162 | 163 | private void ApplyTemplate() 164 | { 165 | if (this.grid == null) 166 | { 167 | return; 168 | } 169 | 170 | this.plotPresenter = this.CreatePlotPresenter(); 171 | this.grid.Children.Add(this.plotPresenter); 172 | 173 | this.renderContext = this.CreateRenderContext(); 174 | 175 | this.overlays = new AbsoluteLayout(); 176 | this.grid.Children.Add(this.overlays); 177 | 178 | this.zoomControl = new ContentView(); 179 | this.overlays.Children.Add(this.zoomControl); 180 | } 181 | 182 | /// 183 | /// Pans all axes. 184 | /// 185 | public void PanAllAxes(double deltaX, double deltaY) 186 | { 187 | if (this.ActualModel != null) 188 | { 189 | this.ActualModel.PanAllAxes(deltaX, deltaY); 190 | } 191 | 192 | this.InvalidatePlot(false); 193 | } 194 | 195 | /// 196 | /// Resets all axes. 197 | /// 198 | public void ResetAllAxes() 199 | { 200 | if (this.ActualModel != null) 201 | { 202 | this.ActualModel.ResetAllAxes(); 203 | } 204 | 205 | this.InvalidatePlot(false); 206 | } 207 | 208 | /// 209 | /// Stores text on the clipboard. 210 | /// 211 | /// The text. 212 | public void SetClipboardText(string text) 213 | { 214 | Clipboard.SetTextAsync(text); 215 | } 216 | 217 | /// 218 | /// Sets the cursor type. 219 | /// 220 | /// The cursor type. 221 | public void SetCursorType(CursorType cursorType) 222 | { 223 | } 224 | 225 | /// 226 | /// Shows the tracker. 227 | /// 228 | /// The tracker data. 229 | public void ShowTracker(TrackerHitResult trackerHitResult) 230 | { 231 | if (trackerHitResult == null) 232 | { 233 | this.HideTracker(); 234 | return; 235 | } 236 | 237 | var trackerTemplate = this.DefaultTrackerTemplate; 238 | if (trackerHitResult.Series != null && !string.IsNullOrEmpty(trackerHitResult.Series.TrackerKey)) 239 | { 240 | var match = this.TrackerDefinitions.FirstOrDefault(t => t.TrackerKey == trackerHitResult.Series.TrackerKey); 241 | if (match != null) 242 | { 243 | trackerTemplate = match.TrackerTemplate; 244 | } 245 | } 246 | 247 | if (trackerTemplate == null) 248 | { 249 | this.HideTracker(); 250 | return; 251 | } 252 | 253 | if (!ReferenceEquals(trackerTemplate, this.currentTrackerTemplate)) 254 | { 255 | this.HideTracker(); 256 | 257 | var tracker = (ContentView)trackerTemplate.CreateContent(); 258 | this.overlays.Children.Add(tracker); 259 | AbsoluteLayout.SetLayoutBounds(tracker, new Rect(0, 0, 1, 1)); 260 | this.currentTracker = tracker; 261 | this.currentTrackerTemplate = trackerTemplate; 262 | } 263 | 264 | if (this.currentTracker != null) 265 | { 266 | this.currentTracker.BindingContext = trackerHitResult; 267 | } 268 | } 269 | 270 | /// 271 | /// Shows the zoom rectangle. 272 | /// 273 | /// The rectangle. 274 | public void ShowZoomRectangle(OxyRect r) 275 | { 276 | this.zoomControl.WidthRequest = r.Width; 277 | this.zoomControl.HeightRequest = r.Height; 278 | 279 | AbsoluteLayout.SetLayoutBounds(zoomControl, 280 | new Rect(r.Left, r.Top, r.Width, r.Height)); 281 | 282 | this.zoomControl.ControlTemplate = this.ZoomRectangleTemplate; 283 | this.zoomControl.IsVisible = true; 284 | } 285 | 286 | /// 287 | /// Zooms all axes. 288 | /// 289 | /// The zoom factor. 290 | public void ZoomAllAxes(double factor) 291 | { 292 | if (this.ActualModel != null) 293 | { 294 | this.ActualModel.ZoomAllAxes(factor); 295 | } 296 | 297 | this.InvalidatePlot(false); 298 | } 299 | 300 | /// 301 | /// Clears the background of the plot presenter. 302 | /// 303 | protected abstract void ClearBackground(); 304 | 305 | /// 306 | /// Creates the plot presenter. 307 | /// 308 | /// The plot presenter. 309 | protected abstract View CreatePlotPresenter(); 310 | 311 | /// 312 | /// Creates the render context. 313 | /// 314 | /// The render context. 315 | protected abstract IRenderContext CreateRenderContext(); 316 | 317 | /// 318 | /// Called when the model is changed. 319 | /// 320 | protected void OnModelChanged() 321 | { 322 | lock (this.modelLock) 323 | { 324 | if (this.ActualModel != null) 325 | { 326 | ((IPlotModel)this.ActualModel).AttachPlotView(null); 327 | this.ActualModel = null; 328 | } 329 | 330 | if (this.Model != null) 331 | { 332 | IPlotModel plotModel = this.Model; 333 | var oldPlotView = this.Model.PlotView; 334 | if (!ReferenceEquals(oldPlotView, null) && 335 | !ReferenceEquals(oldPlotView, this)) 336 | { 337 | // This PlotModel is already in use by some other PlotView control. 338 | plotModel.AttachPlotView(null); 339 | } 340 | 341 | plotModel.AttachPlotView(this); 342 | this.ActualModel = this.Model; 343 | } 344 | } 345 | 346 | this.InvalidatePlot(); 347 | } 348 | 349 | /// 350 | /// Renders the plot model to the plot presenter. 351 | /// 352 | protected void Render() 353 | { 354 | if (this.plotPresenter == null || this.renderContext == null || !(this.isInVisualTree = this.IsInVisualTree())) 355 | { 356 | return; 357 | } 358 | 359 | this.RenderOverride(); 360 | } 361 | 362 | /// 363 | /// Renders the plot model to the plot presenter. 364 | /// 365 | protected virtual void RenderOverride() 366 | { 367 | RenderStarted?.Invoke(); 368 | 369 | var dpiScale = this.UpdateDpi(); 370 | this.ClearBackground(); 371 | 372 | this.HideTracker(); 373 | 374 | if (this.ActualModel != null) 375 | { 376 | // round width and height to full device pixels 377 | var width = (int)(this.plotPresenter.Width * dpiScale) / dpiScale; 378 | var height = (int)(this.plotPresenter.Height * dpiScale) / dpiScale; 379 | 380 | lock (this.ActualModel.SyncRoot) 381 | { 382 | ((IPlotModel)this.ActualModel).Render(this.renderContext, new OxyRect(0, 0, width, height)); 383 | } 384 | } 385 | 386 | RenderFinished?.Invoke(); 387 | } 388 | 389 | /// 390 | /// Updates the DPI scale of the render context. 391 | /// 392 | /// The DPI scale. 393 | protected virtual double UpdateDpi() 394 | { 395 | return DeviceDisplay.MainDisplayInfo.Density; 396 | } 397 | 398 | /// 399 | /// Called when the model is changed. 400 | /// 401 | //event data. 402 | private static void ModelChanged(BindableObject bindable, object oldValue, object newValue) 403 | { 404 | ((PlotViewBase)bindable).OnModelChanged(); 405 | } 406 | 407 | /// 408 | /// Invokes the specified action on the dispatcher, if necessary. 409 | /// 410 | /// The action. 411 | private void BeginInvoke(Action action) 412 | { 413 | if (!CheckAccess()) 414 | { 415 | this.Dispatcher.Dispatch(action); 416 | } 417 | else 418 | { 419 | action(); 420 | } 421 | } 422 | 423 | /// 424 | /// Determines whether the calling thread is the main thread 425 | /// 426 | /// 427 | private bool CheckAccess() 428 | { 429 | return Thread.CurrentThread.ManagedThreadId == mainThreadId; 430 | } 431 | 432 | /// 433 | /// Gets a value indicating whether the is connected to the visual tree. 434 | /// 435 | /// true if the PlotViewBase is connected to the visual tree; false otherwise. 436 | private bool IsInVisualTree() 437 | { 438 | Microsoft.Maui.Controls.Element dpObject = this; 439 | while ((dpObject = dpObject.Parent) != null) 440 | { 441 | if (dpObject is Page) 442 | { 443 | return true; 444 | } 445 | } 446 | 447 | return false; 448 | } 449 | 450 | /// 451 | /// This event fires every time Layout updates the layout of the trees associated with current Dispatcher. 452 | /// 453 | /// The sender. 454 | /// The event args. 455 | private void OnSizeUpdated(object sender, EventArgs e) 456 | { 457 | // if we were not in the visual tree the last time we tried to render but are now, we have to render 458 | if (!this.isInVisualTree && this.IsInVisualTree()) 459 | { 460 | this.Render(); 461 | } 462 | } 463 | } -------------------------------------------------------------------------------- /Source/OxyPlot.Maui.Skia/RenderTarget.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // Copyright (c) 2020 OxyPlot contributors 4 | // 5 | // -------------------------------------------------------------------------------------------------------------------- 6 | 7 | namespace OxyPlot.Maui.Skia; 8 | 9 | /// 10 | /// Defines a render target for . 11 | /// 12 | internal enum RenderTarget 13 | { 14 | /// 15 | /// Indicates that the renders to a screen. 16 | /// 17 | /// 18 | /// The render context may try to snap shapes to device pixels and will use sub-pixel text rendering. 19 | /// 20 | Screen, 21 | 22 | /// 23 | /// Indicates that the renders to a pixel graphic. 24 | /// 25 | /// 26 | /// The render context may try to snap shapes to pixels, but will not use sub-pixel text rendering. 27 | /// 28 | PixelGraphic, 29 | 30 | /// 31 | /// Indicates that the renders to a vector graphic. 32 | /// 33 | /// 34 | /// The render context will not use any rendering enhancements that are specific to pixel graphics. 35 | /// 36 | VectorGraphic 37 | } -------------------------------------------------------------------------------- /Source/OxyPlot.Maui.Skia/SkiaExtensions.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // 3 | // Copyright (c) 2020 OxyPlot contributors 4 | // 5 | // -------------------------------------------------------------------------------------------------------------------- 6 | 7 | namespace OxyPlot.Maui.Skia; 8 | 9 | /// 10 | /// Provides extension methods for conversion between SkiaSharp and oxyplot objects. 11 | /// 12 | public static class SkiaExtensions 13 | { 14 | /// 15 | /// Converts a to a ; 16 | /// 17 | /// The . 18 | /// The . 19 | public static OxyColor ToOxyColor(this SKColor color) 20 | { 21 | return OxyColor.FromArgb(color.Alpha, color.Red, color.Green, color.Blue); 22 | } 23 | 24 | /// 25 | /// Converts a to a ; 26 | /// 27 | /// The . 28 | /// The . 29 | public static SKColor ToSKColor(this OxyColor color) 30 | { 31 | return new SKColor(color.R, color.G, color.B, color.A); 32 | } 33 | } -------------------------------------------------------------------------------- /Source/OxyPlot.Maui.Skia/Tracker/TrackerControl.xaml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 15 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /Source/OxyPlot.Maui.Skia/Tracker/TrackerDefinition.cs: -------------------------------------------------------------------------------- 1 | namespace OxyPlot.Maui.Skia; 2 | 3 | /// 4 | /// Represents a tracker definition. 5 | /// 6 | /// The tracker definitions make it possible to show different trackers for different series. 7 | /// The property is matched with the 8 | /// in the TrackerDefinitions collection in the control. 9 | public class TrackerDefinition : ContentView 10 | { 11 | /// 12 | /// Identifies the dependency property. 13 | /// 14 | public static readonly BindableProperty TrackerKeyProperty = 15 | BindableProperty.Create(nameof(TrackerKey), typeof(PlotModel), typeof(TrackerDefinition)); 16 | 17 | /// 18 | /// Identifies the dependency property. 19 | /// 20 | public static readonly BindableProperty TrackerTemplateProperty = 21 | BindableProperty.Create(nameof(TrackerTemplate), typeof(ControlTemplate), typeof(TrackerDefinition)); 22 | 23 | /// 24 | /// Gets or sets the tracker key. 25 | /// 26 | /// The Plot will use this property to find the TrackerDefinition that matches the TrackerKey of the current series. 27 | public string TrackerKey 28 | { 29 | get => (string)this.GetValue(TrackerKeyProperty); 30 | set => this.SetValue(TrackerKeyProperty, value); 31 | } 32 | 33 | /// 34 | /// Gets or sets the tracker template. 35 | /// 36 | /// The tracker control will be added/removed from the Tracker overlay as necessary. 37 | /// The DataContext of the tracker will be set to a TrackerHitResult with the current tracker data. 38 | public ControlTemplate TrackerTemplate 39 | { 40 | get => (ControlTemplate)this.GetValue(TrackerTemplateProperty); 41 | set => this.SetValue(TrackerTemplateProperty, value); 42 | } 43 | } -------------------------------------------------------------------------------- /Source/OxyPlot.Maui.Skia/Tracker/TrackerHelper.cs: -------------------------------------------------------------------------------- 1 | namespace OxyPlot.Maui.Skia.Tracker; 2 | 3 | internal static class TrackerHelper 4 | { 5 | /// 6 | /// Gets the nearest tracker hit. 7 | /// 8 | /// The series. 9 | /// The point. 10 | /// Snap to points. 11 | /// Check points only (no interpolation). 12 | /// The distance from the series at which the tracker fires 13 | /// The value indicating whether to check distance 14 | /// when showing tracker between data points. 15 | /// 16 | /// is ignored if is equal to False. 17 | /// 18 | /// A tracker hit result. 19 | public static TrackerHitResult GetNearestHit( 20 | Series.Series series, 21 | ScreenPoint point, 22 | bool snap, 23 | bool pointsOnly, 24 | double firesDistance, 25 | bool checkDistanceBetweenPoints) 26 | { 27 | if (series == null) 28 | { 29 | return null; 30 | } 31 | 32 | // Check data points only 33 | if (snap || pointsOnly) 34 | { 35 | var result = series.GetNearestPoint(point, false); 36 | if (ShouldTrackerOpen(result, point, firesDistance)) 37 | { 38 | return result; 39 | } 40 | } 41 | 42 | // Check between data points (if possible) 43 | if (!pointsOnly) 44 | { 45 | var result = series.GetNearestPoint(point, true); 46 | if (!checkDistanceBetweenPoints || ShouldTrackerOpen(result, point, firesDistance)) 47 | { 48 | return result; 49 | } 50 | } 51 | 52 | return null; 53 | } 54 | 55 | private static bool ShouldTrackerOpen(TrackerHitResult result, ScreenPoint point, double firesDistance) => 56 | result?.Position.DistanceTo(point) < firesDistance; 57 | } -------------------------------------------------------------------------------- /Source/OxyplotMauiSample/App.xaml: -------------------------------------------------------------------------------- 1 |  2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Source/OxyplotMauiSample/App.xaml.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace OxyplotMauiSample 3 | { 4 | public partial class App 5 | { 6 | public App() 7 | { 8 | InitializeComponent(); 9 | } 10 | 11 | protected override Window CreateWindow(IActivationState activationState) 12 | { 13 | return new Window(new AppShell()); 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /Source/OxyplotMauiSample/AppShell.xaml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Source/OxyplotMauiSample/AppShell.xaml.cs: -------------------------------------------------------------------------------- 1 | namespace OxyplotMauiSample 2 | { 3 | public partial class AppShell : Shell 4 | { 5 | public AppShell() 6 | { 7 | InitializeComponent(); 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /Source/OxyplotMauiSample/MainPage.xaml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 |