[MAUI 项目实战] 手势控制音乐播放器:圆形进度条

我们将绘制一个圆形的音乐播放控件,它包含一个圆形的进度条、专辑页面和播放按钮。

在这里插入图片描述

关于图形绘制

使用MAUI的绘制功能,需要Microsoft.Maui.Graphics库。

Microsoft.Maui.Graphics 是一个实验性的跨平台图形库,它可以在 .NET MAUI 中使用。它提供了一组基本的图形元素,如矩形、圆形、线条、路径、文本和图像。它还提供了一组基本的图形操作,如填充、描边、裁剪、变换和渐变。

Microsoft.Maui.Graphics在不同的目标平台上使用一致的API访问本机图形功能,而底层实现使用了不同的图形渲染引擎。其中通用性较好的是SkiaSharp图形库,支持几乎所有的操作系统,在不同平台上的表现也近乎一致。

创建自定义控件

在项目中添加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>

创建CircleSlider.xaml文件,添加如下内容:

<?xml version="1.0" encoding="UTF-8"?>
<ContentView 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"
             
             x:Class="MatoMusic.Controls.CircleSlider">
  <ContentView.Content>

      <forms:SKCanvasView x:Name="canvasView"
                          PaintSurface="OnCanvasViewPaintSurface" />

    </ContentView.Content>
</ContentView>

SKCanvasView是SkiaSharp.Views.Maui.Controls封装的View控件。

打开CircleSlider.xaml.cs文件

控件将包含以下可绑定属性:

  • Maximum:最大值
  • Minimum:最小值
  • Value:当前值
  • TintColor:进度条颜色
  • ContainerColor:进度条背景颜色
  • BorderWidth:进度条宽度

定义两个SKPaint画笔属性,OutlinePaint用于绘制进度条背景,ArcPaint用于绘制进度条本身。他们的描边宽度StrokeWidth则是圆形进度条的宽度。
两个画笔的初始值样式为SKPaintStyle.Stroke,描边宽度为BorderWidth的值。

private SKPaint _outlinePaint;

public SKPaint OutlinePaint
{
    get
    {
        if (_outlinePaint == null)
        {
            SKPaint outlinePaint = new SKPaint
            {
                Style = SKPaintStyle.Stroke,
                StrokeWidth = BorderWidth,
            };
            _outlinePaint = outlinePaint;
        }
        return
            _outlinePaint;
    }
    set { _outlinePaint = value; }
}

private SKPaint _arcPaint;

public SKPaint ArcPaint
{
    get
    {
        if (_arcPaint == null)
        {
            SKPaint arcPaint = new SKPaint
            {
                Style = SKPaintStyle.Stroke,
                StrokeWidth = BorderWidth,
            };
            _arcPaint = arcPaint;
        }

        return _arcPaint;
    }
    set { _arcPaint = value; }
}

SetStrokeWidth用于设置描边宽度,并产生一个动效,

在BorderWidth发生变更的时候,会出现一个动效。宽度会缓慢地变化至新的值。刷新率为10ms一次,每次变化的值为1。


private float _borderWidth;

public float BorderWidth
{
    get { return _borderWidth; }
    set
    {
        var old_borderWidth = _borderWidth;

        var span = value - old_borderWidth;

        SetStrokeWidth(span, old_borderWidth);

        _borderWidth = value;

        this.ArcPaint.StrokeWidth = _borderWidth;
        this.OutlinePaint.StrokeWidth = _borderWidth;
    }
}

private async void SetStrokeWidth(float span, float old_borderWidth)
{
    if (span > 0)
    {
        for (int i = 0; i <= span; i++)
        {
            await Task.Delay(10);
            this.ArcPaint.StrokeWidth = old_borderWidth + i;
            this.OutlinePaint.StrokeWidth = old_borderWidth + i;
            RefreshMainRectPadding();
        }
    }
    else
    {
        for (int i = 0; i >= span; i--)
        {
            await Task.Delay(10);
            this.ArcPaint.StrokeWidth = old_borderWidth + i;
            this.OutlinePaint.StrokeWidth = old_borderWidth + i;
            RefreshMainRectPadding();

        }
    }

}

