Windows Community Toolkit 3.0 - Gaze Interaction

概述git

Gaze Input & Tracking - 也就是視覺輸入和跟蹤,是一種和鼠標/觸摸屏輸入很是不同的交互方式,利用人類眼球的識別和眼球方向角度的跟蹤,來判斷人眼的目標和意圖,從而很是方便的完成對設備的控制和操做。這種交互方式,應用場景很是普遍,好比 AR/VR/MR 中,利用視覺追蹤,來判斷 Reaility 中場景物體的方向和展現;再好比閱讀中,根據視覺輸入和追蹤,來自動滾動和翻頁等;再好比遊戲中依靠視覺追蹤來決定人物的走位等,讓遊戲控制變得很是簡單。github

Windows 10 秋季創意者更新公佈了對視覺追蹤的原生支持,而在 Windows 10 四月更新中爲開發者增長了 Windows Gaze Input API 來支持視覺追蹤開發,讓開發者能夠在應用中加入視覺追蹤的交互方式來處理視覺輸入和跟蹤。express

而在 Windows Community Toolkit 3.0 中,也加入了 Gaze Interaction Library,它基於 Windows Gaze Input API 建立,提供了一系列的開發者幫助類,幫助開發者能夠更容易的實現對用戶視覺的追蹤。它旨在把經過 Windows API 來處理眼球追蹤的原始數據流的負責過程封裝處理,讓開發者能夠更方便的在 Windows App 中集成。windows

下面是 Windows Community Toolkit Sample App 的示例截圖和 code/doc 地址:api

 

Windows Community Toolkit Doc - Gaze Interactionide

Windows Community Toolkit Source Code - Gaze Interactionthis

Namespace: Microsoft.Toolkit.Uwp.Input.GazeInteraction; Nuget: Microsoft.Toolkit.Uwp.Input.GazeInteraction;spa

 

開發過程代理

代碼結構分析指針

首先來看 GazeInteraction 的代碼結構,經過類的命名能夠看出,開發語言使用的是 C++,並且類結構和數量都比較複雜。能夠看到 GazeInteraction 的代碼在 Microsoft.Toolkit.Uwp.Input namespace 下,這也意味着 GazeInteraction 會被做爲一種 Input 方式來作處理。

 

來看一下在 Visual Studio 中打開的目錄,會更清晰一些:

由於是 C++ 語言編寫的庫,因此能夠很清楚的看到,主要功能被劃分在 Headers 和 Sources 中,Headers 中主要是 cpp 對應的頭文件,以及一些枚舉類,變量定義類;Sources 中就是整個 GazeInteraction 的主要代碼處理邏輯;

咱們挑選其中比較重要的幾個類來說解:

  • GazeInput.cpp - Gaze 輸入的主要處理邏輯
  • GazePointer.cpp - Gaze 指針的主要處理邏輯
  • GazePointerProxy.cpp - Gaze 指針的代理處理邏輯
  • GazeTargetItem.cpp - Gaze 操做目標的主要處理邏輯

1. GazeInput.cpp

在 GazeInput.h 中能夠看到,定義了不少 public 的依賴屬性,主要針對的是 GazeInput 的光標屬性,以及不少 get/set 方法,以及 propertychanged 通知事件。

GazeInput 中定義的依賴屬性有:

  • Interaction - 獲取和設置視覺交互屬性,它有三個枚舉值:Enabled/Disabled/Inherited;
  • IsCursorVisible - 視覺交互的光標是否顯示,布爾值,默認爲 false;
  • CursorRadius - 獲取和設置視覺光標的半徑;
  • GazeElement - 視覺元素,附加到控件的代理對象容許訂閱每一個視覺事件;
  • FixationDuration - 獲取和設置從 Enter 狀態到 Fixation 狀態的轉換所需時間跨度,當 StateChanged 時間被觸發,PointerState 被設置爲 Fixation,單位是 ms,默認爲 350 ms; 
  • DwellDuration - 獲取和設置從 Fixation 狀態到 DWell 狀態的轉換所需時間跨度,當 StateChanged 時間被觸發,PointerState 被設置爲 DWell,單位是 ms,默認爲 400 ms;
  • RepeatDelayDuration - 獲取和設置第一次重複發生的持續時間,能夠防止無心的重複調用;
  • DwellRepeatDuration - 獲取和設置 Dwell 重複駐留調用的持續時間;
  • ThresholdDuration - 獲取和設置從 Enter 狀態到 Exit 狀態的轉換所需時間跨度,當 StateChanged 時間被觸發,PointerState 被設置爲 Exit,單位是 ms,默認爲 50 ms;
  • MaxDwellRepeatCount - 控件重複調用的最大次數,用戶的視覺不須要離開並從新進入控件。默認值爲 0,禁用重複調用,開發者能夠設置爲 >0 的值來啓用重複調用;
  • IsSwitchEnabled - 標識切換是否可用,布爾值;

