使用 Xamarin Forms Shapes 的椭圆形进度条

Oval progress bar using Xamarin Forms Shapes

我想使用 Xamarin Forms 绘制椭圆形进度条,如下所示:

我发现 5 岁 ProgressRingPlugin 似乎按预期工作。但是,我试图避免在我的移动应用程序中添加其他包,并且 Visual Studio 显示有关此插件的警告(它与项目不完全兼容)。

请告知如何使用标准 Xamarin Forms 5 绘制此类控件。我相信使用 Arc shape 需要它。很高兴获得有关如何使用 XF 在图像上创建控件的解决方案。

如果ProgressRingPlugin与XF 5.0不兼容,可以参考这个repo: https://github.com/jsuarezruiz/TemplateUI#circleprogressbar

在尝试了几种解决方案后,我决定自己实现CircleProgressBar控件。

CircleProgressBar.xaml

<?xml version="1.0" encoding="UTF-8" ?>
<ContentView
    xmlns="http://xamarin.com/schemas/2014/forms"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    x:Class="YourNamespace.Controls.CircleProgressBar">
    <Grid >

        <Path x:Name="grayPath" Stroke="LightGray" 
              StrokeThickness="4" HorizontalOptions="Start" VerticalOptions="Start">
            <Path.Data>
                <PathGeometry>
                    <PathGeometry.Figures>
                        <PathFigureCollection>
                            <PathFigure x:Name="grayPathFigure">
                                <PathFigure.Segments>
                                    <PathSegmentCollection>
                                        <ArcSegment x:Name="grayArcSegment" SweepDirection="Clockwise" />
                                    </PathSegmentCollection>
                                </PathFigure.Segments>
                            </PathFigure>
                        </PathFigureCollection>
                    </PathGeometry.Figures>
                </PathGeometry>
            </Path.Data>
        </Path>


        <Path x:Name="pathRoot" Stroke="Green" 
              StrokeThickness="4" HorizontalOptions="Start" VerticalOptions="Start">
            <Path.Data>
                <PathGeometry>
                    <PathGeometry.Figures>
                        <PathFigureCollection>
                            <PathFigure x:Name="pathFigure">
                                <PathFigure.Segments>
                                    <PathSegmentCollection>
                                        <ArcSegment x:Name="arcSegment" SweepDirection="Clockwise" />
                                    </PathSegmentCollection>
                                </PathFigure.Segments>
                            </PathFigure>
                        </PathFigureCollection>
                    </PathGeometry.Figures>
                </PathGeometry>
            </Path.Data>
        </Path>        
    </Grid>
</ContentView>

CircleProgressBar.cs

using System;
using System.Collections.Generic;
using Xamarin.Forms;
using Xamarin.Forms.Shapes;

namespace YourNamespace.Controls
{       
    public partial class CircleProgressBar : ContentView
    {
        public CircleProgressBar()
        {
            InitializeComponent();

            Angle = (Percentage * 360) / 100;
            RenderArc();
        }

        public int Radius
        {
            get { return (int)GetValue(RadiusProperty); }
            set { SetValue(RadiusProperty, value); }
        }

        public Brush SegmentColor
        {
            get { return (Brush)GetValue(SegmentColorProperty); }
            set { SetValue(SegmentColorProperty, value); }
        }

        public int StrokeThickness
        {
            get { return (int)GetValue(StrokeThicknessProperty); }
            set { SetValue(StrokeThicknessProperty, value); }
        }

        public double Percentage
        {
            get { return (double)GetValue(PercentageProperty); }
            set { SetValue(PercentageProperty, value); }
        }

        public double Angle
        {
            get { return (double)GetValue(AngleProperty); }
            set { SetValue(AngleProperty, value); }
        }

        // Using a BindableProperty as the backing store for Percentage.  This enables animation, styling, binding, etc...
        public static readonly BindableProperty PercentageProperty =
            BindableProperty.Create("Percentage", typeof(double), typeof(CircleProgressBar),
             65d, propertyChanged: OnPercentageChanged);

