Wykrywanie gestów rzutowych na układzie siatki


1105

Chcę uzyskać flingwykrywanie gestów w mojej aplikacji na Androida.

Mam to, GridLayoutco zawiera 9 ImageViews. Źródło można znaleźć tutaj: Układ siatki Romaina Guysa .

Plik, który biorę pochodzi od Romaina Guy'a aplikacji Photostream i został tylko nieznacznie dostosowany.

W przypadku prostego kliknięcia muszę tylko ustawić onClickListenerdla każdego ImageViewdodawanego jako główny, activityktóry implementuje View.OnClickListener. Nieskończenie bardziej skomplikowane wydaje się wdrożenie czegoś, co rozpoznaje fling. Zakładam, że dzieje się tak, ponieważ może się rozciągaćviews ?

  • Jeśli moja aktywność się implementuje OnGestureListener, nie wiem, jak ustawić to jako detektor gestów dla dodawanych widoków Gridlub Image.

    public class SelectFilterActivity extends Activity implements
       View.OnClickListener, OnGestureListener { ...
  • Jeśli moja aktywność się implementuje, OnTouchListenerto nie mam onFlingmetody override(ma dwa zdarzenia jako parametry pozwalające mi ustalić, czy rzutowanie było warte uwagi).

    public class SelectFilterActivity extends Activity implements
        View.OnClickListener, OnTouchListener { ...
  • Jeśli zrobię niestandardowy View, GestureImageViewto rozszerza ImageViewnie wiem, jak powiedzieć aktywności, żefling wystąpił z widoku. W każdym razie próbowałem tego i metody nie były wywoływane, kiedy dotknąłem ekranu.

Naprawdę potrzebuję konkretnego przykładu tego działania w różnych widokach. Co, kiedy i jak mam to załączyć listener? Muszę też być w stanie wykryć pojedyncze kliknięcia.

// Gesture detection
mGestureDetector = new GestureDetector(this, new GestureDetector.SimpleOnGestureListener() {

    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        int dx = (int) (e2.getX() - e1.getX());
        // don't accept the fling if it's too short
        // as it may conflict with a button push
        if (Math.abs(dx) > MAJOR_MOVE && Math.abs(velocityX) > Math.absvelocityY)) {
            if (velocityX > 0) {
                moveRight();
            } else {
                moveLeft();
            }
            return true;
        } else {
            return false;
        }
    }
});

Czy można umieścić przezroczysty widok na górze ekranu, aby uchwycić rzuty?

Jeśli zdecyduję się nie wyświetlać inflatepodrzędnych widoków obrazów z XML, czy mogę przekazać GestureDetectorparametr konstruktora do nowej podklasy ImageView, którą tworzę?

Jest to bardzo prosta czynność, dla której próbuję uruchomić flingwykrywanie: SelectFilterActivity (na podstawie strumienia zdjęć) .

Patrzyłem na te źródła:

Do tej pory nic mi nie działało i liczyłem na pewne wskazówki.


Jak rozwiązać ten problem? Odpowiedz stackoverflow.com/questions/60464912/…
Bishwash

Odpowiedzi:


818

Dzięki Code Shogun , którego kod dostosowałem do mojej sytuacji.

Pozwól, aby Twoje działanie było realizowane OnClickListenerjak zwykle:

public class SelectFilterActivity extends Activity implements OnClickListener {

  private static final int SWIPE_MIN_DISTANCE = 120;
  private static final int SWIPE_MAX_OFF_PATH = 250;
  private static final int SWIPE_THRESHOLD_VELOCITY = 200;
  private GestureDetector gestureDetector;
  View.OnTouchListener gestureListener;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    /* ... */

    // Gesture detection
    gestureDetector = new GestureDetector(this, new MyGestureDetector());
    gestureListener = new View.OnTouchListener() {
      public boolean onTouch(View v, MotionEvent event) {
        return gestureDetector.onTouchEvent(event);
      }
    };

  }

  class MyGestureDetector extends SimpleOnGestureListener {
    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
      try {
        if (Math.abs(e1.getY() - e2.getY()) > SWIPE_MAX_OFF_PATH)
          return false;
        // right to left swipe
        if(e1.getX() - e2.getX() > SWIPE_MIN_DISTANCE && Math.abs(velocityX) > SWIPE_THRESHOLD_VELOCITY) {
          Toast.makeText(SelectFilterActivity.this, "Left Swipe", Toast.LENGTH_SHORT).show();
        } else if (e2.getX() - e1.getX() > SWIPE_MIN_DISTANCE && Math.abs(velocityX) > SWIPE_THRESHOLD_VELOCITY) {
          Toast.makeText(SelectFilterActivity.this, "Right Swipe", Toast.LENGTH_SHORT).show();
        }
      } catch (Exception e) {
         // nothing
      }
      return false;
    }

    @Override
    public boolean onDown(MotionEvent e) {
      return true;
    }
  }
}

Dołącz detektor gestów do wszystkich widoków dodawanych do głównego układu;

// Do this for each view added to the grid
imageView.setOnClickListener(SelectFilterActivity.this); 
imageView.setOnTouchListener(gestureListener);

Z podziwem obserwuj, jak uderzają twoje przesłonięte metody, zarówno onClick(View v)aktywność, jak i onFlingdetektor gestów.

public void onClick(View v) {
  Filter f = (Filter) v.getTag();
  FilterFullscreenActivity.show(this, input, f);
}

Taniec po „rzucaniu” jest opcjonalny, ale zachęcany.


109
Dziękuję za ten kod! To było bardzo pomocne. Jednak wpadłem na jeden bardzo frustrujący haczyk podczas próby uruchomienia gestów. W mojej SimpleOnGestureListener muszę zastąpić opcję onDown, aby zarejestrować dowolny gest. Może po prostu zwrócić wartość true, ale muszę ją zdefiniować. PS: Nie wiem, czy to moja wersja interfejsu API, czy sprzęt, ale używam 1.5 na HTC Droid Eris.
Cdsboy,

Wypróbowałem Twój kod i nie ma znaczenia, czy przeciągam palcem, czy klikam (myszką, ponieważ pracuję w emulatorze), zawsze otrzymuję Toast, który zdefiniowałem w metodzie onClick, więc emulator wykrywa tylko kliknięcia, bez przeciągnięć. Dlaczego tak jest
Łomża