這些屬性的定義讓視覺輸入能夠做爲一種輸入方式,實現對系統界面元素的操做。

2. GazePointer.cpp

GazePointer 類主要處理的是 GazeInput 的定位和相關功能,代碼量比較大,不過每一個方法功能都比較容易懂,咱們經過幾個方法來看一些重要信息:

1). GazePointer 構造方法,看到方法中初始化了 NullFilter 和 GazeCursor,還定義了一段時間接收不到視覺輸入的定時處理,以及觀察器;

GazePointer::GazePointer()
{
    _nonInvokeGazeTargetItem = ref new NonInvokeGazeTargetItem();

    // Default to not filtering sample data
    Filter = ref new NullFilter();

    _gazeCursor = ref new GazeCursor();

    // timer that gets called back if there gaze samples haven't been received in a while
    _eyesOffTimer = ref new DispatcherTimer();
    _eyesOffTimer->Tick += ref new EventHandler<Object^>(this, &GazePointer::OnEyesOff);

    // provide a default of GAZE_IDLE_TIME microseconds to fire eyes off 
    EyesOffDelay = GAZE_IDLE_TIME;

    InitializeHistogram();

    _watcher = GazeInputSourcePreview::CreateWatcher();
    _watcher->Added += ref new TypedEventHandler<GazeDeviceWatcherPreview^, GazeDeviceWatcherAddedPreviewEventArgs^>(this, &GazePointer::OnDeviceAdded);
    _watcher->Removed += ref new TypedEventHandler<GazeDeviceWatcherPreview^, GazeDeviceWatcherRemovedPreviewEventArgs^>(this, &GazePointer::OnDeviceRemoved);
    _watcher->Start();
}

2). GetProperty 方法,這裏咱們主要看看 PointerState,主要有 Fixation/DWell/DWellRepeat/Enter 和 Exit;

static DependencyProperty^ GetProperty(PointerState state)
{
    switch (state)
    {
    case PointerState::Fixation: return GazeInput::FixationDurationProperty;
    case PointerState::Dwell: return GazeInput::DwellDurationProperty;
    case PointerState::DwellRepeat: return GazeInput::DwellRepeatDurationProperty;
    case PointerState::Enter: return GazeInput::ThresholdDurationProperty;
    case PointerState::Exit: return GazeInput::ThresholdDurationProperty;
    default: return nullptr;
    }
}

3). GetElementStateDelay 方法,由於 GazePointer 有不少不一樣的狀態,咱們看一個典型的獲取某個 state delay 的邏輯;根據用戶設置或默認設置的值,再根據 pointer state 和是否 repeat 來判斷 ticks 的值;  

TimeSpan GazePointer::GetElementStateDelay(UIElement ^element, PointerState pointerState)
{
    auto property = GetProperty(pointerState);
    auto defaultValue = GetDefaultPropertyValue(pointerState);
    auto ticks = GetElementStateDelay(element, property, defaultValue);

    switch (pointerState)
    {
    case PointerState::Dwell:
    case PointerState::DwellRepeat:
        _maxHistoryTime = max(_maxHistoryTime, 2 * ticks);
        break;
    }

    return ticks;
}
TimeSpan GazePointer::GetElementStateDelay(UIElement ^element, DependencyProperty^ property, TimeSpan defaultValue)
{
    UIElement^ walker = element;
    Object^ valueAtWalker = walker->GetValue(property);

    while (GazeInput::UnsetTimeSpan.Equals(valueAtWalker) && walker != nullptr)
    {
        walker = GetInheritenceParent(walker);

        if (walker != nullptr)
        {
            valueAtWalker = walker->GetValue(property);
        }
    }

    auto ticks = GazeInput::UnsetTimeSpan.Equals(valueAtWalker) ? defaultValue : safe_cast<TimeSpan>(valueAtWalker);

    return ticks;
}

4). GetHitTarget 方法,獲取擊中的目標,根據指針的位置,和每一個 target 在視覺樹中的位置,以及層級關係,來判斷該次擊中是否可用,應該產生什麼後續事件;

GazeTargetItem^ GazePointer::GetHitTarget(Point gazePoint)
{
    GazeTargetItem^ invokable;

    switch (Window::Current->CoreWindow->ActivationMode)
    {
    default:
        invokable = _nonInvokeGazeTargetItem;
        break;

    case CoreWindowActivationMode::ActivatedInForeground:
    case CoreWindowActivationMode::ActivatedNotForeground:
        auto elements = VisualTreeHelper::FindElementsInHostCoordinates(gazePoint, nullptr, false);
        auto first = elements->First();
        auto element = first->HasCurrent ? first->Current : nullptr;

        invokable = nullptr;

        if (element != nullptr)
        {
            invokable = GazeTargetItem::GetOrCreate(element);

            while (element != nullptr && !invokable->IsInvokable)
            {
                element = dynamic_cast<UIElement^>(VisualTreeHelper::GetParent(element));

                if (element != nullptr)
                {
                    invokable = GazeTargetItem::GetOrCreate(element);
                }
            }
        }
        ...break;
    }

    return invokable;
}

