[Android iOS Visual] fixes background in ActivityIndicator (#5283)
authorPavel Yakovlev <v-payako@microsoft.com>
Sat, 23 Feb 2019 09:30:47 +0000 (12:30 +0300)
committerShane Neuville <shane94@hotmail.com>
Sat, 23 Feb 2019 09:30:47 +0000 (02:30 -0700)
* [Android, iOS Visual] fixes background in AcrivityIndicator

* [Android] reduce nesting layouts

* [Android] simplification

* Some calculations for iOS

* [Android] removed padding, added size limit
[iOS] fixes stroke width

* [iOS] default size as in Android

* Use the Android ratio of 10 instead of the spec of 12

* [Android] fix API23

* [iOS] fix unexpected padding

* [Android] fixes padding in API 23 -- IsRunning -- added gallery

* [Android] fixes start with IsRunning = false

* [Android] again fix isRunning

* address comments

Xamarin.Forms.Controls/ControlGalleryPages/VisualGallery.xaml
Xamarin.Forms.Controls/CoreGallery.cs
Xamarin.Forms.Controls/GalleryPages/MaterialActivityIndicatorGallery.cs [new file with mode: 0644]
Xamarin.Forms.Controls/GalleryPages/MaterialProgressBarGallery.cs
Xamarin.Forms.Material.iOS/MaterialActivityIndicatorRenderer.cs
Xamarin.Forms.Platform.Android/Material/MaterialActivityIndicatorRenderer.cs
Xamarin.Forms.Platform.Android/Renderers/CircularProgress.cs [new file with mode: 0644]
Xamarin.Forms.Platform.Android/Resources/drawable-v24/MaterialActivityIndicatorBackground.xml [new file with mode: 0644]
Xamarin.Forms.Platform.Android/Resources/drawable/MaterialActivityIndicatorBackground.xml
Xamarin.Forms.Platform.Android/Xamarin.Forms.Platform.Android.csproj

index e644fc5..983d5d2 100644 (file)
                 <ActivityIndicator Color="{StaticResource PrimaryColor}" IsRunning="false" HorizontalOptions="FillAndExpand" />
             </StackLayout>
 
-            <Label Text="Custom Background Color" Margin="0,0,0,-10" />
+            <Label Text="Custom Background Color + Height 200" Margin="0,0,0,-10" />
             <StackLayout Orientation="Horizontal">
-                <ActivityIndicator BackgroundColor="{StaticResource SecondaryColor}" IsRunning="true" HorizontalOptions="FillAndExpand" />
-                <ActivityIndicator BackgroundColor="{StaticResource SecondaryColor}" IsRunning="false" HorizontalOptions="FillAndExpand" />
+                <ActivityIndicator HeightRequest="200" BackgroundColor="{StaticResource SecondaryColor}" IsRunning="true" HorizontalOptions="FillAndExpand" />
+                <ActivityIndicator HeightRequest="200" BackgroundColor="{StaticResource SecondaryColor}" IsRunning="false" HorizontalOptions="FillAndExpand" />
             </StackLayout>
 
-            <Label Text="Custom" Margin="0,0,0,-10" />
-            <StackLayout Orientation="Horizontal">
+            <Label Text="Custom + Height 144 (Max)" Margin="0,0,0,-10" />
+            <StackLayout Orientation="Horizontal" BackgroundColor="Lime">
+                <ActivityIndicator HeightRequest="144" Color="{StaticResource PrimaryColor}" BackgroundColor="{StaticResource SecondaryColor}" IsRunning="true" HorizontalOptions="FillAndExpand" />
+                <ActivityIndicator HeightRequest="144" Color="{StaticResource PrimaryColor}" BackgroundColor="{StaticResource SecondaryColor}" IsRunning="false" HorizontalOptions="FillAndExpand" />
+            </StackLayout>
+
+            <Label Text="Default size" Margin="0,0,0,-10" />
+            <StackLayout Orientation="Horizontal" BackgroundColor="Yellow">
                 <ActivityIndicator Color="{StaticResource PrimaryColor}" BackgroundColor="{StaticResource SecondaryColor}" IsRunning="true" HorizontalOptions="FillAndExpand" />
                 <ActivityIndicator Color="{StaticResource PrimaryColor}" BackgroundColor="{StaticResource SecondaryColor}" IsRunning="false" HorizontalOptions="FillAndExpand" />
             </StackLayout>
 
-
             <Label Text="Progress Bars" FontSize="Large" />
 
             <Label Text="Animating" Margin="0,0,0,-10" />
index ee8eb97..07af2a2 100644 (file)
@@ -322,6 +322,7 @@ namespace Xamarin.Forms.Controls
                                new GalleryPageFactory(() => new PickerCoreGalleryPage(), "Picker Gallery"),
                                new GalleryPageFactory(() => new ProgressBarCoreGalleryPage(), "ProgressBar Gallery"),
                                new GalleryPageFactory(() => new MaterialProgressBarGallery(), "[Material] ProgressBar & Slider Gallery"),
+                               new GalleryPageFactory(() => new MaterialActivityIndicatorGallery(), "[Material] ActivityIndicator Gallery"),
                                new GalleryPageFactory(() => new ScrollGallery(), "ScrollView Gallery"),
                                new GalleryPageFactory(() => new ScrollGallery(ScrollOrientation.Horizontal), "ScrollView Gallery Horizontal"),
                                new GalleryPageFactory(() => new ScrollGallery(ScrollOrientation.Both), "ScrollView Gallery 2D"),
diff --git a/Xamarin.Forms.Controls/GalleryPages/MaterialActivityIndicatorGallery.cs b/Xamarin.Forms.Controls/GalleryPages/MaterialActivityIndicatorGallery.cs
new file mode 100644 (file)
index 0000000..80ee304
--- /dev/null
@@ -0,0 +1,92 @@
+namespace Xamarin.Forms.Controls
+{
+       public class MaterialActivityIndicatorGallery : ContentPage
+       {
+               public MaterialActivityIndicatorGallery()
+               {
+                       Visual = VisualMarker.Material;
+
+                       var activityIndicator = new ActivityIndicator()
+                       {
+                               IsRunning = false,
+                               BackgroundColor = Color.Red,
+                               HeightRequest = 50
+                       };
+
+                       var IsRunGrid = new Grid
+                       {
+                               Padding = 0,
+                               ColumnSpacing = 6,
+                               RowSpacing = 6,
+                               ColumnDefinitions =
+                               {
+                                       new ColumnDefinition { Width = GridLength.Star },
+                                       new ColumnDefinition { Width = 50 }
+                               }
+                       };
+
+                       IsRunGrid.AddChild(new Label { Text = "Is Running" }, 0, 0);
+                       var isRunSwitch = new Switch {
+                               IsToggled = activityIndicator.IsRunning,
+                               HorizontalOptions = LayoutOptions.Center,
+                               VerticalOptions = LayoutOptions.Center
+                       };
+                       isRunSwitch.Toggled += (_, e) => activityIndicator.IsRunning = e.Value;
+                       IsRunGrid.AddChild(isRunSwitch, 1, 0);
+
+                       var primaryPicker = new ColorPicker { Title = "Primary Color", Color = activityIndicator.Color };
+                       primaryPicker.ColorPicked += (_, e) =>
+                       {
+                               activityIndicator.Color = e.Color;
+                       };
+                       var backgroundPicker = new ColorPicker { Title = "Background Color", Color = activityIndicator.BackgroundColor };
+                       backgroundPicker.ColorPicked += (_, e) => activityIndicator.BackgroundColor = e.Color;
+                       
+                       var heightPicker = MaterialProgressBarGallery.CreateValuePicker("Height", value => activityIndicator.HeightRequest = value);
+
+                       var content = new StackLayout
+                       {
+                               Children = { activityIndicator },
+                               BackgroundColor = Color.Blue
+                       };
+
+                       var backgroundPanelPicker = new ColorPicker { Title = "Back panel Color", Color = content.BackgroundColor };
+                       backgroundPanelPicker.ColorPicked += (_, e) => content.BackgroundColor = e.Color;
+
+                       Content = new StackLayout
+                       {
+                               Padding = 10,
+                               Spacing = 10,
+                               Children =
+                               {
+                                       new ScrollView
+                                       {
+                                               Margin = new Thickness(-10, 0),
+                                               Content = new StackLayout
+                                               {
+                                                       Padding = 10,
+                                                       Spacing = 10,
+                                                       Children =
+                                                       {
+                                                               IsRunGrid,
+                                                               primaryPicker,
+                                                               backgroundPicker,
+                                                               backgroundPanelPicker,
+                                                               heightPicker,
+                                                       }
+                                               }
+                                       },
+
+                                       new BoxView
+                                       {
+                                               HeightRequest = 1,
+                                               Margin = new Thickness(-10, 0),
+                                               Color = Color.Black
+                                       },
+
+                                       content
+                               }
+                       };
+               }
+       }
+}
index a2690c0..e76a39e 100644 (file)
@@ -77,7 +77,7 @@ namespace Xamarin.Forms.Controls
                        };
                }
 
