Udostępnij za pośrednictwem


Wywoływanie zdarzeń z efektów

Efekt może definiować i wywoływać zdarzenie, sygnalizując zmiany w bazowym widoku natywnym. W tym artykule przedstawiono sposób implementowania śledzenia palców wielodotykowego niskiego poziomu oraz generowania zdarzeń sygnalizujących aktywność dotykową.

Efekt opisany w tym artykule zapewnia dostęp do zdarzeń dotyku niskiego poziomu. Te zdarzenia niskiego poziomu nie są dostępne w istniejących GestureRecognizer klasach, ale są one istotne dla niektórych typów aplikacji. Na przykład aplikacja do malowania palcami musi śledzić poszczególne palce podczas poruszania się na ekranie. Klawiatura muzyczna musi wykrywać naciśnięcia i zwalniania na poszczególnych klawiszach, a także palec szybujący od jednego klawisza do drugiego w glissando.

Efekt jest idealny do śledzenia palców wielodotykowych, ponieważ może być przymocowany do dowolnego Xamarin.Forms elementu.

Zdarzenia platformy Touch

Wszystkie systemy iOS, Android i platforma uniwersalna systemu Windows obejmują interfejs API niskiego poziomu, który umożliwia aplikacjom wykrywanie aktywności dotykowej. Wszystkie te platformy rozróżniają trzy podstawowe typy zdarzeń dotykowych:

  • Naciśnięcie, gdy palec dotyka ekranu
  • Przeniesione, gdy palec dotykający ekranu porusza się
  • Zwalniane, gdy palec jest zwalniany z ekranu

W środowisku wielodotykowym wiele palców może jednocześnie dotknąć ekranu. Różne platformy zawierają numer identyfikacyjny (ID), którego aplikacje mogą używać do rozróżnienia między wieloma palcami.

W systemie iOS UIView klasa definiuje trzy metody, które można zastąpić, TouchesBegan, TouchesMovedi TouchesEnded odpowiadające tym trzem podstawowym zdarzeń. W artykule Multi-Touch Finger Tracking opisano sposób używania tych metod. Jednak program systemu iOS nie musi zastąpić klasy, która pochodzi z UIView , aby używać tych metod. System iOS UIGestureRecognizer definiuje również te same trzy metody i można dołączyć wystąpienie klasy pochodzącej z UIGestureRecognizer dowolnego UIView obiektu.

W systemie View Android klasa definiuje przesłoniętą metodę o nazwie OnTouchEvent , aby przetworzyć wszystkie działania dotykowe. Typ działania dotykowego jest definiowany przez elementy Downczłonkowskie wyliczenia, , MovePointerDown, Upi PointerUp zgodnie z opisem w artykule Multi-Touch Finger Tracking. System Android View definiuje również zdarzenie o nazwie Touch , które umożliwia dołączanie programu obsługi zdarzeń do dowolnego View obiektu.

W platforma uniwersalna systemu Windows (UWP) UIElement klasa definiuje zdarzenia o nazwach PointerPressed, PointerMovedi PointerReleased. Opisano je w artykule Handle Pointer Input (Dane wejściowe wskaźnika obsługi) w witrynie MSDN i dokumentacji interfejsu UIElement API dla klasy .

Interfejs Pointer API w platforma uniwersalna systemu Windows ma na celu ujednolicenie danych wejściowych myszy, dotyku i pióra. Z tego powodu zdarzenie jest wywoływane, gdy mysz przechodzi przez element nawet wtedy, PointerMoved gdy przycisk myszy nie jest naciśnięty. Obiekt PointerRoutedEventArgs , który towarzyszy tym zdarzeń, ma właściwość o nazwie, która ma właściwość o nazwie PointerIsInContact , która wskazuje, czy naciśnięcie przycisku myszy lub palcem jest w kontakcie z ekranem.

Ponadto platforma UWP definiuje jeszcze dwa zdarzenia o nazwie PointerEntered i PointerExited. Wskazują one, kiedy mysz lub palec przenosi się z jednego elementu do drugiego. Rozważmy na przykład dwa sąsiadujące elementy o nazwach A i B. Oba elementy mają zainstalowane programy obsługi dla zdarzeń wskaźnika. Po naciśnięciu palca na PointerPressed A zdarzenie jest wywoływane. Gdy palec porusza się, A wywołuje PointerMoved zdarzenia. Jeśli palec przechodzi z A do B, A wywołuje PointerExited zdarzenie, a B wywołuje PointerEntered zdarzenie. Jeśli palec zostanie zwolniony, B wywołuje PointerReleased zdarzenie.

Platformy iOS i Android różnią się od platformy UWP: widok, który najpierw pobiera wywołanie TouchesBegan lub OnTouchEvent gdy palec dotyka widoku, nadal uzyskuje wszystkie działania dotykowe, nawet jeśli palec przenosi się do różnych widoków. Platforma UWP może zachowywać się podobnie, jeśli aplikacja przechwytuje wskaźnik: W PointerEntered procedurze obsługi zdarzeń wywołania CapturePointer elementu, a następnie pobiera wszystkie działania dotykowe z tego palca.

