更新记录

1.4.1(2026-09-14) 下载此版本

  • 与各平台统一版本,同步更新 Android 内核;UniApp 接口及平台兼容范围不变。

1.4.0(2026-09-10) 下载此版本

  • 新增长按事件与内置操作抽屉,支持显式列表/网格布局、可选列表图标、自定义按钮、分组、禁用和危险操作样式。
  • 新增媒体加载成功、失败与会话打开事件,支持失败提示与 retryLevixel() 重试。
  • 抽屉关闭后再派发业务回调;连续打开或更新列表时,回调保留对应会话的媒体身份。
  • 补齐取消打开时的回调结束与替换会话的关闭事件。
  • 关闭结果在查看器退场完成后返回;关闭或再次打开会取消仍在测量来源、解析路径的旧打开请求。

1.3.0(2026-09-07) 下载此版本

  • 修复 Android 列表滚动到底部时,打开或关闭查看器导致列表跳动、回场位置偏移的问题;查看器使用独立全屏窗口,不再改变宿主页面布局。
  • 修复 App 原生插件版 iOS 因桥接容器与可见页面视口不一致而缺失共享转场的问题。
  • UniApp 回场改为依据源矩形与有效页面视口的真实交集:部分可见时保留共享转场,完全位于视口外时安全淡出。
  • 修复 UniApp iOS 合成源锚点因透明父视图被注册表拒绝、导致关闭时退化为淡出的问题,恢复按当前媒体位置执行的共享回场。
  • warmupLevixelItem 仅记录已解码源图尺寸,不再隐式下载或落盘;点击等待采用短预算,受管预览同时限制单文件大小、总字节数、条目数和空闲时间。
  • 修复 Android 长视频拖动进度条后重复触发画面交接、导致封面瞬间闪现的问题;页面重新激活及取消返回手势也不会重复播放封面交接动画。
  • 同一次打开请求中的媒体 id 必须唯一,Android/iOS 原生运行时与公共 JavaScript 选择器入口会一致拒绝重复值。
  • iOS 共享转场改用稳定媒体 id 匹配回场源,列表插入、删除或重排后不再依赖旧下标。
  • 新增 initialItemId、稀疏 sourceBindings 与显式组件查询上下文,支持前插、后追加、重排、分页和虚拟列表按稳定媒体 ID 绑定当前已挂载源;未挂载项只取消自身源转场,不再拖累整组。
  • 打开结果、下标变化和源图显隐事件同时返回稳定 itemId,宿主列表更新后不会再误用新数组解释旧会话下标。
查看更多

平台兼容性

uni-app(5.24)

Vue2 Vue2插件版本 Vue3 Vue3插件版本 Chrome Safari app-vue app-vue插件版本 app-nvue Android Android插件版本 iOS iOS插件版本 鸿蒙
√ 1.1.1 √ 1.1.1 × × √ 1.1.1 × 6.0 1.1.1 15 1.1.1 ×
微信小程序 支付宝小程序 抖音小程序 百度小程序 快手小程序 京东小程序 鸿蒙元服务 QQ小程序 飞书小程序 小红书小程序 快应用-华为 快应用-联盟
× × × × × × × × × - × ×

uni-app x(5.24)

Chrome Safari Android Android插件版本 iOS iOS插件版本 鸿蒙 微信小程序
× × 6.0 1.2.0 15 1.2.0 × ×

其他

多语言 暗黑模式 宽屏模式 蒸汽模式
× × × √

Levixel 共享转场图片视频查看器

Levixel 以列表中源媒体当前可见的位置、尺寸和圆角为转场起点,将同一内容连续展开到原生全屏查看器;关闭时再沿对应路径回到源位置。缩略图、加载状态与原始媒体在同一视觉链路中连续交接,让用户始终感知为同一份媒体在列表与全屏之间展开和归位。

交互取向参考 Google Photos 与 iPhone 系统“照片”App 中以媒体为中心的直接操控方式。上述产品仅作为交互参考;Levixel 与其不存在隶属或授权关系,也未使用上述产品的代码。本项目自身的开源衍生来源见随包提供的 THIRD_PARTY_NOTICES.md。

核心能力

  • 图片与视频混合分页浏览
  • 以可见源为锚点的开场与回场共享转场
  • 双指缩放、缩放后平移与双击复位
  • 图片未放大时竖拖关闭,并支持点按关闭和系统返回
  • 缩略图未就绪时直接进入原生加载状态,加载完成后连续交接
  • 图片与视频长按事件、可配置的列表或网格操作抽屉
  • 会话与媒体加载事件、加载失败提示和重试
  • 经典 uni-app 与 uni-app x Vapor 使用同一套公共 JavaScript API