Wypróbowałem ten kod i nie działał. nadal nie byłem w stanie przewijać, gdy zastosuję odbiornik onClick do jednego z widoków podrzędnych w widoku mojej galerii
Jonathan

Iomza: próbowałeś wstawiać instrukcje break i przechodzić przez kod?
IgorGanapolsky

Wyrazy uznania za korzystanie z wewnętrznej klasy! Bardzo czyste podejście.
IgorGanapolsky

211

Jedna z powyższych odpowiedzi wspomina obsługę różnych gęstości pikseli, ale sugeruje ręczne obliczenie parametrów przeciągnięcia. Warto zauważyć, że faktycznie można uzyskać skalowane, rozsądne wartości z systemu za pomocą ViewConfigurationklasy:

final ViewConfiguration vc = ViewConfiguration.get(getContext());
final int swipeMinDistance = vc.getScaledPagingTouchSlop();
final int swipeThresholdVelocity = vc.getScaledMinimumFlingVelocity();
final int swipeMaxOffPath = vc.getScaledTouchSlop();
// (there is also vc.getScaledMaximumFlingVelocity() one could check against)

Zauważyłem, że użycie tych wartości powoduje, że „odczucie” rzutu jest bardziej spójne między aplikacją a resztą systemu.


11
Używam swipeMinDistance = vc.getScaledPagingTouchSlop()i swipeMaxOffPath = vc.getScaledTouchSlop().
Thomas Ahle,

8
getScaledTouchSlopniezręcznie daje mi bardzo niewielki wynik kompensacji. Na przykład tylko 24 piksele na ekranie o wysokości 540, bardzo trudno jest trzymać go w zasięgu palcem. : S
WonderCsabo

148

Robię to trochę inaczej i napisałem dodatkową klasę detektora, która implementuje View.onTouchListener

onCreatepo prostu dodaj go do najniższego układu takiego jak ten:

ActivitySwipeDetector activitySwipeDetector = new ActivitySwipeDetector(this);
lowestLayout = (RelativeLayout)this.findViewById(R.id.lowestLayout);
lowestLayout.setOnTouchListener(activitySwipeDetector);

gdzie id.lowestLayout to id.xxx dla widoku najniższego w hierarchii układu, a najniższy układ jest zadeklarowany jako układ względny

A potem jest faktyczna klasa detektora przesunięcia aktywności:

public class ActivitySwipeDetector implements View.OnTouchListener {

static final String logTag = "ActivitySwipeDetector";
private Activity activity;
static final int MIN_DISTANCE = 100;
private float downX, downY, upX, upY;

public ActivitySwipeDetector(Activity activity){
    this.activity = activity;
}

public void onRightSwipe(){
    Log.i(logTag, "RightToLeftSwipe!");
    activity.doSomething();
}

public void onLeftSwipe(){
    Log.i(logTag, "LeftToRightSwipe!");
    activity.doSomething();
}

public void onDownSwipe(){
    Log.i(logTag, "onTopToBottomSwipe!");
    activity.doSomething();
}

public void onUpSwipe(){
    Log.i(logTag, "onBottomToTopSwipe!");
    activity.doSomething();
}

public boolean onTouch(View v, MotionEvent event) {
    switch(event.getAction()){
        case MotionEvent.ACTION_DOWN: {
            downX = event.getX();
            downY = event.getY();
            return true;
        }
        case MotionEvent.ACTION_UP: {
            upX = event.getX();
            upY = event.getY();

            float deltaX = downX - upX;
            float deltaY = downY - upY;

       // swipe horizontal?
        if(Math.abs(deltaX) > Math.abs(deltaY))
        {
            if(Math.abs(deltaX) > MIN_DISTANCE){
                // left or right
                if(deltaX > 0) { this.onRightSwipe(); return true; }
                if(deltaX < 0) { this.onLeftSwipe(); return true; }
            }
            else {
                    Log.i(logTag, "Horizontal Swipe was only " + Math.abs(deltaX) + " long, need at least " + MIN_DISTANCE);
                    return false; // We don't consume the event
            }
        }
        // swipe vertical?
        else 
        {
            if(Math.abs(deltaY) > MIN_DISTANCE){
                // top or down
                if(deltaY < 0) { this.onDownSwipe(); return true; }
                if(deltaY > 0) { this.onUpSwipe(); return true; }
            }
            else {
                    Log.i(logTag, "Vertical Swipe was only " + Math.abs(deltaX) + " long, need at least " + MIN_DISTANCE);
                    return false; // We don't consume the event
            }
        }

            return true;
        }
    }
    return false;
}

}

Działa naprawdę dobrze dla mnie!


1
To znacznie ułatwiło mi zastosowanie funkcji gestów i wymagało okablowania „mniej”: D Dzięki @Thomas
nemesisfixx

5
Wygląda to na schludną klasę użyteczności - ale myślę, że twoimi czterema metodami ... swipe () powinny być interfejsy
Someone Somewhere

2
zwroty te nie powinny tam być (wiersz „nie konsumujemy zdarzenia”), prawda? Wyłącza funkcję przewijania w pionie.
Marek Sebera,

5
w szczególności metoda onTouch (). po pierwsze, jeśli delta X nie jest wystarczająco duża, wraca bez sprawdzania delty Y. w rezultacie nigdy nie wykrywa przeciągnięć w lewo-prawo. po drugie, nie powinno również zwracać wartości true, jeśli spada po znalezieniu braku przeciągnięcia. po trzecie, nie powinno zwracać wartości true po działaniu. Zapobiega to działaniu dowolnego innego detektora, takiego jak onClick.
Jeffrey Blattman,

1
@Piotr to nie problem, o ile obiekt zawierający odwołanie ma taki sam zakres jak samo działanie. problem występuje, gdy trzymasz odwołanie do działania w miejscu, które ma większy zasięg niż działanie ... na przykład z elementu statycznego.
Jeffrey Blattman,

94

Lekko zmodyfikowałem i naprawiłem rozwiązanie Thomasa Fankhausera

Cały system składa się z dwóch plików: SwipeInterface i ActivitySwipeDetector


SwipeInterface.java

import android.view.View;

