code小生,一个专注 Android 领域的技术分享平台
作者:r17171709
地址:https://www.jianshu.com/p/206713510003
声明:本文来自 r17171709 投稿,转发等请联系原作者授权
今年地产行业兴起VR看房这种模式以提升购房人在带看过程中的体验。近期没有打算购房的朋友可能不知道什么叫VR看房,这里简单做个普及:
这个技术主要有三项核心功能:VR看房、VR讲房、VR带看
VR看房是一种沉浸式看房体验,购房人点击APP上的VR房源,轻触屏幕任意处即可获得包括房屋真实空间的尺寸、朝向、远近等深度信息
VR讲房是在VR看房的基础上,增加语音讲解服务功能。房产经纪人会将房屋最为核心的优势、特色还有一些不足进行主动讲解并录入到系统中。这些录音会随着购房人浏览房源时到达相应位置而进行播放。这个感觉就类似于参观博物馆时配发的讲解器一样,每到一个地点就自动触发语音播放
VR带看则是一种全新的交互场景体验,打破了传统线上看房的固定限制。与伪全息或3D看房不同的是,VR带看可以随意调整自己的位置和视角,获得不同视觉和看房体验,以求达到用户最大的满意度。购房人可与经纪人提前预约看房时间,并实时连线进行交互,改变了传统线上看房“异步传输”的现状(即拍摄后完全没有任何同步互动和反馈)
以上只是一个新概念的普及。本文不会教你如何实现VR看房的功能,因为,我也不会。。。那能教你什么呢?使用陀螺仪移动图片Drawable。打开贝壳找房App就能看到文中所示的效果。
VR看房入口效果
同为地产企业,最近有同事也在开发这个功能,我也顺带学习下这个效果是如何实现的。两年多前已经有大神在github上分享了利用陀螺仪让ImageView自动滚动的功能,功能相对比较单一,只能横或纵方向滚动,链接地址为PanoramaImageView。随后偶然看到贝壳的小伙也基于那个项目对其进行功能扩展,实现了多方向自动滚动的功能,链接地址为GyroscopeImageDemo。两个项目都很优秀,那我们就跟随大神们的脚步,来一步一步分析这个功能是如何实现的吧
本文涉及到的代码都在github上,欢迎star、fork
https://github.com/r17171709/android_demo/tree/master/GyroscopeImageDemo
Android系统内置很多传感器组件,陀螺仪就是其中一个。陀螺仪的轴由于陀螺效应始终与初始方向平行,这样就可以通过与初始方向的偏差计算来出实际方向。角度需要角速度与时间积分计算,得到的角度变化量与初始角度相加,就得到目标角度,其中积分时间Dt越小,输出角度越准。
看起来好像很复杂,但实际上到Android Api已经为你封装的好好的。你只要继承SensorEventListener,并将其绑定在SensorManager上,即可让你妥妥的可以实现角度计算功能
SensorManager sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
val sensor = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)
sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_GAME)
精度与耗电量成正比,所以我们也要选择适合的传感器精度才行。内置四种精度分别为
SensorManager.SENSOR_DELAY_FASTEST(0微秒):最快。最低延迟,一般不是特别敏感的处理不推荐使用,该模式可能在成手机电力大量消耗,由于传递的为原始数据,诉法不处理好会影响游戏逻辑和UI的性能
SensorManager.SENSOR_DELAY_GAME(20000微秒):游戏。游戏延迟,一般绝大多数的实时性较高的游戏都是用该级别
SensorManager.SENSOR_DELAY_NORMAL(200000微秒):普通。标准延时,对于一般的益智类或EASY级别的游戏可以使用,但过低的采样率可能对一些赛车类游戏有跳帧现象
SensorManager.SENSOR_DELAY_UI(60000微秒):用户界面。一般对于屏幕方向自动旋转使用,相对节省电能和逻辑处理,一般游戏开发中不使用
最后就是监听回调。通过两次检测到手机旋转的时间差进行角度累加计算
class GyroscopeAngel : SensorEventListener {
private var timestamp = 0.toLong()
private val NS2S = 1.0f / 1000000000.0f
private val angle = longArrayOf(0.toLong(), 0.toLong(), 0.toLong())
override fun onAccuracyChanged(p0: Sensor?, p1: Int) {
}
override fun onSensorChanged(p0: SensorEvent?) {
if (p0?.sensor?.type == Sensor.TYPE_GYROSCOPE) {
if (timestamp == 0L) {
// 从 x、y、z 轴的正向位置观看处于原始方位的设备,如果设备逆时针旋转,将会收到正值;否则,为负值
timestamp = p0.timestamp
return
}
// 得到两次检测到手机旋转的时间差(纳秒),并将其转化为秒
val dT = (p0.timestamp -timestamp) * NS2S
// 将手机在各个轴上的旋转角度相加,即可得到当前位置相对于初始位置的旋转弧度
angle[0] += (p0.values[0] * dT).toLong()
angle[1] += (p0.values[1] * dT).toLong()
angle[2] += (p0.values[2] * dT).toLong()
// 将弧度转化为角度
val anglex = Math.toDegrees(angle[0].toDouble()).toFloat()
val angley = Math.toDegrees(angle[1].toDouble()).toFloat()
val anglez = Math.toDegrees(angle[2].toDouble()).toFloat()
timestamp = p0.timestamp
}
}
}
以上代码就是我们今天传感器部分代码的原型,我们在此基础上将它与ImageView关联起来
Drawable在ImageView怎么移动呢?其实很简单,就在onDraw的时候直接移动canvas即可
canvas?.save()
canvas?.translate(currentOffsetX, currentOffsetY)
super.onDraw(canvas)
canvas?.restore()
这里面还有一个别别窍的地方。我们要注意一下ScaleType。ImageView的ScaleType类别很多,但是只有ScaleType.Center是以原图的几何中心点和ImageView的几何中心点为基准,按图片原来的尺寸居中显示并不剪裁。这样的话canvas在移动过程中,之前未显示部分的图片才能被展现出来。如果你选择ScaleType.CENTER_CROP,那多余的部分被剪裁掉,移动过程中就是背景色来占位了
当然这个移动也要有范围约束的,不是随便移的。这里移动范围就是超出控件可视区域部分图片的长与宽
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
if (drawable != null) {
mWidth = MeasureSpec.getSize(widthMeasureSpec) - paddingLeft - paddingRight
mHeight = MeasureSpec.getSize(heightMeasureSpec) - paddingTop - paddingBottom
mDrawableWidth = drawable.intrinsicWidth
mDrawableHeight = drawable.intrinsicHeight
mMaxOffsetX = Math.abs((mDrawableWidth - mWidth) * 0.5f)
mMaxOffsetY = Math.abs((mDrawableHeight - mHeight) * 0.5f)
}
}
我们通过接口,将传感器回调过来的X轴Y轴角度比例传到ImageView中,对视图进行刷新
interface GyroscopeImpl {
fun updateProgress(progressX: Float, progressY: Float)
}
override fun updateProgress(progressX: Float, progressY: Float) {
this.progressX = progressX
this.progressY = progressY
invalidate()
}
最后不要忘记在视图创建和销毁的时候将其在传感器类中进行添加和删除,以免浪费资源
刚才的预备知识里面已经完成我们大部分的工作了,下面我们就来将这个偏移范围调整一下即可
val dt = (p0.timestamp - mLastTimestamp) * NS2S * 2.0f
views.forEach {
it.mRotateRadianY += p0.values[1] * dt
it.mRotateRadianX += p0.values[0] * dt
if (it.mRotateRadianY > mMaxRotateRadian) {
it.mRotateRadianY = mMaxRotateRadian.toFloat()
}
else if (it.mRotateRadianY < -mMaxRotateRadian) {
it.mRotateRadianY = -mMaxRotateRadian.toFloat()
}
if (it.mRotateRadianX > mMaxRotateRadian) {
it.mRotateRadianX = mMaxRotateRadian.toFloat()
}
else if (it.mRotateRadianX < -mMaxRotateRadian) {
it.mRotateRadianX = -mMaxRotateRadian.toFloat()
}
// 注意此处,X与Y方向是反过来的
it.updateProgress((it.mRotateRadianY / mMaxRotateRadian).toFloat(), (it.mRotateRadianX / mMaxRotateRadian).toFloat())
}
最大弧度区间为(0, π/2],自己转转手机就知道了,移动距离可以自行修改
最后将偏移的比例传到ImageView中,进行刷新视图即可
网络加载图片框架
框架很多,对于一般情况下的使用都问题不大,但在这个场景下坑还是蛮多的。首先是Fresco,这玩意的ScaleType.Center与ImageView的不同,图片被剪裁了,玩不了。Glide4也不行,但是Glide3是可以的。Picasso没问题
RecyclerView上的使用
在RecyclerView上使用是没有问题的,如果你想横向当Banner展示而不用ViewPager的话,建议你使用DiscreteScrollView,它将RecyclerView包装成ViewPager,并且提供诸如scrollToPosition、smoothScrollToPosition、getCurrentItem这种ViewPager中的方法
这是无限循环banner的效果
images.add("http://wx2.sinaimg.cn/large/6e9ad2bdly1fnih8uqgkuj2140140b2b.jpg")
images.add("http://vrlab-public.ljcdn.com//release//vradmin//1000000020129136//images//FF41C450.png")
images.add("http://wx2.sinaimg.cn/large/6e9ad2bdly1fnih8s6on4j21401401kz.jpg")
images.add("http://wx2.sinaimg.cn/large/6e9ad2bdly1fnih8uqgkuj2140140b2b.jpg")
images.add("http://vrlab-public.ljcdn.com//release//vradmin//1000000020129136//images//FF41C450.png")
rv_image.setHasFixedSize(true)
rv_image.scrollToPosition(1)
rv_image.addOnItemChangedListener { _, adapterPosition ->
if (adapterPosition == 0) {
rv_image.scrollToPosition(3)
}
if (adapterPosition == 4) {
rv_image.scrollToPosition(1)
}
}
val adapter = MainAdapter(requireContext(), images)
rv_image.adapter = adapter
来看看我们demo的效果
demo