AR 眼镜之-拍照/录像动效切换-实现方案

news2025/7/19 4:23:32

目录

📂 前言

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)第一阶段动效
  1. 左移右边部分的 View;

  2. 放大右边部分的 View。

2)第二阶段动效
  1. 动态调整右边部分的约束;

  2. 缩小右边部分的 View;

  3. 从左往右移动左边部分;

  4. 从 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">

    <ImageView
        android: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" />

    <LinearLayout
        android: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">

        <ImageView
            android:id="@+id/focIcon"
            android:layout_width="32dp"
            android:layout_height="32dp"
            android:contentDescription="@null"
            android:src="@drawable/ic_camera_picture_foc" />

        <com.agg.ui.AGGTextView
            android: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 = false
            binding.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 = true
            binding.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 = VISIBLE

        AnimatorSwitchHelper.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 Int
                val layoutParams = binding.focLayout.layoutParams as ViewGroup.LayoutParams
                layoutParams.width = width
                binding.focLayout.layoutParams = layoutParams
            }
        }

3.2 第二阶段动效

1)动态调整右边部分的约束

        第一阶段在 XML 中默认配置的是 layout_constraintStart_toStartOf="parent",能保证放大时以左边为锚点从左往右放大;而第二阶段缩小时需要以右边为锚点,此时需要动态改变约束如下:

private fun changeConstraint(binding: ActivityMainBinding) {
    Log.i(TAG, "changeConstraint: ")
    val focLayoutId = R.id.focLayout
    val constraintLayout = binding.parent
    ConstraintSet().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 Int
        val layoutParams = binding.focLayout.layoutParams as ViewGroup.LayoutParams
        layoutParams.width = width
        binding.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 = 1000
    start()
}
6)还原默认约束

        动效做完后需要还原默认约束,保证下次动效的正常进行。

if (!isFirstSwitch) restoreConstraint(binding)

private fun restoreConstraint(binding: ActivityMainBinding) {
    Log.i(TAG, "restoreConstraint: ")
    val focLayoutId = R.id.focLayout
    val constraintLayout = binding.parent
    ConstraintSet().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.simpleName
    var isAnimating = false
    var isFirstSwitch = true

    fun startAnimator(binding: ActivityMainBinding) {
        Log.i(TAG, "startAnimator: isAnimating=$isAnimating,isFirstSwitch=$isFirstSwitch")
        isAnimating = true
        val defWidth = binding.focLayout.width
        if (!isFirstSwitch) restoreConstraint(binding)
        if (isFirstSwitch) binding.focLayout.x = binding.focLayout.x - 86

        // 1. 放大Foc的View
        val focBgBigAnim = ValueAnimator.ofInt(defWidth, defWidth + 86).apply {
            addUpdateListener { animation ->
                val width = animation.animatedValue as Int
                val layoutParams = binding.focLayout.layoutParams as ViewGroup.LayoutParams
                layoutParams.width = width
                binding.focLayout.layoutParams = layoutParams
            }
            addListener(object : AnimatorListenerAdapter() {
                override fun onAnimationEnd(p0: Animator) {
                    Log.i(TAG, "onAnimationEnd: focBgBigAnim")
                    // 为绘制反向动画,需修改约束方向
                    changeConstraint(binding)
                    isFirstSwitch = false
                }
            })
        }
        // 2.1 缩小Foc的View
        val focBgSmallAnim = ValueAnimator.ofInt(defWidth + 86, defWidth).apply {
            addUpdateListener { animation ->
                val width = animation.animatedValue as Int
                val layoutParams = binding.focLayout.layoutParams as ViewGroup.LayoutParams
                layoutParams.width = width
                binding.focLayout.layoutParams = layoutParams
            }
            addListener(object : AnimatorListenerAdapter() {
                override fun onAnimationEnd(p0: Animator) {
                    Log.i(TAG, "onAnimationEnd: focBgSmallAnim")
                    isAnimating = false
                }
            })
        }
        // 2.2 从左往右移动Nor的View
        val norBgTransAnim = ObjectAnimator.ofFloat(binding.norIcon, "translationX", -80f, 0f)
        // 2.3 透明度渐显Nor的View
        val norBgAlphaAnim = ObjectAnimator.ofFloat(binding.norIcon, "alpha", 0f, 1f)

        AnimatorSet().apply {
            playSequentially(focBgBigAnim, focBgSmallAnim)
            playTogether(focBgSmallAnim, norBgTransAnim, norBgAlphaAnim)
            duration = 1000
            start()
        }
    }

    private fun changeConstraint(binding: ActivityMainBinding) {
        Log.i(TAG, "changeConstraint: ")
        val focLayoutId = R.id.focLayout
        val constraintLayout = binding.parent
        ConstraintSet().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.focLayout
        val constraintLayout = binding.parent
        ConstraintSet().apply {
            clone(constraintLayout)
            clear(focLayoutId, ConstraintSet.END)
            connect(
                focLayoutId, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START, 110
            )
            applyTo(constraintLayout)
        }
    }

}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2273974.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

