需求
h5页面可以感应手机移动,作出相应的微动效果
实现方案
手机移动产生页面微动效果的主要用到 deviceorientation 事件,这个事件主要是监听并接收设备方向变化信息。 在window上监听 deviceorientation 事件,在回调函数中获取手机转动角度:
window.addEventListener('deviceorientation', handleFunc, false);
function handleFunc(evnet){
var alpha = event.alpha;
var beta = event.beta;
var gamma = event.gamma;
}
aplha:行动装置水平放置时,绕 Z 轴旋转的角度,数值为 0 度到 360 度。 beta:行动装置水平放置时,绕 X 轴旋转的角度,数值为 -180 度到 180 度。 gamma:行动装置水平放置时,绕 Z 轴旋转的角度,数值为 -90 度到 90 度。
具体实现
1.动画区域布局
index.js
import React from 'react'
import './index.less'
const classPrefix = 'valentine-header'
const cx = s => `${classPrefix}-${s}`
export default class ValentineHeader extends React.PureComponent {
render() {
return <header className={classPrefix}>
<img
className={cx('cloud')}
src={'//img.ikstatic.cn/MTU4OTI3NjIwMjIwNSMyNDUjcG5n.png'}
ref={el => (this.cloud = el)}
alt=''
/>
<img
className={cx('light')}
src={'//img.ikstatic.cn/MTU4OTI3NjIzMTg5MSMyMTUjcG5n.png'}
ref={el => (this.light = el)}
alt=''
/>
<img
className={cx('heart')}
src={'//img.ikstatic.cn/MTU4OTI3NjI0MzIyNiM5ODEjcG5n.png'}
ref={el => (this.heart = el)}
alt=''
/>
<img
className={cx('dots')}
src={'//img.ikstatic.cn/MTU4OTI3NjI1NTIxMSM0NzMjcG5n.png'}
ref={el => (this.dots = el)}
alt=''
/>
</header>
}
}
index.less
@class-prefix: ~'valentine-header';
.@{class-prefix} {
width: 100vw;
height: 85vw;
position: relative;
overflow: hidden;
background-color: #ffd6ee;
height: 100vh;
&-cloud {
width: 187vw;
position: absolute;
top: -30vw;
left: -33vw;
z-index: 10;
}
&-text {
width: 100vw;
position: absolute;
top: 7vw;
z-index: 9;
}
&-light {
width: 111vw;
position: absolute;
top: -19vw;
left: -5.6vw;
z-index: 2;
}
&-heart {
width: 145vw;
position: absolute;
top: 20vw;
left: -52vw;
z-index: 8;
transform: rotate(-15deg);
}
&-dots {
width: 95vw;
position: absolute;
top: 2vw;
left: 11vw;
z-index: 6;
}
}
2.监听事件
在window上监听 deviceorientation 事件,获取手动运动与方向数据,使需要动画的DOM产生对于的位置移动,为了使动画体验感更好,采取分层次动画,从上到下img元素移动的相对位置依次放大
componentDidMount() {
window.addEventListener('deviceorientation', this.deviceorientationChange, false)
}
deviceorientationChange=(event) => {
let { beta, gamma } = event;
beta = Math.round(beta)
gamma = Math.round(gamma)
let X = Math.round(gamma / 9)
let Y = Math.round(beta / 18- 5)
this.cloud.style.transform = `translate(${X}px, ${Y}px)`
this.light.style.transform = `translate(${X * 3}px, ${Y * 3}px)`
this.heart.style.transform = `translate(${X * 4}px, ${Y * 4}px)`
this.dots.style.transform = `translate(${X * 6}px, ${Y * 6}px)`
}
到这一步的时候安卓手机上可以查看当前动画效果了。
3.函数节流
但是很明显的感觉到,动画感应非常的灵敏,肉眼可见动画元素随着手机的移动还会产生抖动的效果。所以还需要给deviceorientation 事件做个节流:
import _throttle from 'lodash/throttle'
componentDidMount() {
window.addEventListener('deviceorientation',_throttle(this.deviceorientationChange, 300,
{ leading: true, trailing: false }
), false)
}
4.动画过渡
再给所有动画的节点style加上transition:(如下所示)
&-cloud{
width: 187vw;
position: absolute;
top: -30vw;
left: -33vw;
z-index: 10;
transition: transform 0.5s linear;
}
5.运动边界考虑
这时动画效果就比较顺滑了,但是还有一个临界点的问题;比如gamma,手机转动至90度gamma的值会突变从90变成-90,或者-90到90。这时动画的demo会很明显的从动画范围最左边一下子闪至最右,体验感非常不好。所以需要控制运动范围,超过规定范围,我们的动画便不会再继续延伸了;
lastX = 0
lastY = 0
lastGamma = 0
lastbBeta = 0
deviceorientationChange=(event) => {
let { beta, gamma } = event;
beta = Math.round(beta)
gamma = Math.round(gamma)
let X = Math.round(gamma / 9)
let Y = Math.round(beta / 18- 5)
if (this.lastGamma < -50 || this.lastGamma > 50) {
X = this.lastX
}
if (this.lastbBeta < -50 || this.lastbBeta > 50) {
Y = this.lastY
}
this.cloud.style.transform = `translate(${X}px, ${Y}px)`
this.light.style.transform = `translate(${X * 3}px, ${Y * 3}px)`
this.heart.style.transform = `translate(${X * 4}px, ${Y * 4}px)`
this.dots.style.transform = `translate(${X * 6}px, ${Y * 6}px)`
this.lastX = X
this.lastY = Y
this.lastGamma = gamma
this.lastbBeta = beta
}
iOS上使用
到这里安卓手机上的动画效果就达预期了,但是iOS手机上页面不会给出任何动画迹象。 首先,本地调试localhost我们的协议都是http,iOS12.4以上版本则必须要在https协议下才能正常使用使用陀螺仪;这里有个小技巧,可以利用Charles的mapRemote来map https to http,这样就可以在https环境下调试。 ios 13系统调用陀螺仪 需要通过window.DeviceOrientationEvent.requestPermission申请用户权限,具体移步官网; 特别需要注意的是,requestPermission方法的”首次“调用 需要 由用户交互触发如click,touchend事件等。”首次“的含义,指的是用户 完全退出app(而不是 app切换到后台运行) 后再次打开为首次。getPermission在componentDidMount里直接调用是不会执行的。
getPermission=() => {
if (window.DeviceOrientationEvent && DeviceOrientationEvent.requestPermission) {
DeviceOrientationEvent.requestPermission()
}
}
欢迎指正!