Compartilhar via


Passando parâmetros de efeito como propriedades anexadas

Propriedades anexadas podem ser usadas para definir parâmetros de efeito que respondem a alterações de propriedade de runtime. Este artigo demonstra o uso de propriedades anexadas para passar parâmetros para um efeito e a alteração de um parâmetro em runtime.

O processo para criar parâmetros de efeito que respondem a alterações de propriedade de runtime é o seguinte:

  1. Crie uma classe static que contém uma propriedade anexada para cada parâmetro a ser passado para o efeito.
  2. Adicione outra propriedade anexada à classe que será usada para controlar a adição ou remoção do efeito para o controle a que a classe será anexada. Certifique-se de que a propriedade anexada registre um delegado propertyChanged que será executado quando o valor da propriedade for alterado.
  3. Crie getters e setters static para cada propriedade anexada.
  4. Implemente a lógica no delegado propertyChanged para adicionar e remover o efeito.
  5. Implemente uma classe aninhada dentro da classe static, com o mesmo nome do efeito, que cria uma subclasse da classe RoutingEffect. Para o construtor, chame o construtor da classe base passando uma concatenação do nome do grupo de resolução e a ID exclusiva que foi especificada em cada classe de efeito específica da plataforma.

Em seguida, é possível passar parâmetros para o efeito adicionando as propriedades anexadas e valores de propriedade ao controle apropriado. Além disso, os parâmetros podem ser alterados no runtime especificando um novo valor da propriedade anexada.

Observação

Uma propriedade anexada é um tipo especial de propriedade associável, definida em uma classe, mas anexada a outros objetos e reconhecível no XAML como atributos que contêm uma classe e um nome de propriedade separados por um ponto. Para obter mais informações, confira Propriedades anexadas.

O aplicativo de exemplo demonstra um ShadowEffect que adiciona uma sombra ao texto exibido por um controle Label. Além disso, a cor da sombra pode ser alterada em runtime. O seguinte diagrama ilustra as responsabilidades de cada projeto no aplicativo de exemplo, bem como as relações entre elas:

Responsabilidades do projeto de efeito de sombra

Um controle Label no HomePage é personalizado pelo LabelShadowEffect em cada projeto específico da plataforma. Os parâmetros são passados para cada LabelShadowEffect por meio das propriedades anexadas na classe ShadowEffect. Cada classe LabelShadowEffect é derivada da classe PlatformEffect de cada plataforma. Isso faz com que uma sombra seja adicionada ao texto exibido pelo controle Label, conforme mostrado nas capturas de tela seguir:

Efeito de sombra em cada plataforma

Criando parâmetros de efeito

Uma classe static deve ser criada para representar os parâmetros em vigor, conforme demonstrado no exemplo de código a seguir:

public static class ShadowEffect
{
  public static readonly BindableProperty HasShadowProperty =
    BindableProperty.CreateAttached ("HasShadow", typeof(bool), typeof(ShadowEffect), false, propertyChanged: OnHasShadowChanged);
  public static readonly BindableProperty ColorProperty =
    BindableProperty.CreateAttached ("Color", typeof(Color), typeof(ShadowEffect), Color.Default);
  public static readonly BindableProperty RadiusProperty =
    BindableProperty.CreateAttached ("Radius", typeof(double), typeof(ShadowEffect), 1.0);
  public static readonly BindableProperty DistanceXProperty =
    BindableProperty.CreateAttached ("DistanceX", typeof(double), typeof(ShadowEffect), 0.0);
  public static readonly BindableProperty DistanceYProperty =
    BindableProperty.CreateAttached ("DistanceY", typeof(double), typeof(ShadowEffect), 0.0);

  public static bool GetHasShadow (BindableObject view)
  {
    return (bool)view.GetValue (HasShadowProperty);
  }

  public static void SetHasShadow (BindableObject view, bool value)
  {
    view.SetValue (HasShadowProperty, value);
  }
  ...

  static void OnHasShadowChanged (BindableObject bindable, object oldValue, object newValue)
  {
    var view = bindable as View;
    if (view == null) {
      return;
    }

    bool hasShadow = (bool)newValue;
    if (hasShadow) {
      view.Effects.Add (new LabelShadowEffect ());
    } else {
      var toRemove = view.Effects.FirstOrDefault (e => e is LabelShadowEffect);
      if (toRemove != null) {
        view.Effects.Remove (toRemove);
      }
    }
  }

  class LabelShadowEffect : RoutingEffect
  {
    public LabelShadowEffect () : base ("MyCompany.LabelShadowEffect")
    {
    }
  }
}

O ShadowEffect contém cinco propriedades anexadas, com getters e setters static para cada propriedade anexada. Quatro dessas propriedades representam parâmetros a serem passados para cada LabelShadowEffect específico da plataforma. A classe ShadowEffect também define uma propriedade anexada HasShadow que é usada para controlar a adição ou remoção do efeito para o controle a que a classe ShadowEffect é anexada. Essa propriedade anexada registra o método OnHasShadowChanged que será executado quando o valor da propriedade for alterado. Esse método adiciona ou remove o efeito com base no valor da propriedade anexada HasShadow.

A classe LabelShadowEffect aninhada, que cria uma subclasse da classe RoutingEffect, dá suporte à adição e à remoção de efeitos. A classe RoutingEffect representa um efeito independente de plataforma que encapsula um efeito interno, que é geralmente é específico da plataforma. Isso simplifica o processo de remoção do efeito, porque não há nenhum acesso de tempo de compilação às informações de tipo para um efeito específico da plataforma. O construtor LabelShadowEffect chama o construtor da classe base, passando um parâmetro composto por uma concatenação do nome do grupo de resolução e pela ID exclusiva que foi especificada em cada classe de efeito específica da plataforma. Isso habilita a adição e a remoção do efeito no método OnHasShadowChanged, da seguinte maneira:

  • Adição de efeito – uma nova instância do LabelShadowEffect é adicionada à coleção Effects do controle. Isso substitui o uso do método Effect.Resolve para adicionar o efeito.
  • Remoção de efeito – a primeira instância da LabelShadowEffectEffects coleção do controle é recuperada e removida.

Consumindo o efeito

Cada LabelShadowEffect específico da plataforma pode ser consumido adicionando as propriedades anexadas a um controle Label, conforme demonstrado no exemplo de código XAML a seguir:

<Label Text="Label Shadow Effect" ...
       local:ShadowEffect.HasShadow="true" local:ShadowEffect.Radius="5"
       local:ShadowEffect.DistanceX="5" local:ShadowEffect.DistanceY="5">
  <local:ShadowEffect.Color>
    <OnPlatform x:TypeArguments="Color">
        <On Platform="iOS" Value="Black" />
        <On Platform="Android" Value="White" />
        <On Platform="UWP" Value="Red" />
    </OnPlatform>
  </local:ShadowEffect.Color>
</Label>

O Label equivalente em C# é mostrado no exemplo de código a seguir:

var label = new Label {
  Text = "Label Shadow Effect",
  ...
};

Color color = Color.Default;
switch (Device.RuntimePlatform)
{
    case Device.iOS:
        color = Color.Black;
        break;
    case Device.Android:
        color = Color.White;
        break;
    case Device.UWP:
        color = Color.Red;
        break;
}

ShadowEffect.SetHasShadow (label, true);
ShadowEffect.SetRadius (label, 5);
ShadowEffect.SetDistanceX (label, 5);
ShadowEffect.SetDistanceY (label, 5);
ShadowEffect.SetColor (label, color));

Definir a propriedade anexada ShadowEffect.HasShadow como true executa o método ShadowEffect.OnHasShadowChanged que adiciona ou remove o LabelShadowEffect ao controle Label. Nos dois exemplos de código, a propriedade anexada ShadowEffect.Color fornece valores de cor específicos da plataforma. Para obter mais informações, confira Classe do dispositivo.

