首页 时尚 演艺 游戏 八卦 音乐 影视 活动 热点 快讯 聚焦 综合 资讯
当前位置:首页 > 热点 > 正文

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

2023-06-18 15:10:41    来源:博客园

@


(相关资料图)

目录弧形基类定义绘制弧弧形进度条(ProgressBar)添加动画宽度补偿文本弧形滑块(Slider)创建控制柄拖动事件处理项目地址进度条(ProgressBar)用于展示任务的进度,告知用户当前状态和预期;

滑块(Slider)通过拖动滑块在一个固定区间内进行选择数值范围。

进度条和滑块都是进度值在UI界面的映射,其中滑块可以抽象成为带控制柄(Thumb)的进度条,是界面元素和进度值的双向绑定。

在某些场景下,我们需要一种更加直观的进度条,比如弧形进度条。今天在MAUI中实现一个弧形进度条和滑块。

使用.NET MAU实现跨平台支持,本项目可运行于Android、iOS平台。

弧形基类

新建.NET MAUI项目,命名CircleWidget

在项目中添加SkiaSharp绘制功能的引用Microsoft.Maui.Graphics.Skia以及SkiaSharp.Views.Maui.Controls

        
定义

对于弧形进度条的绘制,以及属性定义等,我们将其抽象为一个基类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 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部分我们添加弧形进度条的布局,代码如下:

                                                            

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 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部分,我们在原弧形进度条的布局基础上,添加弧形滑块控制柄的布局,代码如下:

...                                                                                
创建控制柄

重写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

猜您喜欢
  • [MAUI]弧形进度条与弧形滑块的交互实现
    [MAUI]弧形进度条与弧形滑块的交互实现
    @[toc]进度条(ProgressBar)用于展示任务的进度,告知用户当前状态和 2023-06-18
  • 全球热议:打开“露营+”的N种方式:青岛探索露营+天文科普研学
    全球热议:打开“露营+”的N种方式:青岛探索露营+天文科普研学
    突出核心竞争力,营地建设正朝着“多业态”“精致化”方向发展打开“露 2023-06-18
  • 微软再谈Xbox Series S游戏开发:做好规划就没问题 当前速递
    微软再谈Xbox Series S游戏开发:做好规划就没问题 当前速递
    对于玩家来说,XboxSeriesS可能是一笔很划算的交易,但对开发人员来说 2023-06-18
  • 全球新资讯:Chapter 5 :回忆
    全球新资讯:Chapter 5 :回忆
    或许 别人不是很理解,为什么南宫城总是这么冷静。或许 别人不是很 2023-06-18
  • 今日快看!以“艾”之名!北京大兴礼贤镇首届艾草文化节开幕
    今日快看!以“艾”之名!北京大兴礼贤镇首届艾草文化节开幕
    端午将至,艾草飘香。6月18日,北京大兴礼贤镇首届艾草文化节在平地村 2023-06-18
  • 外币报表折算差额例题_外币报表折算差额|每日热闻
    外币报表折算差额例题_外币报表折算差额|每日热闻
    1、外币报表折算差额会计分录:外币报表折算差额会计处理大致有两种方法 2023-06-18
  • 防止日本病(上) 环球快看
    防止日本病(上) 环球快看
    防止日本病的药方中,财政补贴生育养育和教育不是浪费性的政府消费,这 2023-06-18
  • 品质旗舰再度升级,全新UR-V即将焕新登场
    品质旗舰再度升级,全新UR-V即将焕新登场
    即将焕新上市的全新UR-V,理性而不刻板,感性而不出格,完美契合资深中 2023-06-18
  • 多省份用电负荷创新高!多部门采取措施保供应 当前时讯
    多省份用电负荷创新高!多部门采取措施保供应 当前时讯
    近期我国多地持续高温天气,多省份用电负荷创下新高。根据当前情况,多 2023-06-18
  • 福鼎人王某冬被抓,是因为……
    福鼎人王某冬被抓,是因为……
    ! 6月12日下午,福鼎市公安局刑侦大队办案民警收到了黄先生送的一面印 2023-06-18
  • 上线12年之后 腾讯“QQ安全达人”功能宣布即将下线
    上线12年之后 腾讯“QQ安全达人”功能宣布即将下线
    腾讯电脑管家近日发布公告,自2023年6月20日起,QQ安全达人功能将下线 2023-06-18
  • 世界通讯!ET5 Touring引人瞩目 可蔚来还是更需要一款“Model Y”
    世界通讯!ET5 Touring引人瞩目 可蔚来还是更需要一款“Model Y”
    2023年6月15日,蔚来带来了一款“特殊”的全新产品——ET5 Touring,也 2023-06-18
  • 二价酸酯 DBE 商品报价动态(2023-06-18) 全球头条
    二价酸酯 DBE 商品报价动态(2023-06-18) 全球头条
    交易商品牌 产地交货地最新报价二价酸酯DBE 含量99 9%山东海登新材料 2023-06-18
  • 一身的科技与狠活儿 静态体验保时捷911 GT3 RS(992)|天天速讯
    一身的科技与狠活儿 静态体验保时捷911 GT3 RS(992)|天天速讯
    不管您是富得流油的收藏控,还是想从中赚一笔的中间商,只要有机会一 2023-06-18
  • 胡兵吐槽50万积分被东航清零,多次沟通无果,态度巨差无比|焦点报道
    胡兵吐槽50万积分被东航清零,多次沟通无果,态度巨差无比|焦点报道
    近日,胡兵通过社交平台吐槽东航,称自己近50万的积分一夜之间被清零, 2023-06-18
  • 折扣店的“羊毛”,年轻人快薅不动了|天天看点
    折扣店的“羊毛”,年轻人快薅不动了|天天看点
    图片来源@视觉中国文| Tech星球,作者 | 林京田雨最近逛“嗨特购” 2023-06-18
  • 绿化带里隐蔽处的垃圾也不放过,志愿者们在行动 世界时快讯
    绿化带里隐蔽处的垃圾也不放过,志愿者们在行动 世界时快讯
    潮新闻客户端记者詹程开通讯员王蕾为了营造洁净、美丽的社区环境,提高 2023-06-18
  • 拍照时路人入镜了算不算侵权?关于肖像权要明白这些 世界快报
    拍照时路人入镜了算不算侵权?关于肖像权要明白这些 世界快报
    拍照时路人入镜了算不算侵权?关于肖像权要明白这些 2023-06-18
  • 今年可转债轮动什么阈值好?|环球时快讯
    今年可转债轮动什么阈值好?|环球时快讯
    在可转债的轮动中,可转债的价格阈值的高低,是一个非常敏感的数字。所 2023-06-18
  • 八子补肾胶囊荣登2023年星辰会“最受欢迎明星单品榜单”引领潮流
    八子补肾胶囊荣登2023年星辰会“最受欢迎明星单品榜单”引领潮流
    6月16日,第三届中国医药流通贸易大会暨第三届世界医药零售业大会(2023 2023-06-18
  • Copyright © 2008-2015 当代娱乐网版权所有   Inc. All Rights Reserved.    联系邮箱:55 16 53 8 @qq.com  京ICP备2021034106号-22