Andoid 仿自如裸眼 3D 效果

2023-10-12 01:10
文章标签 3d 效果 裸眼 andoid 自如

本文主要是介绍Andoid 仿自如裸眼 3D 效果,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

前言

1.自如的思路分析探究

2.具体实现

2.1 实现效果

2.2 具体实现

2.3 使用步骤

3.补充说明

4.最后


前言

        前段时间自如技术团队发布了一篇名为《自如客APP裸眼3D效果的实现》的技术分享文章,简述了通过将图层分为前中后景,监听手机倾斜角度,再根据倾斜角度反向移动前后景,实现类似裸眼 3D 的效果。 该文章中已将思路与原理讲述清楚,抱着好奇心尝试仿现了一下。

1.自如的思路分析探究

1.1 自如 APP 上的裸眼 3D 效果

        UI 层面上:将普通的 2D 图像切割出 背景中景前景 三个部分

1.2 普通 2D 图像
1.3 切割出来的 背景中景前景

    ( “ 切割 ” 这部分操作当然是交给专业的 UI 同事来进行)

        技术层面上:通过 Android 中的 磁场传感器加速度传感器 监听设备的倾斜角度,保持 中景 不动,根据倾斜角度反向移动 背景前景 ,将 2D 图像转化为景深效果,呈现出类似裸眼 3D 的视觉效果。

image.png
1.4 根据倾斜角移动前后景(图片来自自如技术文章)

 

思路上就是这么清晰和简单,需求如下:

        根据设备倾斜角度 平稳移动 前后景,实现裸眼 3D 效果

        其中前后景在 Y 轴上的移动范围和速度均比 X 轴小和慢

2.具体实现

2.1 实现效果

2.1.1 仿现效果

2.2 具体实现

