一文了解 Android 车机如何处理中控的旋钮输入?

news/2024/4/23 13:25:18/文章来源:https://blog.csdn.net/allisonchen/article/details/132008554

前言

上篇文章《从实体按键看 Android 车载的自定义事件机制》带大家了解了 Android 车机支持自定义输入的机制 CustomInputService。事实上,除了支持自定义事件,对于中控上常见的音量控制、焦点控制的旋钮事件,Android 车机也是支持的。

那本篇文章带大家看下 Android 车机处理旋钮事件的内在原理:

  1. 定义
  2. 监听和订阅
  3. 接收
  4. 处理
  5. 模拟

1. 定义

和自定义输入所支持的事件一致,支持旋钮输入的事件类型也在如下文件 types.hal 中定义。

// hardware/interfaces/automotive/vehicle/2.0/types.hal/*** Property to feed H/W rotary events to android* ...*/HW_ROTARY_INPUT = (0x0A20| VehiclePropertyGroup:SYSTEM| VehiclePropertyType:INT32_VEC| VehicleArea:GLOBAL),enum RotaryInputType : int32_t {ROTARY_INPUT_TYPE_SYSTEM_NAVIGATION = 0,ROTARY_INPUT_TYPE_AUDIO_VOLUME = 1,
};

HW_ROTARY_INPUT 代表该事件在底层的 Property 定义,供 VehicleHal 对其发起监听。

该事件涵盖了一些旋钮所必须的数据:

  • 第 0 位代表哪种旋钮硬件,由 RotaryInputType 枚举细分,包括控制焦点的旋钮 TYPE_SYSTEM_NAVIGATION 和控制音量的旋钮 TYPE_AUDIO_VOLUME
  • 第 1 位代表旋转计数,正数代表顺时针计数 clockwise,负数代表逆时针计数 counterclockwise
  • 第 2 位代表旋钮事件的目标屏幕 VehicleDisplay,默认是 MAIN,即 center console,中控屏幕
  • 第 3 位及以后代表持续计数事件之间的时间差,单位为 ns

2. 监听和订阅

上层处理事件输入的 CarInputService 在初始化的时候,会向调度车机输入的中间层 InputHalService 注册监听。

// packages/services/Car/service/src/com/android/car/CarInputService.java
public class CarInputService ... {...@Overridepublic void init() {if (!mInputHalService.isKeyInputSupported()) {return;}mInputHalService.setInputListener(this);...}...
}

InputHalService 判断支持旋钮输入的话,向和 HAL 层交互的 VehicleHal 注册 HW_ROTARY_INPUT Property 的订阅。

// packages/services/Car/service/src/com/android/car/hal/InputHalService.java
public class InputHalService extends HalServiceBase {...public void setInputListener(InputListener listener) {...boolean rotaryInputSupported;synchronized (mLock) {mListener = listener;...rotaryInputSupported = mRotaryInputSupported;}...if (rotaryInputSupported) {mHal.subscribeProperty(this, HW_ROTARY_INPUT);}...}public boolean isRotaryInputSupported() {synchronized (mLock) {return mRotaryInputSupported;}}...
}

3. 接收

当旋钮事件发生,将通过 HAL 层抵达上述订阅该 Property 的 VehicleHal,其将找出处理方 HalServiceBaseInputHalService 并继续分发。

// packages/services/Car/service/src/com/android/car/hal/VehicleHal.java
public class VehicleHal implements HalClientCallback {...@Overridepublic void onPropertyEvent(ArrayList<HalPropValue> propValues) {synchronized (mLock) {for (int i = 0; i < propValues.size(); i++) {HalPropValue v = propValues.get(i);int propId = v.getPropId();HalServiceBase service = mPropertyHandlers.get(propId);if (service == null) {continue;}service.getDispatchList().add(v);mServicesToDispatch.add(service);VehiclePropertyEventInfo info = mEventLog.get(propId);if (info == null) {info = new VehiclePropertyEventInfo(v);mEventLog.put(propId, info);} else {info.addNewEvent(v);}}}for (HalServiceBase s : mServicesToDispatch) {s.onHalEvents(s.getDispatchList());s.getDispatchList().clear();}mServicesToDispatch.clear();}...
}

