Entry ClearButtonVisibility Handler (#564)

* Basic implementation for Standard and iOS handler.

* Property comment.

* Sample Entry controls added to sample project.

* PortHandler attributes added.

* Implemented Android handler with OnTouch and OnFocus listeners.

* Removed redundant check for clear button visibility on touch for Android handler.

* EntryStub implementation.

* Device tests for iOS and Android.

* Merge from main

Co-authored-by: E.Z. Hart <hartez@gmail.com>
This commit is contained in:
Burak Kaan Köse 2021-03-24 23:05:11 +01:00 коммит произвёл GitHub
Родитель 613dbc0072
Коммит 14fbdce843
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
15 изменённых файлов: 227 добавлений и 5 удалений

Просмотреть файл

@ -550,6 +550,7 @@ namespace Microsoft.Maui.Controls.Compatibility.Platform.Android
}
// Entry clear button management
[PortHandler("Focus management part might need to be reworked after IsFocused implementation.")]
public abstract partial class EntryRendererBase<TControl>
{
Drawable _clearBtn;

Просмотреть файл

@ -28,7 +28,7 @@ namespace Microsoft.Maui.Controls.Compatibility
{
typeof(Button),
typeof(ContentPage),
typeof(DatePicker),
typeof(DatePicker),
typeof(Editor),
typeof(Entry),
typeof(Label),

Просмотреть файл

@ -562,6 +562,7 @@ namespace Microsoft.Maui.Controls.Compatibility.Platform.iOS
Control.UserInteractionEnabled = !Element.IsReadOnly;
}
[PortHandler]
void UpdateClearButtonVisibility()
{
Control.ClearButtonMode = Element.ClearButtonVisibility == ClearButtonVisibility.WhileEditing ? UITextFieldViewMode.WhileEditing : UITextFieldViewMode.Never;

Просмотреть файл

@ -50,6 +50,12 @@ namespace Maui.Controls.Sample.Pages
verticalStack.Add(new Label { Text = loremIpsum, MaxLines = 2, LineBreakMode = LineBreakMode.TailTruncation });
verticalStack.Add(new Label { Text = "This should have five times the line height!", LineHeight = 5 });
var visibleClearButtonEntry = new Entry() { ClearButtonVisibility = ClearButtonVisibility.WhileEditing, Placeholder = "This Entry will show clear button if has input." };
var hiddenClearButtonEntry = new Entry() { ClearButtonVisibility = ClearButtonVisibility.Never, Placeholder = "This Entry will not..." };
verticalStack.Add(visibleClearButtonEntry);
verticalStack.Add(hiddenClearButtonEntry);
verticalStack.Add(new Editor { Placeholder = "This is an editor placeholder." } );
var paddingButton = new Button
{

Просмотреть файл

@ -19,5 +19,10 @@
/// Gets an enumeration value that controls the appearance of the return button.
/// </summary>
ReturnType ReturnType { get; }
/// <summary>
/// Gets an enumeration value that shows/hides clear button on the Entry.
/// </summary>
ClearButtonVisibility ClearButtonVisibility { get; }
}
}

Просмотреть файл

@ -1,37 +1,60 @@
using System;
using Android.Content.Res;
using Android.Graphics.Drawables;
using Android.Text;
using Android.Views;
using AndroidX.AppCompat.Widget;
using AndroidX.Core.Content;
using Microsoft.Extensions.DependencyInjection;
using static Android.Views.View;
namespace Microsoft.Maui.Handlers
{
public partial class EntryHandler : AbstractViewHandler<IEntry, AppCompatEditText>
{
TextWatcher Watcher { get; } = new TextWatcher();
EntryTouchListener TouchListener { get; } = new EntryTouchListener();
EntryFocusChangeListener FocusChangeListener { get; } = new EntryFocusChangeListener();
static ColorStateList? DefaultTextColors { get; set; }
static Drawable? ClearButtonDrawable { get; set; }
protected override AppCompatEditText CreateNativeView()
{
return new AppCompatEditText(Context);
}
// Returns the default 'X' char drawable in the AppCompatEditText.
protected virtual Drawable GetClearButtonDrawable()
=> ContextCompat.GetDrawable(Context, Resource.Drawable.abc_ic_clear_material);
protected override void ConnectHandler(AppCompatEditText nativeView)
{
Watcher.Handler = this;
TouchListener.Handler = this;
FocusChangeListener.Handler = this;
nativeView.OnFocusChangeListener = FocusChangeListener;
nativeView.AddTextChangedListener(Watcher);
nativeView.SetOnTouchListener(TouchListener);
}
protected override void DisconnectHandler(AppCompatEditText nativeView)
{
nativeView.RemoveTextChangedListener(Watcher);
nativeView.SetOnTouchListener(null);
nativeView.OnFocusChangeListener = null;
FocusChangeListener.Handler = null;
Watcher.Handler = null;
TouchListener.Handler = null;
}
protected override void SetupDefaults(AppCompatEditText nativeView)
{
base.SetupDefaults(nativeView);
ClearButtonDrawable = GetClearButtonDrawable();
DefaultTextColors = nativeView.TextColors;
}
@ -94,6 +117,30 @@ namespace Microsoft.Maui.Handlers
handler.TypedNativeView?.UpdateCharacterSpacing(entry);
}
public static void MapClearButtonVisibility(EntryHandler handler, IEntry entry)
{
handler.TypedNativeView?.UpdateClearButtonVisibility(entry, ClearButtonDrawable);
}
void OnFocusedChange(bool hasFocus)
{
if (TypedNativeView == null || VirtualView == null)
return;
// This will eliminate additional native property setting if not required.
if (VirtualView.ClearButtonVisibility == ClearButtonVisibility.WhileEditing)
TypedNativeView?.UpdateClearButtonVisibility(VirtualView, ClearButtonDrawable);
}
bool OnTouch(MotionEvent? motionEvent)
{
if (TypedNativeView == null || VirtualView == null)
return false;
// Check whether the touched position inbounds with clear button.
return HandleClearButtonTouched(motionEvent);
}
void OnTextChanged(string? text)
{
if (VirtualView == null || TypedNativeView == null)
@ -105,6 +152,47 @@ namespace Microsoft.Maui.Handlers
var nativeText = text ?? string.Empty;
if (mauiText != nativeText)
VirtualView.Text = nativeText;
// Text changed should trigger clear button visibility.
TypedNativeView.UpdateClearButtonVisibility(VirtualView, ClearButtonDrawable);
}
/// <summary>
/// Checks whether the touched position on the EditText is inbounds with clear button and clears if so.
/// This will return True to handle OnTouch to prevent re-activating keyboard after clearing the text.
/// </summary>
/// <returns>True if clear button is clicked and Text is cleared. False if not.</returns>
bool HandleClearButtonTouched(MotionEvent? motionEvent)
{
if (motionEvent == null || TypedNativeView == null || VirtualView == null)
return false;
var rBounds = ClearButtonDrawable?.Bounds;
if (rBounds != null)
{
var x = motionEvent.GetX();
var y = motionEvent.GetY();
if (motionEvent.Action == MotionEventActions.Up
&& ((x >= (TypedNativeView.Right - rBounds.Width())
&& x <= (TypedNativeView.Right - TypedNativeView.PaddingRight)
&& y >= TypedNativeView.PaddingTop
&& y <= (TypedNativeView.Height - TypedNativeView.PaddingBottom)
&& (VirtualView.FlowDirection == FlowDirection.LeftToRight))
|| (x >= (TypedNativeView.Left + TypedNativeView.PaddingLeft)
&& x <= (TypedNativeView.Left + rBounds.Width())
&& y >= TypedNativeView.PaddingTop
&& y <= (TypedNativeView.Height - TypedNativeView.PaddingBottom)
&& VirtualView.FlowDirection == FlowDirection.RightToLeft)))
{
TypedNativeView.Text = null;
return true;
}
}
return false;
}
class TextWatcher : Java.Lang.Object, ITextWatcher
@ -124,5 +212,26 @@ namespace Microsoft.Maui.Handlers
Handler?.OnTextChanged(s?.ToString());
}
}
// TODO: Maybe better to have generic version in IAndroidViewHandler?
class EntryTouchListener : Java.Lang.Object, IOnTouchListener
{
public EntryHandler? Handler { get; set; }
public bool OnTouch(View? v, MotionEvent? e)
{
return Handler?.OnTouch(e) ?? false;
}
}
// TODO: Maybe better to have generic version in IAndroidViewHandler?
class EntryFocusChangeListener : Java.Lang.Object, IOnFocusChangeListener
{
public EntryHandler? Handler { get; set; }
public void OnFocusChange(View? v, bool hasFocus)
{
Handler?.OnFocusedChange(hasFocus);
}
}
}
}

