mirror of
https://github.com/bitwarden/android.git
synced 2024-12-27 11:28:28 +03:00
PM-3349 Fix Picker selection style by doing a custom PickerHandler for Android which uses SetSingleChoiceItems(...) to provide with the appropriate UI
This commit is contained in:
parent
a1e4f0aaa2
commit
6c04ac67b1
4 changed files with 367 additions and 0 deletions
299
src/Core/Controls/Picker/PickerHandler.Android.cs
Normal file
299
src/Core/Controls/Picker/PickerHandler.Android.cs
Normal file
|
@ -0,0 +1,299 @@
|
||||||
|
#if ANDROID
|
||||||
|
using System;
|
||||||
|
using System.Collections.Specialized;
|
||||||
|
using Android.App;
|
||||||
|
using Android.Content.PM;
|
||||||
|
using Android.Content.Res;
|
||||||
|
using Android.Graphics.Drawables;
|
||||||
|
using Android.Text;
|
||||||
|
using Android.Text.Style;
|
||||||
|
using Android.Widget;
|
||||||
|
using Microsoft.Maui;
|
||||||
|
using Microsoft.Maui.Handlers;
|
||||||
|
using Microsoft.Maui.Platform;
|
||||||
|
using AGravityFlags = Android.Views.GravityFlags;
|
||||||
|
using ALayoutDirection = Android.Views.LayoutDirection;
|
||||||
|
using AppCompatAlertDialog = AndroidX.AppCompat.App.AlertDialog;
|
||||||
|
using AResource = Android.Resource;
|
||||||
|
using ATextAlignment = Android.Views.TextAlignment;
|
||||||
|
using ATextDirection = Android.Views.TextDirection;
|
||||||
|
|
||||||
|
namespace Bit.Core.Controls.Picker
|
||||||
|
{
|
||||||
|
// HACK: Due to https://github.com/dotnet/maui/issues/19681 and not willing to use reflection to access
|
||||||
|
// the alert dialog, we need to redefine the PickerHandler implementation for a custom one of ours
|
||||||
|
// which handles showing the current selected item. Remove this workaround when MAUI releases a fix for this.
|
||||||
|
// This is an adapted copy from https://github.com/dotnet/maui/blob/main/src/Core/src/Handlers/Picker/PickerHandler.Android.cs
|
||||||
|
public partial class PickerHandler : ViewHandler<IPicker, MauiPicker>
|
||||||
|
{
|
||||||
|
AppCompatAlertDialog? _dialog;
|
||||||
|
|
||||||
|
protected override MauiPicker CreatePlatformView() =>
|
||||||
|
new MauiPicker(Context);
|
||||||
|
|
||||||
|
protected override void ConnectHandler(MauiPicker platformView)
|
||||||
|
{
|
||||||
|
platformView.FocusChange += OnFocusChange;
|
||||||
|
platformView.Click += OnClick;
|
||||||
|
|
||||||
|
base.ConnectHandler(platformView);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void DisconnectHandler(MauiPicker platformView)
|
||||||
|
{
|
||||||
|
platformView.FocusChange -= OnFocusChange;
|
||||||
|
platformView.Click -= OnClick;
|
||||||
|
|
||||||
|
base.DisconnectHandler(platformView);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a Android-specific mapping
|
||||||
|
public static void MapBackground(IPickerHandler handler, IPicker picker)
|
||||||
|
{
|
||||||
|
handler.PlatformView?.UpdateBackground(picker);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO Uncomment me on NET8 [Obsolete]
|
||||||
|
public static void MapReload(IPickerHandler handler, IPicker picker, object? args) => Reload(handler);
|
||||||
|
|
||||||
|
internal static void MapItems(IPickerHandler handler, IPicker picker) => Reload(handler);
|
||||||
|
|
||||||
|
public static void MapTitle(IPickerHandler handler, IPicker picker)
|
||||||
|
{
|
||||||
|
handler.PlatformView?.UpdateTitle(picker);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void MapTitleColor(IPickerHandler handler, IPicker picker)
|
||||||
|
{
|
||||||
|
handler.PlatformView?.UpdateTitleColor(picker);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void MapSelectedIndex(IPickerHandler handler, IPicker picker)
|
||||||
|
{
|
||||||
|
handler.PlatformView?.UpdateSelectedIndex(picker);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void MapCharacterSpacing(IPickerHandler handler, IPicker picker)
|
||||||
|
{
|
||||||
|
handler.PlatformView?.UpdateCharacterSpacing(picker);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void MapFont(IPickerHandler handler, IPicker picker)
|
||||||
|
{
|
||||||
|
var fontManager = handler.GetRequiredService<IFontManager>();
|
||||||
|
|
||||||
|
handler.PlatformView?.UpdateFont(picker, fontManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void MapHorizontalTextAlignment(IPickerHandler handler, IPicker picker)
|
||||||
|
{
|
||||||
|
handler.PlatformView?.UpdateHorizontalAlignment(picker.HorizontalTextAlignment);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void MapTextColor(IPickerHandler handler, IPicker picker)
|
||||||
|
{
|
||||||
|
handler.PlatformView.UpdateTextColor(picker);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void MapVerticalTextAlignment(IPickerHandler handler, IPicker picker)
|
||||||
|
{
|
||||||
|
handler.PlatformView?.UpdateVerticalAlignment(picker.VerticalTextAlignment);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnFocusChange(object? sender, global::Android.Views.View.FocusChangeEventArgs e)
|
||||||
|
{
|
||||||
|
if (PlatformView == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (e.HasFocus)
|
||||||
|
{
|
||||||
|
if (PlatformView.Clickable)
|
||||||
|
PlatformView.CallOnClick();
|
||||||
|
else
|
||||||
|
OnClick(PlatformView, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
else if (_dialog != null)
|
||||||
|
{
|
||||||
|
_dialog.Hide();
|
||||||
|
_dialog = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnClick(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (_dialog == null && VirtualView != null)
|
||||||
|
{
|
||||||
|
using (var builder = new AppCompatAlertDialog.Builder(Context))
|
||||||
|
{
|
||||||
|
if (VirtualView.TitleColor == null)
|
||||||
|
{
|
||||||
|
builder.SetTitle(VirtualView.Title ?? string.Empty);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var title = new SpannableString(VirtualView.Title ?? string.Empty);
|
||||||
|
#pragma warning disable CA1416 // https://github.com/xamarin/xamarin-android/issues/6962
|
||||||
|
title.SetSpan(new ForegroundColorSpan(VirtualView.TitleColor.ToPlatform()), 0, title.Length(), SpanTypes.ExclusiveExclusive);
|
||||||
|
#pragma warning restore CA1416
|
||||||
|
builder.SetTitle(title);
|
||||||
|
}
|
||||||
|
|
||||||
|
string[] items = VirtualView.GetItemsAsArray();
|
||||||
|
|
||||||
|
for (var i = 0; i < items.Length; i++)
|
||||||
|
{
|
||||||
|
var item = items[i];
|
||||||
|
if (item == null)
|
||||||
|
items[i] = String.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.SetSingleChoiceItems(items, VirtualView.SelectedIndex, (s, e) =>
|
||||||
|
{
|
||||||
|
var selectedIndex = e.Which;
|
||||||
|
VirtualView.SelectedIndex = selectedIndex;
|
||||||
|
base.PlatformView?.UpdatePicker(VirtualView);
|
||||||
|
_dialog.Dismiss();
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.SetNegativeButton(AResource.String.Cancel, (o, args) => { });
|
||||||
|
|
||||||
|
_dialog = builder.Create();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_dialog == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_dialog.UpdateFlowDirection(PlatformView);
|
||||||
|
|
||||||
|
_dialog.SetCanceledOnTouchOutside(true);
|
||||||
|
|
||||||
|
_dialog.DismissEvent += (sender, args) =>
|
||||||
|
{
|
||||||
|
_dialog = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
_dialog.Show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Reload(IPickerHandler handler)
|
||||||
|
{
|
||||||
|
handler.PlatformView.UpdatePicker(handler.VirtualView);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class PickerExtensions
|
||||||
|
{
|
||||||
|
const AGravityFlags HorizontalGravityMask = AGravityFlags.CenterHorizontal | AGravityFlags.End | AGravityFlags.Start;
|
||||||
|
|
||||||
|
internal static void UpdatePicker(this MauiPicker platformPicker, IPicker picker)
|
||||||
|
{
|
||||||
|
platformPicker.Hint = picker.Title;
|
||||||
|
|
||||||
|
if (picker.SelectedIndex == -1 || picker.SelectedIndex >= picker.GetCount())
|
||||||
|
platformPicker.Text = null;
|
||||||
|
else
|
||||||
|
platformPicker.Text = picker.GetItem(picker.SelectedIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void UpdateHorizontalAlignment(this EditText view, TextAlignment alignment, AGravityFlags orMask = AGravityFlags.NoGravity)
|
||||||
|
{
|
||||||
|
if (!Rtl.IsSupported)
|
||||||
|
{
|
||||||
|
view.Gravity = (view.Gravity & ~HorizontalGravityMask) | alignment.ToHorizontalGravityFlags() | orMask;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
view.TextAlignment = alignment.ToTextAlignment();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static AGravityFlags ToHorizontalGravityFlags(this TextAlignment alignment)
|
||||||
|
{
|
||||||
|
switch (alignment)
|
||||||
|
{
|
||||||
|
case TextAlignment.Center:
|
||||||
|
return AGravityFlags.CenterHorizontal;
|
||||||
|
case TextAlignment.End:
|
||||||
|
return AGravityFlags.End;
|
||||||
|
default:
|
||||||
|
return AGravityFlags.Start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static ATextAlignment ToTextAlignment(this TextAlignment alignment)
|
||||||
|
{
|
||||||
|
switch (alignment)
|
||||||
|
{
|
||||||
|
case TextAlignment.Center:
|
||||||
|
return ATextAlignment.Center;
|
||||||
|
case TextAlignment.End:
|
||||||
|
return ATextAlignment.ViewEnd;
|
||||||
|
default:
|
||||||
|
return ATextAlignment.ViewStart;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void UpdateFlowDirection(this AndroidX.AppCompat.App.AlertDialog alertDialog, MauiPicker platformPicker)
|
||||||
|
{
|
||||||
|
var platformLayoutDirection = platformPicker.LayoutDirection;
|
||||||
|
|
||||||
|
// Propagate the MauiPicker LayoutDirection to the AlertDialog
|
||||||
|
var dv = alertDialog.Window?.DecorView;
|
||||||
|
|
||||||
|
if (dv is not null)
|
||||||
|
dv.LayoutDirection = platformLayoutDirection;
|
||||||
|
|
||||||
|
var lv = alertDialog?.ListView;
|
||||||
|
|
||||||
|
if (lv is not null)
|
||||||
|
{
|
||||||
|
lv.LayoutDirection = platformLayoutDirection;
|
||||||
|
lv.TextDirection = platformLayoutDirection.ToTextDirection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static ATextDirection ToTextDirection(this ALayoutDirection direction)
|
||||||
|
{
|
||||||
|
switch (direction)
|
||||||
|
{
|
||||||
|
case ALayoutDirection.Ltr:
|
||||||
|
return ATextDirection.Ltr;
|
||||||
|
case ALayoutDirection.Rtl:
|
||||||
|
return ATextDirection.Rtl;
|
||||||
|
default:
|
||||||
|
return ATextDirection.Inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static T GetRequiredService<T>(this IElementHandler handler)
|
||||||
|
where T : notnull
|
||||||
|
{
|
||||||
|
var services = handler.GetServiceProvider();
|
||||||
|
|
||||||
|
var service = services.GetRequiredService<T>();
|
||||||
|
|
||||||
|
return service;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IServiceProvider GetServiceProvider(this IElementHandler handler)
|
||||||
|
{
|
||||||
|
var context = handler.MauiContext ??
|
||||||
|
throw new InvalidOperationException($"Unable to find the context. The {nameof(ElementHandler.MauiContext)} property should have been set by the host.");
|
||||||
|
|
||||||
|
var services = context?.Services ??
|
||||||
|
throw new InvalidOperationException($"Unable to find the service provider. The {nameof(ElementHandler.MauiContext)} property should have been set by the host.");
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class Rtl
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// True if /manifest/application@android:supportsRtl="true"
|
||||||
|
/// </summary>
|
||||||
|
public static readonly bool IsSupported =
|
||||||
|
(Android.App.Application.Context?.ApplicationInfo?.Flags & ApplicationInfoFlags.SupportsRtl) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
#endif
|
53
src/Core/Controls/Picker/PickerHandler.cs
Normal file
53
src/Core/Controls/Picker/PickerHandler.cs
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
#if ANDROID
|
||||||
|
using Microsoft.Maui.Handlers;
|
||||||
|
using Microsoft.Maui.Platform;
|
||||||
|
|
||||||
|
namespace Bit.Core.Controls.Picker
|
||||||
|
{
|
||||||
|
// HACK: Due to https://github.com/dotnet/maui/issues/19681 and not willing to use reflection to access
|
||||||
|
// the alert dialog, we need to redefine the PickerHandler implementation for a custom one of ours
|
||||||
|
// which handles showing the current selected item. Remove this workaround when MAUI releases a fix for this.
|
||||||
|
// This is a copy from https://github.com/dotnet/maui/blob/main/src/Core/src/Handlers/Picker/PickerHandler.cs
|
||||||
|
public partial class PickerHandler : ViewHandler<IPicker, MauiPicker>, IPickerHandler
|
||||||
|
{
|
||||||
|
public static IPropertyMapper<IPicker, IPickerHandler> Mapper = new PropertyMapper<IPicker, PickerHandler>(ViewMapper)
|
||||||
|
{
|
||||||
|
#if __ANDROID__ || WINDOWS
|
||||||
|
[nameof(IPicker.Background)] = MapBackground,
|
||||||
|
#endif
|
||||||
|
[nameof(IPicker.CharacterSpacing)] = MapCharacterSpacing,
|
||||||
|
[nameof(IPicker.Font)] = MapFont,
|
||||||
|
[nameof(IPicker.SelectedIndex)] = MapSelectedIndex,
|
||||||
|
[nameof(IPicker.TextColor)] = MapTextColor,
|
||||||
|
[nameof(IPicker.Title)] = MapTitle,
|
||||||
|
[nameof(IPicker.TitleColor)] = MapTitleColor,
|
||||||
|
[nameof(ITextAlignment.HorizontalTextAlignment)] = MapHorizontalTextAlignment,
|
||||||
|
[nameof(ITextAlignment.VerticalTextAlignment)] = MapVerticalTextAlignment,
|
||||||
|
[nameof(IPicker.Items)] = MapItems,
|
||||||
|
};
|
||||||
|
|
||||||
|
public static CommandMapper<IPicker, IPickerHandler> CommandMapper = new(ViewCommandMapper)
|
||||||
|
{
|
||||||
|
};
|
||||||
|
|
||||||
|
public PickerHandler() : base(Mapper, CommandMapper)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public PickerHandler(IPropertyMapper? mapper)
|
||||||
|
: base(mapper ?? Mapper, CommandMapper)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public PickerHandler(IPropertyMapper? mapper, CommandMapper? commandMapper)
|
||||||
|
: base(mapper ?? Mapper, commandMapper ?? CommandMapper)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
IPicker IPickerHandler.VirtualView => VirtualView;
|
||||||
|
|
||||||
|
Microsoft.Maui.Platform.MauiPicker IPickerHandler.PlatformView => PlatformView;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
|
@ -72,6 +72,7 @@
|
||||||
<Folder Include="Utilities\Automation\" />
|
<Folder Include="Utilities\Automation\" />
|
||||||
<Folder Include="Utilities\Prompts\" />
|
<Folder Include="Utilities\Prompts\" />
|
||||||
<Folder Include="Resources\Localization\" />
|
<Folder Include="Resources\Localization\" />
|
||||||
|
<Folder Include="Controls\Picker\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<MauiImage Include="Resources\Images\dotnet_bot.svg">
|
<MauiImage Include="Resources\Images\dotnet_bot.svg">
|
||||||
|
@ -99,4 +100,7 @@
|
||||||
<Generator>MSBuild:Compile</Generator>
|
<Generator>MSBuild:Compile</Generator>
|
||||||
</MauiXaml>
|
</MauiXaml>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<None Remove="Controls\Picker\" />
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
|
@ -3,6 +3,7 @@ using CommunityToolkit.Maui;
|
||||||
using FFImageLoading.Maui;
|
using FFImageLoading.Maui;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Maui.Controls.Compatibility.Hosting;
|
using Microsoft.Maui.Controls.Compatibility.Hosting;
|
||||||
|
using Microsoft.Maui.Handlers;
|
||||||
using SkiaSharp.Views.Maui.Controls.Hosting;
|
using SkiaSharp.Views.Maui.Controls.Hosting;
|
||||||
using AppEffects = Bit.App.Effects;
|
using AppEffects = Bit.App.Effects;
|
||||||
|
|
||||||
|
@ -40,6 +41,16 @@ public static class MauiProgram
|
||||||
})
|
})
|
||||||
.ConfigureMauiHandlers(handlers =>
|
.ConfigureMauiHandlers(handlers =>
|
||||||
{
|
{
|
||||||
|
#if ANDROID
|
||||||
|
// HACK: Due to https://github.com/dotnet/maui/issues/19681 and not willing to use reflection to access
|
||||||
|
// the alert dialog, we need to redefine the PickerHandler implementation for a custom one of ours
|
||||||
|
// which handles showing the current selected item. Remove this workaround when MAUI releases a fix for this.
|
||||||
|
if (handlers.FirstOrDefault(h => h.ServiceType == typeof(Picker)) is ServiceDescriptor sd)
|
||||||
|
{
|
||||||
|
handlers.Remove(sd);
|
||||||
|
handlers.AddHandler(typeof(IPicker), typeof(Controls.Picker.PickerHandler));
|
||||||
|
}
|
||||||
|
#endif
|
||||||
customHandlers?.Invoke(handlers);
|
customHandlers?.Invoke(handlers);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue