dotnet / maui

.NET MAUI is the .NET Multi-platform App UI, a framework for building native device applications spanning mobile, tablet, and desktop.
https://dot.net/maui
MIT License
21.87k stars 1.69k forks source link

Enable Button to have a Content #8191

Open AndreasReitberger opened 2 years ago

AndreasReitberger commented 2 years ago

Description

It would be great to allow the Button control to accept not only a Text, but also a Content or FormattedText.

Public API Changes

<Button 
          VerticalOptions="Start"
          Command="{Binding EnableWebCamCommand}"
          TextColor="{DynamicResource White}"
          FontSize="24" 
          Margin="5,5"
          BackgroundColor="{DynamicResource Transparent}"
          >
          <Button.Content>
              <Grid>
                  <Grid.ColumnDefinitions>
                      <ColumnDefinition Width="Auto"/>
                      <ColumnDefinition />
                  </Grid.ColumnDefinitions>
                  <Label 
                      Text="{StaticResource MaterialDesign_Webcam}"
                      Style="{StaticResource MaterialFontFamilyIconLabelStyle}"
                      TextColor="{DynamicResource White}"
                      VerticalTextAlignment="Center"
                      />
                  <Label
                      Text="{x:Static localization:Strings.On}"
                      TextColor="{DynamicResource White}"
                      Style="{StaticResource LabelStyle}"
                      Grid.Column="1"
                      VerticalTextAlignment="Center"
                      />
              </Grid>
          </Button.Content>
</Button>

Intended Use-Case

In this case, for instance, an icon and a label can be added to the button. Maybe supporting FormattedText like on the Label control would do the same job (in my case)

VladislavAntonyuk commented 2 years ago

it is not needed as you can use GestureRecognizers to achieve command and clicks

Symbai commented 2 years ago

it is not needed as you can use GestureRecognizers to achieve command and clicks

Which is much more complicated plus the GestureRecognizer does not work properly, for example #7403 the above proposal already exist in WPF and makes the developer's work much easier. Then it also wouldn't need an extra ImageButton which is also buggy at the moment.

AndreasReitberger commented 2 years ago

it is not needed as you can use GestureRecognizers to achieve command and clicks

How should this work out? Just add the GestureRecognizer to a label and make it Button like in style?

I rather prefer to do it like above. This makes it more easier and it also does work like this in WPF.

I do not know how much effort this is, but in my opinion this is the "cleanest" way.

Best, Andreas

mobilewares commented 2 years ago

it is not needed as you can use GestureRecognizers to achieve command and clicks

It's very limited if other base controls are used - the TapGestureRecognizer only fires an event after the press is complete (finger taken off control). In the App I'm currently working on (where I have custom Templated button control) inherited from a Border class I really need visual/haptic feedback (and command to be executed) on the press action not the release action.

Also it makes total sense to allow for much more powerful customization of buttons OOB too - I think what Syncfusion have done in their Xamarin control set is more along the lines of what devs would regularly need.

yairp03 commented 2 years ago

Also, GestureRecognizers don't do the pressing and releasing animation of a button

davepruitt commented 1 year ago

I 100% support this. We need a content button.

Stratosf commented 1 year ago

Yes please! We need that. Does anyone have a workaround for now ?

janseris commented 1 year ago

@Stratosf Please upvote the post so that it gains priority because .NET GitHub is built on this system.

ghost commented 1 year ago

We've moved this issue to the Backlog milestone. This means that it is not going to be worked on for the coming release. We will reassess the backlog following the current release and consider this item at that time. To learn more about our issue management process and to have better expectation regarding different types of issues you can read our Triage Process.

JPZV commented 1 year ago

I totally agree with @yairp03 and @Symbai, using GestureRecognizers which was proposed by @VladislavAntonyuk is awful and pretty much a very bad way for archiving this. There're multiple issues, like overlapping other Tap gestures (like scrolling), uncommanded clicks by the user, no feedback for the user (i.e. the animations), and many visual issues.

I mean, you can implement that with some workarounds on Maui side, like putting a BoxView with alpha and making it visible on press and invisible on release, but that is so ugly, specially if buttons like from Android have nice animations.

So, I made a new Control using Handlers. It's not bullet proof nor it works like you could expect, but at least it's a starting point (and a better workaround for now). I didn't implement Windows nor Tizen as I don't work on those platforms, but you should make it work by looking the code and implement it on those sides.

