Włącz przeciąganie i upuszczanie

Platforma Android typu „przeciągnij i upuść” umożliwia dodawanie do aplikacji interaktywnych funkcji przeciągania i upuszczania. Korzystając z tej funkcji, użytkownicy mogą kopiować i przenosić tekst, obrazy, obiekty oraz dowolne treści, które można reprezentować za pomocą identyfikatorów URI, z jednego View do innego w aplikacji lub między aplikacjami w trybie wielu okien.

Przeciąganie i upuszczanie ciągu tekstowego i obrazu w aplikacji. Przeciąganie i upuszczanie ciągu tekstowego i obrazu między aplikacjami w trybie podzielonego ekranu.
Rysunek 1. Przeciągnij i upuść w aplikacji.
Rysunek 2. Przeciąganie i upuszczanie między aplikacjami

Platforma zawiera klasę zdarzeń przeciągania, detektory przeciągania oraz klasy i metody pomocnicze. Platforma służy głównie do przenoszenia danych, ale możesz też używać jej do innych działań w interfejsie. Możesz na przykład utworzyć aplikację, która miesza kolory, gdy użytkownik przeciągnie ikonę koloru na inną. W pozostałej części dokumentu jest jednak opis platformy „przeciągnij i upuść” w kontekście przenoszenia danych.

Przegląd

Operacja przeciągania i upuszczania rozpoczyna się, gdy użytkownik wykonuje w interfejsie gest, który aplikacja rozpoznaje jako sygnał do rozpoczęcia przeciągania danych. W odpowiedzi aplikacja powiadamia system o rozpoczęciu operacji przeciągania i upuszczania. System wywołuje aplikację, by uzyskać reprezentację przeciąganych danych, czyli cień przeciągnięty.

Gdy użytkownik przesunie cień na układ aplikacji, system wysyła zdarzenia przeciągania do odbiorników i metod wywołania zwrotnego powiązanych z obiektami View w układzie. Jeśli użytkownik zwolni cień przez przeciąganie w widoku, który może zaakceptować dane (obszar docelowy), system wyśle dane do widoku docelowego. Operacja przeciągania i upuszczania kończy się, gdy użytkownik zwolni cień, niezależnie od tego, czy rzucany cień znajduje się nad celem, czy nie.

Utwórz detektor zdarzeń przeciągania, implementując element View.OnDragListener. Ustaw odbiornik za pomocą metody setOnDragListener() obiektu View. Każdy widok w układzie ma też metodę wywołania zwrotnego onDragEvent().

Aplikacja powiadamia system o możliwości rozpoczęcia operacji przeciągania i upuszczania, wywołując metodę startDragAndDrop(), która informuje system, że ma wysyłać zdarzenia przeciągania. Metoda ta zapewnia również systemowi dane, które przeciąga użytkownik, oraz metadane opisujące dane. Możesz wywołać startDragAndDrop() w dowolnym elemencie View w bieżącym układzie. System używa obiektu View tylko po to, by uzyskać dostęp do ustawień globalnych w układzie.

Podczas przeciągania i upuszczania system wysyła zdarzenia przeciągania do odbiorników lub metod wywołań zwrotnych obiektów View w układzie. Odbiorcy lub metody wywołania zwrotnego używają metadanych, aby zdecydować, czy zaakceptować dane po ich usunięciu. Jeśli użytkownik umieści dane w elemencie docelowym (View, który je akceptuje), system wyśle obiekt zdarzenia przeciągania zawierający te dane do metody detektora lub wywołania zwrotnego w miejscu docelowym.

Przeciąganie detektorów zdarzeń i metod wywołań zwrotnych

Element View otrzymuje zdarzenia przeciągania z detektorem zdarzeń przeciągania, który implementuje metodę View.OnDragListener, lub za pomocą metody wywołania zwrotnego onDragEvent() widoku. Gdy system wywoła metodę lub detektor, zwróci argument DragEvent.

W większości przypadków lepiej użyć odbiornika niż wywołania zwrotnego. Podczas projektowania interfejsów zwykle nie umieszczasz w klasie klas View, ale korzystanie z metody wywołania zwrotnego wymusza utworzenie podklas w celu zastąpienia tej metody. W ramach porównania możesz wdrożyć 1 klasę detektora, a następnie używać jej z wieloma różnymi obiektami View. Możesz ją też zaimplementować jako anonimową klasę wbudowaną lub wyrażenie lambda. Aby ustawić odbiornik obiektu View, wywołaj setOnDragListener().

Zamiast tego możesz zmienić domyślną implementację parametru onDragEvent() bez zastępowania tej metody. Ustaw OnReceiveContentListener w widoku danych. Więcej informacji znajdziesz w sekcji setOnReceiveContentListener(). Następnie metoda onDragEvent() domyślnie wykonuje te działania:

  • Zwraca wartość „true” (prawda) w odpowiedzi na wywołanie funkcji startDragAndDrop().
  • Wywołuje performReceiveContent(), jeśli dane przeciągnij i upuść w widoku. Dane są przekazywane do metody jako obiekt ContentInfo. Metoda wywołuje metodę OnReceiveContentListener.

  • Zwraca wartość „prawda”, jeśli dane przeciągnij i upuść w widoku, a OnReceiveContentListener zużywa dowolną treść.

Określ OnReceiveContentListener do obsługi danych specjalnie na potrzeby Twojej aplikacji. Aby zapewnić zgodność wsteczną do poziomu interfejsu API 24, użyj wersji Jetpack OnReceiveContentListener.

Możesz mieć detektor zdarzeń przeciągania i metodę wywołania zwrotnego dla obiektu View. W takim przypadku system najpierw wywoła odbiornik. System nie wywołuje metody wywołania zwrotnego, chyba że odbiornik zwróci wartość false.

Kombinacja metody onDragEvent() i View.OnDragListener jest podobna do kombinacji elementów onTouchEvent() i View.OnTouchListener używanych ze zdarzeniami dotknięcia.

Proces przeciągania i upuszczania

Proces przeciągania i upuszczania składa się z 4 kroków lub stanów: rozpoczęcie, kontynuowanie, upuszczanie i zakończenie.

Rozpoczęto

