最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

MAUI模仿iOS多任務(wù)切換卡片滑動(dòng)的交互實(shí)現(xiàn)代碼

 更新時(shí)間:2023年05月05日 08:50:08   作者:林曉lx  
這篇文章主要介紹了[MAUI]模仿iOS多任務(wù)切換卡片滑動(dòng)的交互實(shí)現(xiàn),使用.NET MAU實(shí)現(xiàn)跨平臺(tái)支持,本項(xiàng)目可運(yùn)行于Android、iOS平臺(tái),需要的朋友可以參考下

上一篇博文的評(píng)論,大家對(duì)MAUI還是比較感興趣的,非常感謝大家的關(guān)注,這個(gè)專欄我爭(zhēng)取周更??。

App之間的多任務(wù)切換相信你們都很熟悉。蘋果設(shè)備從iOS9開始使用水平排列的疊層卡片來(lái)展現(xiàn)多任務(wù)

動(dòng)圖來(lái)自iPhone 使用手冊(cè) - 在 iPhone 上的應(yīng)用之間切換

這個(gè)設(shè)計(jì)利用屏幕深度(z方向)和水平空間(x軸方向)的平順結(jié)合,在有限的屏幕空間內(nèi),展現(xiàn)了更多的卡片,滑動(dòng)屏幕時(shí),每一個(gè)卡片在屏幕中央的時(shí)候也能得到大面積的展示。

今天我們?cè)?a target="_blank">.NET MAUI中實(shí)現(xiàn)這個(gè)優(yōu)秀交互效果
,最終效果如下:

使用.NET MAU實(shí)現(xiàn)跨平臺(tái)支持,本項(xiàng)目可運(yùn)行于Android、iOS平臺(tái)。

原理

使用過(guò)的App將以屏幕截圖的卡片方式展現(xiàn),卡片從右到左依次排列,最近使用的app卡片將靠前,并疊層在其他久未使用的app卡片之上。

平鋪分布

平鋪分布是經(jīng)典的卡片布局,它的卡片分部是均勻的

在有限的屏幕寬度內(nèi)呈現(xiàn)6張卡片,疊層放置后每張卡片可顯示部分的寬度為屏幕寬度的1/6

卡片在屏幕橫軸的位置與其偏移量是一個(gè)線性關(guān)系,如下圖:

iOS多任務(wù)卡片分布

在iOS多任務(wù)卡片的布局中,卡片在屏幕范圍內(nèi)的布局由左向右的密度依次降低:

它的布局位置是由4段二階貝塞爾曲線拼接成的完整曲線函數(shù)計(jì)算而來(lái)的。

二階貝塞爾曲線,可以通過(guò)三個(gè)點(diǎn),來(lái)確定一條平滑的曲線。詳情請(qǐng)參考這里

卡片在屏幕橫軸的位置與其偏移量如下圖:

同樣是在頁(yè)面上從左至右呈現(xiàn)6張卡片。利用貝塞爾曲線函數(shù)的特性,編號(hào)靠前的卡片(1,2,3)的偏移量“滯后”,編號(hào)靠后的卡片(4,5,6)的偏移量“追趕”,這樣保證了編號(hào)靠后的卡片(較新的App任務(wù))布局密度降低,從而有更大面積的展示。

計(jì)算每一個(gè)卡片的偏移量,卡片的大小隨偏移量成正比,效果如下圖:

接下來(lái)我們用幾張App截圖代替顏色交替的卡片并賦予其動(dòng)效。

創(chuàng)建布局

新建.NET MAUI項(xiàng)目,命名MultitaskingCardList。將界面圖片資源文件拷貝到項(xiàng)目\Resources\Images中并將他們包含在MauiImage資源清單中。

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

在MainPage.xaml中,創(chuàng)建一個(gè)橫向StackLayout作為App后臺(tái)任務(wù)卡片容器,我們將使用綁定集合的方式,將App后臺(tái)任務(wù)添加到這個(gè)容器中。

代碼如下:

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

它的DataTemplate代表一個(gè)App后臺(tái)任務(wù),使用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>

對(duì)卡片Grid的樣式進(jìn)行定義:

寬度300,高度550,左邊距-220,這使得屏幕區(qū)域范圍內(nèi)有大概5-6個(gè)卡片可見。

<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>

效果如下:

創(chuàng)建分布函數(shù)

為了快速映射位置與偏移量,我們?cè)陧?yè)面加載時(shí)計(jì)算出貝塞爾函數(shù)曲線上的離散點(diǎn)

二階貝塞爾曲線由三個(gè)點(diǎn)確定,分別是:
起始點(diǎn)、終止點(diǎn)(也稱錨點(diǎn))、控制點(diǎn)

