#if __IOS__
RunningApp.Tap(x => x.Marked("Increment"));
#else
- RunningApp.Tap(x => x.Text("+"));
+ RunningApp.Tap("+");
#endif
+
+ RunningApp.WaitForElement(q => q.Marked("Stepper value is 0"));
+
+
+#if __IOS__
+ RunningApp.Tap(x => x.Marked("Decrement"));
+#else
+ RunningApp.Tap("−");
+#endif
+
RunningApp.WaitForElement(q => q.Marked("Stepper value is 0"));
}
#endif
<Label Text="Custom (Disabled)" Margin="0,0,0,-10" />
<Slider Minimum="-100" Maximum="100" IsEnabled="false"
ThumbColor="{StaticResource DarkRedColor}" MaximumTrackColor="{StaticResource SecondaryColor}" MinimumTrackColor="{StaticResource PrimaryColor}" />
-
+
+ <Label Text="Steppers" FontSize="Large" />
+ <Label Text="Default" Margin="0,0,0,-10" />
+ <Stepper HorizontalOptions="Start" />
+
+ <Label Text="Height 100" Margin="0,0,0,-10" />
+ <Stepper HeightRequest="100"></Stepper>
+
+ <Label Text="Red background" Margin="0,0,0,-10" />
+ <Stepper BackgroundColor="Red"></Stepper>
+
</StackLayout>
</ScrollView>
-</ContentPage>
\ No newline at end of file
+</ContentPage>
--- /dev/null
+using System;
+using System.ComponentModel;
+using CoreGraphics;
+using MaterialComponents;
+using UIKit;
+using Xamarin.Forms;
+using MButton = MaterialComponents.Button;
+
+[assembly: ExportRenderer(typeof(Xamarin.Forms.Stepper), typeof(Xamarin.Forms.Platform.iOS.Material.MaterialStepperRenderer), new[] { typeof(VisualRendererMarker.Material) })]
+
+namespace Xamarin.Forms.Platform.iOS.Material
+{
+ public class MaterialStepperRenderer : ViewRenderer<Stepper, MaterialStepper>
+ {
+ ButtonScheme _buttonScheme;
+
+ public MaterialStepperRenderer()
+ {
+ VisualElement.VerifyVisualFlagEnabled();
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (Control is MaterialStepper control)
+ {
+ control.DecrementButton.TouchUpInside -= OnStep;
+ control.IncrementButton.TouchUpInside -= OnStep;
+ }
+
+ _buttonScheme?.Dispose();
+ _buttonScheme = null;
+
+ base.Dispose(disposing);
+ }
+
+ protected override void OnElementChanged(ElementChangedEventArgs<Stepper> e)
+ {
+ _buttonScheme?.Dispose();
+ _buttonScheme = CreateButtonScheme();
+
+ base.OnElementChanged(e);
+
+ if (e.NewElement != null)
+ {
+ if (Control == null)
+ {
+ var stepper = CreateNativeControl();
+ stepper.DecrementButton.TouchUpInside += OnStep;
+ stepper.IncrementButton.TouchUpInside += OnStep;
+ SetNativeControl(stepper);
+ }
+
+ UpdateButtons();
+ ApplyTheme();
+ }
+ }
+
+ protected virtual ButtonScheme CreateButtonScheme()
+ {
+ return new ButtonScheme
+ {
+ ColorScheme = MaterialColors.Light.CreateColorScheme(),
+ ShapeScheme = new ShapeScheme(),
+ TypographyScheme = new TypographyScheme(),
+ };
+ }
+
+ protected virtual void ApplyTheme()
+ {
+ OutlinedButtonThemer.ApplyScheme(_buttonScheme, Control.DecrementButton);
+ OutlinedButtonThemer.ApplyScheme(_buttonScheme, Control.IncrementButton);
+ }
+
+ protected override MaterialStepper CreateNativeControl()
+ {
+ return new MaterialStepper();
+ }
+
+ protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
+ {
+ base.OnElementPropertyChanged(sender, e);
+
+ if (e.IsOneOf(Stepper.MinimumProperty, Stepper.MaximumProperty, Stepper.ValueProperty, VisualElement.IsEnabledProperty))
+ {
+ UpdateButtons();
+ }
+ }
+
+ protected override void SetBackgroundColor(Color color)
+ {
+ // don't call base
+ }
+
+ void UpdateButtons()
+ {
+ if (Element is Stepper stepper && Control is MaterialStepper control)
+ {
+ control.DecrementButton.Enabled = stepper.IsEnabled && stepper.Value > stepper.Minimum;
+ control.IncrementButton.Enabled = stepper.IsEnabled && stepper.Value < stepper.Maximum;
+ }
+ }
+
+ void OnStep(object sender, EventArgs e)
+ {
+ if (Element is Stepper stepper && sender is MButton button)
+ {
+ var increment = stepper.Increment;
+ if (button == Control.DecrementButton)
+ increment = -increment;
+
+ stepper.SetValueFromRenderer(Stepper.ValueProperty, stepper.Value + increment);
+ }
+ }
+ }
+
+ public class MaterialStepper : UIView
+ {
+ const int DefaultButtonSpacing = 4;
+
+ public MaterialStepper()
+ {
+ DecrementButton = new MButton();
+ DecrementButton.SetTitle("-", UIControlState.Normal);
+
+ IncrementButton = new MButton();
+ IncrementButton.SetTitle("+", UIControlState.Normal);
+
+ AddSubviews(DecrementButton, IncrementButton);
+ }
+
+ public MButton DecrementButton { get; }
+
+ public MButton IncrementButton { get; }
+
+ public override CGSize SizeThatFits(CGSize size)
+ {
+ var dec = DecrementButton.SizeThatFits(CGSize.Empty);
+ var inc = IncrementButton.SizeThatFits(CGSize.Empty);
+ var btn = new CGSize(
+ Math.Max(dec.Width, inc.Width),
+ Math.Max(dec.Height, inc.Height));
+ return new CGSize(btn.Width * 2 + DefaultButtonSpacing, btn.Height);
+ }
+
+ public override void LayoutSubviews()
+ {
+ base.LayoutSubviews();
+
+ var btn = new CGSize((Bounds.Width - DefaultButtonSpacing) / 2, Bounds.Height);
+ DecrementButton.Frame = new CGRect(0, 0, btn.Width, btn.Height);
+ IncrementButton.Frame = new CGRect(btn.Width + DefaultButtonSpacing, 0, btn.Width, btn.Height);
+ }
+ }
+}
<Compile Include="MaterialFrameRenderer.cs" />
<Compile Include="MaterialProgressBarRenderer.cs" />
<Compile Include="MaterialSliderRenderer.cs" />
+ <Compile Include="MaterialStepperRenderer.cs" />
<Compile Include="FormsMaterial.cs" />
</ItemGroup>
<ItemGroup>
--- /dev/null
+using AButton = Android.Widget.Button;
+
+namespace Xamarin.Forms.Platform.Android
+{
+ public interface IStepperRenderer
+ {
+ Stepper Element { get; }
+
+ AButton UpButton { get; }
+
+ AButton DownButton { get; }
+
+ AButton CreateButton();
+ }
+}
--- /dev/null
+#if __ANDROID_28__
+using System.ComponentModel;
+using Android.Content;
+using Android.Views;
+using Android.Widget;
+using Xamarin.Forms;
+using Xamarin.Forms.Platform.Android.Material;
+using AButton = Android.Widget.Button;
+using MButton = Android.Support.Design.Button.MaterialButton;
+
+[assembly: ExportRenderer(typeof(Xamarin.Forms.Stepper), typeof(MaterialStepperRenderer), new[] { typeof(VisualRendererMarker.Material) })]
+
+namespace Xamarin.Forms.Platform.Android.Material
+{
+ public class MaterialStepperRenderer : ViewRenderer<Stepper, LinearLayout>, IStepperRenderer
+ {
+ const int DefaultButtonSpacing = 4;
+
+ MButton _downButton;
+ MButton _upButton;
+
+ public MaterialStepperRenderer(Context context) : base(context)
+ {
+ VisualElement.VerifyVisualFlagEnabled();
+
+ AutoPackage = false;
+ }
+
+ protected override LinearLayout CreateNativeControl()
+ {
+ return new LinearLayout(Context)
+ {
+ Orientation = Orientation.Horizontal,
+ Focusable = true,
+ DescendantFocusability = DescendantFocusability.AfterDescendants
+ };
+ }
+
+ protected override void OnElementChanged(ElementChangedEventArgs<Stepper> e)
+ {
+ base.OnElementChanged(e);
+
+ if (e.OldElement == null)
+ {
+ if (Control == null)
+ {
+ var layout = CreateNativeControl();
+ StepperRendererManager.CreateStepperButtons(this, out _downButton, out _upButton);
+ layout.AddView(_downButton, new LinearLayout.LayoutParams(LayoutParams.WrapContent, LayoutParams.MatchParent)
+ {
+ Weight = 1,
+ RightMargin = (int)(Context.ToPixels(DefaultButtonSpacing) / 2),
+ });
+ layout.AddView(_upButton, new LinearLayout.LayoutParams(LayoutParams.WrapContent, LayoutParams.MatchParent)
+ {
+ Weight = 1,
+ LeftMargin = (int)(Context.ToPixels(DefaultButtonSpacing) / 2),
+ });
+
+ SetNativeControl(layout);
+ }
+ }
+
+ StepperRendererManager.UpdateButtons(this, _downButton, _upButton);
+ }
+
+ protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
+ {
+ base.OnElementPropertyChanged(sender, e);
+
+ StepperRendererManager.UpdateButtons(this, _downButton, _upButton, e);
+ }
+
+ protected override void UpdateBackgroundColor()
+ {
+ // don't call base
+ }
+
+ // IStepperRenderer
+
+ Stepper IStepperRenderer.Element => Element;
+
+ AButton IStepperRenderer.UpButton => _upButton;
+
+ AButton IStepperRenderer.DownButton => _downButton;
+
+ AButton IStepperRenderer.CreateButton()
+ {
+ var button = new MButton(MaterialContextThemeWrapper.Create(Context), null, Resource.Attribute.materialOutlinedButtonStyle);
+
+ // the buttons are meant to be "square", but are usually wide,
+ // so, copy the vertical properties into the horizontal properties
+ button.SetMinimumWidth(button.MinimumHeight);
+ button.SetMinWidth(button.MinHeight);
+ button.SetPadding(button.PaddingTop, button.PaddingTop, button.PaddingBottom, button.PaddingBottom);
+
+ return button;
+ }
+ }
+}
+#endif
using System;
using System.ComponentModel;
using Android.Content;
-using Android.Views;
using Android.Widget;
+using Android.Views;
using AButton = Android.Widget.Button;
-using Object = Java.Lang.Object;
namespace Xamarin.Forms.Platform.Android
{
- public class StepperRenderer : ViewRenderer<Stepper, LinearLayout>
+ public class StepperRenderer : ViewRenderer<Stepper, LinearLayout>, IStepperRenderer
{
AButton _downButton;
AButton _upButton;
protected override LinearLayout CreateNativeControl()
{
- return new LinearLayout(Context) { Orientation = Orientation.Horizontal };
+ return new LinearLayout(Context)
+ {
+ Orientation = Orientation.Horizontal,
+ Focusable = true,
+ DescendantFocusability = DescendantFocusability.AfterDescendants
+ };
}
protected override void OnElementChanged(ElementChangedEventArgs<Stepper> e)
if (e.OldElement == null)
{
- _downButton = new AButton(Context) { Text = "-", Gravity = GravityFlags.Center, Tag = this };
- _downButton.SetHeight((int)Context.ToPixels(10.0));
-
- _downButton.SetOnClickListener(StepperListener.Instance);
-
- _upButton = new AButton(Context) { Text = "+", Tag = this };
-
- _upButton.SetOnClickListener(StepperListener.Instance);
- _upButton.SetHeight((int)Context.ToPixels(10.0));
-
var layout = CreateNativeControl();
-
- layout.AddView(_downButton);
- layout.AddView(_upButton);
-
+ StepperRendererManager.CreateStepperButtons(this, out _downButton, out _upButton);
+ layout.AddView(_downButton, new LinearLayout.LayoutParams(LayoutParams.WrapContent, LayoutParams.MatchParent));
+ layout.AddView(_upButton, new LinearLayout.LayoutParams(LayoutParams.WrapContent, LayoutParams.MatchParent));
SetNativeControl(layout);
}
- UpdateButtonEnabled();
+ StepperRendererManager.UpdateButtons(this, _downButton, _upButton);
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
-
- switch (e.PropertyName)
- {
- case "Minimum":
- UpdateButtonEnabled();
- break;
- case "Maximum":
- UpdateButtonEnabled();
- break;
- case "Value":
- UpdateButtonEnabled();
- break;
- case "IsEnabled":
- UpdateButtonEnabled();
- break;
- }
+ StepperRendererManager.UpdateButtons(this, _downButton, _upButton, e);
}
- void UpdateButtonEnabled()
- {
- Stepper view = Element;
- _upButton.Enabled = view.IsEnabled ? view.Value < view.Maximum : view.IsEnabled;
- _downButton.Enabled = view.IsEnabled ? view.Value > view.Minimum : view.IsEnabled;
- }
+ Stepper IStepperRenderer.Element => Element;
- class StepperListener : Object, IOnClickListener
- {
- public static readonly StepperListener Instance = new StepperListener();
+ AButton IStepperRenderer.UpButton => _upButton;
- public void OnClick(global::Android.Views.View v)
- {
- var renderer = v.Tag as StepperRenderer;
- if (renderer == null)
- return;
-
- Stepper stepper = renderer.Element;
- if (stepper == null)
- return;
+ AButton IStepperRenderer.DownButton => _downButton;
- if (v == renderer._upButton)
- ((IElementController)stepper).SetValueFromRenderer(Stepper.ValueProperty, stepper.Value + stepper.Increment);
- else if (v == renderer._downButton)
- ((IElementController)stepper).SetValueFromRenderer(Stepper.ValueProperty, stepper.Value - stepper.Increment);
- }
+ AButton IStepperRenderer.CreateButton()
+ {
+ var button = new AButton(Context);
+ button.SetHeight((int)Context.ToPixels(10.0));
+ return button;
}
}
-}
\ No newline at end of file
+}
<?xml version="1.0" encoding="UTF-8" ?>
<resources>
+ <attr name="materialOutlinedButtonStyle" format="reference"/>
+
<!-- Material Base Theme -->
<style name="XamarinFormsMaterialTheme" parent="Theme.MaterialComponents.Light.DarkActionBar">
<item name="materialButtonStyle">@style/XamarinFormsMaterialButton</item>
+ <item name="materialOutlinedButtonStyle">@style/XamarinFormsMaterialButtonOutlined</item>
</style>
<!-- Material Sliders -->
<style name="XamarinFormsMaterialProgressBarCircular" parent="Widget.AppCompat.ProgressBar">
<item name="android:background">@drawable/MaterialActivityIndicatorBackground</item>
</style>
-
<!-- Material Buttons (All Styles) -->
<style name="XamarinFormsMaterialButton" parent="Widget.MaterialComponents.Button">
<item name="android:insetTop">0dp</item>
<item name="android:paddingTop">8dp</item>
<item name="android:paddingBottom">8dp</item>
</style>
+ <style name="XamarinFormsMaterialButtonOutlined" parent="Widget.MaterialComponents.Button.OutlinedButton">
+ <item name="android:insetTop">0dp</item>
+ <item name="android:insetBottom">0dp</item>
+ <item name="android:minHeight">36dp</item>
+ <item name="android:paddingTop">8dp</item>
+ <item name="android:paddingBottom">8dp</item>
+ </style>
<style name="XamarinFormsMaterialEntryFilled" parent="Widget.MaterialComponents.TextInputLayout.FilledBox">
<item name="boxCollapsedPaddingTop">8dp</item>
</style>
-
</resources>
--- /dev/null
+using System.ComponentModel;
+using Android.Views;
+using AButton = Android.Widget.Button;
+using AView = Android.Views.View;
+
+namespace Xamarin.Forms.Platform.Android
+{
+ public static class StepperRendererManager
+ {
+ public static void CreateStepperButtons<TButton>(IStepperRenderer renderer, out TButton downButton, out TButton upButton)
+ where TButton : AButton
+ {
+ downButton = (TButton)renderer.CreateButton();
+ downButton.Id = Platform.GenerateViewId();
+ downButton.Focusable = true;
+ upButton = (TButton)renderer.CreateButton();
+ upButton.Id = Platform.GenerateViewId();
+ upButton.Focusable = true;
+
+ downButton.Gravity = GravityFlags.Center;
+ downButton.Tag = renderer as Java.Lang.Object;
+ downButton.SetOnClickListener(StepperListener.Instance);
+ upButton.Gravity = GravityFlags.Center;
+ upButton.Tag = renderer as Java.Lang.Object;
+ upButton.SetOnClickListener(StepperListener.Instance);
+
+ // IMPORTANT:
+ // Do not be decieved. These are NOT the same characters. Neither are a "minus" either.
+ // The Text is a visually pleasing "minus", and the description is the phonetically correct "minus".
+ // The little key on your keyboard is a dash/hyphen.
+ downButton.Text = "-";
+ downButton.ContentDescription = "−";
+
+ // IMPORTANT:
+ // Do not be decieved. These are NOT the same characters.
+ // The Text is a visually pleasing "plus", and the description is the phonetically correct "plus"
+ // (which, unlike the minus, IS found on your keyboard).
+ upButton.Text = "+";
+ upButton.ContentDescription = "+";
+
+ downButton.NextFocusForwardId = upButton.Id;
+ }
+
+ public static void UpdateButtons<TButton>(IStepperRenderer renderer, TButton downButton, TButton upButton, PropertyChangedEventArgs e = null)
+ where TButton : AButton
+ {
+ if (!(renderer?.Element is Stepper stepper))
+ return;
+
+ // NOTE: a value of `null` means that we are forcing an update
+ if (e == null ||
+ e.IsOneOf(Stepper.MinimumProperty, Stepper.MaximumProperty, Stepper.ValueProperty, VisualElement.IsEnabledProperty))
+ {
+ downButton.Enabled = stepper.IsEnabled && stepper.Value > stepper.Minimum;
+ upButton.Enabled = stepper.IsEnabled && stepper.Value < stepper.Maximum;
+ }
+ }
+
+ class StepperListener : Java.Lang.Object, AView.IOnClickListener
+ {
+ public static readonly StepperListener Instance = new StepperListener();
+
+ public void OnClick(AView v)
+ {
+ if (!(v?.Tag is IStepperRenderer renderer))
+ return;
+
+ if (!(renderer?.Element is Stepper stepper))
+ return;
+
+ var increment = stepper.Increment;
+ if (v == renderer.DownButton)
+ increment = -increment;
+
+ ((IElementController)stepper).SetValueFromRenderer(Stepper.ValueProperty, stepper.Value + increment);
+ }
+ }
+ }
+}
<Compile Include="ButtonLayoutManager.cs" />
<Compile Include="Material\MaterialSliderRenderer.cs" />
<AndroidResource Include="Resources\color\white_disabled_material.xml" />
+ <Compile Include="IStepperRenderer.cs" />
+ <Compile Include="StepperRendererManager.cs" />
+ <Compile Include="Material\MaterialStepperRenderer.cs" />
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.