安卓应用开发学习:手机摇一摇功能应用尝试--摇骰子和摇红包
一、引言
前几天,我发布的日志《安卓应用开发学习:查看手机传感器信息》记录了如何查看手机传感器的信息,通过上述的方法,可以看到我的OPPO手机支持19种传感器。本篇日志就记录一下常见的加速度传感器的典型应用——“摇一摇”功能。本应用通过加速度传感器来实现摇骰子或摇红包。最终效果如下:
摇骰子

摇红包

游戏结束

二、功能实现
加速传感器是最常见的传感器之一,有很多应用的摇手机功能就是用到了这个传感器。本次通过学习相关资料,在我的手机上实现了摇骰子和摇红包两个小应用,并且在摇动手机的过程中手机还会振动。大体的实现方法如下:
1.实现振动功能
1.1先要在AndroidManifest.xml文件中添加如下权限。
<uses-permission android:name="android.permission.VIBRATE" />
1.2 在你创建的Activity中申明一个Vibrator对象。
private Vibrator mVibrator;
1.3在需要实现手机振动功能的代码块中,执行vibrate()方法。
mVibrator.vibrate(300); // 系统检测到摇一摇事件后,震动手机提示用户
该方法参数有两种形式:
一是形如 mVibrator.vibrate(300) 的单参数,表示让手机持续振动指定的毫秒数;
二是形如 mVibrator.vibrate({500, 200, 500}, -1) 的双参数,表示先振动500毫秒,然后停止200毫秒,再振动500毫秒。第二个参数为-1表示无循环,为正数,表示循环次数。
2.摇一摇功能的实现
2.1在你创建的Activity中申明一个SensorManager对象。
private SensorManager mSensorMgr;
2.2重写活动页面的onResume方法,在该方法中注册传感器监听事件,并指定待监听的传感器类型为加速度传感器。
@Overrideprotected void onResume() {super.onResume();// 给加速度传感器注册传感监听器mSensorMgr.registerListener(this,mSensorMgr.getDefaultSensor(Sensor.TYPE_ACCELEROMETER),SensorManager.SENSOR_DELAY_NORMAL);}
2.3重写活动页面的onPause方法,在该方法中注销监听器。
@Overrideprotected void onPause() {super.onPause();mSensorMgr.unregisterListener(this); // 注销当前活动的传感监听器}
2.3编写一个传感器事件监听器,该监听器继承自SensorEventListener。
在活动页面名称后面添加“implements SensorEventListener”(如下),
public class ShakeActivity extends AppCompatActivity implements SensorEventListener {...}
然后按Alt + Enter,Android Studio 自动添加onSensorChanged方法和onAccuracyChanged方法。
onSensorChanged方法在感应信息变化时触发,业务逻辑就写在这里。在本应用中添加的代码是检测手机晃动的幅度是否大于阀值,一旦大于阀值,就让手机振动500毫秒。
onAccuracyChanged方法在精度改变时触发,一般无需处理。
@Overridepublic void onSensorChanged(SensorEvent event) {if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) { // 加速度变更事件// values[0]:X轴,values[1]:Y轴,values[2]:Z轴float[] values = event.values;if ((Math.abs(values[0]) > 15 || Math.abs(values[1]) > 15|| Math.abs(values[2]) > 15)) {mVibrator.vibrate(300); // 系统检测到摇一摇事件后,震动手机提示用户}}}@Overridepublic void onAccuracyChanged(Sensor sensor, int accuracy) {// 当传感器精度改变时回调该方法,一般无需处理}
3.游戏模式的选择
3.1本应用支持2种游戏模式,因此在界面设计上用到了RadioGroup和RadioButton。
<TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_gravity="center_vertical"android:gravity="center_horizontal"android:text="游戏模式:" /><RadioGroupandroid:id="@+id/rg_mode"android:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="horizontal"><RadioButtonandroid:id="@+id/rb_dice"android:layout_width="80dp"android:layout_height="20dp"android:checked="true"android:text="摇骰子" /><RadioButtonandroid:id="@+id/rb_welfare"android:layout_width="80dp"android:layout_height="20dp"android:text="摇红包" /></RadioGroup>
3.2在活动页面声明如下变量:
private int mMode; // 游戏模式private final int MODE_DICE = 0; // 游戏模式1:摇骰子private final int MODE_WELFARE = 1; // 游戏模式2:摇红包private boolean mState = false; // 游戏状态private int diceCount; // 统计摇骰子次数private int welfareNumber; // 统计红包个数private final int[] welfareArr = {1, 5, 10} ; // 红包,可根据情况调整
3.2在活动页面中编写单选按钮组事件监听器,该监听器继承自RadioGroup.OnCheckedChangeListener。通过单选按钮的改变触发响应事件,在onCheckedChanged方法中编写游戏模式变更的逻辑代码。
@Overridepublic void onCheckedChanged(RadioGroup group, int checkedId) {// 根据单选按钮结果,设置游戏模式if (checkedId == R.id.rb_dice) {mMode = MODE_DICE; // 摇骰子模式} else if (checkedId == R.id.rb_welfare) {mMode = MODE_WELFARE; // 摇红包模式}}
4.游戏的执行逻辑
4.1游戏模式选择。默认是选中了摇一摇模式。
4.2点“开始”按钮。当前是非游戏状态;将变量diceCount和welfareNumber都设为0;将游戏状态变量mState设为Ture;页面中显示“请开始摇手机”;将按钮文本改为停止。游戏过程中游戏模式可随时切换,不会终止游戏。
4.3只有mState为Ture时摇动手机,才会进行检测。当检测到有效摇动时,手机会振动300毫秒,并执行startGame方法(延时300毫秒后执行,避免此方法实际执行次数与振动次数有大的差异。
4.4startGame方法中首先检查游戏模式。
如果是摇骰子模式,则产生三个1-6的随机数(设定为三个骰子)。将本次的结果显示在页面上,并将摇骰子次数统计变量diceCount加1。
如果是摇红包模式,则产生一个1-10的随机数,将该随机数与数组welfareArr中的元素进行对比,如果该随机数在数组中,则在页面中显示中奖信息。获得红包统计变量welfareNumber加1。
5.游戏状态下点“停止”按钮,结束游戏。将按钮文本改为开始;页面中显示摇骰子次数和获得红包个数。
三、代码展示
最终的代码如下:
1. 界面设计文件 activity_shake.xml
<?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:layout_height="match_parent"tools:context=".ShakeActivity"><TextViewandroid:id="@+id/tv_title"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginTop="20dp"android:text="摇一摇"android:textSize="28sp"android:textStyle="bold"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent" /><LinearLayoutandroid:id="@+id/ll_mode"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginStart="10dp"android:layout_marginTop="30dp"android:layout_marginEnd="10dp"android:orientation="horizontal"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toBottomOf="@+id/tv_title"><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_gravity="center_vertical"android:gravity="center_horizontal"android:text="游戏模式:" /><RadioGroupandroid:id="@+id/rg_mode"android:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="horizontal"><RadioButtonandroid:id="@+id/rb_dice"android:layout_width="80dp"android:layout_height="20dp"android:checked="true"android:text="摇骰子" /><RadioButtonandroid:id="@+id/rb_welfare"android:layout_width="80dp"android:layout_height="20dp"android:text="摇红包" /></RadioGroup></LinearLayout><Buttonandroid:id="@+id/btn_start"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginTop="30dp"android:text="开始"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toBottomOf="@+id/ll_mode" /><TextViewandroid:id="@+id/tv_shake"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginTop="30dp"android:textSize="17sp"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toBottomOf="@+id/btn_start" /></androidx.constraintlayout.widget.ConstraintLayout>
2.逻辑代码 ShakeActivity.java
import androidx.appcompat.app.AppCompatActivity;import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.Vibrator;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.RadioGroup;
import android.widget.TextView;import java.util.Arrays;
import java.util.Locale;
import java.util.Random;public class ShakeActivity extends AppCompatActivity implements SensorEventListener,RadioGroup.OnCheckedChangeListener, View.OnClickListener {private final static String TAG = "ShakeActivity";private TextView tv_shake; // 声明一个文本视图对象private SensorManager mSensorMgr; // 声明一个传感管理器对象private Vibrator mVibrator; // 声明一个震动器对象private Button btn_start; // 开始按钮private int mMode; // 游戏模式private final int MODE_DICE = 0; // 游戏模式1:摇骰子private final int MODE_WELFARE = 1; // 游戏模式2:摇红包private boolean mState = false; // 游戏状态private int diceCount; // 统计摇骰子次数private int welfareNumber; // 统计红包个数private final int[] welfareArr = {1, 5, 10} ; // 红包 , 20, 50, 100, 500, 1000@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_shake);// 游戏模式选择按钮组RadioGroup rg_mode = findViewById(R.id.rg_mode);rg_mode.setOnCheckedChangeListener(this);tv_shake = findViewById(R.id.tv_shake);btn_start = findViewById(R.id.btn_start);btn_start.setOnClickListener(this); // 开始按钮点击监听// 从系统服务中获取传感管理器对象mSensorMgr = (SensorManager) getSystemService(Context.SENSOR_SERVICE);// 从系统服务中获取震动器对象mVibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);mMode = MODE_DICE; // 单选按钮初始状态选中的遥骰子}@Overrideprotected void onPause() {super.onPause();mSensorMgr.unregisterListener(this); // 注销当前活动的传感监听器}@Overrideprotected void onResume() {super.onResume();// 给加速度传感器注册传感监听器mSensorMgr.registerListener(this,mSensorMgr.getDefaultSensor(Sensor.TYPE_ACCELEROMETER),SensorManager.SENSOR_DELAY_NORMAL);}@Overridepublic void onSensorChanged(SensorEvent event) {if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) { // 加速度变更事件// values[0]:X轴,values[1]:Y轴,values[2]:Z轴float[] values = event.values;if ((Math.abs(values[0]) > 30 || Math.abs(values[1]) > 30|| Math.abs(values[2]) > 30)) {if (mState) {mVibrator.vibrate(300); // 系统检测到摇一摇事件后,震动手机提示用户new Handler().postDelayed(() -> {// 延时后要执行的代码startGame();}, 300); // 延迟时间毫秒}}}}@Overridepublic void onAccuracyChanged(Sensor sensor, int accuracy) {// 当传感器精度改变时回调该方法,一般无需处理}@Overridepublic void onCheckedChanged(RadioGroup group, int checkedId) {// 根据单选按钮结果,设置游戏模式if (checkedId == R.id.rb_dice) {mMode = MODE_DICE; // 摇骰子模式} else if (checkedId == R.id.rb_welfare) {mMode = MODE_WELFARE; // 摇红包模式}}@Overridepublic void onClick(View v) {if (v.getId() == R.id.btn_start) {if (mState) { // 点击时处于游戏状态btn_start.setText("开始");String info = String.format(Locale.CHINESE, "%s%d次,%s%d个。","本次摇骰子", diceCount, "获得红包", welfareNumber);tv_shake.setText(info);} else { // 点击时处于非游戏状态diceCount = 0;welfareNumber = 0;tv_shake.setText("请开始摇手机");btn_start.setText("停止");}mState = !mState;}}private void startGame() {String info = ""; // 检查游戏模式if (mMode == MODE_DICE) { // 摇骰子// 设定有3个骰子,产生3个1-6的随机数Random random = new Random();int dice1 = random.nextInt(6) + 1;int dice2 = random.nextInt(6) + 1;int dice3 = random.nextInt(6) + 1;info = String.format(Locale.CHINESE,"%s%d,%d,%d。", "您摇出的骰子点数是:",dice1, dice2, dice3);diceCount +=1;} else if (mMode == MODE_WELFARE) { // 摇红包int lottery;info = "很遗憾,您没有中奖";// 生成一个0-10的随机数int randomNumber = (int)(Math.random() * 11); // 1001Log.d(TAG, "随机数为:" + randomNumber);// 检查该随机数是否在红包列表中boolean isInArray = Arrays.stream(welfareArr).anyMatch(n -> n == randomNumber);if (isInArray) {lottery = randomNumber;info = String.format(Locale.CHINESE,"%s%d%s", "恭喜您中了", lottery,"元!");welfareNumber +=1;}}tv_shake.setText(info);}
}
相关文章:
安卓应用开发学习:手机摇一摇功能应用尝试--摇骰子和摇红包
一、引言 前几天,我发布的日志《安卓应用开发学习:查看手机传感器信息》记录了如何查看手机传感器的信息,通过上述的方法,可以看到我的OPPO手机支持19种传感器。本篇日志就记录一下常见的加速度传感器的典型应用——“摇一摇”功…...
HTML中的<fieldset>标签元素框的使用
HTML 提供的 <fieldset> 标签用于在表单中分组相关元素。 <fieldset> 标签会在相关元素周围绘制一个框。 <legend> 标签为 fieldset 元素定义标题。 语法如下: <fieldset><legend>标题</legend><!-- 元素内容... -->…...
Linux驱动入门实验班——SR501红外模块驱动(附百问网视频链接)
目录 一、工作方式 二、接口图 三、编写思路 1.构造file_operations结构体 2.实现read函数 3.编写入口函数 4.编写中断处理函数 5.编写出口函数 6.声明出入口函数以及协议 四、源码 五、课程链接 一、工作方式 SR501人体红外感应模块有两种工作模式: …...
windows C++- Com技术简介(上)
在介绍C和winrt与COM组件技术的关系之前,有必要介绍一下com组件技术,这项技术比较古老,但是它一直作为windows的基石存在。COM 是一类独立于平台且面向对象的分布式系统,用于创建可交互的二进制软件组件。 COM 技术是 Microsoft O…...
Jenkins持续集成工具学习
一、从装修厨房看项目开发效率优化 二、持续集成工具 三、JavaEE项目部署方式对比 四、JenkinsSVN持续集成环境搭建 五、JenkinsGitHub持续集成环境搭建...
Redis:查询是否包含某个字符/字符串之三
上一篇:Redis:查询是否包含某个字符/字符串之二-CSDN博客 摘要: 遍历key,在跟进value的类型遍历value是否包含指定字符串 search_strings ,这里使用redis-py库,默认只能处理utf-8编码,如果存在…...
【Redis】数据类型详解及其应用场景
目录 Redis 常⻅数据类型预备知识基本全局命令小结 数据结构和内部编码单线程架构引出单线程模型为什么单线程还能这么快 Redis 常⻅数据类型 Redis 提供了 5 种数据结构,理解每种数据结构的特点对于 Redis 开发运维⾮常重要,同时掌握每种数据结构的常⻅…...
PARA-Drive:设计并行模型实现端到端自动驾驶
论文链接 https://openaccess.thecvf.com/content/CVPR2024/papers/Weng_PARA-Drive_Parallelized_Architecture_for_Real-time_Autonomous_Driving_CVPR_2024_paper.pdfhttps://openaccess.thecvf.com/content/CVPR2024/papers/Weng_PARA-Drive_Parallelized_Architecture_fo…...
vs2022 x64 C/C++和汇编混编 遇到的坑
vs2022 x64 C/C和汇编混编 遇到的坑 遇到的问题二、问题复现1.出错代码2.问题分析2.1 堆栈对齐问题 3.解决方案 总结奇数和偶数个寄存器的影响为什么 sub rsp, 8 对奇数个寄存器有用?结论 遇到的问题 0x00007FFFFAE24A29 (msvcp140.dll)处(位于 TestCompileConsole…...
PHP概述、环境搭建与基本语法讲解
目录 【学习目标、重难点知识】 什么是网站? 1. PHP 介绍 1.1. PHP 概述 1.1.1. PHP 是什么? 1.1.2. PHP 都能做什么? 1.2. PHP 环境搭建 1.2.1. PhpStudy 2. PHP 基本语法 2.1. PHP 语法入门 2.1.1. 第一个 PHP 程序 2.1.2. PHP …...
实现信创Linux麦克风摄像头录制(源码,银河麒麟、统信UOS)
随着信创国产化浪潮的来临,在国产操作系统上的应用开发的需求越来越多,其中一个就是需要在银河麒麟或统信UOS上实现录制摄像头视频和麦克风声音,将它们录制成一个mp4文件。那么这个要如何实现了? 一. 技术方案 要完成这些功能&a…...
深度学习9--目标检测
1.概念介绍 目标检测不仅可以检测数字,而且可以检测动物的种类、汽车的种类等。例如,自动驾驶车辆需要自动识别前方物体是车辆还是行人,需要自动识别道路两 旁的指示牌和前方的红绿灯颜色。对于自动检测的算法,有两个要求…...
第131天:内网安全-横向移动Kerberos 攻击SPN扫描WinRMWinRSRDP
案例一:域横向移动-RDP-明文&NTLM RDP利用的三种方式 1.直接在当前被控主机上进行远程连接 2.建立节点进行连接 3.端口转发,(访问当前主机的2222端口等于访问目标的3389) 第一种方式(动静太大) 直接利用被控主机进行远程连接…...
微信小程序的四种弹窗使用
在做小程序的过程中,弹窗也算是非常实用的功能了,这几天写的几个功能就用到了弹窗,也可能是初学者的问题,比较菜,想找一个可以带图片的自定义的弹窗,,这里简单介绍一下官方封装好的四个弹窗…...
我的第一个CUDA程序
MatAdd算法 实现两个矩阵对应元素相加 #include <stdio.h> #include <stdlib.h>// 矩阵加法函数 void MatAdd(int height, int width) {// 在主机内存中为 A、B 和 C 分配内存float* A (float*)malloc(height * width * sizeof(float));float* B (float*)malloc…...
workerman下的webman路由浏览器跨域的一种问题
软件版本 "php": ">7.2", "workerman/webman-framework": "^1.5.0",问题情景 使用“分组路由”做API接口前后端分离跨域,在接口测试工具调试是能正常获取数据的;但在网页浏览器上调试就遇到了CORS、404的错…...
Windows11 -MASKRCNN-部署测试
文章目录 Detectron2环境配置搭建python 环境安装Cuda \CUDNN 、PyTorch、 torchvision、cudatoolkit1、Cuda \CUDNN2、 PyTorch、 torchvision、cudatoolkit进入python测试:错误信息 3、detectron2环境在安装detecteron中,遇到报错:编译的时…...
函数(子程序)的常见、易混淆概念详解【对初学者有帮助】
C语⾔中的函数也被称做子程序,意思就是⼀个完成某项特定的任务的⼀小段代码。 C语⾔标准中提供了许多库函数,点击下面的链接可以查看c语言的库函数和头文件。 C/C官⽅的链接:https://zh.cppreference.com/w/c/header 目录 一、函数头与函…...
TiDB-从0到1-DM工具
TiDB从0到1系列 TiDB-从0到1-体系结构TiDB-从0到1-分布式存储TiDB-从0到1-分布式事务TiDB-从0到1-MVCCTiDB-从0到1-部署篇TiDB-从0到1-配置篇TiDB-从0到1-集群扩缩容TiDB-从0到1-数据导出导入TiDB-从0到1-BR工具 一、DM原理 支持全量抽取数据\检测新的数据变化同步到下游实例…...
AppScan——Web 应用安全扫描的得力工具
一、引言 在当今数字化时代,Web 应用成为企业业务的重要支撑,但同时也面临着各种安全威胁。AppScan 作为一款专业的 Web 应用安全扫描工具,为保障 Web 应用的安全性提供了有力的支持。本文将对 AppScan 进行详细介绍,包括其功能、…...
T型翼/尾板导向的穿浪双体船姿态控制【附代码】
✨ 长期致力于穿浪双体船、T型翼、尾板、多自由度姿态控制、舒适性评估研究工作,擅长数据搜集与处理、建模仿真、程序编写、仿真设计。 ✅ 专业定制毕设、代码 ✅ 如需沟通交流,点击《获取方式》 (1)动态水翼升力模型与耦合运动方…...
用C语言解决‘换硬币’问题?我来教你如何调试和验证你的循环逻辑
用C语言解决‘换硬币’问题?我来教你如何调试和验证你的循环逻辑 当你第一次面对"换硬币"这类组合问题时,那种既兴奋又困惑的感觉我至今记忆犹新。作为C语言初学者,理解多重循环的运作机制就像在迷宫中寻找出口——每次你以为找到了…...
浅聊26上半年软考架构师
2026年上半年架构师考试已然落幕,大家都考的如何?架构师共有三门考试,上午综合知识(75道选择题)案例分析,时间为8.30-12.30;下午论文,时间为14.30-16.30。下面说说我整体的备考过程。…...
收藏必看|2026 版大厂 AI 岗位薪资曝光!普通程序员转型大模型最全指南
深夜收到大厂 HR 好友发来的内部资料,再三叮嘱切勿对外泄露。如今网络信息传播速度极快,这份 2026 年企业 AI 岗真实薪资内幕,也值得给广大程序员、零基础入行小白参考借鉴。 翻看完整薪资台账后,真切感受到当下大模型赛道的薪资差…...
BurpSuite 2025插件开发JDK版本兼容性实战指南
1. 为什么BurpSuite插件开发环境总在JDK版本上翻车?你是不是也经历过:下载好BurpSuite最新版2025.4,兴冲冲打开插件开发文档,照着官方示例写完第一个HelloWorld插件,一编译——java.lang.UnsupportedClassVersionError…...
串口通信粘包问题:成因深度解析与项目实战解决方案
在嵌入式开发、工业工控、上位机下位机交互项目中,串口(RS232/RS485)是最基础、最常用的通信方式。绝大多数开发者都遇到过这样的问题:串口接收的数据偶尔错乱、解析报错、数据拼接异常,单次接收的数据时而半包、时而多…...
第3篇:系统透视——信息部门如何构建“税务友好型”IT架构
本篇导读:如果你是信息总监或IT负责人,请通读全文,尤其是“系统合规设计的三必须”和“现场检查SOP”;如果你是财税人员,请重点阅读“研产供销全链条的系统对接要求”和“与IT部门的协作要点”;如果你是老板…...
0.2毫秒快速启动的操作系统
在工业控制以及航空航天等核心场景,极速启动就是高可靠系统的生命线。0.2毫秒超快启动搭配硬件看门狗,让设备在掉电重启、异常恢复时瞬时归位,关键任务永不延误! https://www.bilibili.com/video/BV11mLY6VERt/?spm_id_from333.1…...
双系统Ubuntu磁盘告急?别重装!用GParted无损扩容保姆级教程(附U盘启动盘制作)
双系统Ubuntu磁盘告急?别重装!用GParted无损扩容保姆级教程(附U盘启动盘制作)当你在Windows和Ubuntu双系统环境下工作时,是否遇到过这样的窘境:当初安装时给Ubuntu分配的空间捉襟见肘,而Windows…...
2026年一键生成论文工具对比实测:5款神器从选题到格式全流程护航
写论文的焦虑,是每个科研人和学生都心照不宣的“隐形压力”。选题无从下手,文献检索耗时费力,逻辑框架反复推翻,格式排版让人抓狂,查重降重更是像在和系统玩“猫鼠游戏”。2026年的AI工具早已不是过去那种“打字机”&a…...