To make it work, just download the zip below (at the end of my comment) and add its content to your solution, then add this lines on MauiProgram.cs

.ConfigureMauiHandlers(handlers =>
{
    handlers.AddHandler(typeof(ContentButton), typeof(ContentButtonHandler));
});

(You will need to add the respective namespace, but just use intellisense for that)

Then, add this namespace to the XAML where you'll use the ContentButton:

xmlns:nm="clr-namespace:NotMaui.Controls"

And then, just use the ContentButton like a mix between a ContentView and a Button. E.g.:

<nm:ContentButton BorderColor="Transparent"
                  BorderWidth="7"
                  Command="{Binding ButtonClickCommand}">

    <nm:ContentButton.Triggers>
        <DataTrigger TargetType="nm:ContentButton" Binding="{Binding IsReadyToSubmit}" Value="True">
            <Setter Property="BorderColor" Value="{StaticResource Primary}"/>
        </DataTrigger>
    </nm:ContentButton.Triggers>

    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>

        <Image Grid.Column="0"
               VerticalOptions="Center"
               Source="{AppThemeBinding Light=add_item.png, Dark=add_item_dark.png}"/>

        <VerticalStackLayout Grid.Column="1"
                             VerticalOptions="Center">
            <Label Text="Add Item"/>

            <Label Text="{Binding ParentName}"
                   Style={StaticResource Subtitle}/>
        </VerticalStackLayout>
    </Grid>
</nm:ContentButton>

You may want to add an default Style, as it'll not consume the default one for the original Button:

<Style TargetType="nm:ContentButton">
        <Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
        <Setter Property="CornerRadius" Value="8"/>
        <Setter Property="Padding" Value="14,10"/>
        <Setter Property="MinimumHeightRequest" Value="44"/>
        <Setter Property="MinimumWidthRequest" Value="44"/>
        <Setter Property="VisualStateManager.VisualStateGroups">
            <VisualStateGroupList>
                <VisualStateGroup x:Name="CommonStates">
                    <VisualState x:Name="Normal" />
                    <VisualState x:Name="Disabled">
                        <VisualState.Setters>
                            <Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray600}}" />
                        </VisualState.Setters>
                    </VisualState>
                </VisualStateGroup>
            </VisualStateGroupList>
        </Setter>
    </Style>

I hope my Workaround helps you, and I totally hope that Microsoft use part of my work (which is based on ImageButton, Button, and ContentView, with some resources from the Internet) to implement an official ContentButton. Again, this should be a Must feature 🙏

NotMaui.zip

ADL450 commented 1 year ago

My reason for wanting this: I need small buttons 25x25 with text in it and MAUI won't display text (of any font size) when button size is 32 or lower. Having a content button would surely have been an easier way to tailor a custom-made button to my needs.

In case anyone wonder why small buttons, let me reassure you it's not for day to day operation by end users. It's meant for a customization mode that allows run-time changes of any text displayed within the app (combined with responsive UI that adjust itself).

JPZV commented 1 year ago

MAUI won't display text (of any font size) when button size is 32 or lower

Check if your button have any padding and/or margin larger than zero. You may want this:

<ContentButton Text="hi" WithRequest="25" HeightRequest="25" Padding="0" FontSize="27"/>
ADL450 commented 1 year ago

It did solve my issue thanks @JPZV but I implemented your solution and works like a charm. I did have to implement a Windows handler to make it work though. Even though I may not have been in need for it this time, I still believe it truly make sense to have a content button.

aaronsuydam commented 1 year ago

Also 100% Support this

Sebosek commented 1 year ago

@VladislavAntonyuk Unfortunately, the way how the button and gesture recognizer work is probably different. Currently, I have a bug that is not present if I use buttons, but using the gesture recognizer the bug appears. As expected, I have to use a gesture recognizer because that only way how can I have content, but I can't, and I am officially screwed. 🐖😬 https://github.com/dotnet/maui/issues/15147

cavasinf commented 11 months ago

I'm trying to replicate a phone keypad, with the 0 button when long pressed adds a +, but with the current state of <Button> component there's no way to render that + on the 0 text

image

larsduewel commented 11 months ago

Also, Gesture Recognizers don't do the pressing and releasing animation of a button

Totally agree, a button needs a proper press and release animation on all supported platforms. I wonder why this is not an high priority issue, since this is one of the first issues you run into when start to develop UI's.

