# 触控板管理

目前**右镜腿**可以触发MotionEvent TP事件，相对于手机的二维TP事件，左右镜腿产生的TP事件，只有一维，即X坐标动态变化，Y坐标为固定值。除了左右镜腿外，还有**戒指**配件，配对连接后也会产生TP事件。

\
**事件响应**：TP事件分发至Activity或View级别后，在分发流程中会将原始MotionEvent传递给**TouchDispatcher**，TouchDispatcher会将原生TP事件识别为**单击、长按、双击**、**三击**、**前滑&后滑**等手势（部分手势带有系统默认音效），并以**CommonTouchCallback**接口暴露出去，调用方只需根据对应手势完成自己业务逻辑即可。

基于**TouchDispatcher**和**CommonTouchCallback**，针对Activity封装了BaseTouchActivity和BaseEventActivity。BaseTouchActivity自动注册了手势监听。BaseEventActivity继承自BaseTouchActivity，并将对应的手势转成了kotlin Flow事件流（BaseMirrorActivity继承自BaseEventActivity），将事件映射为了TempleAction子类对象，事件监听示例代码如下:

<pre class="language-kotlin"><code class="lang-kotlin">class TPEventActivity : BaseMirrorActivity&#x3C;ActivityTpEventBinding>() {
    private var fixPosFocusTracker: FixPosFocusTracker? = null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        initFocusTarget()
        initEvent()
    }

    private fun initFocusTarget() {
        val focusHolder = FocusHolder(false)
<strong>        mBindingPair.setLeft {
</strong><strong>            focusHolder.addFocusTarget(
</strong>                FocusInfo(
                    btnEvent,
<strong>                    eventHandler = { action -> handleAction(action) },
</strong><strong>                    focusChangeHandler = { hasFocus ->
</strong><strong>                        mBindingPair.updateView {
</strong><strong>                            triggerFocus(hasFocus, btnEvent, mBindingPair.checkIsLeft(this))
</strong><strong>                        }
</strong><strong>                    }
</strong><strong>                )
</strong>            )
            focusHolder.currentFocus(mBindingPair.left.btnEvent)
<strong>        }
</strong><strong>        fixPosFocusTracker = FixPosFocusTracker(focusHolder).apply {
</strong><strong>            focusObj.reqFocus()
</strong><strong>        }
</strong><strong>    }
</strong>
    private fun handleAction(action: TempleAction) {
        when (action) {
            is TempleAction.LongClick -> {
                FToast.show("LongClick")
            }

            is TempleAction.Click -> {
                FToast.show("Click")
            }

            is TempleAction.DoubleClick -> {
                FToast.show("DoubleClick")
                finish()
            }

            is TempleAction.TripleClick -> {
                FToast.show("TripleClick")
            }

            is TempleAction.SlideBackward -> {
                FToast.show("SlideBackward")
            }

            is TempleAction.SlideForward -> {
                FToast.show("SlideForward")
            }

            is TempleAction.SlideUpwards -> {
                FToast.show("SlideUpwards")
            }

            is TempleAction.SlideDownwards -> {
                FToast.show("SlideDownwards")
            }


            else -> Unit
        }
    }

    private fun initEvent() {
<strong>        lifecycleScope.launch {
</strong><strong>            repeatOnLifecycle(Lifecycle.State.RESUMED) {
</strong><strong>                templeActionViewModel.state.collect {
</strong><strong>                    fixPosFocusTracker?.handleFocusTargetEvent(it)
</strong><strong>                }
</strong><strong>            }
</strong><strong>        }
</strong><strong>    }
</strong>
    private fun triggerFocus(hasFocus: Boolean, view: View, isLeft: Boolean) {
        view.setBackgroundColor(getColor(if (hasFocus) com.ffalcon.mercury.android.sdk.R.color.color_rayneo_theme_0 else R.color.black))
        // 3D效果
        make3DEffectForSide(view, isLeft, hasFocus)
    }
}
</code></pre>

如果当前视图没有具体的组件需要监听TP事件，只需要实现类似原生Activity返回的效果，那么只需要与TempleAction事件流对接，眼镜推荐的做法是双击退出页面，示例代码如下:

```kotlin
class DialogActivity : BaseMirrorActivity<LayoutDialogBinding>() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        initEvent()
    }

    private fun initEvent() {
        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.RESUMED) {
                templeActionViewModel.state.collect {
                    FLogger.i("DemoActivity", "action = $it")
                    when (it) {
                        is TempleAction.DoubleClick -> {
                            finish()
                        }
                        is TempleAction.Click -> {
                            showDialog()
                        }
                        else -> Unit
                    }
                }
            }
        }
    }
```