public interface SwipeInterface {

    public void bottom2top(View v);

    public void left2right(View v);

    public void right2left(View v);

    public void top2bottom(View v);

}

Detektor

import android.util.Log;
import android.view.MotionEvent;
import android.view.View;

public class ActivitySwipeDetector implements View.OnTouchListener {

    static final String logTag = "ActivitySwipeDetector";
    private SwipeInterface activity;
    static final int MIN_DISTANCE = 100;
    private float downX, downY, upX, upY;

    public ActivitySwipeDetector(SwipeInterface activity){
        this.activity = activity;
    }

    public void onRightToLeftSwipe(View v){
        Log.i(logTag, "RightToLeftSwipe!");
        activity.right2left(v);
    }

    public void onLeftToRightSwipe(View v){
        Log.i(logTag, "LeftToRightSwipe!");
        activity.left2right(v);
    }

    public void onTopToBottomSwipe(View v){
        Log.i(logTag, "onTopToBottomSwipe!");
        activity.top2bottom(v);
    }

    public void onBottomToTopSwipe(View v){
        Log.i(logTag, "onBottomToTopSwipe!");
        activity.bottom2top(v);
    }

    public boolean onTouch(View v, MotionEvent event) {
        switch(event.getAction()){
        case MotionEvent.ACTION_DOWN: {
            downX = event.getX();
            downY = event.getY();
            return true;
        }
        case MotionEvent.ACTION_UP: {
            upX = event.getX();
            upY = event.getY();

            float deltaX = downX - upX;
            float deltaY = downY - upY;

            // swipe horizontal?
            if(Math.abs(deltaX) > MIN_DISTANCE){
                // left or right
                if(deltaX < 0) { this.onLeftToRightSwipe(v); return true; }
                if(deltaX > 0) { this.onRightToLeftSwipe(v); return true; }
            }
            else {
                Log.i(logTag, "Swipe was only " + Math.abs(deltaX) + " long, need at least " + MIN_DISTANCE);
            }

            // swipe vertical?
            if(Math.abs(deltaY) > MIN_DISTANCE){
                // top or down
                if(deltaY < 0) { this.onTopToBottomSwipe(v); return true; }
                if(deltaY > 0) { this.onBottomToTopSwipe(v); return true; }
            }
            else {
                Log.i(logTag, "Swipe was only " + Math.abs(deltaX) + " long, need at least " + MIN_DISTANCE);
                v.performClick();
            }
        }
        }
        return false;
    }

}

jest używany w następujący sposób:

ActivitySwipeDetector swipe = new ActivitySwipeDetector(this);
LinearLayout swipe_layout = (LinearLayout) findViewById(R.id.swipe_layout);
swipe_layout.setOnTouchListener(swipe);

I podczas implementacji Activitymusisz zaimplementować metody z SwipeInterface , i możesz dowiedzieć się, na którym wywołano zdarzenie View Swipe .

@Override
public void left2right(View v) {
    switch(v.getId()){
        case R.id.swipe_layout:
            // do your stuff here
        break;
    }       
}

Lekko zmodyfikowałem go ponownie, zobacz v.performClick();, który jest używany do nieużywania zdarzenia do OnClickListener, jeśli jest ustawiony w tym samym widoku
Marek Sebera

Cześć, jestem początkującym, więc to pytanie może być naprawdę oczywiste lub trywialne, ale proszę o odpowiedź. Część, w której napisałeś, jest używana jako: ActivitySwipeDetector swipe = new ActivitySwipeDetector (this); To oświadczenie będzie częścią MainActivity, prawda? Wtedy „to” będzie działaniem MainActivity. Konstruktor pobiera instancję SwipeInterface. Proszę mi pomóc tutaj. Wielkie dzięki.
Chocolava,

@Chocolava utwórz nowe pytanie, komentarz nie jest dobrym miejscem do zadawania takich pytań.
Marek Sebera

@MarekSebera to nie działa z ScrollView i ListView? jak sobie z nimi poradzić?
Duc Tran

@ ponownie Silentbang, nie jest to miejsce do zadawania takich pytań. proszę utworzyć nowy wątek pytania.
Marek Sebera

65

Powyższy kod detektora gestów przesuwania jest bardzo przydatny! Możesz jednak chcieć sprawić, by gęstość tego rozwiązania była agnostyczna, używając następujących wartości względnych (REL_SWIPE)zamiast wartości bezwzględnych(SWIPE_)

DisplayMetrics dm = getResources().getDisplayMetrics();

int REL_SWIPE_MIN_DISTANCE = (int)(SWIPE_MIN_DISTANCE * dm.densityDpi / 160.0f);
int REL_SWIPE_MAX_OFF_PATH = (int)(SWIPE_MAX_OFF_PATH * dm.densityDpi / 160.0f);
int REL_SWIPE_THRESHOLD_VELOCITY = (int)(SWIPE_THRESHOLD_VELOCITY * dm.densityDpi / 160.0f);

8
+1 za podniesienie tego. Należy zauważyć, że w API 4. wprowadzono DensityMetrics.densityDpi. Aby zapewnić zgodność wsteczną z API 1, należy zamiast tego użyć DensityMetrics.density. Spowoduje to zmianę obliczeń na SWIPE_MIN_DISTANCE * dm.density.
Thane Anthem

Skąd masz numer 160.0f?
IgorGanapolsky

developer.android.com/guide/practices/screens_support.html Piksel niezależny od gęstości (dp) Konwersja jednostek dp na piksele ekranowe jest prosta: px = dp * (dpi / 160)
paiego

Szukałem tego wszystkiego. Żaden przykład onFling () w Internecie nie ma tego, co doprowadzi do słabego UX. Dzięki!
Sandy,

160,0f to 160 DPI, które jest standardową gęstością, na której opiera się DP (piksele niezależne od gęstości). public static final int DENSITY_MEDIUM Dodano w API poziom 4 Standardowe skwantowane DPI dla ekranów o średniej gęstości. Stała wartość: 160 (0x000000a0)
paiego

35

Moja wersja rozwiązania zaproponowana przez Thomasa Fankhausera i Marka Seberę (nie obsługuje przeciągnięć pionowych):

SwipeInterface.java

import android.view.View;

public interface SwipeInterface {

