Как по-настоящему избежать одновременного нажатия нескольких кнопок в Xamarin.Forms?

Я использую несколько кнопок в представлении, и каждая кнопка приводит к ее собственной всплывающей странице. При одновременном нажатии нескольких кнопок он переходит на разные всплывающие окна за раз.

Я создал образец страницы контента с тремя кнопками (каждый из них идет на другую всплывающую страницу), чтобы продемонстрировать эту проблему:

screenshots

Страница XAML:

<ContentPage.Content>
    <AbsoluteLayout>

        <!-- button 1 -->
        <Button x:Name="button1" Text="Button 1"
            BackgroundColor="White" Clicked="Button1Clicked"
            AbsoluteLayout.LayoutFlags="All"
            AbsoluteLayout.LayoutBounds="0.5, 0.3, 0.5, 0.1"/>

        <!-- button 2 -->
        <Button x:Name="button2" Text="Button 2"
            BackgroundColor="White" Clicked="Button2Clicked"
            AbsoluteLayout.LayoutFlags="All"
            AbsoluteLayout.LayoutBounds="0.5, 0.5, 0.5, 0.1"/>

        <!-- button 3 -->
        <Button x:Name="button3" Text="Button 3"
            BackgroundColor="White" Clicked="Button3Clicked"
            AbsoluteLayout.LayoutFlags="All"
            AbsoluteLayout.LayoutBounds="0.5, 0.7, 0.5, 0.1"/>

        <!-- popup page 1 -->
        <AbsoluteLayout x:Name="page1" BackgroundColor="#7f000000" IsVisible="false"
            AbsoluteLayout.LayoutFlags="All"
            AbsoluteLayout.LayoutBounds="0.5, 0.5, 1.0, 1.0">
            <BoxView Color="Red"
                AbsoluteLayout.LayoutFlags="All"
                AbsoluteLayout.LayoutBounds="0.5, 0.5, 0.75, 0.3"/>
            <Label Text="Button 1 clicked" TextColor="White"
                HorizontalTextAlignment="Center"
                AbsoluteLayout.LayoutFlags="All"
                AbsoluteLayout.LayoutBounds="0.5, 0.45, 0.75, 0.05"/>
            <Button Text="Back" BackgroundColor="White" Clicked="Back1Clicked"
                AbsoluteLayout.LayoutFlags="All"
                AbsoluteLayout.LayoutBounds="0.5, 0.6, 0.5, 0.1"/>
        </AbsoluteLayout>

        <!-- popup page 2 -->
        <AbsoluteLayout x:Name="page2" BackgroundColor="#7f000000" IsVisible="false"
            AbsoluteLayout.LayoutFlags="All"
            AbsoluteLayout.LayoutBounds="0.5, 0.5, 1.0, 1.0">
            <BoxView Color="Green"
                AbsoluteLayout.LayoutFlags="All"
                AbsoluteLayout.LayoutBounds="0.5, 0.5, 0.75, 0.3"/>
            <Label Text="Button 2 clicked" TextColor="White"
                HorizontalTextAlignment="Center"
                AbsoluteLayout.LayoutFlags="All"
                AbsoluteLayout.LayoutBounds="0.5, 0.45, 0.75, 0.05"/>
            <Button Text="Back" BackgroundColor="White" Clicked="Back2Clicked"
                AbsoluteLayout.LayoutFlags="All"
                AbsoluteLayout.LayoutBounds="0.5, 0.6, 0.5, 0.1"/>
        </AbsoluteLayout>

        <!-- popup page 3 -->
        <AbsoluteLayout x:Name="page3" BackgroundColor="#7f000000" IsVisible="false"
            AbsoluteLayout.LayoutFlags="All"
            AbsoluteLayout.LayoutBounds="0.5, 0.5, 1.0, 1.0">
            <BoxView Color="Blue"
                AbsoluteLayout.LayoutFlags="All"
                AbsoluteLayout.LayoutBounds="0.5, 0.5, 0.75, 0.3"/>
            <Label Text="Button 3 clicked" TextColor="White"
                HorizontalTextAlignment="Center"
                AbsoluteLayout.LayoutFlags="All"
                AbsoluteLayout.LayoutBounds="0.5, 0.45, 0.75, 0.05"/>
            <Button Text="Back" BackgroundColor="White" Clicked="Back3Clicked"
                AbsoluteLayout.LayoutFlags="All"
                AbsoluteLayout.LayoutBounds="0.5, 0.6, 0.5, 0.1"/>
        </AbsoluteLayout>

    </AbsoluteLayout>
