返回博客首页
← 所有文章

如何在 NativeScript 中构建一个简单的轮播图

2019 年 6 月 6 日 — 作者:Phani Sajja

构建一个很棒的应用程序不仅仅是构建应用程序的核心功能,一个很棒的应用程序必须提供很棒的用户体验。增强用户体验的第一步是让用户与应用程序无缝地社交。这就是轮播图发挥作用的地方。当我开始为我们的 Kinvey Microapps 容器构建轮播图时,我意识到这对于 NativeScript 来说是一个很棒的游乐场示例 - 这恰好是我贡献给 市场 🙌 的第一个示例。

在本文中,我将向您逐步介绍如何使用 NativeScript 和 Angular 框架构建一个简单的轮播图。

此处的动画 GIF(图 1)提供了我们将要构建的内容的预览

nativescript carousel

图 1:轮播图 / 幻灯片

在此实现中,我们有三个不同的(子)视图或幻灯片。我们使用滑动手势对其中两个进行动画处理。手势和动画将在不同的部分中介绍。首先,让我们查看应用程序的结构并构建视图。

应用程序结构

图 2 部分显示了应用程序的结构

app structure

图 2:应用程序文件夹结构

由于我们使用的是 Angular 框架,因此以下路由在我们的示例应用程序中定义。

import { NgModule } from "@angular/core";
import { Routes } from "@angular/router";

import { NativeScriptRouterModule } from "nativescript-angular/router";

const routes: Routes = [
    { path: "", redirectTo: "/welcome", pathMatch: "full" },
    { path: "welcome", loadChildren: "./pages/welcome/welcome.module#WelcomeModule" },
    { path: "home", loadChildren: "./pages/home/home.module#HomeModule" }
];

@NgModule({
    imports: [NativeScriptRouterModule.forRoot(routes)],
    exports: [NativeScriptRouterModule]
})
export class AppRoutingModule { }

如您所见,/welcome 是我们的默认路由。在本文的其余部分中,我们将重点关注为该路由构建功能。

构建用户界面

对于此示例,我们有三个简单的视图:幻灯片 1、幻灯片 2 和幻灯片 3。

幻灯片 1 (app/pages/welcome/slides/slide1.xml)

<GridLayout row="0" rows="*, 2*, *">
    <GridLayout width="57%" row="0" horizontalAlignment="center" verticalAlignment="center">
        <Label class="lobster-regular carousel-item-head" text="Welcome to Payments App" textWrap="true"></Label>
    </GridLayout>
    <GridLayout class="carousel-item-circle" row="1" horizontalAlignment="center" verticalAlignment="center">
        <Label class="fa carousel-item-icon" text="&#xf19c;" textWrap="true"></Label>
    </GridLayout>
    <GridLayout width="49%" row="2" horizontalAlignment="center" verticalAlignment="center">
        <Label class="opensans-regular carousel-item-desc" text="Let's see a quick overview of our features." textWrap="true"></Label>
    </GridLayout>
</GridLayout>

幻灯片 2 (app/pages/welcome/slides/slide2.xml)

<GridLayout row="0" rows="*, 2*, *">
    <GridLayout width="56%" row="0" horizontalAlignment="center" verticalAlignment="center">
        <Label class="lobster-regular carousel-item-head" text="Simple & Fast" textWrap="true"></Label>
    </GridLayout>
    <GridLayout class="carousel-item-circle" row="1" horizontalAlignment="center" verticalAlignment="center">
        <Label class="fa carousel-item-icon" text="&#xf1d8;" textWrap="true"></Label>
    </GridLayout>
    <GridLayout width="49%" row="2" horizontalAlignment="center" verticalAlignment="center">
        <Label class="opensans-regular carousel-item-desc" text="It is easy to use and makes transactions quick even while you are on the move." textWrap="true"></Label>
    </GridLayout>
</GridLayout>

幻灯片 3 (app/pages/welcome/slides/slide3.xml)

