当前位置: 首页 > news >正文

Android 如何实现搜索功能:本地搜索?数据模型如何设计?数据如何展示和保存?

目录

  1. 效果图
  2. 为什么需要搜索功能
  3. 如何设计搜索本地的功能,如何维护呢?
  4. 总结

一、效果图

在这里插入图片描述在这里插入图片描述

二、为什么需要搜索功能

找一个选项,需要花非常多的时间,并且每次都需要指导客户在哪里,现在只要让他们搜索一下就可以。这也是模仿手机里面的设置功能来进行开发的。这些选项我是存储在本地的。参数太多,暂时还没考虑做到后台,当然,即使后面做到后台,也只是替换数据而已。

在这里插入图片描述

三、如何设计搜索本地的功能,如何维护呢?

我们可以看到效果图:

  1. 有开关类的、有输入类的。
  2. 有分类:系统设置、串口设置、功能开启等等。
  3. 有默认值,如:123456,默认是关的等等。

so,我们需要设计数据模型。

3.1 设计数据模型

大家可以思考一下为什么数据模型要这样设计?有默认值,有key,有nameId

/*** nameId:显示名称,这里存储的是rid,方便后续国际化* nameS:名称* category:分类* drfault:默认值* type:类型 1 多选项值 2 开关 3 输入类* mmkvName:保存key*/
enum class OtherEnum(var nameId:Int,var nameS:String,var category:String,var drfault:Any,var type:Int,var mmkvName: String) {MDB(R.string.base_two_code,"通讯协议","串口设置","MDB", 2,MMKVName.MDB),CHANGE(R.string.base_nayax,"找零功能","找零设置",false, 1,MMKVName.CHANGE),CONTACT_NUMBER(R.string.leak_canary_test_class_name,"联系方式","系统设置","123456", 2,MMKVName.CONTACT_NUMBER),
}

3.2 Repo类

这里只是增加了一个要展示的数据,后面如果把数据放在了后端,也就只是替换这一部分就可以。

