From 3bbba15effd786e79e63fa3d66306350405d638f Mon Sep 17 00:00:00 2001 From: kamu Date: Sat, 20 Oct 2018 11:23:48 +0900 Subject: [PATCH 1/6] Add Feedback Effect --- .../AddCommandPlatformEffect.cs | 3 +- .../AddDatePickerPlatformEffect.cs | 1 + .../AddNumberPickerPlatform.cs | 1 + .../AddTextPlatformEffect.cs | 1 + .../AddTimePickerPlatformEffect.cs | 1 + .../AddTouchPlatformEffect.cs | 1 + AiForms.Effects.Droid/AiEffectBase.cs | 51 ++++ .../AiForms.Effects.Droid.csproj | 3 +- .../AlterColorPlatformEffect.cs | 1 + AiForms.Effects.Droid/AlterColorSlider.cs | 1 + AiForms.Effects.Droid/AlterColorStatusbar.cs | 1 + AiForms.Effects.Droid/AlterColorSwitch.cs | 1 + AiForms.Effects.Droid/AlterColorTextView.cs | 1 + .../AlterLineHeightPlatformEffect.cs | 1 + AiForms.Effects.Droid/BorderPlatformEffect.cs | 1 + .../FeedbackPlatformEffect.cs | 279 ++++++++++++++++++ AiForms.Effects.Droid/IAiEffectDroid.cs | 1 + .../LineHeightForEditText.cs | 1 + .../LineHeightForTextView.cs | 1 + .../PlaceholderPlatformEffect.cs | 1 + .../SizeToFitPlatformEffect.cs | 1 + .../ToFlatButtonPlatformEffect.cs | 1 + .../AddCommandPlatformEffect.cs | 14 +- .../AddDatePickerPlatformEffect.cs | 1 + .../AddNumberPickerPlatform.cs | 1 + .../AddTimePickerPlatformEffect.cs | 1 + AiForms.Effects.iOS/AddTouchPlatformEffect.cs | 1 + .../AiForms.Effects.iOS.csproj | 1 + .../AlterColorPlatformEffect.cs | 1 + AiForms.Effects.iOS/AlterColorSlider.cs | 1 + AiForms.Effects.iOS/AlterColorSwitch.cs | 1 + .../AlterLineHeightPlatformEffect.cs | 1 + AiForms.Effects.iOS/BorderPlatformEffect.cs | 1 + AiForms.Effects.iOS/FeedbackPlatformEffect.cs | 148 ++++++++++ AiForms.Effects.iOS/Initialize.cs | 1 + AiForms.Effects.iOS/LineHeightForLabel.cs | 1 + AiForms.Effects.iOS/LineHeightForTextView.cs | 1 + AiForms.Effects.iOS/NoCaretField.cs | 1 + AiForms.Effects.iOS/NumberPickerSource.cs | 1 + .../PlaceholderPlatformEffect.cs | 1 + .../SizeToFitPlatformEffect.cs | 1 + .../TouchEffectGestureRecognizer.cs | 1 + AiForms.Effects/AiForms.Effects.csproj | 4 + AiForms.Effects/AiRoutingEffectBase.cs | 14 + AiForms.Effects/Feedback.cs | 96 ++++++ .../AiEffects.TestApp.Droid.csproj | 5 +- .../AiEffects.TestApp.iOS.csproj | 1 + .../AiEffects.TestApp.csproj | 1 + .../Views/AddNumberPickerPage.xaml | 20 +- .../AiEffects.TestApp/Views/ViewCellPage.xaml | 8 +- nuget/AiEffects_mac.nuspec | 15 +- 51 files changed, 666 insertions(+), 32 deletions(-) create mode 100644 AiForms.Effects.Droid/FeedbackPlatformEffect.cs create mode 100644 AiForms.Effects.iOS/FeedbackPlatformEffect.cs create mode 100644 AiForms.Effects/AiRoutingEffectBase.cs create mode 100644 AiForms.Effects/Feedback.cs diff --git a/AiForms.Effects.Droid/AddCommandPlatformEffect.cs b/AiForms.Effects.Droid/AddCommandPlatformEffect.cs index cdb67dc..4aa8e03 100644 --- a/AiForms.Effects.Droid/AddCommandPlatformEffect.cs +++ b/AiForms.Effects.Droid/AddCommandPlatformEffect.cs @@ -16,6 +16,7 @@ [assembly: ExportEffect(typeof(AddCommandPlatformEffect), nameof(AddCommand))] namespace AiForms.Effects.Droid { + [Android.Runtime.Preserve(AllMembers =true)] public class AddCommandPlatformEffect : AiEffectBase { public static SoundEffect PlaySoundEffect = SoundEffect.KeyClick; @@ -569,7 +570,7 @@ protected override void Dispose(bool disposing) } } - + [Android.Runtime.Preserve(AllMembers = true)] internal class ContainerOnLayoutChangeListener : Java.Lang.Object, Android.Views.View.IOnLayoutChangeListener { private Android.Widget.FrameLayout _layout; diff --git a/AiForms.Effects.Droid/AddDatePickerPlatformEffect.cs b/AiForms.Effects.Droid/AddDatePickerPlatformEffect.cs index 817b071..abe618e 100644 --- a/AiForms.Effects.Droid/AddDatePickerPlatformEffect.cs +++ b/AiForms.Effects.Droid/AddDatePickerPlatformEffect.cs @@ -9,6 +9,7 @@ [assembly: ExportEffect(typeof(AddDatePickerPlatformEffect), nameof(AddDatePicker))] namespace AiForms.Effects.Droid { + [Android.Runtime.Preserve(AllMembers = true)] public class AddDatePickerPlatformEffect : AiEffectBase { Android.Views.View _view; diff --git a/AiForms.Effects.Droid/AddNumberPickerPlatform.cs b/AiForms.Effects.Droid/AddNumberPickerPlatform.cs index af71106..34cb22b 100644 --- a/AiForms.Effects.Droid/AddNumberPickerPlatform.cs +++ b/AiForms.Effects.Droid/AddNumberPickerPlatform.cs @@ -11,6 +11,7 @@ [assembly: ExportEffect(typeof(AddNumberPickerPlatform), nameof(AddNumberPicker))] namespace AiForms.Effects.Droid { + [Android.Runtime.Preserve(AllMembers = true)] public class AddNumberPickerPlatform : AiEffectBase { private AlertDialog _dialog; diff --git a/AiForms.Effects.Droid/AddTextPlatformEffect.cs b/AiForms.Effects.Droid/AddTextPlatformEffect.cs index ab81dba..3a44bf2 100644 --- a/AiForms.Effects.Droid/AddTextPlatformEffect.cs +++ b/AiForms.Effects.Droid/AddTextPlatformEffect.cs @@ -10,6 +10,7 @@ [assembly: ExportEffect(typeof(AddTextPlatformEffect), nameof(AddText))] namespace AiForms.Effects.Droid { + [Android.Runtime.Preserve(AllMembers = true)] public class AddTextPlatformEffect : AiEffectBase { private TextView _textView; diff --git a/AiForms.Effects.Droid/AddTimePickerPlatformEffect.cs b/AiForms.Effects.Droid/AddTimePickerPlatformEffect.cs index fb2a1e0..b17abdd 100644 --- a/AiForms.Effects.Droid/AddTimePickerPlatformEffect.cs +++ b/AiForms.Effects.Droid/AddTimePickerPlatformEffect.cs @@ -10,6 +10,7 @@ [assembly: ExportEffect(typeof(AddTimePickerPlatformEffect), nameof(AddTimePicker))] namespace AiForms.Effects.Droid { + [Android.Runtime.Preserve(AllMembers = true)] public class AddTimePickerPlatformEffect : AiEffectBase { Android.Views.View _view; diff --git a/AiForms.Effects.Droid/AddTouchPlatformEffect.cs b/AiForms.Effects.Droid/AddTouchPlatformEffect.cs index fb1c3dd..c915220 100644 --- a/AiForms.Effects.Droid/AddTouchPlatformEffect.cs +++ b/AiForms.Effects.Droid/AddTouchPlatformEffect.cs @@ -9,6 +9,7 @@ [assembly: ExportEffect(typeof(AddTouchPlatformEffect), nameof(AddTouch))] namespace AiForms.Effects.Droid { + [Android.Runtime.Preserve(AllMembers = true)] public class AddTouchPlatformEffect:PlatformEffect { WeakReference _viewRef; diff --git a/AiForms.Effects.Droid/AiEffectBase.cs b/AiForms.Effects.Droid/AiEffectBase.cs index e2e06e6..93d7a48 100644 --- a/AiForms.Effects.Droid/AiEffectBase.cs +++ b/AiForms.Effects.Droid/AiEffectBase.cs @@ -3,15 +3,19 @@ using System.Linq.Expressions; using System.Reflection; using System.Linq; +using Xamarin.Forms; namespace AiForms.Effects.Droid { + [Android.Runtime.Preserve(AllMembers = true)] public abstract class AiEffectBase : PlatformEffect { public static bool IsFastRenderers = global::Xamarin.Forms.Forms.Flags.Any(x => x == "FastRenderers_Experimental"); IVisualElementRenderer _renderer; bool _isDisposed = false; + WeakReference _navigationRef; + protected bool IsDisposed { get { if (_isDisposed) { @@ -30,6 +34,24 @@ protected bool IsDisposed { } } + protected override void OnAttached() + { + Element view = Element; + while (!(view is NavigationPage) && view != null) + { + view = view.Parent; + } + + _navigationRef = new WeakReference(view as NavigationPage); + + if (_navigationRef.TryGetTarget(out var navi)) + { + navi.Popped += PagePopped; + } + Xamarin.Forms.Application.Current.ModalPopped += ModalPopped; + } + + // whether Element is FastRenderer.(Exept Button) protected bool IsFastRenderer{ get{ @@ -82,5 +104,34 @@ Func CreateGetField(Type t) return lambda.Compile(); } + + void PagePopped(object sender, NavigationEventArgs e) + { + Clear(); + } + + void ModalPopped(object sender, ModalPoppedEventArgs e) + { + Clear(); + } + + void Clear() + { + if (_navigationRef.TryGetTarget(out var navi)) + { + navi.Popped -= PagePopped; + } + Xamarin.Forms.Application.Current.ModalPopped -= ModalPopped; + _navigationRef = null; + + // For Android, when a page is popped, OnDetached is automatically not called. (when iOS, it is called) + // So, made the Popped & ModalPopped event subscribe in advance + // and make the effect manually removed when the page is popped. + if (IsAttached && !IsDisposed) + { + var toRemove = Element.Effects.Cast().FirstOrDefault(x => x.EffectId == ResolveId); + Element.Effects.Remove(toRemove); + } + } } } diff --git a/AiForms.Effects.Droid/AiForms.Effects.Droid.csproj b/AiForms.Effects.Droid/AiForms.Effects.Droid.csproj index 6c1fa9c..dec7851 100644 --- a/AiForms.Effects.Droid/AiForms.Effects.Droid.csproj +++ b/AiForms.Effects.Droid/AiForms.Effects.Droid.csproj @@ -9,7 +9,7 @@ Library AiForms.Effects.Droid AiForms.Effects.Droid - v8.1 + v9.0 Resources\Resource.designer.cs Resource Resources @@ -131,6 +131,7 @@ + diff --git a/AiForms.Effects.Droid/AlterColorPlatformEffect.cs b/AiForms.Effects.Droid/AlterColorPlatformEffect.cs index 6ec567b..597e249 100644 --- a/AiForms.Effects.Droid/AlterColorPlatformEffect.cs +++ b/AiForms.Effects.Droid/AlterColorPlatformEffect.cs @@ -7,6 +7,7 @@ [assembly: ExportEffect(typeof(AlterColorPlatformEffect), nameof(AlterColor))] namespace AiForms.Effects.Droid { + [Android.Runtime.Preserve(AllMembers = true)] public class AlterColorPlatformEffect : AiEffectBase { IAiEffectDroid _effect; diff --git a/AiForms.Effects.Droid/AlterColorSlider.cs b/AiForms.Effects.Droid/AlterColorSlider.cs index 82c787a..db2a37c 100644 --- a/AiForms.Effects.Droid/AlterColorSlider.cs +++ b/AiForms.Effects.Droid/AlterColorSlider.cs @@ -6,6 +6,7 @@ namespace AiForms.Effects.Droid { + [Android.Runtime.Preserve(AllMembers = true)] public class AlterColorSlider : IAiEffectDroid { SeekBar _seekbar; diff --git a/AiForms.Effects.Droid/AlterColorStatusbar.cs b/AiForms.Effects.Droid/AlterColorStatusbar.cs index 70baa32..b588bc6 100644 --- a/AiForms.Effects.Droid/AlterColorStatusbar.cs +++ b/AiForms.Effects.Droid/AlterColorStatusbar.cs @@ -6,6 +6,7 @@ namespace AiForms.Effects.Droid { + [Android.Runtime.Preserve(AllMembers = true)] public class AlterColorStatusbar : IAiEffectDroid { Window _window; diff --git a/AiForms.Effects.Droid/AlterColorSwitch.cs b/AiForms.Effects.Droid/AlterColorSwitch.cs index 5abd1e4..6398db3 100644 --- a/AiForms.Effects.Droid/AlterColorSwitch.cs +++ b/AiForms.Effects.Droid/AlterColorSwitch.cs @@ -7,6 +7,7 @@ namespace AiForms.Effects.Droid { + [Android.Runtime.Preserve(AllMembers = true)] public class AlterColorSwitch : IAiEffectDroid { SwitchCompat _aSwitch; diff --git a/AiForms.Effects.Droid/AlterColorTextView.cs b/AiForms.Effects.Droid/AlterColorTextView.cs index 076ab00..13a3823 100644 --- a/AiForms.Effects.Droid/AlterColorTextView.cs +++ b/AiForms.Effects.Droid/AlterColorTextView.cs @@ -7,6 +7,7 @@ namespace AiForms.Effects.Droid { + [Android.Runtime.Preserve(AllMembers = true)] public class AlterColorTextView : IAiEffectDroid { TextView _textview; diff --git a/AiForms.Effects.Droid/AlterLineHeightPlatformEffect.cs b/AiForms.Effects.Droid/AlterLineHeightPlatformEffect.cs index de23f4f..4a2a9c3 100644 --- a/AiForms.Effects.Droid/AlterLineHeightPlatformEffect.cs +++ b/AiForms.Effects.Droid/AlterLineHeightPlatformEffect.cs @@ -5,6 +5,7 @@ [assembly: ExportEffect(typeof(AlterLineHeightPlatformEffect), nameof(AlterLineHeight))] namespace AiForms.Effects.Droid { + [Android.Runtime.Preserve(AllMembers = true)] public class AlterLineHeightPlatformEffect : AiEffectBase { private IAiEffectDroid _effect; diff --git a/AiForms.Effects.Droid/BorderPlatformEffect.cs b/AiForms.Effects.Droid/BorderPlatformEffect.cs index b3d1177..50fb062 100644 --- a/AiForms.Effects.Droid/BorderPlatformEffect.cs +++ b/AiForms.Effects.Droid/BorderPlatformEffect.cs @@ -8,6 +8,7 @@ [assembly: ExportEffect(typeof(BorderPlatformEffect), nameof(Border))] namespace AiForms.Effects.Droid { + [Android.Runtime.Preserve(AllMembers = true)] public class BorderPlatformEffect : AiEffectBase { Android.Views.View _view; diff --git a/AiForms.Effects.Droid/FeedbackPlatformEffect.cs b/AiForms.Effects.Droid/FeedbackPlatformEffect.cs new file mode 100644 index 0000000..e84e47a --- /dev/null +++ b/AiForms.Effects.Droid/FeedbackPlatformEffect.cs @@ -0,0 +1,279 @@ +using System; +using System.Linq; +using AiForms.Effects; +using AiForms.Effects.Droid; +using Android.Content; +using Android.Content.Res; +using Android.Graphics.Drawables; +using Android.Media; +using Android.Views; +using Android.Widget; +using Xamarin.Forms; +using Xamarin.Forms.Platform.Android; + +[assembly: ExportEffect(typeof(FeedbackPlatformEffect), nameof(Feedback))] +namespace AiForms.Effects.Droid +{ + [Android.Runtime.Preserve(AllMembers = true)] + public class FeedbackPlatformEffect:AiEffectBase + { + public static SoundEffect PlaySoundEffect = SoundEffect.KeyClick; + public static Type[] TapSoundEffectElementType = { + typeof(ContentPresenter), + typeof(ContentView), + typeof(Frame), + typeof(Cell), + typeof(Xamarin.Forms.ScrollView), + typeof(TemplatedView), + typeof(Xamarin.Forms.AbsoluteLayout), + typeof(Grid), + typeof(Xamarin.Forms.RelativeLayout), + typeof(StackLayout) + }; + + private Android.Views.View _view; + private RippleDrawable _ripple; + private Drawable _orgDrawable; + private FrameLayout _rippleOverlay; + private FastRendererOnLayoutChangeListener _fastListener; + private bool _enableSound; + private bool _isTapTargetSoundEffect; + private AudioManager _audioManager; + + + protected override void OnAttached() + { + base.OnAttached(); + + _view = Control ?? Container; + + _isTapTargetSoundEffect = TapSoundEffectElementType.Any(x => x == Element.GetType()); + + if (_audioManager == null) + { + _audioManager = (AudioManager)_view.Context.GetSystemService(Context.AudioService); + } + + _view.Clickable = true; + _view.LongClickable = true; + + SetUpRipple(); + + UpdateEnableSound(); + + if(IsClickable) + { + _view.Touch += OnViewTouch; + } + + UpdateEffectColor(); + UpdateIsEnabled(); + } + + protected override void OnDetached() + { + System.Diagnostics.Debug.WriteLine(Element.GetType().FullName); + if (!IsDisposed) + { + if (!IsClickable) + { + _view.Touch -= OnOverlayTouch; + _view.RemoveOnLayoutChangeListener(_fastListener); + _rippleOverlay.RemoveFromParent(); + _fastListener.Dispose(); + _fastListener = null; + _rippleOverlay.Dispose(); + _rippleOverlay = null; + } + else + { + _view.Touch -= OnViewTouch; + _view.Background = _orgDrawable; + _orgDrawable = null; + } + } + + _ripple?.Dispose(); + _ripple = null; + + _audioManager = null; + _view = null; + } + + protected override void OnElementPropertyChanged(System.ComponentModel.PropertyChangedEventArgs e) + { + base.OnElementPropertyChanged(e); + + if (IsDisposed) + { + return; + } + + if (e.PropertyName == Feedback.EffectColorProperty.PropertyName) + { + UpdateEffectColor(); + } + else if (e.PropertyName == Feedback.EnableSoundProperty.PropertyName) + { + UpdateEnableSound(); + } + else if(e.PropertyName == VisualElement.IsEnabledProperty.PropertyName) + { + UpdateIsEnabled(); + } + } + + void OnViewTouch(object sender, Android.Views.View.TouchEventArgs e) + { + if(e.Event.Action == MotionEventActions.Down) + { + PlaySound(); + } + + e.Handled = false; + } + + void OnOverlayTouch(object sender, Android.Views.View.TouchEventArgs e) + { + if (e.Event.Action == MotionEventActions.Down) + { + PlaySound(); + } + + _view.DispatchTouchEvent(e.Event); + + e.Handled = false; + } + + void PlaySound() + { + if (_isTapTargetSoundEffect && _enableSound) + { + _audioManager?.PlaySoundEffect(PlaySoundEffect); + } + } + + void UpdateEffectColor() + { + var color = Feedback.GetEffectColor(Element); + _ripple?.SetColor(getPressedColorSelector(color.ToAndroid())); + } + + void UpdateEnableSound() + { + _enableSound = Feedback.GetEnableSound(Element); + } + + void UpdateIsEnabled() + { + if(!IsClickable) + { + _rippleOverlay.Enabled = (Element as VisualElement).IsEnabled; + } + } + + void SetUpRipple() + { + _ripple = CreateRipple(Android.Graphics.Color.Transparent); + + if (!IsClickable) + { + _rippleOverlay = new FrameLayout(_view.Context) + { + Clickable = true, + LongClickable = true, + Foreground = _ripple + }; + _fastListener = new FastRendererOnLayoutChangeListener(this); + _view.AddOnLayoutChangeListener(_fastListener); + _view.RequestLayout(); + } + else + { + _orgDrawable = _view.Background; + _view.Background = _ripple; + } + } + + void SetUpOverlay() + { + var parent = _view.Parent as Android.Views.ViewGroup; + + parent.AddView(_rippleOverlay); + + _rippleOverlay.BringToFront(); + _rippleOverlay.Touch += OnOverlayTouch; + } + + + RippleDrawable CreateRipple(Android.Graphics.Color color) + { + if (!IsClickable) + { + var mask = new ColorDrawable(Android.Graphics.Color.White); + return new RippleDrawable(getPressedColorSelector(color), null, mask); + } + + var back = _view.Background; + if (back == null) + { + var mask = new ColorDrawable(Android.Graphics.Color.White); + return new RippleDrawable(getPressedColorSelector(color), null, mask); + } + else + { + return new RippleDrawable(getPressedColorSelector(color), back, null); + } + } + + ColorStateList getPressedColorSelector(int pressedColor) + { + return new ColorStateList( + new int[][] + { + new int[]{} + }, + new int[] + { + pressedColor, + }); + } + + internal class FastRendererOnLayoutChangeListener : Java.Lang.Object, Android.Views.View.IOnLayoutChangeListener + { + bool _alreadyGotParent = false; + FeedbackPlatformEffect _effect; + + public FastRendererOnLayoutChangeListener(FeedbackPlatformEffect effect) + { + _effect = effect; + } + + // Because FastRenderer of Label or Image can't be set ClickListener, + // insert FrameLayout with same position and same size on the view. + public void OnLayoutChange(Android.Views.View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) + { + _effect._rippleOverlay.Layout(v.Left, v.Top, v.Right, v.Bottom); + + if (_alreadyGotParent) + { + return; + } + + _alreadyGotParent = true; + + // FastRenderer Control's parent can be got at only this timing. + _effect.SetUpOverlay(); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _effect = null; + } + base.Dispose(disposing); + } + } + } +} diff --git a/AiForms.Effects.Droid/IAiEffectDroid.cs b/AiForms.Effects.Droid/IAiEffectDroid.cs index cad46d8..27c5548 100644 --- a/AiForms.Effects.Droid/IAiEffectDroid.cs +++ b/AiForms.Effects.Droid/IAiEffectDroid.cs @@ -3,6 +3,7 @@ namespace AiForms.Effects.Droid { + [Android.Runtime.Preserve(AllMembers = true)] public interface IAiEffectDroid : IAiEffect { void OnDetachedIfNotDisposed(); diff --git a/AiForms.Effects.Droid/LineHeightForEditText.cs b/AiForms.Effects.Droid/LineHeightForEditText.cs index c9153f2..85e9077 100644 --- a/AiForms.Effects.Droid/LineHeightForEditText.cs +++ b/AiForms.Effects.Droid/LineHeightForEditText.cs @@ -5,6 +5,7 @@ namespace AiForms.Effects.Droid { + [Android.Runtime.Preserve(AllMembers = true)] public class LineHeightForEditText : IAiEffectDroid { private Android.Views.ViewGroup _container; diff --git a/AiForms.Effects.Droid/LineHeightForTextView.cs b/AiForms.Effects.Droid/LineHeightForTextView.cs index 46b96b0..866b785 100644 --- a/AiForms.Effects.Droid/LineHeightForTextView.cs +++ b/AiForms.Effects.Droid/LineHeightForTextView.cs @@ -4,6 +4,7 @@ namespace AiForms.Effects.Droid { + [Android.Runtime.Preserve(AllMembers = true)] public class LineHeightForTextView : IAiEffectDroid { private Android.Views.View _container; diff --git a/AiForms.Effects.Droid/PlaceholderPlatformEffect.cs b/AiForms.Effects.Droid/PlaceholderPlatformEffect.cs index 1889344..e86b30e 100644 --- a/AiForms.Effects.Droid/PlaceholderPlatformEffect.cs +++ b/AiForms.Effects.Droid/PlaceholderPlatformEffect.cs @@ -8,6 +8,7 @@ [assembly: ExportEffect(typeof(PlaceholderPlatformEffect), nameof(Placeholder))] namespace AiForms.Effects.Droid { + [Android.Runtime.Preserve(AllMembers = true)] public class PlaceholderPlatformEffect : AiEffectBase { EditText _editText; diff --git a/AiForms.Effects.Droid/SizeToFitPlatformEffect.cs b/AiForms.Effects.Droid/SizeToFitPlatformEffect.cs index 1ad4234..01aa12d 100644 --- a/AiForms.Effects.Droid/SizeToFitPlatformEffect.cs +++ b/AiForms.Effects.Droid/SizeToFitPlatformEffect.cs @@ -11,6 +11,7 @@ [assembly: ExportEffect(typeof(SizeToFitPlatformEffect), nameof(SizeToFit))] namespace AiForms.Effects.Droid { + [Android.Runtime.Preserve(AllMembers = true)] public class SizeToFitPlatformEffect : AiEffectBase { FormsTextView _view; diff --git a/AiForms.Effects.Droid/ToFlatButtonPlatformEffect.cs b/AiForms.Effects.Droid/ToFlatButtonPlatformEffect.cs index 2a5390e..df058f7 100644 --- a/AiForms.Effects.Droid/ToFlatButtonPlatformEffect.cs +++ b/AiForms.Effects.Droid/ToFlatButtonPlatformEffect.cs @@ -11,6 +11,7 @@ [assembly: ExportEffect(typeof(ToFlatButtonPlatformEffect), nameof(ToFlatButton))] namespace AiForms.Effects.Droid { + [Android.Runtime.Preserve(AllMembers = true)] public class ToFlatButtonPlatformEffect : AiEffectBase { private ColorStateList Colors; diff --git a/AiForms.Effects.iOS/AddCommandPlatformEffect.cs b/AiForms.Effects.iOS/AddCommandPlatformEffect.cs index 6ce2016..69160fc 100644 --- a/AiForms.Effects.iOS/AddCommandPlatformEffect.cs +++ b/AiForms.Effects.iOS/AddCommandPlatformEffect.cs @@ -14,6 +14,7 @@ [assembly: ExportEffect(typeof(AddCommandPlatformEffect), nameof(AddCommand))] namespace AiForms.Effects.iOS { + [Foundation.Preserve(AllMembers =true)] public class AddCommandPlatformEffect : PlatformEffect { public static uint PlaySoundNo = 1306; @@ -44,14 +45,14 @@ protected override void OnAttached() { _view = Control ?? Container; - _tapGesture = new UITapGestureRecognizer(async (obj) => { + _tapGesture = new UITapGestureRecognizer((obj) => { if (_command == null) return; if (!_command.CanExecute(_commandParameter)) return; - await TapAnimation(0.3, _alpha, 0); + TapAnimation(0.3, _alpha, 0); if (_enableSound) PlayClickSound(); @@ -222,13 +223,13 @@ void UpdateLongCommand() _longCommand?.Execute(_longCommandParameter ?? Element); - await TapAnimation(0.5, 0, _alpha, false); + TapAnimation(0.5, 0, _alpha, false); } else if (obj.State == UIGestureRecognizerState.Ended || obj.State == UIGestureRecognizerState.Cancelled || obj.State == UIGestureRecognizerState.Failed) { - await TapAnimation(0.5, _alpha, 0); + TapAnimation(0.5, _alpha, 0); } }); _view.AddGestureRecognizer(_longTapGesture); @@ -264,14 +265,15 @@ void UpdateEnableSound() _enableSound = AddCommand.GetEnableSound(Element); } - async Task TapAnimation(double duration, double start = 1, double end = 0, bool remove = true) + async void TapAnimation(double duration, double start = 1, double end = 0, bool remove = true) { if (_layer != null) { _layer.Frame = new CGRect(0, 0, Container.Bounds.Width, Container.Bounds.Height); Container.AddSubview(_layer); Container.BringSubviewToFront(_layer); _layer.Alpha = (float)start; - await UIView.AnimateAsync(duration, () => { + await UIView.AnimateAsync(duration, () => + { _layer.Alpha = (float)end; }); if (remove) { diff --git a/AiForms.Effects.iOS/AddDatePickerPlatformEffect.cs b/AiForms.Effects.iOS/AddDatePickerPlatformEffect.cs index 96385c9..eb0456a 100644 --- a/AiForms.Effects.iOS/AddDatePickerPlatformEffect.cs +++ b/AiForms.Effects.iOS/AddDatePickerPlatformEffect.cs @@ -11,6 +11,7 @@ [assembly: ExportEffect(typeof(AddDatePickerPlatformEffect), nameof(AddDatePicker))] namespace AiForms.Effects.iOS { + [Foundation.Preserve(AllMembers = true)] public class AddDatePickerPlatformEffect : PlatformEffect { UIView _view; diff --git a/AiForms.Effects.iOS/AddNumberPickerPlatform.cs b/AiForms.Effects.iOS/AddNumberPickerPlatform.cs index 0280844..6cd296b 100644 --- a/AiForms.Effects.iOS/AddNumberPickerPlatform.cs +++ b/AiForms.Effects.iOS/AddNumberPickerPlatform.cs @@ -9,6 +9,7 @@ [assembly: ExportEffect(typeof(NumberPickerPlatform), nameof(AddNumberPicker))] namespace AiForms.Effects.iOS { + [Foundation.Preserve(AllMembers = true)] public class NumberPickerPlatform : PlatformEffect { private UIPickerView _picker; diff --git a/AiForms.Effects.iOS/AddTimePickerPlatformEffect.cs b/AiForms.Effects.iOS/AddTimePickerPlatformEffect.cs index 5431678..f73a8cb 100644 --- a/AiForms.Effects.iOS/AddTimePickerPlatformEffect.cs +++ b/AiForms.Effects.iOS/AddTimePickerPlatformEffect.cs @@ -11,6 +11,7 @@ [assembly: ExportEffect(typeof(AddTimePickerPlatformEffect), nameof(AddTimePicker))] namespace AiForms.Effects.iOS { + [Foundation.Preserve(AllMembers = true)] public class AddTimePickerPlatformEffect : PlatformEffect { UIDatePicker _picker; diff --git a/AiForms.Effects.iOS/AddTouchPlatformEffect.cs b/AiForms.Effects.iOS/AddTouchPlatformEffect.cs index 91f0fbb..7de0dfc 100644 --- a/AiForms.Effects.iOS/AddTouchPlatformEffect.cs +++ b/AiForms.Effects.iOS/AddTouchPlatformEffect.cs @@ -7,6 +7,7 @@ [assembly: ExportEffect(typeof(AddTouchPlatformEffect), nameof(AddTouch))] namespace AiForms.Effects.iOS { + [Foundation.Preserve(AllMembers = true)] public class AddTouchPlatformEffect : PlatformEffect { UIView _view; diff --git a/AiForms.Effects.iOS/AiForms.Effects.iOS.csproj b/AiForms.Effects.iOS/AiForms.Effects.iOS.csproj index 90567da..119edec 100644 --- a/AiForms.Effects.iOS/AiForms.Effects.iOS.csproj +++ b/AiForms.Effects.iOS/AiForms.Effects.iOS.csproj @@ -83,6 +83,7 @@ + diff --git a/AiForms.Effects.iOS/AlterColorPlatformEffect.cs b/AiForms.Effects.iOS/AlterColorPlatformEffect.cs index 25541f8..149d2f0 100644 --- a/AiForms.Effects.iOS/AlterColorPlatformEffect.cs +++ b/AiForms.Effects.iOS/AlterColorPlatformEffect.cs @@ -8,6 +8,7 @@ [assembly: ExportEffect(typeof(AlterColorPlatformEffect), nameof(AlterColor))] namespace AiForms.Effects.iOS { + [Foundation.Preserve(AllMembers = true)] public class AlterColorPlatformEffect : PlatformEffect { IAiEffect _effect; diff --git a/AiForms.Effects.iOS/AlterColorSlider.cs b/AiForms.Effects.iOS/AlterColorSlider.cs index b5389af..c5a25bc 100644 --- a/AiForms.Effects.iOS/AlterColorSlider.cs +++ b/AiForms.Effects.iOS/AlterColorSlider.cs @@ -4,6 +4,7 @@ using Xamarin.Forms.Platform.iOS; namespace AiForms.Effects.iOS { + [Foundation.Preserve(AllMembers = true)] public class AlterColorSlider : IAiEffect { UISlider _slider; diff --git a/AiForms.Effects.iOS/AlterColorSwitch.cs b/AiForms.Effects.iOS/AlterColorSwitch.cs index 51d8361..d0ad23a 100644 --- a/AiForms.Effects.iOS/AlterColorSwitch.cs +++ b/AiForms.Effects.iOS/AlterColorSwitch.cs @@ -4,6 +4,7 @@ using Xamarin.Forms.Platform.iOS; namespace AiForms.Effects.iOS { + [Foundation.Preserve(AllMembers = true)] public class AlterColorSwitch : IAiEffect { UISwitch _uiswitch; diff --git a/AiForms.Effects.iOS/AlterLineHeightPlatformEffect.cs b/AiForms.Effects.iOS/AlterLineHeightPlatformEffect.cs index 31512e4..81c14cb 100644 --- a/AiForms.Effects.iOS/AlterLineHeightPlatformEffect.cs +++ b/AiForms.Effects.iOS/AlterLineHeightPlatformEffect.cs @@ -6,6 +6,7 @@ [assembly: ExportEffect(typeof(AlterLineHeightPlatformEffect), nameof(AlterLineHeight))] namespace AiForms.Effects.iOS { + [Foundation.Preserve(AllMembers = true)] public class AlterLineHeightPlatformEffect : PlatformEffect { diff --git a/AiForms.Effects.iOS/BorderPlatformEffect.cs b/AiForms.Effects.iOS/BorderPlatformEffect.cs index 9ce1eb8..f36065e 100644 --- a/AiForms.Effects.iOS/BorderPlatformEffect.cs +++ b/AiForms.Effects.iOS/BorderPlatformEffect.cs @@ -9,6 +9,7 @@ [assembly: ExportEffect(typeof(BorderPlatformEffect), nameof(Border))] namespace AiForms.Effects.iOS { + [Foundation.Preserve(AllMembers = true)] public class BorderPlatformEffect : PlatformEffect { UIView _view; diff --git a/AiForms.Effects.iOS/FeedbackPlatformEffect.cs b/AiForms.Effects.iOS/FeedbackPlatformEffect.cs new file mode 100644 index 0000000..377397b --- /dev/null +++ b/AiForms.Effects.iOS/FeedbackPlatformEffect.cs @@ -0,0 +1,148 @@ +using System; +using AiForms.Effects; +using AiForms.Effects.iOS; +using AudioToolbox; +using UIKit; +using Xamarin.Forms; +using Xamarin.Forms.Platform.iOS; +using System.Linq; + +[assembly: ExportEffect(typeof(FeedbackPlatformEffect), nameof(Feedback))] +namespace AiForms.Effects.iOS +{ + public class FeedbackPlatformEffect:PlatformEffect + { + public static uint PlaySoundNo = 1306; + + TouchRecognizer _toucheController; + TouchEffectGestureRecognizer _touchRecognizer; + UIView _view; + UIView _layer; + bool _enableSound; + SystemSound _clickSound; + float _alpha; + VisualElement visualElement => Element as VisualElement; + + protected override void OnAttached() + { + _view = Control ?? Container; + + _view.UserInteractionEnabled = true; + + _layer = new UIView { + Alpha = 0, + Opaque = false, + UserInteractionEnabled = false + }; + _view.AddSubview(_layer); + + _layer.TranslatesAutoresizingMaskIntoConstraints = false; + + _layer.TopAnchor.ConstraintEqualTo(_view.TopAnchor).Active = true; + _layer.LeftAnchor.ConstraintEqualTo(_view.LeftAnchor).Active = true; + _layer.BottomAnchor.ConstraintEqualTo(_view.BottomAnchor).Active = true; + _layer.RightAnchor.ConstraintEqualTo(_view.RightAnchor).Active = true; + + + _view.BringSubviewToFront(_layer); + + _toucheController = new TouchRecognizer(); + _touchRecognizer = new TouchEffectGestureRecognizer(_toucheController); + + _view.AddGestureRecognizer(_touchRecognizer); + + _toucheController.TouchBegin += OnTouchBegin; + _toucheController.TouchEnd += OnTouchEnd; + _toucheController.TouchCancel += OnTouchEnd; + + UpdateEffectColor(); + UpdateEnableSound(); + } + + protected override void OnDetached() + { + _toucheController.TouchBegin -= OnTouchBegin; + _toucheController.TouchEnd -= OnTouchEnd; + _toucheController.TouchCancel -= OnTouchEnd; + + _view.RemoveGestureRecognizer(_touchRecognizer); + _touchRecognizer.Dispose(); + + _touchRecognizer = null; + _toucheController = null; + + _layer.RemoveFromSuperview(); + _layer.RemoveConstraints(_layer.Constraints); + _layer.Dispose(); + _layer = null; + + _clickSound?.Dispose(); + _clickSound = null; + + _view = null; + } + + protected override void OnElementPropertyChanged(System.ComponentModel.PropertyChangedEventArgs e) + { + base.OnElementPropertyChanged(e); + + if (e.PropertyName == Feedback.EffectColorProperty.PropertyName) + { + UpdateEffectColor(); + } + else if (e.PropertyName == Feedback.EnableSoundProperty.PropertyName) + { + UpdateEnableSound(); + } + } + + async void OnTouchBegin(object sender, TouchEventArgs e) + { + if (!visualElement.IsEnabled) return; + + // if it is a variety of Picker, make it fire. + _view.Subviews.FirstOrDefault(x => x is NoCaretField)?.BecomeFirstResponder(); + + if (_enableSound) + { + PlayClickSound(); + } + + await UIView.AnimateAsync(0.5, () => + { + _layer.Alpha = _alpha; + }); + } + + async void OnTouchEnd(object sender, TouchEventArgs e) + { + if (!visualElement.IsEnabled) return; + + await UIView.AnimateAsync(0.5, () => + { + _layer.Alpha = 0; + }); + } + + void UpdateEffectColor() + { + var color = Feedback.GetEffectColor(Element); + + _alpha = color.A < 1.0f ? 1f : 0.3f; + _layer.BackgroundColor = color.ToUIColor(); + } + + void UpdateEnableSound() + { + _enableSound = Feedback.GetEnableSound(Element); + } + + void PlayClickSound() + { + if (_clickSound == null) + _clickSound = new SystemSound(PlaySoundNo); + + _clickSound.PlaySystemSoundAsync(); + } + } +} diff --git a/AiForms.Effects.iOS/Initialize.cs b/AiForms.Effects.iOS/Initialize.cs index e4c66b1..5afa259 100644 --- a/AiForms.Effects.iOS/Initialize.cs +++ b/AiForms.Effects.iOS/Initialize.cs @@ -1,5 +1,6 @@ namespace AiForms.Effects.iOS { + [Foundation.Preserve(AllMembers = true)] public static class Effects { public static void Init() { } diff --git a/AiForms.Effects.iOS/LineHeightForLabel.cs b/AiForms.Effects.iOS/LineHeightForLabel.cs index 0d2d629..f089777 100644 --- a/AiForms.Effects.iOS/LineHeightForLabel.cs +++ b/AiForms.Effects.iOS/LineHeightForLabel.cs @@ -5,6 +5,7 @@ namespace AiForms.Effects.iOS { + [Foundation.Preserve(AllMembers = true)] public class LineHeightForLabel : IAiEffect { private UIView _container; diff --git a/AiForms.Effects.iOS/LineHeightForTextView.cs b/AiForms.Effects.iOS/LineHeightForTextView.cs index 1c245ea..04b636e 100644 --- a/AiForms.Effects.iOS/LineHeightForTextView.cs +++ b/AiForms.Effects.iOS/LineHeightForTextView.cs @@ -5,6 +5,7 @@ namespace AiForms.Effects.iOS { + [Foundation.Preserve(AllMembers = true)] public class LineHeightForTextView : IAiEffect { private UITextView _nativeTextView; diff --git a/AiForms.Effects.iOS/NoCaretField.cs b/AiForms.Effects.iOS/NoCaretField.cs index d7bb0e7..4974105 100644 --- a/AiForms.Effects.iOS/NoCaretField.cs +++ b/AiForms.Effects.iOS/NoCaretField.cs @@ -4,6 +4,7 @@ namespace AiForms.Effects.iOS { + [Foundation.Preserve(AllMembers = true)] internal class NoCaretField : UITextField { public NoCaretField() : base(new CGRect()) diff --git a/AiForms.Effects.iOS/NumberPickerSource.cs b/AiForms.Effects.iOS/NumberPickerSource.cs index 8b6340d..05475d3 100644 --- a/AiForms.Effects.iOS/NumberPickerSource.cs +++ b/AiForms.Effects.iOS/NumberPickerSource.cs @@ -5,6 +5,7 @@ namespace AiForms.Effects.iOS { + [Foundation.Preserve(AllMembers = true)] internal class NumberPickerSource : UIPickerViewModel { public IList Items { get; private set; } diff --git a/AiForms.Effects.iOS/PlaceholderPlatformEffect.cs b/AiForms.Effects.iOS/PlaceholderPlatformEffect.cs index a886ec0..bf11cdf 100644 --- a/AiForms.Effects.iOS/PlaceholderPlatformEffect.cs +++ b/AiForms.Effects.iOS/PlaceholderPlatformEffect.cs @@ -8,6 +8,7 @@ [assembly: ExportEffect(typeof(PlaceholderPlatformEffect), nameof(Placeholder))] namespace AiForms.Effects.iOS { + [Foundation.Preserve(AllMembers = true)] public class PlaceholderPlatformEffect : PlatformEffect { UITextView _textView; diff --git a/AiForms.Effects.iOS/SizeToFitPlatformEffect.cs b/AiForms.Effects.iOS/SizeToFitPlatformEffect.cs index 03774d7..1f11970 100644 --- a/AiForms.Effects.iOS/SizeToFitPlatformEffect.cs +++ b/AiForms.Effects.iOS/SizeToFitPlatformEffect.cs @@ -11,6 +11,7 @@ [assembly: ExportEffect(typeof(SizeToFitPlatformEffect), nameof(SizeToFit))] namespace AiForms.Effects.iOS { + [Foundation.Preserve(AllMembers = true)] public class SizeToFitPlatformEffect : PlatformEffect { UILabel _view; diff --git a/AiForms.Effects.iOS/TouchEffectGestureRecognizer.cs b/AiForms.Effects.iOS/TouchEffectGestureRecognizer.cs index b2438de..b91383d 100644 --- a/AiForms.Effects.iOS/TouchEffectGestureRecognizer.cs +++ b/AiForms.Effects.iOS/TouchEffectGestureRecognizer.cs @@ -3,6 +3,7 @@ namespace AiForms.Effects.iOS { + [Foundation.Preserve(AllMembers = true)] public class TouchEffectGestureRecognizer : UIGestureRecognizer { diff --git a/AiForms.Effects/AiForms.Effects.csproj b/AiForms.Effects/AiForms.Effects.csproj index f00f18e..5441091 100644 --- a/AiForms.Effects/AiForms.Effects.csproj +++ b/AiForms.Effects/AiForms.Effects.csproj @@ -2,10 +2,14 @@ netstandard2.0 + 0.0.5-pre + + + \ No newline at end of file diff --git a/AiForms.Effects/AiRoutingEffectBase.cs b/AiForms.Effects/AiRoutingEffectBase.cs new file mode 100644 index 0000000..01c6201 --- /dev/null +++ b/AiForms.Effects/AiRoutingEffectBase.cs @@ -0,0 +1,14 @@ +using System; +using Xamarin.Forms; + +namespace AiForms.Effects +{ + public class AiRoutingEffectBase : RoutingEffect + { + public string EffectId { get; } + public AiRoutingEffectBase(string effectId) : base(effectId) + { + EffectId = effectId; + } + } +} diff --git a/AiForms.Effects/Feedback.cs b/AiForms.Effects/Feedback.cs new file mode 100644 index 0000000..fa0f159 --- /dev/null +++ b/AiForms.Effects/Feedback.cs @@ -0,0 +1,96 @@ +using System; +using System.Linq; +using Xamarin.Forms; + +namespace AiForms.Effects +{ + public static class Feedback + { + public static readonly BindableProperty OnProperty = + BindableProperty.CreateAttached( + propertyName: "On", + returnType: typeof(bool), + declaringType: typeof(Feedback), + defaultValue: false, + propertyChanged: OnOffChanged + ); + + public static void SetOn(BindableObject view, bool value) + { + view.SetValue(OnProperty, value); + } + + public static bool GetOn(BindableObject view) + { + return (bool)view.GetValue(OnProperty); + } + + public static readonly BindableProperty EffectColorProperty = + BindableProperty.CreateAttached( + "EffectColor", + typeof(Color), + typeof(Feedback), + Color.Transparent, + propertyChanged: OnPropertyChanged + ); + + public static void SetEffectColor(BindableObject view, Color value) + { + view.SetValue(EffectColorProperty, value); + } + + public static Color GetEffectColor(BindableObject view) + { + return (Color)view.GetValue(EffectColorProperty); + } + + public static readonly BindableProperty EnableSoundProperty = + BindableProperty.CreateAttached( + "EnableSound", + typeof(bool), + typeof(Feedback), + false); + + public static void SetEnableSound(BindableObject view, bool value) + { + view.SetValue(EnableSoundProperty, value); + } + public static bool GetEnableSound(BindableObject view) + { + return (bool)view.GetValue(EnableSoundProperty); + } + + static void OnPropertyChanged(BindableObject bindable, object oldValue, object newValue) + { + if (GetOn(bindable)) return; + + SetOn(bindable, true); + } + + static void OnOffChanged(BindableObject bindable, object oldValue, object newValue) + { + var view = bindable as View; + if (view == null) + return; + + if ((bool)newValue) + { + view.Effects.Add(new FeedbackRoutingEffect()); + } + else + { + var toRemove = view.Effects.FirstOrDefault(e => e is FeedbackRoutingEffect); + if (toRemove != null) + view.Effects.Remove(toRemove); + } + } + + class FeedbackRoutingEffect : AiRoutingEffectBase + { + public FeedbackRoutingEffect() : base("AiForms." + nameof(Feedback)) + { + } + } + + } +} diff --git a/Tests/AiEffects.TestApp/AiEffects.TestApp.Droid/AiEffects.TestApp.Droid.csproj b/Tests/AiEffects.TestApp/AiEffects.TestApp.Droid/AiEffects.TestApp.Droid.csproj index 58fa336..9e8f691 100644 --- a/Tests/AiEffects.TestApp/AiEffects.TestApp.Droid/AiEffects.TestApp.Droid.csproj +++ b/Tests/AiEffects.TestApp/AiEffects.TestApp.Droid/AiEffects.TestApp.Droid.csproj @@ -9,7 +9,7 @@ Library AiEffects.TestApp.Droid AiEffects.TestApp.Droid - v8.1 + v9.0 True Resources\Resource.designer.cs Resource @@ -17,6 +17,7 @@ Assets true Properties\AndroidManifest.xml + 0.0.5-pre true @@ -28,7 +29,7 @@ 4 None arm64-v8a;armeabi;armeabi-v7a;x86 - true + 4G true diff --git a/Tests/AiEffects.TestApp/AiEffects.TestApp.iOS/AiEffects.TestApp.iOS.csproj b/Tests/AiEffects.TestApp/AiEffects.TestApp.iOS/AiEffects.TestApp.iOS.csproj index a81ef7a..840710d 100644 --- a/Tests/AiEffects.TestApp/AiEffects.TestApp.iOS/AiEffects.TestApp.iOS.csproj +++ b/Tests/AiEffects.TestApp/AiEffects.TestApp.iOS/AiEffects.TestApp.iOS.csproj @@ -10,6 +10,7 @@ AiEffects.TestApp.iOS AiEffects.Sample.iOS Resources + 0.0.5-pre true diff --git a/Tests/AiEffects.TestApp/AiEffects.TestApp/AiEffects.TestApp.csproj b/Tests/AiEffects.TestApp/AiEffects.TestApp/AiEffects.TestApp.csproj index 851f2d2..17e2893 100644 --- a/Tests/AiEffects.TestApp/AiEffects.TestApp/AiEffects.TestApp.csproj +++ b/Tests/AiEffects.TestApp/AiEffects.TestApp/AiEffects.TestApp.csproj @@ -2,6 +2,7 @@ netstandard2.0 + 0.0.5-pre diff --git a/Tests/AiEffects.TestApp/AiEffects.TestApp/Views/AddNumberPickerPage.xaml b/Tests/AiEffects.TestApp/AiEffects.TestApp/Views/AddNumberPickerPage.xaml index 13e3cfd..707cd80 100644 --- a/Tests/AiEffects.TestApp/AiEffects.TestApp/Views/AddNumberPickerPage.xaml +++ b/Tests/AiEffects.TestApp/AiEffects.TestApp/Views/AddNumberPickerPage.xaml @@ -12,21 +12,27 @@ ef:AddNumberPicker.Min="10" ef:AddNumberPicker.Max="999" ef:AddNumberPicker.Number="{Binding LabelNumber}" - ef:AddNumberPicker.Title="数値を選択" /> + ef:AddNumberPicker.Title="数値を選択" + ef:Feedback.EffectColor="#00FF00" + ef:Feedback.EnableSound="true" /> + ef:AddNumberPicker.Command="{Binding SelectedCommand}" + ef:Feedback.EffectColor="#FF0000" + ef:Feedback.EnableSound="true" /> + ef:AddNumberPicker.Command="{Binding SelectedCommand}" + ef:Feedback.EffectColor="#FF0000" + ef:Feedback.EnableSound="true" >