Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >[UWP]为番茄钟应用设计一个平平无奇的状态按钮

[UWP]为番茄钟应用设计一个平平无奇的状态按钮

作者头像
dino.c
发布于 2019-11-13 13:30:29
发布于 2019-11-13 13:30:29
69700
代码可运行
举报
文章被收录于专栏:dino.c的专栏dino.c的专栏
运行总次数:0
代码可运行

1. 为什么需要设计一个状态按钮

OnePomodoro应用里有个按钮用来控制计时器的启动/停止,本来这应该是一个包含“已启动”和“已停止”两种状态的按钮,但我以前在WPF和UWP上做过太多StateButton、ProgressButton之类的东西,已经厌倦了这种控件,所以我在OnePomodoro应用里只是简单地使用两个按钮来实现这个功能:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<Button Content="&#xE768;"
        Visibility="{x:Bind ViewModel.IsTimerInProgress,Converter={StaticResource NegationBoolToVisibilityConverter}}"
        Command="{Binding StartTimerCommand}" />
<Button Content="&#xE769;"
        Visibility="{x:Bind  ViewModel.IsTimerInProgress,Converter={StaticResource BoolToVisibilityConverter}}"
        Command="{Binding StopTimerCommand}" />

颇有花花公子玩腻了找个良家结婚的意味。但两个按钮实际用起来很不顺手,手感也不好,尤其状态切换时会有种撕裂的感觉,越用越不爽,最后还是花时间又做了一个状态按钮PomodoroStateButton。这个按钮目标是要低调又炫丽,可以匹配OnePomodoro的多个主题。期间试玩了很多种技术,最后留下了这个成果:

看起来简直就是平平无奇。

下面说说实现细节。

2. 按钮状态

我做自定义控件一定会先写代码部分,然后再写XAML部分,功能和外观要做到解耦,写起来也不会乱。

PomodoroStateButton 继承自Button,除了Button本身的CommonStates,PomodoroStateButton还包含以下两组VisualState:

  • ProgressStates:Idle为番茄钟计时器正在计时,Busy为番茄钟停止的状态。
  • PromodoroStates:Inwork为正处于工作状态,Break为休息状态。

虽然是一个放飞自我的控件,但基本的规则还是要遵守的,VisualState对应的TemplateVisualState不能省:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
[TemplateVisualState(GroupName = ProgressStatesName, Name = IdleStateName)]
[TemplateVisualState(GroupName = ProgressStatesName, Name = BusyStateName)]
[TemplateVisualState(GroupName = PromodoroStatesName, Name = InworkStateName)]
[TemplateVisualState(GroupName = PromodoroStatesName, Name = BreakStateName)]

public class PomodoroStateButton : Button
{
    private const string ProgressStatesName = "ProgressStates";
    private const string IdleStateName = "Idle";
    private const string BusyStateName = "Busy";

    private const string PromodoroStatesName = "PromodoroStates";
    private const string InworkStateName = "Inwork";
    private const string BreakStateName = "Break";

    protected virtual void UpdateVisualStates(bool useTransitions)
    {
        VisualStateManager.GoToState(this, IsInPomodoro ? InworkStateName : BreakStateName, useTransitions);
        VisualStateManager.GoToState(this, IsTimerInProgress ? BusyStateName : IdleStateName, useTransitions);
    }

有了这些按钮基本就满足番茄钟的需求了。

3. ICommand

需要支持Start和Stop两个Command。要实现ICommand支持,控件中要执行如下步骤:

  • 定义Command和CommandParameter属性。
  • 监视Command的CanExecuteChanged事件。 在CanExecuteChanged的事件处理函数及CommandParameter的PropertyChangedCallback中,根据Command.CanExecute(CommandParameter)的结果设置控件的IsEnabled属性。 在某个事件(Click或者ValueChanged)中执行Command。

因为从需求来说这个按钮不需要CommandParameter,也不需要监视CanExecuteChanged事件,所以实现得简单些:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public ICommand StartCommand
{
    get => (ICommand)GetValue(StartCommandProperty);
    set => SetValue(StartCommandProperty, value);
}

public ICommand StopCommand
{
    get => (ICommand)GetValue(StopCommandProperty);
    set => SetValue(StopCommandProperty, value);
}

private void OnClick(object sender, RoutedEventArgs e)
{
    if (IsTimerInProgress)
    {
        if (StopCommand != null && StopCommand.CanExecute(this))
            StopCommand.Execute(this);
    }
    else
    {
        if (StartCommand != null && StartCommand.CanExecute(this))
            StartCommand.Execute(this);
    }
}

4. 变形

写完代码部分才开始写XAML部分。

PomodoroStateButton的ControlTempalte中最核心的是一个Polygon,在计时器启动和停止之间按钮图标需要改变它的形状,本来是三角形,需要被用户变成正方形的形状。这部分的操纵在ProgressStates里做。如果只是简单地隐藏/显示或者更换Points会很无聊,这里我使用了以前介绍过的ProgressToPointCollectionBridge,为了让变形流畅些我让三角形先变成圆形再变形到正方形,还加入了旋转动画:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<VisualTransition From="Idle" To="Busy">
    <Storyboard >
        <DoubleAnimation Storyboard.TargetName="ProgressToPointCollectionBridge" Storyboard.TargetProperty="Progress" To="1" EnableDependentAnimation="True" Duration="0:0:0.3">
            <DoubleAnimation.EasingFunction>
                <CubicEase EasingMode="EaseOut"/>
            </DoubleAnimation.EasingFunction>
        </DoubleAnimation>
        <DoubleAnimation Storyboard.TargetName="ShapeCompositeTransform" Storyboard.TargetProperty="Rotation" To="180" EnableDependentAnimation="True" Duration="0:0:0.3">
            <DoubleAnimation.EasingFunction>
                <CubicEase EasingMode="EaseOut"/>
            </DoubleAnimation.EasingFunction>
        </DoubleAnimation>

