[MAUI]弧形进度条与弧形滑块的交互实现
@
进度条(ProgressBar)用于展示任务的进度,告知用户当前状态和预期;
滑块(Slider)通过拖动滑块在一个固定区间内进行选择数值范围。
进度条和滑块都是进度值在UI界面的映射,其中滑块可以抽象成为带控制柄(Thumb)的进度条,是界面元素和进度值的双向绑定。
在某些场景下,我们需要一种更加直观的进度条,比如弧形进度条。今天在MAUI中实现一个弧形进度条和滑块。

使用.NET MAU实现跨平台支持,本项目可运行于Android、iOS平台。
弧形基类
新建.NET MAUI项目,命名CircleWidget
在项目中添加SkiaSharp绘制功能的引用Microsoft.Maui.Graphics.Skia以及SkiaSharp.Views.Maui.Controls。
<ItemGroup>
<PackageReference Include="Microsoft.Maui.Graphics.Skia" Version="7.0.59" />
<PackageReference Include="SkiaSharp.Views.Maui.Controls" Version="2.88.3" />
</ItemGroup>
定义
对于弧形进度条的绘制,以及属性定义等,我们将其抽象为一个基类CircleProgressBase.cs,代码如下:
public abstract class CircleProgressBase : ContentView, IProgress
控件将包含以下可绑定属性:
- Maxiumum:最大值
- Minimum:最小值
- Progress:当前进度
- AnimationLength:动画时长
- BorderWidth:描边宽度
- LabelContent:标签内容
- ContainerColor:容器颜色,即进度条的背景色
- ProgressColor:进度条颜色
public abstract double Maximum { get; set; }
public abstract double Minimum { get; set; }
public abstract Color ContainerColor { get; set; }
public abstract Color ProgressColor { get; set; }
public abstract double Progress { get; set; }
public abstract double AnimationLength { get; set; }
public abstract double BorderWidth { get; set; }
public abstract View LabelContent { get; set; }
以及ValueChange事件,此事件用于在进度值改变时触发。
public event EventHandler<double> ValueChanged;
实时进度值RealtimeProgress,应用于缓动动画中的实时渲染,稍后会详细说明。
protected double _realtimeProgress;
以及进度条宽度补偿值,稍后会详细说明。
protected float _mainRectPadding;
绘制弧
Skia中,通过AddArc方法绘制弧,需要传入一个SKRect对象,其代表一个弧(或椭弧)的外接矩形。startAngle和sweepAngle分别代表顺时针起始角度和扫描角度。
通过startAngle和sweepAngle可以绘制出一个弧,如下图红色部分所示:

在OnCanvasViewPaintSurface中,通过给定起始角度为正上方,扫描角度为360对于100%进度,通过插值计算出当前进度对应的扫描角度,绘制出进度条。
protected virtual void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
SKRect rect = new SKRect(_mainRectPadding, _mainRectPadding, info.Width - _mainRectPadding, info.Height - _mainRectPadding);
float startAngle = -90;
float sweepAngle = (float)((_realtimeProgress / SumValue) * 360);
canvas.DrawOval(rect, OutlinePaint);
using (SKPath path = new SKPath())
{
path.AddArc(rect, startAngle, sweepAngle);
canvas.DrawPath(path, ArcPaint);
}
}
其中SumValue表明进度条的总进度,通过Maximum和Minimum计算得出。
public double SumValue => Maximum - Minimum;
创建进度条轨道背景画刷和进度条画刷:
protected SKPaint _outlinePaint;
public SKPaint OutlinePaint
{
get
{
if (_outlinePaint == null)
{
RefreshMainRectPadding();
SKPaint outlinePaint = new SKPaint
{
Color = this.ContainerColor.ToSKColor(),
Style = SKPaintStyle.Stroke,
StrokeWidth = (float)BorderWidth,
};
_outlinePaint = outlinePaint;
}
return _outlinePaint;
}
}
protected SKPaint _arcPaint;
public SKPaint ArcPaint
{
get
{
if (_arcPaint == null)
{
RefreshMainRectPadding();
SKPaint arcPaint = new SKPaint
{
Color = this.ProgressColor.ToSKColor(),
Style = SKPaintStyle.Stroke,
StrokeWidth = (float)BorderWidth,
StrokeCap = SKStrokeCap.Round,
};
_arcPaint = arcPaint;
}
return _arcPaint;
}
}
弧形进度条(ProgressBar)
控件由进度条和进度文本Label组成,进度文本位于控件中心
创建CircleProgressBar,他将继承CircleProgressBase,在Xaml部分我们添加弧形进度条的布局,代码如下:
<?xml version="1.0" encoding="UTF-8"?>
<controls:CircleProgressBase xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:forms="clr-namespace:SkiaSharp.Views.Maui.Controls;assembly=SkiaSharp.Views.Maui.Controls"
xmlns:controls="clr-namespace:CircleWidget.Controls;assembly=CircleWidget"
x:Class="CircleWidget.Controls.CircleProgressBar">
<controls:CircleProgressBase.Content>
<Grid>
<forms:SKCanvasView x:Name="canvasView"
PaintSurface="OnCanvasViewPaintSurface" />
<ContentView x:Name="MainContent"></ContentView>
<Label FontSize="28"
HorizontalOptions="Center"
VerticalOptions="Center"
x:Name="labelView"></Label>
</Grid>
</controls:CircleProgressBase.Content>
</controls:CircleProgressBase>
SKCanvasView是SkiaSharp.Views.Maui.Controls封装的View控件。
效果如下

CodeBehind 中,我们将添加各抽象属性的具体实现。
在Progress值变更时,重新渲染进度条,并触发ValueChanged事件。
var obj = (CircleProgressBar)bindable;
obj.canvasView?.InvalidateSurface();
obj.ValueChanged?.Invoke(obj, obj.Progress);
添加动画
我们在控件外部更改Progress值的时候,因为缓动函数的执行,进度条并未立即达到目标值,在此期间,_realtimeProgress值代表实时发生的进度值。
Progress值的变更,是一个“请求”,类似HeightRequest。完成动画实际上是一个异步过程。
添加函数UpdateProgressWithAnimate,当触发Progress值变更请求时,调用此函数,将会执行动画。
protected virtual void UpdateProgressWithAnimate(Action<double, bool> finished = null)
{
this.AbortAnimation("ReshapeAnimations");
var scaleAnimation = new Animation();
double progressTarget = this.Progress;
double progressOrigin = this._realtimeProgress;
var animateAction = (double r) =>
{
this._realtimeProgress = r;
ValueChanged?.Invoke(this, this._realtimeProgress);
};
var scaleUpAnimation0 = new Animation(animateAction, progressOrigin, progressTarget);
scaleAnimation.Add(0, 1, scaleUpAnimation0);
scaleAnimation.Commit(this, "ReshapeAnimations", 16, (uint)this.AnimationLength, finished: finished);
}
可以给动画添加一个自定义缓动函数
如添加一个反复弹跳至目标值的缓动函数,拟合函数图像如下:

应用到代码中:
var myEasing = (double x) => {
if (x < 1 / 2.75f)
{
return 7.5625f * x * x;
}
if (x < 2 / 2.75f)
{
x -= 1.5f / 2.75f;
return 7.5625f * x * x + .75f;
}
if (x < 2.5f / 2.75f)
{
x -= 2.25f / 2.75f;
return 7.5625f * x * x + .9375f;
}
x -= 2.625f / 2.75f;
return 7.5625f * x * x + .984375f;
};
var scaleUpAnimation0 = new Animation(animateAction, progressOrigin, progressTarget, myEasing);
scaleAnimation.Add(0, 1, scaleUpAnimation0);
scaleAnimation.Commit(this, "ReshapeAnimations", 16, (uint)this.AnimationLength, finished: finished);
在Progress值变更时的触发函数改写为:
var obj = (CircleSlider)bindable;
obj.UpdateProgressWithAnimate();
效果如下:

当然,这在每一次的变更时,都会应用动画。如果频繁密集地更改进度,这将会导致动画的堆积,造成性能问题。
我们通过一个阈值限制动画发生的频次,当变更的进度值超过阈值时,才应用动画。
CircleProgressBase 中添加一个常量:
protected const int ANIMATE_THROTTLE = 10;
当新值相较于旧值的变化幅度超过阈值时(10%或以上的进度变更请求),应用动画,否则直接更新进度条。
protected virtual void UpdateProgress()
{
this._realtimeProgress = this.Progress;
ValueChanged?.Invoke(this, this._realtimeProgress);
}
var obj = (CircleSlider)bindable;
var valueChangedSpan = (double)oldValue - (double)newValue;
if (Math.Abs(valueChangedSpan) > ANIMATE_THROTTLE)
{
obj.UpdateProgressWithAnimate();
}
else
{
obj.UpdateProgress();
}
宽度补偿
在Skia中,当我们设置path的宽度(StrokeWidth), path的绘制是以path的中心线为基准,向两边扩张的,如下图

当默认绘制区域(canvas)的尺寸等同于控件尺寸时,绘制有可能溢出,为了保持绘制在控件内部,我们需要对绘制区域进行补偿。
创建_mainRectPadding的更新函数RefreshMainRectPadding,当控件尺寸变更时
protected virtual void RefreshMainRectPadding()
{
//边界补偿
this._mainRectPadding = (float)(this.BorderWidth / 2);
this.Padding = this._mainRectPadding;
}
当BorderWidth变更时,调用此函数,更新_mainRectPadding的值。
protected virtual void CircleProgressBar_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
...
if (e.PropertyName == nameof(BorderWidth))
{
this.RefreshMainRectPadding();
}
}
文本
最后将进度文本控件值变更添加到CircleProgressBar_ValueChanged中,完成控件的实现。
private void CircleProgressBar_ValueChanged(object sender, double e)
{
this.labelView.Text = e.ToString(LABEL_FORMATE);
this.canvasView?.InvalidateSurface();
}
LABEL_FORMATE是一个常量,用于格式化进度文本的显示。
string格式化请参考官方文档
protected const string LABEL_FORMATE = "0";
弧形滑块(Slider)
弧形滑块的实现,与弧形进度条的实现类似,我们只需要在CircleProgressBar的基础上,添加控制柄的布局和拖动事件处理
创建CircleSlider,他将继承CircleProgressBase,在Xaml部分,我们在原弧形进度条的布局基础上,添加弧形滑块控制柄的布局,代码如下:
<!-- 进度条布局 -->
...
<!-- 控制柄布局 -->
<ContentView x:Name="ThumbContent"
Background="transparent"
HeightRequest="50"
WidthRequest="50">
<ContentView.GestureRecognizers>
<PanGestureRecognizer PanUpdated="PanGestureRecognizer_PanUpdated"></PanGestureRecognizer>
</ContentView.GestureRecognizers>
<Border Background="white"
Opacity="0.5"
StrokeThickness="0">
<Border.StrokeShape>
<RoundRectangle CornerRadius="50" />
</Border.StrokeShape>
<Border.Shadow>
<Shadow Brush="Black"
Offset="20,20"
Radius="40"
Opacity="0.8" />
</Border.Shadow>
</Border>
</ContentView>
创建控制柄
重写OnCanvasViewPaintSurface方法,添加控制柄的位置更新逻辑
protected override void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
...
var thumbX = Math.Sin(sweepAngle * Math.PI / 180) * (this.Width/2-1.25*this._mainRectPadding);
var thumbY = Math.Cos(sweepAngle * Math.PI / 180) * (this.Height / 2-1.25*this._mainRectPadding);
this.ThumbContent.TranslationX=thumbX;
this.ThumbContent.TranslationY=-thumbY;
}
效果如下:

拖动事件处理
添加一个PanGestureRecognizer的事件处理函数,用于处理控制柄的拖动事件
首先计算触摸点的坐标,以圆心为原点,触摸点的坐标(PositionX,PositionY)是原ThumbContent的坐标(TranslationX,TranslationY)与触摸点的偏移量(e.TotalX,e.TotalY)的和。
当控制柄被拖动时,我们需要计算出拖动的角度,触摸点与圆心的连线与X轴的夹角即为拖动的角度(sweepAngle)。
很容易得出,PositionX与PositionY的比值,是角度sweepAngle的正切值,他们的关系如下图所示:

将角度转换为进度值,更新进度条的值。
private void PanGestureRecognizer_PanUpdated(object sender, PanUpdatedEventArgs e)
{
var thumb = sender as ContentView;
var PositionX = thumb.TranslationX+e.TotalX;
var PositionY = thumb.TranslationY+e.TotalY;
this.test.TranslationX = thumb.TranslationX+e.TotalX;
this.test.TranslationY = thumb.TranslationY+e.TotalY;
var sweepAngle = AngleNormalize(Math.Atan2(PositionX, -PositionY)*180/Math.PI);
var targetProgress = sweepAngle*SumValue/360;
this.Progress=targetProgress;
}

sweepAngle的取值范围为[-180,180],我们需要将其转换为[0,360]的取值范围,这里我们使用AngleNormalize函数进行转换。
private double AngleNormalize(double value)
{
double twoPi = 360;
while (value <= -180) value += twoPi;
while (value > 180) value -= twoPi;
value= (value + twoPi) % twoPi;
return value;
}
将可绑定属性Progress的绑定模式改为TwoWay。
public static readonly BindableProperty ProgressProperty =
BindableProperty.Create("Progress", typeof(double), typeof(CircleSlider), 0.5, defaultBindingMode:BindingMode.TwoWay)
最终效果如下:

项目地址
Mato.Maui控件库
Mato.Maui
[MAUI]弧形进度条与弧形滑块的交互实现的更多相关文章
- android弧形进度条,有详细注释的,比较简单
Java code? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 ...
- 用layer-list实现弧形进度条
本文转载自:http://www.linuxidc.com/Linux/2013-04/82743.htm 之前我有写过如何用style或者是layer-list实现自定义的横向进度条(http:// ...
- [WPF] 使用三种方式实现弧形进度条
1. 需求 前天看到有人问弧形进度条怎么做,我模仿了一下,成果如下图所示: 当时我第一反应是可以用 Microsoft.Toolkit.Uwp.UI.Controls 里的 RadialGauge 实 ...
- 【iOS实现一个颜色渐变的弧形进度条】
在Github上看到一些进度条的功能,都是通过Core Graph来实现.无所谓正确与否,但是开发效率明显就差很多了,而且运行效率还是值得考究的.其实使用苹果提供的Core Animation能够非常 ...
- 用canvas画弧形进度条
function toCanvas(id ,progress){ //canvas进度条 var canvas = document.getElementById(id), ctx = canvas. ...
- Android自定义控件系列之应用篇——圆形进度条
一.概述 在上一篇博文中,我们给大家介绍了Android自定义控件系列的基础篇.链接:http://www.cnblogs.com/jerehedu/p/4360066.html 这一篇博文中,我们将 ...
- iOS圆弧渐变进度条的实现
由于项目需要一个环形渐变进度条显示课程,这方便网上的确有很多相关资料但是,都是比较零散的而且,大多数只是放一堆代码就算完了.这里我想详细写一篇我自己实现这个进度条的过程. 实现一个圆弧进度条主要分为三 ...
- N 种仅仅使用 HTML/CSS 实现各类进度条的方式
本文将介绍如何使用 HTML/CSS 创建各种基础进度条及花式进度条及其动画的方式,通过本文,你可能可以学会: 通过 HTML 标签 <meter> 创建进度条 通过 HTML 标签 &l ...
- 使用原生JS+CSS或HTML5实现简单的进度条和滑动条效果(精问)
使用原生JS+CSS或HTML5实现简单的进度条和滑动条效果(精问) 一.总结 一句话总结:进度条动画效果用animation,自动效果用setIntelval 二.使用原生JS+CSS或HTML5实 ...
- canvas-圆弧形可拖动进度条
一.效果如下: See the Pen XRmNRK by pangyongsheng (@pangyongsheng) on CodePen. 链接dome 二. 本文是实现可拖动滑块实现的基本思路 ...
随机推荐
- HTTP协议分析与Unity用法
一.http协议简介 http协议是Hyper Text Transfer Protocol(超文本传输协议)的缩写,是用于从万维网服务器传输超文本到本地浏览器的传送协议,使用TCP/IP通信协议传输 ...
- 排队论——系统运行指标的R语言实现
排队是在日常生活中经常遇到的现象,如顾客到商店购买物品.病人到医院看病常常要排队.此时要求服务的数量超过服务机构(服务台.服务员等)的容量.也就是说,到达的顾客不能立即得到服务,因而出现了排队现象.这 ...
- 统计模拟实验—R实现(蒲丰投针)
统计模拟实验 统计模拟是数理统计.和计算机科学的结合,是一门综合性学科.在科学研究和生产实际的各个领域中,普遍存在着大量数据的分析处理工作.如何应用数理统计中的方法来解决实际问题,以及如何解决在应用中 ...
- Linux磁盘LVM根目录扩容
LVM 的基本概念 物理卷 Physical Volume (PV):可以在上面建立卷组的媒介,可以是硬盘分区,也可以是硬盘本身或者回环文件(loopback file).物理卷包括一个特殊的 hea ...
- 【CTF】日志 2019.7.13 pwn 堆溢出基础知识
十六进制两位表示一个字节 堆溢出 先上堆图: 堆的数据结构 一般情况下,物理相邻的两个空闲 chunk 会被合并为一个 chunk struct malloc_chunk { INTERNAL_SIZ ...
- 我来泼盆冷水:正面迎击AI的时代千万别被ChatGPT割了韭菜
前言 ChatGPT从出来的时候我就一直密切关注,为此还加了不少群,用了不少套壳的程序,公司还开了专门的培训会,技术团队还为此搭建了接入ChatGPT的服务,帮助全公司的产品.商务.测试.运维.研发一 ...
- day72:drf:反序列化功能&模型类序列化器Modelserializer&drf视图APIView
目录 1.续:反序列化功能(5-8) 1.用户post类型提交数据,反序列化功能的步骤 2.反序列化功能的局部钩子和全局钩子 局部钩子和全局钩子在序列化器中的使用 反序列化相关校验的执行顺序 3.反序 ...
- yolov5训练自己的数据集
1.安装cuda 可以先看看自己的 显卡信息,支持哪个cuda版本 cuda下载地址:https://developer.nvidia.com/cuda-toolkit-archive 我的RTX30 ...
- Yapi及Swgger使用+注解
1.Yapi 1.1 介绍 YApi 是高效.易用.功能强大的 api 管理平台,旨在为开发.产品.测试人员提供更优雅的接口管理服务.可以帮助开发者轻松创建.发布.维护 API,YApi 还为用户提供 ...
- 长连接Netty服务内存泄漏,看我如何一步步捉“虫”解决
作者:京东科技 王长春 背景 事情要回顾到双11.11备战前夕,在那个风雨交加的夜晚,一个急促的咚咚报警,惊破了电闪雷鸣的黑夜,将沉浸在梦香,熟睡的我惊醒. 一看手机咚咚报警,不好!有大事发生了!电话 ...