    public void onLeftToRight(View v);

    public void onRightToLeft(View v);
}

ActivitySwipeDetector.java

import android.content.Context;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;

public class ActivitySwipeDetector implements View.OnTouchListener {

    static final String logTag = "ActivitySwipeDetector";
    private SwipeInterface activity;
    private float downX, downY;
    private long timeDown;
    private final float MIN_DISTANCE;
    private final int VELOCITY;
    private final float MAX_OFF_PATH;

    public ActivitySwipeDetector(Context context, SwipeInterface activity){
        this.activity = activity;
        final ViewConfiguration vc = ViewConfiguration.get(context);
        DisplayMetrics dm = context.getResources().getDisplayMetrics();
        MIN_DISTANCE = vc.getScaledPagingTouchSlop() * dm.density;
        VELOCITY = vc.getScaledMinimumFlingVelocity();
        MAX_OFF_PATH = MIN_DISTANCE * 2;            
    }

    public void onRightToLeftSwipe(View v){
        Log.i(logTag, "RightToLeftSwipe!");
        activity.onRightToLeft(v);
    }

    public void onLeftToRightSwipe(View v){
        Log.i(logTag, "LeftToRightSwipe!");
        activity.onLeftToRight(v);
    }

    public boolean onTouch(View v, MotionEvent event) {
        switch(event.getAction()){
        case MotionEvent.ACTION_DOWN: {
            Log.d("onTouch", "ACTION_DOWN");
            timeDown = System.currentTimeMillis();
            downX = event.getX();
            downY = event.getY();
            return true;
        }
        case MotionEvent.ACTION_UP: {
            Log.d("onTouch", "ACTION_UP");
            long timeUp = System.currentTimeMillis();
            float upX = event.getX();
            float upY = event.getY();

            float deltaX = downX - upX;
            float absDeltaX = Math.abs(deltaX); 
            float deltaY = downY - upY;
            float absDeltaY = Math.abs(deltaY);

            long time = timeUp - timeDown;

            if (absDeltaY > MAX_OFF_PATH) {
                Log.i(logTag, String.format("absDeltaY=%.2f, MAX_OFF_PATH=%.2f", absDeltaY, MAX_OFF_PATH));
                return v.performClick();
            }

            final long M_SEC = 1000;
            if (absDeltaX > MIN_DISTANCE && absDeltaX > time * VELOCITY / M_SEC) {
                if(deltaX < 0) { this.onLeftToRightSwipe(v); return true; }
                if(deltaX > 0) { this.onRightToLeftSwipe(v); return true; }
            } else {
                Log.i(logTag, String.format("absDeltaX=%.2f, MIN_DISTANCE=%.2f, absDeltaX > MIN_DISTANCE=%b", absDeltaX, MIN_DISTANCE, (absDeltaX > MIN_DISTANCE)));
                Log.i(logTag, String.format("absDeltaX=%.2f, time=%d, VELOCITY=%d, time*VELOCITY/M_SEC=%d, absDeltaX > time * VELOCITY / M_SEC=%b", absDeltaX, time, VELOCITY, time * VELOCITY / M_SEC, (absDeltaX > time * VELOCITY / M_SEC)));
            }

        }
        }
        return false;
    }

}

czy ktoś może mi powiedzieć, jak zadzwonić na zajęcia. ActivitySwipeDetector swipe = new ActivitySwipeDetector (this); oczywiście daje błąd, ponieważ nie ma takiego konstruktora. Czy powinienem podać ActivitySwipeDetector swipe = new ActivitySwipeDetector (this, null);
abdfahim

@AbdullahFahim ActivitySwipeDetector (this, YourActivity.this);
Anton Kashpor

25

To pytanie jest trochę stare i w lipcu 2011 r. Google wydało Pakiet zgodności, wersja 3), który obejmuje wersjęViewPager zgodną z systemem Android 1.6 w górę. W GestureListenerodpowiedzi na to pytanie wysłane nie czują się bardzo elegancko na Androida. Jeśli szukasz kodu używanego do przełączania zdjęć w Galerii Androida lub przełączania widoków w nowej aplikacji Play Market, to na pewno ViewPager.

Oto kilka linków, aby uzyskać więcej informacji:


Jednym z problemów związanych z ViewPager jest brak kontroli parametrów odległości i prędkości dla gestu rzutu.
almalkawi

ViewPager nie jest używany w galerii.
Anthony

18

Istnieje wbudowany interfejs, którego można używać bezpośrednio do wszystkich gestów:
Oto wyjaśnienie dla użytkownika na poziomie podstawowym: wprowadź opis zdjęcia tutaj Istnieją 2 rodzaje importu, zachowaj ostrożność przy wyborze, że oba są różne wprowadź opis zdjęcia tutaj wprowadź opis zdjęcia tutaj


1
A jakie są kolejne kroki? Jak ustawić tego odbiornika na określony widok? A jeśli ten widok jest częścią fragmentu?
Stan

16

W Internecie (i na tej stronie) jest kilka propozycji użycia ViewConfiguration. getScaledTouchSlop (), aby mieć wartość skalowaną dla urządzenia SWIPE_MIN_DISTANCE.

getScaledTouchSlop()jest przeznaczony do „ przewijania odległości „progu ”, a nie przesuwania. Odległość progu przewijania musi być mniejsza niż odległość progowa „przechodzenia między stronami”. Na przykład ta funkcja zwraca 12 pikseli na moim telefonie Samsung GS2, a przykłady podane na tej stronie mają około 100 pikseli.

Z API poziomu 8 (Android 2.2, Froyo), masz getScaledPagingTouchSlop(), przeznaczony do przesuwania strony. Na moim urządzeniu zwraca 24 (piksele). Więc jeśli jesteś na poziomie API <8, myślę, że „2 * getScaledTouchSlop()” powinno być „standardowym” progiem przeciągnięcia. Ale użytkownicy mojej aplikacji z małymi ekranami powiedzieli mi, że jest jej za mało ... Podobnie jak w mojej aplikacji, możesz przewijać w pionie i zmieniać stronę w poziomie. Przy proponowanej wartości czasami zmieniają stronę zamiast przewijania.


13

Również jako niewielkie ulepszenie.