    </Storyboard>
</VisualTransition>

<Border.Resources>
    <controls:ProgressToPointCollectionBridge x:Name="ProgressToPointCollectionBridge"
                   Progress="0">
        <PointCollection>三角形的点</PointCollection>
        <PointCollection>圆型的点</PointCollection>
        <PointCollection>正方形的点</PointCollection>
    </controls:ProgressToPointCollectionBridge>
</Border.Resources>

<Polygon Points="{Binding Source={StaticResource ProgressToPointCollectionBridge},Path=Points}"/>

顺便提一下其它的变形方案。

HandyControl提供了GeometryAnimation,可以像使用其它线性动画那样使用变形动画:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<hc:GeometryAnimationUsingKeyFrames Storyboard.TargetProperty="Data" Storyboard.TargetName="PathDemo">
    <hc:DiscreteGeometryKeyFrame KeyTime="0:0:0.7" Value="{StaticResource FaceBookGeometry}"/>
    <hc:EasingGeometryKeyFrame KeyTime="0:0:1.2" Value="{StaticResource TwitterGeometry}">
        <hc:EasingGeometryKeyFrame.EasingFunction>
            <QuarticEase EasingMode="EaseInOut"/>
        </hc:EasingGeometryKeyFrame.EasingFunction>
    </hc:EasingGeometryKeyFrame>
</hc:GeometryAnimationUsingKeyFrames>

也可以使用MorphSVG,或类似的SVG变形库:

5. 传递AlphaMask

我在使用GetAlphaMask制作阴影这篇文章里介绍了如何使用GetAlphaMask函数获取元素的AlphaMask,在 PomodoroStateButton里我也使用这个函数获取了ControlTemplate中的Polygon(就是上面变形的部分)的AlphaMask,并使用这个AlphaMask创建阴影、处理MouseEnter/MouseLeave的动画、Pressed的状态变换、还有Inwork/Break状态切换的动画。这还真是累坏它了,而要在一个元素上处理这个多动画我也会累,所以我没有使用DropShadowPanel那种ContentControl的方案,因为那样只能由ContentControl自己拥有Polygon的AlphaMask。而是创建了多个ButtonDecorator控件,让它们都用RelativeElement="{Binding ElementName=Shape}"的方式关联Polygon,然后再通过GetAlphaMask函数获取Polygon的AlphaMask,做到人手一份Polygon的AlphaMask,然后各自进行动画,这样避免了动画太过复杂。XML大致这样:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<controls:ButtonDecorator  x:Name="Shadow"
                           RelativeElement="{Binding ElementName=Shape}" 
                           Style="{StaticResource Shadow}"/>
<controls:ButtonDecorator RelativeElement="{Binding ElementName=Shape}"
                          x:Name="Outline"
                          Style="{StaticResource Outline}"/>
<controls:ButtonDecorator RelativeElement="{Binding ElementName=Shape}"
                          Style="{StaticResource Glow}"
                          IsInPomodoro="{TemplateBinding IsInPomodoro}"/>
<Polygon Points="{Binding Source={StaticResource ProgressToPointCollectionBridge},Path=Points}"
         StrokeThickness="4"
         Stretch="None" 
         StrokeEndLineCap="Round"
         x:Name="Shape"/>

6. 传递ButtonState

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<VisualState x:Name="Pressed">
    <VisualState.Setters>
        <Setter Target="RootGrid.(RevealBrush.State)" Value="Pressed" />
        <Setter Target="RootGrid.Background" Value="{ThemeResource ButtonRevealBackgroundPressed}" />
        <Setter Target="ContentPresenter.BorderBrush" Value="{ThemeResource ButtonRevealBorderBrushPressed}" />
        <Setter Target="ContentPresenter.Foreground" Value="{ThemeResource ButtonForegroundPressed}" />
    </VisualState.Setters>