于此同时,因为描边宽度变化了,需要对Padding进行补偿。调用RefreshMainRectPadding方法计算一个新的Padding值,BoderWidth缩小时,Padding也随之增大。

private void RefreshMainRectPadding()
{
    this._mainRectPadding =  this.BorderWidth / 2;
}

在视觉上,进度条宽度从内向外扩张变细。

在这里插入图片描述

若设为原宽度减去计算值,从视觉上是从外向内收缩变细。

private void RefreshMainRectPadding()
{
    this._mainRectPadding =  15 -  this.BorderWidth / 2;
}

在这里插入图片描述

接下来写订阅了CanvaseView的PaintSurface事件的方法OnCanvasViewPaintSurface。在这个方法中,我们将编写圆形进度条的绘制逻辑。

PaintSurface事件在绘制图形时触发。程序运行时会实时触发这个方法,它的参数SKPaintSurfaceEventArgs事件附带的对象具有两个属性:

  • Info类型SKImageInfo
  • Surface类型SKSurface

SKImageInfo对象包含如宽度和高度等有关绘图区域的信息,对象SKSurface为绘制本身,我们需要利用SKImageInfo宽度和高度等信息,结合业务数据,在SKSurface绘制出我们想要的图形。

清空上一次绘制的图形,调用SKSurface.Canvas获取Canvas对象,调用Canvas.Clear方法清空上一次绘制的图形。

canvas.Clear();

rect是一个SKRect对象,进度条本身是圆形,我们需要一个正方形的区域来控制圆形区域。

sweepAngle是当前进度对应的角度,首先计算出总进度值,通过计算当前进度对应总进度的比值,换算成角度,将这一角度赋值给sweepAngle。

startAngle是进度条的起始角度,我们将其设置为-90度,即从正上方开始绘制。


SKRect rect = new SKRect(_mainRectPadding, _mainRectPadding, info.Width - _mainRectPadding, info.Height - _mainRectPadding);
float startAngle = -90;
float sweepAngle = (float)((Value / SumValue) * 360);

调用Canvas.DrawOval,使用OutlinePaint画笔绘制进度条背景,它是一个圆形

canvas.DrawOval(rect, OutlinePaint);

创建绘制路径path,调用AddArc方法,将rect对象和起始角度和终止角度传入,即可绘制出弧形。

using (SKPath path = new SKPath())
{
    path.AddArc(rect, startAngle, sweepAngle);
    canvas.DrawPath(path, ArcPaint);
}

绘制部分的完整代码如下:

private void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{

    var SumValue = Maximum - Minimum;


    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)((Value / SumValue) * 360);

    canvas.DrawOval(rect, OutlinePaint);

    using (SKPath path = new SKPath())
    {
        path.AddArc(rect, startAngle, sweepAngle);
        canvas.DrawPath(path, ArcPaint);
    }
}

使用控件

在MainPage.xaml中添加一个CircleSlider控件,
设置的Maximum,是当前曲目的时长,Value是当前曲目的进度

<controls:CircleSlider 
    HeightRequest="250"
    WidthRequest="250"
    x:Name="MainCircleSlider"
    Maximum="{Binding Duration}"
    Minimum="0.0"
    TintColor="#FFFFFF"
    ContainerColor="#4CFFFFFF"
    IsEnabled="{Binding Canplay}"
    ValueChanged="OnValueChanged"
    Value="{Binding CurrentTime,Mode=TwoWay} ">
</controls:CircleSlider>

创建专辑封面

使用MAUI的VisualElement中的Clip属性,创建Clip裁剪,可以传入一个Geometry对象,这里我们使用RoundRectangleGeometry,将它的CornerRadius属性设置为图片宽度的一半,即可实现圆形图片。