<GridLayout row="0" rows="*, 2*, *">
    <GridLayout width="56%" row="0" horizontalAlignment="center" verticalAlignment="center">
        <Label class="lobster-regular carousel-item-head" text="Safe & Secure" textWrap="true"></Label>
    </GridLayout>
    <GridLayout class="carousel-item-circle" row="1" horizontalAlignment="center" verticalAlignment="center">
        <Label class="fa carousel-item-icon" text="&#xf132;" textWrap="true"></Label>
    </GridLayout>
    <GridLayout width="49%" row="2" horizontalAlignment="center" verticalAlignment="center">
        <Label class="opensans-regular carousel-item-desc" text="Every transaction made through Payments App is secure." textWrap="true"></Label>
    </GridLayout>
</GridLayout>

您可能已经注意到,视图的代码片段在结构上是相似的 - 顶部有一个标题,中间有一个在圆圈中渲染的 Font Awesome 图标,底部有一个摘要。您可以根据需要定义更简单或更复杂的视图。

以上每个视图都将嵌入到父视图中。以下是我们的欢迎页面,当我们导航到 /welcome 路由时,它将在屏幕上渲染。

欢迎页面 (app/pages/welcome/welcome.component.html)

<GridLayout (swipe)="onSwipe($event)">
    <GridLayout rows="3*, *">
        <GridLayout row="0" rows="*" class="m-t-20">
            <ContentView #slideContent row="0" id="slide-content">
                <GridLayout row="0" rows="*">
                    <Label class="opensans-semi-bold carousel-loading" text="Loading..." textWrap="true"></Label>
                </GridLayout>
            </ContentView>
        </GridLayout>
        <GridLayout row="2" rows="auto, auto">
            <GridLayout row="0" rows="*" columns="*, auto, *" id="carousel-slider" class="m-b-20">
                <StackLayout [ngClass]="getSliderItemClass(0)" verticalAlignment="top" col="0" horizontalAlignment="right"></StackLayout>
                <StackLayout [ngClass]="getSliderItemClass(1)" style="margin: 0 5" verticalAlignment="top" col="1" horizontalAlignment="center"></StackLayout>
                <StackLayout [ngClass]="getSliderItemClass(2)" verticalAlignment="top" col="2" horizontalAlignment="left"></StackLayout>
            </GridLayout>
            <GridLayout row="1" rows="auto" verticalAlignment="bottom" class="m-t-10">
                <Button text="Get Started" class="opensans-semi-bold skip-intro" (tap)="skipIntro()"></Button>
            </GridLayout>
        </GridLayout>
    </GridLayout>
</GridLayout>

在第 4 行,我们将 ContentView 上的引用变量声明为 #slideContent。我们将在我们的 Angular 组件中进一步使用此引用来动态附加其中一个视图,如下所示

app/pages/welcome/welcome.component.ts

@ViewChild('slideContent') slideElement: ElementRef;

我们在单独的 XML 文件中定义了我们的幻灯片,因此我们需要初始化这些视图。NativeScript 提供了一种通过 UI Builder 动态加载视图的好方法。在继续添加手势和动画之前,让我们看看如何在组件初始化时加载和初始化视图。

欢迎页面 (app/pages/welcome/welcome.component.ts)

ngOnInit() {
    // Other statements

    this.slideView = this.slideElement.nativeElement;
    this.loadSlides(this.slideFiles, this.slidesPath).then((slides: any) => {
      var row = new ItemSpec(1, GridUnitType.STAR);
      let gridLayout = new GridLayout();
      slides.forEach((element, i) => {
        GridLayout.setColumn(element, 0);
        if (i > 0)
          element.opacity = 0
        gridLayout.addChild(element);
      });
      gridLayout.addRow(row);

      this.slideView.content = (this.slidesView = gridLayout);
    });
}

private loadSlides(slideFiles, slidesPath) {
    return new Promise(function (resolve, reject) {
      const slides = []
      const currentAppFolder = fs.knownFolders.currentApp();
      const path = fs.path.normalize(currentAppFolder.path + "/" + slidesPath);
      slideFiles.forEach((dataFile, i) => {
        const slidePath = path + "/" + dataFile;
        slides.push(builder.load(slidePath))
      });

      resolve(slides);
    });
}

加载视图后,我们将第一个视图附加到父视图,我们之前将其声明为引用变量。

图 4 显示了应用程序在模拟器或设备上启动时加载的初始屏幕。当附加其他视图时,图 4 和图 5 将添加到应用程序中。

app screens

是时候进行动画了

在本节中,我们将添加滑动手势,然后对视图进行动画处理,使我们的实现与 GIF(图 1)中的动画相匹配。

