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 0000000000..8074c4c571 Binary files /dev/null and b/src/Android/Perspex.AndroidTestApplication/Resources/drawable/Icon.png differ 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 0000000000..ed4f82f847 Binary files /dev/null and b/src/Android/Perspex.AndroidTestApplication/github_icon.png differ diff --git a/src/Android/Perspex.AndroidTestApplication/packages.config b/src/Android/Perspex.AndroidTestApplication/packages.config new file mode 100644 index 0000000000..027adeed7e --- /dev/null +++ b/src/Android/Perspex.AndroidTestApplication/packages.config @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ 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 0000000000..72b35e0692 Binary files /dev/null and b/src/Android/Perspex.AndroidTestApplication/pattern.jpg differ 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