Просмотреть файл

@ -16,6 +16,7 @@ namespace Microsoft.Maui.Handlers
public static void MapIsReadOnly(IViewHandler handler, IEntry entry) { }
public static void MapFont(IViewHandler handler, IEntry entry) { }
public static void MapReturnType(IViewHandler handler, IEntry entry) { }
public static void MapClearButtonVisibility(IViewHandler handler, IEntry entry) { }
public static void MapCharacterSpacing(IViewHandler handler, IEntry entry) { }
}
}

Просмотреть файл

@ -14,6 +14,7 @@
[nameof(IEntry.IsReadOnly)] = MapIsReadOnly,
[nameof(IEntry.Font)] = MapFont,
[nameof(IEntry.ReturnType)] = MapReturnType,
[nameof(IEntry.ClearButtonVisibility)] = MapClearButtonVisibility,
[nameof(IEntry.CharacterSpacing)] = MapCharacterSpacing
};

Просмотреть файл

@ -119,6 +119,11 @@ namespace Microsoft.Maui.Handlers
handler.TypedNativeView?.UpdateCharacterSpacing(entry);
}
public static void MapClearButtonVisibility(EntryHandler handler, IEntry entry)
{
handler.TypedNativeView?.UpdateClearButtonVisibility(entry);
}
void OnEditingChanged(object? sender, EventArgs e) => OnTextChanged();
void OnEditingEnded(object? sender, EventArgs e) => OnTextChanged();