InputHalService 首先确保上层的 InputListener 确实存在,此后再检查该 HalProperty 是何种类型。HW_ROTARY_INPUT 旋钮事件的话调用 dispatchRotaryInput() 继续。

public class InputHalService extends HalServiceBase {...@Overridepublic void onHalEvents(List<HalPropValue> values) {InputListener listener;synchronized (mLock) {listener = mListener;}if (listener == null) {return;}for (int i = 0; i < values.size(); i++) {HalPropValue value = values.get(i);switch (value.getPropId()) {case HW_ROTARY_INPUT:dispatchRotaryInput(listener, value);break;...}}}...
}

dispatchRotaryInput() 将执行如下步骤:

  1. 检查必要数据是否齐全,即起码包括旋钮硬件类型、旋钮计数、目标屏幕这 3 位
  2. 按照 index 取出这三位数据
  3. 检查旋钮计数是否为 0,因为无法判断 0 是顺时针还是逆时针
  4. 检查目标屏幕是否为中控屏幕 MAIN、仪表屏幕 INSTRUMENT_CLUSTER 中的一个
  5. 检查旋钮计数的时间差数值位数是否匹配(比如:旋转了 3 格的话,那么时间差必须要占 2 位)
  6. 根据旋钮硬件类型转化为 CarInputManager 中定义的事件类型
    • 焦点控制的话转换为 INPUT_TYPE_ROTARY_NAVIGATION
    • 音量控制的话转换为 INPUT_TYPE_ROTARY_VOLUME
  7. 提取持续计数的时间差到 timestamps 数组中
  8. 根据旋钮计数方向,转换到的事件类型以及时间差数组封装 RotaryEvent 对象交由 InputListener 继续分发
public class InputHalService extends HalServiceBase {...private void dispatchRotaryInput(InputListener listener, HalPropValue value) {int timeValuesIndex = 3;  // remaining values are time deltas in nanosecondsif (value.getInt32ValuesSize() < timeValuesIndex) {return;}int rotaryInputType = value.getInt32Value(0);int detentCount = value.getInt32Value(1);int vehicleDisplay = value.getInt32Value(2);long timestamp = value.getTimestamp();  // for first detent, uptime nanosecondsboolean clockwise = detentCount > 0;detentCount = Math.abs(detentCount);if (detentCount == 0) { // at least there should be one eventreturn;}if (vehicleDisplay != VehicleDisplay.MAIN&& vehicleDisplay != VehicleDisplay.INSTRUMENT_CLUSTER) {return;}if (value.getInt32ValuesSize() != (timeValuesIndex + detentCount - 1)) {return;}int carInputManagerType;switch (rotaryInputType) {case ROTARY_INPUT_TYPE_SYSTEM_NAVIGATION:carInputManagerType = CarInputManager.INPUT_TYPE_ROTARY_NAVIGATION;break;case ROTARY_INPUT_TYPE_AUDIO_VOLUME:carInputManagerType = CarInputManager.INPUT_TYPE_ROTARY_VOLUME;break;default: ...}long[] timestamps = new long[detentCount];long uptimeToElapsedTimeDelta = CarServiceUtils.getUptimeToElapsedTimeDeltaInMillis();...RotaryEvent event = new RotaryEvent(carInputManagerType, clockwise, timestamps);listener.onRotaryEvent(event, convertDisplayType(vehicleDisplay));}...
}

4. 处理

监听章节里提到 InputListener 为 CarInputService,所以将传递到 CarInputService 的 onRotaryEvent() 进行处理。

onRotaryEvent() 先检查是否有使用 InputEventCapture 监听旋钮事件的 Service 存在:

  • 如果有监听,交由 Capture 该事件的 Service 专门处理
  • 如果没有,转换为 Android 标准 KeyEvent 进行处理
// packages/services/Car/service/src/com/android/car/CarInputService.java
public class CarInputService ... {...@Overridepublic void onRotaryEvent(RotaryEvent event, @DisplayTypeEnum int targetDisplay) {if (!mCaptureController.onRotaryEvent(targetDisplay, event)) {List<KeyEvent> keyEvents = rotaryEventToKeyEvents(event);for (KeyEvent keyEvent : keyEvents) {onKeyEvent(keyEvent, targetDisplay);}}}...
}

专门处理

Car App 提供了一个专门控制焦点的 RotaryService,它在绑定时通过 CarInputManager 的 requestInputEventCapture() 申请监听了 INPUT_TYPE_ROTARY_NAVIGATION 类型的旋钮事件。

// packages/apps/Car/RotaryController/src/com/android/car/rotary/RotaryService.java
public class RotaryService ... {/** Input types to capture. */private final int[] mInputTypes = new int[]{CarInputManager.INPUT_TYPE_ROTARY_NAVIGATION,...};...@Overridepublic void onServiceConnected() {super.onServiceConnected();mCar = Car.createCar(this, null, Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER,(car, ready) -> {mCar = car;if (ready) {mCarInputManager =(CarInputManager) mCar.getCarManager(Car.CAR_INPUT_SERVICE);...mCarInputManager.requestInputEventCapture(CarOccupantZoneManager.DISPLAY_TYPE_MAIN,mInputTypes,CarInputManager.CAPTURE_REQ_FLAGS_ALLOW_DELAYED_GRANT,/* callback= */ this);}});...}...
}

自然的,RotaryService 的 onRotaryEvent() 会得到调用,首先将检查目标屏幕是否符合预期,必须是 MAIN 即中控屏幕。通过的话,调用 handleRotaryEvent() 继续处理。

public class RotaryService ... {...@Overridepublic void onRotaryEvents(int targetDisplayType, @NonNull List<RotaryEvent> events) {if (!isValidDisplayType(targetDisplayType)) {return;}for (RotaryEvent rotaryEvent : events) {handleRotaryEvent(rotaryEvent);}}private static boolean isValidDisplayType(int displayType) {if (displayType == CarOccupantZoneManager.DISPLAY_TYPE_MAIN) {return true;}return false;}...
}

handleRotaryEvent() 将检查 RotaryEvent 中的硬件 type,确保确实来自于焦点控制旋钮 INPUT_TYPE_ROTARY_NAVIGATION,通过的话调用 handleRotateEvent() 继续。

public class RotaryService ... {...private void handleRotaryEvent(RotaryEvent rotaryEvent) {if (rotaryEvent.getInputType() != CarInputManager.INPUT_TYPE_ROTARY_NAVIGATION) {return;}boolean clockwise = rotaryEvent.isClockwise();int count = rotaryEvent.getNumberOfClicks();long eventTime = rotaryEvent.getUptimeMillisForClick(count - 1);handleRotateEvent(clockwise, count, eventTime);}...
}

handleRotateEvent() 主要是依据屏幕的设置和当前 focus 的 Node 情况来决定是调用 performScrollAction() 执行屏幕滚动,还是寻找到目标 Node 调用 performFocusAction() 来执行焦点的移动。

其本质上是通过 InputManager 向系统注入 SCROLL 触摸事件,或者通过 Accessibility 向上面的或下面的待 focus 的 AccessibilityNode 发送 FOCUS Action 操作。

public class RotaryService ... {...private void handleRotateEvent(boolean clockwise, int count, long eventTime) {int rotationCount = getRotateAcceleration(count, eventTime);if (mInProjectionMode) {injectMotionEvent(DEFAULT_DISPLAY, clockwise ? rotationCount : -rotationCount);return;}if (initFocus() || mFocusedNode == null) {return;}if (mInDirectManipulationMode) {if (DirectManipulationHelper.supportRotateDirectly(mFocusedNode)) {performScrollAction(mFocusedNode, clockwise);} else {AccessibilityWindowInfo window = mFocusedNode.getWindow();if (window == null) {L.w("Failed to get window of " + mFocusedNode);return;}int displayId = window.getDisplayId();window.recycle();injectMotionEvent(displayId, clockwise ? rotationCount : -rotationCount);}return;}int remainingRotationCount = rotationCount;int direction = clockwise ? View.FOCUS_FORWARD : View.FOCUS_BACKWARD;Navigator.FindRotateTargetResult result =mNavigator.findRotateTarget(mFocusedNode, direction, rotationCount);if (result != null) {if (performFocusAction(result.node)) {remainingRotationCount -= result.advancedCount;}Utils.recycleNode(result.node);} else {L.w("Failed to find rotate target from " + mFocusedNode);}if (remainingRotationCount > 0 && isInFocusedWindow(mFocusedNode)) {AccessibilityNodeInfo scrollableContainer =mNavigator.findScrollableContainer(mFocusedNode);if (scrollableContainer != null) {injectScrollEvent(scrollableContainer, clockwise, remainingRotationCount);scrollableContainer.recycle();}}}...
}

标准处理

和导航旋钮事件不同,系统没有 Capture 音量旋钮事件 INPUT_TYPE_ROTARY_VOLUME 的 Service,那么它得执行标准处理。

首先,得将 RotatryEvent 转换为标准的按键编号 Key Code,具体的执行如下逻辑:

  1. 焦点控制按钮的话,依据方向 mapping 顺时针为焦点前进的 KEYCODE_NAVIGATE_NEXT,逆时针为焦点后退的 KEYCODE_NAVIGATE_PREVIOUS
  2. 音量控制按钮的话,mapping 为音量 +/- Key Code,顺时针为 KEYCODE_VOLUME_UP,逆时针则是 KEYCODE_VOLUME_DOWN
  3. 按照计数次数批量调用 createKeyEvent() 创建 KeyEvent 对象,并添加到待处理 keyEvents 列表中。
public class CarInputService ... {...private static List<KeyEvent> rotaryEventToKeyEvents(RotaryEvent event) {int numClicks = event.getNumberOfClicks();int numEvents = numClicks * 2; // up / down per each clickboolean clockwise = event.isClockwise();int keyCode;switch (event.getInputType()) {case CarInputManager.INPUT_TYPE_ROTARY_NAVIGATION:keyCode = clockwise? KeyEvent.KEYCODE_NAVIGATE_NEXT: KeyEvent.KEYCODE_NAVIGATE_PREVIOUS;break;case CarInputManager.INPUT_TYPE_ROTARY_VOLUME:keyCode = clockwise? KeyEvent.KEYCODE_VOLUME_UP: KeyEvent.KEYCODE_VOLUME_DOWN;break;...}ArrayList<KeyEvent> keyEvents = new ArrayList<>(numEvents);for (int i = 0; i < numClicks; i++) {long uptime = event.getUptimeMillisForClick(i);KeyEvent downEvent = createKeyEvent(/* down= */ true, uptime, uptime, keyCode);KeyEvent upEvent = createKeyEvent(/* down= */ false, uptime, uptime, keyCode);keyEvents.add(downEvent);keyEvents.add(upEvent);}return keyEvents;}    ...
}

接着,遍历准备好的 keyEvents 列表,逐个处理。

public class CarInputService ... {...@Overridepublic void onRotaryEvent(RotaryEvent event, @DisplayTypeEnum int targetDisplay) {if (!mCaptureController.onRotaryEvent(targetDisplay, event)) {List<KeyEvent> keyEvents = rotaryEventToKeyEvents(event);// 遍历列表,逐个处理for (KeyEvent keyEvent : keyEvents) {onKeyEvent(keyEvent, targetDisplay);}}}...
}

CarInputService 的 onKeyEvent() 直接处理的 Code 只有激活语音助手的 KEYCODE_VOICE_ASSIST 和拨打电话的 KEYCODE_CALL。其他的 Key Code 执行一般处理:

  1. 如果目标屏幕是 INSTRUMENT_CLUSTER 即仪表屏幕的话,调用 handleInstrumentClusterKey()InstrumentClusterKeyListener 执行仪表上的事件,貌似是 Cluster app 完成,具体不再展开
  2. 检查是否有使用 InputEventCapture 监听 NAVIGATE_ 焦点控制、VOLUME_ 音量控制 KeyEvent 的 Service 存在,有的话回调 onKeyEvent() Callback
  3. 如果没有 Capture 处理的好,告知 KeyEventListener 进行兜底处理
public class CarInputService ... {...@Overridepublic void onKeyEvent(KeyEvent event, @DisplayTypeEnum int targetDisplayType) {// Special case key code that have special "long press" handling for automotiveswitch (event.getKeyCode()) {case KeyEvent.KEYCODE_VOICE_ASSIST:handleVoiceAssistKey(event);return;case KeyEvent.KEYCODE_CALL:handleCallKey(event);return;default:break;}assignDisplayId(event, targetDisplayType);// Allow specifically targeted keys to be routed to the clusterif (targetDisplayType == CarOccupantZoneManager.DISPLAY_TYPE_INSTRUMENT_CLUSTER&& handleInstrumentClusterKey(event)) {return;}if (mCaptureController.onKeyEvent(targetDisplayType, event)) {return;}mMainDisplayHandler.onKeyEvent(event);}...
}

KeyEventListener 在 CarInputService 初始化的时候指定,具体的就是通过 InputManagerHelper 注入 KeyEvent。

public class CarInputService ... {...private final KeyEventListener mMainDisplayHandler;public CarInputService( ... ) {this(context, inputHalService, userService, occupantZoneService, bluetoothService,new Handler(CarServiceUtils.getCommonHandlerThread().getLooper()),context.getSystemService(TelecomManager.class),event -> InputManagerHelper.injectInputEvent(context.getSystemService(InputManager.class), event),() -> Calls.getLastOutgoingCall(context),() -> getViewLongPressDelay(context),() -> context.getResources().getBoolean(R.bool.config_callButtonEndsOngoingCall),new InputCaptureClientController(context));}...
}

InputManagerHelper 没啥特别的,直接调用 InputManager 的标准方法 injectInputEvent() 完成注入,后续由 InputManagerService 开始 Dispatch、Transport 等一系列处理。

// packages/services/Car/car-builtin-lib/src/android/car/builtin/input/InputManagerHelper.java
public class InputManagerHelper {...public static boolean injectInputEvent(@NonNull InputManager inputManager,@NonNull android.view.InputEvent event) {return inputManager.injectInputEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);}
}

5. 模拟

当旋钮按键环境尚未到位的时候,我们可以使用 adb 命令模拟旋钮事件来验证代码链路。

格式:

adb shell cmd car_service inject-rotary [-d display] [-i input_type] [-c clockwise] [-dt delta_times_ms]
  • display,目标屏幕:0 代表中控屏幕,1 代表仪表屏幕,默认是 0
  • input_type,按钮类型: 10 代表焦点控制,11 代表音量控制,默认是 10
  • clockwise,旋钮方向: true 代表顺时针方向,false 代表逆时针,默认是 false
  • delta_times_ms,持续旋转计数的时间间隔:多次旋转事件和当前时刻的间隔列表,按降序排列,默认是 0,表示只有一次旋转

下面将介绍几个命令示例,帮助大家更好地理解该命令的使用。

adb shell cmd car_service inject-rotary

没有指定任何参数,全部都是默认的操作,表示针对中控屏幕发送焦点控制的旋钮事件,方向为逆时针、焦点后退 1 格

adb shell cmd car_service inject-rotary -d 1 -i 11 -c true

表示针对仪表屏幕发送音量控制的旋钮事件,方向为顺时针、调低 1 格

adb shell cmd car_service inject-rotary -c true -dt 100 50

表示针对中控屏幕发送焦点控制的旋钮事件,方向为顺时针、3 次计数、焦点前进 3 格

结语

与自定义输入相比,旋钮事件的处理流程有细微差异,主要体现在 CarInputService 会针对音量、焦点两种的旋钮控制,存在特定的处理逻辑。最后,结合一张图回顾下整体流程:

  1. 支持音量控制焦点控制的两种旋钮硬件产生 HW_ROTARY_INPUT Propery 变化

  2. 由和 HAL 层交互的 VehicleHal 订阅到 Propery 变化,将事件提取为 HalPropValue 类型

  3. 并发送给车机输入的中间服务 InputHalService 接收和进一步地封装为 RotaryEvent 类型

  4. 分发到处理事件输入的专用服务 CarInputService

    a. 如果有 Capture 音量/焦点的 Rotary 事件的交由其专门处理:Car App 的 RotaryService,其将决定通过 InputManager 注入 SCROLL 滚动还是通过 Accessibility 触发焦点 Focus 操作;

    b. 如果没有,则执行标准处理:

    • 首先按照 Rotary 类型和旋钮方向、计数封装为 Android 标准 KeyEvent 列表
    • 如果目标屏幕为仪表的话,列表交由 Cluster App 处理
    • 反之检查是否有 Capture 该 KeyEvent 的 Service 需要处理
    • 最后交由 InputManager 逐个注入该 KeyEvent,继而由系统的 InputManagerService 进行调度

推荐阅读