对于移动应用程序中的轮播图,最合适的手势是滑动和拖动。让我们将滑动手势添加到 UI。

请记住,我们在欢迎页面的实现中已对滑动手势进行了编程。为了便于参考,在此再次复制它。

app/pages/welcome/welcome.component.html

<GridLayout (swipe)="onSwipe($event)">

当用户在应用程序上滑动时,将调用以下子例程。

欢迎页面 (app/pages/welcome/welcome.component.ts)

onSwipe(args: SwipeGestureEventData) {
    let prevSlideNum = this.currentSlideNum;
    let count = this.slideCount;
    if (args.direction == 2) {
      this.currentSlideNum = (this.currentSlideNum + 1) % count;
    } else if (args.direction == 1) {
      this.currentSlideNum = (this.currentSlideNum - 1 + count) % count;
    } else {
      // We are interested in left and right directions
      return;
    }

    const currSlide = this.slidesView.getChildAt(prevSlideNum);
    const nextSlide = this.slidesView.getChildAt(this.currentSlideNum);

    this.animate(currSlide, nextSlide, args.direction);
}

在我们的示例中,我们只使用左右滑动。对于每次滑动,我们根据用户是向左滑动还是向右滑动来识别要进行动画处理的两个幻灯片。这两个幻灯片之一是当前幻灯片,它由以下语句识别

let prevSlideNum = this.currentSlideNum;

下一个幻灯片将根据滑动的方向进行识别,并且是动画结束时将附加到视图的幻灯片。如果方向是从右到左,那么我们可以计算幻灯片编号,如以下语句所示

this.currentSlideNum = (this.currentSlideNum + 1) % count;

如果方向是从左到右,那么我们可以计算幻灯片编号,如以下语句所示

this.currentSlideNum = (this.currentSlideNum - 1 + count) % count;

请注意,识别下一个幻灯片的逻辑基于实现循环队列的逻辑。

现在我们已经确定了要进行动画处理的两个幻灯片,我们可以确定要进行动画处理的两个视图。

注意:这些视图已附加到欢迎页面。我们只是在加载视图时通过将其不透明度设置为 0 来隐藏它们(请再次查看初始化部分)。

欢迎页面 (app/pages/welcome/welcome.component.ts)

const currSlide = this.slidesView.getChildAt(prevSlideNum);
const nextSlide = this.slidesView.getChildAt(this.currentSlideNum);

最后,我们需要对这两个幻灯片进行动画处理。NativeScript 提供了一种方法可以同时对多个视图进行动画处理。以下子例程包含对两个视图进行动画处理的逻辑

欢迎页面 (app/pages/welcome/welcome.component.ts)

animate(currSlide, nextSlide, direction) {
    nextSlide.translateX = (direction == 2 ? this.screenWidth : -this.screenWidth);
    nextSlide.opacity = 1;
    var definitions = new Array<AnimationDefinition>();

    definitions.push({
      target: currSlide,
      translate: { x: (direction == 2 ? -this.screenWidth : this.screenWidth), y: 0 },
      duration: 500
    });

    definitions.push({
      target: nextSlide,
      translate: { x: 0, y: 0 },
      duration: 500
    });

    var animationSet = new Animation(definitions);

    animationSet.play().then(() => {
      // console.log("Animation finished");
    })
      .catch((e) => {
        console.log(e.message);
      });
}

如我们所知,如果我们将平移动画应用于视图,它将移动到一个新的位置。为了实现滑动效果,我们同时对上面识别的两个视图进行动画处理。首先,我们将当前显示在屏幕上的视图平移到屏幕外部的一个新位置。然后,我们将屏幕外部的视图平移到可见屏幕上。

以下是包含加载幻灯片、向幻灯片添加手势以及对幻灯片进行动画处理的逻辑的代码隐藏文件。

app/pages/welcome/welcome.component.ts

import { Component, OnInit, ViewChild, ElementRef } from "@angular/core";
import { RouterExtensions } from "nativescript-angular/router";

import { Page, ContentView } from "ui/page";
import { SwipeGestureEventData } from "ui/gestures/gestures";
import { GridLayout, GridUnitType, ItemSpec } from "ui/layouts/grid-layout";
import { AnimationDefinition, Animation } from 'ui/animation';
import { screen } from "platform";