W odpowiedzi na gest przeciągania przez użytkownika aplikacja wywołuje startDragAndDrop(), aby nakazać systemowi rozpoczęcie operacji przeciągania i upuszczania. Argumenty metody zapewniają:

  • Dane do przeciągnięcia.
  • Wywołanie zwrotne dotyczące rysowania cienia
  • Metadane opisujące przeciągnięte dane: system odpowiada, wywołując aplikację w celu uzyskania cienia. Następnie system wyświetli na urządzeniu cień. Następnie system wysyła zdarzenie przeciągania z typem działania ACTION_DRAG_STARTED do odbiornika wszystkich obiektów View w bieżącym układzie. Aby nadal otrzymywać zdarzenia przeciągania, w tym możliwe zdarzenia upuszczania, detektor musi zwrócić wartość true. To spowoduje zarejestrowanie słuchacza w systemie. Tylko zarejestrowane detektory otrzymują zdarzenia przeciągania. Na tym etapie detektory mogą też zmieniać wygląd obiektu View, aby wskazać, że widok może zaakceptować zdarzenie spadku. : jeśli detektor zdarzeń przeciągania zwróci wartość false, nie otrzyma zdarzeń przeciągania dla bieżącej operacji, dopóki system nie wyśle zdarzenia przeciągania z typem działania ACTION_DRAG_ENDED. Zwracając false, odbiornik informuje system, że nie chce korzystać z przeciągania i upuszczania ani akceptować przeciągniętych danych.
Kontynuuję
Użytkownik kontynuuje przeciąganie. Gdy cień przeciągania przecina ramkę ograniczającą elementu docelowego, system wysyła co najmniej 1 zdarzenie przeciągania do detektora zdarzeń przeciągania w miejscu docelowym. W odpowiedzi na zdarzenie detektor może zmienić wygląd miejsca docelowego View. Jeśli na przykład zdarzenie wskazuje, że cień przeciągania wchodzi w ramkę obszaru docelowego (typ działania ACTION_DRAG_ENTERED), słuchacz może zareagować, podświetlając View.
Usunięto
Użytkownik puści cień w obrębie ramki ograniczającej cel. System wysyła do detektora elementu docelowego zdarzenie przeciągania z typem działania ACTION_DROP. Obiekt zdarzenia przeciągania zawiera dane, które są przekazywane do systemu w wywołaniu funkcji startDragAndDrop(), które uruchamiają operację. Jeśli detektor przetworzy upuszczone dane, powinien zwrócić do systemu wartość logiczną true. Ten krok występuje tylko wtedy, gdy użytkownik upuść cień w ramce ograniczającej element View, którego odbiornik jest zarejestrowany do odbierania zdarzeń przeciągania (elementu docelowego). Jeśli użytkownik zwolni cień w innej sytuacji, zdarzenie przeciągnięcia ACTION_DROP nie zostanie wysłane.
Zakończone

Po zwolnieniu cienia przez użytkownika i wysłaniu przez system

zdarzenia przeciągania z typem działania ACTION_DROP. W razie potrzeby system wyśle zdarzenie przeciągania z typem działania ACTION_DRAG_ENDED, aby wskazać, że operacja przeciągania i upuszczania została zakończona. Dzieje się tak niezależnie od tego, gdzie użytkownik zwolni cień. Zdarzenie jest wysyłane do każdego detektora, który został zarejestrowany do odbierania zdarzeń przeciągania nawet wtedy, gdy detektor też otrzyma zdarzenie ACTION_DROP.

Szczegółowy opis każdej z tych czynności znajdziesz w sekcji Przeciąganie i upuszczanie.

Przeciąganie zdarzeń

System wysyła zdarzenie przeciągania w formie obiektu DragEvent, który zawiera typ działania opisującego to, co dzieje się w procesie przeciągania i upuszczania. W zależności od typu działania obiekt może też zawierać inne dane.

Detektory zdarzeń przeciągania otrzymują obiekt DragEvent. Aby poznać rodzaj działania, słuchacze wywołują metodę DragEvent.getAction(). Stałe w klasie DragEvent może mieć 6 możliwych wartości zdefiniowanych przez stałe w tabeli 1:

Tabela 1. Typy działań DragEvent

Typ działania Znaczenie
ACTION_DRAG_STARTED Aplikacja wywołuje funkcję startDragAndDrop() i uzyskuje cień przeciągania. Jeśli detektor chce nadal otrzymywać zdarzenia przeciągania w ramach tej operacji, musi zwrócić do systemu wartość logiczną true.
ACTION_DRAG_ENTERED Cień przeciągania wchodzi w granicę detektora zdarzeń przeciągania: View. Jest to pierwszy typ działania zdarzenia odbieranego przez odbiornik, gdy cień przeciągania wejdzie w ramkę ograniczającą.
ACTION_DRAG_LOCATION Po wystąpieniu zdarzenia ACTION_DRAG_ENTERED cień przeciągania wciąż mieści się w ramce ograniczającej zdarzenie View odbiornika.
ACTION_DRAG_EXITED Po wystąpieniu ACTION_DRAG_ENTERED i co najmniej 1 zdarzeniu ACTION_DRAG_LOCATION cień przeciągania przesuwa się poza ramkę ograniczającą odbiornik View zdarzenia przeciągania.
ACTION_DROP Cień przeciągania zwolni się nad obiektem View detektora zdarzeń przeciągania. Ten typ działania jest wysyłany do detektora obiektu View tylko wtedy, gdy w odpowiedzi na zdarzenie przeciągania ACTION_DRAG_STARTED zwraca on wartość logiczną true. Ten typ działania nie jest wysyłany, jeśli użytkownik zwolni cień przez przeciąganie na element View, którego detektor nie jest zarejestrowany, lub jeśli użytkownik zwolni cień przeciągania na element, który nie jest częścią bieżącego układu.

Jeśli udało się przetworzyć spadek, odbiornik zwraca wartość logiczną true. W przeciwnym razie musi zwrócić wartość false.

ACTION_DRAG_ENDED System kończy operację przeciągania i upuszczania. Ten typ działania nie musi być poprzedzony zdarzeniem ACTION_DROP. Jeśli system wyśle żądanie ACTION_DROP, odbiór działania ACTION_DRAG_ENDED nie oznacza, że spadek się powiodła. Aby uzyskać wartość zwracaną w odpowiedzi na ACTION_DROP, detektor musi wywołać getResult(), jak pokazano w tabeli 2. Jeśli zdarzenie ACTION_DROP nie zostało wysłane, getResult() zwraca false.