  • 从实体按键看 Android 车载的自定义事件机制
  • 如何打造车载语音交互:Google Voice Interaction 给你答案
  • Android 车机初体验:Auto,Automotive 傻傻分不清楚?

参考文档

  • https://developer.android.google.cn/training/cars
  • https://source.android.google.cn/docs/devices/automotive/hmi/rotary_controller/app_developers

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

如若内容造成侵权/违法违规/事实不符,请联系编程新知网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于YOLOv5的S弯识别

基于YOLOv5的S弯识别 目录 基于YOLOv5的S弯识别技术背景算法介绍具体实现1、下载仓库2、配置环境3、数据处理4、转成engine文件5、使用代码实现识别 技术总结 技术背景 S弯识别是一个在自动驾驶和机器人领域中很常见的任务&#xff0c;它需要识别道路上的弯道&#xff0c;特别…

【机器学习】Cost Function

Cost Function 1、计算 cost2、cost 函数的直观理解3、cost 可视化总结附录 首先&#xff0c;导入所需的库&#xff1a; import numpy as np %matplotlib widget import matplotlib.pyplot as plt from lab_utils_uni import plt_intuition, plt_stationary, plt_update_onclic…

EXCEL,如何比较2个表里的数据差异(使用数据透视表)

目录 1 问题: 需要比较如下2个表的内容差异 1.1 原始数据喝问题 1.2 提前总结 2 使用EXCEL公式方法 2.1 新增辅助列&#xff1a; 辅助index 2.2 具体公式 配合条件格式 使用 3 数据透视表方法 3.1 新增辅助列&#xff1a; 辅助index 3.2 需要先打开 数据透视表向导 …

使用 NVM(Node Version Manager)管理 Node.js 版本

使用 NVM&#xff08;Node Version Manager&#xff09;管理 Node.js 版本 步骤一&#xff1a;安装 NVM NVM 是一个用于安装和管理不同版本的 Node.js 的工具。首先&#xff0c;你需要确保你的系统上已经安装了 NVM。可以通过以下命令检查 NVM 是否已经安装&#xff1a; nvm …

1.Flink概述

1.1 技术架构 应用框架层: 在API层之上构建的满足特定应用场景的计算框架&#xff0c;总体上分为流计算和批处理两类应用框架。API 层&#xff1a; Flink对外提供能力的接口 &#xff0c;实现了面向流计算的DataStream API和面向批处理的DataSet API。运行时层&#xff1a;Flin…

微信小程序,商城底部工具栏的实现

效果演示&#xff1a; 前提条件&#xff1a; 去阿里云矢量图标&#xff0c;下载8个图标&#xff0c;四个黑&#xff0c;四个红&#xff0c;如图&#xff1a; 新建文件夹icons&#xff0c;把图标放到该文件夹&#xff0c;然后把该文件夹移动到该项目的文件夹里面。如图所示 app…

嵌入式开发:单片机嵌入式Linux学习路径

SOC&#xff08;System on a Chip&#xff09;的本质区别在于架构和功能。低端SOC如基于Cortex-M架构的芯片&#xff0c;如STM32和NXP LPC1xxx系列&#xff0c;不具备MMU&#xff08;Memory Management Unit&#xff09;&#xff0c;适用于轻量级实时操作系统如uCOS和FreeRTOS。…

细讲TCP三次握手四次挥手(三)

TCP/IP 协议族 在互联网使用的各种协议中最重要和最著名的就是 TCP/IP 两个协议。现在人们经常提到的 TCP/IP 并不一定是单指 TCP 和 IP 这两个具体的协议&#xff0c;而往往是表示互联网所使用的整个 TCP/IP 协议族。 互联网协议套件&#xff08;英语&#xff1a;Internet Pr…

《吐血整理》进阶系列教程-拿捏Fiddler抓包教程(17)-Fiddler如何充当第三者再识AutoResponder标签-下

1.简介 上一篇宏哥主要讲解的一些在电脑端的操作和应用&#xff0c;今天宏哥讲解和分享一下&#xff0c;在移动端的操作和应用。其实移动端和PC端都是一样的操作&#xff0c;按照宏哥前边抓取移动端包设置好&#xff0c;就可以开始实战了。 2.界面功能解析 根据下图图标注位…

基于vue+uniapp微信小程序公司企业后勤服务(设备)系统

本系统分为用户和管理员两个角色&#xff0c;其中用户可以注册登陆系统&#xff0c;查看公司公告&#xff0c;查看设备&#xff0c;设备入库&#xff0c;查看通讯录&#xff0c;会议室预约&#xff0c;申请出入&#xff0c;申请请假等功能。管理员可以对员工信息&#xff0c;会…

JS正则表达式:常用正则手册/RegExp/正则积累

一、正则基础语法 JavaScript 正则表达式 | 菜鸟教程 JS正则表达式语法大全&#xff08;非常详细&#xff09; 二、使用场景 2.1、校验中国大陆手机号的正则表达式 正则 /^1[3456789]\d{9}$/解释 序号正则解释1^1以数字 1 开头2[3456789]第二位可以是 3、4、5、6、7、8、…

Unity 四元素

//-------------旋转------------ // //设置角度 (超过90或负数时&#xff0c;会设置-1结果是359这样的问题&#xff0c;可以使用下面旋转的方式) transform.rotate new Quaternion(0,0,0,0);//Quaternion四元数 transform.localEulerAngles new Vector3(0,0,0);//EulerA…

Windows环境下安装及部署Nginx

一、安装Nginx教程 1、官网下载地址&#xff1a;https://nginx.org/en/download.html 2、下载教程&#xff1a;选择Stable version版本下载到本地 3、下载完成后&#xff0c;解压放入本地非中文的文件夹中&#xff1a; 4、启动nginx&#xff1a;双击nginx.exe&#xff0c;若双击…

【计算机网络】408统考2014年题36

题目描述 【2014年题36】主机甲与主机乙之间使用后退N帧(GBN)协议传输数据&#xff0c;甲的发送窗口尺寸为1000&#xff0c;数据帧长为1000字节&#xff0c;信道带宽为100Mbps&#xff0c;乙每收到一个数据帧就立即利用一个短帧&#xff08;忽略其传输延迟&#xff09;进行确认…

TCP网络通信编程之字节流

目录 【TCP字节流编程】 // 网络编程中&#xff0c;一定是server端先运行 【案例1】 【思路分析】 【客户端代码】 【服务端代码】 【结果展示】 【案例2】 【题目描述】 【注意事项】 【服务端代码】 【客户端代码】 【代码结果】 【TCP字节流编程】 // 网络编程中&a…

iTOP-RK3568开发板Docker 安装 Ubuntu 18.04

Docker 下载安装 Ubuntu18.04&#xff0c;输入以下命令&#xff1a; sudo apt update docker pull ubuntu:18.04 切换 Shell 到 Ubuntu 18.04&#xff0c;输入以下命令&#xff1a; docker container run -p 8000:3000 -it ubuntu:18.04 /bin/bash -p 参数&#xff1a;容器的…

在CSDN学Golang云原生(Kubernetes声明式资源管理Kustomize)

一&#xff0c;生成资源 在 Kubernetes 中&#xff0c;我们可以通过 YAML 或 JSON 文件来定义和创建各种资源对象&#xff0c;例如 Pod、Service、Deployment 等。下面是一个简单的 YAML 文件示例&#xff0c;用于创建一个 Nginx Pod&#xff1a; apiVersion: v1 kind: Pod m…

C++STL库中的list

文章目录 list的介绍及使用 list的常用接口 list的模拟实现 list与vector的对比 一、list的介绍及使用 1. list是可以在常数范围内在任意位置进行插入和删除的序列式容器&#xff0c;并且该容器可以前后双向迭代。 2. list的底层是双向带头循环链表结构&#xff0c;双向带头循…

ansible-playbook

playbook的组成 playbooks 本身由以下各部分组成 &#xff08;1&#xff09;Tasks&#xff1a;任务&#xff0c;即通过 task 调用 ansible 的模板将多个操作组织在一个 playbook 中运行 &#xff08;2&#xff09;Variables&#xff1a;变量 &#xff08;3&#xff09;Templat…

mac下安装vue cli脚手架并搭建一个简易项目

目录 1、确定本电脑下node和npm版本是否为项目所需版本。 2、下载vue脚手架 3、创建项目 1、下载node。 如果有node&#xff0c;打开终端&#xff0c;输入node -v和npm -v , 确保node和npm的版本&#xff0c;(这里可以根据自己的需求去选择&#xff0c;如果对最新版本的内容有…