Xamarin.Forms: 在 UWP 中自定义悬停按钮样式

Xamarin.Forms: Customize button style on hover in UWP

我在 Xamarin Forms 项目的 App.xaml 中定义了一些样式。但是,如果您将鼠标悬停在按钮上或按下它,这不会影响按钮。此处字体颜色变为黑色,按钮周围出现灰色边框。现在我想覆盖这个样式。

第一次尝试: 添加定义到UWP项目App.xaml

<Application
    x:Class="YourApp.UWP.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:YourApp.UWP"
    RequestedTheme="Light">

    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.ThemeDictionaries>
                <ResourceDictionary x:Key="Light">
                    <SolidColorBrush x:Key="ButtonPointerOverBackgroundThemeBrush" Color="#00FF00" />
                </ResourceDictionary>
            </ResourceDictionary.ThemeDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>

结果:完全没有变化

第二次尝试:覆盖 UWP 项目App.xaml中的PointOver视觉状态

<Application
    x:Class="YourApp.UWP.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:YourApp.UWP"
    RequestedTheme="Light">

    <Application.Resources>
        <ResourceDictionary>
            <Style TargetType="Button" x:Key="HoverButtonStyle">
                <Setter Property="Template">
                    <Setter.Value>
                        <ControlTemplate TargetType="Button">
                            <Grid>
                                <VisualStateManager.VisualStateGroups>
                                    <VisualStateGroup x:Name="CommonStates">
                                        <VisualState x:Name="PointerOver">
                                            <Storyboard>
                                                <ObjectAnimationUsingKeyFrames Storyboard.TargetName="Border"
                                                                                Storyboard.TargetProperty="Background">
                                                    <DiscreteObjectKeyFrame KeyTime="0" Value="#00FF00" />
                                                </ObjectAnimationUsingKeyFrames>
                                                <ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter"
                                                                                Storyboard.TargetProperty="Foreground">
                                                    <DiscreteObjectKeyFrame KeyTime="0" Value="#00FF00" />
                                                </ObjectAnimationUsingKeyFrames>
                                            </Storyboard>
                                        </VisualState>
                                    </VisualStateGroup>
                                </VisualStateManager.VisualStateGroups>
                            </Grid>
                        </ControlTemplate>
                    </Setter.Value>
                </Setter>
            </Style>
        </ResourceDictionary>
    </Application.Resources>
</Application>

结果:完全没有变化,我想我必须应用样式(如果我这样做按钮似乎不在这里)

第三次尝试:添加完整的按钮样式并应用