-               Grid CreateValuePicker(string title, Action<double> changed)
+               internal static Grid CreateValuePicker(string title, Action<double> changed)
                {
                        // 50%
                        Slider slider = new Slider(0, 100, 50);
index 80a5940..e3af54f 100644 (file)
@@ -1,3 +1,4 @@
+using System;
 using System.ComponentModel;
 using CoreAnimation;
 using CoreGraphics;
@@ -11,11 +12,17 @@ namespace Xamarin.Forms.Platform.iOS.Material
 {
        public class MaterialActivityIndicatorRenderer : ViewRenderer<ActivityIndicator, MActivityIndicator>
        {
+               // by Material spec the stroke width is 1/12 of the diameter, 
+               // but Android's native progress indicator is 1/10 of the diameter.
+               const float _strokeRatio = 10;
+               const float _defaultRadius = 22;
+               const float _defaultStrokeWidth = 4;
+               const float _defaultSize = 2 * _defaultRadius + _defaultStrokeWidth;
+
                SemanticColorScheme _defaultColorScheme;
                SemanticColorScheme _colorScheme;
 
                CAShapeLayer _backgroundLayer;
-               CGPoint _center;
 
                public MaterialActivityIndicatorRenderer()
                {
@@ -39,7 +46,7 @@ namespace Xamarin.Forms.Platform.iOS.Material
 
                                        _backgroundLayer = new CAShapeLayer
                                        {
-                                               LineWidth = 4,
+                                               LineWidth = Control.StrokeWidth,
                                                FillColor = UIColor.Clear.CGColor,
                                                Hidden = true
                                        };
@@ -48,6 +55,7 @@ namespace Xamarin.Forms.Platform.iOS.Material
 
                                UpdateColor();
                                UpdateIsRunning();
+                               SetBackgroundColor(Element.BackgroundColor);
 
                                ApplyTheme();
                        }
@@ -68,8 +76,8 @@ namespace Xamarin.Forms.Platform.iOS.Material
                        return new MActivityIndicator
                        {
                                IndicatorMode = ActivityIndicatorMode.Indeterminate,
-                               StrokeWidth = 4,
-                               Radius = 24
+                               StrokeWidth = _defaultStrokeWidth,
+                               Radius = _defaultRadius
                        };
                }
 
@@ -77,12 +85,28 @@ namespace Xamarin.Forms.Platform.iOS.Material
                {
                        base.LayoutSubviews();
 
-                       if (_center != Control.Center)
-                       {
-                               _center = Control.Center;
-                               _backgroundLayer.Path = UIBezierPath.FromArc(_center, Control.Radius - 2, 0, 360, true).CGPath;
-                       }
-                       SetBackgroundColor(Element.BackgroundColor);
+                       // try get the radius for this size
+                       var min = NMath.Min(Control.Bounds.Width, Control.Bounds.Height);
+                       var stroke = min / _strokeRatio;
+                       var radius = min / 2;
+
+                       // but, in the end use the limit set by the control
+                       Control.Radius = radius;
+                       Control.StrokeWidth = Control.Radius / (_strokeRatio / 2);
+
+                       _backgroundLayer.LineWidth = Control.StrokeWidth;
+                       _backgroundLayer.Path = UIBezierPath.FromArc(Control.Center, Control.Radius - Control.StrokeWidth / 2, 0, 360, true).CGPath;
+               }
+
+               public override CGSize SizeThatFits(CGSize size)
+               {
+                       if (nfloat.IsInfinity(size.Width))
+                               size.Width = _defaultSize;
+                       if (nfloat.IsInfinity(size.Height))
+                               size.Height = _defaultSize;
+                       var min = NMath.Min(size.Width, size.Height);
+                       size.Width = size.Height = min;
+                       return size;
                }
 
                protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
index 6304c57..d58ad5e 100644 (file)
@@ -2,15 +2,12 @@
 using System;
 using System.ComponentModel;
 using Android.Content;
-using Android.Content.Res;
-using Android.Graphics.Drawables;
 using Android.Support.V4.View;
 using Android.Views;
 using Android.Widget;
 using Xamarin.Forms;
 using Xamarin.Forms.Platform.Android.FastRenderers;
 using Xamarin.Forms.Platform.Android.Material;
-using AColor = Android.Graphics.Color;
 using AProgressBar = Android.Widget.ProgressBar;
 using AView = Android.Views.View;
 
@@ -26,7 +23,7 @@ namespace Xamarin.Forms.Platform.Android.Material
                bool _disposed;
 
                ActivityIndicator _element;
-               AProgressBar _control;
+               CircularProgress _control;
 
                VisualElementTracker _visualElementTracker;
                VisualElementRenderer _visualElementRenderer;
@@ -40,9 +37,15 @@ namespace Xamarin.Forms.Platform.Android.Material
                {
                        VisualElement.VerifyVisualFlagEnabled();
 
-                       _control = new AProgressBar(new ContextThemeWrapper(context, Resource.Style.XamarinFormsMaterialProgressBarCircular), null, Resource.Style.XamarinFormsMaterialProgressBarCircular);
-                       _control.Indeterminate = true;
-                       AddView(_control);
+                       _control = new CircularProgress(new ContextThemeWrapper(context, Resource.Style.XamarinFormsMaterialProgressBarCircular), null, Resource.Style.XamarinFormsMaterialProgressBarCircular)
+                       {
+                               // limiting size to compare iOS realization
+                               // https://github.com/material-components/material-components-ios/blob/develop/components/ActivityIndicator/src/MDCActivityIndicator.m#L425
+                               MinSize = (int)Context.ToPixels(10),
+                               MaxSize = (int)Context.ToPixels(144),
+                               DefaultColor = MaterialColors.Light.PrimaryColor
+                       };
+                       AddView(Control);
 
                        _visualElementRenderer = new VisualElementRenderer(this);
                        _motionEventHelper = new MotionEventHelper();
@@ -113,7 +116,9 @@ namespace Xamarin.Forms.Platform.Android.Material
 
                                e.NewElement.PropertyChanged += OnElementPropertyChanged;
 
-                               UpdateColorsAndRuning();
+                               UpdateColor();
+                               UpdateBackgroundColor();
+                               UpdateIsRunning();
 
                                ElevationHelper.SetElevation(this, e.NewElement);
                        }
@@ -123,8 +128,12 @@ namespace Xamarin.Forms.Platform.Android.Material
                {
                        ElementPropertyChanged?.Invoke(this, e);
 
-                       if (e.IsOneOf(ActivityIndicator.IsRunningProperty, ActivityIndicator.ColorProperty, VisualElement.BackgroundColorProperty))
-                               UpdateColorsAndRuning();
+                       if (e.Is(ActivityIndicator.IsRunningProperty))
+                               UpdateIsRunning();
+                       else if (e.Is(ActivityIndicator.ColorProperty))
+                               UpdateColor();
+                       else if (e.Is(VisualElement.BackgroundColorProperty))
+                               UpdateBackgroundColor();
                }
 
                public override bool OnTouchEvent(MotionEvent e)
@@ -135,30 +144,22 @@ namespace Xamarin.Forms.Platform.Android.Material
                        return _motionEventHelper.HandleMotionEvent(Parent, e);
                }
 
-               void UpdateColorsAndRuning()
+               void UpdateIsRunning()
                {
-                       if (Element == null || Control == null)
-                               return;
+                       if (Element != null && _control != null)
+                               _control.IsRunning = Element.IsRunning;
+               }
 
-                       var background = Element.BackgroundColor.IsDefault 
-                               ? AColor.Transparent 
-                               : Element.BackgroundColor.ToAndroid();
-                       (_control.Background as GradientDrawable)?.SetColor(background);
+               void UpdateColor()
+               {
+                       if (Element != null && _control != null)
+                               _control.SetColor(Element.Color);
+               }
 
-                       if (Element.IsRunning)
-                       {
-                               var progress = Element.Color.IsDefault 
-                                       ? MaterialColors.Light.PrimaryColor 
-                                       : Element.Color.ToAndroid();
-                               _control.IndeterminateTintList = ColorStateList.ValueOf(progress);
-                       }
-                       else
-                       {
-                               _control.Visibility = Element.BackgroundColor.IsDefault 
-                                       ? ViewStates.Gone 
-                                       : ViewStates.Visible;
-                               _control.IndeterminateTintList = ColorStateList.ValueOf(AColor.Transparent);
-                       }
+               void UpdateBackgroundColor()
+               {
+                       if (Element != null && _control != null)
+                               _control.SetBackgroundColor(Element.BackgroundColor);
                }
 
                // IVisualElementRenderer
diff --git a/Xamarin.Forms.Platform.Android/Renderers/CircularProgress.cs b/Xamarin.Forms.Platform.Android/Renderers/CircularProgress.cs
new file mode 100644 (file)
index 0000000..52293cb
--- /dev/null
@@ -0,0 +1,91 @@
+using System;
+using Android.Content;
+using Android.OS;
+using Android.Util;
+using AProgressBar = Android.Widget.ProgressBar;
+using Android.Graphics.Drawables;
+using AColor = Android.Graphics.Color;
+using Android.Content.Res;
+using Android.Views;
+using Android.Graphics;
+
+namespace Xamarin.Forms.Platform.Android
+{
+       internal class CircularProgress : AProgressBar
+       {
+               public int MaxSize { get; set; } = int.MaxValue;
+
+               public int MinSize { get; set; } = 0;
+
+               public AColor DefaultColor { get; set; }
+
+               const int _paddingRatio = 10;
+
+               const int _paddingRatio23 = 14;
+
+               bool _isRunning;
+
+               AColor _backgroudColor;
+
+               public CircularProgress(Context context, IAttributeSet attrs, int defStyleAttr) : base(context, attrs, defStyleAttr)
+               {
+                       Indeterminate = true;
+               }
+
+               public override void Draw(Canvas canvas)
+               {
+                       base.Draw(canvas);
+                       if (_isRunning != IsRunning)
+                               IsRunning = _isRunning;
+               }
+
+               public void SetColor(Color color)
+               {
+                       var progress = color.IsDefault ? DefaultColor : color.ToAndroid();
+                       IndeterminateTintList = ColorStateList.ValueOf(progress);
+               }
+
+               public void SetBackgroundColor(Color color)
+               {
+                       _backgroudColor = color.IsDefault ? AColor.Transparent : color.ToAndroid();
+                       (Background as GradientDrawable)?.SetColor(_backgroudColor);
+               }
+
+               AnimatedVectorDrawable CurrentDrawable => IndeterminateDrawable.Current as AnimatedVectorDrawable;
+
+               public bool IsRunning
+               {
+                       get => CurrentDrawable?.IsRunning ?? false;
+                       set
+                       {
+                               if (CurrentDrawable == null)
+                                       return;
+
+                               _isRunning = value;
+                               if (_isRunning && !CurrentDrawable.IsRunning)
+                                               CurrentDrawable.Start();
+                               else if (CurrentDrawable.IsRunning)
+                                               CurrentDrawable.Stop();
+                               
+                               PostInvalidate();
+                       }
+               }
+
+               public override void Layout(int l, int t, int r, int b)
+               {
+                       var width = r - l;
+                       var height = b - t;
+                       var squareSize = Math.Min(Math.Max(Math.Min(width, height), MinSize), MaxSize);
+                       l += (width - squareSize) / 2;
+                       t += (height - squareSize) / 2;
+                       int strokeWidth;
+                       if (Build.VERSION.SdkInt < BuildVersionCodes.N)
+                               strokeWidth = squareSize / _paddingRatio23;
+                       else
+                               strokeWidth = squareSize / _paddingRatio;
+
+                       squareSize += strokeWidth;
+                       base.Layout(l - strokeWidth, t - strokeWidth, l + squareSize, t + squareSize);
+               }
+       }
+}
\ No newline at end of file
diff --git a/Xamarin.Forms.Platform.Android/Resources/drawable-v24/MaterialActivityIndicatorBackground.xml b/Xamarin.Forms.Platform.Android/Resources/drawable-v24/MaterialActivityIndicatorBackground.xml
new file mode 100644 (file)
index 0000000..72295e8
--- /dev/null
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--  thickness = width / thicknessRatio = 48 / 12 = 4
+      thicknessRatio = width / thickness = 48 / 4 = 12 -->
+<shape
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="ring"
+    android:thicknessRatio="12"
+    android:innerRadiusRatio="3"
+    android:useLevel="false">
+  <solid android:color="#000" />
+</shape>
\ No newline at end of file
index c254b90..e56b905 100644 (file)
@@ -1,10 +1,11 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!-- innerRadius = (diameter / 2) - (thickness * 2) = 16dp -->
+<!-- API23 and less
+magic innerRadiusRatio = 90% pi -->
 <shape
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:shape="ring"
-    android:innerRadius="16dp" 
-    android:thickness="4dp"
+    android:thicknessRatio="12"
+    android:innerRadiusRatio="2.8274333882308139146163790449516"
     android:useLevel="false">
   <solid android:color="#000" />
 </shape>
\ No newline at end of file
index 1c281aa..dce5e7d 100644 (file)
     <Compile Include="Material\MaterialEntryRenderer.cs" />
     <Compile Include="Material\MaterialProgressBarRenderer.cs" />
     <Compile Include="IPickerRenderer.cs" />
+    <Compile Include="Renderers\CircularProgress.cs" />
     <Compile Include="Renderers\PickerEditText.cs" />
     <Compile Include="Renderers\FontImageSourceHandler.cs" />
     <Compile Include="Renderers\FormsWebViewClient.cs" />
       <SubType>Designer</SubType>
     </AndroidResource>
   </ItemGroup>
-  <ItemGroup />
   <ItemGroup>
     <AndroidResource Include="Resources\drawable\MaterialActivityIndicatorBackground.xml">
+      <Generator>MSBuild:UpdateGeneratedFiles</Generator>
       <SubType>Designer</SubType>
     </AndroidResource>
   </ItemGroup>
   <ItemGroup>
-    <AndroidResource Include="Resources\drawable\MaterialProgressBar.xml">
+    <AndroidResource Include="Resources\drawable-v24\MaterialActivityIndicatorBackground.xml">
       <SubType>Designer</SubType>
     </AndroidResource>
   </ItemGroup>
   <ItemGroup>
-    <Folder Include="Resources\color\" />
+    <AndroidResource Include="Resources\drawable\MaterialProgressBar.xml">
+      <SubType>Designer</SubType>
+    </AndroidResource>
   </ItemGroup>
+  <ItemGroup />
   <Target Name="BeforeBuild" Condition=" '$(CreateAllAndroidTargets)' == 'true' ">
     <!--  create 8.1 binaries-->
     <MSBuild Targets="Restore" Projects="@(ProjectToBuild)">