Android:弹出对话框方式梳理一览(一)

news2024/12/23 13:59:26

Android:弹出对话框方式梳理一览(一)

在这里插入图片描述

Guide|导言

在Android开发中,对话框可能是我们与用户交互的非常常用的方式,包括弹出一个小界面,可能在实际场景中都非常实用。本篇文章主要就是对Android弹出对话框的一些方式的梳理,同时也帮助我自己巩固,避免遗忘。

本文主要还是参考Google的官方文档,详见:对话框|Android开发者;

Dialog|对话框的基类

在Android中,Dialog类是对话框的基类,它负责实现对话框的一些共有属性,不过我们一般不直接使用Dialog类,而是使用它的衍生类,比如AlertDialog(可显示标题、最多三个按钮、可选项目列表或自定义布局的对话框),DatePickerDialog 或 TimePickerDialog(一个对话框,带有可让用户选择日期或时间的预定义界面)。

之前的文章中我们已经简单介绍过了Android中的Window相关机制,实际上Dialog也是通过Window机制显示出来的,我们可以简单看一眼源码(此处跳过也不影响后边内容):

Dialog(@UiContext @NonNull Context context, @StyleRes int themeResId,
            boolean createContextThemeWrapper) {
		......
		//获取WindowManager服务
        mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
		//新创建一个Window用来显示对话框
        final Window w = new PhoneWindow(mContext);
        mWindow = w;
        w.setCallback(this);
        w.setOnWindowDismissedCallback(this);
        w.setOnWindowSwipeDismissedCallback(() -> {
            if (mCancelable) {
                cancel();
            }
        });
        //将新创建出来的Window与WindowManager关联
        w.setWindowManager(mWindowManager, null, null);
		//设置弹出的位置为中心
        w.setGravity(Gravity.CENTER);
        mListenersHandler = new ListenersHandler(this);
    }

一些重要的点已经注释在了代码中,可以看到,在Dialog的构造方法中就创建出来了一个Window来显示需要弹出的内容,所以说它本质上也是使用到了Window机制来进行内容的显示。接下来继续看它的show方法的部分逻辑:

    public void show() {
        ......
        mWindowManager.addView(mDecor, l);
        if (restoreSoftInputMode) {
            l.softInputMode &=
                    ~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION;
        }

        mShowing = true;

        sendShowMessage();
    }

这个show方法就是最终显示对话框的方法,可以看到,这里显然是使用到了windowManager的addView方法,所以到这里我们就可以明确其显示原理了,就是使用到了windowManagerService的功能。

Basic|基础使用

介绍完了Dialog的基本原理,接下来我们来了解Dialog的基础使用方法。使用Dialog大致可以分为五步:

  1. 创建Dialog的构造器
  2. 配置Dialog的内容
  3. [可选]进行一些生命周期配置(比如onStart,onStop等,此处可能在自定义Dialog中使用到)
  4. 显示Dialog
  5. 注销/隐藏Dialog

如果只是简单的使用,其实两步就可以概括:

  • 创建Dialog
  • 显示Dialog

比如一个最简单的示例如下所示:

//1.创建构造器
val dialogBuilder = AlertDialog.Builder(this)
//2.配置dialog内容
dialogBuilder.apply {
    //配置正文内容
    setMessage("This is the Message!")
    //配置标题
    setTitle("This is a Dialog!")
}
//3.创建并显示dialog
dialogBuilder.create().show()

这里注意似乎语法糖有一些问题,比如这样写:

 //1.创建构造器
 val dialogBuilder = AlertDialog.Builder(this)
 //2.配置dialog内容
 dialogBuilder.apply {
     //配置正文内容
     setMessage("This is the Message!")
     //配置标题 -- 使用语法糖将会导致失效
     title = "This is a Dialog!"
 }
 //3.创建并显示dialog
 dialogBuilder.create().show()