<Style TargetType="Button" x:Key="HoverButtonStyle">
    <Setter Property="Background" Value="{ThemeResource ButtonBackgroundThemeBrush}" />
    <Setter Property="Foreground" Value="{ThemeResource ButtonForegroundThemeBrush}"/>
    <Setter Property="BorderBrush" Value="{ThemeResource ButtonBorderThemeBrush}" />
    <Setter Property="BorderThickness" Value="{ThemeResource ButtonBorderThemeThickness}" />
    <Setter Property="Padding" Value="12,4,12,4" />
    <Setter Property="HorizontalAlignment" Value="Left" />
    <Setter Property="VerticalAlignment" Value="Center" />
    <Setter Property="FontFamily" Value="{ThemeResource ContentControlThemeFontFamily}" />
    <Setter Property="FontWeight" Value="SemiBold" />
    <Setter Property="FontSize" Value="{ThemeResource ControlContentThemeFontSize}" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="Button">
                <Grid>
                    <VisualStateManager.VisualStateGroups>
                        <VisualStateGroup x:Name="CommonStates">
                            <VisualState x:Name="Normal" />
                            <VisualState x:Name="PointerOver">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames Storyboard.TargetName="Border"
                                                       Storyboard.TargetProperty="Background">
                                        <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource ButtonPointerOverBackgroundThemeBrush}" />
                                    </ObjectAnimationUsingKeyFrames>
                                    <ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter"
                                                       Storyboard.TargetProperty="Foreground">
                                        <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ButtonPointerOverForegroundThemeBrush}" />
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </VisualState>
                            <VisualState x:Name="Pressed">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames Storyboard.TargetName="Border"
                                                       Storyboard.TargetProperty="Background">
                                        <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ButtonPressedBackgroundThemeBrush}" />
                                    </ObjectAnimationUsingKeyFrames>
                                    <ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter"
                                                       Storyboard.TargetProperty="Foreground">
                                        <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ButtonPressedForegroundThemeBrush}" />
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </VisualState>
                            <VisualState x:Name="Disabled">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames Storyboard.TargetName="Border"
                                                       Storyboard.TargetProperty="Background">
                                        <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ButtonDisabledBackgroundThemeBrush}" />
                                    </ObjectAnimationUsingKeyFrames>
                                    <ObjectAnimationUsingKeyFrames Storyboard.TargetName="Border"
                                                       Storyboard.TargetProperty="BorderBrush">
                                        <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ButtonDisabledBorderThemeBrush}" />
                                    </ObjectAnimationUsingKeyFrames>
                                    <ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter"
                                                       Storyboard.TargetProperty="Foreground">
                                        <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ButtonDisabledForegroundThemeBrush}" />
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </VisualState>
                        </VisualStateGroup>
                        <VisualStateGroup x:Name="FocusStates">
                            <VisualState x:Name="Focused">
                                <Storyboard>
                                    <DoubleAnimation Storyboard.TargetName="FocusVisualWhite"
                                         Storyboard.TargetProperty="Opacity"
                                         To="1"
                                         Duration="0" />
                                    <DoubleAnimation Storyboard.TargetName="FocusVisualBlack"
                                         Storyboard.TargetProperty="Opacity"
                                         To="1"
                                         Duration="0" />
                                </Storyboard>
                            </VisualState>
                            <VisualState x:Name="Unfocused" />
                            <VisualState x:Name="PointerFocused" />
                        </VisualStateGroup>
                    </VisualStateManager.VisualStateGroups>
                    <Border x:Name="Border"
                Background="{TemplateBinding Background}"
                BorderBrush="{TemplateBinding BorderBrush}"
                BorderThickness="{TemplateBinding BorderThickness}"
                Margin="3">
                        <ContentPresenter x:Name="ContentPresenter"
                              Content="{TemplateBinding Content}"
                              ContentTransitions="{TemplateBinding ContentTransitions}"
                              ContentTemplate="{TemplateBinding ContentTemplate}"
                              Margin="{TemplateBinding Padding}"
                              HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                              VerticalAlignment="{TemplateBinding VerticalContentAlignment}" 
                              AutomationProperties.AccessibilityView="Raw"/>
                    </Border>
                    <Rectangle x:Name="FocusVisualWhite"
                   IsHitTestVisible="False"
                   Stroke="{ThemeResource FocusVisualWhiteStrokeThemeBrush}"
                   StrokeEndLineCap="Square"
                   StrokeDashArray="1,1"
                   Opacity="0"
                   StrokeDashOffset="1.5" />
                    <Rectangle x:Name="FocusVisualBlack"
                   IsHitTestVisible="False"
                   Stroke="{ThemeResource FocusVisualBlackStrokeThemeBrush}"
                   StrokeEndLineCap="Square"
                   StrokeDashArray="1,1"
                   Opacity="0"
                   StrokeDashOffset="0.5" />
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

自定义渲染器:

protected override void OnElementChanged(ElementChangedEventArgs<Button> e)
{
    base.OnElementChanged(e);

    if (this.Element != null)
    {
        this.Control.Style = Windows.UI.Xaml.Application.Current.Resources["HoverButtonStyle"] as Windows.UI.Xaml.Style;
    }
}

结果:似乎应用了样式,但我在 Xamarin Forms 中定义的背景颜色没有占据按钮的整个宽度。边框颜色也没有改变。

这是怎么做到的?

现在我发现了这种样式的工作原理。首先,您必须找到基础 UWP class(按住 Ctrl 并单击 class 名称或查看 here). E.g. for Picker it is ComboBox. If you use Google you come to this page, where you find everything you need to know about overwriting the default layout of a ComboBox. For a Button it is this page 等等。所以解决方案是有一个像这样的 App.xaml(UWP 项目)(选择​​您选择的颜色):