STM32-笔记39-SPI-W25Q128

一、什么是SPI&#xff1f; SPI是串行外设接口&#xff08;Serial Peripheral Interface&#xff09;的缩写&#xff0c;是一种高速的&#xff0c;全双工&#xff0c;同步的通信总线&#xff0c;并且 在芯片的管脚上只占用四根线&#xff0c;节约了芯片的管脚&#xff0c;同时为…

JavaScript动态渲染页面爬取之Selenium

前面这篇博客讲解了 Ajax 的分析方法&#xff0c;利用 Ajax接口可以非常方便地爬取数据。只要能找到 Ajax接口的规律&#xff0c;就可以通过某些参数构造出对应的请求&#xff0c;自然就能轻松爬取数据啦。 但是在很多情况下&#xff0c;Ajax请求的接口含有加密参数&#xff0…

python学习笔记—14—函数

1. 函数 (1) len与my_len str "supercarrydoinb"def my_len(tmp_str):cnt 0for i in tmp_str:cnt 1return cntstr_len_1 len(str) str_len_2 my_len(str) print(f"len {str_len_1}") print(f"my_len {str_len_2}") (2) 函数传参数量不受…

Unity性能优化总结

目录 前言 移动端常见性能优化指标​编辑 包体大小优化 FPS CPU占用率 GPU占用率 内存 发热和耗电量 流量优化 前言 终于有时间了,我将在最近两个项目中进行优化的一些经验进行归纳总结以飨读者。因为我习惯用思维导图,所以归纳的内容主要以图来表达希望对大家有用。…

51单片机——定时器中断(重点)

STC89C5X含有3个定时器&#xff1a;定时器0、定时器1、定时器2 注意&#xff1a;51系列单片机一定有基本的2个定时器&#xff08;定时器0和定时器1&#xff09;&#xff0c;但不全有3个中断&#xff0c;需要查看芯片手册&#xff0c;通常我们使用的是基本的2个定时器&#xff…

基于html5实现音乐录音播放动画源码

源码介绍 基于html5实现音乐录音播放动画源码是一款类似Shazam的UI&#xff0c;点击按钮后&#xff0c;会变成为一个监听按钮。旁边会有音符飞入这个监听按钮&#xff0c;最后转换成一个音乐播放器。 效果预览 源码获取 基于html5实现音乐录音播放动画源码

对话|全年HUD前装将超330万台,疆程技术瞄准人机交互“第一屏”

2024年&#xff0c;在高阶智驾进入快速上车的同时&#xff0c;座舱人机交互也在迎来新的增长点。Chat GPT、AR-HUD、车载投影等新配置都在带来新增量机会。 高工智能汽车研究院监测数据显示&#xff0c;2024年1-10月&#xff0c;中国市场&#xff08;不含进出口&#xff09;乘用…

【技术支持】安卓无线adb调试连接方式

Android 10 及更低版本&#xff0c;需要借助 USB 手机和电脑需连接在同一 WiFi 下&#xff1b;手机开启开发者选项和 USB 调试模式&#xff0c;并通过 USB 连接电脑&#xff08;即adb devices可以查看到手机&#xff09;&#xff1b;设置手机的监听adb tcpip 5555;拔掉 USB 线…

Dependency check 通过Maven构建时,配置Mysql数据库遇到的三个坑

使用过Dependency check的同学&#xff0c;一定会遇到这个问题—— 每次执行依赖扫描时&#xff0c;由于网络问题会导致NVD下载种子数据的过程中的种种失败&#xff0c;不仅浪费了大量时间&#xff0c;还会因为下载文件的不完整性直接导致依赖检测的失败。所以我在使用Dependen…

uniApp通过xgplayer(西瓜播放器)接入视频实时监控