Obiekt DragEvent zawiera też dane i metadane, które aplikacja udostępnia systemowi w wywołaniu startDragAndDrop(). Część danych jest ważna tylko w przypadku określonych typów działań, jak podano w tabeli 2. Więcej informacji o zdarzeniach i powiązanych z nimi danych znajdziesz w sekcji Przeciąganie i upuszczanie.

Tabela 2. Prawidłowe dane DragEvent według typu działania

Wartość: getAction()
Wartość: getClipDescription()
Wartość: getLocalState()
Wartość: getX()
Wartość: getY()
Wartość: getClipData()
Wartość: getResult()
ACTION_DRAG_STARTED ✓ ✓ ✓ ✓    
ACTION_DRAG_ENTERED ✓ ✓        
ACTION_DRAG_LOCATION ✓ ✓ ✓ ✓    
ACTION_DRAG_EXITED ✓ ✓        
ACTION_DROP ✓ ✓ ✓ ✓ ✓  
ACTION_DRAG_ENDED   ✓       ✓

Metody DragEvent getAction(), describeContents(), writeToParcel() i toString() zawsze zwracają prawidłowe dane.

Jeśli metoda nie zawiera prawidłowych danych dla określonego typu działania, zwraca null lub 0 w zależności od typu wyniku.

Przeciąganie cienia

Podczas przeciągania i upuszczania system wyświetla obraz, który użytkownik przeciąga. Na potrzeby przenoszenia danych ten obraz przedstawia przeciągane dane. W przypadku innych operacji obraz przedstawia pewien aspekt przeciągania.

Jest to tzw. cień przez przeciąganie. Tworzysz ją za pomocą metod zadeklarowanych dla obiektu View.DragShadowBuilder. Kreator jest przekazywany do systemu po rozpoczęciu operacji przeciągania i upuszczania za pomocą startDragAndDrop(). W ramach odpowiedzi na startDragAndDrop() system wywołuje metody wywołania zwrotnego zdefiniowane w View.DragShadowBuilder, aby uzyskać cień przeciągania.

Klasa View.DragShadowBuilder ma 2 konstruktory:

View.DragShadowBuilder(View)

Ten konstruktor akceptuje wszystkie obiekty View aplikacji. Konstruktor przechowuje obiekt View w obiekcie View.DragShadowBuilder, więc wywołania zwrotne mogą uzyskać do niego dostęp w celu utworzenia cienia. Widok nie musi być widokiem View, który użytkownik wybierze, aby rozpocząć operację przeciągania.

Jeśli używasz tego konstruktora, nie musisz rozszerzać obiektu View.DragShadowBuilder ani zastępować jego metod. Domyślnie otrzymujesz cień przeciągania, który wygląda tak samo jak obiekt View przekazywany jako argument, położony pod lokalizacją, w której użytkownik dotyka ekranu.

View.DragShadowBuilder()

Jeśli używasz tego konstruktora, w obiekcie View.DragShadowBuilder nie będzie dostępny żaden obiekt View. Pole jest ustawione na null. Musisz rozszerzyć zakres View.DragShadowBuilder i zastąpić stosowane w nim metody. W przeciwnym razie otrzymasz niewidoczny cień. System nie zgłasza błędu.

Klasa View.DragShadowBuilder ma 2 metody, które razem tworzą cień przeciągania:

onProvideShadowMetrics()

System wywołuje tę metodę natychmiast po wywołaniu funkcji startDragAndDrop(). Użyj tej metody, aby przesłać do systemu wymiary i punkt styku przeciągania cienia. Ta metoda ma 2 parametry:

outShadowSize: obiekt Point. Szerokość cienia przeciągania zostanie wpisana w polu x, a jego wysokość – w y.

outShadowTouchPoint: obiekt Point. Punkt styczności z klientem to miejsce w cieniu, które musi znajdować się pod palcem użytkownika podczas przeciągania. Pozycja X pojawia się w pozycji x, a pozycja Y – w y.

onDrawShadow()

Bezpośrednio po wywołaniu onProvideShadowMetrics() system wywołuje metodę onDrawShadow(), aby utworzyć cień przeciągania. Ma ona jeden argument – obiekt Canvas, który system tworzy na podstawie parametrów podanych w parametrze onProvideShadowMetrics(). Ta metoda generuje cień przeciągania na podany element Canvas.

Aby poprawić wydajność, rozmiar cienia przeciągania powinien być mały. W przypadku pojedynczego produktu możesz użyć ikony. Jeśli wybierasz wiele produktów, lepiej jest umieszczać ikony w stosunku do siebie, a nie całe obrazy rozłożone na ekranie.

Przeciąganie i upuszczanie

W tej sekcji znajdziesz szczegółowe instrukcje rozpoczynania przeciągania, reagowania na zdarzenia podczas przeciągania, reagowania na zdarzenia upuszczania oraz zakończenia operacji przeciągania i upuszczania.

Rozpocznij przeciąganie

Użytkownik rozpoczyna przeciąganie gestem przeciągania, zwykle naciskając i przytrzymując obiekt View. W odpowiedzi aplikacja musi:

  1. Utwórz obiekt ClipData i obiekt ClipData.Item na potrzeby przenoszonych danych. W ramach ClipData podaj metadane, które są przechowywane w obiekcie ClipDescription w obrębie ClipData. W przypadku operacji przeciągania i upuszczania, która nie odzwierciedla przenoszenia danych, można użyć elementu null zamiast rzeczywistego obiektu.

    Ten fragment kodu pokazuje na przykład, jak odpowiedzieć na gest dotknięcia i przytrzymania elementu ImageView za pomocą utworzenia obiektu ClipData zawierającego tag (lub etykietę) obiektu ImageView:

    Kotlin

    // Create a string for the ImageView label.
    val IMAGEVIEW_TAG = "icon bitmap"
    ...
    val imageView = ImageView(context).apply {
    // Set the bitmap for the ImageView from an icon bitmap defined elsewhere.
    setImageBitmap(iconBitmap)
    tag = IMAGEVIEW_TAG
    setOnLongClickListener { v ->
            // Create a new ClipData. This is done in two steps to provide
            // clarity. The convenience method ClipData.newPlainText() can
            // create a plain text ClipData in one step.
    
            // Create a new ClipData.Item from the ImageView object's tag.
            val item = ClipData.Item(v.tag as? CharSequence)
    
            // Create a new ClipData using the tag as a label, the plain text
            // MIME type, and the already-created item. This creates a new
            // ClipDescription object within the ClipData and sets its MIME type
            // to "text/plain".
            val dragData = ClipData(
                v.tag as? CharSequence,
                arrayOf(ClipDescription.MIMETYPE_TEXT_PLAIN),
                item)
    
            // Instantiate the drag shadow builder.
            val myShadow = MyDragShadowBuilder(view: this)
    
            // Start the drag.
            v.startDragAndDrop(dragData,  // The data to be dragged.
                               myShadow,  // The drag shadow builder.
                               null,      // No need to use local data.
                               0          // Flags. Not currently used, set to 0.
            )
    
           // Indicate that the long-click is handled.
           true
    }
    }
    

    Java

    // Create a string for the ImageView label.
    private static final String IMAGEVIEW_TAG = "icon bitmap";
    ...
    // Create a new ImageView.
    ImageView imageView = new ImageView(context);
    
    // Set the bitmap for the ImageView from an icon bitmap defined elsewhere.
    imageView.setImageBitmap(iconBitmap);
    
    // Set the tag.
    imageView.setTag(IMAGEVIEW_TAG);
    
    // Set a long-click listener for the ImageView using an anonymous listener
    // object that implements the OnLongClickListener interface.
    imageView.setOnLongClickListener( v -> {
    
    // Create a new ClipData. This is done in two steps to provide clarity. The
    // convenience method ClipData.newPlainText() can create a plain text
    // ClipData in one step.
    
    // Create a new ClipData.Item from the ImageView object's tag.
    ClipData.Item item = new ClipData.Item((CharSequence) v.getTag());
    
    // Create a new ClipData using the tag as a label, the plain text MIME type,
    // and the already-created item. This creates a new ClipDescription object
    // within the ClipData and sets its MIME type to "text/plain".
    ClipData dragData = new ClipData(
            (CharSequence) v.getTag(),
            new String[] { ClipDescription.MIMETYPE_TEXT_PLAIN },
            item);
    
    // Instantiate the drag shadow builder.
    View.DragShadowBuilder myShadow = new MyDragShadowBuilder(imageView);
    
    // Start the drag.
    v.startDragAndDrop(dragData,  // The data to be dragged.
                           myShadow,  // The drag shadow builder.
                           null,      // No need to use local data.
                           0          // Flags. Not currently used, set to 0.
    );
    
    // Indicate that the long-click is handled.
    return true;
    });
    
  2. Określ myDragShadowBuilder, zastępując metody z View.DragShadowBuilder. Ten fragment kodu tworzy mały, prostokątny, szary cień do przeciągania elementu TextView:

    Kotlin

    private class MyDragShadowBuilder(view: View) : View.DragShadowBuilder(view) {
    
    private val shadow = ColorDrawable(Color.LTGRAY)
    
    // Define a callback that sends the drag shadow dimensions and touch point
    // back to the system.
    override fun onProvideShadowMetrics(size: Point, touch: Point) {
    
            // Set the width of the shadow to half the width of the original
            // View.
            val width: Int = view.width / 2
    
            // Set the height of the shadow to half the height of the original
            // View.
            val height: Int = view.height / 2
    
            // The drag shadow is a ColorDrawable. Set its dimensions to
            // be the same as the Canvas that the system provides. As a result,
            // the drag shadow fills the Canvas.
            shadow.setBounds(0, 0, width, height)
    
            // Set the size parameter's width and height values. These get back
            // to the system through the size parameter.
            size.set(width, height)
    
            // Set the touch point's position to be in the middle of the drag
            // shadow.
            touch.set(width / 2, height / 2)
    }
    
    // Define a callback that draws the drag shadow in a Canvas that the system
    // constructs from the dimensions passed to onProvideShadowMetrics().
    override fun onDrawShadow(canvas: Canvas) {
    
            // Draw the ColorDrawable on the Canvas passed in from the system.
            shadow.draw(canvas)
    }
    }
    

    Java

    private static class MyDragShadowBuilder extends View.DragShadowBuilder {
    
    // The drag shadow image, defined as a drawable object.
    private static Drawable shadow;
    
    // Constructor.
    public MyDragShadowBuilder(View view) {
    
            // Store the View parameter.
            super(view);
    
            // Create a draggable image that fills the Canvas provided by the
            // system.
            shadow = new ColorDrawable(Color.LTGRAY);
    }
    
    // Define a callback that sends the drag shadow dimensions and touch point
    // back to the system.
    @Override
    public void onProvideShadowMetrics (Point size, Point touch) {
    
            // Define local variables.
            int width, height;
    
            // Set the width of the shadow to half the width of the original
            // View.
            width = getView().getWidth() / 2;
    
            // Set the height of the shadow to half the height of the original
            // View.
            height = getView().getHeight() / 2;
    
            // The drag shadow is a ColorDrawable. Set its dimensions to
            // be the same as the Canvas that the system provides. As a result,
            // the drag shadow fills the Canvas.
            shadow.setBounds(0, 0, width, height);
    
            // Set the size parameter's width and height values. These get back
            // to the system through the size parameter.
            size.set(width, height);
    
            // Set the touch point's position to be in the middle of the drag
            // shadow.
            touch.set(width / 2, height / 2);
    }
    
    // Define a callback that draws the drag shadow in a Canvas that the system
    // constructs from the dimensions passed to onProvideShadowMetrics().
    @Override
    public void onDrawShadow(Canvas canvas) {
    
            // Draw the ColorDrawable on the Canvas passed in from the system.
            shadow.draw(canvas);
    }
    }
    

Reakcja na rozpoczęcie przeciągania

Podczas przeciągania system wysyła zdarzenia przeciągania do odbiorników obiektów View w bieżącym układzie. Słuchacze reagują, wywołując DragEvent.getAction(), aby określić typ działania. Na początku przeciągania ta metoda zwraca ACTION_DRAG_STARTED.

