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) 框架提供了一套预定义的类库、工具和服务&…...

UE5 学习系列(二)用户操作界面及介绍
这篇博客是 UE5 学习系列博客的第二篇,在第一篇的基础上展开这篇内容。博客参考的 B 站视频资料和第一篇的链接如下: 【Note】:如果你已经完成安装等操作,可以只执行第一篇博客中 2. 新建一个空白游戏项目 章节操作,重…...
Python爬虫实战:研究MechanicalSoup库相关技术
一、MechanicalSoup 库概述 1.1 库简介 MechanicalSoup 是一个 Python 库,专为自动化交互网站而设计。它结合了 requests 的 HTTP 请求能力和 BeautifulSoup 的 HTML 解析能力,提供了直观的 API,让我们可以像人类用户一样浏览网页、填写表单和提交请求。 1.2 主要功能特点…...

大话软工笔记—需求分析概述
需求分析,就是要对需求调研收集到的资料信息逐个地进行拆分、研究,从大量的不确定“需求”中确定出哪些需求最终要转换为确定的“功能需求”。 需求分析的作用非常重要,后续设计的依据主要来自于需求分析的成果,包括: 项目的目的…...

K8S认证|CKS题库+答案| 11. AppArmor
目录 11. AppArmor 免费获取并激活 CKA_v1.31_模拟系统 题目 开始操作: 1)、切换集群 2)、切换节点 3)、切换到 apparmor 的目录 4)、执行 apparmor 策略模块 5)、修改 pod 文件 6)、…...

【WiFi帧结构】
文章目录 帧结构MAC头部管理帧 帧结构 Wi-Fi的帧分为三部分组成:MAC头部frame bodyFCS,其中MAC是固定格式的,frame body是可变长度。 MAC头部有frame control,duration,address1,address2,addre…...
MySQL 隔离级别:脏读、幻读及不可重复读的原理与示例
一、MySQL 隔离级别 MySQL 提供了四种隔离级别,用于控制事务之间的并发访问以及数据的可见性,不同隔离级别对脏读、幻读、不可重复读这几种并发数据问题有着不同的处理方式,具体如下: 隔离级别脏读不可重复读幻读性能特点及锁机制读未提交(READ UNCOMMITTED)允许出现允许…...

Python:操作 Excel 折叠
💖亲爱的技术爱好者们,热烈欢迎来到 Kant2048 的博客!我是 Thomas Kant,很开心能在CSDN上与你们相遇~💖 本博客的精华专栏: 【自动化测试】 【测试经验】 【人工智能】 【Python】 Python 操作 Excel 系列 读取单元格数据按行写入设置行高和列宽自动调整行高和列宽水平…...
【HTML-16】深入理解HTML中的块元素与行内元素
HTML元素根据其显示特性可以分为两大类:块元素(Block-level Elements)和行内元素(Inline Elements)。理解这两者的区别对于构建良好的网页布局至关重要。本文将全面解析这两种元素的特性、区别以及实际应用场景。 1. 块元素(Block-level Elements) 1.1 基本特性 …...
Axios请求超时重发机制
Axios 超时重新请求实现方案 在 Axios 中实现超时重新请求可以通过以下几种方式: 1. 使用拦截器实现自动重试 import axios from axios;// 创建axios实例 const instance axios.create();// 设置超时时间 instance.defaults.timeout 5000;// 最大重试次数 cons…...

用docker来安装部署freeswitch记录
今天刚才测试一个callcenter的项目,所以尝试安装freeswitch 1、使用轩辕镜像 - 中国开发者首选的专业 Docker 镜像加速服务平台 编辑下面/etc/docker/daemon.json文件为 {"registry-mirrors": ["https://docker.xuanyuan.me"] }同时可以进入轩…...