兼容范围

安装本插件请使用 HBuilderX 5.24 或更高版本。正式支持范围如下:

宿主 页面类型 Android iOS
经典 uni-app Vue 2 / Vue 3 App Vue API 21+ iOS 13.0+,arm64 真机
uni-app x 仅 Vapor API 23+ iOS 15+,arm64 真机

市场兼容表要求经典 uni-app 与 uni-app x 使用共同的最低系统声明,因此统一填写 Android 6 / API 23 与 iOS 15;上表分别列出各宿主的实际支持范围。

uni-app x 不支持 VDOM。本 UniApp 交付也不覆盖 nvue、Web、小程序或 HarmonyOS;Web 与 HarmonyOS 可使用 Levixel 的对应平台包。

安装与引入

新项目推荐直接从 DCloud 插件市场导入。也可以从 GitHub Releases 下载匹配版本的 levixel-uniapp-<version>.zip,将 ZIP 根目录内容放入项目的 uni_modules/Sandrox-Levixel/。

业务代码只需要引入高层 SDK:

import {
  openLevixel,
  closeLevixel,
  retryLevixel,
  onLevixelEvent,
  prepareLevixelItem,
  warmupLevixelItem,
  openLevixelFromSelector,
} from '@/uni_modules/Sandrox-Levixel/js_sdk/index.js'

utssdk 下的接口属于插件内部实现,业务代码不应直接调用。

媒体数据

const items = [
  {
    id: 'photo-1',
    type: 'image',
    url: 'https://example.com/photo.jpg',
    thumbnailUrl: 'https://example.com/photo-thumb.jpg',
    width: 1600,
    height: 1200,
    alt: '海岸',
  },
  {
    id: 'video-1',
    type: 'video',
    url: 'https://example.com/video.mp4',
    posterUrl: 'https://example.com/video-poster.jpg',
    width: 1920,
    height: 1080,
  },
]
  • id:媒体稳定标识,必须非空且在同一次打开请求中唯一。
  • type:image 或 video。
  • url:原图或视频地址,必填。
  • thumbnailUrl:图片缩略图,可选;图片未提供时使用 url。
  • posterUrl:视频封面;视频需要共享转场时应提供,也可用 thumbnailUrl。
  • width、height:媒体原始尺寸,可选但建议提供。
  • alt:媒体说明,可选。

接入原则

  1. 动态、分页和虚拟列表使用 initialItemId 与 sourceBindings,按稳定媒体 id 描述当前已挂载源;绑定数量和顺序不需要等于 items。
  2. prepareLevixelItem 用于显式准备稳定的本地转场预览;warmupLevixelItem 只记录已经解码的源图尺寸。两者都不是打开前置条件。
  3. 即使缩略图尚未显示,点击后也应调用 openLevixelFromSelector;查看器会进入无共享源转场的原生加载状态,不要在业务层因 ready 状态而拦截。
  4. 大列表准备预览时应限制为 2–3 个并发任务,避免集中占用 JS 与网络资源。
  5. 原图和视频由原生查看器在打开时按需加载;列表只需渲染缩略图或视频封面。

经典 uni-app 示例

<template>
  <view class="gallery">
    <image
      v-for="item in items"
      :key="item.id"
      :id="`levixel-source-${item.id}`"
      class="levixel-source"
      :src="sourceFor(item)"
      mode="aspectFill"
      @load="handleLoad(item, $event)"
      @click="openViewer(item)"
    />
  </view>
</template>

<script>
import {
  openLevixelFromSelector,
  prepareLevixelItem,
  warmupLevixelItem,
} from '@/uni_modules/Sandrox-Levixel/js_sdk/index.js'

export default {
  data() {
    return {
      items: [
        {
          id: 'photo-1',
          type: 'image',
          url: 'https://example.com/photo.jpg',
          thumbnailUrl: 'https://example.com/photo-thumb.jpg',
        },
      ],
      previewSources: {},
    }
  },
  onReady() {
    this.items.slice(0, 3).forEach(item => this.preparePreview(item))
  },
  methods: {
    sourceFor(item) {
      return this.previewSources[item.id]
        || item.thumbnailUrl
        || item.posterUrl
        || item.url
    },
    async preparePreview(item) {
      const prepared = await prepareLevixelItem(item)
      if (prepared) {
        this.previewSources = {
          ...this.previewSources,
          [item.id]: prepared.src,
        }
      }
    },
    handleLoad(item, event) {
      warmupLevixelItem(item, event).catch(() => {})
    },
    async openViewer(selectedItem) {
      const snapshot = this.items.slice()
      await openLevixelFromSelector({
        items: snapshot,
        initialItemId: selectedItem.id,
        theme: 'dark',
        sourceBindings: snapshot.map(item => ({
          itemId: item.id,
          selector: `#levixel-source-${item.id}`,
          objectFit: 'cover',
          cornerRadius: uni.upx2px(12),
        })),
      })
    },
  },
}
</script>