</ContentPage.Content>

Обработчики событий С#:

void Button1Clicked(object sender, EventArgs e)
{
    // ... do something first ...
    page1.IsVisible = true;
    Console.WriteLine("Button 1 Clicked!");
}

void Button2Clicked(object sender, EventArgs e)
{
    // ... do something first ...
    page2.IsVisible = true;
    Console.WriteLine("Button 2 Clicked!");
}

void Button3Clicked(object sender, EventArgs e)
{
    // ... do something first ...
    page3.IsVisible = true;
    Console.WriteLine("Button 3 Clicked!");
}

void Back1Clicked(object sender, EventArgs e)
{
    page1.IsVisible = false;
}

void Back2Clicked(object sender, EventArgs e)
{
    page2.IsVisible = false;
}

void Back3Clicked(object sender, EventArgs e)
{
    page3.IsVisible = false;
}

Ожидаемое:
При нажатии button1 открывается page1 всплывающее окно страницы и нажав кнопку назад в всплывающем окне скрывает раскрывающие страницы. Аналогично button3 button2 и button3.

Актуально:
При нажатии нескольких кнопок (например. button1 и button2) в то же время открывает как всплывающие страницы (page1 и page2). Двойной щелчок одной кнопкой быстро может также запустить ту же кнопку дважды.


Некоторые исследования по избежанию двойного щелчка
При поиске похожих вопросов в stackoverflow (например, это и это) я пришел к выводу, что вы должны установить внешнюю переменную для управления тем, выполняются ли события или нет. Это моя реализация в Xamarin.forms:

С# struct как внешняя переменная, чтобы я мог получить доступ к этой переменной в отдельных классах:

// struct to avoid multiple button click at the same time
public struct S
{
    // control whether the button events are executed
    public static bool AllowTap = true;

    // wait for 200ms after allowing another button event to be executed
    public static async void ResumeTap() {
        await Task.Delay(200);
        AllowTap = true;
    }
}

Затем каждый обработчик событий кнопки изменяется так же (это относится и к Button2Clicked() и Button3Clicked()):

void Button1Clicked(object sender, EventArgs e)
{
    // if some buttons are clicked recently, stop executing the method
    if (!S.AllowTap) return; S.AllowTap = false; //##### * NEW * #####//

    // ... do something first ...
    page1.IsVisible = true;
    Console.WriteLine("Button 1 Clicked!");

    // allow other button event to be fired after the wait specified in struct S
    S.ResumeTap(); //##### * NEW * #####//
}

Это работает в целом довольно хорошо. Двойное нажатие одной и той же кнопки быстро запускает событие кнопки только один раз, и одновременное нажатие нескольких кнопок открывает только одну всплывающую страницу.


Реальная проблема
По-прежнему можно открыть более 1 всплывающих страниц после изменения кода (добавление общей переменной состояния AllowTap в struct S), как описано выше. Например, если пользователь, удерживая button1 и button2 с помощью 2 пальца, релиз button1, ждать около секунды, а затем отпустить button2, как всплывающие страницы page1 и page2 будет открыт.


Неудачная попытка исправить эту проблему
Я попытался отключить все кнопки, если нажата кнопка button1, button2 или button3, и включите все кнопки, если нажата кнопка "Назад".

void disableAllButtons()
{
    button1.IsEnabled = false;
    button2.IsEnabled = false;
    button3.IsEnabled = false;
}

void enableAllButtons()
{
    button1.IsEnabled = true;
    button2.IsEnabled = true;
    button3.IsEnabled = true;
}

Затем каждый обработчик событий кнопки изменяется так же (это относится и к Button2Clicked() и Button3Clicked()):

void Button1Clicked(object sender, EventArgs e)
{
    if (!S.AllowTap) return; S.AllowTap = false;

    // ... do something first ...
    disableAllButtons(); //##### * NEW * #####//
    page1.IsVisible = true;
    Console.WriteLine("Button 1 Clicked!");

    S.ResumeTap();
}

И каждый обработчик события на задней панели изменяется таким же образом (то же самое относится к Back2Clicked() и Back3Clicked()):

void Back1Clicked(object sender, EventArgs e)
{
    page1.IsVisible = false;
    enableAllButtons(); //##### * NEW * #####//
}

Тем не менее, эта же проблема все еще сохраняется (возможность удерживать еще одну кнопку и отпускать ее позже, чтобы одновременно запустить две кнопки).