import * as fs from "file-system";
import * as builder from "ui/builder";

@Component({
  selector: "welcome",
  moduleId: module.id,
  templateUrl: "./welcome.component.html"
})
export class WelcomeComponent implements OnInit {
  private slidesPath = 'pages/welcome/slides';
  private slideFiles = ['slide1.xml', 'slide2.xml', 'slide3.xml'];

  private currentSlideNum: number = 0;
  private slideCount = 3;

  private screenWidth;

  private slidesView: GridLayout;

  @ViewChild('slideContent') slideElement: ElementRef;
  private slideView: ContentView;

  constructor(
    private page: Page,
    private nav: RouterExtensions,
  ) {
    this.screenWidth = screen.mainScreen.widthDIPs;
  }

  ngOnInit() {
    this.page.actionBarHidden = true;
    this.page.cssClasses.add("welcome-page-background");
    this.page.backgroundSpanUnderStatusBar = true;

    this.slideView = this.slideElement.nativeElement;

    this.loadSlides(this.slideFiles, this.slidesPath).then((slides: any) => {
      var row = new ItemSpec(1, GridUnitType.STAR);
      let gridLayout = new GridLayout();
      slides.forEach((element, i) => {
        GridLayout.setColumn(element, 0);
        if (i > 0)
          element.opacity = 0
        gridLayout.addChild(element);
      });
      gridLayout.addRow(row);

      this.slideView.content = (this.slidesView = gridLayout);
    });
  }

  private loadSlides(slideFiles, slidesPath) {
    return new Promise(function (resolve, reject) {
      const slides = []
      const currentAppFolder = fs.knownFolders.currentApp();
      const path = fs.path.normalize(currentAppFolder.path + "/" + slidesPath);
      slideFiles.forEach((dataFile, i) => {
        const slidePath = path + "/" + dataFile;
        slides.push(builder.load(slidePath))
      });

      resolve(slides);
    });
  }

  onSwipe(args: SwipeGestureEventData) {
    let prevSlideNum = this.currentSlideNum;
    let count = this.slideCount;
    if (args.direction == 2) {
      this.currentSlideNum = (this.currentSlideNum + 1) % count;
    } else if (args.direction == 1) {
      this.currentSlideNum = (this.currentSlideNum - 1 + count) % count;
    } else {
      // We are interested in left and right directions
      return;
    }

    const currSlide = this.slidesView.getChildAt(prevSlideNum);
    const nextSlide = this.slidesView.getChildAt(this.currentSlideNum);

    this.animate(currSlide, nextSlide, args.direction);
  }

  animate(currSlide, nextSlide, direction) {
    nextSlide.translateX = (direction == 2 ? this.screenWidth : -this.screenWidth);
    nextSlide.opacity = 1;
    var definitions = new Array<AnimationDefinition>();

    definitions.push({
      target: currSlide,
      translate: { x: (direction == 2 ? -this.screenWidth : this.screenWidth), y: 0 },
      duration: 500
    });

    definitions.push({
      target: nextSlide,
      translate: { x: 0, y: 0 },
      duration: 500
    });

    var animationSet = new Animation(definitions);

    animationSet.play().then(() => {
      // console.log("Animation finished");
    })
      .catch((e) => {
        console.log(e.message);
      });
  }

  itemSelected(item: number) {
    console.log(item)
  }

  skipIntro() {
    // this.nav.navigate(["/home"], { clearHistory: true });
    this.nav.navigate(["/home"]);
  }

  getSliderItemClass(item: number) {
    if (item == this.currentSlideNum)
      return "caro-item-dot caro-item-dot-selected";

    return "caro-item-dot";
  }
}

轮播图示例的完整代码可以在 此 git 仓库 中找到。轮播图的示例代码可作为 NativeSript 游乐场示例应用程序 获得。

良好的 UI/UX 设计是吸引人用户界面的基础。感谢 Phani Kishore Gudi 设计了轮播图。

总结

我们已经了解了如何在 NativeScript 中实现轮播图。在此示例中,我们利用了 NativeScript 的一些核心功能,包括动态加载视图、向视图添加手势,最后对视图进行动画处理。为了简单起见,我们省略了应用程序的引导和 CSS。

请在下面的评论中分享您对改进本文的意见。