<style>
.levixel-source {
  width: 200rpx;
  height: 200rpx;
  border-radius: 12rpx;
}
</style>

uni-app x Vapor 示例

Vapor 页面使用 Composition API / script setup。公共 SDK 与经典 uni-app 相同,不需要在业务层复制协议或实现平台分支。

<template>
  <view class="gallery">
    <image
      v-for="item in items"
      :key="item.id"
      :id="`levixel-source-${item.id}`"
      class="levixel-source"
      :src="previewSources.get(item.id) || item.thumbnailUrl || item.posterUrl || item.url"
      mode="aspectFill"
      @load="handleLoad(item, $event)"
      @click="openViewer(item)"
    />
  </view>
</template>

<script setup lang="uts">
import { ref } from 'vue'
import {
  openLevixelFromSelector,
  prepareLevixelItem,
  warmupLevixelItem,
} from '@/uni_modules/Sandrox-Levixel/js_sdk/index.js'

type DemoMediaItem = {
  id: string
  type: 'image' | 'video'
  url: string
  thumbnailUrl?: string
  posterUrl?: string
}

const items = ref<DemoMediaItem[]>([
  {
    id: 'photo-1',
    type: 'image',
    url: 'https://example.com/photo.jpg',
    thumbnailUrl: 'https://example.com/photo-thumb.jpg',
  },
])
const previewSources = ref<Map<string, string>>(new Map<string, string>())

async function preparePreview(item: DemoMediaItem) {
  const prepared = await prepareLevixelItem(item)
  if (prepared != null)
    previewSources.value.set(item.id, prepared.src)
}

function handleLoad(item: DemoMediaItem, event: UniImageLoadEvent) {
  warmupLevixelItem(item, event).catch(() => {})
}

async function openViewer(selectedItem: DemoMediaItem) {
  const snapshot = items.value.slice()
  await openLevixelFromSelector({
    items: snapshot,
    initialItemId: selectedItem.id,
    theme: 'dark',
    sourceBindings: snapshot.map(item => ({
      itemId: item.id,
      selector: `#levixel-source-${item.id}`,
      objectFit: 'cover',
      cornerRadius: 6,
    })),
  })
}

items.value.slice(0, 3).forEach(item => preparePreview(item))
</script>

<style>
.levixel-source {
  width: 100px;
  height: 100px;
  border-radius: 6px;
}
</style>

在受支持的 App Vapor 页面中,border-radius 应使用 px,并与对应绑定项的 cornerRadius 数值保持一致。上例可见源为 6px,因此传给 Levixel 的值也是 6;这样开关场期间源图与原生转场快照会使用相同轮廓。

动态、分页与虚拟列表

调用时的 items 是该次全屏查看会话的媒体快照。聊天记录向前插入、瀑布流向后追加或列表重排后,下一次打开传入最新已加载数组即可;Levixel 不会在已经打开的查看器内部代替业务请求下一页。

sourceBindings 应包含当前实际挂载的源,不要按点击下标截取固定数量来猜测可视范围。例如已加载 100 项而虚拟列表只挂载 8 个 cell,就传这 8 项的 { itemId, selector };普通非虚拟列表可传当前已加载并挂载的全部源。SDK 会按 itemId 将它们放回正确位置,原生运行时再按源矩形与有效页面视口是否存在正面积交集决定回场:哪怕只露出一小部分也使用共享转场,完全位于视口外或已经卸载才安全淡出。绑定顺序可与 items 不同,但每个 itemId 必须存在于本次 items,每个选择器在自身查询范围内必须唯一且最多命中一个元素。

const snapshot = loadedItems.slice()
const mountedSnapshot = mountedItems.slice()

await openLevixelFromSelector({
  items: snapshot,
  initialItemId: clickedItem.id,
  sourceBindings: mountedSnapshot.map(item => ({
    itemId: item.id,
    selector: `#levixel-source-${item.id}`,
    objectFit: 'cover',
    cornerRadius: 6,
  })),
})