Além disso, um Button permite que a cor da sombra seja alterada em runtime. Quando o usuário clica no Button, a código a seguir altera a cor da sombra definindo a propriedade anexada ShadowEffect.Color:

ShadowEffect.SetColor (label, Color.Teal);

Consumindo o efeito com um estilo

Os efeitos que podem ser consumidos adicionando propriedades anexadas a um controle também podem ser consumidos por um estilo. O exemplo de código XAML abaixo mostra um estilo explícito para o efeito de sombra, que pode ser aplicado a controles Label:

<Style x:Key="ShadowEffectStyle" TargetType="Label">
  <Style.Setters>
    <Setter Property="local:ShadowEffect.HasShadow" Value="True" />
    <Setter Property="local:ShadowEffect.Radius" Value="5" />
    <Setter Property="local:ShadowEffect.DistanceX" Value="5" />
    <Setter Property="local:ShadowEffect.DistanceY" Value="5" />
  </Style.Setters>
</Style>

O Style pode ser aplicado a um Label definindo sua propriedade Style para a instância de Style usando a extensão de marcação StaticResource, conforme demonstrado no exemplo de código a seguir:

<Label Text="Label Shadow Effect" ... Style="{StaticResource ShadowEffectStyle}" />

Para mais informações sobre estilos, confira Estilos.

Criando o efeito em cada plataforma

As seções a seguir abordam a implementação da classe LabelShadowEffect específica da plataforma.

Projeto do iOS

O exemplo de código a seguir mostra a implementação de LabelShadowEffect para o projeto do iOS:

[assembly:ResolutionGroupName ("MyCompany")]
[assembly:ExportEffect (typeof(LabelShadowEffect), "LabelShadowEffect")]
namespace EffectsDemo.iOS
{
    public class LabelShadowEffect : PlatformEffect
    {
        protected override void OnAttached ()
        {
            try {
                UpdateRadius ();
                UpdateColor ();
                UpdateOffset ();
                Control.Layer.ShadowOpacity = 1.0f;
            } catch (Exception ex) {
                Console.WriteLine ("Cannot set property on attached control. Error: ", ex.Message);
            }
        }

        protected override void OnDetached ()
        {
        }
        ...

        void UpdateRadius ()
        {
            Control.Layer.ShadowRadius = (nfloat)ShadowEffect.GetRadius (Element);
        }

        void UpdateColor ()
        {
            Control.Layer.ShadowColor = ShadowEffect.GetColor (Element).ToCGColor ();
        }

        void UpdateOffset ()
        {
            Control.Layer.ShadowOffset = new CGSize (
                (double)ShadowEffect.GetDistanceX (Element),
                (double)ShadowEffect.GetDistanceY (Element));
        }
    }

O método OnAttached chama métodos que recuperam os valores de propriedade anexada usando os getters ShadowEffect e que definem as propriedades de Control.Layer com os valores da propriedade para criar a sombra. Essa funcionalidade é encapsulada em um bloco try/catch caso o controle a que o efeito está anexado não tenha as propriedades de Control.Layer. Nenhuma implementação é fornecida pelo método OnDetached porque nenhuma limpeza é necessária.

Respondendo a alterações de propriedade

Se qualquer um dos valores da propriedade anexada ShadowEffect for alterado em runtime, o efeito precisará responder exibindo as alterações. Uma versão de substituição do método OnElementPropertyChanged, na classe de efeito específica da plataforma, é o lugar para responder a alterações de propriedade vinculáveis, conforme demonstrado no código de exemplo a seguir:

public class LabelShadowEffect : PlatformEffect
{
  ...
  protected override void OnElementPropertyChanged (PropertyChangedEventArgs args)
  {
    if (args.PropertyName == ShadowEffect.RadiusProperty.PropertyName) {
      UpdateRadius ();
    } else if (args.PropertyName == ShadowEffect.ColorProperty.PropertyName) {
      UpdateColor ();
    } else if (args.PropertyName == ShadowEffect.DistanceXProperty.PropertyName ||
               args.PropertyName == ShadowEffect.DistanceYProperty.PropertyName) {
      UpdateOffset ();
    }
  }
  ...
}

O método OnElementPropertyChanged atualiza o raio, a cor ou o deslocamento da sombra, desde que o valor da propriedade anexada ShadowEffect tenha sido alterado. Uma verificação da propriedade alterada sempre deve ser feita, pois essa substituição pode ser chamada várias vezes.

Projeto do Android

O exemplo de código a seguir mostra a implementação de LabelShadowEffect para o projeto do Android:

[assembly:ResolutionGroupName ("MyCompany")]
[assembly:ExportEffect (typeof(LabelShadowEffect), "LabelShadowEffect")]
namespace EffectsDemo.Droid
{
    public class LabelShadowEffect : PlatformEffect
    {
        Android.Widget.TextView control;
        Android.Graphics.Color color;
        float radius, distanceX, distanceY;