BezierSegments對(duì)象將描述4段連續(xù)的,首尾相連的二階貝塞爾曲線

在MainPage.xaml.cs中訂閱頁(yè)面加載完畢事件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,標(biāo)示貝塞爾曲線上點(diǎn)的數(shù)量,值越大,曲線越平滑,但計(jì)算量也越大,這里取999

var bezeirPointSubdivs = 999;

根據(jù)二階貝塞爾函數(shù)式:

將點(diǎn)坐標(biāo)帶入表達(dá)式,則可以得出輸入輸出值之間的映射關(guān)系,代碼如下:

X軸坐標(biāo)

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

Y軸坐標(biāo):

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

對(duì)每一段的貝塞爾曲線計(jì)算,擬合出一條完整曲線
計(jì)算而得的離散點(diǎn)存入BezeirPoints,代碼如下:

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

我們使用線性插值法(linear interpolation),計(jì)算平移手勢(shì)進(jìn)度,卡片的分布偏移量以及大小等值。

線性插值法是指使用連接兩個(gè)已知量的直線來(lái)確定在這兩個(gè)已知量之間的一個(gè)未知量的值的方法。具體請(qǐng)參考這里

假設(shè)我們已知坐標(biāo)(x0,y0)與(x1,y1),要得到[x0,x1]區(qū)間內(nèi)某一位置x在直線上的值。根據(jù)圖中所示,我們得到兩點(diǎn)式直線方程

創(chuàng)建調(diào)制方法Modulate,代碼如下

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;
}

創(chuàng)建動(dòng)效

我們將為App后臺(tái)任務(wù)容器創(chuàng)建平移手勢(shì),實(shí)現(xiàn)各個(gè)卡片的滾動(dòng)動(dòng)效,當(dāng)用戶指尖在屏幕水平方向上滑動(dòng)時(shí),卡片內(nèi)容也應(yīng)該隨之橫向滾動(dòng)。

原本的實(shí)現(xiàn)方式是控件自監(jiān)聽平移(Pan)事件,通過(guò)x軸方向的平移偏移量,計(jì)算卡片容器中各個(gè)卡片的偏移量,從而實(shí)現(xiàn)卡片滾動(dòng)動(dòng)效。但平移過(guò)后的慣性滑動(dòng)要自行計(jì)算,滑動(dòng)手感不夠流暢,最終效果并不理想,因此改用MAUI的ScrollView控件作為滾動(dòng)框架

因此滾動(dòng)行為(滾動(dòng)阻尼,滾動(dòng)慣性等)由各平臺(tái)的原生代碼實(shí)現(xiàn)。

<ScrollView x:Name="MainScroller"
    Background="Transparent"
    Orientation="Horizontal"
    Scrolled="ScrollView_Scrolled">
    <!--App后臺(tái)任務(wù)卡片容器-->
    <StackLayout>...</StackLayout>
</ScrollView> 

效果如下:

創(chuàng)建RenderTransform方法,實(shí)現(xiàn)卡片的平移,縮放,透明度等動(dòng)效。
relativeOffsetX為卡片去除了滾動(dòng)的影響,相對(duì)于屏幕的X方向位置。即相位置

通過(guò)遍歷BoxLayout中的各卡片相對(duì)位置計(jì)算進(jìn)度值progress

再通過(guò)調(diào)制方法Modulate,計(jì)算卡片的縮放,透明度,偏移量等值。

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);
        }
    }
}

靜態(tài)效果如下:

RenderTransform方法的形參scrollX為滾動(dòng)框架的滾動(dòng)偏移量,即MainScroller.ScrollX。

訂閱滾動(dòng)事件Scrolled,在事件方法中調(diào)用RenderTransform。代碼如下:

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

創(chuàng)建綁定數(shù)據(jù)

創(chuàng)建MainPageViewModel.cs,用于界面綁定數(shù)據(jù)源。

AppTombStone描述App進(jìn)入后臺(tái)時(shí)的狀態(tài)(墓碑機(jī)制)

public class AppTombStone
{
    public AppTombStone() { }
    public string AppName { get; set; }
    public string AppScreen { get; set; }
    public double TestOffset { get; set; }
}

在MainPageViewModel構(gòu)造函數(shù)中,初始化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);
    }

細(xì)節(jié)調(diào)整

首張卡片的處理

這里遇到個(gè)問(wèn)題,當(dāng)滾動(dòng)框架滾動(dòng)到最左側(cè)時(shí),最下方的卡片會(huì)被疊層上方的卡片覆蓋,如下圖所示:

當(dāng)滾動(dòng)框架滾動(dòng)到最左側(cè)時(shí),我們希望首張卡片不被上方的卡片覆蓋,那么它至少應(yīng)當(dāng)滾動(dòng)到屏幕的中部,因此需要加一個(gè)虛擬的BoxView將首張卡前的空間“撐起來(lái)”。

