×
Namespaces

Variants
Actions

在Windows Phone上选择多张图片

From Nokia Developer Wiki
Jump to: navigation, search
WP Metro Icon Multimedia.png
SignpostIcon WP7 70px.png
Article Metadata

代码示例
兼容于
文章
WS_YiLunLuo 在 13 Jun 2012 创建
最后由 hamishwillee 在 16 Jul 2013 编辑

Contents

简介

Windows Phone SDK提供了一个PhotoChooserTask。使用它你可以写很少的代码,就能让用户选择一张图片。但是,它不支持选择多张图片。如果要让用户选择多张图片,你必须自行提供一个界面。这时候你可以通过XNA的MediaLibrary获取手机上的图片。

本文介绍如何使用XNA的MediaLibrary获取手机上的图片,将它们的缩略图显示在一个自定义界面上,并且让用户选择多张图片。在这个过程中我们也会看到如何使用MVVM设计模式,如何针对256M内存手机优化性能,等等。

选择图片界面:

SelectMultiplePhotos.png

选中的图片们(未全部显示):

MultiplePhotosSelected.png

你可以自File:WPMultiPhotoPicker.zip下载本文附带的示例程序。

View Model

我们使用标准的MVVM设计模式。为此,首先要定义一个view model。

    public class ChoosePhotoViewModel : INotifyPropertyChanged
{
private string _name;
public string Name
{
get { return this._name; }
set
{
if (this._name != value)
{
this._name = value;
this.NotifyPropertyChange("Name");
}
}
}
 
// MediaStream does not need to support notification changed.
public Stream MediaStream { get; set; }
 
private BitmapImage _imageSource;
public BitmapImage ImageSource
{
get
{
if (this.MediaStream == null)
{
return null;
}
 
if (this._imageSource == null)
{
this._imageSource = new BitmapImage();
this._imageSource.SetSource(this.MediaStream);
}
 
return this._imageSource;
}
}
 
public bool IsSelected { get; set; }
 
public event PropertyChangedEventHandler PropertyChanged;
 
protected void NotifyPropertyChange(string propertyName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this.PropertyChanged, new PropertyChangedEventArgs(propertyName));
}
}
 
/// <summary>
/// We use the name of the photo to do comparison.
/// </summary>
public override bool Equals(object obj)
{
if (obj is ChoosePhotoViewModel)
{
return this.Name.Equals(((ChoosePhotoViewModel)obj).Name);
}
return false;
}
 
public override int GetHashCode()
{
return this.Name.GetHashCode();
}
}

这些代码没什么好多解释的,就是定义一些属性,对于改变时可能需要通知UI的属性实现INotifyPropertyChanged。这些都是标准代码。

需要注意两点:第一,我们重写了Equals方法,这样一来当比较两张图片是否等价时,就不会做一个基于引用的比较,而是做一个基于图片名称的比较了(我们不考虑图片重名的情况)。推荐在重写Equals的情况下也要重写GetHashCode。

第二,ImageSource属性是只读的,我们希望将它绑定到一个Image控件的Source属性,所以需要一个BitmapImage类型的属性。但是平时操作的话,使用Stream就足够了。

界面

我们用一个标准的页面让用户选择图片。在这个页面上创建一个ListBox,用于显示MediaLibrary中的图片。

            <ListBox x:Name="MediaListBox"
ItemTemplate="{StaticResource MediaDataTemplate}"
ItemsPanel="{StaticResource MediaItemsPanelTemplate}"
ItemContainerStyle="{StaticResource MediaListBoxItemStyle}"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
ScrollViewer.VerticalScrollBarVisibility="Disabled"/>

为了让ListBox按照我们想要的方式排列数据,我们使用Toolkit中提供的WrapPanel定制它的ItemsPanelTemplate,这样一来图片会从上到下排列,如果屏幕上一列排不下,就会在右边出现新的一列。具体控件的使用方法我们就不再复述了。

        <ItemsPanelTemplate x:Key="MediaItemsPanelTemplate">
<toolkit:WrapPanel Orientation="Vertical"/>
</ItemsPanelTemplate>