    <Storyboard>
        <PointerDownThemeAnimation Storyboard.TargetName="RootGrid" />
    </Storyboard>
</VisualState>

上面是是ButtonRevealStyle的部分XAML,应用了ButtonRevealStyle样式的按钮有很复杂的外观,但它的Style写得倒很简洁,这是因为它把状态传递给RevealBrush由它去处理动画(还有PointerDownThemeAnimation之类的),这样分解了复杂的XAML。我也为ButtonDecorator添加了State属性,它是一个ButtonState枚举类型的属性:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public enum ButtonState
{
    //
    // 摘要:
    //     元素处于其默认状态。
    Normal = 0,
    //
    // 摘要:
    //     指针在元素上。
    PointerOver = 1,
    //
    // 摘要:
    //     已按下元素。
    Pressed = 2
}

PomodoroStateButton在CommonStates的个状态间转变时会做轮廓的Outward和Inward动画,阴影也会变颜色,但因为通过传递ButtonState分离了复杂的XAML,所以CommonStates的XAML倒是写得很简单:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<VisualState x:Name="Normal" >
    <VisualState.Setters>
        <Setter Target="Outline.State" Value="Normal"/>
        <Setter Target="Shadow.State" Value="Normal"/>
    </VisualState.Setters>
</VisualState>
<VisualState x:Name="PointerOver">
    <VisualState.Setters>
        <Setter Target="Outline.State" Value="PointerOver"/>
        <Setter Target="Shadow.State" Value="PointerOver"/>
    </VisualState.Setters>
</VisualState>
<VisualState x:Name="Pressed">
    <VisualState.Setters>
        <Setter Target="Outline.State" Value="Pressed"/>
        <Setter Target="Shadow.State" Value="Pressed"/>
        <Setter Target="Shape.Opacity" Value="0.7"/>
    </VisualState.Setters>
</VisualState>

7. 圆周动画

PomodoroStateButton在Inwork和Break之间切换的时候让左右两边的蓝色和红色阴影做半圈圆周运动交换位置,虽然也可以将就些,但当时太闲了就讲究起来了。

之前 里说过怎么做圆周运动,简单来说就是把元素放到一个大的容器里,对整个容器做旋转。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<Page.Resources>
    <Storyboard RepeatBehavior="Forever" x:Key="Sb" >
        <DoubleAnimation Storyboard.TargetName="E1R" BeginTime="0" Storyboard.TargetProperty="Angle" Duration="0:0:4" To="360"/>
    </Storyboard>
</Page.Resources>
<Grid Background="White">
    <Canvas RenderTransformOrigin=".5,.5" Height="100" Width="100">
        <Canvas.RenderTransform>
            <RotateTransform x:Name="E1R" />
        </Canvas.RenderTransform>
        <Rectangle  Width="20"  Height="20"  Fill="MediumPurple"  />
    </Canvas>
</Grid>

但是这样的话里面的元素也会跟着旋转,其中一种解决方法是里面的元素用同样的速度向着反方向做旋转,抵消外层的旋转。但那时我太闲用了另一种方法,也就是平移:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<Page.Resources>
    <Storyboard RepeatBehavior="Forever" x:Key="Sb" >
        <DoubleAnimationUsingKeyFrames Storyboard.TargetName="Translate1" Storyboard.TargetProperty="X" EnableDependentAnimation="True">
            <EasingDoubleKeyFrame KeyTime="0:0:4" Value="120">
                <EasingDoubleKeyFrame.EasingFunction>
                    <QuadraticEase EasingMode="EaseInOut"/>
                </EasingDoubleKeyFrame.EasingFunction>
            </EasingDoubleKeyFrame>
            <EasingDoubleKeyFrame KeyTime="0:0:8" Value="0">
                <EasingDoubleKeyFrame.EasingFunction>
                    <QuadraticEase EasingMode="EaseInOut"/>
                </EasingDoubleKeyFrame.EasingFunction>
            </EasingDoubleKeyFrame>
        </DoubleAnimationUsingKeyFrames>