訂閱BoxView的BindingContextChanged事件,在事件方法中添加如下代碼

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

效果:

為卡片添加裁剪

使用Image.Clip和Image.Shadow屬性,為卡片添加圓角裁剪和陰影效果。

<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>

跳轉(zhuǎn)到最后一張卡片

App后臺(tái)任務(wù)是從右到左排列的,因此在App啟動(dòng)時(shí),需要將滾動(dòng)框架滾動(dòng)到最后一張卡片,代碼如下:

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);
    });
}

最終效果:

項(xiàng)目地址

Github:maui-samples

到此這篇關(guān)于[MAUI]模仿iOS多任務(wù)切換卡片滑動(dòng)的交互實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)ios多任務(wù)切換卡片滑動(dòng)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • ios之UIScrollerView滾動(dòng)視圖總結(jié)

    ios之UIScrollerView滾動(dòng)視圖總結(jié)

    本篇文章主要介紹了ios之UIScrollerView滾動(dòng)視圖總結(jié),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧
    2017-01-01
  • 最新ios面試試題以及解決思路分析

    最新ios面試試題以及解決思路分析

    本篇內(nèi)容給大家整理了關(guān)于IOS程序員面試的試題以及相關(guān)解決方法,需要的朋友們參考一下吧。
    2017-12-12
  • ios開發(fā)Flutter之?dāng)?shù)據(jù)存儲(chǔ)

    ios開發(fā)Flutter之?dāng)?shù)據(jù)存儲(chǔ)

    這篇文章主要為大家介紹了ios開發(fā)Flutter之?dāng)?shù)據(jù)存儲(chǔ)的示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-07-07
  • iOS代碼瘦身實(shí)踐之如何刪除無(wú)用的類

    iOS代碼瘦身實(shí)踐之如何刪除無(wú)用的類

    這篇文章主要給大家介紹了關(guān)于iOS代碼瘦身實(shí)踐之如何刪除無(wú)用的類,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家各位iOS開發(fā)者們具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2019-08-08
  • iOS使用CoreMotion實(shí)現(xiàn)搖一搖功能

    iOS使用CoreMotion實(shí)現(xiàn)搖一搖功能

    這篇文章主要為大家詳細(xì)介紹了iOS使用CoreMotion實(shí)現(xiàn)搖一搖功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2018-06-06
  • 零基礎(chǔ)學(xué)習(xí)iOS直播之播放

    零基礎(chǔ)學(xué)習(xí)iOS直播之播放

    對(duì)于直播來(lái)說(shuō),客戶端主要做兩件事情,推流和播放。本篇主要對(duì)播放進(jìn)行詳細(xì)介紹,需要的朋友一起來(lái)看下吧
    2016-12-12
  • iOS 11 UINavigationItem 去除左右間隙的方法

    iOS 11 UINavigationItem 去除左右間隙的方法

    本篇文章主要介紹了iOS 11 UINavigationItem 去除左右間隙的方法,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧
    2017-10-10
  • iOS繪制3D餅圖的實(shí)現(xiàn)方法

    iOS繪制3D餅圖的實(shí)現(xiàn)方法

    餅圖常用于統(tǒng)計(jì)學(xué)模塊。常見的一般為2D餅圖,這篇文章主要介紹了iOS繪制3D餅圖的實(shí)現(xiàn)方法,3D餅圖更加立體,用戶的好感度也比較高,下面需要的朋友可以參考借鑒,一起來(lái)看看吧。
    2017-01-01
  • 基于IOS端微信分享失效的踩坑及解決方法

    基于IOS端微信分享失效的踩坑及解決方法

    下面小編就為大家分享一篇基于IOS端微信分享失效的踩坑及解決方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧
    2018-01-01
  • IOS實(shí)現(xiàn)手動(dòng)截圖并保存

    IOS實(shí)現(xiàn)手動(dòng)截圖并保存

    這篇文章主要介紹了IOS實(shí)現(xiàn)手動(dòng)剪裁圖片并保存到相冊(cè),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2016-01-01

最新評(píng)論

赣州市| 五常市| 扎兰屯市| 台安县| 宁乡县| 泊头市| 温宿县| 汨罗市| 客服| 周口市| 云南省| 沽源县| 化州市| 六枝特区| 巢湖市| 永寿县| 富阳市| 湖北省| 礼泉县| 定边县| 宽城| 舒兰市| 通榆县| 土默特左旗| 嵊州市| 淮北市| 东安县| 兴国县| 金堂县| 兰溪市| 尉犁县| 栖霞市| 阳城县| 香格里拉县| 游戏| 鹤壁市| 梅河口市| 织金县| 闽侯县| 麻阳| 阳信县|