GazePointer 類中處理方法很是多,這裏不一一列舉,你們能夠詳細閱讀源代碼去理解每個方法的書寫方法。

3. GazePointerProxy.cpp

GazePointerProxy 類主要是爲 GazePointer 設立的代理,包括 Loaded 和 UnLoaded 事件的代理,以及 Enable 狀態和處理的代理;比較典型的 OnLoaded 事件處理:

void GazePointerProxy::OnLoaded(Object^ sender, RoutedEventArgs^ args)
{
    assert(IsLoadedHeuristic(safe_cast<FrameworkElement^>(sender)));

    if (!_isLoaded)
    {
        // Record that we are now loaded.
        _isLoaded = true;

        // If we were previously enabled...
        if (_isEnabled)
        {
            // ...we can now be counted as actively enabled.
            GazePointer::Instance->AddRoot(sender);
        }
    }
    else
    {
        Debug::WriteLine(L"Unexpected Load");
    }
}

4. GazeTargetItem.cpp

Gaze 視覺輸入的 Target Item 類,針對不一樣類型的 Target,進行不一樣的交互和邏輯處理,比較典型的 PivotItemGazeTargetItem 類,會根據 PivotItem 的組成:headerItem 和 headerPanel,設置選中的 Index;

ref class PivotItemGazeTargetItem sealed : GazeTargetItem
{
internal:

    PivotItemGazeTargetItem(UIElement^ element)
        : GazeTargetItem(element)
    {
    }

    void Invoke() override
    {
        auto headerItem = safe_cast<PivotHeaderItem^>(TargetElement);
        auto headerPanel = safe_cast<PivotHeaderPanel^>(VisualTreeHelper::GetParent(headerItem));
        unsigned index;
        headerPanel->Children->IndexOf(headerItem, &index);

        DependencyObject^ walker = headerPanel;
        Pivot^ pivot;
        do
        {
            walker = VisualTreeHelper::GetParent(walker);
            pivot = dynamic_cast<Pivot^>(walker);
        } while (pivot == nullptr);

        pivot->SelectedIndex = index;
    }
};

調用示例

<Page   
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"    
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"    
    mc:Ignorable="d"
    xmlns:g="using:Microsoft.Toolkit.Uwp.Input.GazeInteraction" 
    g:GazeInput.Interaction="Enabled"
    g:GazeInput.IsCursorVisible="True"
    g:GazeInput.CursorRadius="5">
  <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">             
        <Button x:Name="TargetButton" HorizontalAlignment="Center" BorderBrush="#7FFFFFFF"                            
                    g:GazeInput.ThresholdDuration="00:00:00.0500000"
                    g:GazeInput.FixationDuration="00:00:00.3500000"
                    g:GazeInput.DwellDuration="00:00:00.4000000"
                    g:GazeInput.RepeatDelayDuration="00:00:00.4000000"
                    g:GazeInput.DwellRepeatDuration="00:00:00.4000000"
                    g:GazeInput.MaxDwellRepeatCount="0"
                    Width="100"
                    Height="100"
                    />
  </Grid>
</Page>
private void GazeButtonControl_StateChanged(object sender, GazePointerEventArgs ea)
{
    if (ea.PointerState == GazePointerState.Enter)
    {
    }
    if (ea.PointerState == GazePointerState.Fixation)
    {
    }
    if (ea.PointerState == GazePointerState.Dwell)
    {
        if (dwellCount == 0)
        {
            dwellCount = 1;
        }
        else
        {
            dwellCount += 1;
        }
    }
    if (ea.PointerState == GazePointerState.Exit)
    { 
    }
}

// You can respond to dwell progress in the ProgressFeedback handler
private void OnProgressFeedback(object sender, GazeProgressEventArgs e){}private void OnGazeInvoked(object sender, GazeInvokedRoutedEventArgs e){}

 

總結

到這裏咱們就把 Windows Community Toolkit 3.0 中的 Gaze Interation 的源代碼實現過程講解完成了,但願能對你們更好的理解和使用這個功能有所幫助。同時這一功能,對於開發 AR/VR/MR 和基於其餘視覺追蹤設備的應用,會很是有想象空間,但願你們能有不少很好玩的想法,也歡迎和咱們交流。

最後,再跟你們安利一下 WindowsCommunityToolkit 的官方微博:https://weibo.com/u/6506046490你們能夠經過微博關注最新動態。

衷心感謝 WindowsCommunityToolkit 的做者們傑出的工做,感謝每一位貢獻者,Thank you so much, ALL WindowsCommunityToolkit AUTHORS !!!

相關文章
相關標籤/搜索