x-swiper-u 原生轮播
创意原生轮播。一条 list 里图片和视频可以混排,视频走各端系统播放器(无任何三方解码库),带预加载与文件缓存、抖音式播控、双指进全屏、首尾无缝衔接。指示器画在 uvue 覆盖层,加载态、播放键与进度条画在原生层。
兼容性
| Harmony | IOS | Android | WEB | 小程序 |
|---|---|---|---|---|
| 支持 | 支持 | 支持 | 支持 | 微信降级 |
调用
<template>
<view class="wrap">
<x-swiper-u ref="swiperRef" :list="list" :circular="true" effect="stack" :peek="40" :gap="12"
indicator="dot" @change="onChange" @videoplay="onVideoPlay"></x-swiper-u>
</view>
</template>
<script setup lang="uts">
import { XSwiperItem, XSwiperChangeEvent, XSwiperVideoEvent } from "@/uni_modules/x-swiper-u"
// 带可选字段的对象字面量不要用 as 断言,声明成带类型的变量再 push,否则会被 lint 拦下
const picture : XSwiperItem = { type: "image", src: "https://xxx.com/1.jpg", title: "图片" }
const clip : XSwiperItem = { type: "video", src: "https://xxx.com/1.mp4", poster: "https://xxx.com/1.jpg" }
const list = ref<XSwiperItem[]>([picture, clip] as XSwiperItem[])
function onChange(event : XSwiperChangeEvent) : void {
console.log(event.index, event.source)
}
function onVideoPlay(event : XSwiperVideoEvent) : void {
console.log("play", event.index)
}
</script>
<style>
.wrap {
width: 100%;
height: 500px;
}
</style>组件自身撑满父级,父级必须有确定高度。
list 项
| 字段 | 说明 | 默认 |
|---|---|---|
| type | image / video,缺省按扩展名猜,猜不出算图片 | - |
| src | 图片或视频地址,支持 http(s) / file / 本地静态资源 | 必填 |
| title | 卡片底部标题,留空不画 | "" |
| titleColor | 标题颜色 | #ffffff |
| poster | 视频封面,起播后揭掉 | "" |
参数
布局与动效:
| 字段 | 说明 | 默认 |
|---|---|---|
| list | 轮播内容 | [] |
| current | 初始下标 | 0 |
| vertical | 纵向轮播 | false |
| circular | 首尾衔接,滑过末页接首页 | true |
| effect | slide / fade / stack / cube / flip / deck | slide |
| displayCount | 一屏卡片数 | 1 |
| gap | 卡片间距 px | 0 |
| peek | 漏出下一张的长度 px | 0 |
| duration | 换页动画时长 ms | 320 |
| autoplay | 自动轮播 | false |
| interval | 自动轮播间隔 ms | 3000 |
| imageMode | 图片填充方式,见下表 | aspectFill |
| backgroundColor | 容器底色 | #000000 |
imageMode 取值与官方 image 的 mode 对齐:
| 取值 | 说明 |
|---|---|
aspectFill | 等比缩放到铺满容器,超出的部分裁掉,默认值 |
aspectFit | 等比缩放到整幅露出,比例不符时留边 |
scaleToFill | 拉伸铺满,画面会变形 |
widthFix | 宽度铺满容器,高度按原图比例走,多出来的裁掉、不够的留边 |
heightFix | 高度铺满容器,宽度按原图比例走 |
center | 不缩放,按原图尺寸居中显示 |
widthFix / heightFix 需要原图尺寸才能定档,图还没解码完时先按 aspectFill 顶着,拿到尺寸后自动切换;容器尺寸变化时也会重算。
三种布局由 displayCount + peek 组合出来:一屏一张(displayCount=1、peek=0)、一屏一张带漏边(displayCount=1、peek>0)、一屏多卡(displayCount>=2)。vertical 对三种都成立。
fade / cube / flip 只在一屏一张且不漏边时成立,其余情况自动退回 slide;stack / deck 在多卡时也退回 slide。slide 是条带对接:上一张 / 下一张贴在当前页两侧,不会从当前页背后穿过去。deck 是抽卡牌堆:底下最多三层,从顶部层层露出约 8px 并略缩小,顶卡左右滑出并带一点平面转角。
视频:
| 字段 | 说明 | 默认 |
|---|---|---|
| autoplayVideo | 切到视频页自动起播 | true |
| videoMuted | 静音 | false |
| videoLoop | 单个视频循环 | false |
| videoObjectFit | contain / cover / fill | contain |
| videoControls | 中央播放键 + 底部可拖进度条 | true |
| pinchFullscreen | 双指外扩进全屏播放层 | true |
videoObjectFit 与 css 的 object-fit 同义,轮播内与全屏层同时生效:contain 等比缩放到整幅露出,比例不符时留黑边;cover 等比铺满并裁掉超出部分;fill 直接拉伸铺满,画面会变形。图片走 imageMode,两者互不影响。
缓存与预加载:
| 字段 | 说明 | 默认 |
|---|---|---|
| preloadAhead | 向后预加载张数 | 1 |
| preloadBehind | 向前预加载张数 | 1 |
| cacheEnabled | 媒体落盘缓存(视频 + 图片) | true |
| cacheMaxCount | 视频缓存文件数上限 | 8 |
| cacheMaxBytes | 视频缓存总字节上限 | 209715200(200MB) |
cacheMaxCount / cacheMaxBytes 只约束视频。图片文件小、数量多,和视频共用一档上限会被立刻挤掉,因此单独存放并单独按 LRU 淘汰(120 个 / 64MB)。
指示器(画在 uvue 覆盖层):
| 字段 | 说明 | 默认 |
|---|---|---|
| indicator | none / dot / number | dot |
| indicatorShape | circle / rect / round-rect | circle |
| indicatorPosition | top / bottom / left / right | bottom |
| indicatorOffset | 离边距离 px | 12 |
| dotColor | 点颜色 | rgba(255,255,255,0.45) |
| dotActiveColor | 选中点颜色 | #ffffff |
| dotSize | 点基准尺寸 px | 6 |
事件
轮播:
| 事件 | 回参 |
|---|---|
| change | { index, source },source 为 touch / autoplay / api |
| transition | { index, progress, dx, dy },跟手逐帧,progress 为 -1~1 |
| animationfinish | { index, source } |
| click | { index, item, type, src, title, titleColor, poster }。视频页的播放/暂停点击不抛此事件。iOS 上 item 和扁平字段都可能被掏空,请用自己的 list[event.index];不要把事件对象再传回插件方法 |
| dragstart / dragend | { index } |
视频:
| 事件 | 回参 |
|---|---|
| videoplay / videopause / videoended / videoready | { index } |
| timeupdate | { index, current, duration },单位秒 |
| buffering | { index, buffering } |
| fullscreenchange | { index, full } |
| videoerror | { index, errCode, errMsg } |
预加载:
| 事件 | 回参 |
|---|---|
| preloadstart | { index, type, src } |
| preloadprogress | { index, loaded, total },拿不到总长时 total 为 0 |
| preloadready | { index, fromCache } |
| preloadfail | { index, errMsg } |
方法
ref 上可调:
| 方法 | 说明 |
|---|---|
| swipeTo(index, animated) | 跳到指定下标 |
| prev(animated) / next(animated) | 上/下一张 |
| start() / stop() | 开关自动轮播 |
| pause() / resume() | 整体暂停/恢复,页面隐藏时用 |
| playVideo() / pauseVideo() | 播放/暂停当前视频 |
| seekVideo(seconds) | 拖到指定秒 |
| enterFullscreen() / exitFullscreen() | 进出全屏播放层 |
| getCurrent() | 当前下标 |
加载态与图标
每一页都有 加载中 / 就位 / 失败 三态,画在页面正中:
- 图片解码完成、视频 prepared 之前转圈;视频缓冲(buffering)时也转圈。
- 图片解不出来、视频起播失败时换成警告图标,不再是一块黑屏。警告图标是静止的,只有转圈态才转。
- 鸿蒙侧缓存文件读不出来会先退回原始网络地址重试一次,仍失败才判定为失败态。
- 转圈或失败时不叠中央播放键,避免两个图标压在一起。
- 失败页点一下即重拉,整页都是热区,不必精准点中图标。图片重新解一次,视频丢掉旧播放器从头准备。地址没变时各端播放器/浏览器不会重新发起请求,所以重拉是「先清空地址再填回去」,清空那一拍抛出的失败不计入失败态。
- 未就位的页会铺一层
backgroundColor实底占位,保证失败页是纯色而不会透出相邻页。
图标取 Remix Icon 字形,四端同一组码点:加载 ri-loader-4-line(\eec6)、失败 ri-error-warning-line(\eca1)、播放 ri-play-circle-fill(\f008)、暂停 ri-pause-circle-fill(\efd5)。播放/暂停字形自带圆底,居中即为正圆居中,不再手工偏移三角形。
字体来源按端区分,改码点时四端要一起改:
| 端 | 字体来源 |
|---|---|
| Android | 插件内 utssdk/app-android/assets/x-swiper-remixicon.ttf,Typeface.createFromAsset 加载 |
| iOS | 插件内 utssdk/app-ios/Resources/x-swiper-remixicon.ttf,运行时 CTFontManagerRegisterGraphicsFont 注册 |
| Harmony | 插件内 utssdk/app-harmony/resources/rawfile/x-swiper-remixicon.ttf,uiContext.getFont().registerFont 注册 |
| Web | 优先用宿主 App.uvue 里 uni.loadFontFace 注册的 remixicon;没有时插件自己按 /static/tmui4xLibs/static/remixicon.ttf 兜底注册 |
| 微信 | 宿主 App.uvue 引入的 tmx-ui/scss/remixicon.min.css |
三端原生字体文件都带 x-swiper- 前缀,与其它插件的 remixicon.ttf 不会撞名。字体注册失败只是缺个指示,不影响轮播主体。
视频与缓存说明
- 播放器按「当前 + 下一 + 上一」池化 2~3 个,不会一页一个常驻解码器。
- 滑走立刻暂停上一个视频且保留进度,滑回从断点续播(
videoLoop例外)。 - 当前页是视频且正在播时,轮播自身的
autoplay让位,等播完或用户滑走再继续。 - 远程视频先下到
cacheDir/x-swiper-u/再以本地文件播放,同 URL 二次进入直接命中;超出cacheMaxCount或cacheMaxBytes按 LRU 删。远程图片同样落盘(Android 存cacheDir/x-swiper-u/img/,iOS 走 URLCache,鸿蒙走沙箱缓存),重进页面直接出图,不再整张重下。 - 组件卸载或
list大改时取消进行中的下载、释放播放器与定时器。 - 播放中 / 缓冲中一律以播放器自己的状态为准(iOS
timeControlStatus、AndroidMEDIA_INFO_BUFFERING_*、HarmonyonStart/onPause),不从进度回调反推,避免出现「已经在播还转着圈」或「播着却显示播放键」。 - 进度条拖动期间锁住播放器的进度回写,
seek落位后才解锁,手指松开不会弹回原位。 - 全屏层除双指内收外,右上角还有一个「收起」按钮,四端一致。
- 未开
videoLoop时播完停在结尾,再点播放键从头重播(iOS 需先把播放头倒回 0,其余端播放器自带该行为)。 - 进出全屏是给同一个播放器换渲染面。暂停状态下换面不会有新帧,Android 会原地 seek 一次把当前帧补出来,避免进全屏是黑的。
平台差异
- Android 用
MediaPlayer+TextureView,iOS 用AVPlayer+AVPlayerLayer,Harmony 用@kit.MediaKit的Video,Web 用<video>。不带 ExoPlayer / media3 / IJK 等三方库。 - 微信小程序为降级实现,走官方
swiper+video:只有slide、官方circular、vertical、近似的多卡与漏边;不支持stack/cube/flip/deck、精确gap、双指全屏、跟手transition进度、预加载进度事件。视频只给当前页挂节点,切页即销毁,等价于「滑走停上层视频」;播控交给官方控件,playVideo/pauseVideo/seekVideo/enterFullscreen/exitFullscreen在微信端不生效。 - Web 的全屏走 Fullscreen API,部分浏览器需用户手势触发,
enterFullscreen()直接调用可能被拒。 gap/peek传的是逻辑 px,各端自行换算成设备像素或 vp。
版本
版权归https://xui.tmui.design你不得修改及二次开发,仅供TMUI4会员商用使用。不得转给非VIP会员使用,一经查实数倍赔偿,并追究法律责任。
更新日志
1.0.13(2026-08-16)
- iOS 的 click 事件往往只保住
index,item/src过不了 emit。测试页改为用页面自己的list[event.index]取数据;业务侧请同样按下标查自己的列表,不要依赖事件里的嵌套对象。 - 一屏多卡时松手按拖过的页数就近停靠,可以一次跨过 2、3 个索引;一屏一张仍是一次最多翻一页。
1.0.12(2026-08-16)
- 修复 iOS 上 click 的
item始终是undefined:嵌套对象经 iOS 事件通道 / Vue emit 会被丢掉。事件改为同时带上type/src等扁平字段,页面直接读这些字段,不要把事件对象再传回插件方法(会method call failed)。 - 修复一屏多卡时只能点中最左边那张、点其它可见卡却回最左项:各端点击改为按变换后的卡片几何做命中,不再整容器都算
currentIndex。 - 修复一屏多卡向左拖、未松手时右边一大块黑:环状最短路径把本应排在右侧的页折到了左边,
slidePageVisible再把它们藏掉。偏移改为按displayCount加宽可见带,渲染窗口也多留一页。
1.0.11(2026-08-16)
- 新增
deck抽卡牌堆动效:一屏一张时底下最多叠三层,从顶部层层露出约 8px 并略缩小,顶卡左右滑出并带一点平面转角;多卡时与stack一样退回slide。微信官方 swiper 不支持,仍降级为slide。 - 修复 Android 上
deck向右切时,后面那张有概率先闪到最前再缩回去:松手后的插值动画把飞出锚点改成了半途位置,过半页时后一张会被当成顶卡;其它端是一次性跳到目标页,没有这个问题。 - 修复鸿蒙
deck向右快切时多张后牌叠到最顶层:隐式动画会插值zIndex,隐藏牌又从正中全尺寸入场;改为飞出锚点保持手势起点、层级立刻落下、隐藏牌停在牌堆后,并丢掉上一次动画的落位回调。
1.0.10(2026-08-16)
- 修复各端 slide 切换时邻页从当前页背后横穿、透明内容里闪过其它图:此前所有动效都按「离当前越近层级越高」叠层,再叠加环状最短路径,左侧那页会瞬间折到右侧,过渡把它从当前页底下插值过去。slide 改为条带对接(同层、只露和容器相交的页),环状偏移沿上一帧的方向连续走、从场外滑出,落位后再折回最短路径。slide 不再加 3D 透视,页底铺容器底色,完整露出 / 居中时也不会透出背后。fade / stack / cube / flip 仍按距离叠层。
1.0.9(2026-08-15)
- 修复 Android 全屏顶部白条把画面顶下来、且「收起」被让出双倍状态栏高:根因是 Dialog 窗口本身就被系统排在状态栏下面,底下 Activity 的浅色状态栏露成白带;上一版又在这个已经被顶过的内容上再加
statusBarHeight(),「收起」就叠成两档。改为和 iOS 一样把全屏层挂到当前 Activity 窗口上,进全屏时藏起状态栏并关掉对比度白遮罩,退出还原。「收起」只按屏幕坐标让一次:全屏层已经落在状态栏下就不再加,铺到物理顶才按 insets 让开。
1.0.8(2026-08-15)
imageMode补齐到官方image的mode全集,新增widthFix(宽度铺满、高度按原图比例)、heightFix(高度铺满、宽度按原图比例)、center(不缩放居中)。默认值仍是aspectFill,老页面观感不变。五端都没有和widthFix/heightFix一一对应的原生取值,改为按原图与容器的宽高比换算:图更宽时按宽贴合等价于「等比全露出」,反之等价于「等比铺满裁切」,heightFix正好相反。原图尺寸分别取自 Android 的drawable固有尺寸、iOS 的UIImage.size、鸿蒙Image.onComplete事件、web 的naturalWidth,尺寸到达前先按aspectFill顶着,容器尺寸变化时重算。- 修复鸿蒙全屏播放键与进度条始终不刷新:
bindContentCover的 builder 根节点必须是唯一的容器节点,此前直接把自定义组件当了根节点,这棵子树只按初始值渲染一次,之后无论走@ObjectLink还是组件自己的@State都不会再刷新——这也是 1.0.6 换成回调推@State后问题依旧的原因。builder 里补一层Column根节点即可。 - 修复 Android 全屏顶部一条状态栏高的白色空隙:
statusBarColor从 API 35 起已是空操作,而弹层默认不越过状态栏,那一条露的是底下 Activity 的白底。改为关掉 decor 的系统窗口内缩,让全屏层自己的黑底铺满整屏,状态栏图标同时强制成浅色。
1.0.7(2026-08-15)
- 新增
videoObjectFit属性(contain/cover/fill,默认contain),轮播内与全屏层同时生效。此前视频跟着imageMode走:Android 的 TextureView 直接铺满容器,画面是拉伸变形的;其余端按aspectFill大幅裁切。图片仍走imageMode,两者解耦。 - 修复 Android 双指进全屏后画面黑屏、点一下播放才正常:外扩打开全屏时把
pinching清成了 false,抬手那一下被当成单击正好暂停了视频,而 MediaPlayer 换 Surface 后暂停态不会推帧。改为手势结束前不清标志,并在换面后对暂停中的视频原地 seek 一次补帧(进、出全屏两个方向都补)。 - Android 全屏层补上右上角「收起」按钮并按状态栏高度让开,窗口与系统栏一并刷黑。
FLAG_FULLSCREEN在 API 35 已是空操作,弹层会铺到状态栏下面,系统默认那层浅色底就露成了顶部白条。 - Android 全屏播放键从 emoji 换成 remixicon 字形,居中对齐,并跟随播放状态刷新;全屏进度条不再受
videoControls约束。 - 修复 Android 缓存过的图片重进页面仍然黑屏重下:此前只有随组件销毁的内存 LRU,磁盘上什么都没留。远程图片改为落盘到
cacheDir/x-swiper-u/img/,二次进入直接读本地。图片与视频各用一档 LRU 上限,避免小文件被cacheMaxCount(默认 8)连带淘汰。失败重试会连磁盘那份一起删掉,不会反复取到坏图。
1.0.6(2026-08-15)
- 修复 iOS 视频播完后点播放键不重播:
AVPlayer的播放头停在结尾,此时play()是个空操作。改为播完打标记,下次起播先 seek 回 0 再播;中途拖动会清掉这个标记。Android 的PlaybackCompleted与 web<video>本身就会自动回到开头,无需处理。 - 修复鸿蒙全屏播放键字形不变、进度条停在进全屏那一刻:全屏层给 session 挂了
@ObjectLink,而父组件已经@ObjectLink过同一个对象,再在bindContentCover这棵独立子树里挂第二次是坏的——不光 session 的改动传不进来,组件自身的@State也一并不刷新,界面从建层起就再没动过。改成和内嵌页一致:session 只作普通成员持有,播放状态、进度、时长走onFullscreenSync回调推进本地@State。 - 删除 iOS 定位黑屏时留下的
debugLog与事件通道里的日志转发。 - 修复 Android kotlin 编译失败:
dotStyles计算属性引用了写在它后面的dotStyle,Kotlin 的局部函数不提升,改为声明在前;effect/imageMode这两个字面量联合类型的 prop 在 Android 的 props 代码生成里退化成Any,塞进String字段编不过,下发配置时断言回各自的类型。
1.0.5(2026-08-15)
- 修复 iOS 视频已经在播却一直转圈:
isPlaybackBufferEmpty与isPlaybackLikelyToKeepUp各自只在自己变 true 时通知,播放中缓冲短暂见底后 keepUp 早已是 true 不会再发通知,转圈就永远留在屏幕上。改为只听timeControlStatus这一个权威来源,并在结论翻转时才上报 buffering。 - 修复 iOS 自动播放已起播却仍显示播放键:
onReady跑在play()之前,回调里读到的 rate 还是 0。改为先起播再回调,播放键统一由timeControlStatus驱动。 - iOS 全屏层补上右上角「收起」按钮,此前只能双指内收退出。
- 修复 iOS / Android 进度条拖动后弹回原位:
seek落位前播放器报的还是旧进度,会把刚拖到的位置覆盖掉。拖动期间锁住进度回写,iOS 等 seek 完成回调、Android 等onSeekComplete后才解锁,且 iOS 改为零容差 seek 保证定位精确。 - 修复鸿蒙全屏点播放没反应:全屏播放器没接
onStart/onPause,播放状态靠onUpdate硬置成 true,起播失败时按钮显示的是「正在播」,第一下点击成了空点。改为只认播放器回调,并补上onError。 - 修复鸿蒙全屏拖不动进度:
onPrepared未到达时时长为 0,拖动整个是死的。改为退回进全屏那一刻从内嵌播放器抄来的时长,并压住拖动期间的进度回写到onSeeked(带超时兜底)。 - 修复 iOS 点失败图标重拉像没反应:失败响应可能落在 URLCache 里,重拉取到的还是同一条坏响应。重试改为强制走网络。
- 失败态不再被缓冲状态顶掉,只有重拉才会退出失败态(iOS / Android)。
1.0.4(2026-08-15)
- 修复 iOS 列表始终为空导致的黑屏:uvue 侧 6 条的数组传进插件类的实例方法后变成 0 条,原生只收到
[],layoutPages每次都以 items 为空跳过。同一份数组传进顶层导出函数是好的,因此列表改由xSwiperListToJson()在插件内归一化并序列化,类方法只接字符串。 - 列表入口由
setList改名为applyItems,五端一致:iOS 编译产物是 NSObject 子类,setList:正好是属性list的 ObjC setter 选择器形状。 click事件里的item改由组件侧按下标补齐,插件内部不再各存一份列表副本。
1.0.3(2026-08-15)
- 修复 iOS 整块黑屏、手势与 ref 方法全都没反应:容器尺寸是宿主在
bindIOSView之后才下发的,原先靠addObserver(forKeyPath:"bounds")等这个时机,而 UIView 被布局系统改动 frame / bounds 时并不保证发 KVO 通知,回调一直没来,configure/setList又都跑在零尺寸上被hostW <= 0挡掉,页面一个都没建出来。改为由 uvue 侧直接绑定一个重写了layoutSubviews的容器视图,尺寸到位必然回调,并在首次拿到尺寸时补跑播放与自动轮播。 - iOS 去掉中间层容器,宿主排的就是承载页面的那一个 view,避免中间层不跟随宽高时整块零尺寸。
- iOS
setup()之前到达的配置与列表不再被丢弃,绑定完成后补发一次。
1.0.2(2026-08-15)
- 失败页支持点击重拉,五端一致,整页都是热区;图片重新解一次,视频丢掉旧播放器从头准备。
- 修复鸿蒙失败后叹号图标跟着转圈:加载与失败改成两个独立节点,无限旋转的属性动画不再残留在换了字形的同一个节点上。
- 修复鸿蒙换页时有别的图从容器正中横穿而过:动画途中新建的页没有可插值的起始位移,会跟着这次隐式动画从正中飞向目标位,现在先压着不上屏,落位后再亮。
- 未就位的页铺一层实底占位,失败页不会再透出相邻页;就位后恢复透明,不影响 fade 这类叠加动效。
- Web 端新插入的页首次落位强制关掉 transition,同样避免从正中飞入。
1.0.1(2026-08-15)
- 五端补齐每页的加载中 / 失败图标指示,图标取 Remix Icon 字形,加载失败不再只是一块黑屏。
- 中央播放键改用自带圆底的
ri-play-circle-fill字形,四端都是正圆居中,去掉原先手工偏移三角形的做法。 - 修复鸿蒙滑动时页面黑屏、串出别的图:ForEach 键值只用 index,页滑出窗口被淘汰再滑回时子组件仍绑在旧 frame 对象上,键值补上对象创建序号。
- 修复鸿蒙缓存图片加载不出来:沙箱路径改用
fileUri.getUriFromPath生成 URI,读不出来时退回原始网络地址并清掉坏缓存。
1.0.0(2026-08-15)
- 图片视频混排的创意原生轮播:三种布局 + 竖向、五种动效、首尾无缝衔接。
- 视频走各端系统播放器,池化 2~3 个,带预加载与 LRU 文件缓存。
- 抖音式播控:点屏播放/暂停、中央播放键、底部可拖进度条、双指外扩进全屏。
- 微信端按官方 swiper + video 降级。