Отключение multi-touch в моем приложении не будет вариантом, так как мне это нужно на других страницах моего приложения. Кроме того, всплывающие страницы могут также содержать несколько кнопок, что также приводит к другим страницам, поэтому просто использовать кнопку "Назад" на всплывающей странице для установки переменной AllowTap в struct S также не будет.

Любая помощь будет оценена. Благодарю.

РЕДАКТИРОВАТЬ
"Реальная проблема" влияет как на Android, так и на iOS. На Android кнопка не может быть активирована, как только кнопка будет отключена некоторое время, когда пользователь удерживает кнопку. Эта проблема с нажатием кнопки-выключенной кнопки не влияет на кнопки в iOS.

Ответы

Ответ 1

Переместите ваш отключающий вызов (disableAllButtons()) в Pressed событие кнопок.

Это отключит другие кнопки, как только будет обнаружено первое касание.

РЕДАКТИРОВАТЬ
Чтобы предотвратить случайное отключение всего, создайте собственный визуализатор кнопок и подключите собственные события, чтобы отменить отключение при перетаскивании: iOS: UIControlEventTouchDragOutside

РЕДАКТИРОВАТЬ
Android был протестирован в этом сценарии, он имеет ту же проблему, что и в этом вопросе.

Ответ 2

Я думаю, что следующий код будет работать для вас.

void Button1Clicked(object sender, EventArgs e)
{
    disableAllButtons();

    // ... do something first ...
    page1.IsVisible = true;
    Console.WriteLine("Button 1 Clicked!");
}

void Button2Clicked(object sender, EventArgs e)
{
    disableAllButtons();

    // ... do something first ...
    page2.IsVisible = true;
    Console.WriteLine("Button 2 Clicked!");
}

void Button3Clicked(object sender, EventArgs e)
{
    disableAllButtons();

    // ... do something first ...
    page3.IsVisible = true;
    Console.WriteLine("Button 3 Clicked!");
}

void Back1Clicked(object sender, EventArgs e)
{
    enableAllButtons();
}

void Back2Clicked(object sender, EventArgs e)
{
    enableAllButtons();
}

void Back3Clicked(object sender, EventArgs e)
{
    enableAllButtons();
}



void disableAllButtons()
{
    button1.IsEnabled = false;
    button2.IsEnabled = false;
    button3.IsEnabled = false;

    disableAllPages();
}

void enableAllButtons()
{
    button1.IsEnabled = true;
    button2.IsEnabled = true;
    button3.IsEnabled = true;

    disableAllPages();
}

void disableAllPages()
{
    page1.IsVisible = false;
    page2.IsVisible = false;
    page3.IsVisible = false;
}

Ответ 3

Я бы рекомендовал использовать подход MVVM и IsEnabled свойство IsEnabled для каждой кнопки к тому же свойству в Model View, например AreButtonsEnabled:

MyViewModel.cs:

private bool _areButtonsEnabled = true;

public bool AreButtonsEnabled
{
    get => _areButtonsEnabled;
    set
    {
        if (_areButtonsEnabled != value)
        {
            _areButtonsEnabled = value;
            OnPropertyChanged(nameof(AreButtonsEnabled)); // assuming your view model implements INotifyPropertyChanged
        }
    }
}

MyPage.xaml (отображается только одна кнопка):

...
<Button 
    Text="Back" 
    BackgroundColor="White" 
    Clicked="HandleButton1Clicked"
    AbsoluteLayout.LayoutFlags="All"
    AbsoluteLayout.LayoutBounds="0.5, 0.6, 0.5, 0.1"
    IsEnabled={Binding AreButtonsEnabled} />
...

Затем для обработчиков событий для каждой кнопки вы можете установить для свойства AreButtonsEnabled значение false, чтобы отключить все кнопки. Обратите внимание, что вы должны сначала проверить, истинно ли значение AreButtonsEnabled, потому что есть шанс, что пользователь может дважды щелкнуть два раза до вызова события PropertyChanged. Однако, поскольку обработчики событий AreButtonsEnabled кнопок запускаются в основном потоке, значение AreButtonsEnabled будет установлено в false до того, HandleButtonXClicked будет вызван следующий HandleButtonXClicked. Другими словами, значение AreButtonsEnabled будет обновляться, даже если пользовательский интерфейс еще не обновлен.

MyPage.xaml.cs:

HandleButton1Clicked(object sender, EventArgs e)
{
    if (viewModel.AreButtonsEnabled)
    {
        viewModel.AreButtonsEnabled = false;
        // button 1 code...
    }
}

HandleButton2Clicked(object sender, EventArgs e)
{
    if (viewModel.AreButtonsEnabled)
    {
        viewModel.AreButtonsEnabled = false;
        // button 2 code...
    }
}

HandleButton3Clicked(object sender, EventArgs e)
{
    if (viewModel.AreButtonsEnabled)
    {
        viewModel.AreButtonsEnabled = false;
        // button 3 code...
    }
}

А затем просто называется viewModel.AreButtonsEnabled = true; когда вы хотите снова включить кнопки.


Если вам нужен "истинный" шаблон MVVM, вы можете привязывать команды к кнопкам вместо того, чтобы слушать их события Clicked.

MyViewModel.cs:

private bool _areButtonsEnabled = true;

public bool AreButtonsEnabled
{
    get => _areButtonsEnabled;
    set
    {
        if (_areButtonsEnabled != value)
        {
            _areButtonsEnabled = value;
            OnPropertyChanged(nameof(AreButtonsEnabled)); // assuming your view model implements INotifyPropertyChanged
        }
    }
}

public ICommand Button1Command { get; protected set; }

public MyViewModel()
{
    Button1Command = new Command(HandleButton1Tapped);
}

private void HandleButton1Tapped()
{
    // Run on the main thread, to make sure that it is getting/setting the proper value for AreButtonsEnabled
    // And note that calls to Device.BeginInvokeOnMainThread are queued, therefore
    // you can be assured that AreButtonsEnabled will be set to false by one button command
    // before the value of AreButtonsEnabled is checked by another button command.
    // (Assuming you don't change the value of AreButtonsEnabled on another thread)
    Device.BeginInvokeOnMainThread(() => 
    {
        if (AreButtonsEnabled)
        {
            AreButtonsEnabled = false;
            // button 1 code...
        }
    });
}

// don't forget to add Commands for Button 2 and 3

MyPage.xaml (отображается только одна кнопка):

<Button 
    Text="Back" 
    BackgroundColor="White"
    AbsoluteLayout.LayoutFlags="All"
    AbsoluteLayout.LayoutBounds="0.5, 0.6, 0.5, 0.1"
    Command={Binding Button1Command}
    IsEnabled={Binding AreButtonsEnabled} />

И теперь вам не нужно добавлять код в файл MyPage.xaml.cs кодом.

Ответ 4

Общее состояние - это то, что вам нужно. Наименее интрузивный и самый общий способ - просто оберните свой код:

    private bool isClicked;
    private void AllowOne(Action a)
    {
        if (!isClicked)
        {
            try
            {
                isClicked = true;
                a();
            }
            finally
            {
                isClicked = false;
            }               
        }
    }

    void Button1Clicked(object sender, EventArgs e)
    {
        AllowOne(() =>
        {
            // ... do something first ...
            page1.IsVisible = true;
            Console.WriteLine("Button 1 Clicked!");
        });
    }

Шаблон try..finally важен для обеспечения безопасности. В случае, если a() выбрасывает исключение, состояние не скомпрометировано.

Ответ 5

У меня есть это в моей базовой модели:

public bool IsBusy { get; set; }

protected async Task RunIsBusyTaskAsync(Func<Task> awaitableTask)
{
    if (IsBusy)
    {
        // prevent accidental double-tap calls
        return;
    }
    IsBusy = true;
    try
    {
        await awaitableTask();
    }
    finally
    {
        IsBusy = false;
    }
}

Делегат команды тогда выглядит так:

    private async Task LoginAsync()
    {
        await RunIsBusyTaskAsync(Login);
    }

... или если у вас есть параметры:

    private async Task LoginAsync()
    {
        await RunIsBusyTaskAsync(async () => await LoginAsync(Username, Password));
    }

Метод входа будет содержать вашу реальную логику

    private async Task Login()
    {
        var result = await _authenticationService.AuthenticateAsync(Username, Password);

        ...
    }

также вы можете использовать встроенный делегат:

    private async Task LoginAsync()
    {
        await RunIsBusyTaskAsync(async () =>
        {
            // logic here
        });
    }

настройка IsEnabled не требуется. вы можете заменить Func на Action, если действующая логика не асинхронна.

Вы также можете привязать свойство IsBusy для таких вещей, как ActivityIndicator