Как по-настоящему избежать одновременного нажатия нескольких кнопок в Xamarin.Forms?
Я использую несколько кнопок в представлении, и каждая кнопка приводит к ее собственной всплывающей странице. При одновременном нажатии нескольких кнопок он переходит на разные всплывающие окна за раз.
Я создал образец страницы контента с тремя кнопками (каждый из них идет на другую всплывающую страницу), чтобы продемонстрировать эту проблему:
Страница 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