[UWP] 模仿哔哩哔哩的一键三连

1. 一键三连#

什么是一键三连?

哔哩哔哩弹幕网中用户可以通过长按点赞键同时完成点赞、投币、收藏对UP主表示支持,后UP主多用“一键三连”向视频浏览者请求对其作品同时进行点赞、投币、收藏。

去年在云之幻大佬的 哔哩 项目里看到一键三连的 UWP 实现,觉得挺有趣的,这次参考它的代码重新实现一次,最终成果如下:

下面这些是一键三连的核心功能:

  • 可以控制并显示进度
  • 有普通状态和完成状态
  • 可以点击或长按
  • 当切换到完成状态时弹出写泡泡
  • 点击切换状态
  • 长按 2 秒钟切换状态,期间有进度显示

这篇文章将介绍如何使用自定义控件实现上面的功能。写简单的自定义控件的时候,我推荐先写完代码,然后再写控件模板,但这个控件也适合一步步增加功能,所以这篇文章用逐步增加功能的方式介绍如何写这个控件。

2. ProgressButton#

万事起头难,做控件最难的是决定控件名称。不过反正也是玩玩的 Demo,就随便些用 ProgressButton 吧,因为有进度又可以点击。

第二件事就是决定这个按钮继承自哪个控件,可以选择继承 Button 或 RangeBase 以减少需要自己实现的功能。因为长按这个需求破坏了点击这个行为,所以还是放弃 Button 选择 RangeBase 比较好。然后再加上 Content 属性,控件的基础代码如下:

Copy
[ContentProperty(Name = nameof(Content))] public partial class ProgressButton : RangeBase { public ProgressButton() { DefaultStyleKey = typeof(ProgressButton); } public object Content { get => (object)GetValue(ContentProperty); set => SetValue(ContentProperty, value); } }

在控件模板中用一个 CornerRadius 很大的 Border 模仿圆形边框,ContentControl 显示 Content,RadialProgressBar 显示进度,控件模板的大致结构如下:

Copy
<ControlTemplate TargetType="local:ProgressButton"> <Grid x:Name="RootGrid"> <Border x:Name="RootBorder" Margin="{TemplateBinding Padding}" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="1" CornerRadius="100"> <ContentControl x:Name="ContentControl" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}" Content="{TemplateBinding Content}" Foreground="{TemplateBinding Foreground}" /> </Border> <control:RadialProgressBar x:Name="PressProgressBar" Background="Transparent" Foreground="{StaticResource PrimaryColor}" Maximum="{TemplateBinding Maximum}" Minimum="{TemplateBinding Minimum}" Outline="Transparent" Value="{TemplateBinding Value}" /> </Grid> </ControlTemplate>

这时候的调用方式及效果如下所示:

Copy
<lab:ProgressButton x:Name="LikeButton" Content="&#xE9F0;" /> <lab:ProgressButton x:Name="CoinButton" Content="&#xEA45;" Value="0.5" /> <lab:ProgressButton x:Name="FavoriteButton" Content="&#xE9E5;" Value="1" />

3. 状态#

有了上面的代码,后面的功能只需要按部就班地一个个添加上去。我从以前的代码里抄来状态相关的代码。虽然定义了这么多状态备用,其实我也只用到 Idle 和 Completed,其它要用到的话可以修改 ControlTemplate。

Copy
public enum ProgressState { Idle, InProgress, Completed, Faulted, }
  • Idle,空闲的状态。
  • InProgress,开始的状态,暂时不作处理。
  • Completed,完成的状态。
  • Faulted,出错的状态,暂时不作处理。

在控件模板中添加一个粉红色的带一个同色阴影的圆形背景,其它状态下隐藏,在切换到 Completed 状态时显示。为了好看,还添加了 ImplictAnimation 控制淡入淡出。

Copy
<ContentControl x:Name="CompletedElement" Template="{StaticResource CompletedTemplate}" Visibility="Collapsed"> <animations:Implicit.HideAnimations> <animations:OpacityAnimation SetInitialValueBeforeDelay="True" From="1" To="0" Duration="0:0:0.3" /> </animations:Implicit.HideAnimations> <animations:Implicit.ShowAnimations> <animations:OpacityAnimation SetInitialValueBeforeDelay="True" From="0" To="1" Duration="0:0:0.6" /> </animations:Implicit.ShowAnimations> </ContentControl>

在 VisualStateManager 中加入 ProgressStates 这组状态,只需要控制 Completed 状态的 Setters,显示粉红色的背景,隐藏边框,文字变白色。

Copy
<VisualStateGroup x:Name="ProgressStates"> <VisualState x:Name="Idle" /> <VisualState x:Name="InProgress" /> <VisualState x:Name="Completed"> <VisualState.Setters> <Setter Target="RootBorder.BorderBrush" Value="Transparent" /> <Setter Target="ContentControl.Foreground" Value="White" /> <Setter Target="CompletedElement.Visibility" Value="Visible" /> </VisualState.Setters> </VisualState> <VisualState x:Name="Faulted" /> </VisualStateGroup>

4. Button 的 CommonStates#

作为一个 Button,按钮的 PointOver 和 Pressed 状态当然必不可少,这些逻辑我参考了 真篇文章 最后一部分代码(不过我没有加入 Click 事件)。在控件模板中也制作了最简单的处理:

Copy
<VisualStateGroup x:Name="CommonStates"> <VisualState x:Name="PointerOver"> <VisualState.Setters> <Setter Target="ContentControl.Opacity" Value="0.8" /> </VisualState.Setters> </VisualState> <VisualState x:Name="Pressed"> <VisualState.Setters> <Setter Target="ContentControl.Opacity" Value="0.6" /> </VisualState.Setters> </VisualState> </VisualStateGroup>