Głównym powodem bloku try / catch jest to, że e1 może być zerowy dla początkowego ruchu. oprócz try / catch dołącz test na zero i return. podobne do następujących

if (e1 == null || e2 == null) return false;
try {
...
} catch (Exception e) {}
return false;

12

Jest tu wiele doskonałych informacji. Niestety, wiele z tego kodu przetwarzania rozproszonego jest rozrzuconych po różnych witrynach w różnych stanach ukończenia, chociaż można by pomyśleć, że jest to niezbędne w wielu aplikacjach.

Poświęciłem czas na stworzenie flingera, który sprawdza, czy spełnione są odpowiednie warunki. Dodałem detektor rzutowania stron, który dodaje więcej kontroli, aby upewnić się, że rzuty spełniają próg dla rzutów stron. Oba te nasłuchiwania umożliwiają łatwe ograniczenie rzutów do osi poziomej lub pionowej. Możesz zobaczyć, jak jest używany w widoku do przesuwania obrazów . Przyznaję, że ludzie tutaj przeprowadzili większość badań --- właśnie połączyłem to w użyteczną bibliotekę.

Ostatnie kilka dni to mój pierwszy kłopot z kodowaniem na Androidzie; oczekuj znacznie więcej .


Chcę wdrożyć gest przeciągnięcia dwoma palcami. Proszę pomóż mi!
Gaurav Arora,

12

Jest to połączona odpowiedź dwóch odpowiedzi u góry, jeśli ktoś chce działającej implementacji.

package com.yourapplication;

import android.content.Context;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;

public abstract class OnSwipeListener implements View.OnTouchListener {

    private final GestureDetector gestureDetector;

    public OnSwipeListener(Context context){
        gestureDetector = new GestureDetector(context, new OnSwipeGestureListener(context));
        gestureDetector.setIsLongpressEnabled(false);
    }

    @Override
    public boolean onTouch(View view, MotionEvent event) {
        return gestureDetector.onTouchEvent(event);
    }

    private final class OnSwipeGestureListener extends GestureDetector.SimpleOnGestureListener {

        private final int minSwipeDelta;
        private final int minSwipeVelocity;
        private final int maxSwipeVelocity;

        private OnSwipeGestureListener(Context context) {
            ViewConfiguration configuration = ViewConfiguration.get(context);
            // We think a swipe scrolls a full page.
            //minSwipeDelta = configuration.getScaledTouchSlop();
            minSwipeDelta = configuration.getScaledPagingTouchSlop();
            minSwipeVelocity = configuration.getScaledMinimumFlingVelocity();
            maxSwipeVelocity = configuration.getScaledMaximumFlingVelocity();
        }

        @Override
        public boolean onDown(MotionEvent event) {
            // Return true because we want system to report subsequent events to us.
            return true;
        }

        // NOTE: see http://stackoverflow.com/questions/937313/android-basic-gesture-detection
        @Override
        public boolean onFling(MotionEvent event1, MotionEvent event2, float velocityX,
                               float velocityY) {

            boolean result = false;
            try {
                float deltaX = event2.getX() - event1.getX();
                float deltaY = event2.getY() - event1.getY();
                float absVelocityX = Math.abs(velocityX);
                float absVelocityY = Math.abs(velocityY);
                float absDeltaX = Math.abs(deltaX);
                float absDeltaY = Math.abs(deltaY);
                if (absDeltaX > absDeltaY) {
                    if (absDeltaX > minSwipeDelta && absVelocityX > minSwipeVelocity
                            && absVelocityX < maxSwipeVelocity) {
                        if (deltaX < 0) {
                            onSwipeLeft();
                        } else {
                            onSwipeRight();
                        }
                    }
                    result = true;
                } else if (absDeltaY > minSwipeDelta && absVelocityY > minSwipeVelocity
                        && absVelocityY < maxSwipeVelocity) {
                    if (deltaY < 0) {
                        onSwipeTop();
                    } else {
                        onSwipeBottom();
                    }
                }
                result = true;
            } catch (Exception e) {
                e.printStackTrace();
            }
            return result;
        }
    }

    public void onSwipeLeft() {}

    public void onSwipeRight() {}

    public void onSwipeTop() {}

    public void onSwipeBottom() {}
}

Dziękujemy za naprawdę dobre wdrożenie. Dodatkowo chciałbym zaproponować, aby sprawdzić absDeltaY > minSwipeDelta, absVelocityY > minSwipeVelocity, absVelocityY < maxSwipeVelocitytylko w przypadku, gdy minSwipeDelta ! = getScaledTouchSlop , minSwipeVelocity ! = getScaledMinimumFlingVelocity , maxSwipeVelocity ! = getScaledMaximumFlingVelocity , Czyli sprawdzić tylko wtedy, gdy te tak zwane „default” (mam na myśli getScaledTouchSlop, getScaledMinimumFlingVelocity, getScaledMaximumFlingVelocity) wartości są skalowane lub zmieniane według Państwa własne życzenie.
Elia12345,

Chodzi o to, że zgodnie z kodem źródłowym wspomniane „domyślne” wartości są już sprawdzane przez GestureDetector, a OnFling jest wyzwalany tylko wtedy, gdy zostanie potwierdzony (przy okazji, wyzwalanie odbywa się tylko w przypadku ACTION_UP, nie ACTION_MOVElub ACTION_POINTER_UP, tj. Tylko jako wynik w pełni zrealizowanego gestu). (Nie sprawdziłem innych wersji API, więc komentarze są mile widziane).
Elia12345,

11

Możesz użyć biblioteki droidQuery do obsługi rzutów , kliknięć, długich kliknięć i zdarzeń niestandardowych. Implementacja jest oparta na mojej poprzedniej odpowiedzi poniżej, ale droidQuery zapewnia płynną , prostą składnię:

//global variables    private boolean isSwiping = false;
private SwipeDetector.Direction swipeDirection = null;
private View v;//must be instantiated before next call.