&#x1f680; 个人简介&#xff1a;某大型国企资深软件开发工程师&#xff0c;信息系统项目管理师、CSDN优质创作者、阿里云专家博主&#xff0c;华为云云享专家&#xff0c;分享前端后端相关技术与工作常见问题~ &#x1f49f; 作 者&#xff1a;码喽的自我修养&#x1f9…

【OJ刷题】同向双指针问题

这里是阿川的博客&#xff0c;祝您变得更强 ✨ 个人主页&#xff1a;在线OJ的阿川 &#x1f496;文章专栏&#xff1a;OJ刷题入门到进阶 &#x1f30f;代码仓库&#xff1a; 写在开头 现在您看到的是我的结论或想法&#xff0c;但在这背后凝结了大量的思考、经验和讨论 目录 1…

Ubuntu中使用miniconda安装R和R包devtools

安装devtools环境包 sudo apt-get install gfortran -y sudo apt-get install build-essential -y sudo apt-get install libxt-dev -y sudo apt-get install libcurl4-openssl-dev -y sudo apt-get install libxml2.6-dev -y sudo apt-get install libssl-dev -y sudo apt-g…

《分布式光纤测温:解锁楼宇安全的 “高精度密码”》

在楼宇建筑中&#xff0c;因其内部空间庞大&#xff0c;各类电器设施众多&#xff0c;如何以一种既高效又稳定&#xff0c;兼具低成本与高覆盖特性的方式&#xff0c;为那些关键线路节点开展温度监测&#xff0c;是目前在安全监测领域一项重点研究项目&#xff0c;而无锡布里渊…

git撤回提交、删除远端某版本、合并指定版本的更改

撤回提交 vscode的举例 一、只提交了还未推送的情况下 1.撤回最后一次提交&#xff0c;把最后一次提交的更改放到暂存区 git reset --soft HEAD~12.撤回最后一次提交&#xff0c;把最后一次提交的更改放到工作区 git reset --mixed HEAD~13.撤回最后一次提交&#xff0c;不…

【Spring Boot】Spring AOP 快速上手指南:开启面向切面编程新旅程

前言 &#x1f31f;&#x1f31f;本期讲解关于spring aop的入门介绍~~~ &#x1f308;感兴趣的小伙伴看一看小编主页&#xff1a;GGBondlctrl-CSDN博客 &#x1f525; 你的点赞就是小编不断更新的最大动力 &#x1f386;那么废话不…

开源CMS建站系统的安全优势有哪些?

近年来&#xff0c;用户们用开源CMS系统搭建网站的比例也越来越高&#xff0c;它为用户提供了便捷的网站建设解决方案。其中&#xff0c;亿坊CMS建站系统更因安全方面备受用户欢迎&#xff0c;下面带大家一起全面地了解一下。 一、什么是开源CMS&#xff1f; 开源CMS指的是那…

基于单片机的客车载客状况自动检测系统(论文+源码)

1系统整体设计 本课题为客车载客状况自动检测系统&#xff0c;在此以STM32单片机为核心控制器&#xff0c;结合压力传感器、红外传感器、蜂鸣器、语音提示模块、继电器、液晶等构成整个客车载客状况自动检测系统&#xff0c;整个系统架构如图2.1所示&#xff0c;在此通过两个红…

CDP集成Hudi实战-Hive

[〇]关于本文 本文测试一下使用Hive和Hudi的集成 软件版本Hudi1.0.0Hadoop Version3.1.1.7.3.1.0-197Hive Version3.1.3000.7.3.1.0-197Spark Version3.4.1.7.3.1.0-197CDP7.3.1 [一]部署Jar包 1-部署hudi-hive-sync-bundle-1.0.0.jar文件 [rootcdp73-1 ~]# for i in $(se…

网络安全 基础入门-概念名词

域名相关 域名 域名和IP地址相互映射&#xff0c;这样不用去记住能够被机器直接读取的IP地址数串 域名系统(DNS) 它作为将域名和IP地址相互映射的一个分布式数据库&#xff0c;能够使人更方便地访问互联网。DNS使用UDP端口53。 1. 如果是自动获取dns,就向上查询&#xff…

Rust语言使用iced实现简单GUI页面

使用cargo新建一个rust项目 cargo new gui_demo cd gui_demo 编辑Cargo.toml文件 ,添加iced依赖 [package] name "gui_demo" version "0.1.0" edition "2021"[dependencies] iced "0.4.2" 编辑src/main.rs文件&#xff1a; u…