Podejście platformy UWP okazuje się być bardzo przydatne w przypadku niektórych typów aplikacji, na przykład klawiatury muzycznej. Każdy klucz może obsługiwać zdarzenia dotykowe dla tego klucza i wykrywać, kiedy palec przesuwa się z jednego klucza do innego przy użyciu zdarzeń PointerEntered i PointerExited .

Z tego powodu efekt śledzenia dotykowego opisany w tym artykule implementuje podejście platformy UWP.

Interfejs API efektu śledzenia dotykowego

Przykład zawiera klasy (i wyliczenie), które implementują śledzenie dotykowe niskiego poziomu. Te typy należą do przestrzeni nazw TouchTracking i zaczynają się od słowa Touch. Projekt biblioteki TouchTrackingEffectDemos .NET Standard zawiera TouchActionType wyliczenie typu zdarzeń dotykowych:

public enum TouchActionType
{
    Entered,
    Pressed,
    Moved,
    Released,
    Exited,
    Cancelled
}

Wszystkie platformy zawierają również zdarzenie wskazujące, że zdarzenie dotykowe zostało anulowane.

Klasa TouchEffect w bibliotece .NET Standard pochodzi z RoutingEffect i definiuje zdarzenie o nazwie i metodę o nazwie TouchActionOnTouchAction , która wywołuje TouchAction zdarzenie:

public class TouchEffect : RoutingEffect
{
    public event TouchActionEventHandler TouchAction;

    public TouchEffect() : base("XamarinDocs.TouchEffect")
    {
    }

    public bool Capture { set; get; }

    public void OnTouchAction(Element element, TouchActionEventArgs args)
    {
        TouchAction?.Invoke(element, args);
    }
}

Zwróć również uwagę na Capture właściwość . Aby przechwycić zdarzenia dotykowe, aplikacja musi ustawić tę właściwość na true wartość przed zdarzeniem Pressed . W przeciwnym razie zdarzenia dotykowe zachowują się jak te w platforma uniwersalna systemu Windows.

Klasa TouchActionEventArgs w bibliotece .NET Standard zawiera wszystkie informacje, które towarzyszą każdemu zdarzeniu:

public class TouchActionEventArgs : EventArgs
{
    public TouchActionEventArgs(long id, TouchActionType type, Point location, bool isInContact)
    {
        Id = id;
        Type = type;
        Location = location;
        IsInContact = isInContact;
    }

    public long Id { private set; get; }

    public TouchActionType Type { private set; get; }

    public Point Location { private set; get; }

    public bool IsInContact { private set; get; }
}

Aplikacja może używać Id właściwości do śledzenia poszczególnych palców. Zwróć uwagę na IsInContact właściwość . Ta właściwość jest zawsze true w przypadku Pressed zdarzeń i false zdarzeń Released . Jest również zawsze true w przypadku Moved zdarzeń w systemach iOS i Android. Właściwość IsInContact może dotyczyć falseMoved zdarzeń na platforma uniwersalna systemu Windows, gdy program jest uruchomiony na pulpicie, a wskaźnik myszy porusza się bez naciśnięcia przycisku.

Możesz użyć TouchEffect klasy we własnych aplikacjach, dołączając plik w projekcie biblioteki .NET Standard rozwiązania i dodając wystąpienie do Effects kolekcji dowolnego Xamarin.Forms elementu. Dołącz procedurę obsługi do zdarzenia w TouchAction celu uzyskania zdarzeń dotykowych.

Do użycia TouchEffect we własnej aplikacji potrzebne będą również implementacje platformy zawarte w rozwiązaniu TouchTrackingEffectDemos .

Implementacje efektu śledzenia dotykowego

Implementacje TouchEffect systemów iOS, Android i UWP opisane poniżej zaczynają się od najprostszej implementacji (UWP) i kończącej się implementacją systemu iOS, ponieważ jest bardziej złożona strukturalnie niż inne.

Implementacja platformy UWP

Implementacja platformy UNIWERSALNEJ TouchEffect systemu Windows jest najprostsza. Jak zwykle klasa pochodzi z PlatformEffect klasy i zawiera dwa atrybuty zestawu:

[assembly: ResolutionGroupName("XamarinDocs")]
[assembly: ExportEffect(typeof(TouchTracking.UWP.TouchEffect), "TouchEffect")]

namespace TouchTracking.UWP
{
    public class TouchEffect : PlatformEffect
    {
        ...
    }
}

Przesłonięcia OnAttached zapisuje niektóre informacje jako pola i dołącza programy obsługi do wszystkich zdarzeń wskaźnika:

public class TouchEffect : PlatformEffect
{
    FrameworkElement frameworkElement;
    TouchTracking.TouchEffect effect;
    Action<Element, TouchActionEventArgs> onTouchAction;

    protected override void OnAttached()
    {
        // Get the Windows FrameworkElement corresponding to the Element that the effect is attached to
        frameworkElement = Control == null ? Container : Control;

        // Get access to the TouchEffect class in the .NET Standard library
        effect = (TouchTracking.TouchEffect)Element.Effects.
                    FirstOrDefault(e => e is TouchTracking.TouchEffect);

        if (effect != null && frameworkElement != null)
        {
            // Save the method to call on touch events
            onTouchAction = effect.OnTouchAction;

            // Set event handlers on FrameworkElement
            frameworkElement.PointerEntered += OnPointerEntered;
            frameworkElement.PointerPressed += OnPointerPressed;
            frameworkElement.PointerMoved += OnPointerMoved;
            frameworkElement.PointerReleased += OnPointerReleased;
            frameworkElement.PointerExited += OnPointerExited;
            frameworkElement.PointerCanceled += OnPointerCancelled;
        }
    }
    ...
}    

Procedura OnPointerPressed obsługi wywołuje zdarzenie efektu przez wywołanie onTouchAction pola w metodzie CommonHandler :

public class TouchEffect : PlatformEffect
{
    ...
    void OnPointerPressed(object sender, PointerRoutedEventArgs args)
    {
        CommonHandler(sender, TouchActionType.Pressed, args);

        // Check setting of Capture property
        if (effect.Capture)
        {
            (sender as FrameworkElement).CapturePointer(args.Pointer);
        }
    }
    ...
    void CommonHandler(object sender, TouchActionType touchActionType, PointerRoutedEventArgs args)
    {
        PointerPoint pointerPoint = args.GetCurrentPoint(sender as UIElement);
        Windows.Foundation.Point windowsPoint = pointerPoint.Position;  

        onTouchAction(Element, new TouchActionEventArgs(args.Pointer.PointerId,
                                                        touchActionType,
                                                        new Point(windowsPoint.X, windowsPoint.Y),
                                                        args.Pointer.IsInContact));
    }
}

OnPointerPressed Sprawdza również wartość Capture właściwości w klasie effect w bibliotece .NET Standard i wywołuje CapturePointer wartość , jeśli jest to true.

Inne programy obsługi zdarzeń platformy UWP są jeszcze prostsze:

public class TouchEffect : PlatformEffect
{
    ...
    void OnPointerEntered(object sender, PointerRoutedEventArgs args)
    {
        CommonHandler(sender, TouchActionType.Entered, args);
    }
    ...
}

Implementacja systemu Android

Implementacje systemów Android i iOS muszą być bardziej złożone, ponieważ muszą implementować Exited zdarzenia i Entered , gdy palec przechodzi od jednego elementu do drugiego. Obie implementacje mają podobną strukturę.

Klasa systemu Android TouchEffect instaluje program obsługi dla Touch zdarzenia:

view = Control == null ? Container : Control;
...
view.Touch += OnTouch;

Klasa definiuje również dwa słowniki statyczne:

public class TouchEffect : PlatformEffect
{
    ...
    static Dictionary<Android.Views.View, TouchEffect> viewDictionary =
        new Dictionary<Android.Views.View, TouchEffect>();