Просмотреть файл

@ -1,6 +1,8 @@
using System.Collections.Generic;
using Android.Content.Res;
using Android.Graphics.Drawables;
using Android.Text;
using Android.Util;
using AndroidX.AppCompat.Widget;
namespace Microsoft.Maui
@ -132,8 +134,55 @@ namespace Microsoft.Maui
editText.Focusable = isEditable;
}
public static void UpdateFont(this AppCompatEditText editText, IEntry entry, IFontManager fontManager) =>
editText.UpdateFont(entry.Font, fontManager);
public static void UpdateFont(this AppCompatEditText editText, IEntry entry, IFontManager fontManager)
{
var font = entry.Font;
var tf = fontManager.GetTypeface(font);
editText.Typeface = tf;
var sp = fontManager.GetScaledPixel(font);
editText.SetTextSize(ComplexUnitType.Sp, sp);
}
public static void UpdateClearButtonVisibility(this AppCompatEditText editText, IEntry entry, Drawable? ClearButtonDrawable)
{
// Places clear button drawable at the end or start of the EditText based on FlowDirection.
void ShowClearButton()
{
if (entry.FlowDirection == FlowDirection.RightToLeft)
{
editText.SetCompoundDrawablesWithIntrinsicBounds(ClearButtonDrawable, null, null, null);
}
else
{
editText.SetCompoundDrawablesWithIntrinsicBounds(null, null, ClearButtonDrawable, null);
}
}
// Hides clear button drawable from the control.
void HideClearButton()
{
editText.SetCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
}
bool isFocused = editText.IsFocused;
bool hasText = entry.Text?.Length > 0;
bool shouldDisplayClearButton = entry.ClearButtonVisibility == ClearButtonVisibility.WhileEditing
&& hasText
&& isFocused;
if (shouldDisplayClearButton)
{
ShowClearButton();
}
else
{
HideClearButton();
}
}
public static void UpdateReturnType(this AppCompatEditText editText, IEntry entry)
{
@ -174,4 +223,4 @@ namespace Microsoft.Maui
? currentText.Substring(0, maxLength)
: currentText;
}
}
}