        <DoubleAnimationUsingKeyFrames Storyboard.TargetName="Translate1" Storyboard.TargetProperty="Y" EnableDependentAnimation="True">
            <EasingDoubleKeyFrame KeyTime="0:0:2" Value="60">
                <EasingDoubleKeyFrame.EasingFunction>
                    <QuadraticEase EasingMode="EaseOut"/>
                </EasingDoubleKeyFrame.EasingFunction>
            </EasingDoubleKeyFrame>
            <EasingDoubleKeyFrame KeyTime="0:0:4" Value="0">
                <EasingDoubleKeyFrame.EasingFunction>
                    <QuadraticEase EasingMode="EaseIn"/>
                </EasingDoubleKeyFrame.EasingFunction>
            </EasingDoubleKeyFrame>
            <EasingDoubleKeyFrame KeyTime="0:0:6" Value="-60">
                <EasingDoubleKeyFrame.EasingFunction>
                    <QuadraticEase EasingMode="EaseOut"/>
                </EasingDoubleKeyFrame.EasingFunction>
            </EasingDoubleKeyFrame>
            <EasingDoubleKeyFrame KeyTime="0:0:8" Value="0">
                <EasingDoubleKeyFrame.EasingFunction>
                    <QuadraticEase EasingMode="EaseIn"/>
                </EasingDoubleKeyFrame.EasingFunction>
            </EasingDoubleKeyFrame>
        </DoubleAnimationUsingKeyFrames>
    </Storyboard>
</Page.Resources>
<Grid Background="White">
    <Grid Height="100" Width="100">
        <Rectangle Width="20" Height="20" Fill="MediumPurple"  RenderTransformOrigin=".5,.5"  HorizontalAlignment="Left" VerticalAlignment="Center">
            <Rectangle.RenderTransform>
                <TranslateTransform x:Name="Translate1" X="0" Y="0" />
            </Rectangle.RenderTransform>
        </Rectangle>
    </Grid>
</Grid>

选择QuadraticEase,搭配得宜的话可以做到漂亮的圆周运动,效果如下:

当然实际上我使用了CircleEase,效果更调皮些,PomodoroStateButton在Inwork和Break之间切换后的效果如下:

(虽然搞这么复杂也没什么意义。)

8. 结语

这样一个手感还不错,看上去很收敛实际上用了一大堆代码的状态按钮就完成了,使用了两个月下来感觉手感还算好,而且很容易和各种主题的番茄钟搭配。

可以安装我的番茄钟应用试玩一下,安装地址:

一个番茄钟

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2019-11-13 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
[UWP]理解ControlTemplate中的VisualTransition
VisualTransition是控件模板中的重要组成部分,无论是自定义控件或者修改控件样式都会接触到VisualTransition。明明这么重要,博客园上好像都没多少关于VisualTransition的主题。
dino.c
2019/01/18
6780
[UWP]理解ControlTemplate中的VisualTransition
[UWP]理解及扩展Expander
最近在自定义Expander的样式,顺便看了看它的源码。 Expander控件是一个ContentControl,它通过IsExpanded属性或者通过点击Header中的ToggleButton控制内容展开或隐藏。UWP SDK中没提供这个控件,而是在UWP Community Toolkit中 提供 。它是个教科书式的入门级控件,代码简单,虽然仍然不尽如人意,但很适合用于学习如何自定义模版化控件。
dino.c
2019/01/18
8710
[UWP]理解及扩展Expander
[UWP]用Shape做动画
相对于WPF/Silverlight,UWP的动画系统可以说有大幅提高,不过本文无意深入讨论这些动画API,本文将介绍使用Shape做一些进度、等待方面的动画,除此之外也会介绍一些相关技巧。
dino.c
2019/01/18
2K0
[UWP]用Shape做动画
[UWP]用Shape做动画(2):使用与扩展PointAnimation
上一篇几乎都在说DoubleAnimation的应用,这篇说说PointAnimation。
dino.c
2019/01/18
4900
[UWP]用Shape做动画(2):使用与扩展PointAnimation
win10 uwp 按下等待按钮
我们需要一个值让我们知道是不是已经完成了后台,按钮可以按下,在按下时,自动让按钮IsEnable为false。
林德熙
2018/09/18
7580
win10 uwp 按下等待按钮
[WPF自定义控件库]以Button为例谈谈如何模仿Aero2主题
除了以外观为卖点的控件库,WPF的控件库都默认使用“素颜”的外观,然后再提供一些主题包。这样做的最大好处是可以和原生控件或其它控件库兼容,而且对于大部分人来说模仿原生的主题也比自己设计一套好看的UI容易得多。
dino.c
2019/05/29
1.2K0
[WPF 自定义控件]自定义一个“传统”的 Validation.ErrorTemplate
数据绑定模型允许您将与您Binding的对象相关联ValidationRules。 如果用户输入的值无效,你可能希望在应用程序 用户界面 (UI) 上提供一些有关错误的反馈。 提供此类反馈的一种方法是设置Validation.ErrorTemplate附加到自定义ControlTemplate的属性。
dino.c
2020/03/02
1.5K0
UWP 轻量级样式定义(Lightweight Styling)
发布于 2018-09-26 09:17 更新于 2018-12-14 01:54
walterlv
2020/02/10
6960
win10 uwp 使用资源在后台创建控件
本文告诉大家如何使用资源在后台创建控件,本文使用按钮做例子,包括如何绑定资源,找到资源。
林德熙
2018/09/19
6560
win10 uwp 使用资源在后台创建控件
[UWP]做个调皮的BusyIndicator
最近突然想要个BusyIndicator。做过WPF开发的程序员对BusyIndicator应该不陌生,Extended WPF Toolkit 提供了BusyIndicator的开源实现,Silverlight Toolkit也有一个,这次想要把这个控件移植到UWP中。
dino.c
2019/01/18
9080
[UWP]做个调皮的BusyIndicator
win10 uwp 修改Pivot Header 颜色
参见: http://stackoverflow.com/questions/31797875/overriding-pivot-header-foreground-brushes-in-uwp-app-win-10-rtm-sdk
林德熙
2018/09/19
6580
win10 uwp 修改Pivot Header 颜色
win10 uwp 异步进度条 圆形进度条
进度条可以参见:http://edi.wang/post/2016/2/25/windows-10-uwp-modal-progress-dialog
林德熙
2018/09/18
1.6K0
win10 uwp 异步进度条
            圆形进度条
3D 穿梭效果?使用 UWP 也能搞定
这个效果太神奇了,他还问我能不能用 WPF 搞出来,因为我完全没用过 WPF 的 3D,我第一反应是“这太难为我了”。
dino.c
2021/11/15
5180
用动画的方式画出任意的路径(直线、曲线、折现)
发布于 2017-11-20 00:49 更新于 2017-11-20 01:07
walterlv
2018/09/18
7030
用动画的方式画出任意的路径(直线、曲线、折现)
Silverlight之ListBox/Style学习笔记--ListBox版的图片轮换广告
ListBox是一个很有用的控件,其功能直逼Asp.Net中的Repeater,它能实现自定义数据项模板,纵向/横向排列Item(如果扩展一下实现自行折行,几乎就是SL版的Repeater了--实际上WrapPanel已经实现了,不过没有默认集成在SL3中).  这里推荐一个老外的文章 http://blogs.msdn.com/delay/archive/2008/03/05/lb-sv-faq-examples-notes-tips-and-more-for-silverlight-2-beta-1-s
菩提树下的杨过
2018/01/22
1K0
Silverlight之ListBox/Style学习笔记--ListBox版的图片轮换广告
SilverLight企业应用框架设计【二】框架画面
注意,这里每个顶部菜单的ICO图标不是动态的,朋友们,想让他变成动态的就自己动手吧
liulun
2022/05/09
6120
SilverLight企业应用框架设计【二】框架画面
[WPF] 制作一个彩虹按钮
继续玩玩彩虹文字,这次用 LinearGradientBrush 并且制作成按钮,虽然没技术含量反而有些实用,这就是返璞归真吗。
dino.c
2021/11/11
6920
[WPF] 制作一个彩虹按钮
Silverlight Telerik控件学习:RadTransitionControl
如果展示类似这种比较cool的图片轮换效果,用RadTransitionControl控件就对了,它提供的过渡效果非常cool! 原理并不复杂,可参见以前写的 Silverlight之ListBox/Style学习笔记--ListBox版的图片轮换广告. xaml部分: <UserControl xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation" x:Class="Telerik.Sample.Transition"
菩提树下的杨过
2018/01/23
8090
Silverlight Telerik控件学习:RadTransitionControl
[WPF] 玩玩彩虹文字及动画
兴致来了玩玩 WPF 的彩虹文字。不是用 LinearGradientBrush 制作渐变色那种,是指每个文字独立颜色那种彩虹文字。虽然没什么实用价值,但希望这篇文章里用 ItemsControl 拆分文字,以及用工具类提供递增和随机变量的做法可以给读者一些启发,就好了。
dino.c
2021/11/10
8340
[翻译]开发Silverlight 2.0的自定义控件
原文:Developing a Custom Control for Silverlight 2.0 译者:张善友 Download MediaButton_demo - 131.06 KB
张善友
2018/01/30
7340
[翻译]开发Silverlight 2.0的自定义控件
相关推荐
[UWP]理解ControlTemplate中的VisualTransition
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
查看详情【社区公告】 技术创作特训营有奖征文