此外,在ItemTemplate中,我们显示图片,将Image控件的Source属性绑定到view model中。

        <DataTemplate x:Key="MediaDataTemplate">
<Grid>
<Image Source="{Binding ImageSource}" Margin="5"/>
</Grid>
</DataTemplate>

另一方面,默认情况下ListBox的control template不会高亮显示选中的项目,所以我们要自行修改它的template。在template中,我们使用一个CheckBox来表示这个项目是否被选中。

        <Style x:Key="MediaListBoxItemStyle" TargetType="ListBoxItem" BasedOn="{StaticResource PhotoListBoxItemStyle}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Border x:Name="LayoutRoot" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" HorizontalAlignment="{TemplateBinding HorizontalAlignment}" VerticalAlignment="{TemplateBinding VerticalAlignment}">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal"/>
<VisualState x:Name="MouseOver"/>
<VisualState x:Name="Disabled">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Background" Storyboard.TargetName="LayoutRoot">
<DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource TransparentBrush}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="SelectionStates">
<VisualState x:Name="Unselected"/>
<VisualState x:Name="Selected"/>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<CheckBox Style="{StaticResource MediaCheckBoxStyle}" IsChecked="{Binding IsSelected, Mode=TwoWay}">
<ContentPresenter/>
</CheckBox>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

当然,默认情况下CheckBox选中时会显示一个勾,而我们则希望它用一些亮丽的色彩高亮显示。所以还需要修改CheckBox的template。具体template的代码就不详细解释了,因为很多都是通过Expression Blend生成的。

        <Style x:Key="PhotoListBoxItemStyle" TargetType="ListBoxItem">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="BorderBrush" Value="Transparent"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="HorizontalContentAlignment" Value="Left"/>
<Setter Property="VerticalContentAlignment" Value="Top"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Border x:Name="LayoutRoot" HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
VerticalAlignment="{TemplateBinding VerticalAlignment}" Margin="5" Width="190" Height="190" Background="Transparent" CornerRadius="5">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal"/>
<VisualState x:Name="MouseOver"/>
<VisualState x:Name="Disabled"/>
</VisualStateGroup>
<VisualStateGroup x:Name="SelectionStates">
<VisualState x:Name="Unselected"/>
<VisualState x:Name="Selected">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="selectionBorder" Storyboard.TargetProperty="(UIElement.Opacity)" To="1" Duration="00:00:00.3"/>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Grid>
<Border x:Name="selectionBorder" BorderThickness="3" BorderBrush="#FF427EFF" CornerRadius="5" Opacity="0">
<Border.Background>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="#FFEBF9FF" Offset="0"/>
<GradientStop Color="#FF00B8FF" Offset="0.27"/>
<GradientStop Color="#FF00B3FF" Offset="0.948"/>
<GradientStop Color="#FFCEF1FF" Offset="1"/>
</LinearGradientBrush>
</Border.Background>
</Border>
<ContentPresenter Margin="5" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
 
<Style x:Key="PhoneButtonBase" TargetType="ButtonBase">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="{StaticResource PhoneForegroundBrush}"/>
<Setter Property="Foreground" Value="{StaticResource PhoneForegroundBrush}"/>
<Setter Property="BorderThickness" Value="{StaticResource PhoneBorderThickness}"/>
<Setter Property="FontFamily" Value="{StaticResource PhoneFontFamilySemiBold}"/>
<Setter Property="FontSize" Value="{StaticResource PhoneFontSizeMediumLarge}"/>
<Setter Property="Padding" Value="10,3,10,5"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ButtonBase">
<Grid Background="Transparent">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal"/>
<VisualState x:Name="MouseOver"/>
<VisualState x:Name="Pressed">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Foreground" Storyboard.TargetName="ContentContainer">
<DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneBackgroundBrush}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Background" Storyboard.TargetName="ButtonBackground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneForegroundBrush}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="BorderBrush" Storyboard.TargetName="ButtonBackground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneForegroundBrush}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Disabled">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Foreground" Storyboard.TargetName="ContentContainer">
<DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneDisabledBrush}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="BorderBrush" Storyboard.TargetName="ButtonBackground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneDisabledBrush}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Background" Storyboard.TargetName="ButtonBackground">
<DiscreteObjectKeyFrame KeyTime="0" Value="Transparent"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Border x:Name="ButtonBackground" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" CornerRadius="0" Margin="{StaticResource PhoneTouchTargetOverhang}">
<ContentControl x:Name="ContentContainer" ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" Foreground="{TemplateBinding Foreground}" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" Padding="{TemplateBinding Padding}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"/>
</Border>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
 