W odpowiedzi na zdarzenie o typie działania ACTION_DRAG_STARTED detektor zdarzeń przeciągania musi wykonać te czynności:

  1. Wywołaj metodę DragEvent.getClipDescription() i użyj metod typu MIME w zwróconym obiekcie ClipDescription, aby sprawdzić, czy detektor może zaakceptować przeciągnięte dane.

    Jeśli przeciąganie i upuszczanie nie odzwierciedla przenoszenia danych, może być niepotrzebne.

  2. Jeśli detektor zdarzeń przeciągania może zaakceptować upuszczanie, musi zwrócić wartość true, by poinformować system, że ma kontynuować wysyłanie tych zdarzeń. Jeśli detektor nie może zaakceptować upuszczania, musi zwrócić false, a system przestanie wysyłać zdarzenia przeciągania do detektora, dopóki system nie wyśle metody ACTION_DRAG_ENDED w celu zakończenia operacji przeciągania i upuszczania.

W przypadku zdarzenia ACTION_DRAG_STARTED nieprawidłowe są te metody DragEvent: getClipData(), getX(), getY() i getResult().

Obsługa zdarzeń podczas przeciągania

Podczas przeciągania elementów detektory, które w odpowiedzi na zdarzenie przeciągania ACTION_DRAG_STARTED zwracają wartość true, nadal otrzymują zdarzenia przeciągania. Rodzaje zdarzeń przeciągania odbieranych przez odbiornik podczas przeciągania zależą od lokalizacji przeciąganego cienia i widoczności View detektora. Detektory używają zdarzeń przeciągania przede wszystkim do określenia, czy muszą zmienić wygląd View.

Podczas przeciągania funkcja DragEvent.getAction() zwraca jedną z trzech wartości:

  • ACTION_DRAG_ENTERED: detektor odbiera ten typ działania zdarzenia, gdy punkt styczności z klientem (punkt na ekranie pod palcem lub myszą użytkownika) wpisze się w ramkę ograniczającą View detektora.
  • ACTION_DRAG_LOCATION: gdy detektor otrzyma zdarzenie ACTION_DRAG_ENTERED, otrzyma nowe zdarzenie ACTION_DRAG_LOCATION za każdym razem, gdy punkt styczności zostanie przesunięty, aż otrzyma zdarzenie ACTION_DRAG_EXITED. Metody getX() i getY() zwracają współrzędne X i Y punktu styczności z klientem.
  • ACTION_DRAG_EXITED: ten typ działania związanego ze zdarzeniem jest wysyłany do detektora, który wcześniej odbierał ACTION_DRAG_ENTERED. Zdarzenie jest wysyłane, gdy punkt styku cienia przeciągania zmieni się z ramki ograniczającej w elemencie View odbiornika i wyjdzie poza tę ramkę.

Detektor zdarzeń przeciągnięcia nie musi reagować na żaden z tych typów działań. Jeśli detektor zwróci wartość do systemu, zostanie ona zignorowana.

Oto kilka wskazówek dotyczących reagowania na te działania:

  • W odpowiedzi na polecenie ACTION_DRAG_ENTERED lub ACTION_DRAG_LOCATION odbiornik może zmienić wygląd wskaźnika View, aby wskazać, że dane wyświetlenie może potencjalnie skutkować porzuceniem.
  • Zdarzenie o typie działania ACTION_DRAG_LOCATION zawiera prawidłowe dane dla zdarzeń getX() i getY() odpowiadające lokalizacji punktu styczności z klientem. Na podstawie tych informacji słuchacz może zmienić wygląd elementu View w punkcie styczności z klientem lub określić dokładną pozycję, w której użytkownik może uwolnić cień, czyli upuścić dane.
  • W odpowiedzi na polecenie ACTION_DRAG_EXITED detektor musi zresetować wszelkie zmiany wyglądu wprowadzone w odpowiedzi na działanie ACTION_DRAG_ENTERED lub ACTION_DRAG_LOCATION. Informuje to użytkownika, że View nie jest już celem bezpośredniego spadku.

Reagowanie na spadek

Gdy użytkownik zwolni cień przeciągania na element View, a View wcześniej zgłosi, że może zaakceptować przeciągnięte treści, system wyśle do obiektu View zdarzenie przeciągania z typem działania ACTION_DROP.

Detektor zdarzeń przeciągania musi:

  1. Wywołaj getClipData(), aby uzyskać obiekt ClipData, który został pierwotnie przekazany w wywołaniu startDragAndDrop(), i przetworzyć dane. Jeśli przeciąganie i upuszczanie nie odzwierciedla przenoszenia danych, nie jest konieczne.

  2. Zwraca wartość logiczną true, aby wskazać, że spadek został przetworzony prawidłowo, lub false, jeśli nie został przetworzony. Zwrócona wartość staje się wartością zwracaną przez funkcję getResult() dla ostatecznego zdarzenia ACTION_DRAG_ENDED. Jeśli system nie wysyła zdarzenia ACTION_DROP, wartość zwrócona przez funkcję getResult() w przypadku zdarzenia ACTION_DRAG_ENDED to false.

W przypadku zdarzenia ACTION_DROP funkcje getX() i getY() używają układu współrzędnych View, który otrzymuje spadek, aby zwrócić pozycje X i Y punktu styczności z klientem w momencie spadku.

System pozwala użytkownikowi zwolnić cień przeciągania na elemencie View, którego detektor zdarzeń przeciągania nie otrzymuje zdarzeń przeciągania. Pozwala też użytkownikom uwolnić cień przeciągany nad pustymi obszarami interfejsu aplikacji lub obszarami poza aplikacją. W każdym z tych przypadków system nie wysyła zdarzenia z typem działania ACTION_DROP, chociaż wysyła zdarzenie ACTION_DRAG_ENDED.

Reakcja na koniec przeciągania

Natychmiast po zwolnieniu cienia przez użytkownika system wysyła zdarzenie przeciągania z typem działania ACTION_DRAG_ENDED do wszystkich detektorów zdarzeń przeciągania w Twojej aplikacji. Oznacza to, że operacja przeciągania i upuszczania została zakończona.

Każdy detektor zdarzeń przeciągania musi:

  1. Jeśli podczas wykonywania operacji detektor zmieni wygląd obiektu View, będzie on musiał przywrócić domyślny wygląd View. Jest to wizualna wskazówka dla użytkownika, że operacja została zakończona.
  2. Detektor może opcjonalnie wywołać metodę getResult(), by dowiedzieć się więcej o tej operacji. Jeśli detektor zwraca w odpowiedzi na zdarzenie typu działania ACTION_DROP wartość true, wtedy getResult() zwraca wartość logiczną true. We wszystkich innych przypadkach getResult() zwraca wartość logiczną false, nawet wtedy, gdy system nie wysyła zdarzenia ACTION_DROP.
  3. Aby wskazać pomyślne zakończenie operacji przeciągania i upuszczania, detektor musi zwrócić do systemu wartość logiczną true.

