AR 眼镜之-拍照/录像动效切换-实现方案
目录
📂 前言
AR 眼镜系统版本
拍照/录像动效切换
1. 🔱 技术方案
1.1 技术方案概述
1.2 实现方案
1)第一阶段动效
2)第二阶段动效
2. 💠 默认代码配置
2.1 XML 初始布局
2.2 监听滑动对 View 改变
3. ⚛️ 拍照/录像动效切换实现
3.1 第一阶段动效
1)左移右边部分的 View
2)放大右边部分的 View
3.2 第二阶段动效
1)动态调整右边部分的约束
2)缩小右边部分的 View
3)从左往右移动左边部分
4)从 0 到 1 透明度增加左边部分
5)动画集实现
6)还原默认约束
4. ✅ 小结
附录1:动效帮助类代码
📂 前言
AR 眼镜系统版本
W517 Android9。
拍照/录像动效切换

实现效果如上 GIF 的左下角所示,我们看到主要分为:两部分、两阶段。
两部分:左边部分为 Normal 状态 View,右边部分为带有文字描述的 View。
两阶段:右边部分,分为变大阶段、缩小阶段;在右边部分的第二缩小阶段时,会触发左边部分的从左往右移动阶段、从 0 到 1 透明度增加阶段。
1. 🔱 技术方案
1.1 技术方案概述
拍照/录像动效切换主要使用属性动画完成,同时对于放大和缩小的参考方向不同,所以需要动态调整约束,动态调整约束时还需注意 maigin 值,因为文字改变尺寸也会变化。
1.2 实现方案
1)第一阶段动效
-
左移右边部分的 View;
-
放大右边部分的 View。
2)第二阶段动效
-
动态调整右边部分的约束;
-
缩小右边部分的 View;
-
从左往右移动左边部分;
-
从 0 到 1 透明度增加左边部分。
2. 💠 默认代码配置
2.1 XML 初始布局
norIcon 是左边部分 View,focLayout 是右边部分 View。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:id="@+id/parent"android:layout_width="match_parent"android:layout_height="match_parent"android:keepScreenOn="true"><ImageViewandroid:id="@+id/norIcon"android:layout_width="80dp"android:layout_height="104dp"android:layout_marginStart="24dp"android:layout_marginBottom="24dp"android:background="@drawable/shape_34343a_corner_20dp"android:contentDescription="@null"android:paddingHorizontal="24dp"android:paddingVertical="36dp"android:src="@drawable/ic_camera_video_nor"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintStart_toStartOf="parent" /><LinearLayoutandroid:id="@+id/focLayout"android:layout_width="wrap_content"android:layout_height="104dp"android:layout_marginStart="110dp"android:background="@drawable/shape_34343a_corner_20dp"android:gravity="center"android:minWidth="200dp"android:orientation="vertical"android:paddingStart="12dp"android:paddingEnd="16dp"app:layout_constraintBottom_toBottomOf="@id/norIcon"app:layout_constraintStart_toStartOf="parent"><ImageViewandroid:id="@+id/focIcon"android:layout_width="32dp"android:layout_height="32dp"android:contentDescription="@null"android:src="@drawable/ic_camera_picture_foc" /><com.agg.ui.AGGTextViewandroid:id="@+id/focText"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginTop="6dp"android:gravity="center"android:singleLine="true"android:text="@string/tap_to_photo"android:textColor="#FCC810"android:textSize="24sp"app:UITypeface="Bold" /></LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
2.2 监听滑动对 View 改变
/*** 往前滑动:切换为录像模式/拍照模式*/override fun scrollForward() {if (AnimatorSwitchHelper.isAnimating) {Log.e(TAG, "scrollForward: 滑动过快")return}Log.i(TAG, "scrollForward: model=$mIsVideoModel,isRecordingVideo=${isRecording()}")if (mIsVideoModel) {if (isRecording()) stopRecord()switchToPhoto()mIsVideoModel = falsebinding.tips.text = getString(R.string.swipe_forward_to_video_model)binding.norIcon.setImageResource(R.drawable.ic_camera_video_nor)binding.focIcon.setImageResource(R.drawable.ic_camera_picture_foc)binding.focText.text = getString(R.string.tap_to_photo)} else {switchToVideo()mIsVideoModel = truebinding.tips.text = getString(R.string.swipe_forward_to_photo_model)binding.norIcon.setImageResource(R.drawable.ic_camera_picture_nor)binding.focIcon.setImageResource(R.drawable.ic_camera_video_foc)binding.focText.text = getString(R.string.tap_to_record)}binding.tips.visibility = VISIBLEAnimatorSwitchHelper.startAnimator(binding)}
3. ⚛️ 拍照/录像动效切换实现
3.1 第一阶段动效
1)左移右边部分的 View
binding.focLayout.x = binding.focLayout.x - 86
2)放大右边部分的 View
val defWidth = binding.focLayout.width
val focBgBigAnim = ValueAnimator.ofInt(defWidth, defWidth + 86).apply {addUpdateListener { animation ->val width = animation.animatedValue as Intval layoutParams = binding.focLayout.layoutParams as ViewGroup.LayoutParamslayoutParams.width = widthbinding.focLayout.layoutParams = layoutParams}}
3.2 第二阶段动效
1)动态调整右边部分的约束
第一阶段在 XML 中默认配置的是 layout_constraintStart_toStartOf="parent",能保证放大时以左边为锚点从左往右放大;而第二阶段缩小时需要以右边为锚点,此时需要动态改变约束如下:
private fun changeConstraint(binding: ActivityMainBinding) {Log.i(TAG, "changeConstraint: ")val focLayoutId = R.id.focLayoutval constraintLayout = binding.parentConstraintSet().apply {// 修改约束clone(constraintLayout)// 清除原有的约束clear(focLayoutId, ConstraintSet.START)// 设置新的约束connect(focLayoutId,ConstraintSet.END,ConstraintSet.PARENT_ID,ConstraintSet.END,(binding.focLayout.context.resources.displayMetrics.widthPixels - binding.focLayout.x - binding.focLayout.width - 86).toInt())// 自动播放过渡动画——取消播放,与自定义动画重复
// TransitionManager.beginDelayedTransition(constraintLayout)// 应用新的约束applyTo(constraintLayout)}
}
2)缩小右边部分的 View
val focBgSmallAnim = ValueAnimator.ofInt(defWidth + 86, defWidth).apply {addUpdateListener { animation ->val width = animation.animatedValue as Intval layoutParams = binding.focLayout.layoutParams as ViewGroup.LayoutParamslayoutParams.width = widthbinding.focLayout.layoutParams = layoutParams}addListener(object : AnimatorListenerAdapter() {override fun onAnimationEnd(p0: Animator) {Log.i(TAG, "onAnimationEnd: focBgSmallAnim")isAnimating = false}})
}
3)从左往右移动左边部分
val norBgTransAnim = ObjectAnimator.ofFloat(binding.norIcon, "translationX", -80f, 0f)
4)从 0 到 1 透明度增加左边部分
val norBgAlphaAnim = ObjectAnimator.ofFloat(binding.norIcon, "alpha", 0f, 1f)
5)动画集实现
AnimatorSet().apply {playSequentially(focBgBigAnim, focBgSmallAnim)playTogether(focBgSmallAnim, norBgTransAnim, norBgAlphaAnim)duration = 1000start()
}
6)还原默认约束
动效做完后需要还原默认约束,保证下次动效的正常进行。
if (!isFirstSwitch) restoreConstraint(binding)private fun restoreConstraint(binding: ActivityMainBinding) {Log.i(TAG, "restoreConstraint: ")val focLayoutId = R.id.focLayoutval constraintLayout = binding.parentConstraintSet().apply {clone(constraintLayout)clear(focLayoutId, ConstraintSet.END)connect(focLayoutId, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START, 110)applyTo(constraintLayout)}
}
具体动效类的代码,参考附录1。
4. ✅ 小结
对于拍照/录像动效切换,本文只是一个基础实现方案,更多业务细节请参考产品逻辑去实现。
另外,由于本人能力有限,如有错误,敬请批评指正,谢谢。
附录1:动效帮助类代码
object AnimatorSwitchHelper {private val TAG = AnimatorSwitchHelper::class.java.simpleNamevar isAnimating = falsevar isFirstSwitch = truefun startAnimator(binding: ActivityMainBinding) {Log.i(TAG, "startAnimator: isAnimating=$isAnimating,isFirstSwitch=$isFirstSwitch")isAnimating = trueval defWidth = binding.focLayout.widthif (!isFirstSwitch) restoreConstraint(binding)if (isFirstSwitch) binding.focLayout.x = binding.focLayout.x - 86// 1. 放大Foc的Viewval focBgBigAnim = ValueAnimator.ofInt(defWidth, defWidth + 86).apply {addUpdateListener { animation ->val width = animation.animatedValue as Intval layoutParams = binding.focLayout.layoutParams as ViewGroup.LayoutParamslayoutParams.width = widthbinding.focLayout.layoutParams = layoutParams}addListener(object : AnimatorListenerAdapter() {override fun onAnimationEnd(p0: Animator) {Log.i(TAG, "onAnimationEnd: focBgBigAnim")// 为绘制反向动画,需修改约束方向changeConstraint(binding)isFirstSwitch = false}})}// 2.1 缩小Foc的Viewval focBgSmallAnim = ValueAnimator.ofInt(defWidth + 86, defWidth).apply {addUpdateListener { animation ->val width = animation.animatedValue as Intval layoutParams = binding.focLayout.layoutParams as ViewGroup.LayoutParamslayoutParams.width = widthbinding.focLayout.layoutParams = layoutParams}addListener(object : AnimatorListenerAdapter() {override fun onAnimationEnd(p0: Animator) {Log.i(TAG, "onAnimationEnd: focBgSmallAnim")isAnimating = false}})}// 2.2 从左往右移动Nor的Viewval norBgTransAnim = ObjectAnimator.ofFloat(binding.norIcon, "translationX", -80f, 0f)// 2.3 透明度渐显Nor的Viewval norBgAlphaAnim = ObjectAnimator.ofFloat(binding.norIcon, "alpha", 0f, 1f)AnimatorSet().apply {playSequentially(focBgBigAnim, focBgSmallAnim)playTogether(focBgSmallAnim, norBgTransAnim, norBgAlphaAnim)duration = 1000start()}}private fun changeConstraint(binding: ActivityMainBinding) {Log.i(TAG, "changeConstraint: ")val focLayoutId = R.id.focLayoutval constraintLayout = binding.parentConstraintSet().apply {// 修改约束clone(constraintLayout)// 清除原有的约束clear(focLayoutId, ConstraintSet.START)// 设置新的约束connect(focLayoutId,ConstraintSet.END,ConstraintSet.PARENT_ID,ConstraintSet.END,(binding.focLayout.context.resources.displayMetrics.widthPixels - binding.focLayout.x - binding.focLayout.width - 86).toInt())// 自动播放过渡动画——取消播放,与自定义动画重复
// TransitionManager.beginDelayedTransition(constraintLayout)// 应用新的约束applyTo(constraintLayout)}}private fun restoreConstraint(binding: ActivityMainBinding) {Log.i(TAG, "restoreConstraint: ")val focLayoutId = R.id.focLayoutval constraintLayout = binding.parentConstraintSet().apply {clone(constraintLayout)clear(focLayoutId, ConstraintSet.END)connect(focLayoutId, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START, 110)applyTo(constraintLayout)}}}
相关文章:
AR 眼镜之-拍照/录像动效切换-实现方案
目录 📂 前言 AR 眼镜系统版本 拍照/录像动效切换 1. 🔱 技术方案 1.1 技术方案概述 1.2 实现方案 1)第一阶段动效 2)第二阶段动效 2. 💠 默认代码配置 2.1 XML 初始布局 2.2 监听滑动对 View 改变 3. ⚛️…...
2025年中科院分区大类划分公布!新增8155本
2025年中科院分区表变更情况 扩大收录范围 2025年的期刊分区表在原有的自然科学(SCIE)、社会科学(SSCI)和人文科学(AHCI)的基础上,增加了ESCI期刊的收录,并根据这些期刊的数据进行…...
S变换matlab实现
S变换函数 function [st,t,f] st(timeseries,minfreq,maxfreq,samplingrate,freqsamplingrate) % S变换 % Code by huasir Beijing 2025.1.10 % Reference is "Localization of the Complex Spectrum: The S Transform" % from IEEE Transactions on Signal Proc…...
Springboot——钉钉(站内)实现登录第三方应用
文章目录 前言准备1、创建钉钉应用,并开放网页应用2、配置网页应用各项参数发布版本 前端改造后端逻辑1、获取应用免登录 Access_token2、通过免登录 Access_token 和 Auth_Code 获取对应登录人信息 注意事项 前言 PC端的钉钉中工作台,增加第三方应用&a…...
基于深度学习算法的AI图像视觉检测
基于人工智能和深度学习方法的现代计算机视觉技术在过去10年里取得了显著进展。如今,它被广泛用于图像分类、人脸识别、图像中物体的识别等。那么什么是深度学习?深度学习是如何应用在视觉检测上的呢? 什么是深度学习? 深度学习是…...
cJson——序列化格式json和protobuf对比
cJson——序列化格式json和protobuf对比 1. 更小的消息体积2. 更快的序列化与反序列化速度3. 类型安全4. 向后和向前兼容性5. 更低的带宽消耗6. 高效的编码方式7. 易于跨语言支持8. 支持复杂的数据结构9. 更好的支持大型数据交换总结 Protocol Buffers (Protobuf) 和 JSON 都是…...
搭建一个fastapi的项目,调用ollama服务
1. 项目结构 my_project/ │ ├── app/ │ ├── main.py # FastAPI应用的入口 │ ├── services/ # 包含服务逻辑 │ │ └── ollama_service.py │ ├── models/ # 定义数据模型 │ │ └── response.py │ ├─…...
Wireshark编译手册(Windows)
以下是对 Wireshark 官方文档中“Windows 平台的设置和构建说明”部分的翻译和总结: 2.2. Windows 平台 本节提供了在 Windows 上进行 Wireshark 开发的快速设置指南,包含推荐的配置。 2.2.1. 使用 Microsoft Visual Studio 注意:除非您非…...
在高德地图上加载3DTilesLayer图层模型/天地瓦片
1. 引入必要的库 Three.js:一个用于创建和显示3D图形的JavaScript库。vuemap/three-layer:一个Vue插件,它允许你在高德地图中添加Three.js图层。vuemap/layer-3dtiles:一个用于处理3D Tiles格式数据的Vue插件,可以用来…...
深入浅出负载均衡:理解其原理并选择最适合你的实现方式
负载均衡是一种在多个计算资源(如服务器、CPU核心、网络链接等)之间分配工作负载的技术,旨在优化资源利用率、提高系统吞吐量和降低响应时间。负载均衡的实现方式多种多样,以下是几种常见的实现方式: 1. 硬件负载均衡&…...
STM32的存储结构
STM32F103 芯片是基于 ARM Cortex-M3 内核的微控制器,它集成了多种类型的存储器,每种存储器都有其特定的作用和存储对象。以下是关于 STM32F103 中 Flash、ROM 和 SRAM 的详细介绍: 1. Flash Memory (闪存) 作用:Flash 是非易失性…...
@SneakyThrows 注解详解
SneakyThrows 注解详解 1. 基本介绍 SneakyThrows 是 Lombok 提供的注解,用于简化异常处理,自动生成 try-catch 代码块,将检查型异常转换为非检查型异常。 2. 使用对比 2.1 传统写法 public String readFile(String path) {try {return …...
js监测页面可见性
监测切换页面 检测页面的可见性状态document.visibilityState:document.hiddenvisibilitychange 事件 js 检测页面切换至别的应用 检测页面的可见性状态 在JavaScript中,你可以使用Page Visibility API来检测页面的可见性状态。这个API提供了一组接口,允…...
Android wifi常见问题及分析
参考 Android Network/WiFi 那些事儿 前言 本文将讨论几个有意思的网络问题,同时介绍 Android 上常见WiFi 问题的分析思路。 网络基础Q & A 一. 网络分层缘由 分层想必大家很熟悉,是否想过为何需要这样分层? 网上大多都是介绍每一层…...
EFCore HasDefaultValueSql
今天小伙伴在代码中遇到了有关 HasDefaultValue 的疑问,这里整理澄清下... 在使用 Entity Framework Core (EFCore) 配置实体时,HasDefaultValue 方法会为数据库列设置一个默认值。该默认值的行为取决于以下条件: 1. 配置 HasDefaultValue 的…...
Win10微调大语言模型ChatGLM2-6B
在《Win10本地部署大语言模型ChatGLM2-6B-CSDN博客》基础上进行,官方文档在这里,参考了这篇文章 首先确保ChatGLM2-6B下的有ptuning AdvertiseGen下载地址1,地址2,文件中数据留几行 模型文件下载地址 (注意࿱…...
什么叫区块链?怎么保证区块链的安全性?
区块链(Blockchain)是一种分布式数据库或账本技术,它通过去中心化的方式记录交易或其他数据,并确保这些记录是安全、透明和不可篡改的。区块链最初是作为比特币(Bitcoin)加密货币的基础技术而被公众所知&am…...
一、智能体强化学习——强化学习基础
1.1 强化学习与深度学习的基本概念 1.1.1 强化学习的核心思想 什么是强化学习? 强化学习(Reinforcement Learning, RL):指在与环境(Environment)的反复交互中,智能体(Agent&#x…...
【DES加密】
什么是DES DES(Data Encryption Standard) 是一种对称加密算法。它的设计目标是提供高度的数据安全性和性能。 DES的概念 DES使用56位的密钥和64位的明文块进行加密。DES算法的分组大小是64位,因此,如果需要加密的明文长度不足64位,需要进…...
.NET中的框架和运行环境
在.NET生态系统中,框架和运行环境是两个不同的概念,它们各自扮演着重要的角色。 下面我将分别介绍.NET中的框架和运行环境,并解释它们之间的区别。 .NET 框架(Frameworks) 框架提供了一套预定义的类库、工具和服务&…...
uniapp 对接腾讯云IM群组成员管理(增删改查)
UniApp 实战:腾讯云IM群组成员管理(增删改查) 一、前言 在社交类App开发中,群组成员管理是核心功能之一。本文将基于UniApp框架,结合腾讯云IM SDK,详细讲解如何实现群组成员的增删改查全流程。 权限校验…...
Docker 离线安装指南
参考文章 1、确认操作系统类型及内核版本 Docker依赖于Linux内核的一些特性,不同版本的Docker对内核版本有不同要求。例如,Docker 17.06及之后的版本通常需要Linux内核3.10及以上版本,Docker17.09及更高版本对应Linux内核4.9.x及更高版本。…...
k8s从入门到放弃之Ingress七层负载
k8s从入门到放弃之Ingress七层负载 在Kubernetes(简称K8s)中,Ingress是一个API对象,它允许你定义如何从集群外部访问集群内部的服务。Ingress可以提供负载均衡、SSL终结和基于名称的虚拟主机等功能。通过Ingress,你可…...
Vue2 第一节_Vue2上手_插值表达式{{}}_访问数据和修改数据_Vue开发者工具
文章目录 1.Vue2上手-如何创建一个Vue实例,进行初始化渲染2. 插值表达式{{}}3. 访问数据和修改数据4. vue响应式5. Vue开发者工具--方便调试 1.Vue2上手-如何创建一个Vue实例,进行初始化渲染 准备容器引包创建Vue实例 new Vue()指定配置项 ->渲染数据 准备一个容器,例如: …...
linux 错误码总结
1,错误码的概念与作用 在Linux系统中,错误码是系统调用或库函数在执行失败时返回的特定数值,用于指示具体的错误类型。这些错误码通过全局变量errno来存储和传递,errno由操作系统维护,保存最近一次发生的错误信息。值得注意的是,errno的值在每次系统调用或函数调用失败时…...
视频字幕质量评估的大规模细粒度基准
大家读完觉得有帮助记得关注和点赞!!! 摘要 视频字幕在文本到视频生成任务中起着至关重要的作用,因为它们的质量直接影响所生成视频的语义连贯性和视觉保真度。尽管大型视觉-语言模型(VLMs)在字幕生成方面…...
Linux-07 ubuntu 的 chrome 启动不了
文章目录 问题原因解决步骤一、卸载旧版chrome二、重新安装chorme三、启动不了,报错如下四、启动不了,解决如下 总结 问题原因 在应用中可以看到chrome,但是打不开(说明:原来的ubuntu系统出问题了,这个是备用的硬盘&a…...
鸿蒙中用HarmonyOS SDK应用服务 HarmonyOS5开发一个生活电费的缴纳和查询小程序
一、项目初始化与配置 1. 创建项目 ohpm init harmony/utility-payment-app 2. 配置权限 // module.json5 {"requestPermissions": [{"name": "ohos.permission.INTERNET"},{"name": "ohos.permission.GET_NETWORK_INFO"…...
vue3+vite项目中使用.env文件环境变量方法
vue3vite项目中使用.env文件环境变量方法 .env文件作用命名规则常用的配置项示例使用方法注意事项在vite.config.js文件中读取环境变量方法 .env文件作用 .env 文件用于定义环境变量,这些变量可以在项目中通过 import.meta.env 进行访问。Vite 会自动加载这些环境变…...
如何理解 IP 数据报中的 TTL?
目录 前言理解 前言 面试灵魂一问:说说对 IP 数据报中 TTL 的理解?我们都知道,IP 数据报由首部和数据两部分组成,首部又分为两部分:固定部分和可变部分,共占 20 字节,而即将讨论的 TTL 就位于首…...
