教程:产品双列

本教程将引导你逐步实现一个产品双列页面。本教程不假设你有任何 Lynx 的知识基础。你将在本教程中学到的技术将会是构建任何 Lynx 页面和应用程序的基础。

我们要构建什么?

首先,让我们看看结果!要查看应用的实际效果,请下载并安装 LynxExplorer 到你的设备,然后扫描下面生成的二维码。

这个应用能让你快速地在你的设备上查看你的图片,还有丰富的交互。

教程设置

请查看我们详细的安装文档,该文档将指导你创建一个新的 Lynx 项目。

你可能会注意到该项目使用的是 TypeScript。虽然 Lynx 和 ReactLynx 都支持 TypeScript 和普通 JavaScript,但我们推荐使用 TypeScript,以提供更好的开发体验,包括静态类型检查和更好的编辑器智能感知。

这个教程中将展示很多漂亮图片,我们为你准备了一些基准图片,你可以在这里下载资源包,并在搭建中使用它们。

添加样式

由于本教程的重点不在于如何为你的 UI 添加样式,你可以节省一些时间,直接复制下面的 index.css 文件:

index.scss
.gallery-wrapper {
  height: 100vh;
  background-color: black;
}

.single-card {
  display: flex;
  align-items: center;
  justify-content: center;
}