<Application
    x:Class="YourApp.UWP.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:YourApp.UWP"
    RequestedTheme="Light">

    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.ThemeDictionaries>
                <ResourceDictionary x:Key="Light">
                    <SolidColorBrush x:Key="SystemControlHighlightBaseMediumLowBrush" Color="White" />
                    <SolidColorBrush x:Key="SystemControlHighlightBaseHighBrush" Color="White" />
                </ResourceDictionary>
            </ResourceDictionary.ThemeDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>

仅对某些按钮应用样式,您必须执行以下步骤:

在您的 UWP 项目的 App.xaml 中,您需要以下条目:

<Application.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary Source="Styles/DefaultButtonControlTemplate.xaml" />
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</Application.Resources>

这里注册一个样式,它在一个单独的文件中。我有一个名为 Styles 的文件夹,其中放置了文件 DefaultButtonControlTemplate.xaml。从 MSDN 获取的文件内容如下所示:

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:MyApp.UWP.ControlTemplates">

    <ResourceDictionary.MergedDictionaries>
        <ResourceDictionary Source="ColorsAndBrushes.xaml" />
    </ResourceDictionary.MergedDictionaries>

    <ControlTemplate x:Key="DefaultButtonControlTemplate" TargetType="Button">
        <!-- here is the content of the file -->
    </ControlTemplate>

</ResourceDictionary>

如您所见,我正在引用一个通用文件,其中包含我所有的颜色(或 UWP 世界中的画笔)。

最后,您需要一个像这样的自定义渲染器:

public class DefaultButtonRenderer : ButtonRenderer
{
    protected override void OnElementChanged(ElementChangedEventArgs<Button> e)
    {
        base.OnElementChanged(e);

        if (this.Control != null)
        {
            this.Control.Template = Windows.UI.Xaml.Application.Current.Resources["DefaultButtonControlTemplate"] as Windows.UI.Xaml.Controls.ControlTemplate;
        }
    }
}

找到了一种将其全部保留在 UWP 自定义渲染器中的方法,而不必担心修改其他任何内容或其他按钮设置是否会发生冲突。就我而言,我创建了一个自定义 PillButton,因此显然可以更新您的 类 和颜色等等。如果您没有在按钮上看到圆角半径,那么它将是一个使用下面的普通按钮。

[assembly: ExportRenderer(typeof(PillButton), typeof(PillButtonRenderer))]
namespace YourProject.UWP.Renderers
{
    public class PillButtonRenderer : ButtonRenderer
    {
        public PillButton PillButtonElement => Element as PillButton;

        protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.Button> e)
        {
            base.OnElementChanged(e);

            if (Control != null)
            {
                Windows.UI.Xaml.Controls.Button button = Control;

                Resources = (Windows.UI.Xaml.ResourceDictionary)XamlReader.Load(PillButtonStyleDictionary);

                Resources["PillCornerRadius"] = PillButtonElement.CornerRadius;
                Resources["PillBorderWidth"] = PillButtonElement.BorderWidth;

                // if hover color not supplied, then hover color will be lighter version of background color, unless background color is transparent in which case it will be the border color
                var hoverColor = PillButtonElement.UwpHoverColor != default(Color) ? PillButtonElement.UwpHoverColor
                    : (PillButtonElement.BackgroundColor == Color.Transparent
                        ? PillButtonElement.BorderColor
                        : PillButtonElement.BackgroundColor.ChangeColorBrightness(0.15));
                Resources["PillFillColorOnHover"] = new SolidColorBrush(hoverColor.ToUwp());

                // if pressed color not supplied, then make it a darker shade of the hover color
                var pressedColor = PillButtonElement.UwpPressedColor != default(Color) ? PillButtonElement.UwpPressedColor : hoverColor.ChangeColorBrightness(-0.09);
                Resources["PillFillColorOnPressed"] = new SolidColorBrush(pressedColor.ToUwp());

                // if text color on hover/press not supplied, then make it black or white depending on how dark the hover color is
                var textColor = PillButtonElement.PressedTextColor != default(Color) ? PillButtonElement.PressedTextColor : hoverColor.BlackOrWhiteForegroundTextColor();
                Resources["PillTextColorOnHoverOrPressed"] = new SolidColorBrush(textColor.ToUwp());

                // set normal style
                Resources["PillBackgroundColor"] = new SolidColorBrush(PillButtonElement.BackgroundColor.ToUwp());
                Resources["PillTextColor"] = new SolidColorBrush(PillButtonElement.TextColor.ToUwp());
                PillButtonElement.BackgroundColor = Color.Transparent; // hack

                button.Style = Resources["PillButtonStyle"] as Windows.UI.Xaml.Style;
            }
        }