示例媒体 ID 可直接用于 CSS 选择器。若业务 ID 可能包含空格、引号等 CSS 特殊字符,应另外为源元素分配无碰撞的唯一 DOM token,并把对应选择器写入绑定;不要通过会产生碰撞的字符替换来“清洗”媒体 ID。

选择器默认查询当前页面。若整个画廊位于一个自定义组件内,可在顶层传 queryContext: componentInstance;若虚拟列表的每个源分别位于不同 cell 组件内,则在各自 binding 中传对应的 queryContext。不同组件范围可以复用相同的局部选择器:

sourceBindings: mountedCells.map(cell => ({
  itemId: cell.item.id,
  selector: '.levixel-source',
  queryContext: cell.componentInstance,
  objectFit: 'cover',
  cornerRadius: 6,
}))

queryContext 应传 Vue 组件公开实例;选项式 API 可使用当前组件的 this,组合式 API / uni-app x 可使用 getCurrentInstance()!.proxy!。错误的上下文会直接报错,不会退回页面范围误测其他同名元素。

固定且完整渲染的简单画廊也可以使用 index + sourceSelector + sourceStyles;位于自定义组件时同样可在顶层提供 queryContext。该模式要求选择器结果数量和顺序与 items 完全一致。index 与 initialItemId 是互斥的初始媒体定位方式,sourceSelector/sourceStyles 与 sourceBindings 是互斥的源映射方式;这两组选择彼此独立,组内冲突会直接报错。动态列表仍推荐使用 initialItemId + sourceBindings,避免宿主数组变化后继续解释旧下标。

加载与源图交接

prepareLevixelItem 会发起明确的预览准备任务,并将成功结果保存为 Levixel 自有的稳定本地文件。只应为当前可见或即将进入视口的少量媒体预取;大列表不要一次准备全部项目。

warmupLevixelItem 在图片完成解码后记录可用尺寸,不会再次下载或保存媒体。点击打开只会短暂等待已经在途的选中项准备任务;未及时就绪时立即交给原生查看器显示加载状态,不会为了共享转场长时间阻塞用户操作。

受管预览同时受单文件大小、缓存总字节数、条目数和空闲时间约束;淘汰项会立即删除,上次运行意外遗留的文件会在下次初始化时清理。准备失败或缩略图尚未加载时,查看器仍会按媒体 URL 正常打开。

sourceVisibility 默认且建议保持 visible。经典 uni-app 与 x Vapor 均使用该源图交接策略,避免关闭转场最后阶段出现源图纹理闪烁。只有页面完整处理 sourceVisibilityChange,并确认所有目标平台的开关场交接都符合预期时,才应显式传入 hidden;否则源位置可能在关闭末帧短暂留空或闪烁。

长按与操作抽屉

openLevixel 和 openLevixelFromSelector 均接受 actions、actionLayout 和 actionListIcons。经典 uni-app 与 uni-app x 使用相同配置:

await openLevixelFromSelector({
  items: snapshot,
  initialItemId: selectedItem.id,
  sourceBindings: mountedItems.map(item => ({
    itemId: item.id,
    selector: `#levixel-source-${item.id}`,
    objectFit: 'cover',
    cornerRadius: 6,
  })),
  actionLayout: 'list',
  actions: [
    { id: 'inspect', label: '查看详情', group: 'tools',
      onPress: context => showDetails(context.itemId) },
  ],
})

showDetails 是宿主自己的业务方法。插件负责把抽屉呈现在查看器上方;保存、分享、导航及相关权限申请由接入方实现。

按钮字段 含义
id 本次配置内唯一的非空白字符串
label 非空白文字,最多显示两行
icon 图片 URI;列表可省略,网格每项必传
group 可选的非空白分组;省略时归入默认组
disabled 默认 false;禁用项不触发选择或业务回调
destructive 默认 false;危险操作样式,业务确认仍由宿主处理
onPress 可选回调,接收媒体上下文及 actionId

布局由配置决定,与按钮数量无关:

  • actionLayout 默认 'list';一个或多个按钮都可以使用列表。列表图标默认隐藏,即使传入也不会加载或显示;显式设置 actionListIcons: true 后显示已有图标,缺失项保留对齐空位。
  • actionLayout: 'grid' 始终显示图标,每个按钮都必须提供 icon,否则打开前报错。单个按钮也可以用网格。图标加载失败时显示中性占位,保留文字和点击能力。
  • 组与组内按钮按首次出现顺序排列。列表按组分隔,网格的每个组对应一条横向滚动行;内容超高时纵向滚动,取消按钮保持可见。