kisjoke91 commented 10 months ago

+1 The Button should definitely have a Content property.

widavies commented 10 months ago

This seems to do the job https://github.com/IeuanWalker/Maui.StateButton

maexsp commented 7 months ago

To use a GestureRecognizer with any Panel/Border will also create issues when doing UI Automation tests cause the tools like Appium etc. will not be able to identify the button control easily or in the way it should work. Often also because of that a XPath approach is needed to be used which will slow down UI Test performance drasticly. They will not find the UIButton or android.widget.Button as its supposed to be via the OS Automation Interface and this will result in a lot of work. Creating custom AutomationInterface/Properties for each plattform etc.

Any easy alternative with MAUI would be to create a custom control without handlers where a Button is placed within a custom control and in z-Order a ContentView (with InputTransparent) is above the Maui-Button. Create custom properties in the code behind and bind to it in your pages as needed.

Schaeferje commented 5 months ago

I managed to do a simple custom button with layout, that can use "pressed" and "released" events.

I simply put my button on top of my layout with a transparent background, then add an animation to make it look like a real button, not perfect but very simple.

xaml: `<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="Inexpack.Controls.AppButton" x:Name="self">

<Border BackgroundColor="FloralWhite" Margin="2, 5, 2, 0">
    <Border.Shadow>
        <Shadow Radius="5" Offset="2, 2" Opacity="0.3"/>
    </Border.Shadow>
    <Grid Padding="5" Margin="0">
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="auto" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="50" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>

        <Image Grid.Row="0" Grid.Column="0" Grid.RowSpan="2" Source="{Binding Source={x:Reference self}, Path=ActivityImage}" HeightRequest="50" WidthRequest="50" Margin="0, 0, 0, 0"/>

        <Label Grid.Column="1" Grid.Row="0" Text="{Binding Source={x:Reference self}, Path=ActivityName}" FontSize="18" Margin="15, 0, 0, 0"/>
        <Label Grid.Column="1" Grid.Row="1" Text="{Binding Source={x:Reference self}, Path=ActivityDescription}" FontAttributes="Italic" FontSize="12" Margin="15, 0, 0, 0"/>

        <Button Pressed="Button_Pressed" Released="Button_Released" Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" Grid.RowSpan="2" BackgroundColor="Transparent" Margin="-10,-10,-10,-10" Clicked="Button_Clicked"/>
    </Grid>
</Border>

`

and code behind:

`public partial class AppButton : ContentView {

region Public events

public delegate void OnAppButtonClicked(string activityName);
public static event OnAppButtonClicked OnAppButtonPressed;
#endregion Public events

#region Properties
public string ActivityName
{
    get => (string)GetValue(ActivityNameProperty);
    set => SetValue(ActivityNameProperty, value);
}

public ImageSource ActivityImage
{
    get => (string)GetValue(ActivityImageProperty);
    set => SetValue(ActivityImageProperty, value);
}

public string ActivityDescription
{
    get => (string)GetValue(ActivityDescriptionProperty);
    set => SetValue(ActivityDescriptionProperty, value);
}

public static readonly BindableProperty ActivityNameProperty = BindableProperty.Create(nameof(ActivityName), typeof(string), typeof(AppButton));
public static readonly BindableProperty ActivityImageProperty = BindableProperty.Create(nameof(ActivityImage), typeof(string), typeof(AppButton));
public static readonly BindableProperty ActivityDescriptionProperty = BindableProperty.Create(nameof(ActivityDescription), typeof(string), typeof(AppButton));
#endregion Properties

public AppButton()
{
    InitializeComponent();
}

public AppButton(string activityName, string activityDescription, string activityImage)
{
    ActivityName = activityName;
    ActivityDescription = activityDescription;
    ActivityImage = ImageSource.FromResource(activityImage);
}

private void Button_Clicked(object sender, EventArgs e)
{
    OnAppButtonPressed?.Invoke(ActivityName);
}

private async void Button_Pressed(object sender, EventArgs e)
{
    await (sender as Button).BackgroundColorTo(Colors.FloralWhite);
}

private async void Button_Released(object sender, EventArgs e)
{
    await (sender as Button).BackgroundColorTo(Colors.Transparent);  
}

}`

It makes it look almost like a real button, and easiest way to do it in my opinion.

AndreasReitberger commented 2 months ago

Any news on this one? :)