        protected override void OnAttached ()
        {
            try {
                control = Control as Android.Widget.TextView;
                UpdateRadius ();
                UpdateColor ();
                UpdateOffset ();
                UpdateControl ();
            } catch (Exception ex) {
                Console.WriteLine ("Cannot set property on attached control. Error: ", ex.Message);
            }
        }

        protected override void OnDetached ()
        {
        }
        ...

        void UpdateControl ()
        {
            if (control != null) {
                control.SetShadowLayer (radius, distanceX, distanceY, color);
            }
        }

        void UpdateRadius ()
        {
            radius = (float)ShadowEffect.GetRadius (Element);
        }

        void UpdateColor ()
        {
            color = ShadowEffect.GetColor (Element).ToAndroid ();
        }

        void UpdateOffset ()
        {
            distanceX = (float)ShadowEffect.GetDistanceX (Element);
            distanceY = (float)ShadowEffect.GetDistanceY (Element);
        }
    }

O método OnAttached chama métodos que recuperam os valores de propriedade anexada usando os getters ShadowEffect e chama um método que chama o método TextView.SetShadowLayer para criar uma sombra usando os valores da propriedade. Essa funcionalidade é encapsulada em um bloco try/catch caso o controle a que o efeito está anexado não tenha as propriedades de Control.Layer. Nenhuma implementação é fornecida pelo método OnDetached porque nenhuma limpeza é necessária.

Respondendo a alterações de propriedade

Se qualquer um dos valores da propriedade anexada ShadowEffect for alterado em runtime, o efeito precisará responder exibindo as alterações. Uma versão de substituição do método OnElementPropertyChanged, na classe de efeito específica da plataforma, é o lugar para responder a alterações de propriedade vinculáveis, conforme demonstrado no código de exemplo a seguir:

public class LabelShadowEffect : PlatformEffect
{
  ...
  protected override void OnElementPropertyChanged (PropertyChangedEventArgs args)
  {
    if (args.PropertyName == ShadowEffect.RadiusProperty.PropertyName) {
      UpdateRadius ();
      UpdateControl ();
    } else if (args.PropertyName == ShadowEffect.ColorProperty.PropertyName) {
      UpdateColor ();
      UpdateControl ();
    } else if (args.PropertyName == ShadowEffect.DistanceXProperty.PropertyName ||
               args.PropertyName == ShadowEffect.DistanceYProperty.PropertyName) {
      UpdateOffset ();
      UpdateControl ();
    }
  }
  ...
}

O método OnElementPropertyChanged atualiza o raio, a cor ou o deslocamento da sombra, desde que o valor da propriedade anexada ShadowEffect tenha sido alterado. Uma verificação da propriedade alterada sempre deve ser feita, pois essa substituição pode ser chamada várias vezes.

Projeto da Plataforma Universal do Windows

O exemplo de código a seguir mostra a implementação de LabelShadowEffect para o projeto da UWP (Plataforma Universal do Windows):

[assembly: ResolutionGroupName ("MyCompany")]
[assembly: ExportEffect (typeof(LabelShadowEffect), "LabelShadowEffect")]
namespace EffectsDemo.UWP
{
    public class LabelShadowEffect : PlatformEffect
    {
        Label shadowLabel;
        bool shadowAdded = false;