        private const string PillButtonStyleDictionary = @"<ResourceDictionary
    xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation""
    xmlns:x=""http://schemas.microsoft.com/winfx/2006/xaml"">

    <x:Double x:Key=""PillCornerRadius"">0</x:Double>
    <x:Double x:Key=""PillBorderWidth"">0</x:Double>

    <SolidColorBrush
        x:Key=""PillBackgroundColor""
        Color=""Black"" />
    <SolidColorBrush
        x:Key=""PillTextColor""
        Color=""Black"" />
    <SolidColorBrush
        x:Key=""PillFillColorOnHover""
        Color=""Black"" />
    <SolidColorBrush
        x:Key=""PillFillColorOnPressed""
        Color=""Black"" />
    <SolidColorBrush
        x:Key=""PillTextColorOnHoverOrPressed""
        Color=""Black"" />

    <Style
        x:Key=""PillButtonStyle""
        TargetType=""Button"">
        <Setter
            Property=""Background""
            Value=""{ThemeResource SystemControlBackgroundBaseLowBrush}"" />
        <Setter
            Property=""Foreground""
            Value=""{ThemeResource SystemControlForegroundBaseHighBrush}"" />
        <Setter
            Property=""BorderBrush""
            Value=""{ThemeResource SystemControlForegroundTransparentBrush}"" />
        <Setter
            Property=""BorderThickness""
            Value=""{ThemeResource ButtonBorderThemeThickness}"" />
        <Setter
            Property=""Padding""
            Value=""8,4,8,4"" />
        <Setter
            Property=""HorizontalAlignment""
            Value=""Left"" />
        <Setter
            Property=""VerticalAlignment""
            Value=""Center"" />
        <Setter
            Property=""FontFamily""
            Value=""{ThemeResource ContentControlThemeFontFamily}"" />
        <Setter
            Property=""FontWeight""
            Value=""Normal"" />
        <Setter
            Property=""FontSize""
            Value=""{ThemeResource ControlContentThemeFontSize}"" />
        <Setter
            Property=""UseSystemFocusVisuals""
            Value=""True"" />
        <Setter Property=""Template"">
            <Setter.Value>
                <ControlTemplate TargetType=""Button"">
                    <Grid x:Name=""RootGrid"">
                        <VisualStateManager.VisualStateGroups>
                            <VisualStateGroup x:Name=""CommonStates"">
                                <VisualState x:Name=""Normal"">
                                    <Storyboard>
                                        <ObjectAnimationUsingKeyFrames
                                            Storyboard.TargetName=""Pill""
                                            Storyboard.TargetProperty=""Fill"">
                                            <DiscreteObjectKeyFrame
                                                KeyTime=""0""
                                                Value=""{StaticResource PillBackgroundColor}"" />
                                        </ObjectAnimationUsingKeyFrames>
                                        <ObjectAnimationUsingKeyFrames
                                            Storyboard.TargetName=""ContentPresenter""
                                            Storyboard.TargetProperty=""Foreground"">
                                            <DiscreteObjectKeyFrame
                                                KeyTime=""0""
                                                Value=""{StaticResource PillTextColor}"" />
                                        </ObjectAnimationUsingKeyFrames>
                                        <PointerUpThemeAnimation Storyboard.TargetName=""RootGrid"" />
                                    </Storyboard>
                                </VisualState>
                                <VisualState x:Name=""PointerOver"">
                                    <Storyboard>
                                        <ObjectAnimationUsingKeyFrames
                                            Storyboard.TargetName=""Pill""
                                            Storyboard.TargetProperty=""Fill"">
                                            <DiscreteObjectKeyFrame
                                                KeyTime=""0""
                                                Value=""{StaticResource PillFillColorOnHover}"" />
                                        </ObjectAnimationUsingKeyFrames>
                                        <ObjectAnimationUsingKeyFrames
                                            Storyboard.TargetName=""ContentPresenter""
                                            Storyboard.TargetProperty=""Foreground"">
                                            <DiscreteObjectKeyFrame
                                                KeyTime=""0""
                                                Value=""{StaticResource PillTextColorOnHoverOrPressed}"" />
                                        </ObjectAnimationUsingKeyFrames>