如果之前使用过X2的SDK，那么X2的应用可以迁移到X3上，X3 SDK相对X2 新增了DeviceUtil的isX3Device函数，用于判断是否是RayneoX3

```
if(DeviceUtil.isX3Device()){
    // in Rayneo X3
}else{
    // in Rayneo X2
}
```

### CommonTouchBack接口改动

&#x20; 增加了如下函数，在<mark style="color:$warning;">X2系列产品</mark>中不包含下面的事件

```
//上滑
open fun onTPSlideUpwards(args: FlingArgs): Boolean {
    return false
}
//下滑
open fun onTPSlideDownwards(args: FlingArgs): Boolean {
    return false
}
//双指点击
open fun onTPDoubleFingerClick() {
}
//双指长按
open fun onTPDoubleFingerLongClick() {
}
```

修改了如下函数，增加了vertical，表示是否是垂直方向上的滑动数据。

```sql
onTPSlideContinuous(delta: Float, longClick: Boolean = false,vertical:Boolean=false)
```

并且在CommondTouchBack中，增加了属性filterMode,当CommonTouchCallback设置为*OnlyX的时候onTPSlideContinuous只返回X轴滑动数据，*&#x5F53;CommonTouchCallback设置为*OnlyY的时候，onTPSlideContinuous只返回Y轴滑动的数据。**只有RayneoX3支持此项设置**。*

并且TempleAction.TpSlideContinuous也增加了滑动的方向是否是X轴还是Y轴。

### SlideBackward & SlideForward

需要特别说明一下前滑事件TempleAction.SlideForward与后滑事件TempleAction.SlideBackward的处理，在Rayneo眼镜中触控板的滑动与UI的映射关系分为两种，一种是自然模式，一种是非自然模式，用户可以在设置界面里面进行设置，设置路径参考下方视频：

{% file src="/files/llrwm8iezcYkmDo1atXI" %}

**自然模式(系统默认)**

触控板与UI映射关系如下图：

该模式下从镜腿方向往镜片方向滑动会被系统识别为TempleAction.SlideBackward事件，反过来会被识别为TempleAction.SlideForward事件

<figure><img src="/files/1lsc0aAhiRjN1HsH0kDg" alt=""><figcaption></figcaption></figure>

**非自然模式下**

触控板与UI映射关系如下图：

该模式下从镜腿方向往镜片方向滑动会被系统识别为TempleAction.SlideForward事件，反过来会被识别为TempleAction.SlideBackward事件

<figure><img src="/files/NIDWUghgj9ljpymEv3kI" alt=""><figcaption></figcaption></figure>

## 跟手效果

基于TouchDispatcher和CommonTouchCallback，对于一般场景来说已经够用，但如果想实现手机端列表跟手滑动的效果，则无法实现，原因有2个：

* TouchDispatcher其实是对原始事件的二次处理，而列表跟手效果，需要将原始事件直接喂给对应的列表;
* 原始事件的Y坐标为固定值，已经写死，而目标列表可横可竖，且只响应落在列表区域内的事件。这意味着对于横向列表，需要根据列表中心位置修改Y坐标值；对于竖向列表，需要将TP事件对X、Y坐标对调，然后再固定X坐标值。

目前跟手效果的整体实现思路是：原始事件仍旧喂给TouchDispatcher，以触发各类手势，但只响应**单击、双击、长按等**操作，**前滑&后滑**操作不响应。同时RecyclerView**固定焦点位**，截获原始事件流，根据原始事件生成新的MotionEvent，修改Y坐标后，将新TP事件喂给RecyclerView，之后回收新生成的MotionEvent；新生成的TP事件流，只实现跟手效果，不做点击等事件响应。

按场景，可以分为以下3种情况：

* **固定焦点位+ 列表滚动**跟手效果：核心工具类是**RecyclerViewSlidingTracker**
* **移动焦点位+ 列表** 跟手效果：按**滑动距离**方式，核心工具类是**RecyclerViewFocusTracker**
* **移动焦点位 + 固定位置View** 跟手效果：核心工具类是**FixPosFocusTracker**

\
其中，移动焦点位跟手效果的相关的核心逻辑是：滑动一定距离（比如50dp）后，根据滑动方向，触发焦点前后、上下切换逻辑。

