# 触摸与点击事件

用户的鼠标点击、在触屏设备上手指触摸是最常见的界面交互方式。在这些情况下，小程序基础库会提供一系列事件来描述用户的触摸与点击状态。

## 触摸与点击事件列表

所有触摸与点击事件都是冒泡事件。具体来说，它们在冒泡时都可以穿越组件边界，而且具有捕获阶段（相关概念请参考 [事件系统](/miniprogram/dev/framework/component-framework/event-system) 章节）。

### Touch 类事件

Touch 类事件描述单个手指在屏幕上的触摸状态。

| 类型        | 触发条件                                                                         | 最低版本                |
| ----------- | ------------------------------------------------------------------------------- | ---------------------- |
| touchstart  | 手指触摸动作开始                                                                  |                        |
| touchmove   | 手指触摸后移动                                                                    |                        |
| touchcancel | 手指触摸动作被打断，如来电提醒，弹窗                                                |                        |
| touchend    | 手指触摸动作结束                                                                  |                        |
| touchforcechange | 在支持 3D Touch 的设备上，重按时会触发              | [1.9.90](/miniprogram/dev/framework/compatibility) |

Touch 类事件在 event 对象上有 `touches` 和 `changedTouches` 两个数组字段，分别表示当前处于触摸状态的手指触摸信息、发生变化了的手指触摸信息。

每个触摸信息对象，主要包含有以下几个字段。

| 字段名 | 含义 |
| ------ | ---- |
| identifier | 手指标识符，用来表示它对应于哪个手指 |
| clientX | 触摸位置的水平坐标（相对于视口） |
| clientY | 触摸位置的竖直坐标（相对于视口） |
| pageX | 触摸位置的水平坐标（相对于页面，计入页面滚动位置） |
| pageY | 触摸位置的竖直坐标（相对于页面，计入页面滚动位置） |

### Tap 类事件

Tap 类事件用来表达一次“点击“。它可以是触摸引发的，也可以是鼠标点击引发的。

| 类型        | 触发条件                                                                         | 最低版本                |
| ----------- | ------------------------------------------------------------------------------- | ---------------------- |
| tap         | 手指触摸后马上离开、鼠标单击                                                        |                        |
| longpress   | 手指触摸（或鼠标长按）超过 350ms 再离开，如果指定了事件回调函数并触发了这个事件，tap 事件将不被触发  | [1.5.0](/miniprogram/dev/framework/compatibility) |
| longtap     | 手指触摸（或鼠标长按）超过 350ms 再离开（推荐使用longpress事件代替）                             |                        |

Tap 类事件的 detail 包含以下几个字段。

| 字段名 | 含义 |
| ------ | ---- |
| x | 水平坐标（相对于视口） |
| y | 竖直坐标（相对于视口） |

### Pointer 事件