Reagowanie na zdarzenia przeciągania – przykład

Wszystkie zdarzenia przeciągania są odbierane przez metodę lub detektor zdarzeń przeciągania. Ten fragment kodu jest prostym przykładem reagowania na zdarzenia przeciągania:

Kotlin

val imageView = ImageView(this)

// Set the drag event listener for the View.
imageView.setOnDragListener { v, e ->

    // Handle each of the expected events.
    when (e.action) {
        DragEvent.ACTION_DRAG_STARTED -> {
            // Determine whether this View can accept the dragged data.
            if (e.clipDescription.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) {
                // As an example, apply a blue color tint to the View to
                // indicate that it can accept data.
                (v as? ImageView)?.setColorFilter(Color.BLUE)

                // Invalidate the view to force a redraw in the new tint.
                v.invalidate()

                // Return true to indicate that the View can accept the dragged
                // data.
                true
            } else {
                // Return false to indicate that, during the current drag and
                // drop operation, this View doesn't receive events again until
                // ACTION_DRAG_ENDED is sent.
                false
            }
        }
        DragEvent.ACTION_DRAG_ENTERED -> {
            // Apply a green tint to the View.
            (v as? ImageView)?.setColorFilter(Color.GREEN)

            // Invalidate the view to force a redraw in the new tint.
            v.invalidate()

            // Return true. The value is ignored.
            true
        }

        DragEvent.ACTION_DRAG_LOCATION ->
            // Ignore the event.
            true
        DragEvent.ACTION_DRAG_EXITED -> {
            // Reset the color tint to blue.
            (v as? ImageView)?.setColorFilter(Color.BLUE)

            // Invalidate the view to force a redraw in the new tint.
            v.invalidate()

            // Return true. The value is ignored.
            true
        }
        DragEvent.ACTION_DROP -> {
            // Get the item containing the dragged data.
            val item: ClipData.Item = e.clipData.getItemAt(0)

            // Get the text data from the item.
            val dragData = item.text

            // Display a message containing the dragged data.
            Toast.makeText(this, "Dragged data is $dragData", Toast.LENGTH_LONG).show()

            // Turn off color tints.
            (v as? ImageView)?.clearColorFilter()

            // Invalidate the view to force a redraw.
            v.invalidate()

            // Return true. DragEvent.getResult() returns true.
            true
        }

        DragEvent.ACTION_DRAG_ENDED -> {
            // Turn off color tinting.
            (v as? ImageView)?.clearColorFilter()

            // Invalidate the view to force a redraw.
            v.invalidate()

            // Do a getResult() and display what happens.
            when(e.result) {
                true ->
                    Toast.makeText(this, "The drop was handled.", Toast.LENGTH_LONG)
                else ->
                    Toast.makeText(this, "The drop didn't work.", Toast.LENGTH_LONG)
            }.show()

            // Return true. The value is ignored.
            true
        }
        else -> {
            // An unknown action type is received.
            Log.e("DragDrop Example", "Unknown action type received by View.OnDragListener.")
            false
        }
    }
}

Java

View imageView = new ImageView(this);

// Set the drag event listener for the View.
imageView.setOnDragListener( (v, e) -> {

    // Handle each of the expected events.
    switch(e.getAction()) {

        case DragEvent.ACTION_DRAG_STARTED:

            // Determine whether this View can accept the dragged data.
            if (e.getClipDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) {

                // As an example, apply a blue color tint to the View to
                // indicate that it can accept data.
                ((ImageView)v).setColorFilter(Color.BLUE);

                // Invalidate the view to force a redraw in the new tint.
                v.invalidate();

                // Return true to indicate that the View can accept the dragged
                // data.
                return true;

            }

            // Return false to indicate that, during the current drag and drop
            // operation, this View doesn't receive events again until
            // ACTION_DRAG_ENDED is sent.
            return false;

        case DragEvent.ACTION_DRAG_ENTERED:

            // Apply a green tint to the View.
            ((ImageView)v).setColorFilter(Color.GREEN);

            // Invalidate the view to force a redraw in the new tint.
            v.invalidate();

            // Return true. The value is ignored.
            return true;

        case DragEvent.ACTION_DRAG_LOCATION:

            // Ignore the event.
            return true;

        case DragEvent.ACTION_DRAG_EXITED:

            // Reset the color tint to blue.
            ((ImageView)v).setColorFilter(Color.BLUE);

            // Invalidate the view to force a redraw in the new tint.
            v.invalidate();

            // Return true. The value is ignored.
            return true;

        case DragEvent.ACTION_DROP:

            // Get the item containing the dragged data.
            ClipData.Item item = e.getClipData().getItemAt(0);

            // Get the text data from the item.
            CharSequence dragData = item.getText();

            // Display a message containing the dragged data.
            Toast.makeText(this, "Dragged data is " + dragData, Toast.LENGTH_LONG).show();

            // Turn off color tints.
            ((ImageView)v).clearColorFilter();

            // Invalidate the view to force a redraw.
            v.invalidate();

            // Return true. DragEvent.getResult() returns true.
            return true;

        case DragEvent.ACTION_DRAG_ENDED:

            // Turn off color tinting.
            ((ImageView)v).clearColorFilter();

            // Invalidate the view to force a redraw.
            v.invalidate();

            // Do a getResult() and displays what happens.
            if (e.getResult()) {
                Toast.makeText(this, "The drop was handled.", Toast.LENGTH_LONG).show();
            } else {
                Toast.makeText(this, "The drop didn't work.", Toast.LENGTH_LONG).show();
            }

            // Return true. The value is ignored.
            return true;

        // An unknown action type is received.
        default:
            Log.e("DragDrop Example","Unknown action type received by View.OnDragListener.");
            break;
    }

    return false;

});

Przeciąganie i upuszczanie w trybie wielu okien

Urządzenia z Androidem 7.0 (poziom interfejsu API 24) lub nowszym obsługują tryb wielu okien, który umożliwia użytkownikom przenoszenie danych między aplikacjami poprzez przeciąganie i upuszczanie danych. Więcej informacji znajdziesz w artykule na temat obsługi wielu okien.