抽屉使用独立浅色配色,跟随系统字号与系统呈现方式;theme 控制媒体背景。图片和视频均可长按,视频控制按钮与失败重试按钮不触发长按;移动或多指操作会取消识别。不传 actions 或传空数组时只派发 longPress,不显示抽屉。

选中按钮后先关闭抽屉,再通知 action 事件与 onPress,查看器继续保持打开。两处都会收到通知,同一业务操作只在一处执行。取消、点遮罩与系统返回优先关闭抽屉;closeLevixel() 关闭整个查看器。需要展示宿主业务弹窗时,可在按钮回调中先 await closeLevixel(),再打开业务界面。

事件与直接控制

const removeListener = onLevixelEvent(event => {
  console.log(event.type, event.payload)
})

await openLevixel({ items, index: 0 })
// 可由宿主自己的重试按钮调用:
const { retried } = await retryLevixel()
await closeLevixel()
// 页面不再需要订阅时调用:
removeListener()

事件结构为 { type, payload, time },time 是 Unix 毫秒时间戳。媒体上下文包含 sessionId、galleryId、index、itemId 和 mediaType(image 或 video)。

事件 时机及附加字段
ready 事件通道就绪,不代表查看器已经打开
opened 打开转场结束,查看器可交互
longPress 长按识别成功,在自动打开抽屉之前
indexChange 当前页变化,同时保留 currentIndex
mediaLoad 原图解码完成或视频首帧就绪
mediaError 媒体加载失败,附加 code: LOAD_FAILED 与 message
action 选择按钮且抽屉收起后,附加 actionId
dismiss 会话结束,包含最后当前媒体的上下文
sourceVisibilityChange 源图显隐变化,包含 hidden、galleryId、index 和 itemId

原生首次 indexChange 可能早于 opened。相邻页预加载也可能在打开完成前或其他页面显示期间派发媒体事件,应通过 itemId 识别对应媒体,不能假定都是当前页。缩略图或封面就绪不算原图或视频加载成功。

每次打开保留媒体、按钮配置和按钮回调的快照,更新配置在下次打开生效。sessionId 标识一次打开;异步业务用 itemId 关联数据,index 只表示打开时数组内的位置。重新打开会结束旧会话并通知 dismiss。关闭或发起新的打开请求时,仍在测量来源或解析本地路径的旧请求会以 CANCELLED 结束,避免页面退出后又弹出查看器。

加载失败会显示重试按钮。retryLevixel() 返回 { retried: boolean },仅重试当前可重试的失败媒体,不创建新会话;加载中、没有可重试媒体或查看器已关闭时返回 false。closeLevixel() 在整个查看器关闭完成后返回 { closed: true }。

通常优先使用 openLevixelFromSelector,让 SDK 测量当前可见源。只有宿主已经拥有可靠的源图几何时,才直接调用 openLevixel 并传入 sourceHints。

App 原生插件版

选择 App 原生插件工作流的经典 uni-app Android/iOS 项目,可以从对应 GitHub Release 下载 levixel-uniapp-legacy-<version>.zip。

解压后,将 Sandrox-Levixel/ 放入项目的 nativeplugins/,从 @/nativeplugins/Sandrox-Levixel/js_sdk/index.js 引入同名公共 API。该包需要包含插件的自定义调试基座或离线包;标准基座不包含此原生插件。

媒体、源绑定、按钮和事件用法与上文相同。该包使用同版本的平台运行时和原生核心,不属于 DCloud UTS 市场包,也不支持 uni-app x。

权限、隐私与许可

  • 插件不申请相机、相册、定位、麦克风等运行时权限。
  • 插件不包含广告、统计或推广 SDK。
  • 插件不向作者服务器上传数据;远程媒体请求只访问业务传入的 URL。
  • 本地预览缓存仅用于展示与共享转场,并按 LRU 和下次启动清理策略管理。
  • 许可证为 MIT;LICENSE、license.md 与 THIRD_PARTY_NOTICES.md 随包提供。

源码、版本历史与问题反馈见 Levixel GitHub 仓库。

隐私、权限声明

1. 本插件需要申请的系统权限列表:

无

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

插件不采集或上传个人信息;远程媒体仅向业务传入的 URL 发起请求,本地预览仅用于展示与转场缓存。

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率:

无

许可协议

MIT License

Copyright (c) 2026 SandroX

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.