[MAUI]模仿哔哩哔哩的一键三连
@
哔哩哔哩(Bilibili)中用户可以通过长按点赞键同时完成点赞、投币、收藏对UP主表示支持,后UP主多用“一键三连”向视频浏览者请求对其作品同时进行点赞、投币、收藏。
“三连按钮”是一组按钮,轻击时当做普通状态按钮使用,当长按 2 秒钟后,转为三连模式,可以控制并显示进度,并在进度完成时弹出一些泡泡
一直想实现这个效果,但由于.NET MAUI对图形填充渲染问题直到.NET 8才修复。想要的效果直到最近才能实现。
两年前Dino老师用UWP实现过“一键三连”:
今天用MAUI实现。
使用.NET MAU实现跨平台支持,本项目可运行于Android、iOS平台。
创建弧形进度条
新建.NET MAUI项目,命名HoldDownButtonGroup
在项目中添加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>
进度条(ProgressBar)用于展示任务的进度,告知用户当前状态和预期,本项目依赖弧形进度条组件(CircleProgressBar),此组件在本项目中用于展示“三连按钮”长按任务的进度
这里简单介绍弧形进度条组件的原理和实现,更多内容请阅读:[MAUI]弧形进度条与弧形滑块的交互实现。
控件将包含以下可绑定属性:
- Maxiumum:最大值
- Minimum:最小值
- Progress:当前进度
- AnimationLength:动画时长
- BorderWidth:描边宽度
- LabelContent:标签内容
- ContainerColor:容器颜色,即进度条的背景色
- ProgressColor:进度条颜色
创建CircleProgressBar.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="HoldDownButtonGroup.Controls.CircleProgressBar">
<ContentView.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>
</ContentView.Content>
</ContentView>
SKCanvasView是SkiaSharp.Views.Maui.Controls封装的View控件。
绘制弧
Skia中,通过AddArc方法绘制弧,需要传入一个SKRect对象,其代表一个弧(或椭弧)的外接矩形。startAngle和sweepAngle分别代表顺时针起始角度和扫描角度。
通过startAngle和sweepAngle可以绘制出一个弧,如下图红色部分所示:
在CircleProgressBar.xaml.cs
的CodeBehind中,创建OnCanvasViewPaintSurface,通过给定起始角度为正上方,扫描角度为360对于100%进度,通过插值计算出当前进度对应的扫描角度,绘制出进度条。
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)((_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;
创建进度条轨道背景画刷和进度条画刷,其中进度条画刷的StrokeCap属性设置为SKStrokeCap.Round,使得进度条两端为圆形。
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;
}
}
在Progress值变更时,重新渲染进度条,并触发ValueChanged事件。
private void UpdateProgress()
{
this._realtimeProgress = this.Progress;
this.labelView.Text = this.Progress.ToString(LABEL_FORMATE);
this.canvasView?.InvalidateSurface();
}
效果如下
准备物料
点赞、投币、收藏三个按钮的图片来自于哔哩哔哩(Bilibili)网站。这些按钮用svg格式在html中。
我们只需前往哔哩哔哩主站,要打开浏览器的开发者工具,用元素检查器,在找到按钮位置后查看其样式,拷贝path中的svg代码,即可得到这些矢量图片。
拷贝右侧红色区域选中的部分,我们只需要这些svg代码。
在Xaml中我们创建Path元素,并设置Data属性为svg代码。即可得到一个图形
<Path HeightRequest="65"
WidthRequest="65"
x:Name="Icon1"
Fill="Transparent"
Aspect="Uniform"
Data="M 9.77234 30.8573 V 11.7471 H 7.54573 C 5.50932 11.7471 3.85742 13.3931 3.85742 15.425 V 27.1794 C 3.85742 29.2112 5.50932 30.8573 7.54573 30.8573 H 9.77234 Z M 11.9902 30.8573 V 11.7054 C 14.9897 10.627 16.6942 7.8853 17.1055 3.33591 C 17.2666 1.55463 18.9633 0.814421 20.5803 1.59505 C 22.1847 2.36964 23.243 4.32583 23.243 6.93947 C 23.243 8.50265 23.0478 10.1054 22.6582 11.7471 H 29.7324 C 31.7739 11.7471 33.4289 13.402 33.4289 15.4435 C 33.4289 15.7416 33.3928 16.0386 33.3215 16.328 L 30.9883 25.7957 C 30.2558 28.7683 27.5894 30.8573 24.528 30.8573 H 11.9911 H 11.9902 Z"
VerticalOptions="Center"
HorizontalOptions="Center" />
创建气泡
气泡实现分为两个步骤:
Bubble.xaml 创建单一气泡动画
Bubbles.xaml 包含气泡集群。随机生成气泡动画路径,创建气泡集群的动画
Bubbles控件将包含以下可绑定属性:
- Brush:气泡颜色
- BubbleCnt:气泡数量
- BubbleSize:气泡大小
气泡动画算法参考于火火的 BubbleButton,这里只帖关键代码
单一气泡的动画:先变大后消失
public Animation GetAnimation()
{
var scaleAnimation = new Animation();
var scaleUpAnimation0 = new Animation(v => MainBox.Scale = v, 0, 1);
var scaleUpAnimation1 = new Animation(v => MainBox.Scale = v, 1, 0);
scaleAnimation.Add(0, 0.2, scaleUpAnimation0);
scaleAnimation.Add(0.8, 1, scaleUpAnimation1);
scaleAnimation.Finished = () =>
{
this.MainBox.Scale = 0;
};
return scaleAnimation;
}
生成气泡
public void SpawnBubbles()
{
this.PitContentLayout.Clear();
for (int i = 0; i < BubbleCnt; i++)
{
var currentBox = new Bubble();
currentBox.FillColor = i % 2 == 0 ? this.Brush : SolidColorBrush.Transparent;
currentBox.BorderColor = this.Brush;
currentBox.HeightRequest = BubbleSize;
currentBox.WidthRequest = BubbleSize;
currentBox.HorizontalOptions = LayoutOptions.Start;
currentBox.VerticalOptions = LayoutOptions.Start;
this.PitContentLayout.Add(currentBox);
}
}
计算单个气泡的动画路径:随机产生动画运动的随机坐标
private Animation InitAnimation(Bubble element, Size targetSize, bool isOnTop = true)
{
var offsetAnimation = new Animation();
if (targetSize == default)
{
targetSize = element.DesiredSize;
}
var easing = Easing.Linear;
var originX = PitContentLayout.Width / 2;
var originY = PitContentLayout.Height / 2;
var targetX = rnd.Next(-(int)targetSize.Width, (int)targetSize.Width) + (int)targetSize.Width / 2 + originX;
var targetY = isOnTop ? rnd.Next(-(int)(targetSize.Height * 1.5), 0) + (int)targetSize.Height / 2 + originY :
rnd.Next(0, (int)(targetSize.Height * 1.5)) + (int)targetSize.Height / 2 + originY
;
var offsetX = targetX - originX;
var offsetY = targetY - originY;
var offsetAnimation1 = new Animation(v => element.TranslationX = v, originX - targetSize.Width / 2, targetX - targetSize.Width / 2, easing);
var offsetAnimation2 = new Animation(v => element.TranslationY = v, originY - targetSize.Height / 2, targetY - targetSize.Height / 2, easing);
offsetAnimation.Add(0.2, 0.8, offsetAnimation1);
offsetAnimation.Add(0.2, 0.8, offsetAnimation2);
offsetAnimation.Add(0, 1, element.BoxAnimation);
offsetAnimation.Finished = () =>
{
element.TranslationX = originX;
element.TranslationY = originY;
element.Rotation = 0;
};
return offsetAnimation;
}
开始气泡动画
public void StartAnimation()
{
Content.AbortAnimation("ReshapeAnimations");
var offsetAnimationGroup = new Animation();
foreach (var item in this.PitContentLayout.Children)
{
if (item is Bubble)
{
var isOntop = this.PitContentLayout.Children.IndexOf(item) > this.PitContentLayout.Children.Count / 2;
var currentAnimation = InitAnimation(item as Bubble, targetSize, isOntop);
offsetAnimationGroup.Add(0, 1, currentAnimation);
}
}
offsetAnimationGroup.Commit(this, "ReshapeAnimations", 16, 400);
}
创建手势
可喜可贺,在新发布的.NET 8 中, .NET MAUI 引入了指针手势识别器(PointerGestureRecognizer),使用方式如下,终于不用自己实现手势监听控件了。
Xaml:
<Image Source="dotnet_bot.png">
<Image.GestureRecognizers>
<PointerGestureRecognizer PointerEntered="OnPointerEntered"
PointerExited="OnPointerExited"
PointerMoved="OnPointerMoved" />
</Image.GestureRecognizers>
</Image>
C#:
void OnPointerEntered(object sender, PointerEventArgs e)
{
// Handle the pointer entered event
}
void OnPointerExited(object sender, PointerEventArgs e)
{
// Handle the pointer exited event
}
void OnPointerMoved(object sender, PointerEventArgs e)
{
// Handle the pointer moved event
}
具体请阅读官方文档:
.NET 8 中 .NET MAUI 的新增功能以及
识别指针手势
在本项目中,需要监听长按动作,当“三连按钮”长按2秒后,转为三连模式,此时需要监听手指释放情况,当时长不足时取消三连。
由于在之前的文章中实现过监听手势,这里仅简单介绍定义,其余内容不再重复,如需了解请阅读: [MAUI程序设计] 用Handler实现自定义跨平台控件
定义可以监听的手势类别,分别是按下、移动、抬起、取消、进入、退出
public enum TouchActionType
{
Entered,
Pressed,
Moved,
Released,
Exited,
Cancelled
}
添加手势监听器TouchRecognizer,它将提供一个事件OnTouchActionInvoked,用触发手势动作。
public partial class TouchRecognizer: IDisposable
{
public event EventHandler<TouchActionEventArgs> OnTouchActionInvoked;
public partial void Dispose();
}
EventArg类TouchActionEventArgs,用于传递手势动作的参数
创建交互与动效
在页面创建完三个按钮后,在CodeBehind中编写交互逻辑
添加更新圆环进度的方法UpdateProgressWithAnimate,此方法根据圆环进度的百分比,计算圆环进度的动画时间,并开始动画
private void UpdateProgressWithAnimate(CircleProgressBar progressElement, double progressTarget = 100, double totalLenght = 5*1000, Action<double, bool> finished = null)
{
Content.AbortAnimation("ReshapeAnimations");
var scaleAnimation = new Animation();
double progressOrigin = progressElement.Progress;
var animateAction = (double r) =>
{
progressElement.Progress = r;
};
var scaleUpAnimation0 = new Animation(animateAction, progressOrigin, progressTarget);
scaleAnimation.Add(0, 1, scaleUpAnimation0);
var calcLenght = (double)(Math.Abs((progressOrigin - progressTarget) / 100)) * totalLenght;
scaleAnimation.Commit(progressElement, "ReshapeAnimations", 16, (uint)calcLenght, finished: finished);
}
创建“三连成功”的动画方法StartCelebrationAnimate,在这里通过改变按钮的Scale和Fill属性,实现三连成功时“点亮”图标的动画效果。
private void StartCelebrationAnimate(Path progressElement, Action<double, bool> finished = null)
{
var toColor = (Color)this.Resources["BrandColor"];
var fromColor = (Color)this.Resources["DisabledColor"];
var scaleAnimation = new Animation();
var scaleUpAnimation0 = new Animation(v => progressElement.Scale=v, 0, 1.5);
var scaleUpAnimation1 = new Animation(v => progressElement.Fill=GetColor(v, fromColor, toColor), 0, 1);
var scaleUpAnimation2 = new Animation(v => progressElement.Scale=v, 1.5, 1);
scaleAnimation.Add(0, 0.5, scaleUpAnimation0);
scaleAnimation.Add(0, 1, scaleUpAnimation1);
scaleAnimation.Add(0.5, 1, scaleUpAnimation2);
scaleAnimation.Commit(progressElement, "CelebrationAnimate", 16, 400, finished: finished);
}
按钮触发TouchContentView_OnTouchActionInvoked事件:
_dispatcherTimer作用是延时1秒,如果按钮被点击,则开始执行后续操作。当按钮被点击时此Timer会开。
一秒后,开始进入“三连模式”,此时更新圆环进度,当进度完成后开始气泡动画和“三连成功”的动画。
若中途按钮被释放,则取消此Timer,并重置圆环进度。
若按钮未进入“三连模式”,则直接播放按钮的点击动画。
private void TouchContentView_OnTouchActionInvoked(object sender, TouchActionEventArgs e)
{
var layout = ((Microsoft.Maui.Controls.Layout)(sender as TouchContentView).Content).Children;
var bubbles = layout[0] as Bubbles;
var circleProgressBar = layout[1] as CircleProgressBar;
switch (e.Type)
{
case TouchActionType.Entered:
break;
case TouchActionType.Pressed:
_dispatcherTimer =Dispatcher.CreateTimer();
_dispatcherTimer.Interval=new TimeSpan(0, 0, 1);
_dispatcherTimer.Tick+= async (o, e) =>
{
_isInProgress=true;
this.UpdateProgressWithAnimate(ProgressBar1, 100, 2*1000, (d, b) =>
{
if (circleProgressBar.Progress==100)
{
this.bubbles1.StartAnimation();
StartCelebrationAnimate(this.Icon1);
}
});
this.UpdateProgressWithAnimate(ProgressBar2, 100, 2*1000, (d, b) =>
{
if (circleProgressBar.Progress==100)
{
this.bubbles2.StartAnimation();
StartCelebrationAnimate(this.Icon2);
}
});
this.UpdateProgressWithAnimate(ProgressBar3, 100, 2*1000, (d, b) =>
{
if (circleProgressBar.Progress==100)
{
this.bubbles3.StartAnimation();
StartCelebrationAnimate(this.Icon3);
}
});
};
_dispatcherTimer.Start();
break;
case TouchActionType.Moved:
break;
case TouchActionType.Released:
if (!_isInProgress)
{
var brandColor = (Color)this.Resources["BrandColor"];
var disabledColor = (Color)this.Resources["DisabledColor"];
if (circleProgressBar.Progress==100)
{
this.UpdateProgressWithAnimate(ProgressBar1, 0, 1000);
this.UpdateProgressWithAnimate(ProgressBar2, 0, 1000);
this.UpdateProgressWithAnimate(ProgressBar3, 0, 1000);
(ProgressBar1.LabelContent as Path).Fill=disabledColor;
(ProgressBar2.LabelContent as Path).Fill=disabledColor;
(ProgressBar3.LabelContent as Path).Fill=disabledColor;
}
else
{
if (((circleProgressBar.LabelContent as Path).Fill as SolidColorBrush).Color==disabledColor)
{
StartCelebrationAnimate(circleProgressBar.LabelContent as Path);
}
else
{
(circleProgressBar.LabelContent as Path).Fill=disabledColor;
}
}
}
if (_dispatcherTimer!=null)
{
if (_dispatcherTimer.IsRunning)
{
_dispatcherTimer.Stop();
_isInProgress=false;
}
_dispatcherTimer=null;
if (circleProgressBar.Progress==100)
{
return;
}
this.UpdateProgressWithAnimate(ProgressBar1, 0, 1000);
this.UpdateProgressWithAnimate(ProgressBar2, 0, 1000);
this.UpdateProgressWithAnimate(ProgressBar3, 0, 1000);
}
break;
case TouchActionType.Exited:
break;
case TouchActionType.Cancelled:
break;
default:
break;
}
}
进入三联模式动画效果如下:
未进入三联模式动画效果如下:
最终效果如下:
项目地址
[MAUI]模仿哔哩哔哩的一键三连的更多相关文章
- [UWP] 模仿哔哩哔哩的一键三连
1. 一键三连 什么是一键三连? 哔哩哔哩弹幕网中用户可以通过长按点赞键同时完成点赞.投币.收藏对UP主表示支持,后UP主多用"一键三连"向视频浏览者请求对其作品同时进行点赞.投币 ...
- 仿哔哩哔哩应用客户端Android版源码项目
这是一款高仿哔哩哔哩安卓客户端,跟官方网的差不多吧,界面也几乎是一样的,应用里面也加了一些弹出广告,大家可以参考一下吧,安装测试包在源码文件那里,大家可以多多参考一下. 哔哩哔哩弹幕网是国内知名的弹幕 ...
- 在Python中用Request库模拟登录(四):哔哩哔哩(有加密,有验证码)
!已失效! 抓包分析 获取验证码 获取加密公钥 其中hash是变化的,公钥key不变 登录 其中用户名没有被加密,密码被加密. 因为在获取公钥的时候同时返回了一个hash值,推测此hash值与密码加密 ...
- YouTuboba视频搬运~哔哩哔哩
将YouTube上面的视频搬运到哔哩哔哩上面教程 1.首先选择YouTube上面一个视频,需要谷歌登录,然后保存这个视频播放链接. 2.在浏览器中输入这个网址:en.savefrom.net,点击En ...
- 可以在GitHub或者码云里 直接搜索 项目 比如 哔哩哔哩
韩梦飞沙 韩亚飞 313134555@qq.com yue31313 han_meng_fei_sha Search · 哔哩哔哩 哔哩哔哩 · 搜索 - 码云 还有就是 以前的项目 可以不要 ...
- Ajax介绍及爬取哔哩哔哩番剧索引追番人数排行
Ajax,是利用JavaScript在保证页面不被刷新,页面链接不改变的情况下与服务器交换数据并更新部分网页的技术.简单的说,Ajax使得网页无需刷新即可更新其内容.举个例子,我们用浏览器打开新浪微博 ...
- 如何下载B站哔哩哔哩(bilibili)弹幕网站上的视频呢?小白教你个简单方法
对于90后.00后来说,B站肯定听过吧.小编有一个苦恼的地方,有时候想把哔哩哔哩(bilibili)上看到的视频保存到手机相册,不知道咋操作啊.网上百度了下,都是要下载电脑软件的,有些还得要付费的.前 ...
- 2019 哔哩哔哩java面试笔试题 (含面试题解析)
本人5年开发经验.18年年底开始跑路找工作,在互联网寒冬下成功拿到阿里巴巴.今日头条.哔哩哔哩等公司offer,岗位是Java后端开发,因为发展原因最终选择去了哔哩哔哩,入职一年时间了,也成为了面 ...
- python预课05 爬虫初步学习+jieba分词+词云库+哔哩哔哩弹幕爬取示例(数据分析pandas)
结巴分词 import jieba """ pip install jieba 1.精确模式 2.全模式 3.搜索引擎模式 """ txt ...
- 最新 哔哩哔哩java校招面经 (含整理过的面试题大全)
从6月到10月,经过4个月努力和坚持,自己有幸拿到了网易雷火.京东.去哪儿.哔哩哔哩等10家互联网公司的校招Offer,因为某些自身原因最终选择了哔哩哔哩.6.7月主要是做系统复习.项目复盘.Leet ...
随机推荐
- NC200324 魔改森林
题目链接 题目 题目描述 曾经有一道叫做迷雾森林的题目,然而牛牛认为地图中的障碍太多,实在是太难了,所以删去了很多点,出了这道题. 牛牛给出了一个n行m列的网格图 初始牛牛处在最左下角的格点上(n+1 ...
- NC213912 芭芭拉冲鸭~(续)
题目链接 题目 题目描述 芭芭拉这次来到了一棵字母树,这同样是一棵无根树,每个节点上面有一个小写字母. 芭芭拉想知道,自己从x冲刺到y,从x走到y收集所有字母,选择其中一部分字母组成一个回文串,这个回 ...
- Python 写入文件、读取文件内容——open函数/readLines/Write/find函数用法
1.读取.txt整个文件 ww.txt文件在程序文件所在的目录,在文件存储在其他地方,ww.txt需要添加文件路径,如:E:\book1\ww.txt:读取后希望返回的是列表类型,将read改为rea ...
- java 如何实现开箱即用的敏感词控台服务?
sensitive-word-admin sensitive-word-admin 是基于 sensitive-word 实现的, 一款开箱即用的敏感词控台服务. 特性 基本的 CRUD 开箱即用的配 ...
- 使用winhex查看FAT16格式结构
winhex介绍 winhex可以直接查看磁盘二进制信息, 可以比较直观地查看到各种文件系统格式的区别. winhex使用 查看硬盘要管理员权限, 即启动的时候要用邮件管理员权限启动 点击Tools- ...
- Maven如何打包可执行jar包
假设我有一个maven项目叫:hello-world 新建一个HelloWorld类: package com.dylan.mvnbook.helloworld; public class Hello ...
- 问题:AttributeError: module 'lib' has no attribute 'OpenSSL_add_all_algorithms'
分析 在使用支付宝沙箱时,报了这个错误,该问题是没有安装openssl包 解决 pip3 install pyOpenSSL 安装后再次运行如果还是报错,请降低加密库 pip install cryp ...
- 05、etcd 读请求执行流程
本篇内容主要来源于自己学习的视频,如有侵权,请联系删除,谢谢. 1.etcd读请求概览 etcd是典型的读多写少存储,在我们实际业务场景中,读一般占据2/3以上的请求.一个读 请求从client通过R ...
- 在Ubuntu搭建DHCP服务器
一.提供DHCP的服务器,自己必须有固定的IP地址 不然局域网就乱了,服务器自身启动(比如搭建完DHCP服务后,重新启动了服务器)的时候,DHCP服务器没有IP地址,无法和自己的DHCP服务通信. 在 ...
- 【Azure 事件中心】在Windows系统中使用 kafka-consumer-groups.bat 查看Event Hub中kafka的consumer groups信息
问题描述 使用 Apache Flink 连接支持 Apache Kafka的Azure Event Hub后,由于消费端的Consumer Group是动态创建,在门户页面和Service Bus ...