<Image HeightRequest="250"
        WidthRequest="250"
        Margin="7.5"
        Source="{Binding  CurrentMusic.AlbumArt}"
        VerticalOptions="CenterAndExpand"
        HorizontalOptions="CenterAndExpand"
        Aspect="AspectFill">
    <Image.Clip>
        <RoundRectangleGeometry  CornerRadius="125" Rect="0,0,250,250" />
    </Image.Clip>
</Image>

设置一个半透明背景的播放状态指示器,当IsPlaying为False时将显示一个播放按钮

<Grid IsVisible="{Binding IsPlaying, Converter={StaticResource True2FalseConverter}}">
    <BoxView HeightRequest="250"
            WidthRequest="250"
            Margin="7.5"
            Color="#60000000"
            VerticalOptions="CenterAndExpand"
            HorizontalOptions="CenterAndExpand"
            CornerRadius="250" ></BoxView>
    <Label  x:Name="PauseLabel"                               
            HorizontalOptions="CenterAndExpand"
            FontSize="58"  
            TextColor="{Binding Canplay,Converter={StaticResource Bool2StringConverter},ConverterParameter=White|#434343}"
            FontFamily="FontAwesome"
            Margin="0"></Label>
</Grid>

在这里插入图片描述

创建PanContainer对象,用于实现拖动效果,设置AutoAdsorption属性为True,即可实现拖动后自动吸附效果。

关于PanContainer请查看上期的文章:平移手势交互

用一个Grid将专辑封面,CircleSlider,以及播放状态指示器包裹起来。完整代码如下

 <controls1:PanContainer BackgroundColor="Transparent"
                        x:Name="DefaultPanContainer"
                        OnTapped="DefaultPanContainer_OnOnTapped"
                        AutoAdsorption="True"
                        OnfinishedChoise="DefaultPanContainer_OnOnfinishedChoise">
    <Grid PropertyChanged="BindableObject_OnPropertyChanged"
        VerticalOptions="Start"
        HorizontalOptions="Start">
        <Image HeightRequest="250"
                WidthRequest="250"
                Margin="7.5"
                Source="{Binding  CurrentMusic.AlbumArt}"
                VerticalOptions="CenterAndExpand"
                HorizontalOptions="CenterAndExpand"
                Aspect="AspectFill">
            <Image.Clip>
                <RoundRectangleGeometry  CornerRadius="125" Rect="0,0,250,250" />
            </Image.Clip>
        </Image>
        <controls:CircleSlider>...</controls:CircleSlider>
        <Grid IsVisible="{Binding IsPlaying, Converter={StaticResource True2FalseConverter}}">
            <BoxView HeightRequest="250"
                    WidthRequest="250"
                    Margin="7.5"
                    Color="#60000000"
                    VerticalOptions="CenterAndExpand"
                    HorizontalOptions="CenterAndExpand"
                    CornerRadius="250" ></BoxView>
            <Label  x:Name="PauseLabel"                               
                    HorizontalOptions="CenterAndExpand"
                    FontSize="58"  
                    TextColor="{Binding Canplay,Converter={StaticResource Bool2StringConverter},ConverterParameter=White|#434343}"
                    FontFamily="FontAwesome"
                    Margin="0"></Label>
        </Grid>
    </Grid>
</controls1:PanContainer>

在这里插入图片描述


以上就是这个项目的全部内容,感谢阅读

项目地址 

Github:maui-samples

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

用html+javascript打造公文一键排版系统9:主送机关排版

一、主送机关的规定 公文一般在标题和正文之间还有主送机关&#xff0c;相关规定为&#xff1a; 主送机关 编排于标题下空一行位置&#xff0c;居左顶格&#xff0c;回行时仍顶格&#xff0c;最后一个机关名称后标全角冒号。如主送机关名称过多导致公文首页不能显示正文时&…