<Style x:Key="PhoneRadioButtonCheckBoxBase" BasedOn="{StaticResource PhoneButtonBase}" TargetType="ToggleButton">
<Setter Property="Background" Value="{StaticResource PhoneRadioCheckBoxBrush}"/>
<Setter Property="BorderBrush" Value="{StaticResource PhoneRadioCheckBoxBrush}"/>
<Setter Property="FontSize" Value="{StaticResource PhoneFontSizeMedium}"/>
<Setter Property="FontFamily" Value="{StaticResource PhoneFontFamilyNormal}"/>
<Setter Property="HorizontalContentAlignment" Value="Left"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="Padding" Value="0"/>
</Style>
 
<Style x:Key="MediaCheckBoxStyle" BasedOn="{StaticResource PhoneRadioButtonCheckBoxBase}" TargetType="CheckBox">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="CheckBox">
<Grid Background="Transparent" Width="190" Height="190" Margin="5">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal"/>
<VisualState x:Name="MouseOver"/>
<VisualState x:Name="Pressed"/>
<VisualState x:Name="Disabled"/>
</VisualStateGroup>
<VisualStateGroup x:Name="CheckStates">
<VisualState x:Name="Checked">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="selectionBorder" Storyboard.TargetProperty="(UIElement.Opacity)" To="1" Duration="00:00:00.3"/>
</Storyboard>
</VisualState>
<VisualState x:Name="Unchecked"/>
<VisualState x:Name="Indeterminate"/>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Border x:Name="selectionBorder" BorderThickness="3" BorderBrush="#FF427EFF" CornerRadius="5" Opacity="0">
<Border.Background>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="#FFEBF9FF" Offset="0"/>
<GradientStop Color="#FF00B8FF" Offset="0.27"/>
<GradientStop Color="#FF00B3FF" Offset="0.948"/>
<GradientStop Color="#FFCEF1FF" Offset="1"/>
</LinearGradientBrush>
</Border.Background>
</Border>
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

此外,在application bar中,我们定义几个按钮,用于操作。

    <phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar IsVisible="True" IsMenuEnabled="True">
<shell:ApplicationBarIconButton
x:Name="PreviousPageButton"
IconUri="/icons/appbar.back.rest.png"
IsEnabled="True"
Text="Previous"
Click="PreviousPageButton_Click"/>
<shell:ApplicationBarIconButton
x:Name="NextPageButton"
IconUri="/icons/appbar.next.rest.png"
IsEnabled="True"
Text="Next"
Click="NextPageButton_Click"/>
<shell:ApplicationBarIconButton
x:Name="OKButton"
IconUri="/icons/appbar.check.rest.png"
IsEnabled="True"
Text="OK"
Click="OKButton_Click"/>
<shell:ApplicationBarIconButton
x:Name="CancelButton"
IconUri="/icons/appbar.cancel.rest.png"
IsEnabled="True"
Text="Cancel"
Click="CancelButton_Click"/>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>

你可以自Program Files (x86)\Microsoft SDKs\Windows Phone\v7.1\Icons目录下找到很多Windows Phone SDK自带的图标,你有权利在自己的程序中使用它们。

代码逻辑

接下来我们来实现代码逻辑。

首先定义一些变量:

        private ObservableCollection<ChoosePhotoViewModel> _photoDataSource;
private List<ChoosePhotoViewModel> _selectedPhotos;
private int _currentPage = 0;
private int _pageSize = 9;

