zoukankan      html  css  js  c++  java
  • [UWP] 模仿哔哩哔哩的一键三连

    1. 一键三连

    什么是一键三连?

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

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

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

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

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

    2. ProgressButton

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

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

    [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 显示进度,控件模板的大致结构如下:

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

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

    <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。

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

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

    <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,显示粉红色的背景,隐藏边框,文字变白色。

    <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 事件)。在控件模板中也制作了最简单的处理:

    <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 的工作:

    <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,订阅这两个事件,触发后重新抛出。

    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 的值并订阅事件:

    <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 的代码很简单,就是反转一下状态:

    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:

    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


    作者:dino.c
    出处:http://www.cnblogs.com/dino623/
    说明:欢迎转载并请标明来源和作者。如有错漏请指出,谢谢。
  • 相关阅读:
    box-shadow中的理解(bootstrap)
    setTimeout用于取消多次执行mouseover或者mouseenter事件,间接实现hover的悬停加载的效果.
    把之前能运行的springboot项目重新打开运行,报错 Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.
    angularjs服务json文件实现省市区三级联动
    js页面用户信息填写表单
    js页面中实现加载更多功能
    遇见未知的自己
    ASP.NET Core 入门教程1 ASP.NET Core 读取配置文件
    有一种选择叫女程(5)
    有一种选择叫女程(4)
  • 原文地址:https://www.cnblogs.com/dino623/p/Three_Actions_With_One_Click.html
Copyright © 2011-2022 走看看