//swipe-handling code
$.with(v).swipe(new Function() {
    @Override
    public void invoke($ droidQuery, Object... params) {
        if (params[0] == SwipeDetector.Direction.START)
            isSwiping = true;
        else if (params[0] == SwipeDetector.Direction.STOP) {
            if (isSwiping) {                    isSwiping = false;
                if (swipeDirection != null) {
                    switch(swipeDirection) {
                        case DOWN :                                //TODO: Down swipe complete, so do something
                            break;
                        case UP :
                            //TODO: Up swipe complete, so do something
                            break;
                        case LEFT :
                            //TODO: Left swipe complete, so do something
                            break;
                        case RIGHT :
                            //TODO: Right swipe complete, so do something
                            break;
                        default :                                break;
                    }
                }                }
        }
        else {
            swipeDirection = (SwipeDetector.Direction) params[0];
        }
    }
});

Oryginalna odpowiedź

Ta odpowiedź wykorzystuje kombinację składników z innych odpowiedzi tutaj. Składa się z SwipeDetectorklasy, która ma wewnętrzny interfejs do nasłuchiwania zdarzeń. Podaję również, RelativeLayoutaby pokazać, jak zastąpić ViewaonTouch metody, aby umożliwić zarówno wydarzenia machnięcia i inne imprezy wykrywane (takie jak kliknięcia lub długich kliknięć).

SwipeDetector

package self.philbrown;

import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;

/**
 * Detect Swipes on a per-view basis. Based on original code by Thomas Fankhauser on StackOverflow.com,
 * with adaptations by other authors (see link).
 * @author Phil Brown
 * @see <a href="http://stackoverflow.com/questions/937313/android-basic-gesture-detection">android-basic-gesture-detection</a>
 */
public class SwipeDetector implements View.OnTouchListener
{
    /**
     * The minimum distance a finger must travel in order to register a swipe event.
     */
    private int minSwipeDistance;

    /** Maintains a reference to the first detected down touch event. */
    private float downX, downY;

    /** Maintains a reference to the first detected up touch event. */
    private float upX, upY;

    /** provides access to size and dimension contants */
    private ViewConfiguration config;

    /**
     * provides callbacks to a listener class for various swipe gestures.
     */
    private SwipeListener listener;

    public SwipeDetector(SwipeListener listener)
    {
        this.listener = listener;
    }


    /**
     * {@inheritDoc}
     */
    public boolean onTouch(View v, MotionEvent event)
    {
        if (config == null)
        {
                config = ViewConfiguration.get(v.getContext());
                minSwipeDistance = config.getScaledTouchSlop();
        }

        switch(event.getAction())
        {
        case MotionEvent.ACTION_DOWN:
            downX = event.getX();
            downY = event.getY();
            return true;
        case MotionEvent.ACTION_UP:
            upX = event.getX();
            upY = event.getY();

            float deltaX = downX - upX;
            float deltaY = downY - upY;

            // swipe horizontal?
            if(Math.abs(deltaX) > minSwipeDistance)
            {
                // left or right
                if (deltaX < 0)
                {
                        if (listener != null)
                        {
                                listener.onRightSwipe(v);
                                return true;
                        }
                }
                if (deltaX > 0)
                {
                        if (listener != null)
                        {
                                listener.onLeftSwipe(v);
                                return true;
                        }
                }
            }

            // swipe vertical?
            if(Math.abs(deltaY) > minSwipeDistance)
            {
                // top or down
                if (deltaY < 0)
                {
                        if (listener != null)
                        {
                                listener.onDownSwipe(v);
                                return true;
                        }
                }
                if (deltaY > 0)
                {
                        if (listener != null)
                        {
                                listener.onUpSwipe(v);
                                return true;
                        }
                }
            }
        }
        return false;
    }

    /**
     * Provides callbacks to a registered listener for swipe events in {@link SwipeDetector}
     * @author Phil Brown
     */
    public interface SwipeListener
    {
        /** Callback for registering a new swipe motion from the bottom of the view toward its top. */
        public void onUpSwipe(View v);
        /** Callback for registering a new swipe motion from the left of the view toward its right. */
        public void onRightSwipe(View v);
        /** Callback for registering a new swipe motion from the right of the view toward its left. */
        public void onLeftSwipe(View v);
        /** Callback for registering a new swipe motion from the top of the view toward its bottom. */
        public void onDownSwipe(View v);
    }
}

Przesuń widok przechwytywacza

package self.philbrown;

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.RelativeLayout;

import com.npeinc.module_NPECore.model.SwipeDetector;
import com.npeinc.module_NPECore.model.SwipeDetector.SwipeListener;

/**
 * View subclass used for handling all touches (swipes and others)
 * @author Phil Brown
 */
public class SwipeInterceptorView extends RelativeLayout
{
    private SwipeDetector swiper = null;

    public void setSwipeListener(SwipeListener listener)
    {
        if (swiper == null)
            swiper = new SwipeDetector(listener);
    }

    public SwipeInterceptorView(Context context) {
        super(context);
    }

    public SwipeInterceptorView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public SwipeInterceptorView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    @Override
    public boolean onTouchEvent(MotionEvent e)
    {
        boolean swipe = false, touch = false;
        if (swiper != null)
            swipe = swiper.onTouch(this, e);
        touch = super.onTouchEvent(e);
        return swipe || touch;
    }
}

1
Próbowałem zaimplementować to w widoku zawierającym klikalne elementy. Gdy przeciągnięcie rozpoczyna się nad elementem, który można kliknąć (np. Widok listy, w którym zarejestrowano detektor onItemClick), to onTouchEvent nigdy nie jest wywoływany. W związku z tym użytkownik nie może rozpocząć przeciągania po elemencie, który można kliknąć, co jest dla mnie niefortunne, a ja wciąż próbuję wymyślić, jak to obejść, ponieważ nasze klikalne elementy zajmują sporo miejsca na widok i nadal chcemy obsługiwać przeciąganie dla całego widoku. Jeśli przeciągnięcie nie zaczyna się od elementu, który można kliknąć, działa idealnie.
Lo-Tan,

@ Lo-Tan dzieje się tak, ponieważ klikalny element jest widokiem potomnym, a zatem znajduje się nad nim SwipeInterceptorView, więc jego kliknięcie jest obsługiwane jako pierwsze. Możesz to naprawić, wdrażając własny mechanizm klikania, implementując onTouchListenerlub, jako obejście problemu, możesz słuchać długich kliknięć zamiast kliknięć (patrz View.setOnLongClickListener).
Phil