5. 气泡#

气泡动画来源于火火的 BubbleButton,它封装得很优秀,ProgressButton 只需要在 Completed 状态下设置 BubbleView.IsBubbing = true 即可触发气泡动画,这大大减轻了 XAML 的工作:

Copy
<Setter Target="BubbleView.IsBubbing" Value="True" /> <bubblebutton:BubbleView x:Name="BubbleView" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Foreground="{StaticResource PrimaryColor}" />

6. Tapped 和 Holding#

因为要实现长按功能,所以我没有实现 Button 的 Click,而是使用了 GestureRecognizer 的 Tapped 和 Holding,订阅这两个事件,触发后重新抛出。

Copy
private GestureRecognizer _gestureRecognizer = new GestureRecognizer(); public ProgressButton() { _gestureRecognizer.GestureSettings = GestureSettings.HoldWithMouse | GestureSettings.Tap | GestureSettings.Hold; _gestureRecognizer.Holding += OnGestureRecognizerHolding; _gestureRecognizer.Tapped += OnGestureRecognizerTapped; } public event EventHandler<HoldingEventArgs> GestureRecognizerHolding; public event EventHandler<TappedEventArgs> GestureRecognizerTapped; protected override void OnPointerPressed(PointerRoutedEventArgs e) { // SOME CODE var points = e.GetIntermediatePoints(null); if (points != null && points.Count > 0) { _gestureRecognizer.ProcessDownEvent(points[0]); e.Handled = true; } } protected override void OnPointerReleased(PointerRoutedEventArgs e) { // SOME CODE var points = e.GetIntermediatePoints(null); if (points != null && points.Count > 0) { _gestureRecognizer.ProcessUpEvent(points[0]); e.Handled = true; _gestureRecognizer.CompleteGesture(); } } protected override void OnPointerMoved(PointerRoutedEventArgs e) { // SOME CODE _gestureRecognizer.ProcessMoveEvents(e.GetIntermediatePoints(null)); } private void OnGestureRecognizerTapped(GestureRecognizer sender, TappedEventArgs args) { GestureRecognizerTapped?.Invoke(this, args); } private void OnGestureRecognizerHolding(GestureRecognizer sender, HoldingEventArgs args) { GestureRecognizerHolding?.Invoke(this, args); }

由于一键三连属于业务方面的功能(要联网、检查状态、还可能回退),不属于控件应该提供的功能,所以 ProgressButton 只需要实现到这一步就完成了。

7. 实现一键三连#

终于要实现一键三连啦。首先创建三个 ProgressButton, 然后互相双向绑定 Value 的值并订阅事件:

Copy
<lab:ProgressButton x:Name="LikeButton" Content="&#xE9F0;" GestureRecognizerHolding="OnGestureRecognizerHolding" GestureRecognizerTapped="OnGestureRecognizerTapped" /> <lab:ProgressButton x:Name="CoinButton" Content="&#xEA45;" GestureRecognizerHolding="OnGestureRecognizerHolding" GestureRecognizerTapped="OnGestureRecognizerTapped" Value="{Binding ElementName=LikeButton, Path=Value}" /> <lab:ProgressButton x:Name="FavoriteButton" Content="&#xE9E5;" GestureRecognizerHolding="OnGestureRecognizerHolding" GestureRecognizerTapped="OnGestureRecognizerTapped" Value="{Binding ElementName=LikeButton, Path=Value}" />

处理 Tapped 的代码很简单,就是反转一下状态:

Copy
private void OnGestureRecognizerTapped(object sender, Windows.UI.Input.TappedEventArgs e) { var progressButton = sender as ProgressButton; if (progressButton.State == ProgressState.Idle) progressButton.State = ProgressState.Completed; else progressButton.State = ProgressState.Idle; }

Holding 的代码就复杂一些,设置一个动画的 Taget 然后启动动画,动画完成后把所有 ProgressButton 的状态改为 Completed,最后效果可以参考文章开头的 gif:

Copy
private void OnGestureRecognizerHolding(object sender, Windows.UI.Input.HoldingEventArgs e) { var progressButton = sender as ProgressButton; if (e.HoldingState == HoldingState.Started) { if (!_isAnimateBegin) { _isAnimateBegin = true; (_progressStoryboard.Children[0] as DoubleAnimation).From = progressButton.Minimum; (_progressStoryboard.Children[0] as DoubleAnimation).To = progressButton.Maximum; Storyboard.SetTarget(_progressStoryboard.Children[0] as DoubleAnimation, progressButton); _progressStoryboard.Begin(); } } else { _isAnimateBegin = false; _progressStoryboard.Stop(); } } private void OnProgressStoryboardCompleted(object sender, object e) { LikeButton.State = ProgressState.Completed; CoinButton.State = ProgressState.Completed; FavoriteButton.State = ProgressState.Completed; }

8. 最后#

很久没有认真写 UWP 的博客了,我突然有了个大胆的想法,在这个时间点,会不会就算我胡说八道都不会有人认真去验证我写的内容?毕竟现在写 UWP 的人又不多。不过放心,我对 UWP 是认真的,我保证我是个诚实的男人。

不过这个一键三连功能做出来后,又好像,完全没机会用到嘛。难得都做出来了,就用来皮一下。

9. 源码#

uwp_design_and_animation_lab

posted @   dino.c  阅读(4465)  评论(13编辑  收藏  举报
编辑推荐:
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· .NET Core 托管堆内存泄露/CPU异常的常见思路
· PostgreSQL 和 SQL Server 在统计信息维护中的关键差异
· C++代码改造为UTF-8编码问题的总结
· DeepSeek 解答了困扰我五年的技术问题
阅读排行:
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
点击右上角即可分享
微信分享提示
CONTENTS