Просмотреть файл

@ -104,5 +104,10 @@ namespace Microsoft.Maui
var uiFont = fontManager.GetFont(textView.Font);
textField.Font = uiFont;
}
public static void UpdateClearButtonVisibility(this UITextField textField, IEntry entry)
{
textField.ClearButtonMode = entry.ClearButtonVisibility == ClearButtonVisibility.WhileEditing ? UITextFieldViewMode.WhileEditing : UITextFieldViewMode.Never;
}
}
}

Просмотреть файл

@ -1,4 +1,6 @@
using System.Threading.Tasks;
using System.Linq;
using System.Threading.Tasks;
using Android.Graphics.Drawables;
using Android.Text;
using Android.Views.InputMethods;
using AndroidX.AppCompat.Widget;
@ -143,6 +145,24 @@ namespace Microsoft.Maui.DeviceTests
ImeAction GetNativeReturnType(EntryHandler entryHandler) =>
GetNativeEntry(entryHandler).ImeOptions;
bool GetNativeClearButtonVisibility(EntryHandler entryHandler)
{
var nativeEntry = GetNativeEntry(entryHandler);
var unfocusedDrawables = nativeEntry.GetCompoundDrawables();
bool compoundsValidWhenUnfocused = !unfocusedDrawables.Any(a => a != null);
// This will display 'X' drawable.
nativeEntry.RequestFocus();
var focusedDrawables = nativeEntry.GetCompoundDrawables();
// Index 2 for FlowDirection.LeftToRight.
bool compoundsValidWhenFocused = focusedDrawables.Length == 4 && focusedDrawables[2] != null;
return compoundsValidWhenFocused && compoundsValidWhenUnfocused;
}
[Fact(DisplayName = "CharacterSpacing Initializes Correctly")]
public async Task CharacterSpacingInitializesCorrectly()
{

Просмотреть файл

@ -193,6 +193,21 @@ namespace Microsoft.Maui.DeviceTests
await ValidatePropertyInitValue(entry, () => entry.Font.FontAttributes.HasFlag(FontAttributes.Italic), GetNativeIsItalic, isItalic);
}
[Theory(DisplayName = "Validates clear button visibility.")]
[InlineData(ClearButtonVisibility.WhileEditing, true)]
[InlineData(ClearButtonVisibility.Never, false)]
public async Task ValidateClearButtonVisibility(ClearButtonVisibility clearButtonVisibility, bool expected)
{
var entryStub = new EntryStub()
{
ClearButtonVisibility = clearButtonVisibility,
Text = "Test text input.",
FlowDirection = FlowDirection.LeftToRight
};
await ValidatePropertyInitValue(entryStub, () => expected, GetNativeClearButtonVisibility, expected);
}
[Theory(DisplayName = "TextChanged Events Fire Correctly")]
// null/empty
[InlineData(null, null, false)]

Просмотреть файл

@ -149,6 +149,9 @@ namespace Microsoft.Maui.DeviceTests
bool GetNativeIsItalic(EntryHandler entryHandler) =>
GetNativeEntry(entryHandler).Font.FontDescriptor.SymbolicTraits.HasFlag(UIFontDescriptorSymbolicTraits.Italic);
bool GetNativeClearButtonVisibility(EntryHandler entryHandler) =>
GetNativeEntry(entryHandler).ClearButtonMode == UITextFieldViewMode.WhileEditing;
UITextAlignment GetNativeTextAlignment(EntryHandler entryHandler) =>
GetNativeEntry(entryHandler).TextAlignment;

Просмотреть файл

@ -31,6 +31,7 @@ namespace Microsoft.Maui.DeviceTests.Stubs
public TextAlignment HorizontalTextAlignment { get; set; }
public ReturnType ReturnType { get; set; }
public ClearButtonVisibility ClearButtonVisibility { get; set; }
public event EventHandler<StubPropertyChangedEventArgs<string>> TextChanged;