class OtherFragmentRepo @Inject constructor() : BaseRepository() {var arrayList: MutableList<OtherEnum> = Arrays.asList(//用于列表展示OtherEnum.MDB,OtherEnum.CHANGE,OtherEnum.CONTACT_NUMBER,)}

如果有新增的数据,只需要在这里增加就可以了。这样也非常好维护,所以需要数据模型设置好来。

3.3 VM:主要是提供搜索的功能

  1. search方法其实就是遍历所有的集合元素,找到匹配的内容,存放到一个list里面进行展示,通过_readAllDataSuccess进行数据通知界面刷新。因为数据量较小,不到100个左右,所以这里使用的for循环遍历。
class OtherFragmentVM @Inject constructor(private val mRepo: OtherFragmentRepo) : BaseViewModel() {//搜索功能【通信设置、系统设置、功能开启等等】//1. 首先我们需要先添加我们的所有设置。【通过一个bean来存储,一个list来存储所有的】//bean:名称,分类,类型【开关类、输入类、多值类、音量调节】,value//举例:优惠券、功能开启、开关、false//2. 首先把所有的功能项拿到,进行遍历//3. 具体的数据展示:如何展示呢?navigation+fragment??【搜索的时候展示另外一个fragment,而不搜索的时候展示其中一个。】//1.拿到所有的通讯设置var list :ArrayList<OtherEnum> = ArrayList()private var _readAllDataSuccess = MutableLiveData<Int>()//是否读取所有数据成功val readAllDataSuccess: LiveData<Int> get() = _readAllDataSuccess//2.搜索功能fun search(searchText: CharSequence) {list.clear()val arrayList = mRepo.arrayListfor (otherEnum in arrayList) {val stringRes = UiUtil.getStringRes(otherEnum.nameId).uppercase()if (stringRes.contains(searchText.toString().uppercase())) {list.add(otherEnum)}}_readAllDataSuccess.value = list.size}}

3.4 Fragment

  1. 搜索功能:先判断是否输入内容为空,如果为空就提示用户。符合条件就调用search进行模糊匹配搜索。搜索到以后,就将默认显示的navigation进行GONE,将搜索结果进行VISIBLE
class OtherFragment : BaseFragment<BackstageFragmentOtherBinding, OtherFragmentVM>() {private  val TAG = "OtherFragment"override val mViewModel: OtherFragmentVM by viewModels()override fun createVB() = BackstageFragmentOtherBinding.inflate(layoutInflater)private var otherAdapter: OtherAdapter? = nulloverride fun BackstageFragmentOtherBinding.initView() {Log.d(TAG, "OtherFragment initView: ")ivSearch.setOnClickListener {val searchText = etSearch.text.trim()Log.d(TAG, "searchText: "+searchText)if(""==searchText){ToastUtil.switchToastStyleToWarn("请输入内容后搜索")return@setOnClickListener}//进行数据的搜索mViewModel.search(searchText)}with(rvOtherSearch){//设置布局排列方式,默认垂直排列val gridLayoutManager: GridLayoutManager =GridLayoutManager(this@OtherFragment.context, 2, androidx.recyclerview.widget.GridLayoutManager.VERTICAL, false)layoutManager = gridLayoutManagermViewModel.list.add(OtherEnum.CHANGE)otherAdapter = OtherAdapter(mViewModel.list)otherAdapter!!.setItemListener(object : AdapterClickListener {override fun onClickListener(view: View?, position: Int, data: String?) {}})adapter = otherAdapter}}override fun initObserve() {observeLiveData(mViewModel.readAllDataSuccess,::searchResult)}fun searchResult(i: Int) {if (i==0) {ToastUtil.switchToastStyleToWarn("搜索结果为空")mBinding.rvOtherSearch.visibility = View.GONEmBinding.fcvOther.visibility = View.VISIBLEreturn}mBinding.rvOtherSearch.visibility = View.VISIBLEmBinding.fcvOther.visibility = View.GONELog.d(TAG, "searchResult: "+mViewModel.list)otherAdapter?.setData(mViewModel.list)otherAdapter?.notifyDataSetChanged()}override fun initRequestData() {}}

3.5 xml:UI应该如何编写呢?

UI方面:
(1)最简单的:一个输入框;一个搜索按钮;一个recycleview展示搜索结果;一个navigation+FragmentContainerView展示默认的内容,也就是不搜索的时候全部展示。
(2)扩展:1. 搜索历史,热门搜索,输入的时候补全提示。后面可以增加。我们先实现最简单的。

<?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:layout_width="match_parent"android:background="@drawable/no_nav_bg"android:layout_height="match_parent"><net.lucode.hackware.magicindicator.MagicIndicatorandroid:id="@+id/magic_other"android:layout_width="match_parent"android:layout_height="88dp"android:background="@drawable/backstage_shape_product_nav"android:paddingLeft="20dp"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent" /><EditTextandroid:id="@+id/et_search"android:layout_width="1000dp"android:layout_height="80dp"android:background="@drawable/home_other_rectangle_background"android:paddingLeft="20dp"android:maxLines="1"android:inputType="text"android:layout_marginVertical="10dp"android:hint="@string/backstage_search_hint"android:textColor="#2E80DD"android:textSize="32sp"android:textStyle="bold"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintHorizontal_bias="0.51"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toBottomOf="@+id/magic_other" /><ImageViewandroid:id="@+id/iv_search"android:layout_width="90dp"android:layout_height="90dp"android:padding="10dp"android:layout_marginLeft="10dp"android:src="@drawable/search"app:layout_constraintBottom_toBottomOf="@+id/et_search"app:layout_constraintStart_toEndOf="@+id/et_search"app:layout_constraintTop_toTopOf="@+id/et_search" /><androidx.fragment.app.FragmentContainerViewandroid:id="@+id/fcv_other"android:name="androidx.navigation.fragment.NavHostFragment"android:layout_width="match_parent"android:layout_height="0dp"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toBottomOf="@+id/et_search"app:navGraph="@navigation/backstage_other_nav" /><androidx.recyclerview.widget.RecyclerViewandroid:id="@+id/rv_other_search"android:layout_width="match_parent"android:layout_height="0dp"android:visibility="gone"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toBottomOf="@+id/et_search" /></androidx.constraintlayout.widget.ConstraintLayout>

3.6 Adapter:这里我们需要思考子项不一样应该如何设计,保存也数据逻辑也不一样!

  1. 子项不一样,我们应该如何处理呢?我们可以看到otherEnum里面有一个type属性,就是用于定义不同的布局的,如下:
  2. 那么开关类的保存,输入类的保存有应该如何呢?只需要使用不同的布局进行不同的逻辑进行处理就可以。如下:

class OtherAdapter(var productList: MutableList<OtherEnum>) :RecyclerView.Adapter<RecyclerView.ViewHolder>() {private val TAG = "HomeProductAdapter"var SWITCH_TYPE = 1var INPUT_TYPE = 2fun setItemListener(itemListener: AdapterClickListener?) {this.itemListener = itemListener}private var itemListener: AdapterClickListener? = nullprivate var generalParamData: MutableList<String>? = null//开关类布局inner class MyViewHolder(binding: BackstageItemSystemSettingsBinding) :ViewHolder(binding.root) {private val mBinding = bindingfun bind(otherEnum: OtherEnum) {mBinding.run {Log.d(TAG, "MyViewHolder bind: " + otherEnum)tvName.text = UiUtil.getStringRes(productList[layoutPosition].nameId)//开关类的数据保存rgSwitch.setOnCheckedChangeListener { group, checkedId ->when (checkedId) {R.id.rb_close -> {SpUtils.putBoolean(otherEnum.mmkvName,false)}R.id.rb_open -> {SpUtils.putBoolean(otherEnum.mmkvName,true)}else -> {}}}var flag = SpUtils.getBoolean(otherEnum.mmkvName,otherEnum.drfault as Boolean)if(flag == true){rbOpen.isChecked = true}else{rbClose.isChecked = true}}}}//输入类的布局inner class InputViewHolder(binding: BackstageItemInputBinding) : ViewHolder(binding.root) {private val mBinding = bindingfun bind(otherEnum: OtherEnum) {mBinding.run {Log.d(TAG, "InputViewHolder bind: " +otherEnum.mmkvName+":"+ SpUtils.contains(otherEnum.mmkvName))tvName.text = UiUtil.getStringRes(productList[layoutPosition].nameId)var defaultValue = SpUtils.getString(otherEnum.mmkvName,otherEnum.drfault as String)etValue.setText(defaultValue)//输入类的数据保存btnUpdate.setOnClickListener {val value = etValue.text.trim().toString()if(value==""){ToastUtil.switchToastStyleToWarn("输入为空")return@setOnClickListener}SpUtils.putString(otherEnum.mmkvName,value)ToastUtil.switchToastStyleToSuccess("更新成功:"+otherEnum.mmkvName)}}}}override fun getItemViewType(position: Int): Int {//不同的类型,使用不同的布局return productList[position].type}override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {when (viewType) {SWITCH_TYPE -> {return MyViewHolder(BackstageItemSystemSettingsBinding.inflate(LayoutInflater.from(parent.context),parent,false))}INPUT_TYPE -> {Log.d(TAG, "onCreateViewHolder: InputViewHolder:"+viewType)return InputViewHolder(BackstageItemInputBinding.inflate(LayoutInflater.from(parent.context),parent,false))}else -> {}}return MyViewHolder(BackstageItemSystemSettingsBinding.inflate(LayoutInflater.from(parent.context),parent,false))}override fun getItemCount(): Int {return productList.size}override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {if (holder is MyViewHolder) {holder.bind(productList.get(position))} else if (holder is InputViewHolder) {holder.bind(productList.get(position))}itemListener?.onClickListener(holder.itemView, position, null)}fun setData(productList: MutableList<OtherEnum>) {this.productList = productList}
}

四、总结:

其实搜索功能的重点在于数据模型的设计,还有apdater布局的设置。以前都是一个一个控件的增加,数据也可以直接就增加,所以维护很模仿,现在换成了recycleview,所以我们需要思考每一个item,他的数据应该如何展示,默认值是如何,在哪个分类,如何保存数据。

相关文章:

Android 如何实现搜索功能:本地搜索?数据模型如何设计?数据如何展示和保存?

目录 效果图为什么需要搜索功能如何设计搜索本地的功能&#xff0c;如何维护呢&#xff1f;总结 一、效果图 二、为什么需要搜索功能 找一个选项&#xff0c;需要花非常多的时间&#xff0c;并且每次都需要指导客户在哪里&#xff0c;现在只要让他们搜索一下就可以。这也是模…...

【K230 实战项目】气象时钟

【CanMV K230 AI视觉】 气象时钟 功能描述&#xff1a;说明HMDI资源3.5寸屏幕 使用方法 为了方便小伙伴们理解&#xff0c;请查看视频 B站连接 功能描述&#xff1a; 天气信息获取&#xff1a;通过连接到互联网&#xff0c;实时获取天气数据&#xff0c;包括温度、湿度、天气状…...

什么是 HTTP/3?下一代 Web 协议

毫无疑问&#xff0c;发展互联网底层的庞大协议基础设施是一项艰巨的任务。 HTTP 的下一个主要版本基于 QUIC 协议构建&#xff0c;并有望提供更好的性能和更高的安全性。 以下是 Web 应用程序开发人员需要了解的内容。 HTTP/3 的前景与风险 HTTP/3 致力于让互联网对每个人…...

IDEA Project不显示/缺失文件

问题&#xff1a;侧边栏project 模式下缺少部分文件 先点close project 打开项目所在目录&#xff0c;删除目录下的.idea文件夹 重新open project打开这个项目即可解决...

浅谈vue2.0与vue3.0的区别(整理十六点)

目录 1. 实现数据响应式的原理不同 2. 生命周期不同 3. vue 2.0 采用了 option 选项式 API&#xff0c;vue 3.0 采用了 composition 组合式 API 4. 新特性编译宏 5. 父子组件间双向数据绑定 v-model 不同 6. v-for 和 v-if 优先级不同 7. 使用的 diff 算法不同 8. 兄弟组…...

深入理解 MySQL MVCC:多版本并发控制的核心机制

在数据库领域&#xff0c;并发控制是确保多个事务能够正确地并发执行而不破坏数据完整性的关键技术。MySQL 作为广泛使用的关系型数据库管理系统&#xff0c;采用了多版本并发控制&#xff08;Multi-Version Concurrency Control&#xff0c;MVCC&#xff09;机制来实现高效的并…...

Qt6编译达梦8数据库驱动插件

一、编译环境 操作系统&#xff1a;deepin V23 Qt版本&#xff1a; Qt 6.7.2 编译器&#xff1a;gcc/g version 12.3.0&#xff0c;cmake 3.28.3 达梦数据库&#xff1a;开发版V8 二、下载达梦QT接口源码 下载链接&#xff1a; https://eco.dameng.com/downlo…...

什么是机器学习力场

机器学习力场&#xff08;Machine Learning Force Fields, MLFF&#xff09;方法是一类将机器学习技术应用于分子动力学&#xff08;Molecular Dynamics, MD&#xff09;模拟的技术。它通过使用机器学习算法拟合原子之间的相互作用能量和力场&#xff0c;使得在不牺牲精度的前提…...

USB组合设备——串口+鼠标+键盘

文章目录 USB组合设备——串口+鼠标+键盘描述符结构设备描述符配置描述符集合配置描述符接口关联描述符键盘接口描述符鼠标接口描述符类特殊命令CDC 的类特殊命令HID 的类特殊命令接口 2接口3USB组合设备——串口+鼠标+键盘 描述符结构 设备描述符 配置描述符 接口关联描述符…...

python学习——对无人机影像有RGB转换到HSV

问题描述 最近需要对无人机影像中绿色植被信息进行提取&#xff0c;查看相关论文&#xff0c;发现用的比较多的就是HSV色彩转换方法&#xff0c;动手实践一下。 解决思路 #mermaid-svg-5ejGodIusPv6zFVS {font-family:"trebuchet ms",verdana,arial,sans-serif;fon…...

【南方科技大学】CS315 Computer Security 【Lab2 Buffer Overflow】

目录 引言软件要求启动虚拟机环境设置禁用地址空间布局随机化&#xff08;ASLR&#xff09;设置编译器标志以禁用安全功能 概述BOF.ctestShellCode.c解释 createBadfile.c 开始利用漏洞在堆栈上查找返回地址 实验2的作业 之前有写过一个 博客&#xff0c;大家可以先看看栈溢出…...

持续集成与持续交付CI/CD

CI/CD 是指持续集成&#xff08;Continuous Integration&#xff09;和持续部署&#xff08;Continuous Deployment&#xff09;或持续交付&#xff08;Continuous Delivery&#xff09; 持续集成&#xff08;Continuous Integration&#xff09; 持续集成是一种软件开发实践&…...

C++学习笔记之变量作用域

C学习笔记之变量作用域 https://www.runoob.com/cplusplus/cpp-variable-scope.html 在C程序中&#xff0c;通常有 3 个地方可以声明变量 在函数或者代码块当中&#xff0c;为局部变量在函数的参数定义中&#xff0c;为形式参数在所有函数的外部&#xff0c;为全局变量 作用域…...

解决跨境电商平台账号无法访问的常见问题

跨境电商的迅猛发展&#xff0c;越来越多的卖家选择在全球各大电商平台如亚马逊、eBay等进行商品销售。然而&#xff0c;在实际运营过程中&#xff0c;卖家经常会遇到账号无法访问、应用打不开等问题&#xff0c;导致业务受阻。本文将针对这些问题进行详细分析&#xff0c;并提…...

P2847 [USACO16DEC] Moocast G

P2847 [USACO16DEC] Moocast G [USACO16DEC] Moocast G 题面翻译 Farmer John 的 N N N 头牛 ( 1 ≤ N ≤ 1000 1 \leq N \leq 1000 1≤N≤1000) 为了在他们之间传播信息&#xff0c;想要组织一个"哞哞广播"系统。奶牛们决定去用步话机装备自己而不是在很远的距离…...

针对国内AIGC市场,国内目前出台了那些法律法规?

针对国内AIGC市场&#xff0c;特别是AI生成与合成内容方面&#xff0c;中国已经出台了一系列法律法规来规范其发展和应用。 图片源自“央视新闻” 以下是一些主要的法律法规&#xff1a; 一、国家层面的法律法规 《中华人民共和国网络安全法》 施行时间&#xff1a;2017年6月…...

Windows+Ubuntu双系统下时钟设置

Ubuntu默认把系统时间&#xff08;硬件时钟&#xff09;设置为UTC时间&#xff0c;并根据本地时区和夏令时设置自动调整本地时间&#xff0c;这是一种很合理很优雅的处理硬件时钟和本地时钟的模式。而Windows系统是默认情况下把系统时间设置为本地时间&#xff0c;历来独霸电脑…...

一些写leetcode的笔记

标准库中的string类没有实现像C#和Java中string类的split函数&#xff0c;所以想要分割字符串的时候需要我们自己手动实现。但是有了stringstream类就可以很容易的实现&#xff0c;stringstream默认遇到空格、tab、回车换行会停止字节流输出。 #include <sstream> #incl…...

shopify主题开发之template模板解析

在 Shopify 主题开发中&#xff0c;template 文件是核心部分&#xff0c;它们定义了店铺中不同页面的布局和结构。下面将详细介绍 Shopify 主题中的 template 模板。 一、template 文件结构 在 Shopify 主题中&#xff0c;templates 文件夹包含了所有用于生成店铺页面的模板文…...

Zookeeper学习

文章目录 学习第 1 章 Zookeeper 入门1.1 概述Zookeeper工作机制 1.2 特点1.3 数据结构1.4 应用场景统一命名服务统一配置管理统一集群管理服务器动态上下线软负载均衡 1.5 下载zookeeper 第 2 章 Zookeeper 本地安装2.1 本地模式安装安装前准备配置修改操作 Zookeeper本地安装…...

Java 8 Stream API 入门到实践详解

一、告别 for 循环&#xff01; 传统痛点&#xff1a; Java 8 之前&#xff0c;集合操作离不开冗长的 for 循环和匿名类。例如&#xff0c;过滤列表中的偶数&#xff1a; List<Integer> list Arrays.asList(1, 2, 3, 4, 5); List<Integer> evens new ArrayList…...

现代密码学 | 椭圆曲线密码学—附py代码

Elliptic Curve Cryptography 椭圆曲线密码学&#xff08;ECC&#xff09;是一种基于有限域上椭圆曲线数学特性的公钥加密技术。其核心原理涉及椭圆曲线的代数性质、离散对数问题以及有限域上的运算。 椭圆曲线密码学是多种数字签名算法的基础&#xff0c;例如椭圆曲线数字签…...

Spring AI 入门:Java 开发者的生成式 AI 实践之路

一、Spring AI 简介 在人工智能技术快速迭代的今天&#xff0c;Spring AI 作为 Spring 生态系统的新生力量&#xff0c;正在成为 Java 开发者拥抱生成式 AI 的最佳选择。该框架通过模块化设计实现了与主流 AI 服务&#xff08;如 OpenAI、Anthropic&#xff09;的无缝对接&…...

ArcGIS Pro制作水平横向图例+多级标注

今天介绍下载ArcGIS Pro中如何设置水平横向图例。 之前我们介绍了ArcGIS的横向图例制作&#xff1a;ArcGIS横向、多列图例、顺序重排、符号居中、批量更改图例符号等等&#xff08;ArcGIS出图图例8大技巧&#xff09;&#xff0c;那这次我们看看ArcGIS Pro如何更加快捷的操作。…...

使用Matplotlib创建炫酷的3D散点图:数据可视化的新维度

文章目录 基础实现代码代码解析进阶技巧1. 自定义点的大小和颜色2. 添加图例和样式美化3. 真实数据应用示例实用技巧与注意事项完整示例(带样式)应用场景在数据科学和可视化领域,三维图形能为我们提供更丰富的数据洞察。本文将手把手教你如何使用Python的Matplotlib库创建引…...

【电力电子】基于STM32F103C8T6单片机双极性SPWM逆变(硬件篇)

本项目是基于 STM32F103C8T6 微控制器的 SPWM(正弦脉宽调制)电源模块,能够生成可调频率和幅值的正弦波交流电源输出。该项目适用于逆变器、UPS电源、变频器等应用场景。 供电电源 输入电压采集 上图为本设计的电源电路,图中 D1 为二极管, 其目的是防止正负极电源反接, …...

认识CMake并使用CMake构建自己的第一个项目

1.CMake的作用和优势 跨平台支持&#xff1a;CMake支持多种操作系统和编译器&#xff0c;使用同一份构建配置可以在不同的环境中使用 简化配置&#xff1a;通过CMakeLists.txt文件&#xff0c;用户可以定义项目结构、依赖项、编译选项等&#xff0c;无需手动编写复杂的构建脚本…...

基于江科大stm32屏幕驱动,实现OLED多级菜单(动画效果),结构体链表实现(独创源码)

引言 在嵌入式系统中&#xff0c;用户界面的设计往往直接影响到用户体验。本文将以STM32微控制器和OLED显示屏为例&#xff0c;介绍如何实现一个多级菜单系统。该系统支持用户通过按键导航菜单&#xff0c;执行相应操作&#xff0c;并提供平滑的滚动动画效果。 本文设计了一个…...

Vue3 PC端 UI组件库我更推荐Naive UI

一、Vue3生态现状与UI库选择的重要性 随着Vue3的稳定发布和Composition API的广泛采用&#xff0c;前端开发者面临着UI组件库的重新选择。一个好的UI库不仅能提升开发效率&#xff0c;还能确保项目的长期可维护性。本文将对比三大主流Vue3 UI库&#xff08;Naive UI、Element …...

基于开源AI智能名片链动2 + 1模式S2B2C商城小程序的沉浸式体验营销研究

摘要&#xff1a;在消费市场竞争日益激烈的当下&#xff0c;传统体验营销方式存在诸多局限。本文聚焦开源AI智能名片链动2 1模式S2B2C商城小程序&#xff0c;探讨其在沉浸式体验营销中的应用。通过对比传统品鉴、工厂参观等初级体验方式&#xff0c;分析沉浸式体验的优势与价值…...