[MAUI]弧形进度条与弧形滑块的交互实现

news2025/2/23 12:46:00

文章目录

    • 弧形基类
      • 定义
      • 绘制弧
    • 弧形进度条(ProgressBar)
      • 添加动画
      • 宽度补偿
      • 文本
    • 弧形滑块(Slider)
      • 创建控制柄
      • 拖动事件处理
    • 项目地址

进度条(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)

最终效果如下:

在这里插入图片描述

项目地址

Github:maui-samples

Mato.Maui控件库
Mato.Maui

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/660030.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

本地安装部署运行 ChatGLM-6B 的常见问题解答以及后续优化

报错 No module named ‘transformers_modules.THUDM/chatglm-6b’ 报错本身的意思是&#xff0c;没有在指定的路径THUDM/chatglm-6b找到推理用模型 一般常见于自己手动下载模型&#xff0c;而不是通过下边这些文件直接启动&#xff0c;自动下载的情况 你需要修改web_demo.py&…

分层测试终究是大梦一场空?

分层测试分了个寂寞&#xff1f; 分层测试这个风吹了好多年&#xff0c;不分层都不好意思说自己是专业测试。各互联网公司更是对此乐此不疲&#xff0c;测试架构、测试平台&#xff0c;搞了一套又一套&#xff0c;然而。。。 理想总是丰满&#xff0c;现实总是骨干&#xff0…

第十三章 反射操作相关函数

1. 【检测对象是否可被调用】callable()函数 语法参考 callable函数的语法格式如下&#xff1a; callable(object)参数说明&#xff1a; object&#xff1a;对象&#xff1b; 返回值&#xff1a;如果对象可调用返回 True&#xff0c;否则返回 False。 说明&#xff1a;对于…

Python 中print 和return 的区别,你了解吗?

1、print() print()函数的作用是输出数据到控制台,就是打印在你能看到的界面上。 2、return return语句[表达式]退出函数&#xff0c;选择性地向调用方返回一个表达式。不带参数值的return语句返回None。 return作为脚本单独运行时则需要print函数才能显示&#xff0c;但是…

【id:17】【1分】A. DS顺序表--类实现

题目描述 用C语言和类实现顺序表 属性包括&#xff1a;数组、实际长度、最大长度&#xff08;设定为1000&#xff09; 操作包括&#xff1a;创建、插入、删除、查找 类定义参考 输入 第1行先输入n表示有n个数据&#xff0c;即n是实际长度&#xff1b;接着输入n个数据 第2行输…

c语言项目——三子棋小游戏(带详细讲解解析)

1.三子棋是什么&#xff1f; 三子棋是一种民间传统游戏&#xff0c;又叫九宫棋、圈圈叉叉棋、一条龙、井字棋等。游戏分为双方对战&#xff0c;双方依次在9宫格棋盘上摆放棋子&#xff0c;率先将自己的三个棋子走成一条线就视为胜利&#xff0c;而对方就算输了&#xff0c;但是…

【MySQL 索引、事务与存储引擎】

目录 一、索引的介绍2、索引的作用3、创建索引的原刚依据&#xff08;面试题&#xff09; 二、索引的分类和创建1、普通索引2、唯一索引3、主键索引4、组合索引5、全文索引6、查看索引7、删除索引8、分析是否使用索引 三、MySQL事务介绍1、事务的ACID特性2、隔离性 --- 不一致的…

Linux编译器gcc/g++

文章目录 Liinux编译器gcc/g1.背景知识(程序的翻译)2.动态库、静态库 Liinux编译器gcc/g 1.背景知识(程序的翻译) 以gcc编译 以g编译&#xff0c;但是此时会发现没有g这个指令&#xff0c;所有需要安装它&#xff0c;安装指令 yum install gcc gcc-c gcc和g都会形成可执行文…

TransFusion:利用 Transformer 进行鲁棒性融合来进行 3D 目标检测

Query 初始化 Input-dependent 以往 Query 位置是随机生成或学习作为网络参数的&#xff0c;而与输入数据无关&#xff0c;因此需要额外的阶段&#xff08;解码器层&#xff09;来学习模型向真实对象中心移动的过程。 论文提出了一种基于center heatmap 的 input-dependent 初…

在 Python 中使用令牌进行 API 调用

文章目录 在 Python 中进行不带令牌的 API 调用在 Python 中使用令牌调用 API总结 当我们第一次遇到如何在 Python 中调用 API 的问题时&#xff0c;我们的第一个想法是&#xff0c;“什么是 API&#xff1f;”。 API是应用程序编程接口的首字母缩写&#xff1b; 它允许您访问…

