diff --git a/Pixiview.Android/Pixiview.Android.csproj b/Pixiview.Android/Pixiview.Android.csproj index fc31ec3..c0e89aa 100644 --- a/Pixiview.Android/Pixiview.Android.csproj +++ b/Pixiview.Android/Pixiview.Android.csproj @@ -34,14 +34,15 @@ None + true bin\Release prompt 4 true false -true -arm64-v8a + true + arm64-v8a @@ -70,6 +71,14 @@ + + + + + + + + @@ -181,6 +190,22 @@ + + + + + + + + + + + + + + + + @@ -193,6 +218,8 @@ + + diff --git a/Pixiview.Android/Renderers/AppShellRenderer.cs b/Pixiview.Android/Renderers/AppShellRenderer.cs new file mode 100644 index 0000000..55d5bf9 --- /dev/null +++ b/Pixiview.Android/Renderers/AppShellRenderer.cs @@ -0,0 +1,23 @@ +#if TODO +using Android.Content; +using Pixiview.Droid.Renderers; +using Pixiview.Droid.Renderers.AppShellSection; +using Xamarin.Forms; +using Xamarin.Forms.Platform.Android; + +[assembly: ExportRenderer(typeof(Shell), typeof(AppShellRenderer))] +namespace Pixiview.Droid.Renderers +{ + public class AppShellRenderer : ShellRenderer + { + public AppShellRenderer(Context context) : base(context) + { + } + + protected override IShellBottomNavViewAppearanceTracker CreateBottomNavViewAppearanceTracker(ShellItem shellItem) + { + return new AppShellBottomNavViewAppearanceTracker(this, shellItem); + } + } +} +#endif diff --git a/Pixiview.Android/Renderers/AppShellSection/AppColorChangeRevealDrawable.cs b/Pixiview.Android/Renderers/AppShellSection/AppColorChangeRevealDrawable.cs new file mode 100644 index 0000000..a09acf8 --- /dev/null +++ b/Pixiview.Android/Renderers/AppShellSection/AppColorChangeRevealDrawable.cs @@ -0,0 +1,98 @@ +using Android.Animation; +using Android.Graphics; +using Android.Graphics.Drawables; +using System; +using AColor = Android.Graphics.Color; + +namespace Pixiview.Droid.Renderers.AppShellSection +{ + public class AppColorChangeRevealDrawable : AnimationDrawable + { + readonly Point _center; + float _progress; + bool _disposed; + ValueAnimator _animator; + + internal AColor StartColor { get; } + internal AColor EndColor { get; } + + public AppColorChangeRevealDrawable(AColor startColor, AColor endColor, Point center) : base() + { + StartColor = startColor; + EndColor = endColor; + + if (StartColor != EndColor) + { + _animator = ValueAnimator.OfFloat(0, 1); + _animator.SetInterpolator(new Android.Views.Animations.DecelerateInterpolator()); + _animator.SetDuration(500); + _animator.Update += OnUpdate; + _animator.Start(); + _center = center; + } + else + { + _progress = 1; + } + } + + public override void Draw(Canvas canvas) + { + if (_disposed) + return; + + if (_progress == 1) + { + canvas.DrawColor(EndColor); + return; + } + + canvas.DrawColor(StartColor); + var bounds = Bounds; + float centerX = _center.X; + float centerY = _center.Y; + + float width = bounds.Width(); + float distanceFromCenter = Math.Abs(width / 2 - _center.X); + float radius = (width / 2 + distanceFromCenter) * 1.1f; + + var paint = new Paint + { + Color = EndColor + }; + + canvas.DrawCircle(centerX, centerY, radius * _progress, paint); + } + + void OnUpdate(object sender, ValueAnimator.AnimatorUpdateEventArgs e) + { + _progress = (float)e.Animation.AnimatedValue; + + InvalidateSelf(); + } + + protected override void Dispose(bool disposing) + { + if (_disposed) + return; + + _disposed = true; + + if (disposing) + { + if (_animator != null) + { + _animator.Update -= OnUpdate; + + _animator.Cancel(); + + _animator.Dispose(); + + _animator = null; + } + } + + base.Dispose(disposing); + } + } +} diff --git a/Pixiview.Android/Renderers/AppShellSection/AppShellBottomNavViewAppearanceTracker.cs b/Pixiview.Android/Renderers/AppShellSection/AppShellBottomNavViewAppearanceTracker.cs new file mode 100644 index 0000000..1b4be95 --- /dev/null +++ b/Pixiview.Android/Renderers/AppShellSection/AppShellBottomNavViewAppearanceTracker.cs @@ -0,0 +1,170 @@ +using Android.Content.Res; +using Android.Graphics.Drawables; +#if __ANDROID_29__ +using AndroidX.Core.Widget; +using Google.Android.Material.BottomNavigation; +#else +using Android.Support.Design.Internal; +using Android.Support.Design.Widget; +#endif +using System; +using Xamarin.Forms; +using Xamarin.Forms.Platform.Android; +using AColor = Android.Graphics.Color; +using R = Android.Resource; + +namespace Pixiview.Droid.Renderers.AppShellSection +{ + public class AppShellBottomNavViewAppearanceTracker : IShellBottomNavViewAppearanceTracker + { + [System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0052:Remove unread private members", Justification = "")] + IShellContext _shellContext; + ShellItem _shellItem; + ColorStateList _defaultList; + bool _disposed; + ColorStateList _colorStateList; + + public AppShellBottomNavViewAppearanceTracker(IShellContext shellContext, ShellItem shellItem) + { + _shellItem = shellItem; + _shellContext = shellContext; + } + + public virtual void ResetAppearance(BottomNavigationView bottomView) + { + if (_defaultList != null) + { + bottomView.ItemTextColor = _defaultList; + bottomView.ItemIconTintList = _defaultList; + } + + SetBackgroundColor(bottomView, Color.White); + } + + public virtual void SetAppearance(BottomNavigationView bottomView, IShellAppearanceElement appearance) + { + IShellAppearanceElement controller = appearance; + var backgroundColor = controller.EffectiveTabBarBackgroundColor; + var foregroundColor = controller.EffectiveTabBarForegroundColor; + var disabledColor = controller.EffectiveTabBarDisabledColor; + var unselectedColor = controller.EffectiveTabBarUnselectedColor; // currently unused + var titleColor = controller.EffectiveTabBarTitleColor; + + if (_defaultList == null) + { +#if __ANDROID_28__ + _defaultList = bottomView.ItemTextColor ?? bottomView.ItemIconTintList + ?? MakeColorStateList(titleColor.ToAndroid().ToArgb(), disabledColor.ToAndroid().ToArgb(), foregroundColor.ToAndroid().ToArgb()); +#else + _defaultList = bottomView.ItemTextColor ?? bottomView.ItemIconTintList; +#endif + } + + _colorStateList = MakeColorStateList(titleColor, disabledColor, foregroundColor); + bottomView.ItemTextColor = _colorStateList; + bottomView.ItemIconTintList = _colorStateList; + + SetBackgroundColor(bottomView, backgroundColor); + } + + protected virtual void SetBackgroundColor(BottomNavigationView bottomView, Color color) + { + var oldBackground = bottomView.Background; + var colorDrawable = oldBackground as ColorDrawable; + var colorChangeRevealDrawable = oldBackground as AppColorChangeRevealDrawable; + AColor lastColor = colorChangeRevealDrawable?.EndColor ?? colorDrawable?.Color ?? Color.Default.ToAndroid(); + AColor newColor; + + if (color == Color.Default) + newColor = Color.White.ToAndroid(); + else + newColor = color.ToAndroid(); + + if (!(bottomView.GetChildAt(0) is BottomNavigationMenuView menuView)) + { + if (colorDrawable != null && lastColor == newColor) + return; + + if (lastColor != newColor || colorDrawable == null) + { + bottomView.SetBackground(new ColorDrawable(newColor)); + } + } + else + { + if (colorChangeRevealDrawable != null && lastColor == newColor) + return; + + var index = ((IShellItemController)_shellItem).GetItems().IndexOf(_shellItem.CurrentItem); + var menu = bottomView.Menu; + index = Math.Min(index, menu.Size() - 1); + + var child = menuView.GetChildAt(index); + if (child == null) + return; + + var touchPoint = new Android.Graphics.Point(child.Left + (child.Right - child.Left) / 2, child.Top + (child.Bottom - child.Top) / 2); + + bottomView.SetBackground(new AppColorChangeRevealDrawable(lastColor, newColor, touchPoint)); + } + } + + ColorStateList MakeColorStateList(Color titleColor, Color disabledColor, Color unselectedColor) + { + var disabledInt = disabledColor.IsDefault ? + _defaultList.GetColorForState(new[] { -R.Attribute.StateEnabled }, AColor.Gray) : + disabledColor.ToAndroid().ToArgb(); + + var checkedInt = titleColor.IsDefault ? + _defaultList.GetColorForState(new[] { R.Attribute.StateChecked }, AColor.Black) : + titleColor.ToAndroid().ToArgb(); + + var defaultColor = unselectedColor.IsDefault ? + _defaultList.DefaultColor : + unselectedColor.ToAndroid().ToArgb(); + + return MakeColorStateList(checkedInt, disabledInt, defaultColor); + } + + ColorStateList MakeColorStateList(int titleColorInt, int disabledColorInt, int defaultColor) + { + var states = new int[][] { + new int[] { -R.Attribute.StateEnabled }, + new int[] {R.Attribute.StateChecked }, + new int[] { } + }; + + var colors = new[] { disabledColorInt, titleColorInt, defaultColor }; + + return new ColorStateList(states, colors); + } + + #region IDisposable + + public void Dispose() + { + Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + if (_disposed) + return; + + _disposed = true; + + if (disposing) + { + _defaultList?.Dispose(); + _colorStateList?.Dispose(); + + _shellItem = null; + _shellContext = null; + _defaultList = null; + _colorStateList = null; + } + } + + #endregion IDisposable + } +} diff --git a/Pixiview.Android/Renderers/BlurryPanelRenderer.cs b/Pixiview.Android/Renderers/BlurryPanelRenderer.cs index a449b31..5e455d3 100644 --- a/Pixiview.Android/Renderers/BlurryPanelRenderer.cs +++ b/Pixiview.Android/Renderers/BlurryPanelRenderer.cs @@ -17,7 +17,14 @@ namespace Pixiview.Droid.Renderers { base.OnElementChanged(e); - SetBackgroundColor(Color.Black.MultiplyAlpha(.92).ToAndroid()); + if (e.NewElement != null) + { + var color = e.NewElement.BackgroundColor; + if (!color.IsDefault) + { + SetBackgroundColor(color.MultiplyAlpha(.94).ToAndroid()); + } + } } } } diff --git a/Pixiview.Android/Renderers/CardViewRenderer.cs b/Pixiview.Android/Renderers/CardViewRenderer.cs index e9c6175..d930cc4 100644 --- a/Pixiview.Android/Renderers/CardViewRenderer.cs +++ b/Pixiview.Android/Renderers/CardViewRenderer.cs @@ -1,6 +1,5 @@ using Android.Content; using Android.Graphics; -using Android.Views; using Pixiview.Droid.Renderers; using Pixiview.UI; using Xamarin.Forms; @@ -11,8 +10,6 @@ namespace Pixiview.Droid.Renderers { public class CardViewRenderer : VisualElementRenderer { - ViewOutlineProvider original; - public CardViewRenderer(Context context) : base(context) { } @@ -27,39 +24,19 @@ namespace Pixiview.Droid.Renderers var radius = element.CornerRadius; if (radius > 0) { - original = OutlineProvider; - OutlineProvider = new CornerRadiusOutlineProvider(element, radius); - ClipToOutline = true; + //var scale = Resources.DisplayMetrics.Density; + //OutlineProvider = new CornerRadiusOutlineProvider(element, radius, scale); + //ClipToOutline = true; + + var density = Resources.DisplayMetrics.Density; + + Elevation = (float)(element.ShadowOffset.Height + 2) * density; + + var drawable = new RoundCornerDrawable(radius * density); + drawable.SetColorFilter(element.BackgroundColor.ToAndroid(), PorterDuff.Mode.Src); + ((Android.Views.View)this).SetBackground(drawable); } } } - - protected override void OnDetachedFromWindow() - { - OutlineProvider = original; - base.OnDetachedFromWindow(); - } - - private class CornerRadiusOutlineProvider : ViewOutlineProvider - { - private readonly Element element; - private readonly float radius; - - public CornerRadiusOutlineProvider(Element from, float r) - { - element = from; - radius = r; - } - - public override void GetOutline(Android.Views.View view, Outline outline) - { - var scale = view.Resources.DisplayMetrics.Density; - var width = (double)element.GetValue(VisualElement.WidthProperty) * scale; - var height = (double)element.GetValue(VisualElement.HeightProperty) * scale; - - var rect = new Rect(0, 0, (int)width, (int)height); - outline.SetRoundRect(rect, radius * scale); - } - } } } diff --git a/Pixiview.Android/Renderers/OptionEntryRenderer.cs b/Pixiview.Android/Renderers/OptionEntryRenderer.cs new file mode 100644 index 0000000..71f6527 --- /dev/null +++ b/Pixiview.Android/Renderers/OptionEntryRenderer.cs @@ -0,0 +1,28 @@ +using Android.Content; +using Android.Graphics.Drawables; +using Pixiview.Droid.Renderers; +using Pixiview.UI; +using Xamarin.Forms; +using Xamarin.Forms.Platform.Android; + +[assembly: ExportRenderer(typeof(OptionEntry), typeof(OptionEntryRenderer))] +namespace Pixiview.Droid.Renderers +{ + public class OptionEntryRenderer : EntryRenderer + { + public OptionEntryRenderer(Context context) : base(context) + { + } + + protected override void OnElementChanged(ElementChangedEventArgs e) + { + base.OnElementChanged(e); + + if (e.NewElement != null) + { + var drawable = new ColorDrawable(e.NewElement.BackgroundColor.ToAndroid()); + Control.SetBackground(drawable); + } + } + } +} diff --git a/Pixiview.Android/Renderers/RoundImageRenderer.cs b/Pixiview.Android/Renderers/RoundImageRenderer.cs new file mode 100644 index 0000000..a09cb1b --- /dev/null +++ b/Pixiview.Android/Renderers/RoundImageRenderer.cs @@ -0,0 +1,60 @@ +using Android.Content; +using Android.Graphics; +using Pixiview.Droid.Renderers; +using Pixiview.UI; +using Xamarin.Forms; + +[assembly: ExportRenderer(typeof(RoundImage), typeof(RoundImageRenderer))] +namespace Pixiview.Droid.Renderers +{ + public class RoundImageRenderer : Xamarin.Forms.Platform.Android.FastRenderers.ImageRenderer + { + public RoundImageRenderer(Context context) : base(context) + { + } + + public override void SetBackgroundColor(Android.Graphics.Color color) + { + // nothing + } + + protected override void OnDraw(Canvas canvas) + { + if (Element is RoundImage image) + { + var radius = image.CornerRadius; + var mask = image.CornerMasks; + if (radius > 0 && mask != CornerMask.None) + { + var r = Resources.DisplayMetrics.Density * radius; + + var radii = new float[8]; + if ((mask & CornerMask.LeftTop) == CornerMask.LeftTop) + { + radii[0] = radii[1] = r; + } + if ((mask & CornerMask.RightTop) == CornerMask.RightTop) + { + radii[2] = radii[3] = r; + } + if ((mask & CornerMask.RightBottom) == CornerMask.RightBottom) + { + radii[4] = radii[5] = r; + } + if ((mask & CornerMask.LeftBottom) == CornerMask.LeftBottom) + { + radii[6] = radii[7] = r; + } + + var path = new Path(); + int width = Width; + int height = Height; + path.AddRoundRect(new RectF(0, 0, width, height), radii, Path.Direction.Cw); + canvas.ClipPath(path); + } + } + + base.OnDraw(canvas); + } + } +} diff --git a/Pixiview.Android/Renderers/RoundLabelRenderer.cs b/Pixiview.Android/Renderers/RoundLabelRenderer.cs new file mode 100644 index 0000000..b040aff --- /dev/null +++ b/Pixiview.Android/Renderers/RoundLabelRenderer.cs @@ -0,0 +1,70 @@ +using Android.Content; +using Android.Graphics; +using Android.Graphics.Drawables; +using Pixiview.Droid.Renderers; +using Pixiview.UI; +using Xamarin.Forms; +using Xamarin.Forms.Platform.Android; + +[assembly: ExportRenderer(typeof(RoundLabel), typeof(RoundLabelRenderer))] +namespace Pixiview.Droid.Renderers +{ + public class RoundLabelRenderer : Xamarin.Forms.Platform.Android.FastRenderers.LabelRenderer + { + public RoundLabelRenderer(Context context) : base(context) + { + } + + protected override void OnElementChanged(ElementChangedEventArgs