        // Using a BindableProperty as the backing store for StrokeThickness.  This enables animation, styling, binding, etc...
        public static readonly BindableProperty StrokeThicknessProperty =
            BindableProperty.Create("StrokeThickness", typeof(int), typeof(CircleProgressBar),
                5, propertyChanged: OnThicknessChanged);

        // Using a BindableProperty as the backing store for SegmentColor.  This enables animation, styling, binding, etc...
        public static readonly BindableProperty SegmentColorProperty =
            BindableProperty.Create("SegmentColor", typeof(Brush), typeof(CircleProgressBar),
                new SolidColorBrush(Color.Red), propertyChanged: OnColorChanged);

        // Using a BindableProperty as the backing store for Radius.  This enables animation, styling, binding, etc...
        public static readonly BindableProperty RadiusProperty =
            BindableProperty.Create("Radius", typeof(int), typeof(CircleProgressBar),
                25, propertyChanged: OnPropertyChanged);

        // Using a BindableProperty as the backing store for Angle.  This enables animation, styling, binding, etc...
        public static readonly BindableProperty AngleProperty =
            BindableProperty.Create("Angle", typeof(double), typeof(CircleProgressBar),
                120d, propertyChanged: OnPropertyChanged);

        private static void OnColorChanged(BindableObject sender, object old, object newValue)
        {
            CircleProgressBar circle = sender as CircleProgressBar;
            circle.SetColor((SolidColorBrush)newValue);
        }

        private static void OnThicknessChanged(BindableObject sender, object old, object newValue)
        {
            CircleProgressBar circle = sender as CircleProgressBar;
            circle.SetThickness((int)newValue);
        }

        private static void OnPercentageChanged(BindableObject sender, object old, object newValue)
        {
            CircleProgressBar circle = sender as CircleProgressBar;
            if (circle.Percentage > 100) circle.Percentage = 100;
            circle.Angle = (circle.Percentage * 360) / 100;
        }

        private static void OnPropertyChanged(BindableObject sender, object old, object newValue)
        {
            CircleProgressBar circle = sender as CircleProgressBar;
            circle.RenderArc();
        }

        public void SetThickness(int n)
        {
            pathRoot.StrokeThickness = n;
        }

        public void SetColor(SolidColorBrush n)
        {
            pathRoot.Stroke = n;
        }

        public void RenderSpecificArc(Path pathRoot, PathFigure pathFigure, ArcSegment arcSegment, double angle)
        {
            Point startPoint = new Point(Radius, 0);
            Point endPoint = ComputeCartesianCoordinate(angle, Radius);
            endPoint.X += Radius;
            endPoint.Y += Radius;

            pathRoot.WidthRequest = Radius * 2 + StrokeThickness;
            pathRoot.HeightRequest = Radius * 2 + StrokeThickness;
            pathRoot.Margin = new Thickness(StrokeThickness, StrokeThickness, 0, 0);


            bool largeArc = Angle > 180.0;

            Size outerArcSize = new Size(Radius, Radius);

            pathFigure.StartPoint = startPoint;

            if (startPoint.X == Math.Round(endPoint.X) && startPoint.Y == Math.Round(endPoint.Y))
                endPoint.X -= 0.01;

            arcSegment.Point = endPoint;
            arcSegment.Size = outerArcSize;
            arcSegment.IsLargeArc = largeArc;
        }

        public void RenderArc()
        {
            RenderSpecificArc(this.grayPath, this.grayPathFigure, this.grayArcSegment, angle: 360);
            RenderSpecificArc(this.pathRoot, this.pathFigure, this.arcSegment, this.Angle);            
        }

        private Point ComputeCartesianCoordinate(double angle, double radius)
        {
            // convert to radians
            double angleRad = (Math.PI / 180.0) * (angle - 90);

            double x = radius * Math.Cos(angleRad);
            double y = radius * Math.Sin(angleRad);

            return new Point(x, y);
        }
    }
}

您还可以为描边颜色、厚度等添加可绑定属性。

用法:

<Grid>
    <controls:CircleProgressBar WidthRequest="100" HeightRequest="100" Percentage="65"
    <!-- adjust margin of label depending of Circle size-->
    <Label Text="65%" FontSize="14" Margin="18,20,0,0"/>        
</Grid>