W tej chwili próbuję tego. Lub możliwe anulowanie zdarzenia kliknięcia, jeśli zaczną przeciągać :) Dziękuję bardzo.
Lo-Tan,

Jednym z rozwiązań jest dołączenie detektora przesunięcia do każdego widoku w aplikacji. Innym jest zaimplementowanie onInterceptTouchEvent w SwipeInterceptorView.
Edward Falk

7

Wiem, że jest za późno, aby odpowiedzieć, ale wciąż wysyłam wykrywanie machnięcia dla ListView , jak używać Swipe Touch Listener w elemencie ListView .

Refrence: Exterminator13 (jedna z odpowiedzi na tej stronie)

Utwórz jeden ActivitySwipeDetector.class

package com.example.wocketapp;

import android.content.Context;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;

public class ActivitySwipeDetector implements View.OnTouchListener 
{
    static final String logTag = "SwipeDetector";
    private SwipeInterface activity;
    private float downX, downY;
    private long timeDown;
    private final float MIN_DISTANCE;
    private final int VELOCITY;
    private final float MAX_OFF_PATH;

    public ActivitySwipeDetector(Context context, SwipeInterface activity)
    {
        this.activity = activity;
        final ViewConfiguration vc = ViewConfiguration.get(context);
        DisplayMetrics dm = context.getResources().getDisplayMetrics();
        MIN_DISTANCE = vc.getScaledPagingTouchSlop() * dm.density;
        VELOCITY = vc.getScaledMinimumFlingVelocity();
        MAX_OFF_PATH = MIN_DISTANCE * 2;
    }

    public void onRightToLeftSwipe(View v) 
    {
        Log.i(logTag, "RightToLeftSwipe!");
        activity.onRightToLeft(v);
    }

    public void onLeftToRightSwipe(View v) 
    {
        Log.i(logTag, "LeftToRightSwipe!");
        activity.onLeftToRight(v);
    }

    public boolean onTouch(View v, MotionEvent event) 
    {
        switch (event.getAction()) 
        {
            case MotionEvent.ACTION_DOWN:
            {
                Log.d("onTouch", "ACTION_DOWN");
                timeDown = System.currentTimeMillis();
                downX = event.getX();
                downY = event.getY();
                v.getParent().requestDisallowInterceptTouchEvent(false);
                return true;
            }

        case MotionEvent.ACTION_MOVE:
            {
                float y_up = event.getY();
                float deltaY = y_up - downY;
                float absDeltaYMove = Math.abs(deltaY);

                if (absDeltaYMove > 60) 
                {
                    v.getParent().requestDisallowInterceptTouchEvent(false);
                } 
                else
                {
                    v.getParent().requestDisallowInterceptTouchEvent(true);
                }
            }

            break;

            case MotionEvent.ACTION_UP: 
            {
                Log.d("onTouch", "ACTION_UP");
                long timeUp = System.currentTimeMillis();
                float upX = event.getX();
                float upY = event.getY();

                float deltaX = downX - upX;
                float absDeltaX = Math.abs(deltaX);
                float deltaY = downY - upY;
                float absDeltaY = Math.abs(deltaY);

                long time = timeUp - timeDown;

                if (absDeltaY > MAX_OFF_PATH) 
                {
                    Log.e(logTag, String.format(
                            "absDeltaY=%.2f, MAX_OFF_PATH=%.2f", absDeltaY,
                            MAX_OFF_PATH));
                    return v.performClick();
                }

                final long M_SEC = 1000;
                if (absDeltaX > MIN_DISTANCE && absDeltaX > time * VELOCITY / M_SEC) 
                {
                     v.getParent().requestDisallowInterceptTouchEvent(true);
                    if (deltaX < 0) 
                    {
                        this.onLeftToRightSwipe(v);
                        return true;
                    }
                    if (deltaX > 0) 
                    {
                        this.onRightToLeftSwipe(v);
                        return true;
                    }
                }
                else 
                {
                    Log.i(logTag,
                            String.format(
                                    "absDeltaX=%.2f, MIN_DISTANCE=%.2f, absDeltaX > MIN_DISTANCE=%b",
                                    absDeltaX, MIN_DISTANCE,
                                    (absDeltaX > MIN_DISTANCE)));
                    Log.i(logTag,
                            String.format(
                                    "absDeltaX=%.2f, time=%d, VELOCITY=%d, time*VELOCITY/M_SEC=%d, absDeltaX > time * VELOCITY / M_SEC=%b",
                                    absDeltaX, time, VELOCITY, time * VELOCITY
                                            / M_SEC, (absDeltaX > time * VELOCITY
                                            / M_SEC)));
                }

                 v.getParent().requestDisallowInterceptTouchEvent(false);

            }
        }
        return false;
    }
    public interface SwipeInterface 
    {

        public void onLeftToRight(View v);

        public void onRightToLeft(View v);
    }

}

Nazwij to ze swojej klasy aktywności w ten sposób:

yourLayout.setOnTouchListener(new ActivitySwipeDetector(this, your_activity.this));

I nie zapomnij wdrożyć SwipeInterface, który da ci dwie metody @override:

    @Override
    public void onLeftToRight(View v) 
    {
        Log.e("TAG", "L to R");
    }

    @Override
    public void onRightToLeft(View v) 
    {
        Log.e("TAG", "R to L");
    }

Uważam, że a MAX_OFF_PATH = 5 * vc.getScaledPagingTouchSlop()jest wygodniejsze dla przesunięcia kciuka, naturalnie podróżującego po lekkim łuku.
qix

4

Gesty to te subtelne ruchy, które uruchamiają interakcje między ekranem dotykowym a użytkownikiem. Trwa od pierwszego dotknięcia ekranu do momentu, gdy ostatni palec opuści powierzchnię.

Android zapewnia nam klasę o nazwie GestureDetector, za pomocą której możemy wykrywać typowe gesty, takie jak stukanie w dół i w górę, przeciąganie w pionie i poziomie (fling), długie i krótkie naciśnięcie, podwójne stuknięcia itp . i dołącz do nich słuchaczy.

Spraw, by nasza klasa Activity zaimplementowała interfejsy GestureDetector.OnDoubleTapListener (do wykrywania gestów dwukrotnego dotknięcia) i GestureDetector.OnGestureListener i zaimplementuje wszystkie metody abstrakcyjne. Więcej informacji. możesz odwiedzić https://developer.android.com/training/gestures/detector.html . Kurtuazja