                                        <PointerUpThemeAnimation Storyboard.TargetName=""RootGrid"" />
                                    </Storyboard>
                                </VisualState>
                                <VisualState x:Name=""Pressed"">
                                    <Storyboard>
                                        <ObjectAnimationUsingKeyFrames
                                            Storyboard.TargetName=""Pill""
                                            Storyboard.TargetProperty=""Fill"">
                                            <DiscreteObjectKeyFrame
                                                KeyTime=""0""
                                                Value=""{StaticResource PillFillColorOnPressed}"" />
                                        </ObjectAnimationUsingKeyFrames>
                                        <ObjectAnimationUsingKeyFrames
                                            Storyboard.TargetName=""ContentPresenter""
                                            Storyboard.TargetProperty=""Foreground"">
                                            <DiscreteObjectKeyFrame
                                                KeyTime=""0""
                                                Value=""{StaticResource PillTextColorOnHoverOrPressed}"" />
                                        </ObjectAnimationUsingKeyFrames>
                                        <PointerDownThemeAnimation Storyboard.TargetName=""RootGrid"" />
                                    </Storyboard>
                                </VisualState>
                                <VisualState x:Name=""Disabled"">
                                    <Storyboard>
                                        <ObjectAnimationUsingKeyFrames
                                            Storyboard.TargetName=""Pill""
                                            Storyboard.TargetProperty=""Fill"">
                                            <DiscreteObjectKeyFrame
                                                KeyTime=""0""
                                                Value=""{ThemeResource SystemControlBackgroundBaseLowBrush}"" />
                                        </ObjectAnimationUsingKeyFrames>
                                        <ObjectAnimationUsingKeyFrames
                                            Storyboard.TargetName=""ContentPresenter""
                                            Storyboard.TargetProperty=""Foreground"">
                                            <DiscreteObjectKeyFrame
                                                KeyTime=""0""
                                                Value=""{ThemeResource SystemControlDisabledBaseMediumLowBrush}"" />
                                        </ObjectAnimationUsingKeyFrames>
                                        <ObjectAnimationUsingKeyFrames
                                            Storyboard.TargetName=""Pill""
                                            Storyboard.TargetProperty=""Stroke"">
                                            <DiscreteObjectKeyFrame
                                                KeyTime=""0""
                                                Value=""{ThemeResource SystemControlDisabledTransparentBrush}"" />
                                        </ObjectAnimationUsingKeyFrames>
                                    </Storyboard>
                                </VisualState>
                            </VisualStateGroup>
                        </VisualStateManager.VisualStateGroups>
                        <Rectangle
                            x:Name=""Pill""
                            RadiusX=""{StaticResource PillCornerRadius}""
                            RadiusY=""{StaticResource PillCornerRadius}""
                            Stroke=""{TemplateBinding BorderBrush}""
                            StrokeThickness=""{StaticResource PillBorderWidth}"" />
                        <ContentPresenter
                            x:Name=""ContentPresenter""
                            Padding=""{TemplateBinding Padding}""
                            HorizontalContentAlignment=""{TemplateBinding HorizontalContentAlignment}""
                            VerticalAlignment=""Center""
                            VerticalContentAlignment=""{TemplateBinding VerticalContentAlignment}""
                            AutomationProperties.AccessibilityView=""Raw""
                            Content=""{TemplateBinding Content}""
                            ContentTemplate=""{TemplateBinding ContentTemplate}""
                            ContentTransitions=""{TemplateBinding ContentTransitions}"" />
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>";
    }
}

以防万一,这里是用于从 Xamarin Color 转换为 UWP Color 的 ToUwp 颜色扩展,在该代码中使用:

internal static class ColorExtensions
{
    public static Color ToUwp(this Xamarin.Forms.Color color)
    {
        return Color.FromArgb((byte)(color.A * 255),
                              (byte)(color.R * 255),
                              (byte)(color.G * 255),
                              (byte)(color.B * 255));
    }
}