        protected override void OnAttached ()
        {
            try {
                if (!shadowAdded) {
                    var textBlock = Control as Windows.UI.Xaml.Controls.TextBlock;

                    shadowLabel = new Label ();
                    shadowLabel.Text = textBlock.Text;
                    shadowLabel.FontAttributes = FontAttributes.Bold;
                    shadowLabel.HorizontalOptions = LayoutOptions.Center;
                    shadowLabel.VerticalOptions = LayoutOptions.CenterAndExpand;

                    UpdateColor ();
                    UpdateOffset ();

                    ((Grid)Element.Parent).Children.Insert (0, shadowLabel);
                    shadowAdded = true;
                }
            } catch (Exception ex) {
                Debug.WriteLine ("Cannot set property on attached control. Error: ", ex.Message);
            }
        }

        protected override void OnDetached ()
        {
        }
        ...

        void UpdateColor ()
        {
            shadowLabel.TextColor = ShadowEffect.GetColor (Element);
        }

        void UpdateOffset ()
        {
            shadowLabel.TranslationX = ShadowEffect.GetDistanceX (Element);
            shadowLabel.TranslationY = ShadowEffect.GetDistanceY (Element);
        }
    }
}

A Plataforma Universal do Windows não fornece um efeito de sombra, de forma que a implementação de LabelShadowEffect nas duas plataformas simula o efeito adicionando um segundo deslocamento Label atrás do Label principal. O método OnAttached cria o novo Label e define algumas propriedades de layout no Label. Em seguida, ele chama métodos que recuperam os valores da propriedade anexada usando os getters ShadowEffect e cria a sombra definindo as propriedades TextColor, TranslationX e TranslationY para controlar a cor e a localização do Label. Em seguida, o shadowLabel é inserido deslocado atrás do Label principal. Essa funcionalidade é encapsulada em um bloco try/catch caso o controle a que o efeito está anexado não tenha as propriedades de Control.Layer. Nenhuma implementação é fornecida pelo método OnDetached porque nenhuma limpeza é necessária.

Respondendo a alterações de propriedade

Se qualquer um dos valores da propriedade anexada ShadowEffect for alterado em runtime, o efeito precisará responder exibindo as alterações. Uma versão de substituição do método OnElementPropertyChanged, na classe de efeito específica da plataforma, é o lugar para responder a alterações de propriedade vinculáveis, conforme demonstrado no código de exemplo a seguir:

public class LabelShadowEffect : PlatformEffect
{
  ...
  protected override void OnElementPropertyChanged (PropertyChangedEventArgs args)
  {
    if (args.PropertyName == ShadowEffect.ColorProperty.PropertyName) {
      UpdateColor ();
    } else if (args.PropertyName == ShadowEffect.DistanceXProperty.PropertyName ||
                      args.PropertyName == ShadowEffect.DistanceYProperty.PropertyName) {
      UpdateOffset ();
    }
  }
  ...
}

O método OnElementPropertyChanged atualiza a cor ou o deslocamento da sombra, desde que o valor da propriedade anexada ShadowEffect tenha sido alterado. Uma verificação da propriedade alterada sempre deve ser feita, pois essa substituição pode ser chamada várias vezes.

Resumo

Este artigo demonstrou o uso de propriedades anexadas para passar parâmetros para um efeito e a alteração de um parâmetro em runtime. Propriedades anexadas podem ser usadas para definir parâmetros de efeito que respondem a alterações de propriedade de runtime.