    static Dictionary<int, TouchEffect> idToEffectDictionary =
        new Dictionary<int, TouchEffect>();
    ...

Polecenie viewDictionary pobiera nowy wpis za każdym razem, OnAttached gdy jest wywoływane przesłonięcia:

viewDictionary.Add(view, this);

Wpis jest usuwany ze słownika w pliku OnDetached. Każde wystąpienie elementu TouchEffect jest skojarzone z określonym widokiem, do którego jest dołączony efekt. Słownik statyczny umożliwia każdemu TouchEffect wystąpieniu wyliczanie wszystkich innych widoków i odpowiadających TouchEffect im wystąpień. Jest to konieczne, aby umożliwić przesyłanie zdarzeń z jednego widoku do innego.

System Android przypisuje kod identyfikatora do zdarzeń dotykowych, które umożliwiają aplikacji śledzenie poszczególnych palców. Ten idToEffectDictionary kod identyfikatora jest skojarzony z wystąpieniem TouchEffect . Element jest dodawany do tego słownika Touch , gdy program obsługi jest wywoływany dla naciśnięcia palca:

void OnTouch(object sender, Android.Views.View.TouchEventArgs args)
{
    ...
    switch (args.Event.ActionMasked)
    {
        case MotionEventActions.Down:
        case MotionEventActions.PointerDown:
            FireEvent(this, id, TouchActionType.Pressed, screenPointerCoords, true);

            idToEffectDictionary.Add(id, this);

            capture = libTouchEffect.Capture;
            break;

Element jest usuwany z idToEffectDictionary momentu zwolnienia palca z ekranu. Metoda FireEvent po prostu gromadzi wszystkie informacje niezbędne do wywołania OnTouchAction metody:

void FireEvent(TouchEffect touchEffect, int id, TouchActionType actionType, Point pointerLocation, bool isInContact)
{
    // Get the method to call for firing events
    Action<Element, TouchActionEventArgs> onTouchAction = touchEffect.libTouchEffect.OnTouchAction;

    // Get the location of the pointer within the view
    touchEffect.view.GetLocationOnScreen(twoIntArray);
    double x = pointerLocation.X - twoIntArray[0];
    double y = pointerLocation.Y - twoIntArray[1];
    Point point = new Point(fromPixels(x), fromPixels(y));

    // Call the method
    onTouchAction(touchEffect.formsElement,
        new TouchActionEventArgs(id, actionType, point, isInContact));
}

Wszystkie inne typy dotyku są przetwarzane na dwa różne sposoby: jeśli Capture właściwość to true, zdarzenie dotykowe jest dość prostym tłumaczeniem TouchEffect informacji. Staje się to bardziej skomplikowane, gdy Capture jest to false spowodowane tym, że zdarzenia dotykowe mogą być musiały zostać przeniesione z jednego widoku do drugiego. Jest to odpowiedzialność za metodę CheckForBoundaryHop , która jest wywoływana podczas zdarzeń przenoszenia. Ta metoda korzysta z obu słowników statycznych. Wylicza viewDictionary on w celu określenia widoku, który jest obecnie dotykany palcem, i używa idToEffectDictionary go do przechowywania bieżącego TouchEffect wystąpienia (a tym samym bieżącego widoku) skojarzonego z określonym identyfikatorem:

void CheckForBoundaryHop(int id, Point pointerLocation)
{
    TouchEffect touchEffectHit = null;

    foreach (Android.Views.View view in viewDictionary.Keys)
    {
        // Get the view rectangle
        try
        {
            view.GetLocationOnScreen(twoIntArray);
        }
        catch // System.ObjectDisposedException: Cannot access a disposed object.
        {
            continue;
        }
        Rectangle viewRect = new Rectangle(twoIntArray[0], twoIntArray[1], view.Width, view.Height);

        if (viewRect.Contains(pointerLocation))
        {
            touchEffectHit = viewDictionary[view];
        }
    }

    if (touchEffectHit != idToEffectDictionary[id])
    {
        if (idToEffectDictionary[id] != null)
        {
            FireEvent(idToEffectDictionary[id], id, TouchActionType.Exited, pointerLocation, true);
        }
        if (touchEffectHit != null)
        {
            FireEvent(touchEffectHit, id, TouchActionType.Entered, pointerLocation, true);
        }
        idToEffectDictionary[id] = touchEffectHit;
    }
}

Jeśli nastąpiła zmiana w metodzie idToEffectDictionary, metoda potencjalnie wywołuje FireEventExited metodę i Entered przenieść z jednego widoku do innego. Jednak palec mógł zostać przeniesiony do obszaru zajmowanego przez widok bez dołączonego TouchEffectobszaru lub z tego obszaru do widoku z dołączonym efektem.

Zwróć uwagę na blok try i catch po korzystaniu z widoku. Na stronie, która zostanie wyświetlona, a następnie przejdzie z powrotem do strony głównej, metoda nie zostanie wywołana, a elementy pozostaną w elemecie viewDictionary , OnDetached ale system Android uzna je za usunięte.

Implementacja systemu iOS

Implementacja systemu iOS jest podobna do implementacji systemu Android z tą różnicą, że klasa systemu iOS TouchEffect musi utworzyć wystąpienie pochodnej klasy UIGestureRecognizer. Jest to klasa w projekcie systemu iOS o nazwie TouchRecognizer. Ta klasa obsługuje dwa słowniki statyczne, które przechowują TouchRecognizer wystąpienia:

static Dictionary<UIView, TouchRecognizer> viewDictionary =
    new Dictionary<UIView, TouchRecognizer>();

static Dictionary<long, TouchRecognizer> idToTouchDictionary =
    new Dictionary<long, TouchRecognizer>();

Większość struktury tej TouchRecognizer klasy jest podobna do klasy systemu Android TouchEffect .

Ważne

Wiele widoków w programie UIKit nie ma domyślnie włączonej obsługi dotykowej. Dotyk można włączyć, dodając view.UserInteractionEnabled = true; do OnAttached przesłonięcia w klasie w TouchEffect projekcie systemu iOS. Powinno się to zdarzyć po otrzymaniu UIView elementu odpowiadającego elementowi, do którego jest dołączony efekt.

Wprowadzenie efektu dotykowego do pracy

Przykładowy program zawiera pięć stron, które testuje efekt śledzenia dotykowego pod kątem typowych zadań.

Strona Przeciąganie w widoku BoxView umożliwia dodawanie BoxView elementów do elementu AbsoluteLayout , a następnie przeciąganie ich po ekranie. Plik XAML tworzy wystąpienie dwóch Button widoków w celu dodania BoxView elementów do elementu AbsoluteLayout i wyczyszczenia elementu AbsoluteLayout.

Metoda w pliku za pomocą kodu, który dodaje nowy BoxView element do AbsoluteLayout obiektu, dodaje TouchEffect również obiekt do BoxView obiektu i dołącza program obsługi zdarzeń do efektu:

void AddBoxViewToLayout()
{
    BoxView boxView = new BoxView
    {
        WidthRequest = 100,
        HeightRequest = 100,
        Color = new Color(random.NextDouble(),
                          random.NextDouble(),
                          random.NextDouble())
    };

    TouchEffect touchEffect = new TouchEffect();
    touchEffect.TouchAction += OnTouchEffectAction;
    boxView.Effects.Add(touchEffect);
    absoluteLayout.Children.Add(boxView);
}

Program TouchAction obsługi zdarzeń przetwarza wszystkie zdarzenia dotykowe dla wszystkich BoxView elementów, ale musi wykonać pewną ostrożność: nie może zezwalać na dwa palce na jednym BoxView , ponieważ program implementuje przeciąganie, a dwa palce zakłócałyby siebie nawzajem. Z tego powodu strona definiuje klasę osadzoną dla każdego aktualnie śledzonego palca:

class DragInfo
{
    public DragInfo(long id, Point pressPoint)
    {
        Id = id;
        PressPoint = pressPoint;
    }

    public long Id { private set; get; }

    public Point PressPoint { private set; get; }
}

Dictionary<BoxView, DragInfo> dragDictionary = new Dictionary<BoxView, DragInfo>();

Zawiera dragDictionary wpis dla każdego BoxView aktualnie przeciąganego.

Akcja dotykowa Pressed dodaje element do tego słownika i Released usuwa go. Logika Pressed musi sprawdzić, czy w słowniku znajduje się już element .BoxView Jeśli tak, element jest już przeciągany, BoxView a nowe zdarzenie jest drugim palcem na tym samym BoxViewobiekcie . Moved W przypadku akcji i Released program obsługi zdarzeń musi sprawdzić, czy słownik ma dla tego BoxView wpis i czy właściwość touch Id dla tego przeciągniętego BoxView jest zgodna z właściwością w wpisie słownika:

void OnTouchEffectAction(object sender, TouchActionEventArgs args)
{
    BoxView boxView = sender as BoxView;

    switch (args.Type)
    {
        case TouchActionType.Pressed:
            // Don't allow a second touch on an already touched BoxView
            if (!dragDictionary.ContainsKey(boxView))
            {
                dragDictionary.Add(boxView, new DragInfo(args.Id, args.Location));

                // Set Capture property to true
                TouchEffect touchEffect = (TouchEffect)boxView.Effects.FirstOrDefault(e => e is TouchEffect);
                touchEffect.Capture = true;
            }
            break;

        case TouchActionType.Moved:
            if (dragDictionary.ContainsKey(boxView) && dragDictionary[boxView].Id == args.Id)
            {
                Rectangle rect = AbsoluteLayout.GetLayoutBounds(boxView);
                Point initialLocation = dragDictionary[boxView].PressPoint;
                rect.X += args.Location.X - initialLocation.X;
                rect.Y += args.Location.Y - initialLocation.Y;
                AbsoluteLayout.SetLayoutBounds(boxView, rect);
            }
            break;

        case TouchActionType.Released:
            if (dragDictionary.ContainsKey(boxView) && dragDictionary[boxView].Id == args.Id)
            {
                dragDictionary.Remove(boxView);
            }
            break;
    }
}

Logika Pressed ustawia Capture właściwość TouchEffect obiektu na true. Ma to wpływ na dostarczanie wszystkich kolejnych zdarzeń dla tego palca do tej samej procedury obsługi zdarzeń.

Logika Moved przenosi BoxView obiekt przez zmianę dołączonej LayoutBounds właściwości. Location Właściwość argumentów zdarzenia jest zawsze względna względem BoxView przeciągania, a jeśli BoxView obiekt jest przeciągany ze stałą szybkością, Location właściwości kolejnych zdarzeń będą w przybliżeniu takie same. Jeśli na przykład palec naciska BoxView w środku, Pressed akcja przechowuje PressPoint właściwość (50, 50), która pozostaje taka sama w przypadku kolejnych zdarzeń. BoxView Jeśli element jest przeciągany po przekątnej z stałą szybkością, kolejne Location właściwości podczas Moved akcji mogą być wartościami (55, 55), w tym przypadku Moved logika dodaje wartość 5 do pozycji poziomej i pionowej BoxViewobiektu . BoxView Porusza się tak, aby jego środek był ponownie bezpośrednio pod palcem.

Można jednocześnie przenosić wiele BoxView elementów za pomocą różnych palców.

Potrójny zrzut ekranu przedstawiający stronę przeciągania w widoku BoxView

Podklasowanie widoku

Często łatwiej jest elementowi Xamarin.Forms obsługiwać własne zdarzenia dotykowe. Przeciąganie strony Przeciąganie elementu BoxView działa tak samo jak na stronie Przeciąganie obiektu BoxView, ale elementy przeciągnięte przez użytkownika są wystąpieniami klasy pochodzącej DraggableBoxView z BoxViewklasy :

class DraggableBoxView : BoxView
{
    bool isBeingDragged;
    long touchId;
    Point pressPoint;

    public DraggableBoxView()
    {
        TouchEffect touchEffect = new TouchEffect
        {
            Capture = true
        };
        touchEffect.TouchAction += OnTouchEffectAction;
        Effects.Add(touchEffect);
    }

    void OnTouchEffectAction(object sender, TouchActionEventArgs args)
    {
        switch (args.Type)
        {
            case TouchActionType.Pressed:
                if (!isBeingDragged)
                {
                    isBeingDragged = true;
                    touchId = args.Id;
                    pressPoint = args.Location;
                }
                break;

            case TouchActionType.Moved:
                if (isBeingDragged && touchId == args.Id)
                {
                    TranslationX += args.Location.X - pressPoint.X;
                    TranslationY += args.Location.Y - pressPoint.Y;
                }
                break;

            case TouchActionType.Released:
                if (isBeingDragged && touchId == args.Id)
                {
                    isBeingDragged = false;
                }
                break;
        }
    }
}

Konstruktor tworzy i dołącza TouchEffectwłaściwość , a następnie ustawia właściwość po pierwszym utworzeniu Capture wystąpienia tego obiektu. Słownik nie jest wymagany, ponieważ sama klasa przechowuje isBeingDraggedwartości , pressPointi skojarzone touchId z każdym palcem. Obsługa zmienia właściwości iTranslationY, aby logika działała nawet wtedy, gdy element nadrzędny obiektu DraggableBoxView nie jest elementem AbsoluteLayout.TranslationXMoved

Integracja z usługą SkiaSharp

Następne dwa pokazy wymagają grafiki, a w tym celu używają biblioteki SkiaSharp. Przed zapoznaniem się z tymi przykładami warto dowiedzieć się więcej na temat korzystania z biblioteki SkiaSharp Xamarin.Forms . Dwa pierwsze artykuły ("SkiaSharp Drawing Basics" i "SkiaSharp Lines and Paths") obejmują wszystkie potrzebne tutaj elementy.

Strona Rysunku wielokropka umożliwia rysowanie wielokropka, przesuwając palcem na ekranie. W zależności od tego, jak przesuwasz palec, możesz narysować wielokropek od lewej górnej do prawej dolnej lub z dowolnego innego rogu do przeciwnego rogu. Wielokropek jest rysowany z losowym kolorem i nieprzezroczystością.

Potrójny zrzut ekranu przedstawiający stronę rysunku wielokropka

Jeśli następnie dotkniesz jednego z wielokropka, możesz przeciągnąć go do innej lokalizacji. Wymaga to techniki znanej jako "testowanie trafień", która polega na wyszukiwaniu obiektu graficznego w określonym punkcie. Wielokropek SkiaSharp nie Xamarin.Forms są elementami, więc nie mogą wykonywać własnego TouchEffect przetwarzania. Element TouchEffect musi mieć zastosowanie do całego SKCanvasView obiektu.

Plik EllipseDrawPage.xaml tworzy wystąpienie SKCanvasView w jednej komórce Grid. Obiekt TouchEffect jest dołączony do tego Gridobiektu :

<Grid x:Name="canvasViewGrid"
        Grid.Row="1"
        BackgroundColor="White">

    <skia:SKCanvasView x:Name="canvasView"
                        PaintSurface="OnCanvasViewPaintSurface" />
    <Grid.Effects>
        <tt:TouchEffect Capture="True"
                        TouchAction="OnTouchEffectAction" />
    </Grid.Effects>
</Grid>

W systemach Android i platforma uniwersalna systemu Windows TouchEffect można dołączyć je bezpośrednio do SKCanvasViewelementu , ale w systemie iOS, który nie działa. Zwróć uwagę, że właściwość jest ustawiona Capture na true.

Każdy wielokropek renderowany przez bibliotekę SkiaSharp jest reprezentowany przez obiekt typu EllipseDrawingFigure:

class EllipseDrawingFigure
{
    SKPoint pt1, pt2;

    public EllipseDrawingFigure()
    {
    }

    public SKColor Color { set; get; }

    public SKPoint StartPoint
    {
        set
        {
            pt1 = value;
            MakeRectangle();
        }
    }

    public SKPoint EndPoint
    {
        set
        {
            pt2 = value;
            MakeRectangle();
        }
    }

    void MakeRectangle()
    {
        Rectangle = new SKRect(pt1.X, pt1.Y, pt2.X, pt2.Y).Standardized;
    }

    public SKRect Rectangle { set; get; }

    // For dragging operations
    public Point LastFingerLocation { set; get; }

    // For the dragging hit-test
    public bool IsInEllipse(SKPoint pt)
    {
        SKRect rect = Rectangle;

        return (Math.Pow(pt.X - rect.MidX, 2) / Math.Pow(rect.Width / 2, 2) +
                Math.Pow(pt.Y - rect.MidY, 2) / Math.Pow(rect.Height / 2, 2)) < 1;
    }
}

Właściwości StartPoint i EndPoint są używane, gdy program przetwarza dane wejściowe dotyku; Rectangle właściwość jest używana do rysowania wielokropka. Właściwość LastFingerLocation wchodzi w grę, gdy wielokropek jest przeciągany, a IsInEllipse metoda pomaga w testowaniu trafień. Metoda zwraca true wartość , jeśli punkt znajduje się wewnątrz wielokropka.

Plik związany z kodem przechowuje trzy kolekcje:

Dictionary<long, EllipseDrawingFigure> inProgressFigures = new Dictionary<long, EllipseDrawingFigure>();
List<EllipseDrawingFigure> completedFigures = new List<EllipseDrawingFigure>();
Dictionary<long, EllipseDrawingFigure> draggingFigures = new Dictionary<long, EllipseDrawingFigure>();

Słownik draggingFigure zawiera podzbiór kolekcji completedFigures . Procedura obsługi zdarzeń SkiaSharp PaintSurface po prostu renderuje obiekty w tych completedFigures kolekcjach i inProgressFigures :

SKPaint paint = new SKPaint
{
    Style = SKPaintStyle.Fill
};
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
    SKCanvas canvas = args.Surface.Canvas;
    canvas.Clear();

    foreach (EllipseDrawingFigure figure in completedFigures)
    {
        paint.Color = figure.Color;
        canvas.DrawOval(figure.Rectangle, paint);
    }
    foreach (EllipseDrawingFigure figure in inProgressFigures.Values)
    {
        paint.Color = figure.Color;
        canvas.DrawOval(figure.Rectangle, paint);
    }
}

Najtrudniejszą częścią przetwarzania dotykowego jest obsługa Pressed . W tym miejscu jest wykonywane testowanie trafień, ale jeśli kod wykryje wielokropek pod palcem użytkownika, wielokropek może być przeciągnięty tylko wtedy, gdy nie jest obecnie przeciągany przez inny palec. Jeśli nie ma wielokropka pod palcem użytkownika, kod rozpoczyna proces rysowania nowego wielokropka:

case TouchActionType.Pressed:
    bool isDragOperation = false;

    // Loop through the completed figures
    foreach (EllipseDrawingFigure fig in completedFigures.Reverse<EllipseDrawingFigure>())
    {
        // Check if the finger is touching one of the ellipses
        if (fig.IsInEllipse(ConvertToPixel(args.Location)))
        {
            // Tentatively assume this is a dragging operation
            isDragOperation = true;

            // Loop through all the figures currently being dragged
            foreach (EllipseDrawingFigure draggedFigure in draggingFigures.Values)
            {
                // If there's a match, we'll need to dig deeper
                if (fig == draggedFigure)
                {
                    isDragOperation = false;
                    break;
                }
            }

            if (isDragOperation)
            {
                fig.LastFingerLocation = args.Location;
                draggingFigures.Add(args.Id, fig);
                break;
            }
        }
    }

    if (isDragOperation)
    {
        // Move the dragged ellipse to the end of completedFigures so it's drawn on top
        EllipseDrawingFigure fig = draggingFigures[args.Id];
        completedFigures.Remove(fig);
        completedFigures.Add(fig);
    }
    else // start making a new ellipse
    {
        // Random bytes for random color
        byte[] buffer = new byte[4];
        random.NextBytes(buffer);

        EllipseDrawingFigure figure = new EllipseDrawingFigure
        {
            Color = new SKColor(buffer[0], buffer[1], buffer[2], buffer[3]),
            StartPoint = ConvertToPixel(args.Location),
            EndPoint = ConvertToPixel(args.Location)
        };
        inProgressFigures.Add(args.Id, figure);
    }
    canvasView.InvalidateSurface();
    break;

Innym przykładem SkiaSharp jest strona Finger Paint . Możesz wybrać kolor pociągnięcia i szerokość pociągnięcia z dwóch Picker widoków, a następnie narysować jeden lub więcej palców:

Potrójny zrzut ekranu przedstawiający stronę Finger Paint

Ten przykład wymaga również oddzielnej klasy do reprezentowania każdej linii malowanej na ekranie:

class FingerPaintPolyline
{
    public FingerPaintPolyline()
    {
        Path = new SKPath();
    }

    public SKPath Path { set; get; }

    public Color StrokeColor { set; get; }

    public float StrokeWidth { set; get; }
}

Obiekt jest używany do renderowania SKPath każdego wiersza. Plik FingerPaint.xaml.cs przechowuje dwie kolekcje tych obiektów: jedną dla aktualnie rysowanych wielolinii, a drugą dla ukończonych wielolinii:

Dictionary<long, FingerPaintPolyline> inProgressPolylines = new Dictionary<long, FingerPaintPolyline>();
List<FingerPaintPolyline> completedPolylines = new List<FingerPaintPolyline>();

Przetwarzanie Pressed tworzy nowy FingerPaintPolylineobiekt , wywołuje MoveTo obiekt path w celu przechowywania punktu początkowego i dodaje ten obiekt do słownika inProgressPolylines . Przetwarzanie Moved wywołuje LineTo obiekt path z nową pozycją palca, a Released przetwarzanie przenosi ukończoną wielolinię z inProgressPolylines do completedPolylines. Po raz kolejny rzeczywisty kod rysunku SkiaSharp jest stosunkowo prosty:

SKPaint paint = new SKPaint
{
    Style = SKPaintStyle.Stroke,
    StrokeCap = SKStrokeCap.Round,
    StrokeJoin = SKStrokeJoin.Round
};
...
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
    SKCanvas canvas = args.Surface.Canvas;
    canvas.Clear();

    foreach (FingerPaintPolyline polyline in completedPolylines)
    {
        paint.Color = polyline.StrokeColor.ToSKColor();
        paint.StrokeWidth = polyline.StrokeWidth;
        canvas.DrawPath(polyline.Path, paint);
    }

    foreach (FingerPaintPolyline polyline in inProgressPolylines.Values)
    {
        paint.Color = polyline.StrokeColor.ToSKColor();
        paint.StrokeWidth = polyline.StrokeWidth;
        canvas.DrawPath(polyline.Path, paint);
    }
}

Śledzenie widoku do widoku dotykowego

We wszystkich poprzednich przykładach ustawiono Capture właściwość TouchEffect elementu na true, w momencie utworzenia TouchEffect lub wystąpienia Pressed zdarzenia. Gwarantuje to, że ten sam element odbiera wszystkie zdarzenia skojarzone z palcem, który po raz pierwszy nacisnął widok. Ostatnia próbka nie jest ustawiona Capture na true. Powoduje to różne zachowanie, gdy palec w kontakcie z ekranem przenosi się z jednego elementu do drugiego. Element, z którego przesuwa się palec, odbiera zdarzenie z właściwością ustawioną Type na , a drugi element odbiera zdarzenie z ustawieniem TypeTouchActionType.Entered.TouchActionType.Exited

Ten typ przetwarzania dotykowego jest bardzo przydatny w przypadku klawiatury muzycznej. Klawisz powinien być w stanie wykryć, kiedy jest naciśnięty, ale także wtedy, gdy palec przesuwa się z jednego klawisza do drugiego.

Na stronie Klawiatura dyskretna zdefiniowano małe WhiteKey klasy BlackKey pochodzące z Keyklasy , które pochodzą z BoxView klasy .

Klasa Key jest gotowa do użycia w rzeczywistym programie muzycznym. Definiuje właściwości publiczne o nazwie IsPressed i KeyNumber, które mają być ustawione na kod klucza ustanowiony przez standard MIDI. Klasa Key definiuje również zdarzenie o nazwie StatusChanged, które jest wywoływane podczas IsPressed zmiany właściwości.

Na każdym klawiszu dozwolone jest wiele palców. Z tego powodu Key klasa utrzymuje List numery identyfikatorów dotykowych wszystkich palców, które obecnie dotykają tego klucza:

List<long> ids = new List<long>();

Procedura TouchAction obsługi zdarzeń dodaje identyfikator do ids listy zarówno Pressed dla typu zdarzenia, jak i Entered typu, ale tylko wtedy, gdy IsInContact właściwość jest true dla Entered zdarzenia. Identyfikator jest usuwany z List zdarzenia lub Exited :Released

void OnTouchEffectAction(object sender, TouchActionEventArgs args)
{
    switch (args.Type)
    {
      case TouchActionType.Pressed:
          AddToList(args.Id);
          break;

        case TouchActionType.Entered:
            if (args.IsInContact)
            {
                AddToList(args.Id);
            }
            break;

        case TouchActionType.Moved:
            break;

        case TouchActionType.Released:
        case TouchActionType.Exited:
            RemoveFromList(args.Id);
            break;
    }
}

Metody AddToList i RemoveFromList sprawdzają, czy List element zmienił się między pustymi i niepustymi StatusChanged , a jeśli tak, wywołuje zdarzenie.

Różne WhiteKey elementy i BlackKey są rozmieszczone w pliku XAML strony, który wygląda najlepiej, gdy telefon jest przechowywany w trybie poziomym:

Potrójny zrzut ekranu przedstawiający stronę Klawiatura dyskretna

Jeśli zamiatasz palcem przez klucze, zobaczysz niewielkie zmiany koloru, że zdarzenia dotykowe są przenoszone z jednego klucza do drugiego.

Podsumowanie

W tym artykule pokazano, jak wywoływać zdarzenia w efekcie oraz jak pisać i używać efektu implementujące przetwarzanie wielodotykowe niskiego poziomu.