使用elementplus实现文本框的粘贴复制

需求&#xff1a; 文本框仅用于显示展示数据并且用户可以进行复制&#xff0c;并不会进行修改和编辑&#xff0c; 注意点&#xff1a; 1.首先且文本为多行。所以不能使用普通的el-input&#xff0c;这种一行超出就会隐藏了&#xff0c;如果多行超出行数也会隐藏&#xff08;…

uniApp 对接安卓平板刷卡器, 读取串口数据

背景: 设备: 鸿合 电子班牌 刷卡对接 WS-B22CS, 安卓11; 需求: 将刷卡器的数据传递到自己的App中, 作为上下岗信息使用, 以完成业务; 对接方式: 1. 厂家技术首先推荐使用 接收自定义广播的方式来获取, 参考代码如下 对应到uniApp 中的实现如下 <template><view c…

【AltWalker】模型驱动:轻松实现自动化测试用例的生成和组织执行

目录 模型驱动的自动化测试 优势 操作步骤 什么是AltWalker&#xff1f; 安装AltWalker 检查是否安装了正确的版本 牛刀小试 创建一个测试项目 运行测试 运行效果 在线模型编辑器 VScode扩展 本地部署 包含登录、选择产品、支付、退出登录的模型编写 模型效果 1…

SVN代码迁移到Git方法

1.在SVN上新增一个项目 一、点击新建项目 二、创建空白项目 三、填入项目信息 四、myProject项目模板创建成功 2.将代码提交到Git 一、新建一个文件夹myProject&#xff0c;将从SVN下载过来的代码复制一份拷贝到该文件夹下&#xff0c;注意&#xff1a;不要把.SVN文件拷…

2023年第四届“华数杯”数学建模思路 - 案例:感知机原理剖析及实现

# 赛题思路 &#xff08;赛题出来以后第一时间在CSDN分享&#xff09; https://blog.csdn.net/dc_sinor?typeblog 一、感知机的直观理解 感知机应该属于机器学习算法中最简单的一种算法&#xff0c;其原理可以看下图&#xff1a; 比如说我们有一个坐标轴&#xff08;图中的…

【python爬虫】获取某一个网址下面抓取所有的a 超链接下面的内容

import requests as rq from bs4 import BeautifulSoup as bs import re# rooturl是传的是我需要查询和抓取的一个网址&#xff0c;可以是html js 等 def gethtml(rooturl, encoding"utf-8"):#默认解码方式utf-8response rq.get(rooturl)response.encoding encodin…

解决Mysql报错2003 (HY000): Can‘t connect to MySQL server on ‘localhost‘ (10061)

1.找到mysql文件夹&#xff0c;将my,ini文件放入bin文件夹 2.管理员模式打开cmd 3.输入netstat -ano查看端口占用情况&#xff0c;这里我已经开启mysql应用&#xff0c;所以会有3306&#xff0c;如果没有开启是不会有的 4.输入sc delete mysql&#xff0c;删除mysql服务 5.将…

redis的安装和配置

一、nosql 二、redis的安装和配置 redis的安装&#xff1a; redis常见配置&#xff1a; 配置文件redis.conf

自动化测试中的数据驱动

DDT 当测试框架是unittest时&#xff0c;可以使用ddt。ddt 这个类装饰器必须装饰在 TestCase 的子类上&#xff0c;TestCase 是 unittest 框架中的一个基类&#xff0c;它实现了 Test Runner 驱动测试运行所需的接口&#xff08;interface&#xff09;。 DDT 的使用步骤如下&…

软件定时器

Q: 什么是定时器&#xff1f; A: 其实在单片机的学习中&#xff0c;已经接触过无数次定时器了&#xff0c;所谓定时器&#xff0c;简单可以理解为闹钟&#xff0c;到达指定一段时间后&#xff0c;就会响铃。 STM32 芯片自带硬件定时器&#xff0c;精度较高&#xff0c;达到定时…