这里有两个变量用于分页。因为用户的媒体库中可能有很多很多图片,就算使用缩略图,一次性将它们全部加载近内存还是可能会导致内存使用超过60M(通常我们要求大家的程序内存使用量低于60M,否则在256M内存的手机,例如Lumia 610上,可能会有很严重的性能问题)。我们这里每页显示9幅画,这样刚好够在一屏幕上显示大家可以根据情况修改这个数值。

此外,_photoDataSource用于保存当前页面的数据源(它的数量不会超过9个),而_selectedPhotos则用于保存用户选中的图片(为了在跨页面选择时保存之前页面的数据)。

还有,为了方便,在App类中,我们创建一个属性保存选中的图片,这个属性可以被其它页面访问。

        public static ObservableCollection<ChoosePhotoViewModel> PhotoCollection = new ObservableCollection<ChoosePhotoViewModel>();

在OnNavigatedTo方法中,我们初始化这些变量,并且获取第一页的图片。

        protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e)
{
this._photoDataSource = new ObservableCollection<ChoosePhotoViewModel>();
this._selectedPhotos = new List<ChoosePhotoViewModel>();
this.GetPicturesForCurrentPage();
this.MediaListBox.ItemsSource = _photoDataSource;
 
base.OnNavigatedTo(e);
}

以下是GetPicturesForCurrentPage的实现,它对所有页是通用的,而不仅仅针对第一页有效:

        /// <summary>
/// It's not a good idea to display all photos at once.
/// This method implements paging.
/// </summary>
private void GetPicturesForCurrentPage()
{
// Page number should not be less than 0.
if (this._currentPage < 0)
{
this._currentPage = 0;
return;
}
 
MediaLibrary mediaLibrary = new MediaLibrary();
 
// Already the last page.
int pageCount = mediaLibrary.Pictures.Count / this._pageSize;
if (this._currentPage > pageCount)
{
this._currentPage = pageCount;
return;
}
 
// Store the selection.
this.StoreSelection();
 
// Reset the data source.
this._photoDataSource.Clear();
 
var picturesOnCurrentPage = mediaLibrary.Pictures.Skip(this._currentPage * this._pageSize).Take(this._pageSize);
foreach (var picture in picturesOnCurrentPage)
{
Stream pictureStream = picture.GetThumbnail();
ChoosePhotoViewModel viewModel = new ChoosePhotoViewModel()
{
Name = picture.Name,
MediaStream = pictureStream
};
 
// In view model, we override Equals to compare the name of the photo rather than the reference.
// So if a photo with the same name is found in selected photos, Contains will return true.
if (this._selectedPhotos.Contains(viewModel))
{
viewModel.IsSelected = true;
}
 
_photoDataSource.Add(viewModel);
}
}

在这里我们先调用StoreSelection(代码会在后面给出)保存前一页上选中的数据,从而保证今后返回到这一页时选择不会丢失。

然后我们使用XNA的MediaLibrary获取当前页的图片。MediaLibrary.Pictures属性实现了IEnumerable,所以它支持标准的LINQ的Skip和Take方法,我们的分页逻辑因此可以大幅简化。如果你不熟悉标准的LINQ分页,可以参考http://msdn.microsoft.com/en-us/library/bb386988.aspx。虽然这篇文档针对的是LINQ to SQL ,但是对于一般的LINQ也是适用的。

在获取了图片之后,我们不急于将它打开,而是通过GetThumbnail方法获取一个缩略图。在我们的图片选择界面上我们根本不需要显示完整大小的图片,缩略图就足够了。使用缩略图可以大大降低内存使用。

接下来就是针对每一幅画创建一个view model了。注意这个方法是针对所有页通用的,所以我们要考虑用户可能从第二页返回第一页,因此需要判断这幅画是不是曾经被选中过。

下面再给出StoreSelection方法:

        /// <summary>
/// Add selected photos on the current page to selected photo list,
/// and close unselected photos.
/// </summary>
private void StoreSelection()
{
foreach (ChoosePhotoViewModel photo in this._photoDataSource)
{
if (photo.IsSelected)
{
if (!this._selectedPhotos.Contains(photo))
{
this._selectedPhotos.Add(photo);
}
}
// If the media is not chosen, close the thumbnail stream.
else
{
if (this._selectedPhotos.Contains(photo))
{
this._selectedPhotos.Remove(photo);
}
photo.MediaStream.Close();
}
}
}