在 PC 上，pointer 事件也会被触发，可以使用 pointer 事件来实现划入、右键等定制能力（类似于 [web 标准](https://developer.mozilla.org/zh-CN/docs/Web/API/Pointer_events) 的 pointer 事件）。

示例：

```
<view
  bind:pointerdown="pointerdown"
  bind:pointerup="pointerup"
  bind:pointermove="pointermove"
>
  测试代码
</view>
```

右键响应的示例：[在开发者工具中预览效果](https://developers.weixin.qq.com/s/l1pW2qmy7RZJ)

## 点击态

为了防止敏感接口被滥用，部分 API 在触发时会校验「当前调用是否来自用户的点击操作」。这个校验依赖的就是**点击态**——只有当调用发生在用户点击后的有效范围内，接口才会正常执行。

**如果这类接口在没有点击态时被调用，会报错：**

```
can only be invoked by user TAP gesture
```

需要点击态的 API 接口：

| API | 说明 |
| --- | --- |
| [wx.openSetting](/miniprogram/dev/api/open-api/setting/wx.openSetting.md) |  |
| [wx.requestSubscribeDeviceMessage](/miniprogram/dev/api/open-api/subscribe-message/wx.requestSubscribeDeviceMessage.md) |  |
| [wx.exitMiniProgram](/miniprogram/dev/api/navigate/wx.exitMiniProgram.md) | |
| [wx.requestSubscribeMessage](/miniprogram/dev/api/open-api/subscribe-message/wx.requestSubscribeMessage.md) | 也可以在 [wx.requestPayment](/miniprogram/dev/api/payment/wx.requestPayment.md) / [wx.requestOrderPayment](/miniprogram/dev/api/payment/wx.requestOrderPayment.md) 的回调中调用 |
| [wx.addPhoneCalendar](/miniprogram/dev/api/device/calendar/wx.addPhoneCalendar.md) | |
| [wx.addPhoneRepeatCalendar](/miniprogram/dev/api/device/calendar/wx.addPhoneRepeatCalendar.md) | |
| [wx.shareFileMessage](/miniprogram/dev/api/share/wx.shareFileMessage.md) | |
| [wx.shareVideoMessage](/miniprogram/dev/api/share/wx.shareVideoMessage.md) | |
| [wx.addFileToFavorites](/miniprogram/dev/api/open-api/favorites/wx.addFileToFavorites.md) | |
| [wx.addVideoToFavorites](/miniprogram/dev/api/open-api/favorites/wx.addVideoToFavorites.md) | |
| [wx.getUserProfile](/miniprogram/dev/api/open-api/user-info/wx.getUserProfile.md) | |
| [wx.openCustomerServiceChat](/miniprogram/dev/api/open-api/service-chat/wx.openCustomerServiceChat.md) | |
| [wx.chooseLicensePlate](/miniprogram/dev/api/open-api/license-plate/wx.chooseLicensePlate.md) | |
| [wx.openSystemBluetoothSetting](/miniprogram/dev/api/base/system/wx.openSystemBluetoothSetting.md) | |
| [wx.openAppAuthorizeSetting](/miniprogram/dev/api/base/system/wx.openAppAuthorizeSetting.md) | |
| [wx.openOfficialAccountArticle](/miniprogram/dev/api/navigate/wx.openOfficialAccountArticle.md) | |
| [wx.notifyGroupMembers](/miniprogram/dev/api/chattool/wx.notifyGroupMembers.md) | |
| [wx.shareFileToGroup](/miniprogram/dev/api/chattool/wx.shareFileToGroup.md) | |
| [wx.shareImageToGroup](/miniprogram/dev/api/chattool/wx.shareImageToGroup.md) | |
| [wx.shareVideoToGroup](/miniprogram/dev/api/chattool/wx.shareVideoToGroup.md) | |
| [wx.openOfficialAccountProfile](/miniprogram/dev/api/navigate/wx.openOfficialAccountProfile.md) | |
| [wx.shareToOfficialAccount](/miniprogram/dev/api/share/wx.shareToOfficialAccount.md) | |
| [wx.openChannelsLiveNoticeInfo](/miniprogram/dev/api/open-api/channels/wx.openChannelsLiveNoticeInfo.md) | |
| [wx.openInquiriesTopic](/miniprogram/dev/api/navigate/wx.openInquiriesTopic.md) | |
| [wx.openDesignerProfile](/miniprogram/dev/api/open-api/sticker/wx.openDesignerProfile.md) | |
| [wx.navigateToMiniProgram](/miniprogram/dev/api/navigate/wx.navigateToMiniProgram.md) | 只要用户点击过页面任意位置（且中途未离开当前小程序）即可调用，不要求调用发生在 tap 事件回调内 |
| [wx.openEmbeddedMiniProgram](/miniprogram/dev/api/navigate/wx.openEmbeddedMiniProgram.md) | 无点击态时**不会报错**，而是降级为 [wx.navigateToMiniProgram](/miniprogram/dev/api/navigate/wx.navigateToMiniProgram.md) 打开全屏小程序；但若用户从未点击过页面任意位置，仍会报错 |
| [wx.navigateTo](/miniprogram/dev/api/route/wx.navigateTo.md)、[wx.redirectTo](/miniprogram/dev/api/route/wx.redirectTo.md)、[wx.reLaunch](/miniprogram/dev/api/route/wx.reLaunch.md)、[wx.switchTab](/miniprogram/dev/api/route/wx.switchTab.md)、[wx.navigateBack](/miniprogram/dev/api/route/wx.navigateBack.md) | 仅在小程序以[聊天工具](/minigame/dev/guide/open-ability/chat-tool.md)子包模式运行时要求点击态 |
| [VideoContext.startCasting](/miniprogram/dev/api/media/video/VideoContext.startCasting.md)、[VideoContext.switchCasting](/miniprogram/dev/api/media/video/VideoContext.switchCasting.md)、[VideoContext.reconnectCasting](/miniprogram/dev/api/media/video/VideoContext.reconnectCasting.md)、[VideoContext.exitCasting](/miniprogram/dev/api/media/video/VideoContext.exitCasting.md) | 无点击态时接口被静默跳过：不报错，也不会触发 success / fail / complete 回调 |
| [LivePlayerContext.startCasting](/miniprogram/dev/api/media/live/LivePlayerContext.startCasting.md)、[LivePlayerContext.switchCasting](/miniprogram/dev/api/media/live/LivePlayerContext.switchCasting.md)、[LivePlayerContext.reconnectCasting](/miniprogram/dev/api/media/live/LivePlayerContext.reconnectCasting.md)、[LivePlayerContext.exitCasting](/miniprogram/dev/api/media/live/LivePlayerContext.exitCasting.md) | 无点击态时接口被静默跳过：不报错，也不会触发 success / fail / complete 回调 |

### 获得点击态

#### 用户点击时

每次用户点击，对应的 tap 事件回调函数就会获得点击态。

#### 特定 API 的回调内

以下 API 的回调内，由于 API 的回调函数是用户点击时触发的，因此具有点击态。

| API | 说明 |
| --- | --- |
| [wx.requestPayment](/miniprogram/dev/api/payment/wx.requestPayment.md) / [wx.requestOrderPayment](/miniprogram/dev/api/payment/wx.requestOrderPayment.md) | 获得的点击态**不能**用于触发 [wx.openEmbeddedMiniProgram](/miniprogram/dev/api/navigate/wx.openEmbeddedMiniProgram.md)，详见[支付后打开半屏小程序能力的相关调整通知](https://developers.weixin.qq.com/community/develop/doc/000644871006f83372416ff2c66801) |
| [wx.requestAutomaticRepayment](/miniprogram/dev/api/navigate/wx.requestAutomaticRepayment.md) | |
| [wx.showModal](/miniprogram/dev/api/ui/interaction/wx.showModal.md) / [wx.showActionSheet](/miniprogram/dev/api/ui/interaction/wx.showActionSheet.md) | 点击态仅在 success 回调、或接口调用成功后的 complete 回调中获取 |

### 点击态的有效期

点击态只在**当前事件回调所在的宏任务**内有效，核心规则是：

- ✅ 同步代码，以及**仍处在同一个宏任务内的微任务**有效。
    ```js
    // tap
    handleTap: async () => {
      wx.openSetting({}); // ✅ 有效（同步调用，就在 tap 事件回调的宏任务内）

      Promise.resolve().then(() => {
        wx.openSetting({}); // ✅ 有效（微任务，仍未跳出当前宏任务）
      });

      await Promise.resolve(); // await 一个已 resolve 的值，不产生新宏任务
      wx.openSetting({}); // ✅ 有效（仍然在同一个宏任务内）
    };
    ```

- ❌ 一旦跨入**新的宏任务**，点击态即失效。典型场景：`setTimeout` / `setInterval` 的回调，以及 `await` 一个真正的异步任务 `await` 结束后继续执行的代码已经运行在另一个宏任务里，此时不再拥有点击态。
    ```js
    // tap
    handleTap: async () => {
        wx.openSetting({}); // ✅ 有效（同步调用，就在 tap 事件回调的宏任务内）
        setTimeout(() => {
            wx.openSetting({}); // ❌ 报错：can only be invoked by user TAP gesture
        }, 0);
        await new Promise((resolve) => setTimeout(resolve, 1000)); // 等待期间跨入新宏任务

        wx.openSetting({}); // ❌ 报错：can only be invoked by user TAP gesture
    };
    ```

### 点击态在异步请求中的延续

在 tap 事件处理函数里发起异步网络请求是常见场景。因此网络请求等接口在拥有点击态时被调用，会把点击态延续到自身的回调：

```js
// <button bind:tap="handleTap">tap</button>
handleTap: async () => {
  // ✅ 点击态有效
  wx.request({
    url: "https://www.thissitedoesnotexist.com/step1",
    complete: () => {
      // ✅ 点击态依然有效，被 request 延续
      wx.request({
        url: "https://www.thissitedoesnotexist.com/step2",
        complete: () => {
          // ✅ 点击态依然有效，被 request 延续
        },
      });
    },
  });
};
```

以下 API 能够继承延续点击态：

- [wx.chooseMedia](/miniprogram/dev/api/media/video/wx.chooseMedia.md)
- [wx.getSetting](/miniprogram/dev/api/open-api/setting/wx.getSetting.md)
- [wx.request](/miniprogram/dev/api/network/request/wx.request.md)
- [wx.downloadFile](/miniprogram/dev/api/network/download/wx.downloadFile.md)
- [wx.chooseLocation](/miniprogram/dev/api/location/wx.chooseLocation.md)
- [wx.login](/miniprogram/dev/api/open-api/login/wx.login.md)
- [wx.scanCode](/miniprogram/dev/api/device/scan/wx.scanCode.md)
- [wx.chooseAddress](/miniprogram/dev/api/open-api/address/wx.chooseAddress.md)
- [wx.chooseInvoice](/miniprogram/dev/api/open-api/invoice/wx.chooseInvoice.md)
- [wx.chooseInvoiceTitle](/miniprogram/dev/api/open-api/invoice/wx.chooseInvoiceTitle.md)
- [wx.chooseMessageFile](/miniprogram/dev/api/media/image/wx.chooseMessageFile.md)
- [wx.requestVirtualPayment](/miniprogram/dev/api/payment/wx.requestVirtualPayment.md)
- [wx.openHKOfflinePayView](/miniprogram/dev/api/payment/wx.openHKOfflinePayView.md)
