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

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

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

<Button Content=""
        Visibility="{x:Bind ViewModel.IsTimerInProgress,Converter={StaticResource NegationBoolToVisibilityConverter}}"
        Command="{Binding StartTimerCommand}" />
<Button Content=""
        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不能省:

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

这篇文章里有详细介绍:了解模板化控件(7):支持Command

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

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,具体可以见 用Shape做动画(2) 使用与扩展PointAnimation 这篇文章。为了让变形流畅些我让三角形先变成圆形再变形到正方形,还加入了旋转动画:

<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,可以像使用其它线性动画那样使用变形动画:

<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大致这样:

<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

<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枚举类型的属性:

public enum ButtonState
{
    //
    // 摘要:
    //     元素处于其默认状态。
    Normal = 0,
    //
    // 摘要:
    //     指针在元素上。
    PointerOver = 1,
    //
    // 摘要:
    //     已按下元素。
    Pressed = 2
}

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

<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之间切换的时候让左右两边的蓝色和红色阴影做半圈圆周运动交换位置,虽然也可以将就些,但当时太闲了就讲究起来了。

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

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

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

<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. 结语

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

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

一个番茄钟

9. 源码

OnePomodoro_Controls at master

原文地址:https://www.cnblogs.com/dino623/p/create_a_PomodoroStateButton.html

时间: 2024-08-26 10:12:20

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

[UWP]从头开始创建并发布一个番茄钟

1. 自己用的番茄钟自己做 在PC上我一直使用"小番茄"作为我的番茄钟软件,我把它打开后放在副显示器最大化,这样不仅可以让它尽到本分,而且还可以告诉我的同事"我正在专心工作".可是我总是嫌弃它的手感不够愉悦,总想自己写一个番茄钟软件,正好最近很久没写UWP应用了很手痒,于是就抽空写了个自用的番茄钟并发布到微软应用商店. 结果手感也并不愉悦. 另外,本来本来我也打算用Storyboard实现动画,但火火总是劝我不要搞Storyboard,要用Composition A

番茄钟工作法--我们天生爱分享

首先感谢 iroc 给我最迷茫分享了好多东西.以下是番茄钟书籍.pdf的链接,是他极力推荐给我的. 哎呦,不错! http://pan.baidu.com/s/1c2FL4Hi 读一本好书最重要的是记笔记. …… 番茄工作法 发明人:弗朗西斯科.西里洛 要想做到专注,你就得坚决抛开各种杂念. 什么是番茄工作法? 简单的说,就是列出你当天要做的事, 设置25分钟闹钟,然后从第一件事开始.此外还要有每日回顾.做每日承诺,控制每日中断.预估要花的工夫等. 一双脚不能同时跳两场舞.一次只做一件事. 目前

番茄钟App(Pomodoro Tracker)

最近为了学习Swift编程语言,写了一个番茄钟的App(Pomodoro Tracker).刚上线的1.2版本增加了Apple Watch的支持. iPhone版 Apple Watch版 如果你跟我一样有拖延症的话,不妨试用一下或许会解决你的时间管理问题. 另外欢迎提建议和反馈,谢谢. 其它博文: Swift learning resources Xcode 6 模拟器路径 Scrum Planning Card Watch?Kit Learning Resources More blog p

c#编写的番茄钟倒计时器代码

恩  主要大家可以看下思路吧  图形界面里 除了图标和音乐两个资源 别的都是代码. 时间没有用timer组件 是自创的Time类在一个线程中进行的倒计时.  对于导出记录 创建了一个Record类  别的就没什么了  .... Program.cs 代码如下: ?using System; using System.Collections.Generic; using System.Linq; using System.Windows.Forms; namespace 番茄钟 { static

Android 设计一个菱形形状的Imageview组件.

网上没有资料,特来请教下大神 Android 设计一个菱形形状的Imageview组件. >> android 这个答案描述的挺清楚的:http://www.goodpm.net/postreply/android/1010000007107851/Android设计一个菱形形状的Imageview组件.html

设计一个程序能够将某一个目录下面的所有文件名打印出来---File类的使用

,设计一个程序能够将某一个目录下面的所有文件名打印出来 运用到的方法有:返回一个字符串数组,这些字符串指定此抽象路径名表示的目录中的文件和目录:list()           测试此抽象路径名表示的文件是否是一个目录:isDirectory()           返回一个抽象路径名数组,这些路径名表示此抽象路径名表示的目录中的文件:listFiles() package printfilename; import java.io.File; public class PrintFileNam

深度学习:从头设计一个TensorFlow3一样的新一代深度学习系统,到底需要把握哪些要点?

深度学习工具潮流滚滚,各种工具层出不穷.也有各种文章从易用性,可移植性,灵活性和效率方面对于各个系统进行比较.这篇文章希望从系统设计上面来讲来回答这个讨论这个问题:如果想到从头设计一个TensorFlow3一样的新一代深度学习系统,到底需要把握哪些要点. 计算单元:从layer abstraction到operator 大家熟悉的第一代深度学习系统,以cuda-convnet21和caffe为代表.这些系统主要的一大特点是提出了一个以深度学习计算层次layer为基本单元的计算单位.不同的laye

线程池? 如何设计一个动态大小的线程池,有哪些方法?

[线程池?  如何设计一个动态大小的线程池,有哪些方法?] 线程池:顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中, 需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中, 从而减少创建和销毁线程对象的开销. 系统启动一个新线程的成本是比较高的,因为它涉及与操作系统的交互.此时,使用线程池可以很好地提高性能,尤其是当程序中需要创建大量生存期很短暂的线程时,更应该考虑使用线程池. 与数据库连接池相似,线程池在系统启动时即创建大量空闲的线程,程序将一个Runnable

设计一个字节数组缓存类

转 http://blog.csdn.net/kakashi8841/article/details/42025367 版权所有,转载须注明出处! 1.为什么要 在做网络通信的时候,经常需要用到: 读:就是我们需要从网络流里面读取字节数据,并且由于分包的原因,我们需要自己缓存这些数据,而不是读完立刻丢掉. 写:我们需要把各种类型的数据变成字节写入.比如把int.string.short等变成字节数组写入流. 2.需要什么 我们需要设计一个类来实现: 支持可以不停地往这个类中添加字节 支持写入in