这个方法在用户切换页时调用,用于存储当前页上选中的图片。没什么好多解释的,唯一需要注意的是如果一幅画没有被选中,我们就直接把它的thumbnail close掉,从而释放内存。

现在,在翻页按钮被点下时,我们只需要修改_currentPage,并且调用GetPicturesForCurrentPage就可以了。

        private void PreviousPageButton_Click(object sender, System.EventArgs e)
{
this._currentPage--;
this.GetPicturesForCurrentPage();
}
 
private void NextPageButton_Click(object sender, System.EventArgs e)
{
this._currentPage++;
this.GetPicturesForCurrentPage();
}

最后,当用户点击OK, Cancel按钮时,我们是情况返回选中的图片,或者扔掉所有的选中的项目:

        private void OKButton_Click(object sender, System.EventArgs e)
{
this.StoreSelection();
 
// Add all selected photos to App.MediaCollection.
foreach (ChoosePhotoViewModel photo in this._selectedPhotos)
{
// Return the original photo stream rather than the thumbnail.
photo.MediaStream = this.GetOriginalImage(photo.Name);
App.PhotoCollection.Add(photo);
}
 
// Go back to the calling page.
this.NavigationService.GoBack();
}
 
private void CancelButton_Click(object sender, System.EventArgs e)
{
// Close all thumbnail streams.
foreach (ChoosePhotoViewModel photo in this._photoDataSource)
{
photo.MediaStream.Close();
}
 
// Go back to the calling page without doing anything.
this.NavigationService.GoBack();
}

我们之前一直在用缩略图,但是返回图片时最好返回原是图片的stream,而不是缩略图。因此我们写了一个GetOriginalImage方法:

        /// <summary>
/// Get the original image from XNA media library.
/// </summary>
/// <param name="name">The name of the image.</param>
/// <returns>A stream representing the resized image, in jpeg format.</returns>
public Stream GetOriginalImage(string name)
{
MediaLibrary mediaLibrary = new MediaLibrary();
PictureCollection pictureCollection = mediaLibrary.Pictures;
Picture picture = pictureCollection.Where(p => p.Name == name).FirstOrDefault();
if (picture == null)
{
throw new InvalidOperationException(string.Format("Cannot load the picture {0}. Possibly the picture has been deleted.", name));
}
Stream originalImageStream = picture.GetImage();
originalImageStream.Position = 0;
return originalImageStream;
}

在这个方法中,我们使用LINQ根据图片的名字从media library中找到对应的图片,并且打开它。

测试

在MainPage中让我们做一个简单的测试。使用一个ListBox来显示选中的图片:

            <ListBox
x:Name="photoListBox"
ItemTemplate="{StaticResource PhotoDataTemplate}"
Grid.Row="1"/>

这里的ItemTemplate中,我们不指定图片大小,因此会撑满屏幕。如果你发现图片清晰度好象很低,就说明你可能之前没有返回原是图片流,而是返回了缩略图。

        <DataTemplate x:Key="PhotoDataTemplate">
<Grid>
<Image Source="{Binding ImageSource}" Margin="5"/>
</Grid>
</DataTemplate>

在代码中间,我们简单判断是否有选中的图片,如果没有,就跳转到选择图片的页面。

        protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e)
{
if (App.PhotoCollection.Count == 0)
{
this.NavigationService.Navigate(new Uri("/ChoosePhotoPage.xaml", UriKind.Relative));
}
else
{
this.photoListBox.ItemsSource = App.PhotoCollection;
}
base.OnNavigatedTo(e);
}

总结

因为Windows Phone自己没有提供选择多张图片的功能,我们不得不自己实现。可以看到,要做这样一个功能,还是需要花不少功夫的。所以我们要感谢系统提供的那么多组件才行啊,它们帮助我们省下了很多很多心力,让我们可以集中思考组装自己的程序,而不是如何创建每一个组件。

This page was last modified on 16 July 2013, at 10:23.
133 page views in the last 30 days.
×