[MAUI]模仿iOS多任务切换卡片滑动的交互实现

科技资讯 投稿 6000 0 评论

[MAUI]模仿iOS多任务切换卡片滑动的交互实现

目录
    原理
  • 创建布局
  • 创建分布函数
  • 创建动效
  • 创建绑定数据
  • 细节调整
    • 首张卡片的处理
    • 为卡片添加裁剪
    • 跳转到最后一张卡片
  • 项目地址

看了上一篇博文的评论,大家对MAUI还是比较感兴趣的,非常感谢大家的关注,这个专栏我争取周更😉。

动图来自iPhone 使用手册 - 在 iPhone 上的应用之间切换

今天我们在.NET MAUI中实现这个优秀交互效果
,最终效果如下:

原理

使用过的App将以屏幕截图的卡片方式展现,卡片从右到左依次排列,最近使用的app卡片将靠前,并叠层在其他久未使用的app卡片之上。

平铺分布是经典的卡片布局,它的卡片分部是均匀的

卡片在屏幕横轴的位置与其偏移量是一个线性关系,如下图:

在iOS多任务卡片的布局中,卡片在屏幕范围内的布局由左向右的密度依次降低:

二阶贝塞尔曲线,可以通过三个点,来确定一条平滑的曲线。详情请参考这里

创建布局

新建.NET MAUI项目,命名MultitaskingCardList。将界面图片资源文件拷贝到项目\Resources\Images中并将他们包含在MauiImage资源清单中。

<MauiImage Include="Resources\Images\*" />

在MainPage.xaml中,创建一个横向StackLayout作为App后台任务卡片容器,我们将使用绑定集合的方式,将App后台任务添加到这个容器中。

<StackLayout Orientation="Horizontal"
    BindingContextChanged="BoxLayout_BindingContextChanged"
    x:Name="BoxLayout"
    BindableLayout.ItemsSource="{Binding AppTombStones}">

它的DataTemplate代表一个App后台任务,使用Grid布局,App的截图与名称分别位于Grid的第二行和第一行。

<BindableLayout.ItemTemplate>
    <DataTemplate>
        <Grid Style="{StaticResource BoxFrameStyle}" >
            <Grid.RowDefinitions>
                <RowDefinition Height="auto"></RowDefinition>
                <RowDefinition></RowDefinition>
            </Grid.RowDefinitions>
            <Label Margin="25,0,0,0" TranslationY="30"  Text="{Binding AppName}" VerticalOptions="End"></Label>
            <Image  Aspect="AspectFill"
                    Grid.Row="1"
                    HeightRequest="550"
                    WidthRequest="250"
                    Source="{Binding AppScreen}">           
            </Image>

        </Grid>
    </DataTemplate>
</BindableLayout.ItemTemplate>

对卡片Grid的样式进行定义:

<ContentPage.Resources>
    <Style TargetType="Grid"
            x:Key="BoxFrameStyle">

        <Setter Property="WidthRequest"
                Value="300"></Setter>
        <Setter Property="Margin"
                Value="0,0,-220,0"></Setter>
        <Setter Property="AnchorX"
                Value="0"></Setter>
    </Style>
</ContentPage.Resources>

效果如下:

创建分布函数

二阶贝塞尔曲线由三个点确定,分别是:
起始点、终止点(也称锚点)、控制点

在MainPage.xaml.cs中订阅页面加载完毕事件PageLoaded,在事件方法中编写代码如下:

var p0 = new Point(0, 1;
var p1 = new Point(0.1, 0.9988;
var p2 = new Point(0.175, 0.9955;


var p3 = new Point(0.4, 0.99;
var p4 = new Point(0.575, 0.92;
var p5 = new Point(0.7, 0.88;

var p6 = new Point(0.775, 0.71;
var p7 = new Point(0.9, 0.4;
var p8 = new Point(1, 0;

this.BezierSegments = new Point[][] {

    new Point[]{p0,p1,p2},
    new Point[]{p2,p3,p4},
    new Point[]{p4,p5,p6},
    new Point[]{p6,p7,p8}
};

bezeirPointSubdivs,标示贝塞尔曲线上点的数量,值越大,曲线越平滑,但计算量也越大,这里取999

var bezeirPointSubdivs = 999;

根据二阶贝塞尔函数式:

X轴坐标

var bezeirPointX = Math.Pow(1 - (doublej / bezeirPointSubdivs, 2 * BezierSegments[i][0].X + 2 * (doublej / bezeirPointSubdivs * (1 - (doublej / bezeirPointSubdivs * BezierSegments[i][1].X + Math.Pow((doublej / bezeirPointSubdivs, 2 * BezierSegments[i][2].X;

Y轴坐标:

var bezeirPointY = Math.Pow(1 - (doublej / bezeirPointSubdivs, 2 * BezierSegments[i][0].Y + 2 * (doublej / bezeirPointSubdivs * (1 - (doublej / bezeirPointSubdivs * BezierSegments[i][1].Y + Math.Pow((doublej / bezeirPointSubdivs, 2 * BezierSegments[i][2].Y;

对每一段的贝塞尔曲线计算,拟合出一条完整曲线
计算而得的离散点存入BezeirPoints,代码如下:

for (int i = 0; i < this.BezierSegments.Length; i++
    {
        for (int j = 0; j < bezeirPointSubdivs; j++
        {
            var bezeirPointX = Math.Pow(1 - (doublej / bezeirPointSubdivs, 2 * BezierSegments[i][0].X + 2 * (doublej / bezeirPointSubdivs * (1 - (doublej / bezeirPointSubdivs * BezierSegments[i][1].X + Math.Pow((doublej / bezeirPointSubdivs, 2 * BezierSegments[i][2].X;
            var bezeirPointY = Math.Pow(1 - (doublej / bezeirPointSubdivs, 2 * BezierSegments[i][0].Y + 2 * (doublej / bezeirPointSubdivs * (1 - (doublej / bezeirPointSubdivs * BezierSegments[i][1].Y + Math.Pow((doublej / bezeirPointSubdivs, 2 * BezierSegments[i][2].Y;
            BezeirPoints.Add(new Point(bezeirPointX, bezeirPointY;

        }
    }

我们使用线性插值法(linear interpolation,计算平移手势进度,卡片的分布偏移量以及大小等值。

public double Modulate(double value, double[] source, double[] target
{
    if (source.Length != 2 || target.Length != 2
    {
        throw new ArgumentOutOfRangeException(;
    }

    var start = source[0];
    var end = source[1];
    var targetStart = target[0];
    var targetEnd = target[1];
    if (value < start || value > end
    {
        return value;
    }
    var k = (value - start / (end - start;
    var result = k * (targetEnd - targetStart + targetStart;
    return result;
}

创建动效

我们将为App后台任务容器创建平移手势,实现各个卡片的滚动动效,当用户指尖在屏幕水平方向上滑动时,卡片内容也应该随之横向滚动。

因此滚动行为(滚动阻尼,滚动惯性等)由各平台的原生代码实现。

<ScrollView x:Name="MainScroller"
    Background="Transparent"
    Orientation="Horizontal"
    Scrolled="ScrollView_Scrolled">

    <!--App后台任务卡片容器-->
    <StackLayout>...</StackLayout>


</ScrollView> 

效果如下:

relativeOffsetX为卡片去除了滚动的影响,相对于屏幕的X方向位置。即相位置

再通过调制方法Modulate,计算卡片的缩放,透明度,偏移量等值。

private void RenderTransform(double scrollX
{
    var layoutWidth = this.MainLayout.DesiredSize.Width;
    if (this.BezeirPoints == null
    {
        return;
    }
    foreach (var item in this.BoxLayout.Children
    {
        if (item is VisualElement
        {
            var relativeOffsetX = (item as VisualElement.X-scrollX;
            var progress = this.Modulate(relativeOffsetX, new double[] { 0, layoutWidth }, new double[] { 0, 1 };
            (item as VisualElement.ScaleTo(Modulate(progress, new double[] { 0, 1 }, new double[] { 0.72, 0.84 }, 0;
            (item as VisualElement.FadeTo(Modulate(progress, new double[] { 0.2, 0.54 }, new double[] { 0, 1 }, 0;
            var modulatedX = Modulate(1 - GetMappingY(progress, new double[] { 0, 1 }, new double[] { 0, layoutWidth };
            var offsetX = modulatedX - relativeOffsetX;
            (item as VisualElement.TranslateTo(offsetX, 0, 0;
        }
    }
}

静态效果如下:

订阅滚动事件Scrolled,在事件方法中调用RenderTransform。代码如下:

private void ScrollView_Scrolled(object sender, ScrolledEventArgs e
{
    RenderTransform(e.ScrollX;
}

创建绑定数据

创建MainPageViewModel.cs,用于界面绑定数据源。

public class AppTombStone
{
    public AppTombStone( { }

    public string AppName { get; set; }
    public string AppScreen { get; set; }
    public double TestOffset { get; set; }
}

在MainPageViewModel构造函数中,初始化AppTombStone列表,代码如下:

public class MainPageViewModel : INotifyPropertyChanged
{
    public MainPageViewModel(
    {
        var list = new List<AppTombStone>
        {
            new AppTombStone( { AppName="Edge", AppScreen= "p1.png",TestOffset=0},
            new AppTombStone( { AppName="Map", AppScreen= "p2.png",TestOffset=-10 },
            new AppTombStone( { AppName="Photo", AppScreen= "p3.png",TestOffset=-70 },
            new AppTombStone( { AppName="App Store", AppScreen= "p4.png" ,TestOffset=-90},
            new AppTombStone( { AppName="Calculator", AppScreen= "p5.png",TestOffset=-70 },
            new AppTombStone( { AppName="Music", AppScreen= "p6.png" ,TestOffset=-30},
            new AppTombStone( { AppName="File", AppScreen= "p7.png" },
            new AppTombStone( { AppName="Note", AppScreen= "p8.png" },
            new AppTombStone( { AppName="Paint", AppScreen= "p9.png" },
            new AppTombStone( { AppName="Weather", AppScreen= "p10.png" },
            new AppTombStone( { AppName="Chrome", AppScreen= "p11.png" },
            new AppTombStone( { AppName="Book", AppScreen= "p12.png" },
            new AppTombStone( { AppName="Browser", AppScreen= "p13.png" }
        };

        AppTombStones = new ObservableCollection<AppTombStone>(list;
    }

细节调整

首张卡片的处理

这里遇到个问题,当滚动框架滚动到最左侧时,最下方的卡片会被叠层上方的卡片覆盖,如下图所示:

private void BoxLayout_BindingContextChanged(object sender, EventArgs e
    {
        this.BoxLayout.Children.Insert(0, new BoxView(
        {
            WidthRequest=300,
            HeightRequest=500,
            BackgroundColor=Colors.Red
        };
    }

效果:

为卡片添加裁剪

<Image  Aspect="AspectFill"
        Grid.Row="1"
        HeightRequest="550"
        WidthRequest="250"
        Source="{Binding AppScreen}">
    <Image.Clip>
        <RoundRectangleGeometry
            CornerRadius="20"
            Rect="0,20,250,480">
        </RoundRectangleGeometry>
    </Image.Clip>
    <Image.Shadow>
        <Shadow Brush="Black"
                Radius="40"
                Offset="-20,0"
                Opacity="0.3" />
    </Image.Shadow>
</Image>

跳转到最后一张卡片

App后台任务是从右到左排列的,因此在App启动时,需要将滚动框架滚动到最后一张卡片,代码如下:

private async void ContentPage_SizeChanged(object sender, EventArgs e
{
    var layoutWidth = this.MainLayout.DesiredSize.Width;

    var scrollY = this.MainScroller.ScrollY;
    var posX = this.MainScroller.ContentSize.Width-layoutWidth;
    await this.MainScroller.ScrollToAsync(posX, scrollY, false.ContinueWith((t =>
    {
        RenderTransform(this.MainScroller.ScrollX;
    };

}

最终效果:

项目地址

Github:maui-samples

编程笔记 » [MAUI]模仿iOS多任务切换卡片滑动的交互实现

赞同 (32) or 分享 (0)
游客 发表我的评论   换个身份
取消评论

表情
(0)个小伙伴在吐槽