csharpc-sharpdotnetxamlavaloniauicross-platformcross-platform-xamlavaloniaguimulti-platformuser-interfacedotnetcore
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
564 lines
20 KiB
564 lines
20 KiB
using System;
|
|
using System.Collections.Generic;
|
|
using Android.App;
|
|
using Android.Content;
|
|
using Android.Graphics;
|
|
using Android.OS;
|
|
using Android.Runtime;
|
|
using Android.Text;
|
|
using Android.Views;
|
|
using Android.Views.InputMethods;
|
|
using Avalonia.Android.Platform.Specific;
|
|
using Avalonia.Android.Platform.Specific.Helpers;
|
|
using Avalonia.Android.Platform.Storage;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Controls.Platform;
|
|
using Avalonia.Controls.Platform.Surfaces;
|
|
using Avalonia.Input;
|
|
using Avalonia.Input.Raw;
|
|
using Avalonia.Input.TextInput;
|
|
using Avalonia.OpenGL.Egl;
|
|
using Avalonia.OpenGL.Surfaces;
|
|
using Avalonia.Platform;
|
|
using Avalonia.Platform.Storage;
|
|
using Avalonia.Rendering;
|
|
using Avalonia.Rendering.Composition;
|
|
using Java.Lang;
|
|
using Math = System.Math;
|
|
using AndroidRect = Android.Graphics.Rect;
|
|
using Window = Android.Views.Window;
|
|
using Android.Graphics.Drawables;
|
|
|
|
namespace Avalonia.Android.Platform.SkiaPlatform
|
|
{
|
|
class TopLevelImpl : IAndroidView, ITopLevelImpl, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo,
|
|
ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost, ITopLevelImplWithStorageProvider,
|
|
ITopLevelWithSystemNavigationManager
|
|
{
|
|
private readonly IGlPlatformSurface _gl;
|
|
private readonly IFramebufferPlatformSurface _framebuffer;
|
|
|
|
private readonly AndroidKeyboardEventsHelper<TopLevelImpl> _keyboardHelper;
|
|
private readonly AndroidMotionEventsHelper _pointerHelper;
|
|
private readonly AndroidInputMethod<ViewImpl> _textInputMethod;
|
|
private ViewImpl _view;
|
|
|
|
public TopLevelImpl(AvaloniaView avaloniaView, bool placeOnTop = false)
|
|
{
|
|
_view = new ViewImpl(avaloniaView.Context, this, placeOnTop);
|
|
_textInputMethod = new AndroidInputMethod<ViewImpl>(_view);
|
|
_keyboardHelper = new AndroidKeyboardEventsHelper<TopLevelImpl>(this);
|
|
_pointerHelper = new AndroidMotionEventsHelper(this);
|
|
_gl = new EglGlPlatformSurface(this);
|
|
_framebuffer = new FramebufferManager(this);
|
|
|
|
RenderScaling = _view.Scaling;
|
|
|
|
MaxClientSize = new PixelSize(_view.Resources.DisplayMetrics.WidthPixels,
|
|
_view.Resources.DisplayMetrics.HeightPixels).ToSize(RenderScaling);
|
|
|
|
NativeControlHost = new AndroidNativeControlHostImpl(avaloniaView);
|
|
StorageProvider = new AndroidStorageProvider((Activity)avaloniaView.Context);
|
|
|
|
SystemNavigationManager = new AndroidSystemNavigationManager(avaloniaView.Context as IActivityNavigationService);
|
|
}
|
|
|
|
public virtual Point GetAvaloniaPointFromEvent(MotionEvent e, int pointerIndex) =>
|
|
new Point(e.GetX(pointerIndex), e.GetY(pointerIndex)) / RenderScaling;
|
|
|
|
public IInputRoot InputRoot { get; private set; }
|
|
|
|
public virtual Size ClientSize
|
|
{
|
|
get
|
|
{
|
|
AndroidRect rect = new AndroidRect();
|
|
AndroidRect intersection = new AndroidRect();
|
|
|
|
_view.GetWindowVisibleDisplayFrame(intersection);
|
|
_view.GetGlobalVisibleRect(rect);
|
|
|
|
intersection.Intersect(rect);
|
|
|
|
return new PixelSize(intersection.Right - intersection.Left, intersection.Bottom - intersection.Top).ToSize(RenderScaling);
|
|
}
|
|
}
|
|
|
|
public Size? FrameSize => null;
|
|
|
|
public IMouseDevice MouseDevice { get; } = new MouseDevice();
|
|
|
|
public Action Closed { get; set; }
|
|
|
|
public Action<RawInputEventArgs> Input { get; set; }
|
|
|
|
public Size MaxClientSize { get; protected set; }
|
|
|
|
public Action<Rect> Paint { get; set; }
|
|
|
|
public Action<Size, PlatformResizeReason> Resized { get; set; }
|
|
|
|
public Action<double> ScalingChanged { get; set; }
|
|
|
|
public View View => _view;
|
|
|
|
internal InvalidationAwareSurfaceView InternalView => _view;
|
|
|
|
public IPlatformHandle Handle => _view;
|
|
|
|
public IEnumerable<object> Surfaces => new object[] { _gl, _framebuffer, Handle };
|
|
|
|
public IRenderer CreateRenderer(IRenderRoot root) =>
|
|
new CompositingRenderer(root, AndroidPlatform.Compositor, () => Surfaces);
|
|
|
|
public virtual void Hide()
|
|
{
|
|
_view.Visibility = ViewStates.Invisible;
|
|
}
|
|
|
|
public void Invalidate(Rect rect)
|
|
{
|
|
if (_view.Holder?.Surface?.IsValid == true) _view.Invalidate();
|
|
}
|
|
|
|
public Point PointToClient(PixelPoint point)
|
|
{
|
|
return point.ToPoint(RenderScaling);
|
|
}
|
|
|
|
public PixelPoint PointToScreen(Point point)
|
|
{
|
|
return PixelPoint.FromPoint(point, RenderScaling);
|
|
}
|
|
|
|
public void SetCursor(ICursorImpl cursor)
|
|
{
|
|
//still not implemented
|
|
}
|
|
|
|
public void SetInputRoot(IInputRoot inputRoot)
|
|
{
|
|
InputRoot = inputRoot;
|
|
}
|
|
|
|
public virtual void Show()
|
|
{
|
|
_view.Visibility = ViewStates.Visible;
|
|
}
|
|
|
|
public double RenderScaling { get; }
|
|
|
|
void Draw()
|
|
{
|
|
Paint?.Invoke(new Rect(new Point(0, 0), ClientSize));
|
|
}
|
|
|
|
public virtual void Dispose()
|
|
{
|
|
_view.Dispose();
|
|
_view = null;
|
|
}
|
|
|
|
protected virtual void OnResized(Size size)
|
|
{
|
|
Resized?.Invoke(size, PlatformResizeReason.Unspecified);
|
|
}
|
|
|
|
internal void Resize(Size size)
|
|
{
|
|
Resized?.Invoke(size, PlatformResizeReason.Layout);
|
|
}
|
|
|
|
class ViewImpl : InvalidationAwareSurfaceView, ISurfaceHolderCallback, IInitEditorInfo
|
|
{
|
|
private readonly TopLevelImpl _tl;
|
|
private Size _oldSize;
|
|
|
|
public ViewImpl(Context context, TopLevelImpl tl, bool placeOnTop) : base(context)
|
|
{
|
|
_tl = tl;
|
|
if (placeOnTop)
|
|
SetZOrderOnTop(true);
|
|
}
|
|
|
|
public TopLevelImpl TopLevelImpl => _tl;
|
|
|
|
protected override void Draw()
|
|
{
|
|
_tl.Draw();
|
|
}
|
|
|
|
protected override void DispatchDraw(global::Android.Graphics.Canvas canvas)
|
|
{
|
|
// Workaround issue #9230 on where screen remains gray after splash screen.
|
|
// base.DispatchDraw should punch a hole into the canvas so the surface
|
|
// can be seen below, but it does not.
|
|
if (OperatingSystem.IsAndroidVersionAtLeast(29))
|
|
{
|
|
// Android 10+ does this (BlendMode was new)
|
|
var paint = new Paint();
|
|
paint.SetColor(0);
|
|
paint.BlendMode = BlendMode.Clear;
|
|
canvas.DrawRect(0, 0, Width, Height, paint);
|
|
}
|
|
else
|
|
{
|
|
// Android 9 did this
|
|
canvas.DrawColor(Color.Transparent, PorterDuff.Mode.Clear);
|
|
}
|
|
|
|
base.DispatchDraw(canvas);
|
|
}
|
|
|
|
protected override bool DispatchGenericPointerEvent(MotionEvent e)
|
|
{
|
|
bool callBase;
|
|
bool? result = _tl._pointerHelper.DispatchMotionEvent(e, out callBase);
|
|
bool baseResult = callBase ? base.DispatchGenericPointerEvent(e) : false;
|
|
|
|
return result != null ? result.Value : baseResult;
|
|
}
|
|
|
|
public override bool DispatchTouchEvent(MotionEvent e)
|
|
{
|
|
bool callBase;
|
|
bool? result = _tl._pointerHelper.DispatchMotionEvent(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 = _tl._keyboardHelper.DispatchKeyEvent(e, out callBase);
|
|
bool baseResult = callBase ? base.DispatchKeyEvent(e) : false;
|
|
|
|
return res != null ? res.Value : baseResult;
|
|
}
|
|
|
|
void ISurfaceHolderCallback.SurfaceChanged(ISurfaceHolder holder, Format format, int width, int height)
|
|
{
|
|
var newSize = new PixelSize(width, height).ToSize(_tl.RenderScaling);
|
|
|
|
if (newSize != _oldSize)
|
|
{
|
|
_oldSize = newSize;
|
|
_tl.OnResized(newSize);
|
|
}
|
|
|
|
base.SurfaceChanged(holder, format, width, height);
|
|
}
|
|
|
|
public sealed override bool OnCheckIsTextEditor()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
private Func<TopLevelImpl, EditorInfo, IInputConnection> _initEditorInfo;
|
|
|
|
public void InitEditorInfo(Func<TopLevelImpl, EditorInfo, IInputConnection> init)
|
|
{
|
|
_initEditorInfo = init;
|
|
}
|
|
|
|
public sealed override IInputConnection OnCreateInputConnection(EditorInfo outAttrs)
|
|
{
|
|
if (_initEditorInfo != null)
|
|
{
|
|
return _initEditorInfo(_tl, outAttrs);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
}
|
|
|
|
public IPopupImpl CreatePopup() => null;
|
|
|
|
public Action LostFocus { get; set; }
|
|
public Action<WindowTransparencyLevel> TransparencyLevelChanged { get; set; }
|
|
|
|
public WindowTransparencyLevel TransparencyLevel { get; private set; }
|
|
|
|
public void SetFrameThemeVariant(PlatformThemeVariant themeVariant)
|
|
{
|
|
// TODO adjust status bar depending on full screen mode.
|
|
}
|
|
|
|
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels => new AcrylicPlatformCompensationLevels(1, 1, 1);
|
|
|
|
IntPtr EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo.Handle => ((IPlatformHandle)_view).Handle;
|
|
|
|
public PixelSize Size => _view.Size;
|
|
|
|
public double Scaling => RenderScaling;
|
|
|
|
public ITextInputMethodImpl TextInputMethod => _textInputMethod;
|
|
|
|
public INativeControlHostImpl NativeControlHost { get; }
|
|
|
|
public IStorageProvider StorageProvider { get; }
|
|
|
|
public ISystemNavigationManager SystemNavigationManager { get; }
|
|
|
|
public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel)
|
|
{
|
|
if (TransparencyLevel != transparencyLevel)
|
|
{
|
|
bool isBelowR = Build.VERSION.SdkInt < BuildVersionCodes.R;
|
|
bool isAboveR = Build.VERSION.SdkInt > BuildVersionCodes.R;
|
|
if (_view.Context is AvaloniaMainActivity activity)
|
|
{
|
|
switch (transparencyLevel)
|
|
{
|
|
case WindowTransparencyLevel.AcrylicBlur:
|
|
case WindowTransparencyLevel.ForceAcrylicBlur:
|
|
case WindowTransparencyLevel.Mica:
|
|
case WindowTransparencyLevel.None:
|
|
if (!isBelowR)
|
|
{
|
|
activity.SetTranslucent(false);
|
|
}
|
|
if (isAboveR)
|
|
{
|
|
activity.Window?.ClearFlags(WindowManagerFlags.BlurBehind);
|
|
|
|
var attr = activity.Window?.Attributes;
|
|
if (attr != null)
|
|
{
|
|
attr.BlurBehindRadius = 0;
|
|
|
|
activity.Window.Attributes = attr;
|
|
}
|
|
}
|
|
activity.Window.SetBackgroundDrawable(new ColorDrawable(Color.White));
|
|
|
|
if(transparencyLevel != WindowTransparencyLevel.None)
|
|
{
|
|
return;
|
|
}
|
|
break;
|
|
case WindowTransparencyLevel.Transparent:
|
|
if (!isBelowR)
|
|
{
|
|
activity.SetTranslucent(true);
|
|
}
|
|
if (isAboveR)
|
|
{
|
|
activity.Window?.ClearFlags(WindowManagerFlags.BlurBehind);
|
|
|
|
var attr = activity.Window?.Attributes;
|
|
if (attr != null)
|
|
{
|
|
attr.BlurBehindRadius = 0;
|
|
|
|
activity.Window.Attributes = attr;
|
|
}
|
|
}
|
|
activity.Window.SetBackgroundDrawable(new ColorDrawable(Color.Transparent));
|
|
break;
|
|
case WindowTransparencyLevel.Blur:
|
|
if (isAboveR)
|
|
{
|
|
activity.SetTranslucent(true);
|
|
activity.Window?.AddFlags(WindowManagerFlags.BlurBehind);
|
|
|
|
var attr = activity.Window?.Attributes;
|
|
if (attr != null)
|
|
{
|
|
attr.BlurBehindRadius = 120;
|
|
|
|
activity.Window.Attributes = attr;
|
|
}
|
|
activity.Window.SetBackgroundDrawable(new ColorDrawable(Color.Transparent));
|
|
}
|
|
else
|
|
{
|
|
activity.Window?.ClearFlags(WindowManagerFlags.BlurBehind);
|
|
activity.Window.SetBackgroundDrawable(new ColorDrawable(Color.White));
|
|
|
|
return;
|
|
}
|
|
break;
|
|
}
|
|
TransparencyLevel = transparencyLevel;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
internal class AvaloniaInputConnection : BaseInputConnection
|
|
{
|
|
private readonly TopLevelImpl _topLevel;
|
|
private readonly IAndroidInputMethod _inputMethod;
|
|
|
|
public AvaloniaInputConnection(TopLevelImpl topLevel, IAndroidInputMethod inputMethod) : base(inputMethod.View, true)
|
|
{
|
|
_topLevel = topLevel;
|
|
_inputMethod = inputMethod;
|
|
}
|
|
|
|
public TextInputMethodSurroundingText SurroundingText { get; set; }
|
|
|
|
public string ComposingText { get; internal set; }
|
|
|
|
public ComposingRegion? ComposingRegion { get; internal set; }
|
|
|
|
public bool IsComposing => !string.IsNullOrEmpty(ComposingText);
|
|
public bool IsCommiting { get; private set; }
|
|
|
|
public override bool SetComposingRegion(int start, int end)
|
|
{
|
|
//System.Diagnostics.Debug.WriteLine($"Composing Region: [{start}|{end}] {SurroundingText.Text?.Substring(start, end - start)}");
|
|
|
|
ComposingRegion = new ComposingRegion(start, end);
|
|
|
|
return base.SetComposingRegion(start, end);
|
|
}
|
|
|
|
public override bool SetComposingText(ICharSequence text, int newCursorPosition)
|
|
{
|
|
var composingText = text.ToString();
|
|
|
|
ComposingText = composingText;
|
|
|
|
_inputMethod.Client?.SetPreeditText(ComposingText);
|
|
|
|
return base.SetComposingText(text, newCursorPosition);
|
|
}
|
|
|
|
public override bool FinishComposingText()
|
|
{
|
|
if (!string.IsNullOrEmpty(ComposingText))
|
|
{
|
|
CommitText(ComposingText, ComposingText.Length);
|
|
}
|
|
else
|
|
{
|
|
ComposingRegion = new ComposingRegion(SurroundingText.CursorOffset, SurroundingText.CursorOffset);
|
|
}
|
|
|
|
return base.FinishComposingText();
|
|
}
|
|
|
|
public override ICharSequence GetTextBeforeCursorFormatted(int length, [GeneratedEnum] GetTextFlags flags)
|
|
{
|
|
if (!string.IsNullOrEmpty(SurroundingText.Text) && length > 0)
|
|
{
|
|
var start = System.Math.Max(SurroundingText.CursorOffset - length, 0);
|
|
|
|
var end = System.Math.Min(start + length - 1, SurroundingText.CursorOffset);
|
|
|
|
var text = SurroundingText.Text.Substring(start, end - start);
|
|
|
|
//System.Diagnostics.Debug.WriteLine($"Text Before: {text}");
|
|
|
|
return new Java.Lang.String(text);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public override ICharSequence GetTextAfterCursorFormatted(int length, [GeneratedEnum] GetTextFlags flags)
|
|
{
|
|
if (!string.IsNullOrEmpty(SurroundingText.Text))
|
|
{
|
|
var start = SurroundingText.CursorOffset;
|
|
|
|
var end = System.Math.Min(start + length, SurroundingText.Text.Length);
|
|
|
|
var text = SurroundingText.Text.Substring(start, end - start);
|
|
|
|
//System.Diagnostics.Debug.WriteLine($"Text After: {text}");
|
|
|
|
return new Java.Lang.String(text);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public override bool CommitText(ICharSequence text, int newCursorPosition)
|
|
{
|
|
IsCommiting = true;
|
|
var committedText = text.ToString();
|
|
|
|
_inputMethod.Client.SetPreeditText(null);
|
|
|
|
int? start, end;
|
|
|
|
if(SurroundingText.CursorOffset != SurroundingText.AnchorOffset)
|
|
{
|
|
start = Math.Min(SurroundingText.CursorOffset, SurroundingText.AnchorOffset);
|
|
end = Math.Max(SurroundingText.CursorOffset, SurroundingText.AnchorOffset);
|
|
}
|
|
else if (ComposingRegion != null)
|
|
{
|
|
start = ComposingRegion?.Start;
|
|
end = ComposingRegion?.End;
|
|
|
|
ComposingRegion = null;
|
|
}
|
|
else
|
|
{
|
|
start = end = _inputMethod.Client.SurroundingText.CursorOffset;
|
|
}
|
|
|
|
_inputMethod.Client.SelectInSurroundingText((int)start, (int)end);
|
|
|
|
var time = DateTime.Now.TimeOfDay;
|
|
|
|
var rawTextEvent = new RawTextInputEventArgs(KeyboardDevice.Instance, (ulong)time.Ticks, _topLevel.InputRoot, committedText);
|
|
|
|
_topLevel.Input(rawTextEvent);
|
|
|
|
ComposingText = null;
|
|
|
|
ComposingRegion = new ComposingRegion(newCursorPosition, newCursorPosition);
|
|
|
|
return base.CommitText(text, newCursorPosition);
|
|
}
|
|
|
|
public override bool DeleteSurroundingText(int beforeLength, int afterLength)
|
|
{
|
|
var surroundingText = _inputMethod.Client.SurroundingText;
|
|
|
|
var selectionStart = surroundingText.CursorOffset;
|
|
|
|
_inputMethod.Client.SelectInSurroundingText(selectionStart - beforeLength, selectionStart + afterLength);
|
|
|
|
_inputMethod.View.DispatchKeyEvent(new KeyEvent(KeyEventActions.Down, Keycode.ForwardDel));
|
|
|
|
surroundingText = _inputMethod.Client.SurroundingText;
|
|
|
|
selectionStart = surroundingText.CursorOffset;
|
|
|
|
ComposingRegion = new ComposingRegion(selectionStart, selectionStart);
|
|
|
|
return base.DeleteSurroundingText(beforeLength, afterLength);
|
|
}
|
|
|
|
public override bool SetSelection(int start, int end)
|
|
{
|
|
_inputMethod.Client.SelectInSurroundingText(start, end);
|
|
|
|
ComposingRegion = new ComposingRegion(start, end);
|
|
|
|
return base.SetSelection(start, end);
|
|
}
|
|
|
|
public override bool PerformEditorAction([GeneratedEnum] ImeAction actionCode)
|
|
{
|
|
switch (actionCode)
|
|
{
|
|
case ImeAction.Done:
|
|
{
|
|
_inputMethod.IMM.HideSoftInputFromWindow(_inputMethod.View.WindowToken, HideSoftInputFlags.ImplicitOnly);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return base.PerformEditorAction(actionCode);
|
|
}
|
|
}
|
|
}
|
|
|