From 8e68bd532fbbf5b09380cfedb94b339a4bbbd762 Mon Sep 17 00:00:00 2001 From: andrey Date: Thu, 12 Nov 2015 19:46:04 +0200 Subject: [PATCH] Android functionality with default Canvas Implementation --- Perspex.sln | 19 + .../Perspex.Android/AndroidAssetLoader.cs | 82 + .../Perspex.Android/AndroidPlatform.cs | 221 +++ .../AndroidGraphicsExtensions.cs | 246 +++ .../CanvasRendering/AndroidPlatformRender.cs | 65 + .../AndroidTopLevelRenderer.cs | 59 + .../CanvasRendering/BitmapImpl.cs | 34 + .../CanvasRendering/BrushImpl.cs | 363 ++++ .../CanvasRendering/DrawingContext.cs | 248 +++ .../CanvasRendering/FormattedTextImpl.cs | 205 ++ .../CanvasRendering/RenderTarget.cs | 63 + .../CanvasRendering/RenderTargetBitmapImpl.cs | 49 + .../CanvasRendering/StreamGeometryImpl.cs | 158 ++ src/Android/Perspex.Android/CursorFactory.cs | 12 + .../Perspex.Android/Perspex.Android.csproj | 171 ++ .../CanvasPlatform/IAndroidCanvasView.cs | 14 + .../IAndroidViewSupportRender.cs | 13 + .../Platform/CanvasPlatform/MainWindowImpl.cs | 71 + .../CanvasPlatform/PointUnitService.cs | 184 ++ .../Platform/CanvasPlatform/PopupImpl.cs | 137 ++ .../Helpers/AndroidViewRenderHelpers.cs | 688 +++++++ .../CanvasPlatform/SurfaceMainWindowImpl.cs | 33 + .../CanvasPlatform/SurfaceWindowImpl.cs | 194 ++ .../Platform/CanvasPlatform/WindowImpl.cs | 207 ++ .../Perspex.Android/Platform/ClipboardImpl.cs | 47 + .../Platform/Input/AndroidKeyboardDevice.cs | 217 +++ .../Platform/Input/AndroidMouseDevice.cs | 12 + .../Platform/Specific/CommonUITimer.cs | 50 + .../Helpers/AndroidKeyboardEventsHelper.cs | 145 ++ .../Helpers/AndroidTouchEventsHelper.cs | 155 ++ .../Platform/Specific/Helpers/ViewDrawType.cs | 23 + .../Platform/Specific/IAndroidActivity.cs | 12 + .../Platform/Specific/IAndroidView.cs | 16 + .../Platform/Specific/PerspexActivity.cs | 54 + .../Properties/AssemblyInfo.cs | 30 + .../Resources/AboutResources.txt | 50 + .../Resources/Resource.Designer.cs | 60 + .../Resources/Values/Strings.xml | 6 + .../Perspex.Android/SystemDialogImpl.cs | 16 + src/Android/Perspex.Android/packages.config | 9 + .../Perspex.AndroidTestApplication/App.cs | 372 ++++ .../Assets/AboutAssets.txt | 19 + .../GettingStarted.Xamarin | 4 + .../MainActivity.cs | 67 + .../Perspex.AndroidTestApplication.csproj | 215 +++ .../Properties/AndroidManifest.xml | 6 + .../Properties/AssemblyInfo.cs | 30 + .../Resources/AboutResources.txt | 50 + .../Resources/Resource.Designer.cs | 102 + .../Resources/drawable/Icon.png | Bin 0 -> 4147 bytes .../Resources/layout/Main.axml | 9 + .../Resources/values/Strings.xml | 6 + .../SetupTestUI.cs | 162 ++ .../Perspex.AndroidTestApplication/TestUI.cs | 1659 +++++++++++++++++ .../github_icon.png | Bin 0 -> 17223 bytes .../packages.config | 12 + .../pattern.jpg | Bin 0 -> 136642 bytes .../Perspex.Skia.Android.TestApp.csproj | 2 +- .../Perspex.Skia.Android.csproj | 2 +- 59 files changed, 7153 insertions(+), 2 deletions(-) create mode 100644 src/Android/Perspex.Android/AndroidAssetLoader.cs create mode 100644 src/Android/Perspex.Android/AndroidPlatform.cs create mode 100644 src/Android/Perspex.Android/CanvasRendering/AndroidGraphicsExtensions.cs create mode 100644 src/Android/Perspex.Android/CanvasRendering/AndroidPlatformRender.cs create mode 100644 src/Android/Perspex.Android/CanvasRendering/AndroidTopLevelRenderer.cs create mode 100644 src/Android/Perspex.Android/CanvasRendering/BitmapImpl.cs create mode 100644 src/Android/Perspex.Android/CanvasRendering/BrushImpl.cs create mode 100644 src/Android/Perspex.Android/CanvasRendering/DrawingContext.cs create mode 100644 src/Android/Perspex.Android/CanvasRendering/FormattedTextImpl.cs create mode 100644 src/Android/Perspex.Android/CanvasRendering/RenderTarget.cs create mode 100644 src/Android/Perspex.Android/CanvasRendering/RenderTargetBitmapImpl.cs create mode 100644 src/Android/Perspex.Android/CanvasRendering/StreamGeometryImpl.cs create mode 100644 src/Android/Perspex.Android/CursorFactory.cs create mode 100644 src/Android/Perspex.Android/Perspex.Android.csproj create mode 100644 src/Android/Perspex.Android/Platform/CanvasPlatform/IAndroidCanvasView.cs create mode 100644 src/Android/Perspex.Android/Platform/CanvasPlatform/IAndroidViewSupportRender.cs create mode 100644 src/Android/Perspex.Android/Platform/CanvasPlatform/MainWindowImpl.cs create mode 100644 src/Android/Perspex.Android/Platform/CanvasPlatform/PointUnitService.cs create mode 100644 src/Android/Perspex.Android/Platform/CanvasPlatform/PopupImpl.cs create mode 100644 src/Android/Perspex.Android/Platform/CanvasPlatform/Specific/Helpers/AndroidViewRenderHelpers.cs create mode 100644 src/Android/Perspex.Android/Platform/CanvasPlatform/SurfaceMainWindowImpl.cs create mode 100644 src/Android/Perspex.Android/Platform/CanvasPlatform/SurfaceWindowImpl.cs create mode 100644 src/Android/Perspex.Android/Platform/CanvasPlatform/WindowImpl.cs create mode 100644 src/Android/Perspex.Android/Platform/ClipboardImpl.cs create mode 100644 src/Android/Perspex.Android/Platform/Input/AndroidKeyboardDevice.cs create mode 100644 src/Android/Perspex.Android/Platform/Input/AndroidMouseDevice.cs create mode 100644 src/Android/Perspex.Android/Platform/Specific/CommonUITimer.cs create mode 100644 src/Android/Perspex.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs create mode 100644 src/Android/Perspex.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs create mode 100644 src/Android/Perspex.Android/Platform/Specific/Helpers/ViewDrawType.cs create mode 100644 src/Android/Perspex.Android/Platform/Specific/IAndroidActivity.cs create mode 100644 src/Android/Perspex.Android/Platform/Specific/IAndroidView.cs create mode 100644 src/Android/Perspex.Android/Platform/Specific/PerspexActivity.cs create mode 100644 src/Android/Perspex.Android/Properties/AssemblyInfo.cs create mode 100644 src/Android/Perspex.Android/Resources/AboutResources.txt create mode 100644 src/Android/Perspex.Android/Resources/Resource.Designer.cs create mode 100644 src/Android/Perspex.Android/Resources/Values/Strings.xml create mode 100644 src/Android/Perspex.Android/SystemDialogImpl.cs create mode 100644 src/Android/Perspex.Android/packages.config create mode 100644 src/Android/Perspex.AndroidTestApplication/App.cs create mode 100644 src/Android/Perspex.AndroidTestApplication/Assets/AboutAssets.txt create mode 100644 src/Android/Perspex.AndroidTestApplication/GettingStarted.Xamarin create mode 100644 src/Android/Perspex.AndroidTestApplication/MainActivity.cs create mode 100644 src/Android/Perspex.AndroidTestApplication/Perspex.AndroidTestApplication.csproj create mode 100644 src/Android/Perspex.AndroidTestApplication/Properties/AndroidManifest.xml create mode 100644 src/Android/Perspex.AndroidTestApplication/Properties/AssemblyInfo.cs create mode 100644 src/Android/Perspex.AndroidTestApplication/Resources/AboutResources.txt create mode 100644 src/Android/Perspex.AndroidTestApplication/Resources/Resource.Designer.cs create mode 100644 src/Android/Perspex.AndroidTestApplication/Resources/drawable/Icon.png create mode 100644 src/Android/Perspex.AndroidTestApplication/Resources/layout/Main.axml create mode 100644 src/Android/Perspex.AndroidTestApplication/Resources/values/Strings.xml create mode 100644 src/Android/Perspex.AndroidTestApplication/SetupTestUI.cs create mode 100644 src/Android/Perspex.AndroidTestApplication/TestUI.cs create mode 100644 src/Android/Perspex.AndroidTestApplication/github_icon.png create mode 100644 src/Android/Perspex.AndroidTestApplication/packages.config create mode 100644 src/Android/Perspex.AndroidTestApplication/pattern.jpg diff --git a/Perspex.sln b/Perspex.sln index f0368380d7..a32eaa5d79 100644 --- a/Perspex.sln +++ b/Perspex.sln @@ -118,9 +118,16 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Perspex.Skia.Android", "src EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Perspex.Skia.Android.TestApp", "src\Skia\Perspex.Skia.Android.TestApp\Perspex.Skia.Android.TestApp.csproj", "{F92E55A5-ED73-4CCB-AB4B-0541B6757F31}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Android", "Android", "{7CF9789C-F1D3-4D0E-90E5-F1DF67A2753F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Perspex.Android", "src\Android\Perspex.Android\Perspex.Android.csproj", "{7B92AF71-6287-4693-9DCB-BD5B6E927E23}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Perspex.AndroidTestApplication", "src\Android\Perspex.AndroidTestApplication\Perspex.AndroidTestApplication.csproj", "{FF69B927-C545-49AE-8E16-3D14D621AA12}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution src\Shared\RenderHelpers\RenderHelpers.projitems*{fb05ac90-89ba-4f2f-a924-f37875fb547c}*SharedItemsImports = 4 + src\Shared\PlatformSupport\PlatformSupport.projitems*{7b92af71-6287-4693-9dcb-bd5b6e927e23}*SharedItemsImports = 4 src\Shared\PlatformSupport\PlatformSupport.projitems*{e4d9629c-f168-4224-3f51-a5e482ffbc42}*SharedItemsImports = 13 src\Skia\Perspex.Skia\Perspex.Skia.projitems*{2f59f3d0-748d-4652-b01e-e0d954756308}*SharedItemsImports = 13 src\Shared\PlatformSupport\PlatformSupport.projitems*{db070a10-bf39-4752-8456-86e9d5928478}*SharedItemsImports = 4 @@ -300,6 +307,16 @@ Global {F92E55A5-ED73-4CCB-AB4B-0541B6757F31}.Release|Any CPU.ActiveCfg = Release|Any CPU {F92E55A5-ED73-4CCB-AB4B-0541B6757F31}.Release|Any CPU.Build.0 = Release|Any CPU {F92E55A5-ED73-4CCB-AB4B-0541B6757F31}.Release|Any CPU.Deploy.0 = Release|Any CPU + {7B92AF71-6287-4693-9DCB-BD5B6E927E23}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7B92AF71-6287-4693-9DCB-BD5B6E927E23}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7B92AF71-6287-4693-9DCB-BD5B6E927E23}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7B92AF71-6287-4693-9DCB-BD5B6E927E23}.Release|Any CPU.Build.0 = Release|Any CPU + {FF69B927-C545-49AE-8E16-3D14D621AA12}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FF69B927-C545-49AE-8E16-3D14D621AA12}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FF69B927-C545-49AE-8E16-3D14D621AA12}.Debug|Any CPU.Deploy.0 = Debug|Any CPU + {FF69B927-C545-49AE-8E16-3D14D621AA12}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FF69B927-C545-49AE-8E16-3D14D621AA12}.Release|Any CPU.Build.0 = Release|Any CPU + {FF69B927-C545-49AE-8E16-3D14D621AA12}.Release|Any CPU.Deploy.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -335,5 +352,7 @@ Global {D35A9F3D-8BB0-496E-BF72-444038A7DEBB} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {BD43F7C0-396B-4AA1-BAD9-DFDE54D51298} = {3743B0F2-CC41-4F14-A8C8-267F579BF91E} {F92E55A5-ED73-4CCB-AB4B-0541B6757F31} = {3743B0F2-CC41-4F14-A8C8-267F579BF91E} + {7B92AF71-6287-4693-9DCB-BD5B6E927E23} = {7CF9789C-F1D3-4D0E-90E5-F1DF67A2753F} + {FF69B927-C545-49AE-8E16-3D14D621AA12} = {7CF9789C-F1D3-4D0E-90E5-F1DF67A2753F} EndGlobalSection EndGlobal diff --git a/src/Android/Perspex.Android/AndroidAssetLoader.cs b/src/Android/Perspex.Android/AndroidAssetLoader.cs new file mode 100644 index 0000000000..f10d0a8571 --- /dev/null +++ b/src/Android/Perspex.Android/AndroidAssetLoader.cs @@ -0,0 +1,82 @@ +// Copyright (c) The Perspex Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Perspex.Android.Platform.Specific; +using Perspex.Platform; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; + +namespace Perspex.Android.PlatformSupport +{ + /// + /// Loads assets compiled into the application binary. + /// + public class AndroidAssetLoader : IAssetLoader + { + internal Assembly DefaultAssetAssembly { get; set; } + internal string DefaultResourcePrefix { get; set; } + + public AndroidAssetLoader() + { + //Assembly.GetEntryAssembly() not working on mono/xamarin for android!!! + //may be this is better way to have at least the application assembly as default one + DefaultAssetAssembly = PerspexLocator.Current.GetService().Activity.GetType().Assembly; + } + + private static readonly Dictionary AssemblyNameCache = new Dictionary(); + + /// + /// Checks if an asset with the specified URI exists. + /// + /// The URI. + /// True if the asset could be found; otherwise false. + public bool Exists(Uri uri) + { + return OpenInternal(uri, false) != null; + } + + /// + /// Opens the resource with the requested URI. + /// + /// The URI. + /// A stream containing the resource contents. + /// + /// The resource was not found. + /// + public Stream Open(Uri uri) + { + return OpenInternal(uri, true); + } + + private Stream OpenInternal(Uri uri, bool throwIfNotSuccess) + { + //uri.AbsolutePath is NotFiniteNumberException escaped adn can cause problems in assembly names with spaces + bool isAbsolute = uri.IsAbsoluteUri; + var parts = (isAbsolute ? uri.LocalPath : uri.OriginalString).Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); + var asm = parts.Length == 1 ? DefaultAssetAssembly : GetAssembly(parts[0]); + var typeName = parts[parts.Length == 1 ? 0 : 1]; + if (typeName != null && !isAbsolute && !typeName.StartsWith(DefaultResourcePrefix)) + { + typeName = DefaultResourcePrefix + typeName; + } + + var rv = asm.GetManifestResourceStream(typeName); + if (rv == null && throwIfNotSuccess) + throw new FileNotFoundException($"The resource {uri} could not be found."); + return rv; + } + + private static Assembly GetAssembly(string name) + { + Assembly rv; + if (!AssemblyNameCache.TryGetValue(name, out rv)) + AssemblyNameCache[name] = rv = + AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.GetName().Name == name) + ?? Assembly.Load(name); + return rv; + } + } +} \ No newline at end of file diff --git a/src/Android/Perspex.Android/AndroidPlatform.cs b/src/Android/Perspex.Android/AndroidPlatform.cs new file mode 100644 index 0000000000..4781cecead --- /dev/null +++ b/src/Android/Perspex.Android/AndroidPlatform.cs @@ -0,0 +1,221 @@ +using Android.OS; +using Perspex.Android.CanvasRendering; +using Perspex.Android.Platform; +using Perspex.Android.Platform.CanvasPlatform; +using Perspex.Android.Platform.Input; +using Perspex.Android.Platform.Specific; +using Perspex.Android.Platform.Specific.Helpers; +using Perspex.Android.PlatformSupport; +using Perspex.Animation; +using Perspex.Controls.Platform; +using Perspex.Input; +using Perspex.Input.Platform; +using Perspex.Platform; +using Perspex.Shared.PlatformSupport; +using System; +using System.Collections.Generic; +using System.Reactive.Disposables; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; + +namespace Perspex.Android +{ + public class AndroidPlatform : IPlatformThreadingInterface, IPlatformSettings + { + public static readonly AndroidPlatform Instance = new AndroidPlatform(); + public Size DoubleClickSize => new Size(4, 4); + public TimeSpan DoubleClickTime => TimeSpan.FromMilliseconds(200); + + private HashSet _renderingTask = new HashSet(); + + public void RegisterRenderingTaskId(int taskId) + { + _renderingTask.Add(taskId); + } + + public void UnRegisterRenderingTaskId(int taskId) + { + _renderingTask.Add(taskId); + } + + public bool CurrentThreadIsLoopThread + { + get + { + bool res = Looper.MainLooper.Thread.Equals(Java.Lang.Thread.CurrentThread()); + if (!res) + { + if (Task.CurrentId != null) + { + int id = Task.CurrentId.Value; + res = _renderingTask.Contains(Task.CurrentId.Value); + } + } + + return res; + } + } + + public void RunLoop(CancellationToken cancellationToken) + { + } + + private TimeSpan _animationTick = TimeSpan.FromSeconds(1.0 / Animate.FramesPerSecond); + + public IDisposable StartTimer(TimeSpan interval, Action tick) + { + //we should be carefull about the smallest amount of time the timer ticks + //as on android for animations so far there is some issue with 60 fps + //for now we can get set it to 16 fps with observable timer + //and still random native exceptions causing app to stop!!!! + //better not use it + //return Observable.Timer(interval, interval).Subscribe(_ => tick()); + + //System.Threading.Timer + //int ms = (int)interval.TotalMilliseconds; + ////regular timer is working perfect + //return new System.Threading.Timer(_ => tick(), null, ms, ms); + + //System.Timers.Timer + if (interval.TotalMilliseconds == 0) + { + //android ui thread + PerspexLocator.Current.GetService().Activity.RunOnUiThread(tick); + return Disposable.Empty; + } + + if (OverrideAnimateFramesPerSecond > 0) + { + if (_animationTick >= interval) + { + interval = TimeSpan.FromSeconds(1.0 / OverrideAnimateFramesPerSecond); + } + } + + //working not very bad with standard timer + //var timer = new System.Timers.Timer(interval.TotalMilliseconds); + //System.Timers.ElapsedEventHandler elapsed = (s, e) => tick(); + //timer.Elapsed += elapsed; + //timer.Start(); + //return Disposable.Create(() => + //{ + // timer.Stop(); + // timer.Elapsed -= elapsed; + // timer.Dispose(); + //}); + + //common ui timer is optimizing performance + //a lot almost twice, also may be processor usage + //most probably because it optimizes invalidation calls in animations and using less system resources + CommonUITimer timer; + if (!_commonTimers.TryGetValue(interval, out timer)) + { + timer = new CommonUITimer(interval); + _commonTimers.Add(interval, timer); + } + + timer.AddAction(tick); + + return Disposable.Create(() => + { + timer.RemoveAction(tick); + + if (timer.ActiveActionsCount == 0) + { + timer.Dispose(); + _commonTimers.Remove(interval); + } + }); + } + + private Dictionary _commonTimers = new Dictionary(); + + public void Signal() + { + EnsureInvokeOnMainThread(() => Signaled?.Invoke()); + } + + public event Action Signaled; + + public static void Initialize() + { + PerspexLocator.CurrentMutable + .Bind().ToTransient() + .Bind().ToTransient() + .Bind().ToSingleton() + .Bind().ToSingleton() + .Bind().ToConstant(Instance) + .Bind().ToConstant(Instance) + .Bind().ToTransient() + .Bind().ToSingleton() + .Bind().ToTransient() + ; + AndroidPlatformRender.Initialize(); + + Instance.RegisterViewDrawType(); + //set defaults for simple resources + //Assembly.GetEntryAssembly(); //not working on mono for android + var appType = Application.Current.GetType(); + var entryAssembly = appType.Assembly; + AndroidPlatform.Instance.InitializeAssetsLoader(entryAssembly, appType.Namespace + "."); + // SharedPlatform.Register(); + //we have custom Assetloader so no need to overwrite it + PerspexLocator.CurrentMutable + .Bind().ToSingleton(); + } + + public void RegisterViewDrawType() + { + if (Instance.DefaultViewDrawType == ViewDrawType.SurfaceViewCanvasOnDraw) + { + PerspexLocator.CurrentMutable.Bind().ToSingleton(); + } + else + { + PerspexLocator.CurrentMutable.Bind().ToSingleton(); + } + + PerspexLocator.CurrentMutable.Bind().ToTransient(); + } + + public void RegisterViewPointUnits() + { + PerspexLocator.CurrentMutable.Bind().ToSingleton(); + } + + public void InitializeAssetsLoader(Assembly assembly, string defaultResourcePrefix) + { + var assetLoder = PerspexLocator.Current.GetService() as AndroidAssetLoader; + if (assetLoder != null) + { + assetLoder.DefaultAssetAssembly = assembly; + assetLoder.DefaultResourcePrefix = defaultResourcePrefix; + } + } + + private void EnsureInvokeOnMainThread(Action action) + { + var mainHandler = new Handler(global::Android.App.Application.Context.MainLooper); + mainHandler.Post(action); + } + + public bool DrawDebugInfo { get; set; } = false; + + /// + /// Default View DrawType + /// WindowImpl supports: + /// ViewDrawType.CanvasOnDraw + /// ViewDrawType.BitmapOnPreDraw + /// BitmapBackgroundRender + /// by default it is ViewDrawType.CanvasOnDraw simples way of drawing view + /// SurfaceWindowImpl supports: + /// SurfaceViewCanvasOnDraw + /// + public ViewDrawType DefaultViewDrawType { get; set; } = ViewDrawType.SurfaceViewCanvasOnDraw; + + public PointUnit DefaultPointUnit { get; set; } = PointUnit.DP; + + public int OverrideAnimateFramesPerSecond { get; set; } = -1; + } +} \ No newline at end of file diff --git a/src/Android/Perspex.Android/CanvasRendering/AndroidGraphicsExtensions.cs b/src/Android/Perspex.Android/CanvasRendering/AndroidGraphicsExtensions.cs new file mode 100644 index 0000000000..2ee877352a --- /dev/null +++ b/src/Android/Perspex.Android/CanvasRendering/AndroidGraphicsExtensions.cs @@ -0,0 +1,246 @@ +using Perspex.Android.Platform.CanvasPlatform; +using Perspex.Android.Platform.Specific; +using Perspex.Media; +using System; +using AAllignment = Android.Text.Layout.Alignment; +using ACap = Android.Graphics.Paint.Cap; +using AColor = Android.Graphics.Color; +using AJoin = Android.Graphics.Paint.Join; +using AMatrix = Android.Graphics.Matrix; +using APoint = Android.Graphics.PointF; +using ARect = Android.Graphics.Rect; +using ARectF = Android.Graphics.RectF; +using AStyle = Android.Graphics.Paint.Style; +using ATextAlign = Android.Graphics.Paint.Align; +using ATileMode = Android.Graphics.Shader.TileMode; +using ATypeface = Android.Graphics.Typeface; +using ATypeStyle = Android.Graphics.TypefaceStyle; + +namespace Perspex.Android.CanvasRendering +{ + public static class AndroidGraphicsExtensions + { + public static AColor ToAndroidGraphics(this Color c) + { + return new AColor(c.R, c.G, c.B, c.A); + } + + public static AMatrix ToAndroidGraphics(this Matrix matrix) + { + var ps = PointUnitService.Instance; + //TODO: !!! + var transformValues = new float[9]; + transformValues[0] = (float)matrix.M11; + transformValues[1] = (float)matrix.M21; + transformValues[2] = ps.PerspexToNativeXF(matrix.M31); + //transformValues[2] = (float)matrix.M31; + + transformValues[3] = (float)matrix.M12; + transformValues[4] = (float)matrix.M22; + transformValues[5] = ps.PerspexToNativeYF(matrix.M32); + //transformValues[5] = (float)matrix.M32; + + transformValues[6] = 0; + transformValues[7] = 0; + transformValues[8] = 1; + var am = new AMatrix(); + am.SetValues(transformValues); + return am; + } + + //public static Matrix ToPerspex(this AMatrix m) + //{ + // var v = new float[9]; + // m.GetValues(v); + // return new Matrix(v[0], v[3], v[1], v[4], v[2], v[5]); + //} + + public static APoint ToAndroidGraphics(this Point p, bool notransform = false) + { + if (notransform) new APoint((float)p.X, (float)p.Y); + var ps = PointUnitService.Instance; + return new APoint(ps.PerspexToNativeXF(p.X), ps.PerspexToNativeYF(p.Y)); + } + + //public static Point ToPerspex(this APoint p) + //{ + // return new Point(p.X, p.Y); + //} + + public static ARect ToAndroidGraphics(this Rect r, bool notransform = false) + { + if (notransform) return new ARect((int)r.X, (int)r.Y, (int)r.Right, (int)r.Bottom); + var ps = PointUnitService.Instance; + return new ARect(ps.PerspexToNativeXInt(r.X), ps.PerspexToNativeYInt(r.Y), ps.PerspexToNativeXInt(r.Right), ps.PerspexToNativeYInt(r.Bottom)); + } + + public static ARectF ToAndroidGraphicsF(this Rect r) + { + var ps = PointUnitService.Instance; + return new ARectF(ps.PerspexToNativeXF(r.X), ps.PerspexToNativeYF(r.Y), ps.PerspexToNativeXF(r.Right), ps.PerspexToNativeYF(r.Bottom)); + } + + //public static Rect ToPerspex(this ARect r) + //{ + // return new Rect(r.Left, r.Top, r.Width(), r.Height()); + //} + + public static Rect ToPerspex(this ARectF r) + { + var ps = PointUnitService.Instance; + return new Rect(ps.NativeToPerspexX(r.Left), ps.NativeToPerspexY(r.Top), ps.NativeToPerspexX(r.Width()), ps.NativeToPerspexY(r.Height())); + } + + public static AJoin ToAndroidGraphics(this PenLineJoin plj) + { + switch (plj) + { + default: + case PenLineJoin.Bevel: + return AJoin.Bevel; + + case PenLineJoin.Miter: + return AJoin.Miter; + + case PenLineJoin.Round: + return AJoin.Round; + } + } + + public static ACap ToAndroidGraphics(this PenLineCap plc) + { + switch (plc) + { + default: + case PenLineCap.Flat: + return ACap.Butt; + + case PenLineCap.Round: + return ACap.Round; + + case PenLineCap.Square: + return ACap.Square; + //Triangle not supported + case PenLineCap.Triangle: + goto default; + } + } + + public static AStyle ToAndroidGraphics(this BrushUsage bu) + { + switch (bu) + { + default: + case BrushUsage.Fill: + return AStyle.Fill; + + case BrushUsage.Stroke: + return AStyle.Stroke; + + case BrushUsage.Both: + return AStyle.FillAndStroke; + } + } + + public static ATypeface ToAndroidGraphicsTypeface(this FontStyle s, FontWeight fontWeight) + { + ATypeface result = ATypeface.Default; + + if (fontWeight >= FontWeight.Bold) + { + result = ATypeface.DefaultBold; + } + + return result; + } + + public static ATypeStyle ToAndroidGraphicsTypefaceStyle(this FontStyle s, FontWeight fontWeight) + { + ATypeStyle result; + switch (s) + { + default: + case FontStyle.Normal: + result = ATypeStyle.Normal; + break; + + case FontStyle.Italic: + result = ATypeStyle.Italic; + break; + //Oblique not supported, return normal + case FontStyle.Oblique: + goto default; + } + + if (fontWeight >= FontWeight.Bold) + { + result = result == ATypeStyle.Italic ? ATypeStyle.BoldItalic : ATypeStyle.Bold; + } + + return result; + } + + public static ATextAlign ToAndroidGraphics(this TextAlignment ta) + { + switch (ta) + { + default: + case TextAlignment.Left: + return ATextAlign.Left; + + case TextAlignment.Center: + return ATextAlign.Center; + + case TextAlignment.Right: + return ATextAlign.Right; + } + } + + public static AAllignment ToAndroidGraphicsLayoutAligment(this TextAlignment ta) + { + switch (ta) + { + default: + case TextAlignment.Left: + return AAllignment.AlignNormal; + + case TextAlignment.Center: + return AAllignment.AlignCenter; + + case TextAlignment.Right: + return AAllignment.AlignOpposite; + } + } + + public static ATileMode ToAndroidGraphicsX(this TileMode tileMode) + { + switch (tileMode) + { + case TileMode.Tile: return ATileMode.Repeat; + case TileMode.FlipX: return ATileMode.Mirror; + case TileMode.FlipXY: return ATileMode.Mirror; + case TileMode.FlipY: return ATileMode.Clamp; + case TileMode.None: return ATileMode.Clamp; + default: return ATileMode.Clamp; + } + } + + public static ATileMode ToAndroidGraphicsY(this TileMode tileMode) + { + switch (tileMode) + { + case TileMode.Tile: return ATileMode.Repeat; + case TileMode.FlipX: return ATileMode.Clamp; + case TileMode.FlipXY: return ATileMode.Mirror; + case TileMode.FlipY: return ATileMode.Mirror; + case TileMode.None: return ATileMode.Clamp; + default: return ATileMode.Clamp; + } + } + + public static int OpacityToAndroidAlfa(double opacity) + { + return Convert.ToInt32(opacity * 255); + } + } +} \ No newline at end of file diff --git a/src/Android/Perspex.Android/CanvasRendering/AndroidPlatformRender.cs b/src/Android/Perspex.Android/CanvasRendering/AndroidPlatformRender.cs new file mode 100644 index 0000000000..bab5a6a287 --- /dev/null +++ b/src/Android/Perspex.Android/CanvasRendering/AndroidPlatformRender.cs @@ -0,0 +1,65 @@ +using Perspex.Android.Platform.Specific; +using Perspex.Media; +using Perspex.Platform; +using System.IO; +using AG = Android.Graphics; +using System; +using Perspex.Android.Platform.CanvasPlatform; + +namespace Perspex.Android.CanvasRendering +{ + public class AndroidPlatformRender : IPlatformRenderInterface + { + private static readonly AndroidPlatformRender instance = new AndroidPlatformRender(); + + public IFormattedTextImpl CreateFormattedText(string text, string fontFamilyName, double fontSize, + FontStyle fontStyle, + TextAlignment textAlignment, FontWeight fontWeight) + { + return new FormattedTextImpl(text, fontFamilyName, fontSize, fontStyle, textAlignment, fontWeight); + } + + public IStreamGeometryImpl CreateStreamGeometry() + { + return new StreamGeometryImpl(); + } + + public IRenderTarget CreateRenderer(IPlatformHandle handle) + { + return new RenderTarget((IAndroidCanvasView)handle); + } + + public IRenderTargetBitmapImpl CreateRenderTargetBitmap(int pwidth, int pheight) + { + int width = PointUnitService.Instance.PerspexToNativeXInt(pwidth); + int height = PointUnitService.Instance.PerspexToNativeYInt(pwidth); + + return new RenderTargetBitmapImpl(CreateNativeBitmap(width, height)); + } + + public IBitmapImpl CreateBitmap(int pwidth, int pheight) + { + int width = PointUnitService.Instance.PerspexToNativeXInt(pwidth); + int height = PointUnitService.Instance.PerspexToNativeYInt(pwidth); + return new BitmapImpl(CreateNativeBitmap(width, height)); + } + + public IBitmapImpl LoadBitmap(string fileName) + { + return new BitmapImpl(AG.BitmapFactory.DecodeFile(fileName)); + } + + public IBitmapImpl LoadBitmap(Stream stream) + { + return new BitmapImpl(AG.BitmapFactory.DecodeStream(stream)); + } + + public static void Initialize() + => PerspexLocator.CurrentMutable.Bind().ToConstant(instance); + + private AG.Bitmap CreateNativeBitmap(int width, int height) + { + return AG.Bitmap.CreateBitmap(width, height, AG.Bitmap.Config.Argb8888); + } + } +} \ No newline at end of file diff --git a/src/Android/Perspex.Android/CanvasRendering/AndroidTopLevelRenderer.cs b/src/Android/Perspex.Android/CanvasRendering/AndroidTopLevelRenderer.cs new file mode 100644 index 0000000000..d696144c45 --- /dev/null +++ b/src/Android/Perspex.Android/CanvasRendering/AndroidTopLevelRenderer.cs @@ -0,0 +1,59 @@ +using Perspex.Controls; +using Perspex.Controls.Platform; +using Perspex.Platform; +using Perspex.Rendering; +using Perspex.Threading; +using System; +using System.Collections.Generic; + +namespace Perspex.Android.CanvasRendering +{ + internal class AndroidTopLevelRenderer : ITopLevelRenderer + { + public void Attach(TopLevel topLevel) + { + var resources = new List(); + var initialClientSize = topLevel.PlatformImpl.ClientSize; + + + var queueManager = ((IRenderRoot)topLevel).RenderQueueManager; + + if (queueManager == null) + return; + + + var viewport = PlatformManager.CreateRenderTarget(topLevel.PlatformImpl); + resources.Add(viewport); + //resources.Add(queueManager.RenderNeeded.Subscribe(_ + // => + // Dispatcher.UIThread.InvokeAsync(() => topLevel.PlatformImpl.Invalidate(new Rect(topLevel.ClientSize))))); + Action pendingInvalidation = null; + resources.Add(queueManager.RenderNeeded.Subscribe(_ => + { + if (pendingInvalidation == null) + { + pendingInvalidation = () => + { + topLevel.PlatformImpl.Invalidate(new Rect(topLevel.ClientSize)); + pendingInvalidation = null; + }; + Dispatcher.UIThread.InvokeAsync(pendingInvalidation); + } + } + )); + + topLevel.PlatformImpl.Paint = rect => + { + viewport.Render(topLevel); + queueManager.RenderFinished(); + }; + + topLevel.Closed += delegate + { + foreach (var disposable in resources) + disposable.Dispose(); + resources.Clear(); + }; + } + } +} \ No newline at end of file diff --git a/src/Android/Perspex.Android/CanvasRendering/BitmapImpl.cs b/src/Android/Perspex.Android/CanvasRendering/BitmapImpl.cs new file mode 100644 index 0000000000..d34d5aa59a --- /dev/null +++ b/src/Android/Perspex.Android/CanvasRendering/BitmapImpl.cs @@ -0,0 +1,34 @@ +// Copyright (c) The Perspex Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Perspex.Platform; +using System.IO; +using AG = Android.Graphics; + +namespace Perspex.Android.CanvasRendering +{ + public class BitmapImpl : IBitmapImpl + { + public BitmapImpl(AG.Bitmap bitmap) + { + PlatformBitmap = bitmap; + } + + public int PixelWidth => PlatformBitmap.Width; + + public int PixelHeight => PlatformBitmap.Height; + + public AG.Bitmap PlatformBitmap + { + get; + } + + public void Save(string fileName) + { + using (var f = File.OpenWrite(fileName)) + { + PlatformBitmap.Compress(AG.Bitmap.CompressFormat.Png, 100, f); + } + } + } +} \ No newline at end of file diff --git a/src/Android/Perspex.Android/CanvasRendering/BrushImpl.cs b/src/Android/Perspex.Android/CanvasRendering/BrushImpl.cs new file mode 100644 index 0000000000..7ddc3610d7 --- /dev/null +++ b/src/Android/Perspex.Android/CanvasRendering/BrushImpl.cs @@ -0,0 +1,363 @@ +using Android.Graphics; +using Android.Util; +using Perspex.Android.Platform.CanvasPlatform; +using Perspex.Android.Platform.Specific; +using Perspex.Media; +using System; +using System.Collections.Generic; +using System.Linq; +using AColor = Android.Graphics.Color; + +namespace Perspex.Android.CanvasRendering +{ + public enum BrushUsage + { + Fill, + Stroke, + Both + }; + + public interface INativePaintWrapper : IDisposable + { + Paint NativePaint { get; } + + void PushOpacity(double value); + + void PopOpacity(); + } + + public abstract class BrushImpl : IDisposable, INativePaintWrapper + { + public BrushImpl(Brush brush) + { + Brush = brush; + Brush.PropertyChanged += Brush_PropertyChanged; + Init(); + } + + protected virtual void Init() + { + Opacity = Brush.Opacity; + } + + private void Brush_PropertyChanged(object sender, PerspexPropertyChangedEventArgs e) + { + _isDirty = true; + } + + protected bool _isDirty = false; + + public Brush Brush { get; private set; } + + private double _oldOpacity = 1; + + public double Opacity { get; private set; } + + public virtual void Dispose() + { + Brush.PropertyChanged -= Brush_PropertyChanged; + Brush = null; + NativePaint?.Dispose(); + } + + private Paint _nativeBrush; + + public Paint NativePaint + { + get + { + if (_nativeBrush == null) + { + _nativeBrush = new Paint(PaintFlags.AntiAlias); + _isDirty = true; + } + + if (_isDirty) + { + Apply(); + } + + return _nativeBrush; + } + } + + public Paint.Style Style { get; set; } + + public virtual void ApplyTo(Paint paint) + { + paint.Alpha = AndroidGraphicsExtensions.OpacityToAndroidAlfa(Opacity); + paint.SetStyle(Style); + } + + private void Apply() + { + _isDirty = false; + ApplyTo(NativePaint); + } + + private static readonly BrushImpl FallbackBrush = new SolidColorBrushImpl(new SolidColorBrush(Colors.Transparent)); + + public static BrushImpl Create(Brush brush, Size dstRect, Paint.Style style) + { + //Size dstRect = PointService.Instance.PerspexToNative(pdstRect); + BrushImpl impl = null; + + if (brush is SolidColorBrush) + { + impl = new SolidColorBrushImpl(brush as SolidColorBrush); + } + else if (brush is LinearGradientBrush) + { + impl = new LinearGradientBrushImpl(brush as LinearGradientBrush, dstRect); + } + else if (brush is RadialGradientBrush) + { + impl = new RadialGradientBrushImpl(brush as RadialGradientBrush, dstRect); + } + else if (brush is ImageBrush) + { + impl = new ImageBrushImpl(brush as ImageBrush); + } + else if (brush is VisualBrush) + { + // TODO: Implement me + Log.Debug("REND", "VisualBrush not implemented"); + impl = FallbackBrush; + } + else + { + impl = new SolidColorBrushImpl(null); + } + + impl.Style = style; + + return impl; + } + + private Stack _opacityStack = new Stack(); + + public void PushOpacity(double value) + { + _opacityStack.Push(this.Opacity); + Opacity = Opacity * value; + _isDirty = value != 1 ? true : false; + } + + public void PopOpacity() + { + double newOpacity = _opacityStack.Pop(); + if (newOpacity != Opacity) + { + Opacity = newOpacity; + _isDirty = true; + } + } + } + + public class PenImpl : IDisposable, INativePaintWrapper + { + public PenImpl(Pen pen, Size dstRect, Paint.Style style) + { + this.Pen = pen; + if (this.Pen.Brush != null) + { + this.Brush = BrushImpl.Create(this.Pen.Brush, dstRect, style); + } + } + + private BrushImpl Brush { get; set; } + + public Pen Pen { get; } + + private Paint _nativePaint; + + public Paint NativePaint + { + get + { + if (_nativePaint == null) + { + _nativePaint = new Paint(PaintFlags.AntiAlias); + Apply(); + } + + return _nativePaint; + } + set + { + this._nativePaint = value; + } + } + + public void PushOpacity(double opacity) + { + Brush?.PushOpacity(opacity); + } + + public void PopOpacity() + { + Brush?.PopOpacity(); + } + + private void Apply() + { + if (Pen.DashStyle?.Dashes != null && Pen.DashStyle.Dashes.Count > 0) + { + var cray = Pen.DashStyle.Dashes.Select(d => (float)d).ToArray(); + NativePaint.SetPathEffect(new DashPathEffect(cray, (float)Pen.DashStyle.Offset)); + } + + NativePaint.StrokeWidth = PointUnitService.Instance.PerspexToNativeXF(Pen.Thickness); + NativePaint.StrokeMiter = (float)Pen.MiterLimit; + + NativePaint.StrokeJoin = Pen.LineJoin.ToAndroidGraphics(); + NativePaint.StrokeCap = Pen.StartLineCap.ToAndroidGraphics(); + + if (Brush != null) + { + Brush.ApplyTo(NativePaint); + } + } + + public void Dispose() + { + Brush?.Dispose(); + } + } + + public class SolidColorBrushImpl : BrushImpl + { + private AColor _nativeColor; + + public SolidColorBrushImpl(SolidColorBrush brush) : base(brush) + { + } + + public override void ApplyTo(Paint paint) + { + paint.Color = _nativeColor; + base.ApplyTo(paint); + } + + protected override void Init() + { + base.Init(); + var brush = Brush as SolidColorBrush; + + _nativeColor = brush.Color.ToAndroidGraphics(); + } + } + + public class ShaderBrushImpl : BrushImpl + { + protected Shader Shader { get; set; } + + protected Size DestinationSize { get; } + + public ShaderBrushImpl(Brush brush, Size destinationSize) : base(brush) + { + DestinationSize = destinationSize; + } + + public override void ApplyTo(Paint paint) + { + base.ApplyTo(paint); + + if (Shader != null) + { + paint.SetShader(Shader); + } + else + { + //we had some problem better set something + paint.Color = new AColor(0, 0, 0, 255); + } + } + + public override void Dispose() + { + Shader?.Dispose(); + base.Dispose(); + } + } + + public class LinearGradientBrushImpl : ShaderBrushImpl + { + public LinearGradientBrushImpl(LinearGradientBrush brush, Size destinationSize) : base(brush, destinationSize) + { + } + + protected override void Init() + { + base.Init(); + var brush = Brush as LinearGradientBrush; + int n = brush.GradientStops.Count; + var offsets = new float[n]; + var colors = new int[n]; + for (var i = 0; i < n; i++) + { + var s = brush.GradientStops[i]; + offsets[i] = (float)s.Offset; + colors[i] = (int)s.Color.ToUint32(); + } + + var startPoint = brush.StartPoint.ToPixels(DestinationSize); + var endPoint = brush.EndPoint.ToPixels(DestinationSize); + + this.Shader = new LinearGradient( + (float)startPoint.X, (float)startPoint.Y, + (float)endPoint.X, (float)endPoint.Y, + colors, + offsets, + Shader.TileMode.Clamp); + } + } + + public class RadialGradientBrushImpl : ShaderBrushImpl + { + public RadialGradientBrushImpl(RadialGradientBrush brush, Size destinationSize) : base(brush, destinationSize) + { + } + + protected override void Init() + { + base.Init(); + var brush = Brush as RadialGradientBrush; + int n = brush.GradientStops.Count; + var offsets = new float[n]; + var colors = new int[n]; + + for (var i = 0; i < n; i++) + { + var s = brush.GradientStops[i]; + offsets[i] = (float)s.Offset; + colors[i] = (int)s.Color.ToUint32(); + } + + var centerPoint = brush.Center.ToPixels(DestinationSize); + double radius = brush.Center.Unit == RelativeUnit.Absolute ? brush.Radius : Math.Max(brush.Radius * DestinationSize.Width, brush.Radius * DestinationSize.Height); + + Shader = new RadialGradient( + (float)centerPoint.X, (float)centerPoint.Y, + (float)brush.Radius, + colors, + offsets, + Shader.TileMode.Clamp); + } + } + + public class ImageBrushImpl : ShaderBrushImpl + { + public ImageBrushImpl(ImageBrush brush) : base(brush, default(Size)) + { + } + + protected override void Init() + { + base.Init(); + var brush = Brush as ImageBrush; + var bmp = brush.Source.PlatformImpl as BitmapImpl; + Shader = new BitmapShader(bmp.PlatformBitmap, brush.TileMode.ToAndroidGraphicsX(), brush.TileMode.ToAndroidGraphicsY()); + } + } +} \ No newline at end of file diff --git a/src/Android/Perspex.Android/CanvasRendering/DrawingContext.cs b/src/Android/Perspex.Android/CanvasRendering/DrawingContext.cs new file mode 100644 index 0000000000..522012d1d2 --- /dev/null +++ b/src/Android/Perspex.Android/CanvasRendering/DrawingContext.cs @@ -0,0 +1,248 @@ +using Android.Graphics; +using Perspex.Android.Platform.CanvasPlatform; +using Perspex.Android.Platform.Specific; +using Perspex.Media; +using Perspex.Media.Imaging; +using System; +using System.Collections.Generic; +using System.Reactive.Disposables; + +namespace Perspex.Android.CanvasRendering +{ + public class DrawingContextImpl : IDrawingContextImpl + { + private double _currentOpacity = 1.0f; + + private double CurrentOpacity + { + get { return _currentOpacity; } + set + { + _currentOpacity = value; + } + } + + public Canvas _canvas; + private Dictionary _visualCaches; + private Dictionary _cache; + + public DrawingContextImpl(Canvas canvas, Dictionary visualCaches) + { + _canvas = canvas; + _visualCaches = visualCaches; + _cache = visualCaches ?? new Dictionary(); + } + + public void Dispose() + { + _canvas = null; + if (_visualCaches == null) + { + DisposeCaches(); + } + } + + private void DisposeCaches() + { + foreach (var d in _cache) + { + d.Value.Dispose(); + } + _cache.Clear(); + } + + private Matrix _currentTransform = Matrix.Identity; + + public Matrix Transform + { + get { return _currentTransform; } + set + { + if (_currentTransform == value) + return; + _currentTransform = value; + _canvas.Matrix = value.ToAndroidGraphics(); + } + } + + public void DrawImage(IBitmap source, double opacity, Rect sourceRect, Rect destRect) + { + var nativeBitmap = (source.PlatformImpl as BitmapImpl).PlatformBitmap; + _canvas.DrawBitmap(nativeBitmap, sourceRect.ToAndroidGraphics(true), destRect.ToAndroidGraphicsF(), new Paint() { Alpha = AndroidGraphicsExtensions.OpacityToAndroidAlfa(opacity * CurrentOpacity) }); + } + + public void DrawLine(Pen pen, Point pp1, Point pp2) + { + var p1 = PointUnitService.Instance.PerspexToNative(pp1); + var p2 = PointUnitService.Instance.PerspexToNative(pp2); + + var Rect = new Rect(p1.X, p1.Y, p2.X, p2.Y); + PenImpl np; + using ((GetPen(pen, new Size(Rect.Width, Rect.Height), out np))) + { + _canvas.DrawLine((float)p1.X, (float)p1.Y, (float)p2.X, (float)p2.Y, np.NativePaint); + } + } + + public void DrawGeometry(Brush brush, Pen pen, Geometry geometry) + { + var impl = geometry.PlatformImpl as StreamGeometryImpl; + + if (brush != null) + { + BrushImpl nb; + using (var b = GetBrush(brush, geometry.Bounds.Size, out nb)) + { + _canvas.DrawPath(impl.Path, nb.NativePaint); + } + } + + //if the pen brush is null and thickness null on android + //the shape is filled with black !! better not draw the stroke!!! + if (pen?.Brush != null && pen?.Thickness > 0) + { + PenImpl np; + using (var p = GetPen(pen, geometry.Bounds.Size, out np)) + { + _canvas.DrawPath(impl.Path, np.NativePaint); + } + } + } + + public void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0) + { + PenImpl np; + using (GetPen(pen, rect.Size, out np)) + { + if (cornerRadius == 0) + { + _canvas.DrawRect(rect.ToAndroidGraphicsF(), np.NativePaint); + } + else + { + _canvas.DrawRoundRect(rect.ToAndroidGraphicsF(), cornerRadius, cornerRadius, np.NativePaint); + } + } + } + + public void DrawText(Brush foreground, Point origin, FormattedText text) + { + if (!string.IsNullOrEmpty(text.Text)) + { + var ap = origin.ToAndroidGraphics(); + var impl = (FormattedTextImpl)text.PlatformImpl; + _canvas.Save(); + BrushImpl nb; + using (var b = GetBrush(foreground, impl.Measure(), out nb)) + { + nb.ApplyTo(impl.TextPaint); + _canvas.Translate((float)ap.X, (float)ap.Y); + impl.TextLayout.Draw(_canvas); + } + _canvas.Restore(); + } + } + + public void FillRectangle(Brush brush, Rect rect, float cornerRadius = 0) + { + BrushImpl np; + using (var b = GetBrush(brush, rect.Size, out np)) + { + var ar = rect.ToAndroidGraphicsF(); + if (cornerRadius == 0) + { + _canvas.DrawRect(ar, np.NativePaint); + } + else + { + var cr = PointUnitService.Instance.PerspexToNativeXF(cornerRadius); + _canvas.DrawRoundRect(ar, cr, cr, np.NativePaint); + //Canvas.DrawPath(RoundRectPath(rect, cornerRadius), nb.NativePaint); + } + } + } + + private Rect _clip; + + public void PushClip(Rect clip) + { + _canvas.Save(); + _canvas.ClipRect(clip.ToAndroidGraphics()); + _clip = clip; + } + + public void PopClip() => _canvas.Restore(); + + private Stack _opacityStack = new Stack(); + + public void PushOpacity(double opacity) + { + _opacityStack.Push(CurrentOpacity); + CurrentOpacity = CurrentOpacity * opacity; + } + + public void PopOpacity() => CurrentOpacity = _opacityStack.Pop(); + + //private Path RoundRectPath(Rect rc, float radius) + //{ + // var x = (float)rc.TopLeft.X; + // var y = (float)rc.TopLeft.Y; + // var width = (float)rc.Width; + // var height = (float)rc.Height; + // var rx = radius; + // var ry = radius; + // var path = new Path(); + // path.MoveTo(x + rx, y); + // path.LineTo(x + width - rx, y + 0); + // path.QuadTo(x + width, y, x + width, y + ry); + // path.LineTo(x + width, y + height - ry); + // path.QuadTo(x + width, y + height, x + width - rx, y + height); + // path.LineTo(x + rx, y + height); + // path.QuadTo(x, y + height, x, y + height - ry); + // path.LineTo(x, y + ry); + // path.QuadTo(x, y, x + rx, y); + // return path; + //} + + private IDisposable GetPen(Pen pen, Size dstRect, out PenImpl result) + { + var penImpl = GetOrCreatePaintWrapper(pen, () => new PenImpl(pen, dstRect, Paint.Style.Stroke)); + + penImpl.PushOpacity(CurrentOpacity); + + result = penImpl; + + return Disposable.Create(() => penImpl.PopOpacity()); + } + + private IDisposable GetBrush(Brush brush, Size dstRect, out BrushImpl result) + { + var brushImpl = GetOrCreatePaintWrapper(brush, () => BrushImpl.Create(brush, dstRect, Paint.Style.Fill)); + + brushImpl.PushOpacity(CurrentOpacity); + + result = brushImpl; + + return Disposable.Create(() => brushImpl.PopOpacity()); + } + + private T GetOrCreatePaintWrapper(object key, Func createFunc) where T : INativePaintWrapper + { + IDisposable cachedVisual; + T result; + + if (!_cache.TryGetValue(key, out cachedVisual)) + { + result = createFunc(); + + _cache[key] = result; + } + else + { + result = (T)cachedVisual; + } + + return result; + } + } +} \ No newline at end of file diff --git a/src/Android/Perspex.Android/CanvasRendering/FormattedTextImpl.cs b/src/Android/Perspex.Android/CanvasRendering/FormattedTextImpl.cs new file mode 100644 index 0000000000..621812709f --- /dev/null +++ b/src/Android/Perspex.Android/CanvasRendering/FormattedTextImpl.cs @@ -0,0 +1,205 @@ +using Android.Graphics; +using Android.Text; +using Perspex.Android.Platform.CanvasPlatform; +using Perspex.Android.Platform.Specific; +using Perspex.Media; +using Perspex.Platform; +using System; +using System.Collections.Generic; +using AAllignment = Android.Text.Layout.Alignment; +using ARect = Android.Graphics.Rect; +using ATextPaint = Android.Text.TextPaint; + +namespace Perspex.Android.CanvasRendering +{ + public class FormattedTextImpl : IFormattedTextImpl + { + public FormattedTextImpl( + string text, + string fontFamily, + double fontSize, + FontStyle fontStyle, + TextAlignment textAlignment, + FontWeight fontWeight) + { + String = text; + TextPaint = new ATextPaint { TextAlign = textAlignment.ToAndroidGraphics() }; + var typefaceStyle = fontStyle.ToAndroidGraphicsTypefaceStyle(fontWeight); + var typeface = fontStyle.ToAndroidGraphicsTypeface(fontWeight); + TextPaint.SetTypeface(typeface); + TextPaint.TextSize = PointUnitService.Instance.PerspexToNativeFontSize(fontSize); + + Aligment = textAlignment.ToAndroidGraphicsLayoutAligment(); + + var rect = new ARect(); + TextPaint.GetTextBounds(String, 0, String.Length, rect); + //or use measure text to give us other opinion for the text ??? + //double tw = TextFormatting.MeasureText(String, 0, String.Length); + + //give few pixels in advance just in case + _initialSize = PointUnitService.Instance.NativeToPerspex(new Size(rect.Right, rect.Bottom)); + Constraint = _initialSize; + } + + private AAllignment Aligment; + + public string String { get; } + + public ATextPaint TextPaint { get; } + + private Size _initialSize; + + public Size Constraint { get; set; } + + public void Dispose() + { + } + + private StaticLayout _layout = null; + private double _layoutBuildWidth = 0; + + private Dictionary> _localCachedValues = new Dictionary>(); + + private T GetLocalCachedValue(string name, object forKey, Func getFunc) + { + KeyValuePair kvp = default(KeyValuePair); + + bool create = true; + if (_localCachedValues.ContainsKey(name)) + { + kvp = _localCachedValues[name]; + create = kvp.Key != forKey; + } + + if (create) + { + kvp = new KeyValuePair(forKey, getFunc()); + _localCachedValues[name] = kvp; + } + + return (T)kvp.Value; + } + + public StaticLayout TextLayout + { + get + { + if (_layout == null || Math.Abs(_layoutBuildWidth - Constraint.Width) > 1) + { + var newWidth = (Constraint.Width > int.MaxValue ? _initialSize.Width : Constraint.Width) + 10; + + double widthDiff = _layoutBuildWidth - newWidth; + + if (_layout == null || widthDiff < 0) + { + _layoutBuildWidth = newWidth; + _layout = new StaticLayout(String, TextPaint, PointUnitService.Instance.PerspexToNativeXInt(_layoutBuildWidth), Aligment, 1.0f, 0.0f, false); + _localCachedValues.Clear(); + } + } + + return _layout; + } + } + + public IEnumerable GetLines() + { + return GetLocalCachedValue("GetLines", "GetLines", () => + { + List result = new List(); + for (int i = 0; i < TextLayout.LineCount; i++) + { + int start = TextLayout.GetLineStart(i); + int end = TextLayout.GetLineEnd(i); + //string lineContent = String.Substring(start, end - start); + result.Add(new FormattedTextLine(end - start, PointUnitService.Instance.NativeToPerspexY(TextLayout.GetLineTop(i) - TextLayout.GetLineBottom(i)))); + } + return result; + }); + } + + public TextHitTestResult HitTestPoint(Point ppoint) + { + return GetLocalCachedValue("HitTestPoint", ppoint, () => + { + var point = PointUnitService.Instance.PerspexToNative(ppoint); + + bool inside = point.X >= 0 && point.X <= TextLayout.Width && point.Y >= 0 && point.Y <= TextLayout.Height; + int index = -1; + + if (inside) + { + int line = this.TextLayout.GetLineForVertical(Convert.ToInt32(point.Y)); + index = this.TextLayout.GetOffsetForHorizontal(line, (float)point.X); + } + + return new TextHitTestResult { IsInside = inside, IsTrailing = false, TextPosition = index }; + }); + } + + public Rect HitTestTextPosition(int index) + { + return + GetLocalCachedValue("HitTestTextPosition", index, () => + { + int line = TextLayout.GetLineForOffset(index); + int top = TextLayout.GetLineTop(line); + int bottom = TextLayout.GetLineBottom(line); + double left = TextLayout.GetPrimaryHorizontal(index); + + double right = String.Length > index + 1 ? TextLayout.GetPrimaryHorizontal(index + 1) : left; + + return PointUnitService.Instance.NativeToPerspex(new Rect(left, top, right - left, bottom - top)); + }); + } + + public IEnumerable HitTestTextRange(int index, int length) + { + int startLine = TextLayout.GetLineForOffset(index); + int endIndex = index + length; + int endLine = TextLayout.GetLineForOffset(endIndex); + + for (int i = startLine; i <= endLine; i++) + { + int top = TextLayout.GetLineTop(i); + int bottom = TextLayout.GetLineBottom(i); + + double left = 0, right = 0; + + if (startLine == i) + { + left = TextLayout.GetPrimaryHorizontal(index); + } + else + { + left = TextLayout.GetLineLeft(i); + } + + if (endLine == i) + { + right = TextLayout.GetPrimaryHorizontal(endIndex); + } + else + { + right = TextLayout.GetLineRight(i); + } + + yield return PointUnitService.Instance.NativeToPerspex(new Rect(left, top, right - left, bottom - top)); + } + } + + public Size Measure() + { + return PointUnitService.Instance.NativeToPerspex(new Size(this.TextLayout.Width, this.TextLayout.Height)); + } + + public void SetForegroundBrush(Brush brush, int startIndex, int length) + { + var scb = brush as SolidColorBrush; + if (scb != null) + { + TextPaint.Color = scb.Color.ToAndroidGraphics(); + } + } + } +} \ No newline at end of file diff --git a/src/Android/Perspex.Android/CanvasRendering/RenderTarget.cs b/src/Android/Perspex.Android/CanvasRendering/RenderTarget.cs new file mode 100644 index 0000000000..059f858b7a --- /dev/null +++ b/src/Android/Perspex.Android/CanvasRendering/RenderTarget.cs @@ -0,0 +1,63 @@ +// Copyright (c) The Perspex Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Perspex.Android.Platform.CanvasPlatform; +using Perspex.Android.Platform.Specific; +using Perspex.Platform; +using AG = Android.Graphics; + +namespace Perspex.Android.CanvasRendering +{ + /// + /// A android render target. + /// + public class RenderTarget : IRenderTarget + { + private readonly AG.Bitmap _nativeBitmap; + private IAndroidCanvasView _window; + + /// + /// Initializes a new instance of the class. + /// + /// The window. + /// The width of the window. + /// The height of the window. + public RenderTarget(IAndroidCanvasView window) + { + _window = window; + } + + public RenderTarget(AG.Bitmap surface) + { + _nativeBitmap = surface; + } + + /// + /// Resizes the renderer. + /// + /// The new width. + /// The new height. + public void Resize(int width, int height) + { + if (_nativeBitmap != null) + { + this._nativeBitmap.Reconfigure(width, height, AG.Bitmap.Config.Argb8888); + } + } + + /// + /// Creates a DrawingContext that targets a platform-specific resource. + /// + /// A surface wrapped in an . + public Perspex.Media.DrawingContext CreateDrawingContext() + { + var canvas = _nativeBitmap != null ? new AG.Canvas(this._nativeBitmap) : _window.Canvas; + return new Perspex.Media.DrawingContext(new DrawingContextImpl(canvas, _window != null ? _window.VisualCaches : null)); + } + + public void Dispose() + { + this._nativeBitmap?.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/Android/Perspex.Android/CanvasRendering/RenderTargetBitmapImpl.cs b/src/Android/Perspex.Android/CanvasRendering/RenderTargetBitmapImpl.cs new file mode 100644 index 0000000000..900798f9ba --- /dev/null +++ b/src/Android/Perspex.Android/CanvasRendering/RenderTargetBitmapImpl.cs @@ -0,0 +1,49 @@ +// Copyright (c) The Perspex Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Perspex.Platform; +using System.IO; +using AG = Android.Graphics; + +namespace Perspex.Android.CanvasRendering +{ + public class RenderTargetBitmapImpl : IRenderTargetBitmapImpl + { + private readonly RenderTarget _renderTarget; + + public RenderTargetBitmapImpl(AG.Bitmap surface) + { + Surface = surface; + _renderTarget = new RenderTarget(Surface); + } + + public int PixelWidth => Surface.Width; + + public int PixelHeight => Surface.Height; + + public void Dispose() + { + _renderTarget.Dispose(); + } + + public AG.Bitmap Surface { get; } + + public void Save(string fileName) + { + using (var f = File.OpenWrite(fileName)) + { + Surface.Compress(AG.Bitmap.CompressFormat.Png, 100, f); + } + } + + public Perspex.Media.DrawingContext CreateDrawingContext() + { + return _renderTarget.CreateDrawingContext(); + } + + //void IRenderTarget.Resize(int width, int height) + //{ + // this._renderTarget.Resize(width, height); + //} + } +} \ No newline at end of file diff --git a/src/Android/Perspex.Android/CanvasRendering/StreamGeometryImpl.cs b/src/Android/Perspex.Android/CanvasRendering/StreamGeometryImpl.cs new file mode 100644 index 0000000000..a57478a331 --- /dev/null +++ b/src/Android/Perspex.Android/CanvasRendering/StreamGeometryImpl.cs @@ -0,0 +1,158 @@ +using Android.Graphics; +using Perspex.Media; +using Perspex.Platform; +using System; +using System.Linq; +using APath = Android.Graphics.Path; +using ARect = Android.Graphics.RectF; + +namespace Perspex.Android.CanvasRendering +{ + public class StreamGeometryImpl : IStreamGeometryImpl + { + private readonly StreamGeometryContextImpl _impl; + private Matrix _transform = Matrix.Identity; + + public StreamGeometryImpl() + { + _impl = new StreamGeometryContextImpl(null); + } + + public StreamGeometryImpl(StreamGeometryContextImpl impl) + { + _impl = impl; + } + + public APath Path => _impl.Path; + + public Rect Bounds => _impl.CalculatedBounds; + + public Matrix Transform + { + get { return _transform; } + set + { + if (value == Transform) return; + if (!value.IsIdentity) + { + _transform = value; + + Path.Transform(_transform.ToAndroidGraphics()); + } + } + } + + public Rect GetRenderBounds(double strokeThickness) + { + return Bounds.Inflate(strokeThickness); + } + + public IStreamGeometryImpl Clone() + { + return new StreamGeometryImpl(_impl); + } + + public IStreamGeometryContextImpl Open() + { + return _impl; + } + } + + public class StreamGeometryContextImpl : IStreamGeometryContextImpl + { + private double _top = double.MaxValue; + private double _left = double.MaxValue; + private double _bottom = 1; + private double _right = 1; + + public StreamGeometryContextImpl(APath path = null) + { + Path = path != null ? new APath(path) : new APath(); + Path.AddRect(new ARect(), APath.Direction.Cw); + } + + public APath Path { get; } + + public Rect PathBounds + { + get + { + var _bounds = new ARect(); + + Path.ComputeBounds(_bounds, true); + return _bounds.ToPerspex(); + } + } + + public Rect CalculatedBounds + { + get + { + return new Rect(_left, _top, _right - _left, _bottom - _top); + } + } + + public void Dispose() + { + } + + private void ProcessBounds(params Point[] points) + { + _top = Math.Min(_top, points.Min(p => p.Y)); + _left = Math.Min(_left, points.Min(p => p.X)); + _bottom = Math.Max(_bottom, points.Max(p => p.Y)); + _right = Math.Max(_right, points.Max(p => p.X)); + } + + public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection) + { + ProcessBounds(point); + + Path.ArcTo(new Rect(point, size).ToAndroidGraphicsF(), 0, (float)rotationAngle); + } + + public void BeginFigure(Point pstartPoint, bool isFilled) + { + ProcessBounds(pstartPoint); + var sp = pstartPoint.ToAndroidGraphics(); + + Path.MoveTo(sp.X, sp.Y); + } + + public void BezierTo(Point ppoint1, Point ppoint2, Point ppoint3) + { + ProcessBounds(ppoint1, ppoint2, ppoint3); + var point1 = ppoint1.ToAndroidGraphics(); + var point2 = ppoint2.ToAndroidGraphics(); + var point3 = ppoint3.ToAndroidGraphics(); + + Path.CubicTo(point1.X, point1.Y, + point2.X, point2.Y, + point3.X, point3.Y); + } + + public void LineTo(Point ppoint) + { + ProcessBounds(ppoint); + var point = ppoint.ToAndroidGraphics(); + + Path.LineTo(point.X, point.Y); + } + + public void EndFigure(bool isClosed) + { + if (Path != null) + if (isClosed) + Path.Close(); + } + + public void QuadTo(Point ppoint1, Point ppoint2) + { + ProcessBounds(ppoint1, ppoint2); + var point1 = ppoint1.ToAndroidGraphics(); + var point2 = ppoint2.ToAndroidGraphics(); + + Path.QuadTo(point1.X, point1.Y, point2.X, point2.Y); + } + } +} \ No newline at end of file diff --git a/src/Android/Perspex.Android/CursorFactory.cs b/src/Android/Perspex.Android/CursorFactory.cs new file mode 100644 index 0000000000..239e1b32c3 --- /dev/null +++ b/src/Android/Perspex.Android/CursorFactory.cs @@ -0,0 +1,12 @@ +using System; +using Perspex.Input; +using Perspex.Platform; + +namespace Perspex.Android +{ + internal class CursorFactory : IStandardCursorFactory + { + public IPlatformHandle GetCursor(StandardCursorType cursorType) + => new PlatformHandle(IntPtr.Zero, "ZeroCursor"); + } +} \ No newline at end of file diff --git a/src/Android/Perspex.Android/Perspex.Android.csproj b/src/Android/Perspex.Android/Perspex.Android.csproj new file mode 100644 index 0000000000..4b39caf2d7 --- /dev/null +++ b/src/Android/Perspex.Android/Perspex.Android.csproj @@ -0,0 +1,171 @@ + + + + Debug + AnyCPU + 8.0.30703 + 2.0 + {7B92AF71-6287-4693-9DCB-BD5B6E927E23} + {EFBA0AD7-5A72-4C68-AF49-83D382785DCF};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + Library + Properties + Perspex.Android + Perspex.Android + 512 + Resources\Resource.Designer.cs + Off + False + v4.4 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + ..\..\..\packages\Rx-Core.2.2.5\lib\portable-windows8+net45+wp8\System.Reactive.Core.dll + True + + + ..\..\..\packages\Rx-Interfaces.2.2.5\lib\portable-windows8+net45+wp8\System.Reactive.Interfaces.dll + True + + + ..\..\..\packages\Rx-Linq.2.2.5\lib\portable-windows8+net45+wp8\System.Reactive.Linq.dll + True + + + ..\..\..\packages\Rx-PlatformServices.2.2.5\lib\portable-windows8+net45+wp8\System.Reactive.PlatformServices.dll + True + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {3e53a01a-b331-47f3-b828-4a5717e77a24} + Perspex.Markup.Xaml + + + {d211e587-d8bc-45b9-95a4-f297c8fa5200} + Perspex.Animation + + + {799a7bb5-3c2c-48b6-85a7-406a12c420da} + Perspex.Application + + + {b09b78d8-9b26-48b0-9149-d64a2f120f3f} + Perspex.Base + + + {d2221c82-4a25-4583-9b43-d791e3f6820c} + Perspex.Controls + + + {7062ae20-5dcc-4442-9645-8195bdece63e} + Perspex.Diagnostics + + + {62024b2d-53eb-4638-b26b-85eeaa54866e} + Perspex.Input + + + {6b0ed19d-a08b-461c-a9d9-a9ee40b0c06b} + Perspex.Interactivity + + + {42472427-4774-4c81-8aff-9f27b8e31721} + Perspex.Layout + + + {6417b24e-49c2-4985-8db2-3ab9d898ec91} + Perspex.ReactiveUI + + + {eb582467-6abb-43a1-b052-e981ba910e3a} + Perspex.SceneGraph + + + {f1baa01a-f176-4c6a-b39d-5b40bb1b148f} + Perspex.Styling + + + + + + + \ No newline at end of file diff --git a/src/Android/Perspex.Android/Platform/CanvasPlatform/IAndroidCanvasView.cs b/src/Android/Perspex.Android/Platform/CanvasPlatform/IAndroidCanvasView.cs new file mode 100644 index 0000000000..54790d07f5 --- /dev/null +++ b/src/Android/Perspex.Android/Platform/CanvasPlatform/IAndroidCanvasView.cs @@ -0,0 +1,14 @@ +using Android.Graphics; +using Perspex.Android.Platform.Specific; +using System; +using System.Collections.Generic; + +namespace Perspex.Android.Platform.CanvasPlatform +{ + public interface IAndroidCanvasView : IAndroidView + { + Canvas Canvas { get; } + + Dictionary VisualCaches { get; } + } +} \ No newline at end of file diff --git a/src/Android/Perspex.Android/Platform/CanvasPlatform/IAndroidViewSupportRender.cs b/src/Android/Perspex.Android/Platform/CanvasPlatform/IAndroidViewSupportRender.cs new file mode 100644 index 0000000000..a409afebf6 --- /dev/null +++ b/src/Android/Perspex.Android/Platform/CanvasPlatform/IAndroidViewSupportRender.cs @@ -0,0 +1,13 @@ +using Android.Graphics; + +namespace Perspex.Android.Platform.CanvasPlatform +{ + public interface IAndroidViewSupportRender + { + void PreRender(Canvas canvas, Rect rect); + + void Render(Canvas canvas, Rect rect); + + void PostRender(Canvas canvas, Rect rect); + } +} \ No newline at end of file diff --git a/src/Android/Perspex.Android/Platform/CanvasPlatform/MainWindowImpl.cs b/src/Android/Perspex.Android/Platform/CanvasPlatform/MainWindowImpl.cs new file mode 100644 index 0000000000..0eda56c3bd --- /dev/null +++ b/src/Android/Perspex.Android/Platform/CanvasPlatform/MainWindowImpl.cs @@ -0,0 +1,71 @@ +using Android.Views; +using Perspex.Android.Platform.Specific; +using Perspex.Android.Platform.Specific.Helpers; +using Perspex.Input; +using Perspex.Platform; +using AG = Android.Graphics; + +namespace Perspex.Android.Platform.CanvasPlatform +{ + public class MainWindowImpl : + //SurfaceWindowImpl + WindowImpl + , IWindowImpl + { + private IPointUnitService _pointService = PointUnitService.Instance; + + protected override void Init() + { + base.Init(); + + HandleEvents = true; + _keyboardHelper.ActivateAutoShowKeybord(); + } + + private int _statusbarHeight = -1; + + private int StatusBarHeight + { + get + { + if (_statusbarHeight < 0) + { + var a = PerspexLocator.Current.GetService().Activity; + AG.Rect rectangle = new AG.Rect(); + Window window = a.Window; + window.DecorView.GetWindowVisibleDisplayFrame(rectangle); + int statusBarHeight = rectangle.Top; + //int contentViewTop = + // window.FindViewById(Window.IdAndroidContent).Top; + //int titleBarHeight = contentViewTop - statusBarHeight; + _statusbarHeight = statusBarHeight; + } + return _statusbarHeight; + } + } + + public override Point GetPerspexPointFromEvent(MotionEvent e) + { + //toolbar is on top of android screen and it's height is 50 ??? + //may be dependent from dpi ??? + //return base.GetPerspexPointFromEvent(e); + //return new Point(e.GetX(0), e.GetY(0) + StatusBarHeight); + + var point = new Point(e.GetX(0), e.GetY(0) + (DrawType == ViewDrawType.CanvasOnDraw ? StatusBarHeight : 0)); + return _pointService.NativeToPerspex(point); + } + + void ITopLevelImpl.Show() + { + (Parent as ViewGroup)?.RemoveAllViews(); + PerspexLocator.Current.GetService().ContentView = this; + this.Visibility = ViewStates.Visible; + } + + void ITopLevelImpl.SetInputRoot(IInputRoot inputRoot) + { + base.SetInputRoot(inputRoot); + _keyboardHelper.UpdateKeyboardState(inputRoot); + } + } +} \ No newline at end of file diff --git a/src/Android/Perspex.Android/Platform/CanvasPlatform/PointUnitService.cs b/src/Android/Perspex.Android/Platform/CanvasPlatform/PointUnitService.cs new file mode 100644 index 0000000000..e782f83f3c --- /dev/null +++ b/src/Android/Perspex.Android/Platform/CanvasPlatform/PointUnitService.cs @@ -0,0 +1,184 @@ +using Perspex.Android.Platform.Specific; +using System; + +namespace Perspex.Android.Platform.CanvasPlatform +{ + public enum PointUnit + { + Pixel, + DP, //device independent pixel + Custom // CustomScale + } + + public interface IPointUnitService + { + double ScaleX { get; } + double ScaleY { get; } + double FontScale { get; } + + double NativeToPerspexX(double x); + + double NativeToPerspexY(double y); + + double PerspexToNativeX(double x); + + double PerspexToNativeY(double y); + + float PerspexToNativeFontSize(double fontSize); + } + + public class PointUnitService : IPointUnitService + { + public static IPointUnitService Instance => PerspexLocator.Current.GetService() ?? new PointUnitService(); + + private PointUnit _pointUnit; + + public PointUnit PointUnit + { + get { return _pointUnit; } + set + { + _pointUnit = value; + OnPointUnitChanged(); + } + } + + public double ScaleX { get; set; } + + public double ScaleY { get; set; } + + public double FontScale { get; set; } + + public PointUnitService() + { + PointUnit = AndroidPlatform.Instance.DefaultPointUnit; + //ScaleX = 1; + //ScaleY = 1; + ////FontScale = 1; + //ScaleX = 2; + //ScaleY = 2; + //FontScale = 2; + } + + private void OnPointUnitChanged() + { + switch (PointUnit) + { + case PointUnit.Pixel: + ScaleX = 1; + ScaleY = 1; + FontScale = 1; + break; + + case PointUnit.DP: + { + //get android metrics for device pixel format + var activity = PerspexLocator.Current.GetService().Activity; + var dm = activity.Resources.DisplayMetrics; + double scale = Math.Round((double)dm.ScaledDensity, 2); + ScaleX = scale; + ScaleY = scale; + FontScale = scale; + } + break; + + case PointUnit.Custom: + { + double scale = 2; + ScaleX = scale; + ScaleY = scale; + FontScale = scale; + } + break; + } + } + + public double NativeToPerspexX(double x) => ScaleX == 1 ? x : RoundToPerspex(x / ScaleX); + + public double NativeToPerspexY(double y) => ScaleY == 1 ? y : RoundToPerspex(y / ScaleY); + + public double PerspexToNativeX(double x) => ScaleX == 1 ? x : RoundToNative(ScaleX * x); + + public double PerspexToNativeY(double y) => ScaleY == 1 ? y : RoundToNative(ScaleY * y); + + public float PerspexToNativeFontSize(double fontSize) + { + if (FontScale == 1) return (float)fontSize; + return (float)Math.Round(FontScale * fontSize, 1); + } + + private static double RoundToPerspex(double v) + { + // return Math.Ceiling(v); + return Math.Round(v, 5); + } + + private static double RoundToNative(double v) + { + //return Math.Ceiling(v); + return Math.Round(v, 2); + } + } + + public static class PointServiceExtensions + { + //public static Matrix PerspexToNative(this IPointService ps, Matrix m) + //{ + // if (ps.ScaleX == ps.ScaleY && ps.ScaleX == 1) return m; + + // return new Matrix(m.M11, m.M12, m.M21, m.M22, m.M31 * ps.ScaleX, m.M32 * ps.ScaleY); + //} + + //public static double PerspexToNative(this IPointService service, double x) + //{ + // if (service.ScaleX == 1) return x; + // return x * service.ScaleX; + //} + + private static int RoundToInt(double v) + { + return (int)Math.Round(v, 0); + } + + public static double NativeToPerspex(this IPointUnitService ps, double x) => ps.NativeToPerspexX(x); + + public static float PerspexToNativeXF(this IPointUnitService ps, double x) => (float)ps.PerspexToNativeX(x); + + public static float PerspexToNativeYF(this IPointUnitService ps, double y) => (float)ps.PerspexToNativeY(y); + + public static int PerspexToNativeXInt(this IPointUnitService ps, double x) => RoundToInt(ps.PerspexToNativeX(x)); + + public static int PerspexToNativeYInt(this IPointUnitService ps, double y) => RoundToInt(ps.PerspexToNativeX(y)); + + public static Point NativeToPerspex(this IPointUnitService ps, Point point) => (ps.ScaleY == 1 && ps.ScaleX == 1) ? point : new Point(ps.NativeToPerspexX(point.X), ps.NativeToPerspexY(point.Y)); + + public static Point PerspexToNative(this IPointUnitService ps, Point point) => (ps.ScaleY == 1 && ps.ScaleX == 1) ? point : new Point(ps.PerspexToNativeX(point.X), ps.PerspexToNativeY(point.Y)); + + public static Size NativeToPerspex(this IPointUnitService service, Size size) + { + if (service.ScaleX == service.ScaleY && service.ScaleX == 1) return size; + + return new Size(size.Width / service.ScaleX, size.Height / service.ScaleY); + } + + public static Size PerspexToNative(this IPointUnitService service, Size size) + { + if (service.ScaleX == service.ScaleY && service.ScaleX == 1) return size; + return new Size(size.Width * service.ScaleX, size.Height * service.ScaleY); + } + + public static Rect NativeToPerspex(this IPointUnitService service, Rect rect) + { + if (service.ScaleX == service.ScaleY && service.ScaleX == 1) return rect; + + return new Rect(rect.X / service.ScaleX, rect.Y / service.ScaleY, rect.Width / service.ScaleX, rect.Height / service.ScaleY); + } + + public static Rect PerspexToNative(this IPointUnitService service, Rect rect) + { + if (service.ScaleX == service.ScaleY && service.ScaleX == 1) return rect; + + return new Rect(rect.X * service.ScaleX, rect.Y * service.ScaleY, rect.Width * service.ScaleX, rect.Height * service.ScaleY); + } + } +} \ No newline at end of file diff --git a/src/Android/Perspex.Android/Platform/CanvasPlatform/PopupImpl.cs b/src/Android/Perspex.Android/Platform/CanvasPlatform/PopupImpl.cs new file mode 100644 index 0000000000..e958e45862 --- /dev/null +++ b/src/Android/Perspex.Android/Platform/CanvasPlatform/PopupImpl.cs @@ -0,0 +1,137 @@ +// Copyright (c) The Perspex Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Android.Content; +using Android.Views; +using Android.Widget; +using Perspex.Android.Platform.Specific; +using Perspex.Input; +using Perspex.Input.Raw; +using Perspex.Platform; +using System; +using AG = Android.Graphics; + +namespace Perspex.Android.Platform.CanvasPlatform +{ + public class PopupImpl : PopupWindow, IPopupImpl, IPlatformHandle + { + private class PopupPerspexViewContent : WindowImpl + { + private Func _funcDispatchTouchEvent; + + public PopupPerspexViewContent(Context context, Func funcDispatchTouchEvent) : base(context) + { + this._funcDispatchTouchEvent = funcDispatchTouchEvent; + } + + public override bool DispatchTouchEvent(MotionEvent e) + { + bool? res = _funcDispatchTouchEvent(e); + + return res == null ? base.DispatchTouchEvent(e) : res.Value; + } + } + + private PopupPerspexViewContent content; + + private View mainView; + + //public PopupImpl(Context context) : base(context) + public PopupImpl() + { + ContentView = content = new PopupPerspexViewContent(PerspexLocator.Current.GetService().Activity, DispatchTouchEvent); + ContentView.Background = new AG.Drawables.ColorDrawable(AG.Color.Transparent); + mainView = PerspexLocator.Current.GetService().ContentView.View; + //Background = new AG.Drawables.ColorDrawable(AG.Color.); + } + + //public PopupImpl() : this(PerspexLocator.Current.GetService().Activity) + //{ + //} + + private bool? DispatchTouchEvent(MotionEvent e) + { + var p = content.GetPerspexPointFromEvent(e); + + Rect clientBounds = new Rect(ClientSize); + + if (clientBounds.Contains(p)) + { + return null; + } + + return mainView.DispatchTouchEvent(e); + } + + public Size MaxClientSize => content.MaxClientSize; + + public Action Activated { get { return (content as ITopLevelImpl).Activated; } set { (content as ITopLevelImpl).Activated = value; } } + + public Size ClientSize { get { return content.ClientSize; } set { content.ClientSize = value; } } + + public Action Closed { get { return content.Closed; } set { content.Closed = value; } } + + public Action Deactivated { get { return content.Deactivated; } set { content.Deactivated = value; } } + + public Action Input { get { return content.Input; } set { content.Input = value; } } + + public Action Paint { get { return content.Paint; } set { content.Paint = value; } } + + public Action Resized { get { return content.Resized; } set { content.Resized = value; } } + + IntPtr IPlatformHandle.Handle => content.Handle; + + IPlatformHandle ITopLevelImpl.Handle => content; + + public string HandleDescriptor => "PopupImpl"; + + public void Activate() => content.Activate(); + + public void Invalidate(Rect rect) => content.Invalidate(rect); + + public Point PointToScreen(Point point) + { + return content.PointToScreen(point); + } + + public void SetCursor(IPlatformHandle cursor) + { + content.SetCursor(cursor); + } + + public void SetInputRoot(IInputRoot inputRoot) + { + content.SetInputRoot(inputRoot); + } + + private Point position; + + public void SetPosition(Point p) + { + position = p; + } + + public void Show() + { + content.HandleEvents = true; + var ps = PointUnitService.Instance; + //content.Activate(); + Width = ps.PerspexToNativeXInt(MaxClientSize.Width - position.X); + Height = ps.PerspexToNativeYInt(MaxClientSize.Height - position.Y); + content.LayoutParameters = new ViewGroup.LayoutParams(Width, Height); + // content.ClientSize = new Size(MaxClientSize.Width - position.X, MaxClientSize.Height - position.Y); + //Width = Convert.ToInt32(ClientSize.Width); + //Height = Convert.ToInt32(ClientSize.Height); + + base.ShowAtLocation(mainView, GravityFlags.Top | GravityFlags.Left, ps.PerspexToNativeXInt(position.X), ps.PerspexToNativeYInt(position.Y)); + content.Invalidate(); + } + + public void Hide() + { + content.HandleEvents = false; + //content.Hide(); + base.Dismiss(); + } + } +} \ No newline at end of file diff --git a/src/Android/Perspex.Android/Platform/CanvasPlatform/Specific/Helpers/AndroidViewRenderHelpers.cs b/src/Android/Perspex.Android/Platform/CanvasPlatform/Specific/Helpers/AndroidViewRenderHelpers.cs new file mode 100644 index 0000000000..0cd7e891ff --- /dev/null +++ b/src/Android/Perspex.Android/Platform/CanvasPlatform/Specific/Helpers/AndroidViewRenderHelpers.cs @@ -0,0 +1,688 @@ +using Android.Graphics; +using Android.Runtime; +using Android.Text; +using Android.Util; +using Android.Views; +using Perspex.Android.CanvasRendering; +using Perspex.Android.Platform.Specific.Helpers; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using P = Perspex.Platform; + +namespace Perspex.Android.Platform.CanvasPlatform.Specific.Helpers +{ + public abstract class AndroidViewRenderHelper : Java.Lang.Object, IDisposable where TView : View, P.IWindowImpl, IAndroidViewSupportRender + { + private static bool _debug = false; + + protected AndroidViewRenderHelper(TView view) + { + this.View = view; + VisualCaches = null; + } + + public Dictionary VisualCaches { get; protected set; } + + public abstract void OnDraw(Canvas canvas); + + public abstract void Invalidate(Rect rect); + + public TView View { get; private set; } + + public ViewDrawType DrawType { get; private set; } + + public static AndroidViewRenderHelper Create(TView view, ViewDrawType drawType) + { + AndroidViewRenderHelper result; + switch (drawType) + { + case ViewDrawType.BitmapBackgroundRender: + result = new BitmapBackgroundRenderHelper(view); + break; + + case ViewDrawType.BitmapOnPreDraw: + result = new BitmapPreDrawRenderHelper(view); + break; + + case ViewDrawType.CanvasOnDraw: + result = new CanvasOnDrawAndroidViewRenderHelper(view); + break; + + default: + throw new NotSupportedException(); + } + + Init(result, drawType); + + return result; + } + + protected static void Init(AndroidViewRenderHelper helper, ViewDrawType drawType) + { + helper.DrawType = drawType; + helper.SetDebugInfo("DrawType", drawType.ToString()); + } + + public virtual new void Dispose() + { + View = null; + _debugInfoPaint?.Dispose(); + } + + public Dictionary DebugInfo { get; } = new Dictionary(); + + protected void SetDebugInfo(string key, string value) => DebugInfo[key] = value; + + private const int _avgValuesForStat = 50; + + private Dictionary> _renderStats = new Dictionary>(); + + protected void UpdateRenderStat(string key, double ms) + { + Queue statsQueue; + if (!_renderStats.TryGetValue(key, out statsQueue)) + { + statsQueue = new Queue(); + _renderStats[key] = statsQueue; + } + + statsQueue.Enqueue(ms); + if (statsQueue.Count > _avgValuesForStat) statsQueue.Dequeue(); + + SetDebugInfo(key, ms.ToString("0.0")); + SetDebugInfo($"a{key}", (statsQueue.Average()).ToString("0.0")); + } + + protected void RenderView(Canvas canvas, Rect rect) + { + DateTime begin = DateTime.Now; + + View.PreRender(canvas, rect); + View.Render(canvas, rect); + View.PostRender(canvas, rect); + + TimeSpan duration = DateTime.Now - begin; + + if (_debug) Log.Debug("AndroidViewRenderHelper", $"RenderView {duration}"); + + UpdateRenderStat("RenderView", duration.TotalMilliseconds); + UpdateRenderStat("mfps", 1000 / duration.TotalMilliseconds); + } + + private Paint _debugInfoPaint = null; + + public void DrawDebugInfoToCanvas(Canvas canvas) + { + if (!AndroidPlatform.Instance.DrawDebugInfo) return; + + if (DebugInfo.Count == 0) return; + + if (_debugInfoPaint == null) _debugInfoPaint = new TextPaint(PaintFlags.AntiAlias) { Color = Color.Red, TextAlign = Paint.Align.Left, TextSize = 20 }; + + var sb = new StringBuilder(); + foreach (var o in DebugInfo) + { + sb.Append($"{o.Key} = {o.Value}; "); + } + string sdb = sb.ToString(); + var r = new global::Android.Graphics.Rect(); + _debugInfoPaint.GetTextBounds(sdb, 0, sdb.Length, r); + + canvas.DrawText(sdb, View.Width - r.Right, View.Height - r.Bottom, _debugInfoPaint); + + if (_debug) Log.Debug("AndroidViewRenderHelper", sdb); + } + } + + public class CanvasOnDrawAndroidViewRenderHelper : AndroidViewRenderHelper where TView : View, P.IWindowImpl, IAndroidViewSupportRender + { + private Rect _rect; + + public CanvasOnDrawAndroidViewRenderHelper(TView view) : base(view) + { + } + + public override void Invalidate(Rect rect) + { + _rect = rect; + View.Invalidate(rect.ToAndroidGraphics()); + } + + public override void OnDraw(Canvas canvas) + { + RenderView(canvas, _rect); + } + + public override void Dispose() + { + base.Dispose(); + } + } + + public class BitmapBackgroundRenderHelper : AndroidViewRenderHelper where TView : View, P.IWindowImpl, IAndroidViewSupportRender + { + private bool _debug = WindowImpl._debug; + private Bitmap _bitmap; + private Rect _rect; + private Paint _paint = new Paint(); + private ConcurrentStack _invalidateRects = new ConcurrentStack(); + private object _invalidateRectsSync = new object(); + private object _drawSync = new object(); + private Stack _renderingBitmapsCache = new Stack(); + private Dictionary _bmCanvases = new Dictionary(); + private CancellationTokenSource _cancelationTokenSource; + + public BitmapBackgroundRenderHelper(TView view) : base(view) + { + _cancelationTokenSource = new CancellationTokenSource(); + VisualCaches = new Dictionary(); + Task.Factory.StartNew(RenderingLoop, _cancelationTokenSource.Token, _cancelationTokenSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Current); + } + + public override void Invalidate(Rect rect) + { + _rect = rect; + _invalidateRects.Push(rect); + lock (_invalidateRectsSync) + { + Monitor.Pulse(_invalidateRectsSync); + } + } + + public override void OnDraw(Canvas canvas) + { + var begin = DateTime.Now; + lock (_drawSync) + { + if (_bitmap != null) + { + canvas.DrawBitmap(_bitmap, 0, 0, _paint); + } + } + + UpdateRenderStat("OnDraw", (DateTime.Now - begin).TotalMilliseconds); + } + + private void RenderingLoop(object obj) + { + CancellationToken token = (CancellationToken)obj; + if (_debug) Log.Debug("render", "RenderingLoop Start ..."); + + AndroidPlatform.Instance.RegisterRenderingTaskId(Task.CurrentId.Value); + + while (!token.IsCancellationRequested) + { + try + { + Rect rect = default(Rect); + bool hasRect = false; + + hasRect = _invalidateRects.Count > 0; ; + if (!hasRect) + { + lock (_invalidateRectsSync) + { + hasRect = Monitor.Wait(_invalidateRectsSync, 1000); + } + } + + if (!hasRect) + { + DisposeVisualCaches(); + continue; + } + + if (_invalidateRects.Count < 1) continue; + int rCnt = _invalidateRects.Count; + + Rect[] rects = new Rect[rCnt > 1 ? (rCnt - 1) : 1]; + int cnt = _invalidateRects.TryPopRange(rects, 0, rects.Length); + if (_debug) Log.Debug("render", $"RenderingLoop processing {cnt} rects"); + if (cnt == 1) + { + rect = rects[0]; + //assuming this is the last frame so we dispose the caches + DisposeVisualCaches(); + } + else if (cnt > 1) + { + //for now all the rects are the same but may be in the future this can change + var minX = rects.Min(r => r.X); + var minY = rects.Min(r => r.Y); + var maxRight = rects.Max(r => r.Right); + var maxBottom = rects.Max(r => r.Bottom); + rect = new Rect(minX, minY, maxRight - minX, maxBottom - minY); + } + else + { + //Should never happend + hasRect = false; + } + + if (hasRect) + { + Render(rect); + } + } + catch (Exception e) + { + Log.Debug("render", "RenderingLoop Exception {0} ...", e); + } + } + AndroidPlatform.Instance.UnRegisterRenderingTaskId(Task.CurrentId.Value); + Log.Debug("render", "RenderingLoop End ..."); + } + + private void Render(Rect rect) + { + DateTime begin = DateTime.Now; + if (_debug) Log.Debug("render", $"RenderingLoop new invalidation ... {rect} {begin}"); + Bitmap bitmap; + + int w = View.Width; + int h = View.Height; + + if (w == 0) + { + var s = PointUnitService.Instance.PerspexToNative(View.ClientSize); + w = (int)s.Width; + h = (int)s.Height; + } + + if (_renderingBitmapsCache.Count > 0) + { + // _renderingBitmapsCache.TryPop(out bitmap); + bitmap = _renderingBitmapsCache.Pop(); + } + else + { + bitmap = Bitmap.CreateBitmap(w, h, Bitmap.Config.Argb8888); + } + + if (bitmap.Width != w || bitmap.Height != h) + { + bitmap.Dispose(); + bitmap = Bitmap.CreateBitmap(w, h, Bitmap.Config.Argb8888); + foreach (var c in _bmCanvases) + { + c.Value.Dispose(); + } + _bmCanvases.Clear(); + } + + Canvas canvas; + if (!_bmCanvases.TryGetValue(bitmap, out canvas)) + { + canvas = new Canvas(bitmap); + _bmCanvases.Add(bitmap, canvas); + } + + RenderView(canvas, _rect); + + //???no need dispose canvas is cached + //canvas.Dispose(); + lock (_drawSync) + { + if (_bitmap != null) _renderingBitmapsCache.Push(_bitmap); + _bitmap = bitmap; + } + var nr = rect.ToAndroidGraphics(); + + View.PostInvalidate(nr.Left, nr.Top, nr.Right, nr.Bottom); + //View.PostInvalidateDelayed(1, nr.Left, nr.Top, nr.Right, nr.Bottom); + } + + private void DisposeVisualCaches() + { + lock (_drawSync) + { + if (_debug) Log.Debug("render", $"RenderingLoop disposing {VisualCaches.Count} visual caches"); + foreach (var vc in VisualCaches) + { + vc.Value.Dispose(); + } + + VisualCaches.Clear(); + } + } + + public override void Dispose() + { + base.Dispose(); + _cancelationTokenSource.Cancel(); + _cancelationTokenSource = null; + } + } + + public class BitmapPreDrawRenderHelper : AndroidViewRenderHelper where TView : View, P.IWindowImpl, IAndroidViewSupportRender + { + private class OnPreDrawListener : Java.Lang.Object, ViewTreeObserver.IOnPreDrawListener + { + private Func _preDraw; + + public OnPreDrawListener(Func preDraw) + { + _preDraw = preDraw; + } + + public bool OnPreDraw() + { + return _preDraw(); + } + } + + private Bitmap _bitmap; + private Rect _rect; + private Paint _paint = new Paint(); + private OnPreDrawListener _listener; + + public BitmapPreDrawRenderHelper(TView view) : base(view) + { + _listener = new OnPreDrawListener(OnPreDraw); + View.ViewTreeObserver.AddOnPreDrawListener(_listener); + } + + public override void Dispose() + { + base.Dispose(); + View.ViewTreeObserver.RemoveOnPreDrawListener(_listener); + _listener.Dispose(); + _bitmap.Dispose(); + _paint.Dispose(); + } + + public override void Invalidate(Rect rect) + { + _rect = rect; + View.Invalidate(rect.ToAndroidGraphics()); + } + + public override void OnDraw(Canvas canvas) + { + var begin = DateTime.Now; + + if (_bitmap != null) + { + canvas.DrawBitmap(_bitmap, 0, 0, _paint); + } + + UpdateRenderStat("OnDraw", (DateTime.Now - begin).TotalMilliseconds); + } + + public bool OnPreDraw() + { + int w = View.Width; + int h = View.Height; + Bitmap bitmap = _bitmap; + if (bitmap == null) + { + bitmap = Bitmap.CreateBitmap(w, h, Bitmap.Config.Argb8888); + } + else if (bitmap.Width != w || bitmap.Height != h) + { + bitmap.Dispose(); + bitmap = Bitmap.CreateBitmap(w, h, Bitmap.Config.Argb8888); + } + using (var canvas = new Canvas(bitmap)) + { + RenderView(canvas, _rect); + } + _bitmap = bitmap; + return true; + } + } + + public class SurfaceViewRenderHelper : AndroidViewRenderHelper where TView : SurfaceView, P.IWindowImpl, IAndroidViewSupportRender + { + private class SurfaceHolderCallback : Java.Lang.Object, ISurfaceHolderCallback//, ISurfaceHolderCallback2 + { + private SurfaceViewRenderHelper _parent; + + public SurfaceHolderCallback(SurfaceViewRenderHelper parent) + { + _parent = parent; + } + + public void SurfaceChanged(ISurfaceHolder holder, [GeneratedEnum] Format format, int width, int height) + { + _parent.SurfaceChanged(holder, format, width, height); + } + + public void SurfaceCreated(ISurfaceHolder holder) + { + _parent.SurfaceCreated(holder); + } + + public void SurfaceDestroyed(ISurfaceHolder holder) + { + _parent.SurfaceDestroyed(holder); + } + + public void SurfaceRedrawNeeded(ISurfaceHolder holder) + { + _parent.SurfaceRedrawNeeded(holder); + } + } + + public int DelayBetweenDrawms { get; set; } + protected ISurfaceHolder _holder; + private Task _task; + private CancellationTokenSource _cancelationTokenSource; + private CancellationToken _cancelationToken; + + private ConcurrentStack _pendingInvalidations = new ConcurrentStack(); + private object _invalidationRectsSync = new object(); + + private Format _surfaceFormat = Format.Rgba8888; + private Bitmap.Config _bitmapFormat = Bitmap.Config.Argb8888; + + public static new SurfaceViewRenderHelper Create(TView view, ViewDrawType drawType) + { + SurfaceViewRenderHelper result; + + switch (drawType) + { + case ViewDrawType.SurfaceViewCanvasOnDraw: + result = new SurfaceViewRenderHelper(view); + break; + + default: throw new NotSupportedException(); + } + + Init(result, drawType); + + return result; + } + + public SurfaceViewRenderHelper(TView view) : base(view) + { + //max 16fps -60ms, 25fps - 40ms, max 30fps 30ms, max 60 fps-17ms + DelayBetweenDrawms = 40; + view.Holder.AddCallback(new SurfaceHolderCallback(this)); + } + + public override void Invalidate(Rect rect) + { + lock (_invalidationRectsSync) + { + _pendingInvalidations.Push(rect); + Monitor.Pulse(_invalidationRectsSync); + } + } + + public override void OnDraw(Canvas canvas) + { + //do nothing here we are drawing on another thread + } + + public void SurfaceChanged(ISurfaceHolder holder, [GeneratedEnum] Format format, int width, int height) + { + Log.Debug("SurfaceViewRenderAdapter", $"SurfaceChanged format={format} width ={width} height={height}..."); + + var ps = PointUnitService.Instance; + Size newSize = new Size(ps.NativeToPerspexX(width), ps.NativeToPerspexY(height)); + if (View.ClientSize != newSize) + { + View.ClientSize = newSize; + View.Resized?.Invoke(View.ClientSize); + } + + ScheduleRender(); + } + + public void SurfaceCreated(ISurfaceHolder holder) + { + Log.Debug("SurfaceViewRenderAdapter", "SurfaceCreated ..."); + + _holder = holder; + _holder.SetFormat(_surfaceFormat); + + if (_task != null) + { + StopTask(); + } + + if (_task == null) StartTask(); + + ScheduleRender(); + } + + private void SurfaceRedrawNeeded(ISurfaceHolder holder) + { + Log.Debug("SurfaceViewRenderAdapter", "SurfaceRedrawNeeded ..."); + ScheduleRender(); + } + + private void ScheduleRender() + { + if (_pendingInvalidations.Count > 0) + { + Invalidate(new Rect(0, 0, View.ClientSize.Width, View.ClientSize.Height)); + } + } + + private void RenderingLoop(object obj) + { + AndroidPlatform.Instance.RegisterRenderingTaskId(Task.CurrentId.Value); + Log.Debug("SurfaceViewRenderAdapter", $"DrawingTask Enter Id={Task.CurrentId.Value}... "); + + //sometime the ui is not ready for other thread rendering + Thread.Sleep(10 * DelayBetweenDrawms); + + var cancelationToken = (CancellationToken)obj; + while (!cancelationToken.IsCancellationRequested) + { + try + { + bool waitResult = false; + //wait few miliseconds so we are not heating the processor too much + Thread.Sleep(DelayBetweenDrawms); + + if (_pendingInvalidations.Count < 1) + { + lock (_invalidationRectsSync) + { + waitResult = Monitor.Wait(_invalidationRectsSync, 1000); + Thread.Sleep(0); + } + } + + if (cancelationToken.IsCancellationRequested) break; + + //if invalidations are more than one render one time after this + //so we are sure we have correct state + int requestCnt = _pendingInvalidations.Count > 1 ? _pendingInvalidations.Count - 1 : _pendingInvalidations.Count; + + if (requestCnt < 1) + { + //we can not continue with drawing + continue; + } + + var rects = new Rect[requestCnt]; + int rectsCount = _pendingInvalidations.TryPopRange(rects); + if (rectsCount < 1) + { + //should never happen but who knows + continue; + } + + Rect rect; + if (rectsCount == 1) + { + rect = rects[0]; + } + else + { + double minX = rects.Min(r => r.X); + double minY = rects.Min(r => r.Y); + double maxRight = rects.Max(r => r.Right); + double maxBottom = rects.Max(r => r.Bottom); + rect = new Rect(minX, minY, maxRight - minX, maxBottom - minY); + } + + RenderRect(rect); + } + catch (Exception e) + { + Log.Debug("SurfaceViewRenderAdapter", $"Exception in DrawingTask {e}"); + } + } + Log.Debug("SurfaceViewRenderAdapter", $"DrawingTask Exit Id={Task.CurrentId.Value}..."); + } + + private void RenderRect(Rect rect) + { + //Log.Debug("SurfaceViewRenderAdapter", $"RenderRect Enter rect={rect}... "); + var surfaceCanvas = _holder.LockCanvas(); + + try + { + lock (_holder) + { + RenderView(surfaceCanvas, rect); + } + } + finally + { + _holder.UnlockCanvasAndPost(surfaceCanvas); + } + } + + public void SurfaceDestroyed(ISurfaceHolder holder) + { + StopTask(); + } + + private void StartTask() + { + _cancelationTokenSource = new CancellationTokenSource(); + _cancelationToken = _cancelationTokenSource.Token; + _task = Task.Factory.StartNew(RenderingLoop, _cancelationToken, _cancelationToken); + } + + private void StopTask() + { + try + { + Log.Debug("SurfaceViewRenderAdapter", $"StopTask _cancelationTokenSource != null {_cancelationTokenSource != null}"); + if (_cancelationTokenSource != null) + { + _cancelationTokenSource.Cancel(); + Task.WaitAll(_task); + _cancelationTokenSource = null; + _task = null; + } + } + catch (Exception e) + { + Log.Debug("SurfaceViewRenderAdapter", $"Exception in StopTask DrawingTask.Cancel {e}"); + } + } + } +} \ No newline at end of file diff --git a/src/Android/Perspex.Android/Platform/CanvasPlatform/SurfaceMainWindowImpl.cs b/src/Android/Perspex.Android/Platform/CanvasPlatform/SurfaceMainWindowImpl.cs new file mode 100644 index 0000000000..059d95270a --- /dev/null +++ b/src/Android/Perspex.Android/Platform/CanvasPlatform/SurfaceMainWindowImpl.cs @@ -0,0 +1,33 @@ +using Android.Views; +using Perspex.Android.Platform.Specific; +using Perspex.Input; +using Perspex.Platform; + +namespace Perspex.Android.Platform.CanvasPlatform +{ + public class SurfaceMainWindowImpl : SurfaceWindowImpl, IWindowImpl + { + private IPointUnitService _pointService = PointUnitService.Instance; + + protected override void Init() + { + base.Init(); + + HandleEvents = true; + _keyboardHelper.ActivateAutoShowKeybord(); + } + + void ITopLevelImpl.Show() + { + (Parent as ViewGroup)?.RemoveAllViews(); + PerspexLocator.Current.GetService().ContentView = this; + Visibility = ViewStates.Visible; + } + + void ITopLevelImpl.SetInputRoot(IInputRoot inputRoot) + { + base.SetInputRoot(inputRoot); + _keyboardHelper.UpdateKeyboardState(inputRoot); + } + } +} \ No newline at end of file diff --git a/src/Android/Perspex.Android/Platform/CanvasPlatform/SurfaceWindowImpl.cs b/src/Android/Perspex.Android/Platform/CanvasPlatform/SurfaceWindowImpl.cs new file mode 100644 index 0000000000..988a0a5940 --- /dev/null +++ b/src/Android/Perspex.Android/Platform/CanvasPlatform/SurfaceWindowImpl.cs @@ -0,0 +1,194 @@ +using Android.Content; +using Android.Graphics; +using Android.Util; +using Android.Views; +using Perspex.Android.Platform.CanvasPlatform.Specific.Helpers; +using Perspex.Android.Platform.Specific; +using Perspex.Android.Platform.Specific.Helpers; +using Perspex.Input; +using Perspex.Input.Raw; +using Perspex.Platform; +using System; +using System.Collections.Generic; + +using AG = Android.Graphics; + +namespace Perspex.Android.Platform.CanvasPlatform +{ + public class SurfaceWindowImpl : SurfaceView, IWindowImpl, IPlatformHandle, IAndroidCanvasView, IAndroidViewSupportRender + { + public SurfaceWindowImpl(Context context) : base(context) + { + DrawType = ViewDrawType.SurfaceViewCanvasOnDraw; + + _keyboardHelper = new AndroidKeyboardEventsHelper(this); + _touchHelper = new AndroidTouchEventsHelper(this, () => InputRoot, GetPerspexPointFromEvent); + + ClientSize = MaxClientSize; + Resized = size => Invalidate(new Rect(size)); + Init(); + Background = new AG.Drawables.ColorDrawable(AG.Color.Transparent); + } + + public SurfaceWindowImpl() : this(PerspexLocator.Current.GetService().Activity) + { + } + + public void SurfaceRedrawNeeded(ISurfaceHolder holder) + { + Log.Debug("SurfaceView", "SurfaceRedrawNeeded..."); + //TryDraw(holder); + } + + protected override void OnDraw(Canvas canvas) + { + //base.OnDraw(canvas); + this._renderHelper.OnDraw(canvas); + } + + private IPointUnitService _pointService = PointUnitService.Instance; + + public ViewDrawType DrawType { get; } + + private SurfaceViewRenderHelper _renderHelper; + protected AndroidKeyboardEventsHelper _keyboardHelper; + protected AndroidTouchEventsHelper _touchHelper; + + private bool _handleEvents; + + public bool HandleEvents + { + get { return _handleEvents; } + set + { + _handleEvents = value; + _keyboardHelper.HandleEvents = _handleEvents; + } + } + + protected virtual void Init() + { + //SetLayerType(LayerType.Hardware, null); + _renderHelper = SurfaceViewRenderHelper.Create(this, DrawType); + + HandleEvents = false; + + //if the background is not transparent nothing is visible!!! + //this is the case when inherit from GridLayout, not from View + //this.Background = new AG.Drawables.ColorDrawable(AG.Color.Transparent); + } + + public Rect Bounds { get; set; } + + public IInputRoot InputRoot { get; private set; } + IntPtr IPlatformHandle.Handle => Handle; + string IPlatformHandle.HandleDescriptor => "Perspex SurfaceView"; + + public void Resize(double width, double height) + { + if (ClientSize == new Size(width, height)) return; + ClientSize = new Size(width, height); + Resized(ClientSize); + } + + public Size ClientSize { get; set; } + IPlatformHandle ITopLevelImpl.Handle => this; + Action ITopLevelImpl.Activated { get; set; } + public Action Closed { get; set; } + public Action Deactivated { get; set; } + public Action Input { get; set; } + public Action Paint { get; set; } + public Action Resized { get; set; } + + public void Activate() + { + } + + public void SetInputRoot(IInputRoot inputRoot) + { + InputRoot = inputRoot; + } + + public Point PointToScreen(Point point) + { + return point; + } + + public void SetCursor(IPlatformHandle cursor) + { + } + + public Size MaxClientSize => _pointService.NativeToPerspex(new Size(Resources.DisplayMetrics.WidthPixels, Resources.DisplayMetrics.HeightPixels)); + + public View View => this; + + public void SetTitle(string title) + { + } + + void ITopLevelImpl.Show() + { + this.Visibility = ViewStates.Visible; + } + + public IDisposable ShowDialog() + { + throw new NotImplementedException(); + } + + public void Hide() + { + } + + public virtual Point GetPerspexPointFromEvent(MotionEvent e) => _pointService.NativeToPerspex(new Point(e.GetX(), e.GetY())); + + public override bool DispatchTouchEvent(MotionEvent e) + { + bool callBase; + bool? result = _touchHelper.DispatchTouchEvent(e, out callBase); + bool baseResult = callBase ? base.DispatchTouchEvent(e) : false; + + return result != null ? result.Value : baseResult; + } + + public override bool DispatchKeyEvent(KeyEvent e) + { + bool callBase; + bool? res = _keyboardHelper.DispatchKeyEvent(e, out callBase); + bool baseResult = callBase ? base.DispatchKeyEvent(e) : false; + + return res != null ? res.Value : baseResult; + } + + public void Invalidate(Rect rect) + { + this._renderHelper.Invalidate(rect); + } + + public virtual void PreRender(Canvas canvas, Rect rect) + { + } + + public virtual void Render(Canvas canvas, Rect rect) + { + Canvas = canvas; + Paint?.Invoke(rect); + } + + public virtual void PostRender(Canvas canvas, Rect rect) + { + _renderHelper.DrawDebugInfoToCanvas(canvas); + _touchHelper.DrawLastMousePoint(canvas); + } + + public AG.Canvas Canvas { get; private set; } + + public Dictionary VisualCaches => _renderHelper.VisualCaches; + + protected override void OnSizeChanged(int w, int h, int oldw, int oldh) + { + base.OnSizeChanged(w, h, oldw, oldh); + Resize(MaxClientSize.Width, MaxClientSize.Height); + } + } +} \ No newline at end of file diff --git a/src/Android/Perspex.Android/Platform/CanvasPlatform/WindowImpl.cs b/src/Android/Perspex.Android/Platform/CanvasPlatform/WindowImpl.cs new file mode 100644 index 0000000000..6cb0ecc3ab --- /dev/null +++ b/src/Android/Perspex.Android/Platform/CanvasPlatform/WindowImpl.cs @@ -0,0 +1,207 @@ +using Android.Content; +using Android.Views; +using Perspex.Android.Platform.CanvasPlatform.Specific.Helpers; +using Perspex.Android.Platform.Specific; +using Perspex.Android.Platform.Specific.Helpers; +using Perspex.Input; +using Perspex.Input.Raw; +using Perspex.Platform; +using System; +using System.Collections.Generic; +using AG = Android.Graphics; + +namespace Perspex.Android.Platform.CanvasPlatform +{ + public class WindowImpl : View, + //GridLayout, //may be GridLayout can be better if int he future we want to embed native views or controls + IWindowImpl, IPlatformHandle, IAndroidCanvasView, IAndroidViewSupportRender, IDisposable + { + private IPointUnitService _pointService = PointUnitService.Instance; + + public ViewDrawType DrawType { get; } + + private AndroidViewRenderHelper _renderHelper; + protected AndroidKeyboardEventsHelper _keyboardHelper; + protected AndroidTouchEventsHelper _touchHelper; + + private bool _handleEvents; + + public bool HandleEvents + { + get { return _handleEvents; } + set + { + _handleEvents = value; + _keyboardHelper.HandleEvents = _handleEvents; + } + } + + internal static bool _debug = false; + + public WindowImpl(Context context) : base(context) + { + DrawType = AndroidPlatform.Instance.DefaultViewDrawType == ViewDrawType.SurfaceViewCanvasOnDraw ? ViewDrawType.CanvasOnDraw : AndroidPlatform.Instance.DefaultViewDrawType; + + _keyboardHelper = new AndroidKeyboardEventsHelper(this); + _touchHelper = new AndroidTouchEventsHelper(this, () => InputRoot, GetPerspexPointFromEvent); + _renderHelper = AndroidViewRenderHelper.Create(this, DrawType); + + //LayerType = LayerType.Hardware; + //SetLayerType(LayerType.Hardware, null); + + ClientSize = MaxClientSize; + Resized = size => Invalidate(new Rect(size)); + this.Init(); + } + + public WindowImpl() : this(PerspexLocator.Current.GetService().Activity) + { + } + + protected virtual void Init() + { + HandleEvents = false; + + //if the background is not transparent nothing is visible!!! + //this is the case when inherit from GridLayout, not from View + //this.Background = new AG.Drawables.ColorDrawable(AG.Color.Transparent); + } + + public Rect Bounds { get; set; } + + public IInputRoot InputRoot { get; private set; } + IntPtr IPlatformHandle.Handle => Handle; + string IPlatformHandle.HandleDescriptor => "Perspex View"; + + public void Resize(double width, double height) + { + if (ClientSize == new Size(width, height)) return; + ClientSize = new Size(width, height); + Resized(ClientSize); + } + + public Size ClientSize { get; set; } + IPlatformHandle ITopLevelImpl.Handle => this; + Action ITopLevelImpl.Activated { get; set; } + public Action Closed { get; set; } + public Action Deactivated { get; set; } + public Action Input { get; set; } + public Action Paint { get; set; } + public Action Resized { get; set; } + + public void Activate() + { + } + + public void SetInputRoot(IInputRoot inputRoot) + { + InputRoot = inputRoot; + } + + public Point PointToScreen(Point point) + { + return point; + } + + public void SetCursor(IPlatformHandle cursor) + { + } + + public Size MaxClientSize => _pointService.NativeToPerspex(new Size(Resources.DisplayMetrics.WidthPixels, Resources.DisplayMetrics.HeightPixels)); + + public View View => this; + + public void SetTitle(string title) + { + } + + void ITopLevelImpl.Show() + { + this.Visibility = ViewStates.Visible; + } + + public IDisposable ShowDialog() + { + throw new NotImplementedException(); + } + + public void Hide() + { + this.Visibility = ViewStates.Invisible; + } + + public virtual Point GetPerspexPointFromEvent(MotionEvent e) => _pointService.NativeToPerspex(new Point(e.GetX(), e.GetY())); + + public override bool DispatchTouchEvent(MotionEvent e) + { + bool callBase; + bool? result = _touchHelper.DispatchTouchEvent(e, out callBase); + bool baseResult = callBase ? base.DispatchTouchEvent(e) : false; + + return result != null ? result.Value : baseResult; + } + + public override bool DispatchKeyEvent(KeyEvent e) + { + bool callBase; + bool? res = _keyboardHelper.DispatchKeyEvent(e, out callBase); + bool baseResult = callBase ? base.DispatchKeyEvent(e) : false; + + return res != null ? res.Value : baseResult; + } + + public void Invalidate(Rect rect) + { + this._renderHelper.Invalidate(rect); + } + + public AG.Canvas Canvas { get; private set; } + + public Dictionary VisualCaches + { + get + { + return _renderHelper.VisualCaches; + } + } + + protected TimeSpan _onDrawDuration; + + private DateTime _beginDraw; + + public virtual void PreRender(AG.Canvas canvas, Rect rect) + { + _beginDraw = DateTime.Now; + } + + public virtual void Render(AG.Canvas canvas, Rect rect) + { + Canvas = canvas; + Paint?.Invoke(rect); + } + + public virtual void PostRender(AG.Canvas canvas, Rect rect) + { + _renderHelper.DrawDebugInfoToCanvas(canvas); + _touchHelper.DrawLastMousePoint(canvas); + } + + protected override void OnDraw(AG.Canvas canvas) + { + _renderHelper.OnDraw(canvas); + } + + void IDisposable.Dispose() + { + _renderHelper.Dispose(); + _touchHelper.Dispose(); + _keyboardHelper.Dispose(); + } + + protected override void OnSizeChanged(int w, int h, int oldw, int oldh) + { + base.OnSizeChanged(w, h, oldw, oldh); + Resize(MaxClientSize.Width, MaxClientSize.Height); + } + } +} \ No newline at end of file diff --git a/src/Android/Perspex.Android/Platform/ClipboardImpl.cs b/src/Android/Perspex.Android/Platform/ClipboardImpl.cs new file mode 100644 index 0000000000..a9b12b18b5 --- /dev/null +++ b/src/Android/Perspex.Android/Platform/ClipboardImpl.cs @@ -0,0 +1,47 @@ +using Android.Content; +using Android.Runtime; +using Android.Views; +using Perspex.Input.Platform; +using Perspex.Platform; +using System.Threading.Tasks; + +namespace Perspex.Android.Platform +{ + internal class ClipboardImpl : IClipboard + { + private Context context = (PerspexLocator.Current.GetService() as View).Context; + + private ClipboardManager ClipboardManager + { + get + { + return this.context.GetSystemService(Context.ClipboardService).JavaCast(); + } + } + + public Task GetTextAsync() + { + if (ClipboardManager.HasPrimaryClip) + { + return Task.FromResult(ClipboardManager.PrimaryClip.GetItemAt(0).Text); + } + + return Task.FromResult(null); + } + + public Task SetTextAsync(string text) + { + ClipData clip = ClipData.NewPlainText("text", text); + ClipboardManager.PrimaryClip = clip; + + return Task.FromResult(null); + } + + public Task ClearAsync() + { + ClipboardManager.PrimaryClip = null; + + return Task.FromResult(null); + } + } +} \ No newline at end of file diff --git a/src/Android/Perspex.Android/Platform/Input/AndroidKeyboardDevice.cs b/src/Android/Perspex.Android/Platform/Input/AndroidKeyboardDevice.cs new file mode 100644 index 0000000000..dc56409c18 --- /dev/null +++ b/src/Android/Perspex.Android/Platform/Input/AndroidKeyboardDevice.cs @@ -0,0 +1,217 @@ +using System; +using Perspex.Input; +//using Android.InputMethodServices; +using System.Collections.Generic; +using Android.Views; + +namespace Perspex.Android.Platform.Input +{ + public class AndroidKeyboardDevice : KeyboardDevice, IKeyboardDevice { + private static readonly Dictionary KeyDic = new Dictionary + { + // { Keycode.Cancel?, Key.Cancel }, + { Keycode.Del, Key.Back }, + { Keycode.Tab, Key.Tab }, + // { Keycode.Linefeed?, Key.LineFeed }, + { Keycode.Clear, Key.Clear }, + { Keycode.Enter, Key.Return }, + { Keycode.MediaPause, Key.Pause }, + //{ Keycode.?, Key.CapsLock } + //{ Keycode.?, Key.HangulMode } + //{ Keycode.?, Key.JunjaMode } + //{ Keycode.?, Key.FinalMode } + //{ Keycode.?, Key.KanjiMode } + { Keycode.Escape, Key.Escape }, + //{ Keycode.?, Key.ImeConvert } + //{ Keycode.?, Key.ImeNonConvert } + //{ Keycode.?, Key.ImeAccept } + //{ Keycode.?, Key.ImeModeChange } + { Keycode.Space, Key.Space }, + { Keycode.PageUp, Key.Prior }, + { Keycode.PageDown, Key.PageDown }, + // { Keycode.end?, Key.End }, + { Keycode.Home, Key.Home }, + { Keycode.DpadLeft, Key.Left }, + { Keycode.DpadUp, Key.Up }, + { Keycode.DpadRight, Key.Right }, + { Keycode.DpadDown, Key.Down }, + // { Keycode.ButtonSelect?, Key.Select }, + // { Keycode.print?, Key.Print }, + //{ Keycode.execute?, Key.Execute }, + // { Keycode.snap, Key.Snapshot } + { Keycode.Insert, Key.Insert }, + { Keycode.ForwardDel, Key.Delete }, + //{ Keycode.help, Key.Help }, + //{ Keycode.?, Key.D0 } + //{ Keycode.?, Key.D1 } + //{ Keycode.?, Key.D2 } + //{ Keycode.?, Key.D3 } + //{ Keycode.?, Key.D4 } + //{ Keycode.?, Key.D5 } + //{ Keycode.?, Key.D6 } + //{ Keycode.?, Key.D7 } + //{ Keycode.?, Key.D8 } + //{ Keycode.?, Key.D9 } + { Keycode.A, Key.A }, + { Keycode.B, Key.B }, + { Keycode.C, Key.C }, + { Keycode.D, Key.D }, + { Keycode.E, Key.E }, + { Keycode.F, Key.F }, + { Keycode.G, Key.G }, + { Keycode.H, Key.H }, + { Keycode.I, Key.I }, + { Keycode.J, Key.J }, + { Keycode.K, Key.K }, + { Keycode.L, Key.L }, + { Keycode.M, Key.M }, + { Keycode.N, Key.N }, + { Keycode.O, Key.O }, + { Keycode.P, Key.P }, + { Keycode.Q, Key.Q }, + { Keycode.R, Key.R }, + { Keycode.S, Key.S }, + { Keycode.T, Key.T }, + { Keycode.U, Key.U }, + { Keycode.V, Key.V }, + { Keycode.W, Key.W }, + { Keycode.X, Key.X }, + { Keycode.Y, Key.Y }, + { Keycode.Z, Key.Z }, + //{ Keycode.a, Key.A }, + //{ Keycode.b, Key.B }, + //{ Keycode.c, Key.C }, + //{ Keycode.d, Key.D }, + //{ Keycode.e, Key.E }, + //{ Keycode.f, Key.F }, + //{ Keycode.g, Key.G }, + //{ Keycode.h, Key.H }, + //{ Keycode.i, Key.I }, + //{ Keycode.j, Key.J }, + //{ Keycode.k, Key.K }, + //{ Keycode.l, Key.L }, + //{ Keycode.m, Key.M }, + //{ Keycode.n, Key.N }, + //{ Keycode.o, Key.O }, + //{ Keycode.p, Key.P }, + //{ Keycode.q, Key.Q }, + //{ Keycode.r, Key.R }, + //{ Keycode.s, Key.S }, + //{ Keycode.t, Key.T }, + //{ Keycode.u, Key.U }, + //{ Keycode.v, Key.V }, + //{ Keycode.w, Key.W }, + //{ Keycode.x, Key.X }, + //{ Keycode.y, Key.Y }, + //{ Keycode.z, Key.Z }, + //{ Keycode.?, Key.LWin } + //{ Keycode.?, Key.RWin } + //{ Keycode.?, Key.Apps } + //{ Keycode.?, Key.Sleep } + //{ Keycode.?, Key.NumPad0 } + //{ Keycode.?, Key.NumPad1 } + //{ Keycode.?, Key.NumPad2 } + //{ Keycode.?, Key.NumPad3 } + //{ Keycode.?, Key.NumPad4 } + //{ Keycode.?, Key.NumPad5 } + //{ Keycode.?, Key.NumPad6 } + //{ Keycode.?, Key.NumPad7 } + //{ Keycode.?, Key.NumPad8 } + //{ Keycode.?, Key.NumPad9 } + { Keycode.NumpadMultiply, Key.Multiply }, + { Keycode.NumpadAdd, Key.Add }, + { Keycode.NumpadComma, Key.Separator }, + { Keycode.NumpadSubtract, Key.Subtract }, + //{ Keycode.numpaddecimal?, Key.Decimal } + { Keycode.NumpadDivide, Key.Divide }, + { Keycode.F1, Key.F1 }, + { Keycode.F2, Key.F2 }, + { Keycode.F3, Key.F3 }, + { Keycode.F4, Key.F4 }, + { Keycode.F5, Key.F5 }, + { Keycode.F6, Key.F6 }, + { Keycode.F7, Key.F7 }, + { Keycode.F8, Key.F8 }, + { Keycode.F9, Key.F9 }, + { Keycode.F10, Key.F10 }, + { Keycode.F11, Key.F11 }, + { Keycode.F12, Key.F12 }, + //{ Keycode.f13, Key.F13 }, + //{ Keycode.F14, Key.F14 }, + //{ Keycode.L5, Key.F15 }, + //{ Keycode.F16, Key.F16 }, + //{ Keycode.F17, Key.F17 }, + //{ Keycode.L8, Key.F18 }, + //{ Keycode.L9, Key.F19 }, + //{ Keycode.L10, Key.F20 }, + //{ Keycode.R1, Key.F21 }, + //{ Keycode.R2, Key.F22 }, + //{ Keycode.F23, Key.F23 }, + //{ Keycode.R4, Key.F24 }, + // { Keycode.numpad, Key.NumLock } + { Keycode.ScrollLock, Key.Scroll }, + { Keycode.ShiftLeft, Key.LeftShift }, + //{ Keycode.?, Key.RightShift } + //{ Keycode.?, Key.LeftCtrl } + //{ Keycode.?, Key.RightCtrl } + //{ Keycode.?, Key.LeftAlt } + //{ Keycode.?, Key.RightAlt } + //{ Keycode.?, Key.BrowserBack } + //{ Keycode.?, Key.BrowserForward } + //{ Keycode.?, Key.BrowserRefresh } + //{ Keycode.?, Key.BrowserStop } + //{ Keycode.?, Key.BrowserSearch } + //{ Keycode.?, Key.BrowserFavorites } + //{ Keycode.?, Key.BrowserHome } + //{ Keycode.?, Key.VolumeMute } + //{ Keycode.?, Key.VolumeDown } + //{ Keycode.?, Key.VolumeUp } + //{ Keycode.?, Key.MediaNextTrack } + //{ Keycode.?, Key.MediaPreviousTrack } + //{ Keycode.?, Key.MediaStop } + //{ Keycode.?, Key.MediaPlayPause } + //{ Keycode.?, Key.LaunchMail } + //{ Keycode.?, Key.SelectMedia } + //{ Keycode.?, Key.LaunchApplication1 } + //{ Keycode.?, Key.LaunchApplication2 } + //{ Keycode.?, Key.OemSemicolon } + //{ Keycode.?, Key.OemPlus } + //{ Keycode.?, Key.OemComma } + //{ Keycode.?, Key.OemMinus } + //{ Keycode.?, Key.OemPeriod } + //{ Keycode.?, Key.Oem2 } + //{ Keycode.?, Key.OemTilde } + //{ Keycode.?, Key.AbntC1 } + //{ Keycode.?, Key.AbntC2 } + //{ Keycode.?, Key.Oem4 } + //{ Keycode.?, Key.OemPipe } + //{ Keycode.?, Key.OemCloseBrackets } + //{ Keycode.?, Key.Oem7 } + //{ Keycode.?, Key.Oem8 } + //{ Keycode.?, Key.Oem102 } + //{ Keycode.?, Key.ImeProcessed } + //{ Keycode.?, Key.System } + //{ Keycode.?, Key.OemAttn } + //{ Keycode.?, Key.OemFinish } + //{ Keycode.?, Key.DbeHiragana } + //{ Keycode.?, Key.OemAuto } + //{ Keycode.?, Key.DbeDbcsChar } + //{ Keycode.?, Key.OemBackTab } + //{ Keycode.?, Key.Attn } + //{ Keycode.?, Key.DbeEnterWordRegisterMode } + //{ Keycode.?, Key.DbeEnterImeConfigureMode } + //{ Keycode.?, Key.EraseEof } + //{ Keycode.?, Key.Play } + //{ Keycode.?, Key.Zoom } + //{ Keycode.?, Key.NoName } + //{ Keycode.?, Key.DbeEnterDialogConversionMode } + //{ Keycode.?, Key.OemClear } + //{ Keycode.?, Key.DeadCharProcessed } + }; + + internal static Key ConvertKey(Keycode key) { + Key result; + return KeyDic.TryGetValue(key, out result) ? result : Key.None; + } + } +} diff --git a/src/Android/Perspex.Android/Platform/Input/AndroidMouseDevice.cs b/src/Android/Perspex.Android/Platform/Input/AndroidMouseDevice.cs new file mode 100644 index 0000000000..77d11c5262 --- /dev/null +++ b/src/Android/Perspex.Android/Platform/Input/AndroidMouseDevice.cs @@ -0,0 +1,12 @@ +using Perspex.Input; + +namespace Perspex.Android.Platform.Input +{ + public class AndroidMouseDevice : MouseDevice + { + public AndroidMouseDevice() + { + + } + } +} \ No newline at end of file diff --git a/src/Android/Perspex.Android/Platform/Specific/CommonUITimer.cs b/src/Android/Perspex.Android/Platform/Specific/CommonUITimer.cs new file mode 100644 index 0000000000..e62e9294f9 --- /dev/null +++ b/src/Android/Perspex.Android/Platform/Specific/CommonUITimer.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; + +namespace Perspex.Android.Platform.Specific +{ + public class CommonUITimer : IDisposable + { + private System.Timers.Timer _timer; + private TimeSpan _interval; + private Dictionary _actions = new Dictionary(); + + public CommonUITimer(TimeSpan interval) + { + _timer = new System.Timers.Timer(interval.TotalMilliseconds); + _interval = interval; + _timer.Elapsed += _timer_Elapsed; + _timer.Start(); + } + + private void _timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) + { + DateTime now = DateTime.Now; + foreach (var kvp in _actions) + { + if ((now - kvp.Value) >= _interval) + { + kvp.Key(); + } + } + } + + public void AddAction(Action action) + { + _actions[action] = DateTime.Now; + } + + public void Dispose() + { + _timer.Elapsed -= _timer_Elapsed; + _timer?.Stop(); + } + + public void RemoveAction(Action action) + { + _actions.Remove(action); + } + + public int ActiveActionsCount => _actions.Count; + } +} \ No newline at end of file diff --git a/src/Android/Perspex.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs b/src/Android/Perspex.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs new file mode 100644 index 0000000000..bf0792d055 --- /dev/null +++ b/src/Android/Perspex.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs @@ -0,0 +1,145 @@ +using Android.Content; +using Android.Runtime; +using Android.Views; +using Android.Views.InputMethods; +using Perspex.Android.Platform.Input; +using Perspex.Controls; +using Perspex.Input; +using Perspex.Input.Raw; +using Perspex.Platform; +using System; +using System.ComponentModel; + +namespace Perspex.Android.Platform.Specific.Helpers +{ + public class AndroidKeyboardEventsHelper : IDisposable where TView : View, IWindowImpl, IAndroidView + { + private TView _view; + private IInputElement _lastFocusedElement; + + public bool HandleEvents { get; set; } + + public AndroidKeyboardEventsHelper(TView view) + { + this._view = view; + HandleEvents = true; + } + + public bool? DispatchKeyEvent(KeyEvent e, out bool callBase) + { + if (!HandleEvents) + { + callBase = true; + return null; + } + + return DispatchKeyEventInternal(e, out callBase); + } + + private bool? DispatchKeyEventInternal(KeyEvent e, out bool callBase) + { + if (e.Action == KeyEventActions.Multiple) + { + callBase = true; + return null; + } + + var rawKeyEvent = new RawKeyEventArgs( + AndroidKeyboardDevice.Instance, + Convert.ToUInt32(e.EventTime), + e.Action == KeyEventActions.Down ? RawKeyEventType.KeyDown : RawKeyEventType.KeyUp, + AndroidKeyboardDevice.ConvertKey(e.KeyCode), GetModifierKeys(e)); + _view.Input(rawKeyEvent); + + if (e.Action == KeyEventActions.Down && e.UnicodeChar >= 32) + { + var rawTextEvent = new RawTextInputEventArgs( + AndroidKeyboardDevice.Instance, + Convert.ToUInt32(e.EventTime), + Convert.ToChar(e.UnicodeChar).ToString() + ); + _view.Input(rawTextEvent); + } + + if (e.Action == KeyEventActions.Up) + { + //nothing to do here more call base no need of more events + callBase = true; + return null; + } + + callBase = false; + return false; + } + + private static InputModifiers GetModifierKeys(KeyEvent e) + { + var rv = InputModifiers.None; + + if (e.IsCtrlPressed) rv |= InputModifiers.Control; + if (e.IsShiftPressed) rv |= InputModifiers.Shift; + + return rv; + } + + private bool NeedsKeyboard(IInputElement element) + { + //may be some other elements + return element is TextBox; + } + + private void TryShowHideKeyboard(IInputElement element, bool value) + { + var input = _view.Context.GetSystemService(Context.InputMethodService).JavaCast(); + + if (value) + { + //show keyboard + //may be in the future different keyboards support e.g. normal, only digits etc. + //Android.Text.InputTypes + input.ToggleSoftInput(ShowFlags.Forced, HideSoftInputFlags.ImplicitOnly); + } + else + { + //hide keyboard + input.HideSoftInputFromWindow(_view.WindowToken, HideSoftInputFlags.None); + } + } + + public void UpdateKeyboardState(IInputElement element) + { + var focusedElement = element; + bool oldValue = NeedsKeyboard(_lastFocusedElement); + bool newValue = NeedsKeyboard(focusedElement); + + if (newValue != oldValue || newValue) + { + TryShowHideKeyboard(focusedElement, newValue); + } + + _lastFocusedElement = element; + } + + public void ActivateAutoShowKeybord() + { + var kbDevice = (KeyboardDevice.Instance as INotifyPropertyChanged); + + //just in case we've called more than once the method + kbDevice.PropertyChanged -= KeyboardDevice_PropertyChanged; + kbDevice.PropertyChanged += KeyboardDevice_PropertyChanged; + } + + private void KeyboardDevice_PropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(KeyboardDevice.FocusedElement)) + { + UpdateKeyboardState(KeyboardDevice.Instance.FocusedElement); + } + } + + public void Dispose() + { + HandleEvents = false; + } + } +} \ No newline at end of file diff --git a/src/Android/Perspex.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs b/src/Android/Perspex.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs new file mode 100644 index 0000000000..b504415e18 --- /dev/null +++ b/src/Android/Perspex.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs @@ -0,0 +1,155 @@ +using Android.Graphics; +using Android.Views; +using Perspex.Android.Platform.CanvasPlatform; +using Perspex.Input; +using Perspex.Input.Raw; +using Perspex.Platform; +using System; + +namespace Perspex.Android.Platform.Specific.Helpers +{ + public class AndroidTouchEventsHelper : IDisposable where TView : View, IWindowImpl, IAndroidView + { + private TView _view; + public bool HandleEvents { get; set; } + + public AndroidTouchEventsHelper(TView view, Func getInputRoot, Func getPointfunc) + { + this._view = view; + HandleEvents = true; + _getPointFunc = getPointfunc; + _getInputRoot = getInputRoot; + } + + private DateTime _lastTouchMoveEventTime = DateTime.Now; + private Point? _lastTouchMovePoint; + private Func _getPointFunc; + private Func _getInputRoot; + private Point _point; + + public bool? DispatchTouchEvent(MotionEvent e, out bool callBase) + { + if (!HandleEvents) + { + callBase = true; + return null; + } + + RawMouseEventType? mouseEventType = null; + var eventTime = DateTime.Now; + //Basic touch support + switch (e.Action) + { + case MotionEventActions.Move: + //may be bot flood the evnt system with too many event especially on not so powerfull mobile devices + if ((eventTime - _lastTouchMoveEventTime).TotalMilliseconds > 10) + { + mouseEventType = RawMouseEventType.Move; + } + break; + + case MotionEventActions.Down: + mouseEventType = RawMouseEventType.LeftButtonDown; + + break; + + case MotionEventActions.Up: + mouseEventType = RawMouseEventType.LeftButtonUp; + break; + } + + if (mouseEventType != null) + { + //if point is in view otherwise it's possible perspex not to find the proper window to dispatch the event + _point = _getPointFunc(e); + + double x = _view.GetX(); + double y = _view.GetY(); + double r = x + _view.Width; + double b = y + _view.Height; + + if (x <= _point.X && r >= _point.X && y <= _point.Y && b >= _point.Y) + { + var inputRoot = _getInputRoot(); + var mouseDevice = MouseDevice.Instance; + + //in order the controls to work in a predictable way + //we need to generate mouse move before first mouse down event + //as this is the way buttons are working every time + //otherwise there is a problem sometimes + if (mouseEventType == RawMouseEventType.LeftButtonDown) + { + var me = new RawMouseEventArgs(mouseDevice, (uint)eventTime.Ticks, inputRoot, + RawMouseEventType.Move, _point, InputModifiers.None); + _view.Input(me); + } + + var mouseEvent = new RawMouseEventArgs(mouseDevice, (uint)eventTime.Ticks, inputRoot, + mouseEventType.Value, _point, InputModifiers.LeftMouseButton); + _view.Input(mouseEvent); + + if (e.Action == MotionEventActions.Move && mouseDevice.Captured == null) + { + if (_lastTouchMovePoint != null) + { + //raise mouse scroll event so the scrollers + //are moving with the cursor + double vectorX = _point.X - _lastTouchMovePoint.Value.X; + double vectorY = _point.Y - _lastTouchMovePoint.Value.Y; + //based on test correction of 0.02 is working perfect + double correction = 0.02; + var mouseWheelEvent = new RawMouseWheelEventArgs( + mouseDevice, + (uint)eventTime.Ticks, + inputRoot, + _point, + new Vector(vectorX * correction, vectorY * correction), InputModifiers.LeftMouseButton); + _view.Input(mouseWheelEvent); + } + _lastTouchMovePoint = _point; + _lastTouchMoveEventTime = eventTime; + } + else if (e.Action == MotionEventActions.Down) + { + _lastTouchMovePoint = _point; + } + else + { + _lastTouchMovePoint = null; + } + } + } + + callBase = true; + //if return false events for move and up are not received!!! + return e.Action != MotionEventActions.Up; + } + + private Paint _paint; + + public void DrawLastMousePoint(Canvas canvas) + { + if (!AndroidPlatform.Instance.DrawDebugInfo) return; + + if (_paint == null) + { + _paint = new Paint() { Color = Color.Red, }; + _paint.SetStyle(Paint.Style.Fill); + } + + if(AndroidPlatform.Instance.DefaultViewDrawType != ViewDrawType.Skia) + { + canvas.DrawCircle(PointUnitService.Instance.PerspexToNativeXF(_point.X), + PointUnitService.Instance.PerspexToNativeYF(_point.Y), + (float)AndroidPlatform.Instance.DoubleClickSize.Width, + _paint); + } + + } + + public void Dispose() + { + HandleEvents = false; + } + } +} \ No newline at end of file diff --git a/src/Android/Perspex.Android/Platform/Specific/Helpers/ViewDrawType.cs b/src/Android/Perspex.Android/Platform/Specific/Helpers/ViewDrawType.cs new file mode 100644 index 0000000000..fb19de2b00 --- /dev/null +++ b/src/Android/Perspex.Android/Platform/Specific/Helpers/ViewDrawType.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using Android.App; +using Android.Content; +using Android.OS; +using Android.Runtime; +using Android.Views; +using Android.Widget; + +namespace Perspex.Android.Platform.Specific.Helpers +{ + public enum ViewDrawType + { + Skia, + CanvasOnDraw, + BitmapOnPreDraw, + BitmapBackgroundRender, + SurfaceViewCanvasOnDraw + } +} \ No newline at end of file diff --git a/src/Android/Perspex.Android/Platform/Specific/IAndroidActivity.cs b/src/Android/Perspex.Android/Platform/Specific/IAndroidActivity.cs new file mode 100644 index 0000000000..e984606e54 --- /dev/null +++ b/src/Android/Perspex.Android/Platform/Specific/IAndroidActivity.cs @@ -0,0 +1,12 @@ +using Android.App; +using Android.Views; + +namespace Perspex.Android.Platform.Specific +{ + public interface IAndroidActivity + { + Activity Activity { get; } + + IAndroidView ContentView { get; set; } + } +} \ No newline at end of file diff --git a/src/Android/Perspex.Android/Platform/Specific/IAndroidView.cs b/src/Android/Perspex.Android/Platform/Specific/IAndroidView.cs new file mode 100644 index 0000000000..ae004289b9 --- /dev/null +++ b/src/Android/Perspex.Android/Platform/Specific/IAndroidView.cs @@ -0,0 +1,16 @@ +using Android.Graphics; +using Android.Views; +using System; +using System.Collections.Generic; + +namespace Perspex.Android.Platform.Specific +{ + public interface IAndroidView + { + + + View View { get; } + } + + +} \ No newline at end of file diff --git a/src/Android/Perspex.Android/Platform/Specific/PerspexActivity.cs b/src/Android/Perspex.Android/Platform/Specific/PerspexActivity.cs new file mode 100644 index 0000000000..9843a9d82d --- /dev/null +++ b/src/Android/Perspex.Android/Platform/Specific/PerspexActivity.cs @@ -0,0 +1,54 @@ +using Android.App; +using Android.OS; +using Android.Views; +using Android.Widget; + +namespace Perspex.Android.Platform.Specific +{ + public class PerspexActivity : Activity, IAndroidActivity + { + private IAndroidView _contentView; + + public Activity Activity => this; + + public IAndroidView ContentView + { + get + { + return this._contentView; + } + + set + { + this._contentView = value; + var fl = new FrameLayout(this); + fl.AddView(this._contentView.View); + //this.SetContentView(value.View); + this.SetContentView(fl); + } + } + + protected override void OnCreate(Bundle savedInstanceState) + { + PerspexLocator.CurrentMutable.Bind().ToConstant(this); + RequestWindowFeature(WindowFeatures.NoTitle); + base.OnCreate(savedInstanceState); + } + + public override void SetContentView(View view) + { + base.SetContentView(view); + TakeKeyEvents(true); + } + + public override bool DispatchKeyEvent(KeyEvent e) + { + if (_contentView != null) + { + _contentView.View.DispatchKeyEvent(e); + } + + return base.DispatchKeyEvent(e); + } + } +} \ No newline at end of file diff --git a/src/Android/Perspex.Android/Properties/AssemblyInfo.cs b/src/Android/Perspex.Android/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..62fabf9811 --- /dev/null +++ b/src/Android/Perspex.Android/Properties/AssemblyInfo.cs @@ -0,0 +1,30 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. + +[assembly: AssemblyTitle("Perspex.Android")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Perspex.Android")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: ComVisible(false)] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] + +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file diff --git a/src/Android/Perspex.Android/Resources/AboutResources.txt b/src/Android/Perspex.Android/Resources/AboutResources.txt new file mode 100644 index 0000000000..194ae28a59 --- /dev/null +++ b/src/Android/Perspex.Android/Resources/AboutResources.txt @@ -0,0 +1,50 @@ +Images, layout descriptions, binary blobs and string dictionaries can be included +in your application as resource files. Various Android APIs are designed to +operate on the resource IDs instead of dealing with images, strings or binary blobs +directly. + +For example, a sample Android app that contains a user interface layout (main.xml), +an internationalization string table (strings.xml) and some icons (drawable-XXX/icon.png) +would keep its resources in the "Resources" directory of the application: + +Resources/ + drawable-hdpi/ + icon.png + + drawable-ldpi/ + icon.png + + drawable-mdpi/ + icon.png + + layout/ + main.xml + + values/ + strings.xml + +In order to get the build system to recognize Android resources, set the build action to +"AndroidResource". The native Android APIs do not operate directly with filenames, but +instead operate on resource IDs. When you compile an Android application that uses resources, +the build system will package the resources for distribution and generate a class called +"Resource" that contains the tokens for each one of the resources included. For example, +for the above Resources layout, this is what the Resource class would expose: + +public class Resource { + public class drawable { + public const int icon = 0x123; + } + + public class layout { + public const int main = 0x456; + } + + public class strings { + public const int first_string = 0xabc; + public const int second_string = 0xbcd; + } +} + +You would then use R.drawable.icon to reference the drawable/icon.png file, or Resource.layout.main +to reference the layout/main.xml file, or Resource.strings.first_string to reference the first +string in the dictionary file values/strings.xml. \ No newline at end of file diff --git a/src/Android/Perspex.Android/Resources/Resource.Designer.cs b/src/Android/Perspex.Android/Resources/Resource.Designer.cs new file mode 100644 index 0000000000..197fe8a409 --- /dev/null +++ b/src/Android/Perspex.Android/Resources/Resource.Designer.cs @@ -0,0 +1,60 @@ +#pragma warning disable 1591 +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +[assembly: global::Android.Runtime.ResourceDesignerAttribute("Perspex.Android.Resource", IsApplication=false)] + +namespace Perspex.Android +{ + + + [System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Android.Build.Tasks", "1.0.0.0")] + public partial class Resource + { + + static Resource() + { + global::Android.Runtime.ResourceIdManager.UpdateIdValues(); + } + + public partial class Attribute + { + + static Attribute() + { + global::Android.Runtime.ResourceIdManager.UpdateIdValues(); + } + + private Attribute() + { + } + } + + public partial class String + { + + // aapt resource value: 0x7f020001 + public static int ApplicationName = 2130837505; + + // aapt resource value: 0x7f020000 + public static int Hello = 2130837504; + + static String() + { + global::Android.Runtime.ResourceIdManager.UpdateIdValues(); + } + + private String() + { + } + } + } +} +#pragma warning restore 1591 diff --git a/src/Android/Perspex.Android/Resources/Values/Strings.xml b/src/Android/Perspex.Android/Resources/Values/Strings.xml new file mode 100644 index 0000000000..3823c6f4c6 --- /dev/null +++ b/src/Android/Perspex.Android/Resources/Values/Strings.xml @@ -0,0 +1,6 @@ + + + + Hello World, Click Me! + $projectname$ + \ No newline at end of file diff --git a/src/Android/Perspex.Android/SystemDialogImpl.cs b/src/Android/Perspex.Android/SystemDialogImpl.cs new file mode 100644 index 0000000000..60f9de914c --- /dev/null +++ b/src/Android/Perspex.Android/SystemDialogImpl.cs @@ -0,0 +1,16 @@ +using System; +using System.Threading.Tasks; +using Perspex.Controls; +using Perspex.Controls.Platform; +using Perspex.Platform; + +namespace Perspex.Android +{ + internal class SystemDialogImpl : ISystemDialogImpl + { + public Task ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/src/Android/Perspex.Android/packages.config b/src/Android/Perspex.Android/packages.config new file mode 100644 index 0000000000..e889be4b7c --- /dev/null +++ b/src/Android/Perspex.Android/packages.config @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/Android/Perspex.AndroidTestApplication/App.cs b/src/Android/Perspex.AndroidTestApplication/App.cs new file mode 100644 index 0000000000..e1d5182b50 --- /dev/null +++ b/src/Android/Perspex.AndroidTestApplication/App.cs @@ -0,0 +1,372 @@ +using Perspex.Android; +using Perspex.Controls; +using Perspex.Controls.Shapes; +using Perspex.Interactivity; +using Perspex.Layout; +using Perspex.Media; +using Perspex.Media.Imaging; +using Perspex.Platform; +using Perspex.Themes.Default; +using System; + +namespace Perspex.AndroidTestApplication +{ + public class App : Application + { + public App() + { + RegisterServices(); + InitializePlatform(); + + Styles = new DefaultTheme(); + } + + protected void InitializePlatform() + { + AndroidPlatform.Initialize(); + //not needed already default assembly is set to Activity that inherits PerspexActivity + //AndroidPlatform.Instance.SetDefaultAssetAssembly(typeof(App).Assembly); + } + + public Window BuildSimpleTextUI() + { + var window = new Window + { + Title = "Perspex Test Application", + Background = Brushes.Green, + Content = new Grid + { + VerticalAlignment = VerticalAlignment.Top, + HorizontalAlignment = HorizontalAlignment.Left, + Margin = new Thickness(10, 10, 0, 0), + Children = new Controls.Controls + { + new TextBlock + { + Foreground = Brushes.GhostWhite, + Background = Brushes.BlueViolet, + VerticalAlignment = VerticalAlignment.Top, + HorizontalAlignment = HorizontalAlignment.Left, + Text = + "Hello! This is a test of text wrapping and me actually getting something to render for the first time in life. Im and happy to say this was by no means all me. I want to thank everyone who helped me this far.", + FontSize = 80, + Margin = new Thickness(10), + TextWrapping = TextWrapping.Wrap + } + } + } + }; + + return window; + } + + public Window BuildStackPanelUI() + { + var window = new Window + { + Title = "Perspex Test Application", + Background = Brushes.Green, + Content = new Grid + { + VerticalAlignment = VerticalAlignment.Top, + HorizontalAlignment = HorizontalAlignment.Left, + Margin = new Thickness(10, 10, 0, 0), + Children = new Controls.Controls + { + new StackPanel + { + Orientation = Orientation.Horizontal, + [Grid.RowProperty] = 1, + [Grid.ColumnSpanProperty] = 2, + // Background = SolidColorBrush.Parse("#000000"), + Children = new Controls.Controls + { + new Button + { + Content = "Button 1", + Width = 100, + Foreground = Brushes.Blue, + Margin = new Thickness(5) + }, + new Button + { + Content = "Button 2", + Width = 100, + Margin = new Thickness(5) + }, + new Button + { + Content = "Button 3", + Width = 100, + Margin = new Thickness(5) + } + } + } + } + } + }; + + return window; + } + + public Window BuildGridWithSomeButtonsAndStuff() + { + var buttonToClick = new Button + { + Content = "Tap Me", + Width = 200, + Foreground = Brushes.White, + Margin = new Thickness(5), + Background = Brushes.Blue, + FontSize = 50 + }; + + var clickCount = 0; + buttonToClick.Click += (object sender, RoutedEventArgs e) => + { + Console.WriteLine("You clicked it :)"); + buttonToClick.Content = $"Tap x{clickCount}"; + clickCount++; + }; + + var window = new Window + { + Title = "Perspex Test Application", + Background = Brushes.DarkTurquoise, + Content = new Grid + { + Margin = new Thickness(0, 0, 0, 0), // skip the status bar area on iOS + ColumnDefinitions = new ColumnDefinitions + { + new ColumnDefinition(1, GridUnitType.Star), + new ColumnDefinition(1, GridUnitType.Star) + }, + RowDefinitions = new RowDefinitions + { + new RowDefinition(80, GridUnitType.Pixel), + new RowDefinition(160, GridUnitType.Pixel), + new RowDefinition(1, GridUnitType.Star), + new RowDefinition(1, GridUnitType.Star) + }, + Children = new Controls.Controls + { + new TextBlock + { + [Grid.RowProperty] = 0, + [Grid.ColumnSpanProperty] = 2, + Text = "Welcome to Perspex on Android !!!", + Foreground = Brushes.SteelBlue, + Background = Brushes.White, + FontSize = 50, + Margin = new Thickness(5, 7, 5, 5), + TextAlignment = TextAlignment.Left + }, +//// + new StackPanel + { + Orientation = Orientation.Horizontal, + [Grid.RowProperty] = 1, + [Grid.ColumnSpanProperty] = 2, + Background = Brush.Parse("#000000"), + Children = new Controls.Controls + { + buttonToClick, + new Button + { + Content = "Button 2", + Width = 250, + FontSize = 50, + Margin = new Thickness(5) + }, + new Button + { + Content = "Button 3", + Width = 250, + FontSize = 50, + Margin = new Thickness(5) + } + } + }, +//// + new Image + { + [Grid.RowProperty] = 2, + [Grid.ColumnProperty] = 0, + Margin = new Thickness(20), + // Source = new Bitmap("github_icon.png"), + Opacity = 0.4 + }, +//// + new Ellipse + { + [Grid.RowProperty] = 3, + [Grid.ColumnProperty] = 0, + Margin = new Thickness(20), + Fill = Brushes.Blue + }, +//// + new TextBlock + { + [Grid.RowProperty] = 2, + [Grid.ColumnProperty] = 1, + Text = + "How does this look? Is this text going to wrap and then look nice within the bounds of this widget? If not I will be extremely disappointed!\n\nWill we start a new paragraph here? If not there will be hell to pay!!!!", + Foreground = Brushes.Purple, + Background = Brushes.Transparent, + FontSize = 28, + Margin = new Thickness(10, 30, 10, 10) + }, + new Path + { + Data = + StreamGeometry.Parse( + "M 50,50 l 15,0 l 5,-15 l 5,15 l 15,0 l -10,10 l 4,15 l -15,-9 l -15,9 l 7,-15 Z"), + [Grid.RowProperty] = 3, + [Grid.ColumnProperty] = 1, + Margin = new Thickness(20), + Fill = Brushes.White, + Stroke = Brushes.Blue, + StrokeThickness = 4 + } + } + } + }; + + return window; + } + + public Window BuildGridWithSomeButtonsAndStuff2() + { + var buttonToClick = new Button + { + Content = "Tap Me", + Width = 200, + Foreground = Brushes.White, + Margin = new Thickness(5), + Background = Brushes.Blue, + FontSize = 50 + }; + + var clickCount = 0; + buttonToClick.Click += (object sender, RoutedEventArgs e) => + { + Console.WriteLine("You clicked it :)"); + buttonToClick.Content = $"Tap x{clickCount}"; + clickCount++; + }; + + var window = new Window + { + Title = "Perspex Test Application", + Background = Brushes.DarkTurquoise, + Content = new Grid + { + Margin = new Thickness(0, 50, 0, 0), // skip the status bar area on iOS + ColumnDefinitions = new ColumnDefinitions + { + new ColumnDefinition(1, GridUnitType.Star), + new ColumnDefinition(1, GridUnitType.Star) + }, + RowDefinitions = new RowDefinitions + { + new RowDefinition(80, GridUnitType.Pixel), + new RowDefinition(160, GridUnitType.Pixel), + new RowDefinition(1, GridUnitType.Star), + new RowDefinition(1, GridUnitType.Star) + }, + Children = new Controls.Controls + { + new TextBox + { + [Grid.RowProperty] = 0, + [Grid.ColumnSpanProperty] = 2, + Text = "Welcome to Perspex on Android !!!", + Foreground = Brushes.SteelBlue, + Background = Brushes.White, + FontSize = 50, + //Margin = new Thickness(5, 7, 5, 5), + Margin = new Thickness(5, 7, 5, 5), + //VerticalAlignment = VerticalAlignment.Bottom + //TextAlignment = TextAlignment.Left + }, +//// + new StackPanel + { + Orientation = Orientation.Horizontal, + [Grid.RowProperty] = 1, + [Grid.ColumnSpanProperty] = 2, + Background = Brush.Parse("#000000"), + Children = new Controls.Controls + { + buttonToClick, + new Button + { + Content = "Button 2", + Width = 250, + FontSize = 50, + Margin = new Thickness(5) + }, + new Button + { + Content = "Button 3", + Width = 250, + FontSize = 50, + Margin = new Thickness(5) + } + } + }, +//// + new Image + { + [Grid.RowProperty] = 2, + [Grid.ColumnProperty] = 0, + Margin = new Thickness(20), + //Stretch=Stretch.Uniform, + Width=400, + Height=400, + // Source = new Bitmap("github_icon.png"), + Source = new Bitmap(PerspexLocator.Current.GetService().Open(new Uri("res://component/Perspex Test Application/Perspex.AndroidTestApplication.github_icon.png"))), + Opacity = 0.4 + }, +//// + new Ellipse + { + [Grid.RowProperty] = 3, + [Grid.ColumnProperty] = 0, + Margin = new Thickness(20), + Fill = Brushes.Blue, + //Stroke = Brushes.Red, + //StrokeThickness = 4 + }, +//// + new TextBlock + { + [Grid.RowProperty] = 2, + [Grid.ColumnProperty] = 1, + Text = + "How does this look? Is this text going to wrap and then look nice within the bounds of this widget? If not I will be extremely disappointed!\n\nWill we start a new paragraph here? If not there will be hell to pay!!!!", + Foreground = Brushes.Purple, + Background = Brushes.Transparent, + FontSize = 28, + Margin = new Thickness(10, 30, 10, 10) + }, + new Path + { + Data = + StreamGeometry.Parse( + "M 50,50 l 15,0 l 5,-15 l 5,15 l 15,0 l -10,10 l 4,15 l -15,-9 l -15,9 l 7,-15 Z"), + [Grid.RowProperty] = 3, + [Grid.ColumnProperty] = 1, + Margin = new Thickness(20), + Fill = Brushes.White, + Stroke = Brushes.Blue, + StrokeThickness = 4 + } + } + } + }; + + return window; + } + } +} \ No newline at end of file diff --git a/src/Android/Perspex.AndroidTestApplication/Assets/AboutAssets.txt b/src/Android/Perspex.AndroidTestApplication/Assets/AboutAssets.txt new file mode 100644 index 0000000000..ee39886295 --- /dev/null +++ b/src/Android/Perspex.AndroidTestApplication/Assets/AboutAssets.txt @@ -0,0 +1,19 @@ +Any raw assets you want to be deployed with your application can be placed in +this directory (and child directories) and given a Build Action of "AndroidAsset". + +These files will be deployed with you package and will be accessible using Android's +AssetManager, like this: + +public class ReadAsset : Activity +{ + protected override void OnCreate (Bundle bundle) + { + base.OnCreate (bundle); + + InputStream input = Assets.Open ("my_asset.txt"); + } +} + +Additionally, some Android functions will automatically load asset files: + +Typeface tf = Typeface.CreateFromAsset (Context.Assets, "fonts/samplefont.ttf"); \ No newline at end of file diff --git a/src/Android/Perspex.AndroidTestApplication/GettingStarted.Xamarin b/src/Android/Perspex.AndroidTestApplication/GettingStarted.Xamarin new file mode 100644 index 0000000000..e9d4f6a411 --- /dev/null +++ b/src/Android/Perspex.AndroidTestApplication/GettingStarted.Xamarin @@ -0,0 +1,4 @@ + + GS\Android\CS\AndroidApp\GettingStarted.html + false + \ No newline at end of file diff --git a/src/Android/Perspex.AndroidTestApplication/MainActivity.cs b/src/Android/Perspex.AndroidTestApplication/MainActivity.cs new file mode 100644 index 0000000000..994b77dc32 --- /dev/null +++ b/src/Android/Perspex.AndroidTestApplication/MainActivity.cs @@ -0,0 +1,67 @@ +using Android.App; +using Android.Content.PM; +using Android.OS; +using Perspex.Android; +using Perspex.Android.Platform.Specific; +using Perspex.Android.Platform.Specific.Helpers; +using Perspex.Controls; +using Perspex.Controls.Presenters; +using Perspex.Controls.Primitives; +using Perspex.Controls.Templates; +using Perspex.Markup.Xaml.Data; +using Perspex.Platform; +using Perspex.Styling; +using ReactiveUI; +using System; +using System.Reactive.Linq; +using System.Windows.Input; + +namespace Perspex.AndroidTestApplication +{ + [Activity(Label = "Main", + MainLauncher = true, + Icon = "@drawable/icon", + LaunchMode = LaunchMode.SingleInstance/*, + ScreenOrientation = ScreenOrientation.Landscape*/)] + public class MainBaseActivity : PerspexActivity + { + protected override void OnCreate(Bundle savedInstanceState) + { + base.OnCreate(savedInstanceState); + + //set some parameters to android platform + AndroidPlatform.Instance.DrawDebugInfo = true; + //AndroidPlatform.Instance.DefaultViewDrawType = ViewDrawType.BitmapOnPreDraw; + AndroidPlatform.Instance.DefaultViewDrawType = ViewDrawType.CanvasOnDraw; + //AndroidPlatform.Instance.DefaultViewDrawType = ViewDrawType.BitmapBackgroundRender; + //AndroidPlatform.Instance.DefaultViewDrawType = ViewDrawType.SurfaceViewCanvasOnDraw; + AndroidPlatform.Instance.DefaultPointUnit = Android.Platform.CanvasPlatform.PointUnit.DP; + //AndroidPlatform.Instance.DefaultPointUnit = Android.Platform.CanvasPlatform.PointUnit.Pixel; + AndroidPlatform.Instance.OverrideAnimateFramesPerSecond = 16; + + App app; + if (Perspex.Application.Current != null) + app = (App)Perspex.Application.Current; + else + app = new App(); + + var window = TestUI.TestUIBuilder.BuildTestUI(); + //var window = BuildPlatforSetup(); + + window.Show(); + app.Run(window); + } + public static void StartAppFromSetup() + { + PerspexLocator.Current.GetService().Dispose(); + + AndroidPlatform.Instance.RegisterViewDrawType(); + AndroidPlatform.Instance.RegisterViewPointUnits(); + + var window = TestUI.TestUIBuilder.BuildTestUI(); + + window.Show(); + Perspex.Application.Current.Run(window); + } + } +} \ No newline at end of file diff --git a/src/Android/Perspex.AndroidTestApplication/Perspex.AndroidTestApplication.csproj b/src/Android/Perspex.AndroidTestApplication/Perspex.AndroidTestApplication.csproj new file mode 100644 index 0000000000..89fd0ed151 --- /dev/null +++ b/src/Android/Perspex.AndroidTestApplication/Perspex.AndroidTestApplication.csproj @@ -0,0 +1,215 @@ + + + + Debug + AnyCPU + 8.0.30703 + 2.0 + {FF69B927-C545-49AE-8E16-3D14D621AA12} + {EFBA0AD7-5A72-4C68-AF49-83D382785DCF};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + Library + Properties + Perspex.AndroidTestApplication + Perspex Test Application + 512 + true + Resources\Resource.Designer.cs + Off + False + v4.4 + Properties\AndroidManifest.xml + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + True + None + + False + False + False + armeabi,armeabi-v7a,x86 + + + Xamarin + False + True + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + False + SdkOnly + + True + False + False + armeabi,armeabi-v7a,x86 + + + Xamarin + False + False + False + False + False + + + + + + ..\..\..\packages\Serilog.1.5.9\lib\portable-net45+win+wpa81+wp80+MonoAndroid10+MonoTouch10\Serilog.dll + True + + + ..\..\..\packages\Splat.1.6.2\lib\monoandroid\Splat.dll + True + + + ..\..\..\packages\Sprache.2.0.0.47\lib\portable-net4+netcore45+win8+wp8+sl5+MonoAndroid1+MonoTouch1\Sprache.dll + True + + + + + + + + ..\..\..\packages\Rx-Core.2.2.5\lib\portable-windows8+net45+wp8\System.Reactive.Core.dll + True + + + ..\..\..\packages\Rx-Interfaces.2.2.5\lib\portable-windows8+net45+wp8\System.Reactive.Interfaces.dll + True + + + ..\..\..\packages\Rx-Linq.2.2.5\lib\portable-windows8+net45+wp8\System.Reactive.Linq.dll + True + + + ..\..\..\packages\Rx-PlatformServices.2.2.5\lib\portable-windows8+net45+wp8\System.Reactive.PlatformServices.dll + True + + + + + + + + + + + + + + + + + + + + + + + + + + Designer + + + + + + + + + + + {7b92af71-6287-4693-9dcb-bd5b6e927e23} + Perspex.Android + + + {3e53a01a-b331-47f3-b828-4a5717e77a24} + Perspex.Markup.Xaml + + + {d211e587-d8bc-45b9-95a4-f297c8fa5200} + Perspex.Animation + + + {799a7bb5-3c2c-48b6-85a7-406a12c420da} + Perspex.Application + + + {b09b78d8-9b26-48b0-9149-d64a2f120f3f} + Perspex.Base + + + {d2221c82-4a25-4583-9b43-d791e3f6820c} + Perspex.Controls + + + {7062ae20-5dcc-4442-9645-8195bdece63e} + Perspex.Diagnostics + + + {62024b2d-53eb-4638-b26b-85eeaa54866e} + Perspex.Input + + + {6b0ed19d-a08b-461c-a9d9-a9ee40b0c06b} + Perspex.Interactivity + + + {42472427-4774-4c81-8aff-9f27b8e31721} + Perspex.Layout + + + {6417b24e-49c2-4985-8db2-3ab9d898ec91} + Perspex.ReactiveUI + + + {eb582467-6abb-43a1-b052-e981ba910e3a} + Perspex.SceneGraph + + + {f1baa01a-f176-4c6a-b39d-5b40bb1b148f} + Perspex.Styling + + + {3e10a5fa-e8da-48b1-ad44-6a5b6cb7750f} + Perspex.Themes.Default + + + {5fb2b005-0a7f-4dad-add4-3ed01444e63d} + Perspex.HtmlRenderer + + + + + Always + + + + + PreserveNewest + + + + + \ No newline at end of file diff --git a/src/Android/Perspex.AndroidTestApplication/Properties/AndroidManifest.xml b/src/Android/Perspex.AndroidTestApplication/Properties/AndroidManifest.xml new file mode 100644 index 0000000000..6c411b124b --- /dev/null +++ b/src/Android/Perspex.AndroidTestApplication/Properties/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/Android/Perspex.AndroidTestApplication/Properties/AssemblyInfo.cs b/src/Android/Perspex.AndroidTestApplication/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..e435ddc807 --- /dev/null +++ b/src/Android/Perspex.AndroidTestApplication/Properties/AssemblyInfo.cs @@ -0,0 +1,30 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. + +[assembly: AssemblyTitle("Perspex.AndroidTestApplication")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Perspex.AndroidTestApplication")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: ComVisible(false)] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] + +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file diff --git a/src/Android/Perspex.AndroidTestApplication/Resources/AboutResources.txt b/src/Android/Perspex.AndroidTestApplication/Resources/AboutResources.txt new file mode 100644 index 0000000000..194ae28a59 --- /dev/null +++ b/src/Android/Perspex.AndroidTestApplication/Resources/AboutResources.txt @@ -0,0 +1,50 @@ +Images, layout descriptions, binary blobs and string dictionaries can be included +in your application as resource files. Various Android APIs are designed to +operate on the resource IDs instead of dealing with images, strings or binary blobs +directly. + +For example, a sample Android app that contains a user interface layout (main.xml), +an internationalization string table (strings.xml) and some icons (drawable-XXX/icon.png) +would keep its resources in the "Resources" directory of the application: + +Resources/ + drawable-hdpi/ + icon.png + + drawable-ldpi/ + icon.png + + drawable-mdpi/ + icon.png + + layout/ + main.xml + + values/ + strings.xml + +In order to get the build system to recognize Android resources, set the build action to +"AndroidResource". The native Android APIs do not operate directly with filenames, but +instead operate on resource IDs. When you compile an Android application that uses resources, +the build system will package the resources for distribution and generate a class called +"Resource" that contains the tokens for each one of the resources included. For example, +for the above Resources layout, this is what the Resource class would expose: + +public class Resource { + public class drawable { + public const int icon = 0x123; + } + + public class layout { + public const int main = 0x456; + } + + public class strings { + public const int first_string = 0xabc; + public const int second_string = 0xbcd; + } +} + +You would then use R.drawable.icon to reference the drawable/icon.png file, or Resource.layout.main +to reference the layout/main.xml file, or Resource.strings.first_string to reference the first +string in the dictionary file values/strings.xml. \ No newline at end of file diff --git a/src/Android/Perspex.AndroidTestApplication/Resources/Resource.Designer.cs b/src/Android/Perspex.AndroidTestApplication/Resources/Resource.Designer.cs new file mode 100644 index 0000000000..a2725d03e5 --- /dev/null +++ b/src/Android/Perspex.AndroidTestApplication/Resources/Resource.Designer.cs @@ -0,0 +1,102 @@ +#pragma warning disable 1591 +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +[assembly: global::Android.Runtime.ResourceDesignerAttribute("Perspex.AndroidTestApplication.Resource", IsApplication=true)] + +namespace Perspex.AndroidTestApplication +{ + + + [System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Android.Build.Tasks", "1.0.0.0")] + public partial class Resource + { + + static Resource() + { + global::Android.Runtime.ResourceIdManager.UpdateIdValues(); + } + + public static void UpdateIdValues() + { + global::Perspex.Android.Resource.String.ApplicationName = global::Perspex.AndroidTestApplication.Resource.String.ApplicationName; + global::Perspex.Android.Resource.String.Hello = global::Perspex.AndroidTestApplication.Resource.String.Hello; + global::Splat.Resource.String.library_name = global::Perspex.AndroidTestApplication.Resource.String.library_name; + } + + public partial class Attribute + { + + static Attribute() + { + global::Android.Runtime.ResourceIdManager.UpdateIdValues(); + } + + private Attribute() + { + } + } + + public partial class Drawable + { + + // aapt resource value: 0x7f020000 + public const int Icon = 2130837504; + + static Drawable() + { + global::Android.Runtime.ResourceIdManager.UpdateIdValues(); + } + + private Drawable() + { + } + } + + public partial class Layout + { + + // aapt resource value: 0x7f030000 + public const int Main = 2130903040; + + static Layout() + { + global::Android.Runtime.ResourceIdManager.UpdateIdValues(); + } + + private Layout() + { + } + } + + public partial class String + { + + // aapt resource value: 0x7f040002 + public const int ApplicationName = 2130968578; + + // aapt resource value: 0x7f040001 + public const int Hello = 2130968577; + + // aapt resource value: 0x7f040000 + public const int library_name = 2130968576; + + static String() + { + global::Android.Runtime.ResourceIdManager.UpdateIdValues(); + } + + private String() + { + } + } + } +} +#pragma warning restore 1591 diff --git a/src/Android/Perspex.AndroidTestApplication/Resources/drawable/Icon.png b/src/Android/Perspex.AndroidTestApplication/Resources/drawable/Icon.png new file mode 100644 index 0000000000000000000000000000000000000000..8074c4c571b8cd19e27f4ee5545df367420686d7 GIT binary patch literal 4147 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 literal 0 HcmV?d00001 diff --git a/src/Android/Perspex.AndroidTestApplication/Resources/layout/Main.axml b/src/Android/Perspex.AndroidTestApplication/Resources/layout/Main.axml new file mode 100644 index 0000000000..fc5b1b2548 --- /dev/null +++ b/src/Android/Perspex.AndroidTestApplication/Resources/layout/Main.axml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/src/Android/Perspex.AndroidTestApplication/Resources/values/Strings.xml b/src/Android/Perspex.AndroidTestApplication/Resources/values/Strings.xml new file mode 100644 index 0000000000..b2d19c0877 --- /dev/null +++ b/src/Android/Perspex.AndroidTestApplication/Resources/values/Strings.xml @@ -0,0 +1,6 @@ + + + + Hello World, Click Me! + Perspex.AndroidTestApplication + \ No newline at end of file diff --git a/src/Android/Perspex.AndroidTestApplication/SetupTestUI.cs b/src/Android/Perspex.AndroidTestApplication/SetupTestUI.cs new file mode 100644 index 0000000000..48cde691da --- /dev/null +++ b/src/Android/Perspex.AndroidTestApplication/SetupTestUI.cs @@ -0,0 +1,162 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows.Input; +using ReactiveUI; +using Perspex.Android.Platform.CanvasPlatform; +using Perspex.Android.Platform.Specific.Helpers; +using Perspex.Android; +using Perspex.Styling; +using Perspex.Controls.Primitives; +using Perspex.Controls.Templates; +using Perspex.Controls; +using Perspex.Markup.Xaml.Data; +using Perspex.Controls.Presenters; + +namespace Perspex.AndroidTestApplication +{ + public class SetupTestUI + { + private static Style HeaderedContentControlStyle() + { + return new Style(x => x.OfType()) + { + Setters = new[] + { + new Setter(TemplatedControl.TemplateProperty, new FuncControlTemplate(control=> { + return new Border() + { + [~Border.BackgroundProperty] = control[~TemplatedControl.BackgroundProperty], + Child = new StackPanel() + { + Children = new Controls.Controls() + { + new ContentPresenter + { + Name = "headercontentPresenter", + [~ContentPresenter.ContentProperty] = control[~HeaderedContentControl.HeaderProperty], + }, + new ContentPresenter + { + Name = "contentPresenter", + [~ContentPresenter.ContentProperty] = control[~ContentControl.ContentProperty], + } + } + } + }; + })), + }, + }; + } + + + + private Window BuildPlatforSetup() + { + Perspex.Application.Current.Styles.Add(HeaderedContentControlStyle()); + var vm = new PlatformSetupViewModel(); + vm.PointUnit = AndroidPlatform.Instance.DefaultPointUnit.ToString(); + vm.ViewDrawType = AndroidPlatform.Instance.DefaultViewDrawType.ToString(); + var lbPointUnit = new ListBox() + { + Name = "PointUnit", + Items = vm.PointUnits, + [ListBox.SelectedItemProperty] = vm.PointUnit + }; + var lbViewDrawType = new ListBox() + { + Name = "ViewDrawType", + Items = vm.ViewDrawTypes, + [ListBox.SelectedItemProperty] = vm.ViewDrawType + }; + var window = new Window() + { + DataContext = vm, + Content = new Grid() + { + Margin = new Thickness(20, 100, 0, 0), + RowDefinitions = new RowDefinitions() { + new RowDefinition() { Height = GridLength.Auto }, + new RowDefinition() + }, + Children = new Controls.Controls() + { + new Button() + { + [Grid.RowProperty] = 1, + HorizontalAlignment = Layout.HorizontalAlignment.Center, + VerticalAlignment = Layout.VerticalAlignment.Center, + Content = "Start application", + Command = vm.Command + }, + new StackPanel() + { + [Grid.RowProperty] = 0, + Orientation = Orientation.Horizontal, + Gap=10, + Children= new Controls.Controls + { + new HeaderedContentControl() {Content = lbPointUnit, Header="Point Units Type" }, + new HeaderedContentControl() { Content = lbViewDrawType, Header ="Draw View type" }, + } + } + } + } + }; + + var b = new Binding(); + b.SourcePropertyPath = "PointUnit"; + b.Mode = BindingMode.TwoWay; + b.Bind(lbPointUnit, ListBox.SelectedItemProperty); + b = new Binding(); + b.SourcePropertyPath = "ViewDrawType"; + b.Mode = BindingMode.TwoWay; + b.Bind(lbViewDrawType, ListBox.SelectedItemProperty); + + return window; + } + + public class PlatformSetupViewModel : ReactiveObject + { + public PlatformSetupViewModel() + { + ViewDrawTypes = Enum.GetNames(typeof(ViewDrawType)); + PointUnits = Enum.GetNames(typeof(PointUnit)); + var cmd = ReactiveCommand.Create(); + + Command = cmd; + cmd.Subscribe(onNext: o => + { + var pu = (PointUnit)Enum.Parse(typeof(PointUnit), PointUnit); + var vdt = (ViewDrawType)Enum.Parse(typeof(ViewDrawType), ViewDrawType); + AndroidPlatform.Instance.DefaultPointUnit = pu; + AndroidPlatform.Instance.DefaultViewDrawType = vdt; + MainBaseActivity.StartAppFromSetup(); + //restart app ; + }); + } + + public string[] ViewDrawTypes { get; set; } + public string[] PointUnits { get; set; } + + private string _viewDrawType; + + public string ViewDrawType + { + get { return _viewDrawType; } + set { this.RaiseAndSetIfChanged(ref _viewDrawType, value); } + } + + private string _pointUnit; + + public string PointUnit + { + get { return _pointUnit; } + set { this.RaiseAndSetIfChanged(ref _pointUnit, value); } + } + + public ICommand Command { get; set; } + } + } +} \ No newline at end of file diff --git a/src/Android/Perspex.AndroidTestApplication/TestUI.cs b/src/Android/Perspex.AndroidTestApplication/TestUI.cs new file mode 100644 index 0000000000..462e9bc173 --- /dev/null +++ b/src/Android/Perspex.AndroidTestApplication/TestUI.cs @@ -0,0 +1,1659 @@ +using Perspex; +using Perspex.Animation; +using Perspex.Collections; +using Perspex.Controls; +using Perspex.Controls.Html; +using Perspex.Controls.Presenters; +using Perspex.Controls.Primitives; +using Perspex.Controls.Shapes; +using Perspex.Controls.Templates; +using Perspex.Interactivity; +using Perspex.Layout; +using Perspex.Media; +using Perspex.Media.Imaging; +using Perspex.Platform; +using Perspex.Styling; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Linq; + +namespace TestUI +{ + public class TestUIBuilder + { +#if __ANDROID__ + + //private static string imagePath = "res://component/Perspex Test Application/Perspex.AndroidTestApplication.github_icon.png"; + private static string imagePath = "github_icon.png"; + + private static string imagePathP = "pattern.jpg"; + + private static double BigFontSize = 75; + + //public static double FontScale = 2; + //public static double Scale = 2; + public static double FontScale = 1; + + public static double Scale = 1; + public static Thickness DefaultMargin = new Thickness(10, 20, 10, 10); +#else + private static string imagePath = "res://component/TestApplication.github_icon.png"; + private static string imagePathP = "res://component/TestApplication.pattern.jpg"; + //private static string imagePath = "github_icon.png"; + + private static double BigFontSize = 80; + public static double FontScale = 2; + public static double Scale = 2; + public static Thickness DefaultMargin = new Thickness(10, 0, 0, 0); +#endif + + private static System.IO.Stream OpenGitHubIconSteam() + { + return PerspexLocator.Current.GetService().Open(new Uri(imagePath, imagePath.Contains("://") ? UriKind.Absolute : UriKind.Relative)); + } + + private static System.IO.Stream OpenPatternSteam() + { + //"pattern.jpg" + return PerspexLocator.Current.GetService().Open(new Uri(imagePathP, imagePath.Contains("://") ? UriKind.Absolute : UriKind.Relative)); + } + + internal class Node + { + public Node() + { + Children = new PerspexList(); + } + + public string Name { get; set; } + public PerspexList Children { get; set; } + } + + internal class Item + { + public string Name { get; set; } + public string Value { get; set; } + } + + private static readonly PerspexList s_treeData = new PerspexList + { + new Node + { + Name = "Root 1", + Children = new PerspexList + { + new Node + { + Name = "Child 1", + }, + new Node + { + Name = "Child 2", + Children = new PerspexList + { + new Node + { + Name = "Grandchild 1", + }, + new Node + { + Name = "Grandmaster Flash", + }, + } + }, + new Node + { + Name = "Child 3", + }, + } + }, + new Node + { + Name = "Root 2", + }, + }; + + private static readonly PerspexList s_listBoxData = new PerspexList + { + new Item { Name = "Item 1", Value = "Item 1 Value" }, + new Item { Name = "Item 2", Value = "Item 2 Value" }, + new Item { Name = "Item 3", Value = "Item 3 Value" }, + new Item { Name = "Item 4", Value = "Item 4 Value" }, + new Item { Name = "Item 5", Value = "Item 5 Value" }, + new Item { Name = "Item 6", Value = "Item 6 Value" }, + new Item { Name = "Item 7", Value = "Item 7 Value" }, + new Item { Name = "Item 8", Value = "Item 8 Value" }, + }; + + public static Window BuildGridWithSomeButtonsAndStuff() + { + var buttonToClick = new Button + { + Content = "Tap Me", + Width = 200, + Foreground = Brushes.Green, + Margin = new Thickness(5), + Background = Brushes.Blue, + FontSize = 50 + }; + + var clickCount = 0; + buttonToClick.Click += (object sender, RoutedEventArgs e) => + { + Console.WriteLine("You clicked it :)"); + buttonToClick.Content = $"Tap x{clickCount}"; + clickCount++; + }; + + var window = new Window + { + Title = "Perspex Test Application", + Background = Brushes.DarkTurquoise, + Content = new Grid + { + Margin = DefaultMargin, // skip the status bar area on mobile + ColumnDefinitions = new ColumnDefinitions + { + new ColumnDefinition(1, GridUnitType.Star), + new ColumnDefinition(1, GridUnitType.Star) + }, + RowDefinitions = new RowDefinitions + { + new RowDefinition(80, GridUnitType.Pixel), + new RowDefinition(160, GridUnitType.Pixel), + new RowDefinition(1, GridUnitType.Star), + new RowDefinition(1, GridUnitType.Star) + }, + Children = new Controls + { + new TextBox + { + [Grid.RowProperty] = 0, + [Grid.ColumnSpanProperty] = 2, + Text = "Welcome to Perspex on Android !!!", + Foreground = Brushes.SteelBlue, + Background = Brushes.White, + FontSize = 50, + Margin = new Thickness(5, 7, 5, 5), + //TextAlignment = TextAlignment.Left + }, +//// + new StackPanel + { + Orientation = Orientation.Horizontal, + [Grid.RowProperty] = 1, + [Grid.ColumnSpanProperty] = 2, + Background = Brush.Parse("#000000"), + Children = new Controls + { + buttonToClick, + new Button + { + Content = "Button 2", + Width = 250, + FontSize = 50, + Margin = new Thickness(5) + }, + new Button + { + Content = "Button 3", + Width = 250, + FontSize = 50, + Margin = new Thickness(5) + } + } + }, +//// + new Image + { + [Grid.RowProperty] = 2, + [Grid.ColumnProperty] = 0, + Margin = new Thickness(20), + //Source = new Bitmap("github_icon.png"), + Source = new Bitmap(OpenGitHubIconSteam()), + Opacity = 0.4 + }, +//// + new Ellipse + { + [Grid.RowProperty] = 3, + [Grid.ColumnProperty] = 0, + Margin = new Thickness(20), + Fill = Brushes.Blue + }, +//// + new TextBlock + { + [Grid.RowProperty] = 2, + [Grid.ColumnProperty] = 1, + Text = + "How does this look? Is this text going to wrap and then look nice within the bounds of this widget? If not I will be extremely disappointed!\n\nWill we start a new paragraph here? If not there will be hell to pay!!!!", + Foreground = Brushes.Purple, + Background = Brushes.Transparent, + FontSize = 28, + Margin = new Thickness(10, 30, 10, 10) + }, + new Path + { + Data = + StreamGeometry.Parse( + "M 50,50 l 15,0 l 5,-15 l 5,15 l 15,0 l -10,10 l 4,15 l -15,-9 l -15,9 l 7,-15 Z"), + [Grid.RowProperty] = 3, + [Grid.ColumnProperty] = 1, + Margin = new Thickness(20), + Fill = Brushes.White, + Stroke = Brushes.Blue, + StrokeThickness = 4 + } + } + } + }; + + return window; + } + + public static Window BuildSimpleTextUI() + { + var window = new Window + { + Title = "Perspex Test Application", + Background = Brushes.Green, + Content = new Grid + { + VerticalAlignment = VerticalAlignment.Top, + HorizontalAlignment = HorizontalAlignment.Left, + Margin = DefaultMargin, + Children = new Controls + { + new TextBlock + { + Foreground = Brushes.GhostWhite, + Background = Brushes.BlueViolet, + VerticalAlignment = VerticalAlignment.Top, + HorizontalAlignment = HorizontalAlignment.Left, + Text = + "Hello! This is a test of text wrapping and me actually getting something to render for the first time in life. Im and happy to say this was by no means all me. I want to thank everyone who helped me this far.", + FontSize = 80, + Margin = new Thickness(10), + TextWrapping = TextWrapping.Wrap + }, + new CheckBox() { + Content = "CheckBox checked", + IsChecked = true + }, + new CheckBox() { + Content = "CheckBox default", + IsChecked = true + }, + new Canvas() + { + Width = 200, + Height = 200, + Children = new Controls() + { + new Path + { + Name = "checkMark", + Fill = new SolidColorBrush(0xff333333), + Width = 200, + Height = 200, + Stretch = Stretch.Uniform, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + Data = StreamGeometry.Parse("M 1145.607177734375,430 C1145.607177734375,430 1141.449951171875,435.0772705078125 1141.449951171875,435.0772705078125 1141.449951171875,435.0772705078125 1139.232177734375,433.0999755859375 1139.232177734375,433.0999755859375 1139.232177734375,433.0999755859375 1138,434.5538330078125 1138,434.5538330078125 1138,434.5538330078125 1141.482177734375,438 1141.482177734375,438 1141.482177734375,438 1141.96875,437.9375 1141.96875,437.9375 1141.96875,437.9375 1147,431.34619140625 1147,431.34619140625 1147,431.34619140625 1145.607177734375,430 1145.607177734375,430 z"), + //Data = StreamGeometry.Parse("M 114.5607177734375,43.0 C114.5607177734375,43.0 114.1449951171875,43.50772705078125 114.1449951171875,43.50772705078125 114.1449951171875,43.50772705078125 113.9232177734375,43.30999755859375 113.9232177734375,43.30999755859375 113.9232177734375,43.30999755859375 1138,43.45538330078125 113.8,43.45538330078125 113.8,43.45538330078125 114.1482177734375,43.8 114.1482177734375,438 114.1482177734375,43.8 114.196875,43.79375 114.196875,43.79375 114.196875,43.79375 114.7,43.134619140625 114.7,43.134619140625 114.7,43.134619140625 114.5607177734375,43.0 114.5607177734375,43.0 z"), + [Grid.ColumnProperty] = 0, + } + } + } + } + } + }; + + return window; + } + + public static Window BuildSimpleControlsUI() + { + var window = new Window + { + Title = "Perspex Test Application", + Background = Brushes.Green, + Content = new StackPanel + { + VerticalAlignment = VerticalAlignment.Top, + HorizontalAlignment = HorizontalAlignment.Left, + Margin = DefaultMargin, + Children = new Controls + { + //new TextBlock + //{ + // Foreground = Brushes.GhostWhite, + // Background = Brushes.BlueViolet, + // VerticalAlignment = VerticalAlignment.Top, + // HorizontalAlignment = HorizontalAlignment.Left, + // Text = + // "Hello! This is a test of text wrapping and me actually getting something to render for the first time in life. Im and happy to say this was by no means all me. I want to thank everyone who helped me this far.", + // FontSize = 80, + // Margin = new Thickness(10), + // TextWrapping = TextWrapping.Wrap + //}, + new CheckBox() { + Content = "CheckBox checked", + IsChecked = true + }, + //new CheckBox() { + // Content = "CheckBox default", + // IsChecked = true + //}, + } + } + }; + + return window; + } + + public static Window BuildSimpleTextBoxUI() + { + var window = new Window + { + Title = "Perspex Test Application", + Background = Brushes.Green, + Content = new Grid + { + VerticalAlignment = VerticalAlignment.Top, + HorizontalAlignment = HorizontalAlignment.Left, + Margin = DefaultMargin, + Background = Brushes.Yellow, + Children = new Controls + { + new TextBox + { + Foreground = Brushes.GhostWhite, + Background = Brushes.BlueViolet, + VerticalAlignment = VerticalAlignment.Top, + HorizontalAlignment = HorizontalAlignment.Left, + Text = "1234567890 aaaaaaaa bbbbbbbbb ccccccccc ddddddddd eeeeeeeeee", + //Text = + // "Hello! This is a test of text wrapping and me actually getting something to render for the first time in life. Im and happy to say this was by no means all me. I want to thank everyone who helped me this far.", + FontSize = BigFontSize, + Margin = new Thickness(10), + AcceptsReturn = true, + Width=800, + //AcceptsTab = true, + TextWrapping = TextWrapping.Wrap + } + } + } + }; + + return window; + } + + public static Window BuildStackPanelUI() + { + var window = new Window + { + Title = "Perspex Test Application", + Background = Brushes.Green, + Content = new Grid + { + VerticalAlignment = VerticalAlignment.Top, + HorizontalAlignment = HorizontalAlignment.Left, + Margin = DefaultMargin, + Children = new Controls + { + new StackPanel + { + Orientation = Orientation.Horizontal, + [Grid.RowProperty] = 1, + [Grid.ColumnSpanProperty] = 2, + Background = SolidColorBrush.Parse("#000000"), + Children = new Controls + { + new Button + { + Content = "Button 1", + Width = 100, + Foreground = Brushes.Blue, + Margin = new Thickness(5) + }, + new Button + { + Content = "Button 2", + Width = 100, + Margin = new Thickness(5) + }, + new Button + { + Content = "Button 3", + Width = 100, + Margin = new Thickness(5) + } + } + } + } + } + }; + + return window; + } + + private static List stringItemsSource; + + public static IEnumerable StringItems + { + get + { + if (stringItemsSource == null) + { + stringItemsSource = new List(); + for (int i = 0; i < 10; i++) + { + stringItemsSource.Add(string.Format($"Item {i}")); + } + } + + return stringItemsSource; + } + } + + public static Window BuildListBoxUI() + { + var window = new Window + { + Title = "Perspex Test Application", + Background = Brushes.Green, + Content = new Grid + { + VerticalAlignment = VerticalAlignment.Top, + HorizontalAlignment = HorizontalAlignment.Left, + Margin = DefaultMargin, + Children = new Controls + { + new StackPanel + { + Orientation = Orientation.Horizontal, + [Grid.RowProperty] = 0, + [Grid.ColumnSpanProperty] = 2, + Background = new SolidColorBrush(Colors.Azure), + + Children = new Controls() + { + new TextBlock() {Text="vertical listbox" }, + new ListBox() {Items = StringItems, Margin = new Thickness(10), SelectedItem=StringItems.First() }, + new TextBlock() {Text="horizontal listbox" }, + new ListBox() + { + Items = StringItems, Margin = new Thickness(10), + [ScrollViewer.HorizontalScrollBarVisibilityProperty] = ScrollBarVisibility.Auto, + VerticalAlignment =VerticalAlignment.Top, + ItemsPanel = new FuncTemplate(()=>new StackPanel() { Orientation=Orientation.Horizontal }) , + SelectedItem=StringItems.First(), + }, + } + } + } + } + }; + + return window; + } + + public static Window BuildComboBoxUI() + { + var window = new Window + { + Title = "Perspex Test Application", + Background = Brushes.Green, + Content = new Grid + { + VerticalAlignment = VerticalAlignment.Center, + HorizontalAlignment = HorizontalAlignment.Left, + Margin = DefaultMargin, + Children = new Controls + { + new StackPanel + { + Orientation = Orientation.Horizontal, + [Grid.RowProperty] = 0, + [Grid.ColumnSpanProperty] = 2, + Background = new SolidColorBrush(Colors.Azure), + + Children = new Controls() + { + new TextBlock() {Text="vertical combobox:" }, + new DropDown() {Items = StringItems, Margin = new Thickness(10), SelectedItem=StringItems.First() }, + new TextBlock() {Text="horizontal combobox:" }, + new DropDown() { + Items = StringItems, Margin = new Thickness(10), + [ScrollViewer.HorizontalScrollBarVisibilityProperty] = ScrollBarVisibility.Auto, + VerticalAlignment =VerticalAlignment.Top, + ItemsPanel = new FuncTemplate(()=>new StackPanel() { Orientation=Orientation.Horizontal, Gap=5 }), + SelectedItem=StringItems.First() + }, + } + } + } + } + }; + + Application.Current.DataTemplates.Add(new FuncDataTemplate(s => + { + return new Grid() + { + RowDefinitions = new RowDefinitions() + { + new RowDefinition(), + new RowDefinition() + }, + Children = new Controls() + { new Ellipse() {Fill=Brushes.Orange, [Grid.RowProperty]=0, [Grid.RowSpanProperty]=2, Opacity=0.4 }, + //new Rectangle() { Fill=Brushes.Orange, [Grid.RowProperty]=0 }, + new TextBlock() { Text="Item :", [Grid.RowProperty]=0, Margin=new Thickness(1), FontSize=15, Foreground=Brushes.Blue }, + new TextBlock() { Text=s, [Grid.RowProperty]=1, Margin=new Thickness(5), FontSize=15 }, + } + }; + //return new TextBlock() { Text = s, Margin = new Thickness(5) }; + })); + + return window; + } + + public static Control DropDownControlTemplate(DropDown control) + { + Border result = new Border + { + [~Border.BackgroundProperty] = control[~TemplatedControl.BackgroundProperty], + [~Border.BorderBrushProperty] = control[~TemplatedControl.BorderBrushProperty], + [~Border.BorderThicknessProperty] = control[~TemplatedControl.BorderThicknessProperty], + Child = new Grid + { + Name = "container", + ColumnDefinitions = new ColumnDefinitions + { + new ColumnDefinition(new GridLength(1, GridUnitType.Star)), + new ColumnDefinition(GridLength.Auto), + }, + Children = new Controls + { + new ContentControl + { + Name = "contentControl", + Margin = new Thickness(3), + [~ContentControl.ContentProperty] = control[~DropDown.SelectionBoxItemProperty], + [~Layoutable.HorizontalAlignmentProperty] = control[~DropDown.HorizontalContentAlignmentProperty], + [~Layoutable.VerticalAlignmentProperty] = control[~DropDown.VerticalContentAlignmentProperty], + }, + new ToggleButton + { + Name = "toggle", + BorderThickness = 0, + Background = Brushes.Transparent, + ClickMode = ClickMode.Press, + Focusable = false, + Content = new Path + { + Name = "checkMark", + Fill = Brushes.Black, + Width = 8, + Height = 4, + Stretch = Stretch.Uniform, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + Data = StreamGeometry.Parse("F1 M 301.14,-189.041L 311.57,-189.041L 306.355,-182.942L 301.14,-189.041 Z"), + [Grid.ColumnProperty] = 0, + }, + [~~ToggleButton.IsCheckedProperty] = control[~~DropDown.IsDropDownOpenProperty], + [Grid.ColumnProperty] = 1, + }, + new Popup + { + Name = "popup", + Child = new Border + { + BorderBrush = Brushes.Black, + BorderThickness = 1, + Padding = new Thickness(4), + Child = new ItemsPresenter + { + MemberSelector = control.MemberSelector, + [~ItemsPresenter.ItemsProperty] = control[~ItemsControl.ItemsProperty], + [~ItemsPresenter.ItemsPanelProperty] = control[~ItemsControl.ItemsPanelProperty], + } + }, + PlacementTarget = control, + StaysOpen = false, + [~~Popup.IsOpenProperty] = control[~~DropDown.IsDropDownOpenProperty], + [~Layoutable.MinWidthProperty] = control[~Visual.BoundsProperty].Cast().Select(x => (object)x.Width), + } + }, + }, + }; + + return result; + } + + private static TabItem ButtonsTab() + { + var result = new TabItem + { + Header = "Button", + Content = new ScrollViewer() + { + CanScrollHorizontally = false, + Content = new StackPanel + { + Margin = new Thickness(10 * Scale), + Orientation = Orientation.Vertical, + Gap = 4 * Scale, + Children = new Controls + { + new TextBlock + { + Text = "Button", + FontWeight = FontWeight.Medium, + FontSize = 20*FontScale, + Foreground = SolidColorBrush.Parse("#212121"), + }, + new TextBlock + { + Text = "A button control", + FontSize = 13*FontScale, + Foreground = SolidColorBrush.Parse("#727272"), + Margin = new Thickness(0, 0, 0, 10*Scale) + }, + new Button + { + Width = 150*Scale, + Content = "Button" + }, + new Button + { + Width = 150*Scale, + Content = "Disabled", + IsEnabled = false, + }, + new TextBlock + { + Margin = new Thickness(0, 40*Scale, 0, 0), + Text = "ToggleButton", + FontWeight = FontWeight.Medium, + FontSize = 20*FontScale, + Foreground = SolidColorBrush.Parse("#212121"), + }, + new TextBlock + { + Text = "A toggle button control", + FontSize = 13*FontScale, + Foreground = SolidColorBrush.Parse("#727272"), + Margin = new Thickness(0, 0, 0, 10*Scale) + }, + new ToggleButton + { + Width = 150*Scale, + IsChecked = true, + Content = "On" + }, + new ToggleButton + { + Width = 150*Scale, + IsChecked = false, + Content = "Off" + }, + } + } + }, + }; + + return result; + } + + private static TabItem HtmlTab() + { + return new TabItem + { + Header = "Text", + Content = new ScrollViewer() + { + CanScrollHorizontally = false, + Content = new StackPanel() + { + Margin = new Thickness(10), + Orientation = Orientation.Vertical, + Gap = 4, + Children = new Controls + { + new TextBlock + { + Text = "TextBlock", + FontWeight = FontWeight.Medium, + FontSize = 20, + Foreground = SolidColorBrush.Parse("#212121"), + }, + new TextBlock + { + Text = "A control for displaying text.", + FontSize = 13, + Foreground = SolidColorBrush.Parse("#727272"), + Margin = new Thickness(0, 0, 0, 10) + }, + new TextBlock + { + Text = "Lorem ipsum dolor sit amet, consectetuer adipiscing elit.", + FontSize = 11 + }, + new TextBlock + { + Text = "Lorem ipsum dolor sit amet, consectetuer adipiscing elit.", + FontSize = 11, + FontWeight = FontWeight.Medium + }, + new TextBlock + { + Text = "Lorem ipsum dolor sit amet, consectetuer adipiscing elit.", + FontSize = 11, + FontWeight = FontWeight.Bold + }, + new TextBlock + { + Text = "Lorem ipsum dolor sit amet, consectetuer adipiscing elit.", + FontSize = 11, + FontStyle = FontStyle.Italic, + }, + new TextBlock + { + Margin = new Thickness(0, 40, 0, 0), + Text = "HtmlLabel", + FontWeight = FontWeight.Medium, + FontSize = 20, + Foreground = SolidColorBrush.Parse("#212121"), + }, + new TextBlock + { + Text = "A label capable of displaying HTML content", + FontSize = 13, + Foreground = SolidColorBrush.Parse("#727272"), + Margin = new Thickness(0, 0, 0, 10) + }, + new HtmlLabel + { + Background = SolidColorBrush.Parse("#CCCCCC"), + Padding = new Thickness(5), + Text = @"

Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis.

+

Header Level 2

+ +
    +
  1. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  2. +
  3. Aliquam tincidunt mauris eu risus.
  4. +
+ +

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est.

+ +

Header Level 3

+ +
    +
  • Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  • +
  • Aliquam tincidunt mauris eu risus.
  • +
" + } + } + } + } + }; + } + + private static TabItem TextTab() + { + return new TabItem + { + Header = "Input", + Content = new ScrollViewer() + { + Content = new StackPanel + { + Margin = new Thickness(10 * Scale), + Orientation = Orientation.Vertical, + Gap = 4 * Scale, + Children = new Controls + { + new TextBlock + { + Text = "TextBox", + FontWeight = FontWeight.Medium, + FontSize = 20*FontScale, + Foreground = SolidColorBrush.Parse("#212121"), + }, + new TextBlock + { + Text = "A text box control", + FontSize = 13*FontScale, + Foreground = SolidColorBrush.Parse("#727272"), + Margin = new Thickness(0, 0, 0, 10*Scale) + }, + + new TextBox { Text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", Width = 200}, + new TextBox { Width = 200*Scale, Watermark="Watermark"}, + new TextBox { Width = 200*Scale, Watermark="Floating Watermark", UseFloatingWatermark = true }, + new TextBox { AcceptsReturn = true, TextWrapping = TextWrapping.Wrap, Width = 200*Scale, Height = 150*Scale, Text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est." }, + new TextBlock + { + Margin = new Thickness(0, 40*Scale, 0, 0), + Text = "CheckBox", + FontWeight = FontWeight.Medium, + FontSize = 20*FontScale, + Foreground = SolidColorBrush.Parse("#212121"), + }, + new TextBlock + { + Text = "A check box control", + FontSize = 13*FontScale, + Foreground = SolidColorBrush.Parse("#727272"), + Margin = new Thickness(0, 0, 0, 10*Scale) + }, + new CheckBox { IsChecked = true, Margin = new Thickness(0, 0, 0, 5*Scale), Content = "Checked" }, + new CheckBox { IsChecked = false, Content = "Unchecked" }, + new TextBlock + { + Margin = new Thickness(0, 40*Scale, 0, 0), + Text = "RadioButton", + FontWeight = FontWeight.Medium, + FontSize = 20*FontScale, + Foreground = SolidColorBrush.Parse("#212121"), + }, + new TextBlock + { + Text = "A radio button control", + FontSize = 13*FontScale, + Foreground = SolidColorBrush.Parse("#727272"), + Margin = new Thickness(0, 0, 0, 10*Scale) + }, + new RadioButton { IsChecked = true, Content = "Option 1" }, + new RadioButton { IsChecked = false, Content = "Option 2" }, + new RadioButton { IsChecked = false, Content = "Option 3" }, + } + } + } + }; + } + + private static TabItem ListsTab() + { + return new TabItem + { + Header = "Lists", + Content = new ScrollViewer() + { + CanScrollHorizontally = false, + Content = new StackPanel + { + HorizontalAlignment = HorizontalAlignment.Left, + Orientation = Orientation.Vertical, + VerticalAlignment = VerticalAlignment.Top, + Gap = 4 * Scale, + Margin = new Thickness(10), + DataTemplates = new DataTemplates + { + new FuncDataTemplate(x => + new StackPanel + { + Gap = 4*Scale, + Orientation = Orientation.Horizontal, + Children = new Controls + { + new Image { Width = 50*Scale, Height = 50*Scale, Source = new Bitmap(OpenGitHubIconSteam()) }, + new TextBlock { Text = x.Name, FontSize = 18*FontScale } + } + }) + }, + Children = new Controls + { + new TextBlock + { + Text = "ListBox", + FontWeight = FontWeight.Medium, + FontSize = 20*FontScale, + Foreground = SolidColorBrush.Parse("#212121"), + }, + new TextBlock + { + Text = "A list box control.", + FontSize = 13*FontScale, + Foreground = SolidColorBrush.Parse("#727272"), + Margin = new Thickness(0, 0, 0, 10*Scale) + }, + new ListBox + { + BorderThickness = 2*Scale, + Items = s_listBoxData, + Height = 300*Scale, + Width = 300*Scale, + }, + new TextBlock + { + Margin = new Thickness(0, 40*Scale, 0, 0), + Text = "TreeView", + FontWeight = FontWeight.Medium, + FontSize = 20*FontScale, + Foreground = SolidColorBrush.Parse("#212121"), + }, + new TextBlock + { + Text = "A tree view control.", + FontSize = 13*FontScale, + Foreground = SolidColorBrush.Parse("#727272"), + Margin = new Thickness(0, 0, 0, 10*Scale) + }, + new TreeView + { + Name = "treeView", + Items = s_treeData, + Height = 300*Scale, + BorderThickness = 2*Scale, + Width = 300*Scale, + } + } + }, + } + }; + } + + private static TabItem ImagesTab() + { + var imageDeck = new Carousel + { + Width = 400 * Scale, + Height = 400 * Scale, + Transition = new PageSlide(TimeSpan.FromSeconds(0.25)), + Items = new[] + { + new Image { Source = new Bitmap(OpenGitHubIconSteam()), Width = 400*Scale, Height = 400*Scale }, + new Image { Source = new Bitmap(OpenPatternSteam()), Width = 400*Scale, Height = 400*Scale }, + } + }; + + // imageDeck.AutoSelect = true; + + var next = new Button + { + VerticalAlignment = VerticalAlignment.Center, + Padding = new Thickness(20 * Scale), + Content = new Perspex.Controls.Shapes.Path + { + Data = StreamGeometry.Parse("M4,11V13H16L10.5,18.5L11.92,19.92L19.84,12L11.92,4.08L10.5,5.5L16,11H4Z"), + Fill = Brushes.Black + } + }; + + var prev = new Button + { + VerticalAlignment = VerticalAlignment.Center, + Padding = new Thickness(20 * Scale), + Content = new Perspex.Controls.Shapes.Path + { + Data = StreamGeometry.Parse("M20,11V13H8L13.5,18.5L12.08,19.92L4.16,12L12.08,4.08L13.5,5.5L8,11H20Z"), + Fill = Brushes.Black + } + }; + + prev.Click += (s, e) => + { + if (imageDeck.SelectedIndex == 0) + imageDeck.SelectedIndex = 1; + else + imageDeck.SelectedIndex--; + }; + + next.Click += (s, e) => + { + if (imageDeck.SelectedIndex == 1) + imageDeck.SelectedIndex = 0; + else + imageDeck.SelectedIndex++; + }; + + return new TabItem + { + Header = "Images", + Content = new ScrollViewer + { + Content = new StackPanel + { + HorizontalAlignment = HorizontalAlignment.Left, + Orientation = Orientation.Vertical, + VerticalAlignment = VerticalAlignment.Top, + Gap = 4 * Scale, + Margin = new Thickness(10 * Scale), + Children = new Controls + { + new TextBlock + { + Text = "Deck", + FontWeight = FontWeight.Medium, + FontSize = 20*FontScale, + Foreground = SolidColorBrush.Parse("#212121"), + }, + new TextBlock + { + Text = "An items control that displays its items as pages that fill the controls.", + FontSize = 13*FontScale, + Foreground = SolidColorBrush.Parse("#727272"), + Margin = new Thickness(0, 0, 0, 10*Scale) + }, + new StackPanel + { + Name = "deckVisual", + Orientation = Orientation.Horizontal, + Gap = 4*Scale, + Children = new Controls + { + prev, + imageDeck, + next + } + } + } + } + } + }; + } + + private static TabItem LayoutTab() + { + return new TabItem + { + Header = "Layout", + Content = new ScrollViewer + { + Content = new StackPanel + { + HorizontalAlignment = HorizontalAlignment.Left, + Orientation = Orientation.Vertical, + VerticalAlignment = VerticalAlignment.Top, + Gap = 4 * Scale, + Margin = new Thickness(10 * Scale), + Children = new Controls + { + new TextBlock + { + Text = "Grid", + FontWeight = FontWeight.Medium, + FontSize = 20*FontScale, + Foreground = SolidColorBrush.Parse("#212121"), + }, + new TextBlock + { + Text = "Lays out child controls according to a grid.", + FontSize = 13*FontScale, + Foreground = SolidColorBrush.Parse("#727272"), + Margin = new Thickness(0, 0, 0, 10*Scale) + }, + new Grid + { + Width = 600*Scale, + ColumnDefinitions = new ColumnDefinitions + { + new ColumnDefinition(1, GridUnitType.Star), + new ColumnDefinition(1, GridUnitType.Star), + }, + + RowDefinitions = new RowDefinitions + { + new RowDefinition(1, GridUnitType.Auto), + new RowDefinition(1, GridUnitType.Auto) + }, + Children = new Controls + { + new Rectangle + { + Fill = SolidColorBrush.Parse("#FF5722"), + [Grid.ColumnSpanProperty] = 2, + Height = 200*Scale, + Margin = new Thickness(2.5*Scale) + }, + new Rectangle + { + Fill = SolidColorBrush.Parse("#FF5722"), + [Grid.RowProperty] = 1, + Height = 100*Scale, + Margin = new Thickness(2.5*Scale) + }, + new Rectangle + { + Fill = SolidColorBrush.Parse("#FF5722"), + [Grid.RowProperty] = 1, + [Grid.ColumnProperty] = 1, + Height = 100*Scale, + Margin = new Thickness(2.5*Scale) + }, + }, + }, + new TextBlock + { + Margin = new Thickness(0, 40*Scale, 0, 0), + Text = "StackPanel", + FontWeight = FontWeight.Medium, + FontSize = 20*Scale, + Foreground = SolidColorBrush.Parse("#212121"), + }, + new TextBlock + { + Text = "A panel which lays out its children horizontally or vertically.", + FontSize = 13*FontScale, + Foreground = SolidColorBrush.Parse("#727272"), + Margin = new Thickness(0, 0, 0, 10*Scale) + }, + new StackPanel + { + Orientation = Orientation.Vertical, + Gap = 4*Scale, + Width = 300*Scale, + Children = new Controls + { + new Rectangle + { + Fill = SolidColorBrush.Parse("#FFC107"), + Height = 50*Scale, + }, + new Rectangle + { + Fill = SolidColorBrush.Parse("#FFC107"), + Height = 50*Scale, + }, + new Rectangle + { + Fill = SolidColorBrush.Parse("#FFC107"), + Height = 50*Scale, + }, + } + }, + new TextBlock + { + Margin = new Thickness(0, 40*Scale, 0, 0), + Text = "Canvas", + FontWeight = FontWeight.Medium, + FontSize = 20*FontScale, + Foreground = SolidColorBrush.Parse("#212121"), + }, + new TextBlock + { + Text = "A panel which lays out its children by explicit coordinates.", + FontSize = 13*FontScale, + Foreground = SolidColorBrush.Parse("#727272"), + Margin = new Thickness(0, 0, 0, 10*Scale) + }, + new Canvas + { + Background = Brushes.Yellow, + Width = 300*Scale, + Height = 400*Scale, + Children = new Controls + { + new Rectangle + { + Fill = Brushes.Blue, + Width = 63*Scale, + Height = 41*Scale, + [Canvas.LeftProperty] = 40*Scale, + [Canvas.TopProperty] = 31*Scale, + }, + new Ellipse + { + Fill = Brushes.Green, + Width = 58*Scale, + Height = 58*Scale, + [Canvas.LeftProperty] = 130*Scale, + [Canvas.TopProperty] = 79*Scale, + }, + } + }, + } + } + } + }; + } + + private static TabItem AnimationsTab() + { + Border border1; + Border border2; + RotateTransform rotate; + Button button1; + + var result = new TabItem + { + Header = "Animations", + Content = new StackPanel + { + Orientation = Orientation.Vertical, + Gap = 4 * Scale, + Margin = new Thickness(10 * Scale), + Children = new Controls + { + new TextBlock + { + Text = "Animations", + FontWeight = FontWeight.Medium, + FontSize = 20*FontScale, + Foreground = SolidColorBrush.Parse("#212121"), + }, + new TextBlock + { + Text = "A few animations showcased below", + FontSize = 13*FontScale, + Foreground = SolidColorBrush.Parse("#727272"), + Margin = new Thickness(0, 0, 0, 10*Scale) + }, + (button1 = new Button + { + Content = "Animate", + Width = 120*Scale, + [Grid.ColumnProperty] = 1, + [Grid.RowProperty] = 1, + }), + new Canvas + { + ClipToBounds = false, + Children = new Controls + { + (border1 = new Border + { + Width = 100*Scale, + Height = 100*Scale, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + Background = Brushes.Crimson, + RenderTransform = new RotateTransform(), + Child = new TextBox + { + Background = Brushes.White, + Text = "Hello!", + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + }, + [Canvas.LeftProperty] = 100*Scale, + [Canvas.TopProperty] = 100*Scale, + }), + (border2 = new Border + { + Width = 100*Scale, + Height = 100*Scale, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + Background = Brushes.Coral, + Child = new Image + { + Source = new Bitmap(OpenGitHubIconSteam()), + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + }, + RenderTransform = (rotate = new RotateTransform + { + PropertyTransitions = new PropertyTransitions + { + RotateTransform.AngleProperty.Transition(500), + } + }), + PropertyTransitions = new PropertyTransitions + { + Layoutable.WidthProperty.Transition(300), + Layoutable.HeightProperty.Transition(1000), + }, + [Canvas.LeftProperty] = 400*Scale, + [Canvas.TopProperty] = 100*Scale, + }), + } + } + }, + }, + }; + //bool started = false; + button1.Click += (s, e) => + { + if (border2.Width == 100 * Scale) + { + border2.Width = border2.Height = 400 * Scale; + rotate.Angle = 180; + } + else + { + border2.Width = border2.Height = 100 * Scale; + rotate.Angle = 0; + } + + //IDisposable db = null; + //IObservable degrees = null; + //if (started == false) + //{ + // //Animate.Stopwatch.Restart(); + // var start = Animate.Stopwatch.Elapsed; + // if (degrees == null) + // { + // degrees = Animate.Timer + // .Select(x => + // { + // var elapsed = (x - start).TotalSeconds; + // var cycles = elapsed / 4; + // var progress = cycles % 1; + // return 360.0 * progress; + // }); + // } + // db = border1.RenderTransform.Bind( + // RotateTransform.AngleProperty, + // degrees, + // BindingPriority.Animation); + // started = true; + //} + //else + //{ + // if (db != null) + // { + // db.Dispose(); + // db = null; + // } + + // border1.RenderTransform.ClearValue(RotateTransform.AngleProperty); + // border1.RenderTransform = new RotateTransform() { Angle = 0.0 }; + // started = false; + //} + }; + + //#if !__ANDROID__ + var start = Animate.Stopwatch.Elapsed; + var degrees = Animate.Timer + .Select(x => + { + var elapsed = (x - start).TotalSeconds; + var cycles = elapsed / 4; + var progress = cycles % 1; + return 360.0 * progress; + }); + border1.RenderTransform.Bind( + RotateTransform.AngleProperty, + degrees, + BindingPriority.Animation); + //#endif + return result; + } + + private static Window CreateComplexTabUI() + { + // The version of ReactiveUI currently included is for WPF and so expects a WPF + // dispatcher. This makes sure it's initialized. +#if !__ANDROID__ + System.Windows.Threading.Dispatcher foo = System.Windows.Threading.Dispatcher.CurrentDispatcher; +#endif + + if (Application.Current.DataTemplates == null) + Application.Current.DataTemplates = new DataTemplates(); + Application.Current. + DataTemplates.Add( + new FuncTreeDataTemplate( + x => new TextBlock { Text = x.Name }, + x => x.Children, + x => true) + ); + + TabControl container; + + Window window = new Window + { + Title = "Perspex Test Application", + //Width = 900, + //Height = 480, + Content = + new Grid + { + Margin = DefaultMargin, + ColumnDefinitions = new ColumnDefinitions + { + new ColumnDefinition(1, GridUnitType.Star), + new ColumnDefinition(1, GridUnitType.Star), + }, + RowDefinitions = new RowDefinitions + { + new RowDefinition(GridLength.Auto), + new RowDefinition(1, GridUnitType.Star), + new RowDefinition(GridLength.Auto), + }, + Children = new Controls + { + (container = new TabControl + { + Padding = new Thickness(5*Scale), + Items = new[] + { + ButtonsTab(), + TextTab(), + HtmlTab(), + ImagesTab(), + ListsTab(), + LayoutTab(), + AnimationsTab(), + }, + Transition = new CrossFade(TimeSpan.FromSeconds(0.25)), + [Grid.RowProperty] = 1, + [Grid.ColumnSpanProperty] = 2, + }) + } + }, + }; + + // window = TestUI.TestUIBuilder.BuildTestUI(); + Application.Current.Styles.Add(new SampleTabStyle()); + container.Classes.Add(":container"); + + window.FontSize = window.FontSize * FontScale; + return window; + } + + public static Window BuildSimpleTestUI() + { + var window = new Window + { + Title = "Perspex Test Application", + Background = Brushes.Green, + Content = + new Grid + { + //Width = 600 * Scale, + //ColumnDefinitions = new ColumnDefinitions + // { + // new ColumnDefinition(1, GridUnitType.Star), + // new ColumnDefinition(1, GridUnitType.Star), + // }, + + //RowDefinitions = new RowDefinitions + // { + // new RowDefinition(1, GridUnitType.Auto), + // new RowDefinition(1, GridUnitType.Auto) + // }, + Children = new Controls + { + //new Rectangle + //{ + // Fill = SolidColorBrush.Parse("#FF5722"), + // [Grid.ColumnSpanProperty] = 2, + // Height = 200*Scale, + // Margin = new Thickness(2.5*Scale) + //}, + //new Rectangle + //{ + // Fill = SolidColorBrush.Parse("#FF5722"), + // [Grid.RowProperty] = 1, + // Height = 100*Scale, + // Margin = new Thickness(2.5*Scale) + //}, + //new Rectangle + //{ + // Fill = SolidColorBrush.Parse("#FF5722"), + // [Grid.RowProperty] = 1, + // [Grid.ColumnProperty] = 1, + // Height = 100*Scale, + // Margin = new Thickness(2.5*Scale) + //}, + + new Path + { + Name = "checkMark", + Fill = new SolidColorBrush(0xff333333), + //Width = 20, + //Height = 20, + Stretch = Stretch.Uniform, + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Top, + Data = StreamGeometry.Parse("M 1145.607177734375,430 C1145.607177734375,430 1141.449951171875,435.0772705078125 1141.449951171875,435.0772705078125 1141.449951171875,435.0772705078125 1139.232177734375,433.0999755859375 1139.232177734375,433.0999755859375 1139.232177734375,433.0999755859375 1138,434.5538330078125 1138,434.5538330078125 1138,434.5538330078125 1141.482177734375,438 1141.482177734375,438 1141.482177734375,438 1141.96875,437.9375 1141.96875,437.9375 1141.96875,437.9375 1147,431.34619140625 1147,431.34619140625 1147,431.34619140625 1145.607177734375,430 1145.607177734375,430 z"), + //Data = StreamGeometry.Parse("M 114.5607177734375,43.0 C114.5607177734375,43.0 114.1449951171875,43.50772705078125 114.1449951171875,43.50772705078125 114.1449951171875,43.50772705078125 113.9232177734375,43.30999755859375 113.9232177734375,43.30999755859375 113.9232177734375,43.30999755859375 1138,43.45538330078125 113.8,43.45538330078125 113.8,43.45538330078125 114.1482177734375,43.8 114.1482177734375,438 114.1482177734375,43.8 114.196875,43.79375 114.196875,43.79375 114.196875,43.79375 114.7,43.134619140625 114.7,43.134619140625 114.7,43.134619140625 114.5607177734375,43.0 114.5607177734375,43.0 z"), + [Grid.ColumnProperty] = 0, + }, + + }, + } + }; + + return window; + } + + public static Window BuildTestUI() + { + Application.Current.Styles.Add(new Style(x => x.OfType()) + { + Setters = new[] + { + new Setter(TemplatedControl.TemplateProperty, new FuncControlTemplate(DropDownControlTemplate)), + //new Setter(TemplatedControl.BorderBrushProperty, new SolidColorBrush(0xff707070)), + //new Setter(TemplatedControl.BorderThicknessProperty, 2.0), + //new Setter(Control.FocusAdornerProperty, new FuncTemplate(FocusAdornerTemplate)), + //new Setter(DropDown.HorizontalContentAlignmentProperty, HorizontalAlignment.Center), + //new Setter(DropDown.VerticalContentAlignmentProperty, VerticalAlignment.Center), + }, + }); + + //Application.Current.DataTemplates.Add(new FuncDataTemplate(s => + //{ + // return new TextBlock() { Text = s, FontSize = DefaultFontSize }; + //})); + // TextPresenter + Window result; + //result = BuildSimpleTextBoxUI(); + //result = BuildSimpleTextUI(); + //result = BuildSimpleControlsUI(); + //return BuildStackPanelUI(); + //result = BuildListBoxUI(); + //result = BuildComboBoxUI(); + //result = BuildGridWithSomeButtonsAndStuff(); + result = CreateComplexTabUI(); + // result = BuildSimpleTestUI(); +#if __ANDROID__ + // DefaultFontSize = result.FontSize = result.FontSize * 2; +#endif + return result; + } +} + + internal class SampleTabStyle : Styles + { + public SampleTabStyle() + { + this.AddRange(new[] + { + new Style (s => s.Class(":container").OfType ()) + { + Setters = new[] + { + new Setter (TemplatedControl.TemplateProperty, new FuncControlTemplate (TabControlTemplate)) + } + }, + + new Style(s => s.Class(":container").OfType().Child().Child().Child().Child().Child().OfType()) + { + Setters = new[] + { + new Setter (TemplatedControl.TemplateProperty, new FuncControlTemplate (TabItemTemplate)), + } + }, + + new Style(s => s.Name("internalStrip").OfType().Child().OfType()) + { + Setters = new[] + { + new Setter(TemplatedControl.FontSizeProperty, 14.0), + new Setter(TemplatedControl.ForegroundProperty, Brushes.White) + } + }, + + new Style(s => s.Name("internalStrip").OfType().Child().OfType().Class(":selected")) + { + Setters = new[] + { + new Setter(TemplatedControl.ForegroundProperty, Brushes.White), + new Setter(TemplatedControl.BackgroundProperty, new SolidColorBrush(Colors.White) { Opacity = 0.1 }), + }, + }, + }); + } + + public static Control TabItemTemplate(TabItem control) + { + return new ContentPresenter + { + DataTemplates = new DataTemplates + { + new FuncDataTemplate(x => new Border + { + [~Border.BackgroundProperty] = control[~TemplatedControl.BackgroundProperty], + Padding = new Thickness(10), + Child = new TextBlock + { + VerticalAlignment = Perspex.Layout.VerticalAlignment.Center, + Text = x + } + }) + }, + Name = "headerPresenter", + [~ContentPresenter.ContentProperty] = control[~HeaderedContentControl.HeaderProperty], + }; + } + + public static Control TabControlTemplate(TabControl control) + { + return new Grid + { + ColumnDefinitions = new ColumnDefinitions + { + new ColumnDefinition(GridLength.Auto), + new ColumnDefinition(new GridLength(1, GridUnitType.Star)), + }, + Children = new Controls + { + new Border + { + Width = 190, + Background = SolidColorBrush.Parse("#1976D2"), + Child = new ScrollViewer + { + Content = new TabStrip + { + ItemsPanel = new FuncTemplate(() => new StackPanel { Orientation = Orientation.Vertical, Gap = 4 }), + Margin = new Thickness(0, 10, 0, 0), + Name = "internalStrip", + [!ItemsControl.ItemsProperty] = control[!ItemsControl.ItemsProperty], + [!!SelectingItemsControl.SelectedItemProperty] = control[!!SelectingItemsControl.SelectedItemProperty], + } + } + }, + new Carousel + { + Name = "carousel", + MemberSelector = control.ContentSelector, + [~Carousel.TransitionProperty] = control[~TabControl.TransitionProperty], + [!Carousel.ItemsProperty] = control[!ItemsControl.ItemsProperty], + [!Carousel.SelectedItemProperty] = control[!SelectingItemsControl.SelectedItemProperty], + [Grid.ColumnProperty] = 1, + } + } + }; + } + } +} \ No newline at end of file diff --git a/src/Android/Perspex.AndroidTestApplication/github_icon.png b/src/Android/Perspex.AndroidTestApplication/github_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..ed4f82f847472e8d6f681a05a3a6a2deb401e23f GIT binary patch literal 17223 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 + + + + + + + + + + + \ No newline at end of file diff --git a/src/Android/Perspex.AndroidTestApplication/pattern.jpg b/src/Android/Perspex.AndroidTestApplication/pattern.jpg new file mode 100644 index 0000000000000000000000000000000000000000..72b35e069248e9d8a182659f39c57927a271327d GIT binary patch literal 136642 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 literal 0 HcmV?d00001 diff --git a/src/Skia/Perspex.Skia.Android.TestApp/Perspex.Skia.Android.TestApp.csproj b/src/Skia/Perspex.Skia.Android.TestApp/Perspex.Skia.Android.TestApp.csproj index 36cb4b0e0b..614b0210bb 100644 --- a/src/Skia/Perspex.Skia.Android.TestApp/Perspex.Skia.Android.TestApp.csproj +++ b/src/Skia/Perspex.Skia.Android.TestApp/Perspex.Skia.Android.TestApp.csproj @@ -16,7 +16,7 @@ Resources\Resource.Designer.cs Off True - v5.0 + v6.0 Properties\AndroidManifest.xml diff --git a/src/Skia/Perspex.Skia.Android/Perspex.Skia.Android.csproj b/src/Skia/Perspex.Skia.Android/Perspex.Skia.Android.csproj index 2160eb2592..2916c4059e 100644 --- a/src/Skia/Perspex.Skia.Android/Perspex.Skia.Android.csproj +++ b/src/Skia/Perspex.Skia.Android/Perspex.Skia.Android.csproj @@ -15,7 +15,7 @@ Resources\Resource.Designer.cs Off True - v5.0 + v6.0 true