一、背景介绍
首先,让我们看一下iPhone上的新邮件提示效果。
在邮件图标的右上角会出现未读的新邮件数量,苹果的这种设计即简洁又精致,而且相当的实用。
那么经典的效果当然要用我们的实际行动来膜拜!^_^
二、最终效果预览
在该篇文章的最后分享了代码,^_^。
三、实现分解
结构采用自定义按钮+自定义装饰件(Adorner)。
装饰件顾名思义就是用来作装饰用的,好处就是:我们以前都是自己写个控件然后在控件上绘制所有的效果,
而现在有了它,我们可以将一些效果独立出来做成一种装饰件,重用在其他想使用该效果的控件上,增强了
效果的解耦和重用。
1、自定义按钮(PromptableButton)
接下来我们编写一个继承自Button类的PromptableButton。
internal class PromptableButton : Button {
//省略...
}
该按钮需要一个提示数量(PromptCount)属性用于最终的右上角提示显示用,做成依赖属性是为了绑定给用作显示的XAML代码。
CoercePromptCountCallback用于限制PromptCount不能小于0。
public int PromptCount {
get { return (int)GetValue(PromptCountProperty); }
set { SetValue(PromptCountProperty, value); }
}
public static readonly DependencyProperty PromptCountProperty =
DependencyProperty.Register("PromptCount", typeof(int), typeof(PromptableButton),
new FrameworkPropertyMetadata(0, new PropertyChangedCallback(PromptCountChangedCallBack),
new CoerceValueCallback(CoercePromptCountCallback)));
private static object CoercePromptCountCallback(DependencyObject d, object value) {
int promptCount = (int)value;
promptCount = Math.Max(0, promptCount);
return promptCount;
}
该按钮还需要一个封面图片,同样做成依赖属性绑定给界面。
public ImageSource CoverImageSource {
get { return (ImageSource)GetValue(CoverImageSourceProperty); }
set { SetValue(CoverImageSourceProperty, value); }
}
public static readonly DependencyProperty CoverImageSourceProperty =
DependencyProperty.Register("CoverImageSource", typeof(ImageSource), typeof(PromptableButton), new UIPropertyMetadata(null));
因为是自定义按钮,我们需要为该按钮提供一个皮肤,皮肤写在Themes/Generic.xaml资源文件中。
为了应用该皮肤,我们如此做:
static PromptableButton() {
DefaultStyleKeyProperty.OverrideMetadata(typeof(PromptableButton), new FrameworkPropertyMetadata(typeof(PromptableButton)));
}
接下来,看看皮肤的实现。
其中的PART_CoverImage的Source绑定PromptableButton的CoverImageSource属性,用来显示封面图片。
下面的触发器的作用是当按下按钮后对按钮作模糊效果。
<Style TargetType="{x:Type local:PromptableButton}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:PromptableButton}">
<Grid>
<Image Name="PART_CoverImage" Stretch="Fill" Source="{Binding RelativeSource={RelativeSource TemplatedParent},Path=CoverImageSource}">
<Image.Effect>
<BlurEffect x:Name="effect" Radius="0"/>
</Image.Effect>
</Image>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsPressed" Value="True">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="effect"
Storyboard.TargetProperty="Radius"
From="0" To="5" Duration="0:0:0.2" />
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="effect"
Storyboard.TargetProperty="Radius"
From="5" To="0" Duration="0:0:0.2" />
</Storyboard>
</BeginStoryboard>
</Trigger.ExitActions>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
自定义按钮(PromptableButton)完成。
2、自定义装饰件(PromptAdorner)
我们一般都会在Adorner的OnRender方法里实现装饰件的绘制,如果绘制的话需要监听PromptableButton的PromptCount
属性的变化,这样作就显得比较麻烦,依赖属性本身就是用作绑定的最好的对象,为什们我们不能使用XAML来作绘制,并且绑定
PromptCount!
为此,专门做一个负责绘制PromptCount的自定义控件(PromptChrome),为这个控件做一个皮肤,在此皮肤内绑定
PromptCount,并且将这个控件作为装饰件(PromptAdorner)的子控件,说白了就是PromptAdorner负责装饰,装饰些什么东
西则交给PromptChrome来完成。
首先,让我们看看PromptAdorner的代码。
其目的就是管理子控件PromptChrome,并通过ArrangeOverride方法进行布局。
注意其中的_chrome.DataContext = adornedElement;这句话使PromptableButton成为PromptChrome的上下文,
使PromptCount被PromptChrome界面上的元素绑定。
internal class PromptAdorner : Adorner {
protected override int VisualChildrenCount {
get { return 1; }
}
public PromptAdorner(UIElement adornedElement)
: base(adornedElement) {
_chrome = new PromptChrome();
_chrome.DataContext = adornedElement;
this.AddVisualChild(_chrome);
}
protected override Visual GetVisualChild(int index) {
return _chrome;
}
protected override Size ArrangeOverride(Size arrangeBounds) {
_chrome.Arrange(new Rect(arrangeBounds));
return arrangeBounds;
}
PromptChrome _chrome;
}
接下来,我们看一下PromptChrome的代码。
在静态构造中应用皮肤。
在ArrangeOverride布局方法中,将自己放置到右上角。
internal class PromptChrome : Control {
static PromptChrome() {
DefaultStyleKeyProperty.OverrideMetadata(typeof(PromptChrome), new FrameworkPropertyMetadata(typeof(PromptChrome)));
}
protected override Size ArrangeOverride(Size arrangeBounds) {
this.Width = 34;
this.Height = 34;
this.HorizontalAlignment = System.Windows.HorizontalAlignment.Right;
this.VerticalAlignment = System.Windows.VerticalAlignment.Top;
TranslateTransform tt = new TranslateTransform();
tt.X = 10;
tt.Y = -10;
this.RenderTransform = tt;
return base.ArrangeOverride(arrangeBounds);
}
}
接下来是PromptChrome的皮肤。
提示效果的绘制,绑定PromptCount都在内,可参考注释。
<Style TargetType="{x:Type local:PromptChrome}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:PromptChrome}">
<Grid x:Name="container">
<!--最外圈的白色圆框,并对其作阴影效果-->
<Ellipse Fill="White">
<Ellipse.Effect>
<DropShadowEffect BlurRadius="6"
ShadowDepth="6"
Opacity="0.8"
Direction="270"
RenderingBias="Performance"/>
</Ellipse.Effect>
</Ellipse>
<!--内部的上半圆-->
<Ellipse Margin="3">
<Ellipse.Fill>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Offset="0" Color="#FFF4AEB1"/>
<GradientStop Offset="0.5" Color="#FFE3313A"/>
<GradientStop Offset="1" Color="#FFE3313A"/>
</LinearGradientBrush>
</Ellipse.Fill>
</Ellipse>
<!--内部的下半圆,通过采用Exclude模式合并上下两个圆来完成-->
<Path HorizontalAlignment="Center" VerticalAlignment="Center">
<Path.Data>
<CombinedGeometry GeometryCombineMode="Exclude" >
<CombinedGeometry.Geometry1>
<EllipseGeometry Center="14 14" RadiusX="14" RadiusY="14" />
</CombinedGeometry.Geometry1>
<CombinedGeometry.Geometry2>
<EllipseGeometry Center="14 0" RadiusX="18" RadiusY="14"/>
</CombinedGeometry.Geometry2>
</CombinedGeometry>
</Path.Data>
<Path.Fill>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Offset="0" Color="#FFDF151F"/>
<GradientStop Offset="1" Color="#FFBA0004"/>
</LinearGradientBrush>
</Path.Fill>
</Path>
<Viewbox Stretch="Uniform" >
<!--绑定上文中的PromptCount属性-->
<Label Content="{Binding Path=PromptCount}"
x:Name="label"
Foreground="White"
FontWeight="Bold"
FontSize="14"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Viewbox>
</Grid>
<ControlTemplate.Triggers>
<!--使用数据触发器,当PromptCount为0时,隐藏提示-->
<DataTrigger Binding="{Binding Path=PromptCount}" Value="0">
<Setter TargetName="container" Property="Visibility" Value="Hidden"/>
</DataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
四、代码分享 ^_^