2.2.1 自定义 GravityRotationImageView  :

        1.继承于 ImageView ,内部实现 Scroller

        2.提供自定义属性 isBack 区分该 View 用作前景还是后景,前后景移动方向不同,且后景 ImageView 的填充应存在一定的放大倍数

    /*** 设置当前 view 为前景或后景* @param isBack true 后景 ; false 前景*/fun isBack(isBack: Boolean) {/*** 判断该 view 用作前景还是后景* 后景则需调整放大倍数使内容滚动时不会出现白边* 并根据前后景记录对应的滚动方向*/if (isBack) {mDirection = DIRECTION_BACKscaleType = ScaleType.CENTER_CROPscaleX = 1.1fscaleY = 1.2f} else {mDirection = DIRECTION_FRONT}}

        3.提供 handleSensorChangedValues 方法,该方法中根据得到的传感器数据计算倾斜角度,过滤抖动(角度变化过小/过大),并得到需要移动的距离,最后通过 Scroller 辅助移动

    /*** 处理传感器得到的数据,过滤后再根据倾斜角度移动当前 view* 旋转移动过程中,前景后景随旋转角度偏移*/internal fun handleSensorChangedValues(gravity: FloatArray,geomagnetic: FloatArray,maxMovingRange: Float = MOVING_RANGE_DEFAULT) {if (maxMovingRange != MOVING_RANGE_DEFAULT) {mMaxMovingRange = dip2px(this.context, maxMovingRange)}//旋转角度值集val orientationValues = FloatArray(3)//旋转矩阵val rotationMatrix = FloatArray(9)SensorManager.getRotationMatrix(rotationMatrix,null,gravity,geomagnetic)SensorManager.getOrientation(rotationMatrix, orientationValues)// z 轴的偏转角度orientationValues[0] = Math.toDegrees(orientationValues[0].toDouble()).toFloat()// x 轴的偏转角度orientationValues[1] = Math.toDegrees(orientationValues[1].toDouble()).toFloat()// y 轴的偏转角度orientationValues[2] = Math.toDegrees(orientationValues[2].toDouble()).toFloat()val newAngleX = orientationValues[1].toInt()val newAngleY = orientationValues[2].toInt()// x 、 y 轴角度变化值val rotationAngleXChangeValue = abs(newAngleX - rotationAngleX)val rotationAngleYChangeValue = abs(newAngleY - rotationAngleY)var targetX = mScroller.finalXvar targetY = mScroller.finalYif (rotationAngleYChangeValue in (RESPONSE_ANGLE_CHANGE_MIN + 1) until RESPONSE_ANGLE_CHANGE_MAX|| rotationAngleXChangeValue in (RESPONSE_ANGLE_CHANGE_MIN + 1) until RESPONSE_ANGLE_CHANGE_MAX) {if (newAngleX <= 0 && newAngleX > -MAX_ROTATION_ANGLE || newAngleX in 1 until MAX_ROTATION_ANGLE) {targetY = mMaxMovingRange * -mDirection * newAngleX / MAX_ROTATION_ANGLE_Y}if (newAngleY <= 0 && newAngleY > -MAX_ROTATION_ANGLE || newAngleY in 1 until MAX_ROTATION_ANGLE) {targetX = mMaxMovingRange * mDirection * newAngleY / MAX_ROTATION_ANGLE}val dx = targetX - scrollXval dy = targetY - scrollYsmoothScroll(dx, dy)//更新角度rotationAngleX = newAngleXrotationAngleY = newAngleY}}

2.2.2 自定义帮助类 GravityRotationHelper :

        1.构造方法中得到已实现 LifecycleOwner 的 context 对象,通过 Lifecycle 特性在 context 对象的相应生命周期中进行 加速度传感器 磁场传感器 的注册与反注册

    init {if (context is LifecycleOwner) {//获取传感器管理类实例mSensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager//加速度传感器实例val accelerationSensor = mSensorManager?.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)//磁场传感器val magneticSensor = mSensorManager?.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD)context.lifecycle.addObserver(object : LifecycleObserver {@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)fun onResume(@NotNull owner: LifecycleOwner) {//注册监听mSensorManager?.registerListener(mSensorEventListener,accelerationSensor,SensorManager.SENSOR_DELAY_GAME)mSensorManager?.registerListener(mSensorEventListener,magneticSensor,SensorManager.SENSOR_DELAY_GAME)}@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)fun onPause(@NotNull owner: LifecycleOwner) {mSensorManager?.unregisterListener(mSensorEventListener)}})} else {Log.e("GravityRotationHelper","GravityRotationHelper init error : context is LifecycleOwner = false ")}}

        2.提供 attachViews 方法,得到外部需要实现裸眼 3D 效果的前景与后景 View ,旧持有前景后景 View 不为空时,记录并重置对应 scroll 值

    /*** 添加需要实现裸眼 3D 效果的视图组* 旋转移动过程中,前景后景随旋转角度偏移* @param frontView 前景* @param backView 后景* @param maxMovingRange 最大可移动范围 dp*/fun attachViews(frontView: GravityRotationImageView,backView: GravityRotationImageView,maxMovingRange: Float = MOVING_RANGE_DEFAULT) {//旧持有前景后景 View 不为空时,记录并重置对应 scroll 值val oldFrontViewScrollX = mFrontView?.scrollX ?: 0val oldFrontViewScrollY = mFrontView?.scrollY ?: 0val oldBackViewScrollX = mBackView?.scrollX ?: 0val oldBackViewScrollY = mBackView?.scrollY ?: 0val oldRotationAngleX = mFrontView?.rotationAngleX ?: 0val oldRotationAngleY = mFrontView?.rotationAngleY ?: 0mFrontView = frontViewmBackView = backViewmFrontView?.rotationAngleX = oldRotationAngleXmFrontView?.rotationAngleY = oldRotationAngleYmBackView?.rotationAngleX = oldRotationAngleXmBackView?.rotationAngleY = oldRotationAngleY//继承上一组前景后景 View 的 scroll 值mFrontView?.scrollTo(oldFrontViewScrollX, oldFrontViewScrollY)mBackView?.scrollTo(oldBackViewScrollX, oldBackViewScrollY)mMaxMovingRange = maxMovingRange}

        3.传感器数值变化时调用前后景 View 的 handleSensorChangedValues 方法进行移动

    private var mSensorEventListener = object : SensorEventListener {override fun onSensorChanged(event: SensorEvent) {when (event.sensor.type) {Sensor.TYPE_ACCELEROMETER -> {//加速度mAccelerationValues = event.valueshandleAccelerometerAndMagneticData()}Sensor.TYPE_MAGNETIC_FIELD -> {//磁场mMagneticValues = event.valueshandleAccelerometerAndMagneticData()}}}override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}}private fun handleAccelerometerAndMagneticData() {if (mAccelerationValues != null && mMagneticValues != null) {if (mFrontView != null && mBackView !== null) {mFrontView?.handleSensorChangedValues(mAccelerationValues!!,mMagneticValues!!,mMaxMovingRange)mBackView?.handleSensorChangedValues(mAccelerationValues!!,mMagneticValues!!,mMaxMovingRange)}}}

2.3 使用步骤

1.复制 Demo 中的 GravityRotationHelper 和 GravityRotationImageView 以及自定义属性 attrs 到项目中

2.布局中使用 GravityRotationImageView 作为需要实现 3D 效果的前景与后景 View

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"android:layout_width="match_parent"android:layout_height="match_parent"android:clipChildren="false"><com.ziwenl.library.GravityRotationImageViewandroid:id="@+id/iv_back"android:layout_width="match_parent"android:layout_height="250dp"android:paddingBottom="40dp"android:src="@mipmap/banner_a_back"app:isBack="true"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent" /><FrameLayoutandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginBottom="20dp"android:clipChildren="false"app:layout_constraintBottom_toBottomOf="@+id/iv_back"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"><ImageViewandroid:id="@+id/iv_middle"android:layout_width="wrap_content"android:layout_height="wrap_content"android:src="@mipmap/banner_a_middle" /><com.ziwenl.library.GravityRotationImageViewandroid:id="@+id/iv_front"android:layout_width="wrap_content"android:layout_height="wrap_content"android:src="@mipmap/banner_a_front" /></FrameLayout></androidx.constraintlayout.widget.ConstraintLayout>

( ps : 可按需给父 View 设置 android:clipChildren="false" 属性,控制前景移动到边界时是否裁剪 ) 

3.使用帮助类 GravityRotationHelper 绑定前景和后景 View 实现目标效果

    override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)val viewBinding = ActivitySinglepageBinding.inflate(layoutInflater)setContentView(viewBinding.root)GravityRotationHelper(this).attachViews(viewBinding.ivFront, viewBinding.ivBack)}

( ps:关于在 banner 中实现该效果,可参考 demo 中的 BannerActivity 类 )

3.补充说明

  • 提取成帮助类而不是在自定义 View 中进行传感器的创建与注册监听,主要是为了减少耦合及资源开销
  • 自定义 ImageView 是为了使用 Scroller 来进行辅助滚动,如果只是在 View 外部通过监听设备倾斜角再通过 View 的 scroll 方法进行移动,会出现抖动及跳动问题
  • 除了使用 磁场传感器加速度传感器 来感知设备倾斜角度变化,还能使用 陀螺仪传感器 来感知设备的倾斜角度变化,同样能实现目标效果
    private val NS2S = 1.0f / 1000000000.0fprivate var timestamp = 0fprivate fun init(context: Context){val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as? SensorManagerval gyroscopeSensor = sensorManager?.getDefaultSensor(Sensor.TYPE_GYROSCOPE)sensorManager?.registerListener(object : SensorEventListener {override fun onSensorChanged(event: SensorEvent) {if (event.sensor.type == Sensor.TYPE_GYROSCOPE) {if (timestamp != 0f) {val dT = (event.timestamp - timestamp) * NS2Sangle[0] += event.values[0] * dTangle[1] += event.values[1] * dTval angleY = Math.toDegrees(angle[0].toDouble()).toFloat()val angleX = Math.toDegrees(angle[1].toDouble()).toFloat()//TODO}timestamp = event.timestamp.toFloat()}}override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}}, gyroscopeSensor, SENSOR_DELAY_GAME)}

4.最后

        关于该伪裸眼 3D 效果,自自如团队发布技术文章之后,网上也有一系列 Demo 及技术文章,本人在实现过程中遇到了抖动和跳动问题(主要由于传感器数值变化过于敏感及频繁导致),曾去下载一些 Demo 进行参考,发现同样是存在该问题。其中有篇文章是通过 陀螺仪传感器 来实现该效果的,也做了抖动过滤,但在小米 6 上运行时发现会出现卡顿效果,所以最后还是自己调整优化避免了该现象的出现。

        最后感谢 自如大前端团队 的实现方案分享,通过新颖取巧的方式,加强了用户的 UI 体验。而自如的技术文章更着重于分享思路,所以在此基础上进行实现与优化,也是一种不可多得的乐趣。

  • 源码及 Demo 地址:GitHub - ziwenL/GravityRotationTo3D
  • 如有更好的见解或建议,欢迎留言

这篇关于Andoid 仿自如裸眼 3D 效果的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/192157

相关文章

无人叉车3d激光slam多房间建图定位异常处理方案-墙体画线地图切分方案

墙体画线地图切分方案 针对问题:墙体两侧特征混淆误匹配,导致建图和定位偏差,表现为过门跳变、外月台走歪等 ·解决思路:预期的根治方案IGICP需要较长时间完成上线,先使用切分地图的工程化方案,即墙体两侧切分为不同地图,在某一侧只使用该侧地图进行定位 方案思路 切分原理:切分地图基于关键帧位置,而非点云。 理论基础:光照是直线的,一帧点云必定只能照射到墙的一侧,无法同时照到两侧实践考虑:关

防近视护眼台灯什么牌子好?五款防近视效果好的护眼台灯推荐

在家里,灯具是属于离不开的家具,每个大大小小的地方都需要的照亮,所以一盏好灯是必不可少的,每个发挥着作用。而护眼台灯就起了一个保护眼睛,预防近视的作用。可以保护我们在学习,阅读的时候提供一个合适的光线环境,保护我们的眼睛。防近视护眼台灯什么牌子好?那我们怎么选择一个优秀的护眼台灯也是很重要,才能起到最大的护眼效果。下面五款防近视效果好的护眼台灯推荐: 一:六个推荐防近视效果好的护眼台灯的

MiniGPT-3D, 首个高效的3D点云大语言模型,仅需一张RTX3090显卡,训练一天时间,已开源

项目主页:https://tangyuan96.github.io/minigpt_3d_project_page/ 代码:https://github.com/TangYuan96/MiniGPT-3D 论文:https://arxiv.org/pdf/2405.01413 MiniGPT-3D在多个任务上取得了SoTA,被ACM MM2024接收,只拥有47.8M的可训练参数,在一张RTX

SAM2POINT:以zero-shot且快速的方式将任何 3D 视频分割为视频

摘要 我们介绍 SAM2POINT,这是一种采用 Segment Anything Model 2 (SAM 2) 进行零样本和快速 3D 分割的初步探索。 SAM2POINT 将任何 3D 数据解释为一系列多向视频,并利用 SAM 2 进行 3D 空间分割,无需进一步训练或 2D-3D 投影。 我们的框架支持各种提示类型,包括 3D 点、框和掩模,并且可以泛化到不同的场景,例如 3D 对象、室

【Godot4.3】多边形的斜线填充效果基础实现

概述 图案(Pattern)填充是一个非常常见的效果。其中又以斜线填充最为简单。本篇就探讨在Godot4.3中如何使用Geometry2D和CanvasItem的绘图函数实现斜线填充效果。 基础思路 Geometry2D类提供了多边形和多边形以及多边形与折线的布尔运算。按照自然的思路,多边形的斜线填充应该属于“多边形与折线的布尔运算”范畴。 第一个问题是如何获得斜线,这条斜线应该满足什么样

模具要不要建设3D打印中心

随着3D打印技术的日益成熟与广泛应用,模具企业迎来了自建3D打印中心的热潮。这一举措不仅为企业带来了前所未有的发展机遇,同时也伴随着一系列需要克服的挑战,如何看待企业引进增材制造,小编为您全面分析。 机遇篇: 加速产品创新:3D打印技术如同一把钥匙,为模具企业解锁了快速迭代产品设计的可能。企业能够迅速将创意转化为实体模型,缩短产品从设计到市场的周期,抢占市场先机。 强化定制化服务:面

UniApp实现漂亮的音乐歌词滚动播放效果

在现代的音乐播放应用中,歌词的展示和滚动播放已经成为了一个非常常见的功能。今天,我们将通过UniApp来实现一个漂亮的歌词滚动播放功能。我们将使用UniApp提供的组件和API来完成这个任务。 页面结构 在页面的模板部分,我们需要创建一个音频播放器和歌词展示区域。使用<scroll-view>组件来实现歌词的滚动效果。 <template><view class="audio-co

Nuxt3入门:过渡效果(第5节)

你好同学,我是沐爸,欢迎点赞、收藏、评论和关注。 Nuxt 利用 Vue 的 <Transition> 组件在页面和布局之间应用过渡效果。 一、页面过渡效果 你可以启用页面过渡效果,以便对所有页面应用自动过渡效果。 nuxt.config.js export default defineNuxtConfig({app: {pageTransition: {name: 'fade',mode

WPF入门到跪下 第十三章 3D绘图 - 3D绘图基础

3D绘图基础 四大要点 WPF中的3D绘图涉及4个要点: 视口,用来驻留3D内容3D对象照亮部分或整个3D场景的光源摄像机,提供在3D场景中进行观察的视点 一、视口 要展示3D内容,首先需要一个容器来装载3D内容。在WPF中,这个容器就是Viewport3D(3D视口),它继承自FrameworkElement,因此可以像其他元素那样在XAML中使用。 Viewport3D与其他元素相