我正在开发一个计时器应用程序。当您单击主页的“开始”按钮时,它会导航到显示已用时间的另一个页面。
我遇到的问题是,视图在 Android 上加载需要时间(可能是因为我处于调试模式并且我使用的设备相对较旧),并且时间计数活动(在 ViewModel 中)启动了几秒钟在视图实际开始之前。
ViewModel 的时间计数由 View 启动,如下所示:
((TimerSessionViewModel)BindingContext).BeginSession();
ViewModel 在这之后所做的就是基本上使用一个简单的同步机制来计算经过的时间,您可以在这段代码中看到:
_timer = new Timer(TimerCallback, null, TimeSpan.Zero, TimeSpan.FromMilliseconds(TimerToolkitConstants.TimerTickMilliseconds));
public void BeginSession()
{
SessionIsStarted = true;
SessionStarted?.Invoke(this, new EventArgs());
OnSessionStart();
}
private void TimerCallback(object? state)
{
if (SessionIsStarted && !TimerPaused)
{
SessionElapsedTime = SessionElapsedTime.Add(TimeSpan.FromMilliseconds(TimerToolkitConstants.TimerTickMilliseconds));
}
}
-> 所以整个想法是让视图在加载并准备好显示时间计数时调用“BeginSession”。
现在我遇到的问题是,我尝试过的视图提供的所有事件(已加载、出现、布局更改...)都是在视图实际出现在我的手机上之前引发的。现在我确信这很大程度上是由于环境(调试模式、相对较旧的设备)造成的,但即使在这种环境下也能有一些东西运行良好是理想的,这样我就可以知道应用程序即使在恶劣的条件下也能运行良好.
我不确定是否可以做得比我这里做得更好,但我必须征求意见,以防万一我遗漏了一些东西。感谢您对此的任何意见。
对于时间紧迫的操作,我想建议一些优化来进行试验。首先是避免必须导航到新视图。使用 OnePage 架构,其中视图重叠在公共网格上,可见性由
OnePageState
的值控制。这应该可以让您非常快速地从主页面视图切换到“看起来像”导航页面,但实际上并非如此。
第二种是将您的计时基于
Stopwatch
并使用异步 Task.Delay 调用来更新可见显示。即使切换视图需要几十毫秒,经过的时间也是从调用启动命令的那一刻开始计算的,因此仍然会被计算在内。您要求完美同步,重要的是要注意它不会达到用核同位素测量时间的原子精度,但它确实不错。 您提到拥有较旧的 Android,因此如果您想了解设备上的性能,我在 GitHub 上发布了 Clone。
首先创建一个
IValueConverter
类,如果两个 true
值相等,则返回 enum
。在这里,如果 OnePageState
的绑定值不是 OnePageState.Main
以外的任何值,默认的 MAUI 页面现在将变得不可见。同样,当该值变为 OnePageState.Timer
时,备用虚拟页面变得可见。
I值转换器
class EnumToBoolConverter : IValueConverter
{
public object Convert(object unk, Type targetType, object parameter, CultureInfo culture)
{
if (unk is Enum enum1 && parameter is Enum @enum2)
{
return enum1.Equals(@enum2);
}
return false;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) =>
throw new NotImplementedException();
}
计时器布局与毛伊岛默认布局重叠
在公共网格中的毛伊岛默认视图之上添加计时器视图。
<!--1 x 1 Grid to contain overlapping virtual page representations-->
<Grid x:Name="OnePageGrid">
<!--Maui Default Virtual Page-->
<ScrollView
IsVisible="{Binding
OnePageState,
Converter={StaticResource EnumToBoolConverter},
ConverterParameter={x:Static local:OnePageState.Main}}">
<VerticalStackLayout
Spacing="25"
Padding="30,0"
VerticalOptions="Center">
.
.
.
</ScrollView>
接下来,在毛伊岛默认视图之上,我们为 MainPage.Content 堆叠了完全不同的“外观”。
<!--Timer Virtual Page-->
<Grid
IsVisible="{Binding
OnePageState,
Converter={StaticResource EnumToBoolConverter},
ConverterParameter={x:Static local:OnePageState.Timer}}"
RowDefinitions="50,*,50"
BackgroundColor="MidnightBlue">
<HorizontalStackLayout
HorizontalOptions="Fill"
BackgroundColor="White">
<Button
Text ="<--- Main Page"
TextColor="CornflowerBlue"
BackgroundColor="White"
FontSize="Medium"
HorizontalOptions="Start"
Command="{Binding SetOnePageStateCommand}"
CommandParameter="{x:Static local:OnePageState.Main}"/>
</HorizontalStackLayout>
<Label
Grid.Row="1"
Text="{Binding TimerDisplay}"
TextColor="White"
FontSize="48"
FontAttributes="Bold"
HorizontalOptions="Center"
VerticalOptions="Center"
HorizontalTextAlignment="Center"
VerticalTextAlignment="Center"
BackgroundColor="Transparent">
<Label.GestureRecognizers>
<TapGestureRecognizer Command="{Binding StartTimerCommand}"/>
</Label.GestureRecognizers>
</Label>
<Grid
Grid.Row="2"
BackgroundColor="White">
<Button
Text ="Reset"
TextColor="CornflowerBlue"
BackgroundColor="White"
FontSize="Medium"
HorizontalOptions="CenterAndExpand"
Command="{Binding ResetTimerCommand}"/>
</Grid>
</Grid>
</Grid>
建议:避免使用计时器
这是一种无需使用
Timer
实例和由此产生的行李即可简化更新已用时间的方法。
Task _pollingTask;
private Stopwatch _stopwatch = new Stopwatch();
private async void OnStartTimer(object o)
{
if (!_stopwatch.IsRunning)
{
OnePageState = OnePageState.Timer;
try
{
if (_cts != null)
{
_cts.Cancel();
}
_cts = new CancellationTokenSource();
var token = _cts.Token;
_stopwatch.Restart();
while (!token.IsCancellationRequested)
{
var elapsed = _stopwatch.Elapsed;
MainThread.BeginInvokeOnMainThread(() =>
{
TimerDisplay =
elapsed < TimeSpan.FromSeconds(1) ?
elapsed.ToString(@"hh\:mm\:ss\.ff") :
elapsed.ToString(@"hh\:mm\:ss");
});
await Task.Delay(TimeSpan.FromSeconds(0.1), token);
}
}
catch { }
finally
{
_stopwatch.Stop();
_pollingTask?.Dispose();
}
}
}