Skip to content

x-swiper-u 原生轮播

创意原生轮播。一条 list 里图片和视频可以混排,视频走各端系统播放器(无任何三方解码库),带预加载与文件缓存、抖音式播控、双指进全屏、首尾无缝衔接。指示器画在 uvue 覆盖层,加载态、播放键与进度条画在原生层。

兼容性

HarmonyIOSAndroidWEB小程序
支持支持支持支持微信降级

调用

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

字段说明默认
typeimage / video,缺省按扩展名猜,猜不出算图片-
src图片或视频地址,支持 http(s) / file / 本地静态资源必填
title卡片底部标题,留空不画""
titleColor标题颜色#ffffff
poster视频封面,起播后揭掉""

参数

布局与动效:

字段说明默认
list轮播内容[]
current初始下标0
vertical纵向轮播false
circular首尾衔接,滑过末页接首页true
effectslide / fade / stack / cube / flip / deckslide
displayCount一屏卡片数1
gap卡片间距 px0
peek漏出下一张的长度 px0
duration换页动画时长 ms320
autoplay自动轮播false
interval自动轮播间隔 ms3000
imageMode图片填充方式,见下表aspectFill
backgroundColor容器底色#000000

imageMode 取值与官方 imagemode 对齐:

取值说明
aspectFill等比缩放到铺满容器,超出的部分裁掉,默认值
aspectFit等比缩放到整幅露出,比例不符时留边
scaleToFill拉伸铺满,画面会变形
widthFix宽度铺满容器,高度按原图比例走,多出来的裁掉、不够的留边
heightFix高度铺满容器,宽度按原图比例走
center不缩放,按原图尺寸居中显示

widthFix / heightFix 需要原图尺寸才能定档,图还没解码完时先按 aspectFill 顶着,拿到尺寸后自动切换;容器尺寸变化时也会重算。

三种布局由 displayCount + peek 组合出来:一屏一张(displayCount=1peek=0)、一屏一张带漏边(displayCount=1peek>0)、一屏多卡(displayCount>=2)。vertical 对三种都成立。

fade / cube / flip 只在一屏一张且不漏边时成立,其余情况自动退回 slidestack / deck 在多卡时也退回 slideslide 是条带对接:上一张 / 下一张贴在当前页两侧,不会从当前页背后穿过去。deck 是抽卡牌堆:底下最多三层,从顶部层层露出约 8px 并略缩小,顶卡左右滑出并带一点平面转角。

视频:

字段说明默认
autoplayVideo切到视频页自动起播true
videoMuted静音false
videoLoop单个视频循环false
videoObjectFitcontain / cover / fillcontain
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 覆盖层):

字段说明默认
indicatornone / dot / numberdot
indicatorShapecircle / rect / round-rectcircle
indicatorPositiontop / bottom / left / rightbottom
indicatorOffset离边距离 px12
dotColor点颜色rgba(255,255,255,0.45)
dotActiveColor选中点颜色#ffffff
dotSize点基准尺寸 px6

事件

轮播:

事件回参
change{ index, source }sourcetouch / 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()当前下标

加载态与图标

每一页都有 加载中 / 就位 / 失败 三态,画在页面正中:

  1. 图片解码完成、视频 prepared 之前转圈;视频缓冲(buffering)时也转圈。
  2. 图片解不出来、视频起播失败时换成警告图标,不再是一块黑屏。警告图标是静止的,只有转圈态才转。
  3. 鸿蒙侧缓存文件读不出来会先退回原始网络地址重试一次,仍失败才判定为失败态。
  4. 转圈或失败时不叠中央播放键,避免两个图标压在一起。
  5. 失败页点一下即重拉,整页都是热区,不必精准点中图标。图片重新解一次,视频丢掉旧播放器从头准备。地址没变时各端播放器/浏览器不会重新发起请求,所以重拉是「先清空地址再填回去」,清空那一拍抛出的失败不计入失败态。
  6. 未就位的页会铺一层 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.ttfTypeface.createFromAsset 加载