【面试】Java并发编程

ThreadLocal ThreadLocal 为什么会导致内存泄漏 hreadLocalMap使用ThreadLocal的弱引用作为key&#xff0c;如果一个ThreadLocal不存在外部强引用时&#xff0c;Key(ThreadLocal)势必会被GC回收&#xff0c;这样就会导致ThreadLocalMap中key为null&#xff0c; 而value还存在着…

15-ajax、实现过程、封装

定义 &#x1f37f;&#x1f37f;&#x1f37f;Async Javascript and XML 异步的JavaScript 和XML&#xff0c;是一种创建交互式网页应用的网页开发技术&#xff0c;可以在不重新加载整个网页的情况下&#xff0c;与服务器交换数据&#xff0c;并且更新部分网页 离不开 XMLH…

【C语言】万字教学,带你分步实现扫雷游戏(内含递归函数解析),剑指扫雷,一篇足矣

君兮_的个人主页 勤时当勉励 岁月不待人 C/C 游戏开发 带你轻松玩转扫雷游戏 前言一. 扫雷游戏的介绍以及内部需要实现的功能解析1.什么是扫雷游戏2.扫雷游戏所需的几个步骤 二.扫雷游戏的具体实现1.打印菜单菜单上的选择功能 2.初始化以及打印棋盘初始化函数InitBoard打印棋…

如何使用测试驱动开发(TDD)来实现100%的测试覆盖率?

本文以 DDM 为例&#xff0c;简单地介绍一下如何用测试驱动开发(TDD, Test-Driven Development)的方法来驱动出这个函数库。 本文以DDM为例&#xff0c;简单地介绍一下如何用测试驱动开发&#xff08;TDD, Test-Driven Development)的方法来驱动出这个函数库。 DDM简介 DDM是…

vue-cli3的安装和项目创建

一 vue-cli3的安装 &#xff08;注意&#xff1a;vue-cli3在安装之前&#xff0c;需要先删除旧版本&#xff0c;即vue-cli2&#xff09; cnpm i -g vue/cli vue-cli3的卸载&#xff1a;cnpm uninstall -g vue/cli 然后用命令“vue -V”查看是否删除vue&#xff0c;如果没有删…

kafka原理架构深入

目录 1. 下载安装2. 命令行命令3. 概述3.1 定义3.2 基本架构 4. 架构深入4.1 生产者4.1.1 分区4.1.2 数据可靠性保证4.1.3 Exactly Once语义4.1.4 发送消息流程 4.2 broker4.2.1 日志结构4.2.2 存储策略4.2.3 Controller & ZooKeeper4.2.4 高效读写数据 4.3 消费者4.3.1 消…

模型-视图-控制器模式(MVC模式,10种常见体系架构模式之一)

、简介&#xff1a; 架构模式是一个通用的、可重用的解决方案&#xff0c;用于在给定上下文中的软件体系结构中经常出现的问题。架构模式与软件设计模式类似&#xff0c;但具有更广泛的范围。 模型-视图-控制器模式&#xff0c;也称为MVC模式。是软件工程中的一种软件架构模式&…

Word模板引擎poi-tl

文章目录 ◆ 方案对比◆ 版本◆ 特性◆ 模板◆ 数据◆ 输出◆ 数据模型◆ 标签1. 文本2. 图片3. 表格4. 列表5. 嵌套6. 区块对 ◆ SpingEL2. 单系列图标3. 多系列图标4. 组合图表 ◆ 配置1. 标签前后缀2. 标签类型3. 标签匹配值4. 标签值计算5. SpringEL6. 数据模型序列化7. 错…

设计模式之抽象工厂笔记

设计模式之抽象工厂模式笔记 说明Abstract Factory(抽象工厂)目录UML抽象工厂示例类图甜品抽象类甜品提拉米苏类甜品抹茶慕斯类 咖啡抽象类美式咖啡类拿铁咖啡类 甜品工厂接口美式风味的甜品工厂意大利风味的甜品工厂 测试类模式扩展 说明 记录下学习设计模式-抽象工厂模式的写…

吴恩达471机器学习入门课程3第1周——K-means

K-means 聚类 1 - 实现 K-means1.1 找到最近的质心练习11.2 计算质心均值练习2 2 - K-means在样本数据集上的应用3 - 随机初始化4 - K-means图像压缩4.1 数据集可视化处理数据 4.2图像像素上的 K-mean4.3 压缩图片 实现 K-means 算法&#xff0c;并将其用于图像压缩。 您将从一…