Dane dostarcza aplikacja źródłowa, w której rozpoczyna się operacja przeciągania i upuszczania. Dane otrzymuje aplikacja docelowa, na którą kończy się operacja przeciągania i upuszczania.

Po rozpoczęciu operacji przeciągania i upuszczania aplikacja źródłowa musi ustawić flagę DRAG_FLAG_GLOBAL, aby wskazać, że użytkownik może przeciągać dane do innej aplikacji.

Dane są przesyłane poza granice aplikacji, dlatego aplikacje mają do nich dostęp za pomocą identyfikatora URI treści. Wymagania:

  • Aplikacja źródłowa musi ustawić jedną lub obie flagi DRAG_FLAG_GLOBAL_URI_READ i DRAG_FLAG_GLOBAL_URI_WRITE w zależności od uprawnień do odczytu lub zapisu danych, które aplikacja źródłowa chce przyznać aplikacji docelowej.
  • Aplikacja docelowa musi wywołać metodę requestDragAndDropPermissions() bezpośrednio przed obsługą danych przeciąganych przez użytkownika do aplikacji. Jeśli aplikacja docelowa nie potrzebuje już dostępu do funkcji przeciągania i upuszczania, może wywołać metodę release() na obiekcie zwróconym z metody requestDragAndDropPermissions(). W przeciwnym razie uprawnienia zostaną anulowane po zniszczeniu aktywności, która zawiera. Jeśli Twoja implementacja obejmuje uruchomienie nowego działania w celu przetworzenia utraconych identyfikatorów URI, musisz przyznać nowemu działaniu te same uprawnienia. Musisz ustawić dane dotyczące klipu i flagę:

    Kotlin

    intent.setClipData(clipData)
    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
    

    Java

    intent.setClipData(clipData);
    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
    

Poniższe fragmenty kodu pokazują, jak zwolnić dostęp tylko do odczytu, aby przeciągać i upuszczać dane bezpośrednio po wykonaniu operacji przeciągania i upuszczania. Dokładniejszy przykład znajdziesz w przykładzie DrragAndDrop na GitHubie.

Źródła aktywności związanej z przeciąganiem i upuszczaniem

Kotlin

// Drag a file stored in an images/ directory in internal storage.
val internalImagesDir = File(context.filesDir, "images")
val imageFile = File(internalImagesDir, imageFilename)
val uri = FileProvider.getUriForFile(context, contentAuthority, imageFile)

val listener = OnDragStartListener@{ view: View, _: DragStartHelper ->
    val clipData = ClipData(ClipDescription("Image Description",
                                            arrayOf("image/*")),
                            ClipData.Item(uri))
    // Must include DRAG_FLAG_GLOBAL to permit dragging data between apps.
    // This example provides read-only access to the data.
    val flags = View.DRAG_FLAG_GLOBAL or View.DRAG_FLAG_GLOBAL_URI_READ
    return@OnDragStartListener view.startDragAndDrop(clipData,
                                                     View.DragShadowBuilder(view),
                                                     null,
                                                     flags)
}

// Container where the image originally appears in the source app.
val srcImageView = findViewById<ImageView>(R.id.imageView)

// Detect and start the drag event.
DragStartHelper(srcImageView, listener).apply {
    attach()
}

Java

// Drag a file stored in an images/ directory in internal storage.
File internalImagesDir = new File(context.getFilesDir(), "images");
File imageFile = new File(internalImagesDir, imageFilename);
final Uri uri = FileProvider.getUriForFile(context, contentAuthority, imageFile);

// Container where the image originally appears in the source app.
ImageView srcImageView = findViewById(R.id.imageView);

// Enable the view to detect and start the drag event.
new DragStartHelper(srcImageView, (view, helper) -> {
    ClipData clipData = new ClipData(new ClipDescription("Image Description",
                                                          new String[] {"image/*"}),
                                     new ClipData.Item(uri));
    // Must include DRAG_FLAG_GLOBAL to permit dragging data between apps.
    // This example provides read-only access to the data.
    int flags = View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ;
    return view.startDragAndDrop(clipData,
                                 new View.DragShadowBuilder(view),
                                 null,
                                 flags);
}).attach();

Docelowa aktywność związana z przeciąganiem i upuszczaniem

Kotlin

// Container where the image is to be dropped in the target app.
val targetImageView = findViewById<ImageView>(R.id.imageView)

targetImageView.setOnDragListener { view, event ->

    when (event.action) {

        ACTION_DROP -> {
            val imageItem: ClipData.Item = event.clipData.getItemAt(0)
            val uri = imageItem.uri

            // Request permission to access the image data being dragged into
            // the target activity's ImageView element.
            val dropPermissions = requestDragAndDropPermissions(event)
            (view as ImageView).setImageURI(uri)

            // Release the permission immediately afterward because it's no
            // longer needed.
            dropPermissions.release()
            return@setOnDragListener true
        }

        // Implement logic for other DragEvent cases here.

        // An unknown action type is received.
        else -> {
            Log.e("DragDrop Example", "Unknown action type received by View.OnDragListener.")
            return@setOnDragListener false
        }

    }
}

Java

// Container where the image is to be dropped in the target app.
ImageView targetImageView = findViewById(R.id.imageView);

targetImageView.setOnDragListener( (view, event) -> {

    switch (event.getAction()) {

        case ACTION_DROP:
            ClipData.Item imageItem = event.getClipData().getItemAt(0);
            Uri uri = imageItem.getUri();

            // Request permission to access the image data being dragged into
            // the target activity's ImageView element.
            DragAndDropPermissions dropPermissions =
                requestDragAndDropPermissions(event);

            ((ImageView)view).setImageURI(uri);

            // Release the permission immediately afterward because it's no
            // longer needed.
            dropPermissions.release();

            return true;

        // Implement logic for other DragEvent cases here.

        // An unknown action type was received.
        default:
            Log.e("DragDrop Example","Unknown action type received by View.OnDragListener.");
            break;
    }

    return false;
});

DropHelper upraszczający przeciąganie i upuszczanie

Klasa DropHelper upraszcza wdrażanie funkcji przeciągania i upuszczania. Należy do biblioteki Jetpack DragAndDrop i zapewnia zgodność wsteczną nawet do poziomu interfejsu API 24.DropHelper

Za pomocą funkcji DropHelper możesz określić wartości docelowe, dostosować ich wyróżnianie i określić sposób postępowania z usuniętymi danymi.

Określ docelowe wartości