固定焦点位时，需要将TP事件修改坐标后，传给RecycerView，示例代码如下：

```kotlin
// 监听原始事件，实现跟手效果
recyclerViewSlidingTracker.observeOriginMotionEventStream(
    motionEventDispatcher
) { event ->
    // 生产信的MotionEvent，并修改坐标，X与Y坐标是否互换位置，根据使用场景，动态调整
    MotionEvent.obtain(
        event.downTime,
        event.eventTime,
        event.action,
        320f,
        event.x,
        event.metaState
    ).apply {
        val e = this
        FLogger.d(
            "onReceiveEvent：x = ${e.x}, y = ${e.y},action = ${e.actionName()}, deviceId = ${e.deviceId}"
        )
    }
}

lifecycleScope.launchWhenResumed {
    val templeActionViewModel =
        ViewModelProvider(this@FixedFocusPosRVActivity).get<TempleActionViewModel>()
    templeActionViewModel.state.collect {
        if (!favoriteTracker.focusObj.hasFocus || !this.isActive || it.consumed) {
            return@collect
        }
        recyclerViewSlidingTracker.handleActionEvent(it) { action ->
            when (action) {
                is TempleAction.DoubleClick -> {
                    finish()
                }
                is TempleAction.Click -> {
                    if (!action.consumed) {
                        (mBindingPair.left.recyclerView.adapter as FixedFocusPosAdapter)
                            .getCurrentData()?.apply {
                                FToast.show(displayName)
                            }
                    }
                }
                else -> {}
            }
        }
    }
}
```

其中motionEventDispatcher是BaseTouchActivity成员变量，用于截获原始的事件流；observeOriginMotionEventStream函数，用于修改X、Y坐标，将原始事件流转为目标RecyclerView(左屏幕中)可以响应的事件流。完整代码，参考FixedFocusPosRVActivity。

### **FixPosFocusTracker**

**FixPosFocusTracker**的构造参数如下（目前的默认值就是rayneo一方应用的默认模式）：

```kotlin
/** Sliding distance threshold, only trigger focus switch operation when exceeding this distance */
const val IGNORE_DELTA = 50

/**
 * Fixed position focus switch tracking logic handler
 */
class FixPosFocusTracker(
    val focusHolder: FocusHolder,
    /** Whether to support tracking */
    private val continuous: Boolean = false,
    /** Whether to switching focus vertically or horizontally */
    private val isVertical: Boolean = true,
    /** Sliding distance to trigger focus switch */
    private val ignoreDelta: Int = IGNORE_DELTA
) 
```

**FocusHolder：**&#x4F5C;为焦点视图的管理类， 在构造器中有一个**loop**参数，代表焦点切换是否循环，也就是当焦点锁定最后一个/第一个焦点视图的时候，如果再次触发焦点的next()/previous()操作，是否会上焦到第一个/最后一个焦点视图，默认为false，也就是不循环，这个时候触发next()/previous()操作不会有任何行为产生，反之焦点会在整个焦点列表里面循环。

```kotlin
/**
 * Unified tool class for handling focus switching
 * @param [loop] Whether to enable circular focus switching
 */
class FocusHolder(private val loop: Boolean = false) 
```

**continuous ：**&#x662F;否为连续滑动模式？默认为false，也就是单步滑动模式，每一次滑动，只会触发一次焦点的next()/previous()，当置为true的时候，会根据滑动的总距离来切换焦点，可以连续切换多个焦点的next()/previous()**isVertical：**&#x662F;否只接受垂直方向的滑动来切换焦点，默认为true，如果设置为false，就只默认接受水平方向的滑动来切换焦点**ignoreDelta：**&#x7126;点切换的滑动距离阀值，只有当滑动距离超过这个阀值才会触发焦点的切换逻辑这几个参数之间的作用如下：

| 模式   | **FocusHolder.loop** | **continuous** | **isVertical** | **ignoreDelta** |
| ---- | -------------------- | -------------- | -------------- | --------------- |
| 连续滑动 | 受影响                  | true           | 不受影响           | 受影响             |
| 单次滑动 | 受影响                  | false(默认)      | 受影响            | 受影响             |

用户可以根据这些参数实现自定义的TP焦点切换手感，如果追求与一方应用一致的话，使用默认的构造参数就好。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://rayneo.gitbook.io/rayneo-devdoc/x-xi-lie/android-kai-fa/neng-li-jie-shao/chu-kong-ban-guan-li.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