配置出来的标题就会失效,因为这个语法糖关联的方法的是Activity的方法,目前建议还是先别用语法糖。如果需要添加按钮,也按照上面这个方式来添加即可。

添加按钮&Message

对于AlertDialog来说,最多可以添加三个按钮,性质分别是Positive,Negative,Neutral,官方意义上可以理解成确认,取消,中立三个意思。比如说申请权限时的授权拒绝授权仅在使用中允许就差不多可以对应上前面的三个意思。

对于Positive性质的按钮,我们可以调用setPositiveButton(CharSequence text, final OnClickListener listener) 方法来设置,比如:

setPositiveButton("确认",object :DialogInterface.OnClickListener {
                    override fun onClick(dialog: DialogInterface?, which: Int) {
                        Log.d(TAG, "onClick: 确认")
                    }

                })

不过,我们当然也可以替换成Lambda表达式:

setNegativeButton("取消") {dialog,which->
                    Log.d(TAG, "onCreate: 取消")
                }

我们来改进上面这段代码,最终为:

//1.创建构造器
val dialogBuilder = AlertDialog.Builder(this)
//2.配置dialog内容
dialogBuilder.apply {
    //配置正文内容
    setMessage("This is the Message!")
    //配置标题
    setTitle("This is a Dialog!")
    //设置按钮
    //确认
    setPositiveButton("确认",object :DialogInterface.OnClickListener{
        override fun onClick(dialog: DialogInterface?, which: Int) {
            Log.d(TAG, "onClick: 确认")
        }

    })
    //取消
    setNegativeButton("取消"){dialog,which->
        Log.d(TAG, "onCreate: 取消")
    }
    //中立
    setNeutralButton("中立"){dialog,which ->
        Log.d(TAG, "onCreate: 中立")
    }

}
//3.创建并显示dialog
dialogBuilder.create().show()

最终显示的效果:
在这里插入图片描述

添加一般列表

除了一般的按钮和文本之外,我们还可以向对话框内填入列表,不过由于列表显示在对话框的内容区域中,因此对话框无法同时显示消息和列表

添加列表项的最简单的方法通过setItems方法:

setItems(arrayOf("列表一","列表二","列表三"),object :DialogInterface.OnClickListener{
    override fun onClick(dialog: DialogInterface?, which: Int) {
        Log.d(TAG, "index:${which} ,dialog:${dialog}")
    }

})

其中dialog参数为弹出的Dialog对象,which参数为之前传入的String数组的索引值,需要说明的是这个Dialog应该是不会被复用的,是一个非永久的Dialog,我们可以通过打印出来的日志看出来:
在这里插入图片描述

理解了上边的代码后,可以进一步简化为:

setItems(arrayOf("列表一","列表二","列表三"),{dialog, which ->
    Log.d(TAG, "index:${which} ,dialog:${dialog}")
})

实现的效果如下:

在这里插入图片描述
另外,也可以使用实现了ListAdapter接口的Adapter来设置列表项,比如说:

setAdapter(ArrayAdapter(context,
    com.google.android.material.R.layout.support_simple_spinner_dropdown_item,
    arrayOf("1","2")
), object :DialogInterface.OnClickListener{
    override fun onClick(dialog: DialogInterface?, which: Int) {
        Log.d(TAG, "index:${which} ,dialog:${dialog}")
    }

})

添加永久性列表

前边提到过我们添加的DIalog不是一个可复用的,而是每次弹出都会创建一个新的Dialog,接下来我们来介绍可以添加永久性列表的方法。

添加永久性复选框☑️

所谓复选框,就是可以同时选择多个列表项的Dialog,它的添加方法和之前的也类似,我们可以调用setMultiChoiceItems来设置,比如:

setMultiChoiceItems(arrayOf("Item1","Item2","Item3"), booleanArrayOf(true,false,true),
    object :DialogInterface.OnMultiChoiceClickListener {
        override fun onClick(
            dialog: DialogInterface?,
            which: Int,
            isChecked: Boolean
        ) {
            Log.d(TAG, "dialog is ${dialog},index is ${which},is checked? ${isChecked}")
        }

    }
)