Do testu demonstracyjnego. GestureDetectorDemo


4

Jeśli nie chcesz tworzyć osobnej klasy lub tworzyć kodu złożonego,
możesz po prostu utworzyć zmienną GestureDetector w OnTouchListener i ułatwić sobie kod

namVyuVar może być dowolną nazwą Widoku, w której musisz ustawić listę

namVyuVar.setOnTouchListener(new View.OnTouchListener()
{
    @Override
    public boolean onTouch(View view, MotionEvent MsnEvtPsgVal)
    {
        flingActionVar.onTouchEvent(MsnEvtPsgVal);
        return true;
    }

    GestureDetector flingActionVar = new GestureDetector(getApplicationContext(), new GestureDetector.SimpleOnGestureListener()
    {
        private static final int flingActionMinDstVac = 120;
        private static final int flingActionMinSpdVac = 200;

        @Override
        public boolean onFling(MotionEvent fstMsnEvtPsgVal, MotionEvent lstMsnEvtPsgVal, float flingActionXcoSpdPsgVal, float flingActionYcoSpdPsgVal)
        {
            if(fstMsnEvtPsgVal.getX() - lstMsnEvtPsgVal.getX() > flingActionMinDstVac && Math.abs(flingActionXcoSpdPsgVal) > flingActionMinSpdVac)
            {
                // TskTdo :=> On Right to Left fling

                return false;
            }
            else if (lstMsnEvtPsgVal.getX() - fstMsnEvtPsgVal.getX() > flingActionMinDstVac && Math.abs(flingActionXcoSpdPsgVal) > flingActionMinSpdVac)
            {
                // TskTdo :=> On Left to Right fling

                return false;
            }

            if(fstMsnEvtPsgVal.getY() - lstMsnEvtPsgVal.getY() > flingActionMinDstVac && Math.abs(flingActionYcoSpdPsgVal) > flingActionMinSpdVac)
            {
                // TskTdo :=> On Bottom to Top fling

                return false;
            }
            else if (lstMsnEvtPsgVal.getY() - fstMsnEvtPsgVal.getY() > flingActionMinDstVac && Math.abs(flingActionYcoSpdPsgVal) > flingActionMinSpdVac)
            {
                // TskTdo :=> On Top to Bottom fling

                return false;
            }
            return false;
        }
    });
});

3

Wszystkim: nie zapomnij o przypadku MotionEvent.ACTION_CANCEL:

wywołuje 30% machnięcia bez ACTION_UP

i jest w tym przypadku równa ACTION_UP


2

Potrzebowałem bardziej ogólnej klasy, wziąłem klasę Tomasa i dodałem interfejs, który wysyła zdarzenia do twojej aktywności lub fragmentu. zarejestruje on detektor w konstruktorze, więc upewnij się, że implementujesz interfejs, w przeciwnym razie zostanie przekroczony ClassCastException. interfejs zwraca jedną z czterech końcowych wartości int zdefiniowanych w klasie i zwróci widok, na którym został aktywowany.

import android.app.Activity;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;

public class SwipeDetector implements View.OnTouchListener{

    static final int MIN_DISTANCE = 100;
    private float downX, downY, upX, upY;
    public final static int RIGHT_TO_LEFT=1;
    public final static int LEFT_TO_RIGHT=2;
    public final static int TOP_TO_BOTTOM=3;
    public final static int BOTTOM_TO_TOP=4;
    private View v;

    private onSwipeEvent swipeEventListener;


    public SwipeDetector(Activity activity,View v){
        try{
            swipeEventListener=(onSwipeEvent)activity;
        }
        catch(ClassCastException e)
        {
            Log.e("ClassCastException",activity.toString()+" must implement SwipeDetector.onSwipeEvent");
        } 
        this.v=v;
    }
    public SwipeDetector(Fragment fragment,View v){
        try{
            swipeEventListener=(onSwipeEvent)fragment;
        }
        catch(ClassCastException e)
        {
            Log.e("ClassCastException",fragment.toString()+" must implement SwipeDetector.onSwipeEvent");
        } 
        this.v=v;
    }


    public void onRightToLeftSwipe(){   
        swipeEventListener.SwipeEventDetected(v,RIGHT_TO_LEFT);
    }

    public void onLeftToRightSwipe(){   
        swipeEventListener.SwipeEventDetected(v,LEFT_TO_RIGHT);
    }

    public void onTopToBottomSwipe(){   
        swipeEventListener.SwipeEventDetected(v,TOP_TO_BOTTOM);
    }

    public void onBottomToTopSwipe(){
        swipeEventListener.SwipeEventDetected(v,BOTTOM_TO_TOP);
    }

    public boolean onTouch(View v, MotionEvent event) {
        switch(event.getAction()){
        case MotionEvent.ACTION_DOWN: {
            downX = event.getX();
            downY = event.getY();
            return true;
        }
        case MotionEvent.ACTION_UP: {
            upX = event.getX();
            upY = event.getY();

            float deltaX = downX - upX;
            float deltaY = downY - upY;

            //HORIZONTAL SCROLL
            if(Math.abs(deltaX) > Math.abs(deltaY))
            {
                if(Math.abs(deltaX) > MIN_DISTANCE){
                    // left or right
                    if(deltaX < 0) 
                    {
                        this.onLeftToRightSwipe();
                        return true;
                    }
                    if(deltaX > 0) {
                        this.onRightToLeftSwipe();
                        return true; 
                    }
                }
                else {
                    //not long enough swipe...
                    return false; 
                }
            }
            //VERTICAL SCROLL
            else 
            {
                if(Math.abs(deltaY) > MIN_DISTANCE){
                    // top or down
                    if(deltaY < 0) 
                    { this.onTopToBottomSwipe();
                    return true; 
                    }
                    if(deltaY > 0)
                    { this.onBottomToTopSwipe(); 
                    return true;
                    }
                }
                else {
                    //not long enough swipe...
                    return false;
                }
            }

            return true;
        }
        }
        return false;
    }
    public interface onSwipeEvent
    {
        public void SwipeEventDetected(View v , int SwipeType);
    }

}
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.