DropHelper.configureView() to statyczna, przeciążona metoda, która umożliwia określanie wartości docelowych. Jej parametry to między innymi:

Aby na przykład utworzyć obszar docelowy, który akceptuje obrazy, użyj jednego z tych wywołań metod:

Kotlin

configureView(
    myActivity,
    targetView,
    arrayOf("image/*"),
    options,
    onReceiveContentListener)

// or

configureView(
    myActivity,
    targetView,
    arrayOf("image/*"),
    onReceiveContentListener)

Java

DropHelper.configureView(
    myActivity,
    targetView,
    new String[] {"image/*"},
    options,
    onReceiveContentlistener);

// or

DropHelper.configureView(
    myActivity,
    targetView,
    new String[] {"image/*"},
    onReceiveContentlistener);

Drugie wywołanie pomija opcje konfiguracji miejsca docelowego. W takim przypadku kolor zaznaczenia miejsca docelowego jest ustawiony na dodatkowy (lub uzupełniający) kolor motywu, promień narożnika wyróżnienia to 16 dp, a lista komponentów EditText jest pusta. Szczegółowe informacje znajdziesz w sekcji poniżej.

Skonfiguruj docelowe obszary

Klasa wewnętrzna DropHelper.Options pozwala konfigurować wartości docelowe. Podaj instancję klasy dla metody DropHelper.configureView(Activity, View, String[], Options, OnReceiveContentListener). Więcej informacji znajdziesz w poprzedniej sekcji.

Dostosuj wyróżnianie obszaru docelowego

DropHelper konfiguruje docelowe elementy w taki sposób, aby wyświetlały podświetlenie, gdy użytkownicy przeciągają treści nad nimi. DropHelper zapewnia styl domyślny, a DropHelper.Options pozwala ustawić kolor zaznaczenia i określić promień narożnika prostokąta zaznaczenia.

Użyj klasy DropHelper.Options.Builder, aby utworzyć instancję DropHelper.Options i ustawić opcje konfiguracji, jak w tym przykładzie:

Kotlin

val options: DropHelper.Options = DropHelper.Options.Builder()
                                      .setHighlightColor(getColor(R.color.purple_300))
                                      .setHighlightCornerRadiusPx(resources.getDimensionPixelSize(R.dimen.drop_target_corner_radius))
                                      .build()

Java

DropHelper.Options options = new DropHelper.Options.Builder()
                                     .setHighlightColor(getColor(R.color.purple_300))
                                     .setHighlightCornerRadiusPx(getResources().getDimensionPixelSize(R.dimen.drop_target_corner_radius))
                                     .build();

Obsługa komponentów EditText w obszarach docelowych

DropHelper kontroluje też obszar docelowy, gdy zawiera on pola tekstowe z możliwością edytowania.

Miejsca docelowe mogą być pojedynczym widokiem lub hierarchią widoków. Jeśli hierarchia widoku miejsca docelowego zawiera co najmniej 1 komponent EditText, podaj listę tych komponentów, aby DropHelper.Options.Builder.addInnerEditTexts(EditText...) zadbać o prawidłowe wyróżnianie miejsca docelowego i prawidłową obsługę danych tekstowych.

Funkcja DropHelper uniemożliwia komponentom EditText w hierarchii widoku miejsca docelowego rozproszenie uwagi z widoku zawierającego ten element podczas interakcji z przeciąganiem.

Poza tym, jeśli przeciąganie i upuszczanie ClipData obejmuje dane tekstowe i identyfikator URI, DropHelper wybiera w obszarze docelowym jeden z komponentów EditText do obsługi danych tekstowych. Wybór zależy od tej kolejności:

  1. EditText, na którym spada ClipData.
  2. Element EditText zawierający kursor tekstowy (znak wstawienia).
  3. Pierwszy typ EditText podany w wywołaniu DropHelper.Options.Builder.addInnerEditTexts(EditText...).

Aby ustawić EditText jako domyślny moduł obsługi danych tekstowych, przekaż EditText jako pierwszy argument wywołania DropHelper.Options.Builder.addInnerEditTexts(EditText...). Jeśli na przykład miejsce docelowe obsługuje obrazy, ale zawiera pola tekstowe T1, T2 i T3, które można edytować, ustaw T2 jako wartość domyślną w ten sposób:

Kotlin

val options: DropHelper.Options = DropHelper.Options.Builder()
                                      .addInnerEditTexts(T2, T1, T3)
                                      .build()

Java

DropHelper.Options options = new DropHelper.Options.Builder()
                                     .addInnerEditTexts(T2, T1, T3)
                                     .build();

Obsługuj dane w docelowych wartościach

Metoda DropHelper.configureView() akceptuje utworzony przez Ciebie element OnReceiveContentListener do obsługi przeciągania i upuszczania elementu ClipData. Dane przeciągania i upuszczania są przekazywane do odbiornika w obiekcie ContentInfoCompat. Obiekt zawiera dane tekstowe. Multimedia, takie jak obrazy, są reprezentowane przez identyfikatory URI.

Element OnReceiveContentListener obsługuje też dane przekazywane do miejsca docelowego w wyniku działań użytkowników innych niż przeciąganie i upuszczanie (np. kopiowanie i wklejanie), gdy DropHelper.configureView() służy do konfigurowania tych widoków:

  • Wszystkie widoki, jeśli użytkownik korzysta z Androida 12 lub nowszego.
  • AppCompatEditText, jeśli użytkownik ma Androida w wersji starszej do 7.0.

Typy MIME, uprawnienia i weryfikacja treści

Sprawdzanie typu MIME przez DropHelper odbywa się w ramach metody przeciągania i upuszczania ClipDescription, którą tworzy aplikacja dostarczająca dane. Sprawdź ClipDescription, aby mieć pewność, że typy MIME są ustawione prawidłowo.

DropHelper prosi o wszystkie uprawnienia dostępu do identyfikatorów URI treści zawartych w metodzie ClipData przeciągania i upuszczania. Więcej informacji: DragAndDropPermissions. Uprawnienia pozwalają rozpoznać identyfikatory URI treści podczas przetwarzania danych metodą przeciągania i upuszczania.

DropHelper nie weryfikuje danych zwróconych przez dostawców treści podczas rozpoznawania identyfikatorów URI w usuniętych danych. Poszukaj wartości null i sprawdź poprawność wykrytych danych.