前言
在我们预览全景的时候,场景的切换一般都是通过皮肤导航栏来进行切换的,原本是使用MAKE VTOUR Droplet.exe 自动生成的vtour文件夹中默认皮肤xml
也就是主xml中引入的vtourskin.xml,但是ui实在是太丑了,自定义样式比较困难
于是就不使用xml来展示皮肤导航了,而是自己实现一个这样的皮肤导航,通过js来和krpano进行交互,这是实现后的效果,可以显示/隐藏皮肤导航,通过点击,滚轮以及拖动来控制皮肤导航
实现自定义皮肤导航
实现步骤:
- 定义组件
- 实现皮肤导航隐藏/显示
- 实现滚轮滑动效果
- 实现鼠标拖动效果
- 实现点击切换场景
1.定义组件
新建一个SkinControl组件,并且定义接口描述组件需要传入的属性:
show
:用于控制导航条的显示与隐藏scenes
:场景列表onSceneChange
:场景切换事件处理函数,接受一个场景对象作为参数
js
import React, { memo } from 'react'
import type { FC, ReactNode } from 'react'
import type { ISceneType } from '@/views/pano/edit/type'
import { SkinControlWrapper } from '.'
interface IProps {
children?: ReactNode
/**
* boolean: 导航条显示/隐藏
*/
show: boolean
/**
* scenes: 场景列表
*/
scenes: ISceneType[]
/**
* onSceneChange: 场景切换回调
*/
onSceneChange?: (scene: ISceneType) => void
}
const SkinControl: FC<IProps> = ({ show, scenes = [], onSceneChange }) => {
return (
<SkinControlWrapper>
<div></div>
</SkinControlWrapper>
)
}
export default memo(SkinControl)
2. 实现皮肤导航隐藏/显示
皮肤导航隐藏/显示主要通过高度来控制,给div一个class名为skin_control_bar,height设置为0,overflow设置为hidden,背景色设为黑色,透明度给个0.3,再根据show属性动态添加class名为skin_control_show,skin_control_show里height设为100px
html
<SkinControlWrapper>
<div className={`skin_control_bar ${show ? 'skin_control_show' : ''}`}>
</div>
</SkinControlWrapper>
css
.skin_control_bar {
box-sizing: border-box;
background: rgba(0, 0, 0, 0.3);
height: 0;
overflow: hidden;
padding: 0 36px;
position: relative;
width: 100%;
}
.skin_control_show {
height: 100px;
}
emm...有点生硬,skin_control_bar里再给个高度的过渡效果:transition: height 0.15s linear;
3.实现滚轮滑动效果
先将场景展示在皮肤导航里,使用绝对定位+监听鼠标滚轮事件就可以实现滚轮的滑动效果
3.1定位
添加一个class名为carousel的元素作为场景列表的容器,设置容器的定位方式为相对定位,高度为100px,容器的溢出处理方式为隐藏,最后加上pointer-events: none; 防止鼠标事件的穿透
然后在容器里遍历场景列表,添加class名为carousel-item的div,设置绝对定位,top值为0,left为50%
html
<SkinControlWrapper>
<div className={`skin_control_bar ${show ? 'skin_control_show' : ''}`}>
<div className="carousel">
{scenes.map((item, index) => {
return (
<div className="carousel-item" key={index} onClick={() => handleClick(item, index)}>
<div className="carousel-item_box">
<img src={OSS_PATH + '/' + item.thumbUrl} alt="" />
</div>
</div>
)
})}
</div>
</div>
</SkinControlWrapper>
css
.carousel {
position: relative;
z-index: 1;
height: 100px;
overflow: hidden;
pointer-events: none;
.carousel-item {
cursor: pointer;
box-sizing: border-box;
width: 100px;
height: 100px;
padding: 5px;
overflow: hidden;
position: absolute;
top: 0;
left: 50%;
.carousel-item_box {
box-sizing: border-box;
width: 100%;
height: 100%;
border: 2px solid #fff;
border-radius: 5px;
img {
width: 100%;
height: 100%;
object-fit: cover;
pointer-events: none;
border-radius: 5px;
}
}
}
}
3.2排列
所有元素都重叠在一起了,我们需要进行一个初始的排列,先定义一些计算用到的变量:
js
const [progress, setProgress] = useState(50)
const speedWheel = 1 / scenes.length
const speedDrag = -0.15
useRef
获取容器元素
ini
const carouselRef = useRef<HTMLDivElement>(null)
<div ref={carouselRef}></div>
在CSS类.carousel-item中添加自定义的css变量:
--count
: 0,子元素列表的数量;--active
:0,子元素的索引值--x
: 默认值calc(calc(var(--active) * var(--count)) * 100%),子元素的水平位移
css
.carousel-item {
--count: 0;
--active: 0;
--x: calc(calc(var(--active) * var(--count)) * 100%);
cursor: pointer;
box-sizing: border-box;
width: 100px;
height: 100px;
padding: 5px;
overflow: hidden;
position: absolute;
top: 0;
left: 50%;
user-select: none;
transform-origin: 0% 100%;
pointer-events: all;
// 水平偏移
transform: translate(var(--x));
// 动画过渡
transition: transform 0.8s cubic-bezier(0, 0.02, 0, 1);
}
初始化执行animate函数,获取容器中的子元素列表和子元素列表的数量,并根据进度值progress计算当前处于中间元素的索引
js
useEffect(() => {
if (scenes.length) {
animate()
}
}, [scenes, progress])
// 初始化
const animate = () => {
const childNodes = carouselRef.current?.childNodes
const childNodesLength = carouselRef.current?.childNodes.length || 0
const p = Math.max(0, Math.min(progress, 100))
const a = Math.ceil((p / 100) * (childNodesLength - 1))
setProgress(p)
}
然后在遍历每个子元素,设置每个子元对应的css变量
js
// 排列元素位置
const displayItems = (item: HTMLElement, index: number, active: number, length: number) => {
item.style.setProperty('--count', length.toString())
item.style.setProperty('--active', ((index - active) / length).toString())
}
const animate = () => {
// ...
childNodes?.forEach((item, index) =>
displayItems(item as HTMLElement, index, a, childNodesLength)
)
}
为容器添加滚轮监听事件,计算滚动时候滚动的进度,计算方式为滚轮的滚动量 * speedWheel,再重新setState更新progress,重新排列元素位置
html
<div className="carousel" ref={carouselRef} onWheel={handleWheel}>
{scenes.map((item, index) => {
return (
<div className="carousel-item" key={index} onClick={() => handleClick(item, index)}>
<div className="carousel-item_box">
<img src={OSS_PATH + '/' + item.thumbUrl} alt="" />
</div>
</div>
)
})}
</div>
js
const handleWheel = (e: React.WheelEvent<HTMLDivElement>) => {
const wheelProgress = e.deltaY * speedWheel
setProgress(progress + wheelProgress)
}
4. 实现鼠标拖动效果
实现鼠标拖动效果就要监听鼠标事件,记录鼠标按下的位置和鼠标状态,在拖动的过程中重新计算progress的值,重新排列元素位置,当鼠标离开元素或者鼠标释放的时候清除鼠标按下的状态
html
<div
className="carousel"
ref={carouselRef}
onWheel={handleWheel}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseLeave}
>
{scenes.map((item, index) => {
return (
<div className="carousel-item" key={index} onClick={() => handleClick(item, index)}>
<div className="carousel-item_box">
<img src={OSS_PATH + '/' + item.thumbUrl} alt="" />
</div>
</div>
)
})}
</div>
js
const handleMouseDown = (
e: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>
) => {
// 是否移动端触发
const isTouch = 'touches' in e
// 标记鼠标状态
setIsDown(true)
// 记录鼠标按下位置
setStartX(isTouch ? e.touches && e.touches[0].clientX : e.clientX)
}
const handleMouseMove = (
e: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>
) => {
if (!isDown) return
const isTouch = 'touches' in e
const x = isTouch ? e.touches[0].clientX : e.clientX
const mouseProgress = (x - startX) * speedDrag
setProgress(progress + mouseProgress)
setStartX(x)
}
// 清除鼠标状态
const handleMouseUp = () => {
setIsDown(false)
}
const handleMouseLeave = () => {
setIsDown(false)
}
鼠标拖动效果也是实现了,为了适配移动端也要给容器绑定touch事件
html
<div
className="carousel"
ref={carouselRef}
onWheel={handleWheel}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseLeave}
onTouchStart={handleMouseDown}
onTouchMove={handleMouseMove}
onTouchEnd={handleMouseUp}
>
// ...
</div>
5. 实现点击切换场景
当我们点击皮肤导航中的场景时需要展示对应的全景,所以为场景对应的元素绑定点击事件,点击的时候根据场景的name属性来调用krpano中的loadscene
方法来切换全景,并且重新计算progress值,使点击的场景位置处于皮肤导航中间
html
{scenes.map((item, index) => {
return (
<div className="carousel-item" key={index} onClick={() => handleClick(item, index)}>
<div className="carousel-item_box">
<img src={OSS_PATH + '/' + item.thumbUrl} alt="" />
</div>
</div>
)
})}
js
const handleClick = (item: ISceneType, i: number) => {
kp.call(`loadscene(${item.scene_id}, null, MERGE, BLEND(1))`)
setProgress((i / (carouselRef.current?.childNodes.length || 0)) * 100 + 10)
}
结尾
以上就是我对自定义皮肤导航的实现,如果还有更好的方式也欢迎大家一起分享交流,学习学习😊🤞💋😘