.scrollbar {
  position: absolute;
  right: 7px;
  z-index: 1000;
  width: 4px;
  background: linear-gradient(to bottom, #ff6448, #ccddff, #3deae7);
  border-radius: 5px;
  overflow: hidden;
  box-shadow:
    0px 0px 4px 1px rgba(12, 205, 223, 0.4),
    0px 0px 16px 5px rgba(12, 205, 223, 0.5);
}

.scrollbar-effect {
  width: 100%;
  height: 80%;
}

.glow {
  background-color: #333;
  border-radius: 4px;
  background: linear-gradient(
    45deg,
    rgba(255, 255, 255, 0) 20%,
    rgba(255, 255, 255, 0.8) 50%,
    rgba(255, 255, 255, 0) 80%
  );
  animation: flow 3s linear infinite;
}

@keyframes flow {
  0% {
    transform: translateY(-100%);
  }
  100% {
    transform: translateY(100%);
  }
}

.list {
  width: 100vw;
  padding-bottom: 20px;
  padding-left: 20px;
  padding-right: 20px;
  height: calc(100% - 48px);
  list-main-axis-gap: 10px;
  list-cross-axis-gap: 10px;
}

.picture-wrapper {
  border-radius: 10px;
  overflow: hidden;
  width: 100%;
}

.like-icon {
  position: absolute;
  display: grid;
  justify-items: center;
  align-items: center;
  top: 0px;
  right: 0px;
  width: 48px;
  height: 48px;
}

.heart-love {
  width: 16px;
  height: 16px;
}

.circle {
  position: absolute;
  top: calc(50% - 8px);
  left: calc(50% - 8px);
  height: 16px;
  width: 16px;
  border: 2px solid red;
  border-radius: 50%;
  transform: scale(0);
  opacity: 1;
  animation: ripple 1s 1 ease-out;
}

.circleAfter {
  animation-delay: 0.5s;
}

@keyframes ripple {
  0% {
    transform: scale(1);
    opacity: 1;
  }
  100% {
    transform: scale(2);
    opacity: 0;
  }
}

并将其作为全局样式导入:

import '../index.css';

这确保了在你按照本教程操作时,你的 UI 看起来很棒。

在 Lynx 中的样式变体

Lynx 支持多种样式功能,包括全局样式、CSS 模块、内联样式、Sass、CSS 变量等!请参阅 Rspeedy - 样式页面,了解如何选择最佳的样式配置。

你的第一个组件:图片卡

现在,让我们开始创建第一个图片卡片开始,它将是这个页面最主要的部分。

很好,你现在已经能看到图片卡被展示出来了,我们这里使用了 <image> 元件来展示你的图片。你只需要给它一个宽高(或者像这里一样指定 aspectRatio 属性),它就会自动调整大小以适应你指定的宽高。 这个组件能接收一个 picture 属性,来让使用它的你能够随意地更改想让它显示的图片。事实上,所有的组件都能够像这张接收来自外部的输入,让你能够控制它们。

图片的 src 属性

Lynx 的 <image> 元件可以接收一个本地的相对路径作为 src 属性来渲染图片,它是 <image> 元件中最重要的属性。这个应用中的所有图片都来源于本地,使用前需要导入这些路径。

但如果你的图片存储在网上,你也可以轻松地将其替换为网络图片地址,只需将 src 属性的值更改为对应的网络图片链接即可。。

添加交互:给图片卡点赞

我们可以在右上角我们添加一个小小的白色爱心,然后让它成为图片卡的点赞按钮。这里我们实现了一个小组件 LikeIcon

我们想让每一张卡片都能知道自己是否被点赞了,所以给这个组件添加了 isLiked,这是它的内部数据,它能用这样的内部数据来保存你对它的更改。

LikeIcon.tsx
...
  const [isLiked, setIsLiked] = useState(false);
...

然后我们给 <image> 添加了 bindtap 事件,这样当用户点击这个爱心时,它会触发这个事件,修改 isLiked 的状态:

LikeIcon.tsx
...
  const onTap = () => {
    setIsLiked(true);
  }
  return (
      ...
      <image bindtap={onTap}/>
  )
...
什么是 "bindtap"?

如果你来自 Web 开发背景,你可能更熟悉诸如 onclick(HTML 属性)或 onClick(在 React 社区中)的命名约定。Lynx 则遵循一套不同的约定:由于其架构的静态性质,使用 bind*catch*。在事件处理页面了解更多信息。

最后我们用 isLiked 来控制点赞的效果。因为 isLiked 是一个 stateLikeIcon 会响应它的变更,变成红色的点赞图标,用来渲染动画效果的 <view> 也会被条件渲染:

LikeIcon.tsx
...
  return
    ...
      {isLiked && <view className="circle" />}
      {isLiked && <view className="circle circleAfter" />}
      <image src={isLiked ? redHeart : whiteHeart} />
...

为了让这个点赞有更好的视觉互动效果,我们给它添加了动画,这些效果都在 index.scss 中,你也可以在动画中了解更多关于动画的内容。然后将它替换成你更喜欢的样式!

使用<list> 展示更多图片

为了展示你的所有漂亮图片,我们要使用 <list> 来帮助我们。这样你就能获得一个可以滚动的、能展示大量类似图片的页面了:

list 的特殊子元件

每一个 <list> 的子元件都需要是 <list-item>,而且要指定一个唯一且不重复的 keyitem-key 属性,否则它可能无法正常渲染。

当然,我们还提供其他滚动元件,比如 <scroll-view>,来实现类似的效果。我们在这里使用的是瀑布流排版来作为子节点的排版选项,<list> 还接受其他排版类型,可以参考 list

INFO

可以参考这篇管理滚动的文档来进一步了解滚动和滚动元件。

通过元件方法自动滚动

如果你想做一个桌面照片墙,你需要再为这个页面添加一个自动滚动的功能,你的图片会被慢慢自动滚动展示出来,让你轻松地看到更多图片:

我们使用 useEffect 钩子来调用触发 autoScroll 方法。

Gallery.tsx
useEffect(() => {
  listRef.current
    ?.invoke({
      method: 'autoScroll',
      params: {
        rate: '60',
        start: true,
      },
    })
    .exec();
}, []);
什么是 "invoke"?

在 Lynx 中,所有原生元件都有一组可以通过其 ref 调用的“方法”。与 Web 上不同,这种调用是异步的,类似于消息传递。您需要使用 invoke 和方法名称 method 以及参数 param 来调用它们。

来一个自定义滚动条如何?

像绝大多数页面那样,我们可以给这个页面添加一个滚动条,来提示我们还有多少图片未被展示。但我们可以做到更多!比如,我们可以把 <list> 的默认进度条换成我们喜欢的样式:

与用来添加点赞功能的 bindtap 事件类似,我们给 <list> 添加 bindscroll 事件,它会在 <list> 元件发生滚动时触发。

Gallery.tsx
...
const onScroll = (event: ScrollEvent) => {
  scrollbarRef.current?.adjustScrollbar(
    event.detail.scrollTop,
    event.detail.scrollHeight
  );
};
...
<list
  ref={galleryRef}
  className="list"
  list-type="waterfall"
  column-count={2}
  scroll-orientation="vertical"
  custom-list-name="list-container"
  bindscroll={onScroll}
>
...

NiceScrollbar 组件提供了一个内部方法 adjustScrollbar,每当 bindscroll 事件触发,我们调用这个方法来调整滚动条的位置。

INFO

我们在这个组件里使用了很多 react 的技巧,比如用于调用 adjustScrollbar 方法的 forwardRefuseImperativeHandle,如果你对它们不熟悉,可以查阅 React 官方文档,来更好地了解它们。

NiceScrollbar.tsx
...
const adjustScrollbar = (scrollTop: number, scrollHeight: number) => {
  const listHeight = lynx.__globalProps.screenHeight - 48;
  const scrollbarHeight = listHeight * (listHeight / scrollHeight);
  const scrollbarTop = listHeight * (scrollTop / scrollHeight);
  setScrollbarHeight(scrollbarHeight);
  setScrollbarTop(scrollbarTop);
};
...
__globalProps

我们在这个方法里使用了 globalProps,你可以使用 screenHeightscreenWidth 来获取屏幕高度和宽度。

list-item 的 estimated-main-axis-size-px

你可能注意到了这个属性 estimated-main-axis-size-px。这个属性能够在 <list> 中的元件尚未渲染时,估算出它们在主轴上的大小。这在我们添加滚动条时非常有用,因为我们需要知道滚动条需要多长才能覆盖所有的元件。

当然 <list> 也可以支持自动排版,你可以把这个属性去掉,看看效果如何——你的滚动条会自动调整长度,随着元件从预设高度变成真实高度而变化。

src/AddNiceScrollbar/Gallery.tsx
...
  <list>
    {pictureData.map((picture: Picture, index: number) => (
      <list-item
        estimated-main-axis-size-px={calculateEstimatedSize(
          picture.width,
          picture.height
        )}
        item-key={"" + index}
        key={"" + index}
      >
        <LikeImageCard picture={picture} />
      </list-item>
    ))}
  </list>
...

我们提供了一个工具方法,来根据当前的 <list> 的排版信息和图片的尺寸,来估算出图片在主轴上的大小:

src/utils.tsx
export const calculateEstimatedSize = (
  pictureWidth: number,
  pictureHeight: number,
) => {
  // Fixed styles of the gallery
  const galleryPadding = 20;
  const galleryMainAxisGap = 10;
  const gallerySpanCount = 2;
  const galleryWidth = lynx.__globalProps.screenWidth;
  // Calculate the width of each ImageCard and return the relative height of the it.
  const itemWidth =
    (galleryWidth - galleryPadding * 2 - galleryMainAxisGap) / gallerySpanCount;
  return (itemWidth / pictureWidth) * pictureHeight;
};

到这一步,我们已经有完整的页面了!但细心的你可能注意到,我们添加的的滚动条在滚动时还有一些滞后,不是那么地跟手,这是因为我们的调整目前还发生在后台线程,而不是响应触摸滚动的主线程。

什么是后台线程和主线程?

Lynx 的最大特点是双线程架构,你可以在这篇文档中找到更详细的介绍。

一个更跟手的滚动条

要优化滚动条的性能,我们需要引入主线程脚本进行主线程事件处理,将我们上一步对我们滚动条的高度和位置的调整从后台线程迁移到主线程来。

为了让你能更清楚地看到对比效果,我们让这两个滚动条同时存在:

现在你应该能看见,左侧由主线程脚步控制的滚动条会相比我们之前实现的右侧的滚动条更加顺滑跟手。如果你在开发其他界面时也遇到了需要让更新更快响应,不妨试试这个方法。

我们还为你提供了一个通过实现一个高性能轮播图来深入学习主线程脚本的案例,请参考教程:产品详情

收尾

我们将用于对比的多余进度条删掉,这样我们的整个界面就完成了!让我们来看看最后的效果:

恭喜!你已经创建了一个产品双列展示界面。在本教程中,你已经了解了在 Lynx 平台上编写交互式用户界面的基础知识以及与在 Web 上使用它的一些不同之处。

除非另有说明,本项目采用知识共享署名 4.0 国际许可协议进行许可,代码示例采用 Apache License 2.0 许可协议进行许可。