这个方法相比之前添加列表的参数中多了一个 BooleanArray 类型,该参数指定的是第一次弹出复选框时的列表选择状态,比如说这里我们传入的是 true,false,true的参数,最终第一次弹框的效果就是:
在这里插入图片描述
如果想一开始什么都不选中,那传入一个null值即可。

最后我们来验证一下该Dialog是否是一个永久性的,分别点击列表项,我们可以发现每次打印的Dialog都是同一项:
在这里插入图片描述
说明这确实是一个永久性的Dialog。

添加永久性的单选框

最后就是添加一个永久性的单选框,这个其实和一开始的添加一个非永久性的单选框的方法很类似,唯一的一点就是多了一个参数来指定第一次弹出时选中的列表项,跟之前的类似:

setSingleChoiceItems(arrayOf("item1","item2","item3"),1,
    object :DialogInterface.OnClickListener {
        override fun onClick(dialog: DialogInterface?, which: Int) {
            Log.d(TAG, "index:${which} ,dialog:${dialog}")
        }

    }
)

为Dialog添加自定义内容

到之前为止其实都是Dialog的很基础的应用,实际上在使用过程中我们可能需要弹出一个Dialog来展示我们自己的定制化的内容,比如说一个登录页面,这个时候我们就不能用之前的简单的方法了,取而代之,我们可以调用setView方法来为我们的Dialog填充自定义的内容,这个过程实际上类似于动态添加View的过程。

举例来说,我们可以先设计一个布局来描述我们需要填充的具体内容:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:layout_height="match_parent"
    android:orientation="vertical">

    <ImageView
        android:id="@+id/header_img"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:scaleType="centerCrop"
        app:srcCompat="@drawable/images" />

    <com.google.android.material.textfield.TextInputLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <com.google.android.material.textfield.TextInputEditText
            android:id="@+id/password_input"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="password" />
    </com.google.android.material.textfield.TextInputLayout>

    <com.google.android.material.textfield.TextInputLayout
        android:id="@+id/textInputLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <com.google.android.material.textfield.TextInputEditText
            android:id="@+id/user_input"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="user" />
    </com.google.android.material.textfield.TextInputLayout>
</LinearLayout>

这里用了一个线性布局来描述,至于为什么不用更好用的约束布局,因为我测试过发现直接丢一个约束布局可能会带来显示异常的问题,所以如果想使用约束布局的话可能需要在约束布局外边包一层线性布局。

然后我们来配置这个自定义的Dialog:

//1.创建构造器
val dialogBuilder = AlertDialog.Builder(this)
//2.配置dialog内容
dialogBuilder.apply {
	//添加自定义的布局
    setView(R.layout.dialog_layout)
    setNegativeButton("Cancel",object :DialogInterface.OnClickListener {
        override fun onClick(dialog: DialogInterface?, which: Int) {

        }

    })
    setPositiveButton("Confirm",object : DialogInterface.OnClickListener {
        override fun onClick(dialog: DialogInterface?, which: Int) {

        }

    })
}
//3.创建并显示dialog
dialogBuilder.create().show()