IDEA删除本地git仓库、创建本地git仓库、关联其他仓库并上传

IDEA删除本地git仓库、创建本地git仓库、关联其他仓库并上传 删除本地Git仓库 创建本地Git仓库 关联其他仓库并上传 要在IntelliJ IDEA中删除本地Git仓库并创建新的本地Git仓库&#xff0c;以及关联其他仓库并上传&#xff0c;请按照以下步骤进行操作&#xff1a; 删除本地G…

openSUSE安装虚拟化 qemu kvm

1) 第一种&#xff1a;图形界面yast安装虚拟化 左下角开始菜单搜索yast 点一下就能安装&#xff0c;是不是很简单呢 2&#xff09;第二种&#xff1a; 命令行安装 网上关于openSUSE安装qemu kvm的教程比较少&#xff0c;可以搜索centos7 安装qemu kvm的教程&#xff0c;然后…

NLP From Scratch: 生成名称与字符级RNN

NLP From Scratch: 生成名称与字符级RNN 这是我们关于“NLP From Scratch”的三个教程中的第二个。 在<cite>第一个教程< / intermediate / char_rnn_classification_tutorial ></cite> 中&#xff0c;我们使用了 RNN 将名称分类为来源语言。 这次&#xff…

MP4如何转MP3?教你简单好用的转换方法

怎么将MP4格式的视频转换成MP3音频呢&#xff1f;相信大家在看短视频的时候&#xff0c;有的视频背景音乐非常好听&#xff0c;我们又搜不到这个音频的源声在哪里&#xff0c;这时候我们就可以将这段视频直接转换成MP3格式音频&#xff0c;用来当做铃声或者是闹钟非常合适&…

【css】解决元素浮动溢出问题

如果一个元素比包含它的元素高&#xff0c;并且它是浮动的&#xff0c;它将“溢出”到其容器之外&#xff1a;然后可以向包含元素添加 overflow: auto;&#xff0c;来解决此问题&#xff1a; 代码&#xff1a; <!DOCTYPE html> <html> <head> <style>…

解决有道云笔记自动更新问题 关闭有道云笔记自动更新 有道云笔记自动升级v7.2.8

解决有道云笔记自动更新问题 关闭有道云笔记自动更新 有道云笔记自动升级v7.2.8 一、背景 最近使用有道云笔记时&#xff0c;遇到bug问题&#xff1a; 打不开加密的笔记&#xff0c;发现是最新版本v7.2.8&#xff0c;切换到v7.2.7后&#xff0c;一切恢复正常。 在使用有道云笔…

机器学习深度学习——数值稳定性和模型化参数(详细数学推导)

&#x1f468;‍&#x1f393;作者简介&#xff1a;一位即将上大四&#xff0c;正专攻机器学习的保研er &#x1f30c;上期文章&#xff1a;机器学习&&深度学习——Dropout &#x1f4da;订阅专栏&#xff1a;机器学习&&深度学习 希望文章对你们有所帮助 这一部…

MySQL 极速安装使用与卸载版

mysql-5.6.51 极速安装使用与卸载版 mysql-8.1.0下载安装在后 mysql中国官网 MySQLhttps://www.mysql.com/cn/ 点击MySQL社区服务器 点击历史档案 下载完 解压 用管理员运行cmd&#xff0c;进入bin目录&#xff0c;输入mysqld --install&#xff0c;进行安装 这样&#xff…

【LeetCode】88. 合并两个有序数组

这道题我总共想了三种解法。 1.将nums2中的元素依次放入nums1有效元素的后面&#xff0c;再总体进行排序。 import java.util.*; class Solution {public void merge(int[] nums1, int m, int[] nums2, int n) {int j 0;for(int i m;i<mn;i){nums1[i] nums2[j];j;}Arrays…