iOS插件内 utssdk/app-ios/Resources/x-swiper-remixicon.ttf,运行时 CTFontManagerRegisterGraphicsFont 注册
Harmony插件内 utssdk/app-harmony/resources/rawfile/x-swiper-remixicon.ttfuiContext.getFont().registerFont 注册
Web优先用宿主 App.uvueuni.loadFontFace 注册的 remixicon;没有时插件自己按 /static/tmui4xLibs/static/remixicon.ttf 兜底注册
微信宿主 App.uvue 引入的 tmx-ui/scss/remixicon.min.css

三端原生字体文件都带 x-swiper- 前缀,与其它插件的 remixicon.ttf 不会撞名。字体注册失败只是缺个指示,不影响轮播主体。

视频与缓存说明

  1. 播放器按「当前 + 下一 + 上一」池化 2~3 个,不会一页一个常驻解码器。
  2. 滑走立刻暂停上一个视频且保留进度,滑回从断点续播(videoLoop 例外)。
  3. 当前页是视频且正在播时,轮播自身的 autoplay 让位,等播完或用户滑走再继续。
  4. 远程视频先下到 cacheDir/x-swiper-u/ 再以本地文件播放,同 URL 二次进入直接命中;超出 cacheMaxCountcacheMaxBytes 按 LRU 删。远程图片同样落盘(Android 存 cacheDir/x-swiper-u/img/,iOS 走 URLCache,鸿蒙走沙箱缓存),重进页面直接出图,不再整张重下。
  5. 组件卸载或 list 大改时取消进行中的下载、释放播放器与定时器。
  6. 播放中 / 缓冲中一律以播放器自己的状态为准(iOS timeControlStatus、Android MEDIA_INFO_BUFFERING_*、Harmony onStart / onPause),不从进度回调反推,避免出现「已经在播还转着圈」或「播着却显示播放键」。
  7. 进度条拖动期间锁住播放器的进度回写,seek 落位后才解锁,手指松开不会弹回原位。
  8. 全屏层除双指内收外,右上角还有一个「收起」按钮,四端一致。
  9. 未开 videoLoop 时播完停在结尾,再点播放键从头重播(iOS 需先把播放头倒回 0,其余端播放器自带该行为)。
  10. 进出全屏是给同一个播放器换渲染面。暂停状态下换面不会有新帧,Android 会原地 seek 一次把当前帧补出来,避免进全屏是黑的。

平台差异

  1. Android 用 MediaPlayer + TextureView,iOS 用 AVPlayer + AVPlayerLayer,Harmony 用 @kit.MediaKitVideo,Web 用 <video>。不带 ExoPlayer / media3 / IJK 等三方库。
  2. 微信小程序为降级实现,走官方 swiper + video:只有 slide、官方 circularvertical、近似的多卡与漏边;不支持 stack / cube / flip / deck、精确 gap、双指全屏、跟手 transition 进度、预加载进度事件。视频只给当前页挂节点,切页即销毁,等价于「滑走停上层视频」;播控交给官方控件,playVideo / pauseVideo / seekVideo / enterFullscreen / exitFullscreen 在微信端不生效。
  3. Web 的全屏走 Fullscreen API,部分浏览器需用户手势触发,enterFullscreen() 直接调用可能被拒。
  4. gap / peek 传的是逻辑 px,各端自行换算成设备像素或 vp。

版本

版权归https://xui.tmui.design你不得修改及二次开发,仅供TMUI4会员商用使用。不得转给非VIP会员使用,一经查实数倍赔偿,并追究法律责任。

更新日志

1.0.13(2026-08-16)

  • iOS 的 click 事件往往只保住 indexitem / 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 补齐到官方 imagemode 全集,新增 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 视频已经在播却一直转圈:isPlaybackBufferEmptyisPlaybackLikelyToKeepUp 各自只在自己变 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 降级。
最近更新