最终显示的效果就是这样的:
在这里插入图片描述
我们可以和官方文档上实现的效果进行对比:
在这里插入图片描述
可以发现我们的按钮是比较丑的,实际上这部分我们也可以直接做进我们的自定义布局里:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:layout_height="match_parent"
    android:orientation="vertical">

    <ImageView
        android:id="@+id/header_img"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:scaleType="centerCrop"
        app:srcCompat="@drawable/images" />

    <com.google.android.material.textfield.TextInputLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <com.google.android.material.textfield.TextInputEditText
            android:id="@+id/password_input"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="password" />
    </com.google.android.material.textfield.TextInputLayout>

    <com.google.android.material.textfield.TextInputLayout
        android:id="@+id/textInputLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <com.google.android.material.textfield.TextInputEditText
            android:id="@+id/user_input"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="user" />
    </com.google.android.material.textfield.TextInputLayout>

    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <androidx.appcompat.widget.AppCompatButton
            android:id="@+id/positivebtn"
            android:layout_width="0dp"
            android:layout_height="60dp"
            android:layout_margin="0dp"
            android:layout_weight="1"
            android:background="#C8E6C9"
            android:elevation="0dp"
            android:text="Confirm" />

        <androidx.appcompat.widget.AppCompatButton
            android:id="@+id/negbtn"
            android:layout_width="0dp"
            android:layout_height="60dp"
            android:layout_margin="0dp"
            android:layout_weight="1"
            android:background="#FFCDD2"
            android:elevation="0dp"
            android:text="Cancel" />
    </LinearLayout>
</LinearLayout>

最终的效果就好上很多:
在这里插入图片描述
至于我们具体的点击事件的设置,则可以通过布局膨胀器获取具体的View来为按钮设置点击事件监听 :

//2.配置dialog内容
dialogBuilder.apply {
    val customView = layoutInflater.inflate(R.layout.dialog_layout,null,false)
    setView(customView)

    customView.findViewById<Button>(R.id.negbtn).setOnClickListener {
        Toast.makeText(context, "cancel!", Toast.LENGTH_SHORT).show()
    }

    customView.findViewById<Button>(R.id.positivebtn).setOnClickListener {
        Toast.makeText(context, "load!", Toast.LENGTH_SHORT).show()
    }
    
}
//3.创建并显示dialog
dialogBuilder.create().show()

为Dialog添加进出场动画

在使用其他App时,我们往往会发现弹出的Dialog并不都是直接闪现出在屏幕中间的,经常会有一个从底部或者是从左右弹出的动画效果,实现该效果的步骤也很简单,不过我们需要有一些Android动画的基础,我们在anim文件夹下新建弹入和弹出动画:

//弹入动画

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate android:fromYDelta="100%p" android:toYDelta="0"
        android:duration="@android:integer/config_mediumAnimTime"/>
</set>

//弹出动画

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate android:fromYDelta="0" android:toYDelta="100%p"
        android:duration="@android:integer/config_mediumAnimTime"/>
</set>

之后我们在styles文件下新建style属性:

    <style name="BottomAni">
        <item name="android:windowEnterAnimation">@anim/pop_from_bottom</item>
        <item name="android:windowExitAnimation">@anim/exit_from_bottom</item>
    </style>

最后在创建完Dialog时指定window动画属性:

        viewBinding.button.setOnClickListener {
            //1.创建构造器
            val dialogBuilder = AlertDialog.Builder(this)
            //2.配置dialog内容
            dialogBuilder.apply {

                val customView = layoutInflater.inflate(R.layout.dialog_layout,null,false)
                setView(customView)

                customView.findViewById<Button>(R.id.negbtn).setOnClickListener {
                    Toast.makeText(context, "cancel!", Toast.LENGTH_SHORT).show()
                }

                customView.findViewById<Button>(R.id.positivebtn).setOnClickListener {
                    Toast.makeText(context, "load!", Toast.LENGTH_SHORT).show()
                }

        }
        //3.创建并显示dialog
        dialogBuilder.create().apply {
        //指定窗口动画
          window?.attributes?.windowAnimations = R.style.BottomAni
          window?.setGravity(Gravity.BOTTOM)
        }.show()

最终效果:

在这里插入图片描述

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

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

相关文章

陪诊系统|陪诊小程序成品|陪诊系统功能

随着人们对健康的日益关注以及医疗技术的不断进步&#xff0c;陪诊小程序应运而生&#xff0c;通过提供陪同就医、医疗服务和健康管理等功能为患者和家庭成员提供了更多的便利和选择。本文将分析陪诊小程序的关键功能&#xff0c;以便更好地理解其在医疗领域的作用。 在陪诊小程…

分布式任务调度工具 XXL-JOB

默认的账号密码是&#xff1a;admin/123456 一&#xff0c;部署docker容器 docker run \ -e PARAMS"--spring.datasource.urljdbc:mysql://192.168.150.101:3306/xxl_job?Unicodetrue&characterEncodingUTF-8 \ --spring.datasource.usernameroot \ --spring.dataso…

GT资源-Clock资源

一、Transmitter 时钟分布 XCLK&#xff1a;在使用TX buffer的模式下&#xff0c;XCLK来源于TXOUTCLK。在使用TX bypassing的模式下XCLK来源于TXUSERCLK。TXUSRCLK是GTX/GTH中PCS的内部逻辑时钟。TXUSRCLK2是GT Transceiver 用户侧逻辑时钟。 TXUSRCLK与TXUSRCLK2的关系 FPGA …

阿赵UE引擎C++编程学习笔记——信息打印输出

大家好&#xff0c;我是阿赵。   在之前介绍HelloWorld的时候&#xff0c;使用了一个打印的命令&#xff0c;把HelloWorld输出到输出日志里面。   对于我们编写程序代码来说&#xff0c;有2个手段是对程序差错非常重要的&#xff0c;一个是断点&#xff0c;另外一个是输出日…

很快就可以试用Domino 15了

大家好&#xff0c;才是真的好。 前几天在比利时的安普卫特举办的Engage2024大会已经结束&#xff0c;流出的现场照片很多&#xff0c;主要是会议场地照片很多&#xff0c;说是令人震撼&#xff1b;可惜这次一手的PPT和会议内容不多.是的&#xff0c;本来我也是在等与会者写的…

安装Jupyter notebook NbExtensions遇到的问题

在安装 Jupyter notebook NbExtensions 时&#xff0c;使用下列代码&#xff1a; pip install jupyter_nbextensions_configurator jupyter_contrib_nbextensions jupyter contrib nbextension install --user jupyter nbextensions_configurator enable --user ——————…

获取京东商品详情,API返回值说明全攻略

京东商品详情API是开发者获取京东平台上商品详细信息的重要工具。通过调用API并解析返回的响应数据&#xff0c;您可以快速获取商品的各项属性&#xff0c;如商品ID、标题、价格、图片等。下面&#xff0c;我们将为您详细介绍京东商品详情API的返回值说明&#xff0c;帮助您更好…

(五)JVM实战——JVM性能调优与监控

JVM调优案例的场景 为什么要调优&#xff1a;防止或者解决jvm虚拟机中的OOM问题&#xff1b;减少FullGC出现的频率&#xff0c;解决系统运行卡、慢问题JVM调优案例的四个方面 OOM(堆溢出)&#xff1a;java heap spaceOOM(元空间溢出)&#xff1a;MetaspaceOOM(GC overhead lim…

【Android项目】“追茶到底”项目介绍

没有多的介绍&#xff0c;这里只是展示我的项目效果&#xff0c;后面会给出具体的代码实现。 一、用户模块 1、注册&#xff08;第一次登陆的话需要先注册账号&#xff09; 2、登陆&#xff08;具有记住最近登录用户功能&#xff09; 二、点单模块 1、展示饮品列表 2、双向联动…

k8s 资源文件参数介绍

Kubernetes资源文件yaml参数介绍 yaml 介绍 yaml 是一个类似 XML、JSON 的标记性语言。它强调以数据为中心&#xff0c;并不是以标识语言为重点例如 SpringBoot 的配置文件 application.yml 也是一个 yaml 格式的文件 语法格式 通过缩进表示层级关系不能使用tab进行缩进&am…

软考是否存在包过班?

国家考试&#xff0c;虽然有人喊着包过&#xff0c;但你也别轻易相信啊&#xff0c;姐妹&#xff0c;要好好保护好你的钱包啊&#xff01;这种考试是没有所谓的包过的。 给你一些学习软考需要注意的要点&#xff1a; 1、深入理解考试内容&#xff1a; 在准备软考之前&#xf…

前端传递list(数组)类型参数,后端接收失败

一顿报错,我之前遇到的list都是Long类型 貌似用GET也是可以的,但是很奇怪一直报错 就是不可以 后来去百度 查询到可以用两种方法解决这个问题 1、拆开 传 以GET方式&#xff0c;后端GetMappingRequestParam接收。 2、以Post方式传&#xff0c;后端创建dto PostMappingReques…

自动化测试再升级,大模型与软件测试相结合

近年来&#xff0c;软件行业一直在迅速发展&#xff0c;为了保证软件质量和提高效率&#xff0c;软件测试领域也在不断演进。如今&#xff0c;大模型技术的崛起为软件测试带来了前所未有的智能化浪潮。 软件测试一直是确保软件质量的关键环节&#xff0c;但传统的手动测试方法存…

linux系统-部署YUM仓库及NFS共享服务

目录 一、YUM概述 二、准备安装源 软件仓库的提供方式 RPM软件包的来源 构建Centos7软件仓库 在软件仓库中加入非官方RPM包组 三、搭建yum软件仓库 四、NFS文件共享服务 NFS简介 使用NFS发布共享资源&#xff08;安装nfs-utils、rpcbird软件包&#xff09; 设置共享目…

记一次DNS故障导致用户无法充值的问题(上)

背景&#xff1a; 刚刚过去了五一劳动节&#xff0c;回来后一上班接到客服运营团队反馈的节日期间的问题&#xff0c;反馈有部分用户无法充值。拿到的反馈资料有&#xff1a; 无法充值操作视频、问题时间、手机机型、手机网络情况。 1、从视频中看到用户点击支付后没有任何反…

jenkins部署服务到windows系统服务器

1、安装openSSH windows默认不支持ssh协议&#xff0c;需要下载安装&#xff0c;主要适用于jenkins传输文件已经执行命令使用 点击查看下载openSSH 2、项目配置 这里简单说说怎么配置&#xff0c;主要解决点就是ssh执行cmd或shell命令时不能开启新窗口导致应用部署失败或者断…

vue使用pdfjs-dist在电脑上展示PDF文件

安装 安装的时候一定要带上版本号,这里采用的是2.0.943(因为这个版本对于我目前的项目比较合适可以正常使用,其他版本大概率会报错),当前项目使用的是vue2,vue的版本是2.5.10 npm install pdfjs-dist@2.0.943 查看版本发现这玩意版本非常之多 使用 在使用pdfjs-dist库…

Python实现打砖块游戏

提供学习或者毕业设计使用&#xff0c;功能基本都有&#xff0c;不能和市场上正式游戏相提比论&#xff0c;请理性对待&#xff01; 在本文中&#xff0c;我们将使用 Pygame 和 Tkinter 创建一个简单的打砖块游戏。游戏的目标是通过控制挡板来击碎屏幕上的砖块&#xff0c;同时…

GDPU 天码行空11

&#xff08;一&#xff09;实验目的 1、掌握JAVA中IO中各种类及其构造方法&#xff1b; 2、重点掌握IO中类所具有的IO操作方法&#xff1b; 3、熟悉软件中登录模块的开发方法&#xff1b; 4、掌握IO中读写常用方法。 5、进一步熟悉正则规则的使用方法。 &#xff08;二&…

猫咪没精神不吃饭?这招主食冻干喂养教你让猫咪恢复食欲

猫突然不吃东西没精神是生病了吗&#xff1f;当猫咪不吃东西、精神不振时&#xff0c;可能的原因有健康问题、环境因素或食物原因。首先应进行身体检查&#xff0c;观察是否有其他并发症&#xff0c;如无则可排除健康问题。还要给猫咪提供一个舒适的环境&#xff0c;多给它们一…