Przycisk uchwytu - kliknij wewnątrz wiersza w RecyclerView


83

Używam następującego kodu do obsługi kliknięć wierszy. ( źródło )

static class RecyclerTouchListener implements RecyclerView.OnItemTouchListener {

    private GestureDetector gestureDetector;
    private ClickListener clickListener;

    public RecyclerTouchListener(Context context, final RecyclerView recyclerView, final ClickListener clickListener) {
        this.clickListener = clickListener;
        gestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
            @Override
            public boolean onSingleTapUp(MotionEvent e) {
                return true;
            }

            @Override
            public void onLongPress(MotionEvent e) {
                View child = recyclerView.findChildViewUnder(e.getX(), e.getY());
                if (child != null && clickListener != null) {
                    clickListener.onLongClick(child, recyclerView.getChildPosition(child));
                }
            }
        });
    }

    @Override
    public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {

        View child = rv.findChildViewUnder(e.getX(), e.getY());
        if (child != null && clickListener != null && gestureDetector.onTouchEvent(e)) {
            clickListener.onClick(child, rv.getChildPosition(child));
        }
        return false;
    }

    @Override
    public void onTouchEvent(RecyclerView rv, MotionEvent e) {
    }
}

Działa to jednak, jeśli chcę powiedzieć przycisk usuwania w każdym wierszu. Nie jestem pewien, jak to zaimplementować.

Dołączyłem odbiornik OnClick, aby usunąć przycisk, który działa (usuwa wiersz), ale także uruchamia onclick w pełnym wierszu.

Czy ktoś może mi pomóc, jak uniknąć kliknięcia całego wiersza po kliknięciu jednego przycisku.

Dzięki.

Odpowiedzi:


128

w ten sposób obsługuję wiele zdarzeń onClick wewnątrz pliku recyclinglerView:

Edycja: zaktualizowano w celu uwzględnienia wywołań zwrotnych (jak wspomniano w innych komentarzach). Użyłem WeakReferencew, ViewHolderaby wyeliminować potencjalny wyciek pamięci.

Zdefiniuj interfejs:

public interface ClickListener {

    void onPositionClicked(int position);
    
    void onLongClicked(int position);
}

Następnie adapter:

public class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyViewHolder> {
    
    private final ClickListener listener;
    private final List<MyItems> itemsList;

    public MyAdapter(List<MyItems> itemsList, ClickListener listener) {
        this.listener = listener;
        this.itemsList = itemsList;
    }

    @Override public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        return new MyViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.my_row_layout), parent, false), listener);
    }

    @Override public void onBindViewHolder(MyViewHolder holder, int position) {
        // bind layout and data etc..
    }

    @Override public int getItemCount() {
        return itemsList.size();
    }

    public static class MyViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener {

        private ImageView iconImageView;
        private TextView iconTextView;
        private WeakReference<ClickListener> listenerRef;

        public MyViewHolder(final View itemView, ClickListener listener) {
            super(itemView);

            listenerRef = new WeakReference<>(listener);
            iconImageView = (ImageView) itemView.findViewById(R.id.myRecyclerImageView);
            iconTextView = (TextView) itemView.findViewById(R.id.myRecyclerTextView);

            itemView.setOnClickListener(this);
            iconTextView.setOnClickListener(this);
            iconImageView.setOnLongClickListener(this);
        }

        // onClick Listener for view
        @Override
        public void onClick(View v) {

            if (v.getId() == iconTextView.getId()) {
                Toast.makeText(v.getContext(), "ITEM PRESSED = " + String.valueOf(getAdapterPosition()), Toast.LENGTH_SHORT).show();
            } else {
                Toast.makeText(v.getContext(), "ROW PRESSED = " + String.valueOf(getAdapterPosition()), Toast.LENGTH_SHORT).show();
            }
            
            listenerRef.get().onPositionClicked(getAdapterPosition());
        }


        //onLongClickListener for view
        @Override
        public boolean onLongClick(View v) {

            final AlertDialog.Builder builder = new AlertDialog.Builder(v.getContext());
            builder.setTitle("Hello Dialog")
                    .setMessage("LONG CLICK DIALOG WINDOW FOR ICON " + String.valueOf(getAdapterPosition()))
                    .setPositiveButton("OK", new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {

                        }
                    });

            builder.create().show();
            listenerRef.get().onLongClicked(getAdapterPosition());
            return true;
        }
    }
}

Następnie w swojej aktywności / fragmencie - cokolwiek możesz zaimplementować: Clicklistener- lub anonimową klasę, jeśli chcesz:

MyAdapter adapter = new MyAdapter(myItems, new ClickListener() {
            @Override public void onPositionClicked(int position) {
                // callback performed on click
            }

            @Override public void onLongClicked(int position) {
                // callback performed on click
            }
        });

Aby sprawdzić, który element został kliknięty, dopasuj identyfikator widoku ievgetId () == cokolwiekItem.getId ()

Mam nadzieję, że to podejście pomoże!


1
Dzięki, użyłem tego wzorca tylko do mojej realizacji. Jednak problem był inny. Spójrz tutaj stackoverflow.com/questions/30287411/…
Ashwani K

1
Skąd się bierze „to”? Nie ma tego w adapterze, chyba że dopasujesz kontekst, a kiedy robię to z jakiegoś powodu, jest to Casting View.OnclickListener również to
Lion789

2
thisodwołuje się do siebie, ViewHolder (który w tym przypadku jest oddzielną klasą statyczną) - ustawiasz nasłuchiwanie na Viewholder, w którym są powiązane Twoje dane onBindViewHolder(), nie ma to nic wspólnego z kontekstem adaptera. Nie wiem, jakie dokładnie masz problemy, ale to rozwiązanie działa dobrze.
Mark Keen,

1
@YasithaChinthaka Czy próbowałeś ustawić ten atrybut: android:background="?attr/selectableItemBackground"w swoim xml dla widoku?
Mark Keen

2
Chcę tylko wpaść i podziękować, to rozwiązanie jest naprawdę łatwe do naśladowania i wdrożenia.
CodeGeass

51

Uważam, że zazwyczaj:

  • Muszę korzystać z wielu słuchaczy, ponieważ mam kilka przycisków.
  • Chcę, aby moja logika znajdowała się w działaniu, a nie w adapterze lub widoku.

Odpowiedź @ mark-keen działa dobrze, ale posiadanie interfejsu zapewnia większą elastyczność:

public static class MyViewHolder extends RecyclerView.ViewHolder {

    public ImageView iconImageView;
    public TextView iconTextView;

    public MyViewHolder(final View itemView) {
        super(itemView);

        iconImageView = (ImageView) itemView.findViewById(R.id.myRecyclerImageView);
        iconTextView = (TextView) itemView.findViewById(R.id.myRecyclerTextView);

        iconTextView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                onClickListener.iconTextViewOnClick(v, getAdapterPosition());
            }
        });
        iconImageView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                onClickListener.iconImageViewOnClick(v, getAdapterPosition());
            }
        });
    }
}

Gdzie onClickListener jest zdefiniowany w adapterze:

public MyAdapterListener onClickListener;

public interface MyAdapterListener {

    void iconTextViewOnClick(View v, int position);
    void iconImageViewOnClick(View v, int position);
}

I prawdopodobnie ustawione przez twojego konstruktora:

public MyAdapter(ArrayList<MyListItems> newRows, MyAdapterListener listener) {

    rows = newRows;
    onClickListener = listener;
}

Następnie możesz obsłużyć zdarzenia w swojej Aktywności lub wszędzie tam, gdzie używany jest Twój RecyclerView:

mAdapter = new MyAdapter(mRows, new MyAdapter.MyAdapterListener() {
                    @Override
                    public void iconTextViewOnClick(View v, int position) {
                        Log.d(TAG, "iconTextViewOnClick at position "+position);
                    }

                    @Override
                    public void iconImageViewOnClick(View v, int position) {
                        Log.d(TAG, "iconImageViewOnClick at position "+position);
                    }
                });
mRecycler.setAdapter(mAdapter);

To jest inny sposób i opiera się na mojej odpowiedzi (podobnej, której sam używam), jednak w jaki sposób przechodzisz onClickListenerdo statycznej zagnieżdżonej klasy Viewholder? Chyba że brakuje mi czegoś, nie widzę, jak przekazujesz to do swojego ViewHolder. Jeśli używasz tylko jednej metody interfejsu, możesz użyć wyrażenia Lambda, które wszystko skondensuje.
Mark Keen,

public MyAdapterListener onClickListener; jest zmienną składową zdefiniowaną w adapterze w powyższym kodzie i ustawioną w konstruktorze adaptera. (Alternatywnie, ale nie pokazano powyżej, możesz również użyć niestandardowego setera, takiego jak setOnClickListener.)
LordParsley

5
Właśnie pytałem, w jaki sposób uzyskujesz dostęp do zmiennej składowej / instancji w klasie Adapter z poziomu statycznej klasy zagnieżdżonej (ViewHolder).
Mark Keen,

Podobnie jak Mark, nie byłem w stanie zagnieździć się w adapterze. zobacz moją odpowiedź, jak uniknąć konieczności zagnieżdżania się
Tony BenBrahim,

@MarkKeen .. dokładnie to samo pytanie, które miałem.
user2695433

6

Zależało mi na rozwiązaniu, które nie tworzy żadnych dodatkowych obiektów (tj. Odbiorników), które musiałyby być później zbierane jako śmieci, i nie wymagałoby zagnieżdżania uchwytu widoku w klasie adaptera.

W ViewHolderklasie

private static class MyViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {

        private final TextView ....// declare the fields in your view
        private ClickHandler ClickHandler;

        public MyHolder(final View itemView) {
            super(itemView);
            nameField = (TextView) itemView.findViewById(R.id.name);
            //find other fields here...
            Button myButton = (Button) itemView.findViewById(R.id.my_button);
            myButton.setOnClickListener(this);
        }
        ...
        @Override
        public void onClick(final View view) {
            if (clickHandler != null) {
                clickHandler.onMyButtonClicked(getAdapterPosition());
            }
        }

Warto zwrócić uwagę: ClickHandlerinterfejs jest tutaj zdefiniowany, ale nie zainicjowany, więc w onClickmetodzie nie ma założenia , że został kiedykolwiek zainicjowany.

ClickHandlerInterfejs wygląda tak:

private interface ClickHandler {
    void onMyButtonClicked(final int position);
} 

W adapterze ustaw instancję 'ClickHandler' w konstruktorze i onBindViewHoldernadpisz, aby zainicjować `clickHandler 'na uchwycie widoku:

private class MyAdapter extends ...{

    private final ClickHandler clickHandler;

    public MyAdapter(final ClickHandler clickHandler) {
        super(...);
        this.clickHandler = clickHandler;
    }

    @Override
    public void onBindViewHolder(final MyViewHolder viewHolder, final int position) {
        super.onBindViewHolder(viewHolder, position);
        viewHolder.clickHandler = this.clickHandler;
    }

Uwaga: wiem, że viewHolder.clickHandler może być ustawiany wiele razy z dokładnie tą samą wartością, ale jest to tańsze niż sprawdzanie wartości null i rozgałęzień, a nie ma kosztu pamięci, tylko dodatkowa instrukcja.

Na koniec, kiedy tworzysz adapter, jesteś zmuszony przekazać ClickHandlerinstancję do konstruktora, tak jak to:

adapter = new MyAdapter(new ClickHandler() {
    @Override
    public void onMyButtonClicked(final int position) {
        final MyModel model = adapter.getItem(position);
        //do something with the model where the button was clicked
    }
});

Zauważ, że adapterjest to zmienna składowa, a nie zmienna lokalna


Dziękuję za odpowiedź :) Jedna rzecz, którą chcę tutaj dodać, nie używaj adapter.getItem (pozycja), zamiast używać yourmodel.get (pozycja)
Khubaib Raza

5

Chciałem tylko dodać kolejne rozwiązanie, jeśli masz już odbiornik dotyku recyklera i chcesz obsługiwać wszystkie zdarzenia dotykowe w nim, zamiast zajmować się zdarzeniem dotknięcia przycisku oddzielnie w uchwycie widoku. Kluczową rzeczą, jaką robi ta dostosowana wersja klasy, jest zwrócenie widoku przycisku w wywołaniu zwrotnym onItemClick (), gdy jest on dotknięty, w przeciwieństwie do kontenera elementu. Następnie możesz sprawdzić, czy widok jest przyciskiem i wykonać inną akcję. Uwaga, długie naciśnięcie przycisku jest interpretowane jako długie dotknięcie całego wiersza.

public class RecyclerItemClickListener implements RecyclerView.OnItemTouchListener
{
    public static interface OnItemClickListener
    {
        public void onItemClick(View view, int position);
        public void onItemLongClick(View view, int position);
    }

    private OnItemClickListener mListener;
    private GestureDetector mGestureDetector;

    public RecyclerItemClickListener(Context context, final RecyclerView recyclerView, OnItemClickListener listener)
    {
        mListener = listener;

        mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener()
        {
            @Override
            public boolean onSingleTapUp(MotionEvent e)
            {
                // Important: x and y are translated coordinates here
                final ViewGroup childViewGroup = (ViewGroup) recyclerView.findChildViewUnder(e.getX(), e.getY());

                if (childViewGroup != null && mListener != null) {
                    final List<View> viewHierarchy = new ArrayList<View>();
                    // Important: x and y are raw screen coordinates here
                    getViewHierarchyUnderChild(childViewGroup, e.getRawX(), e.getRawY(), viewHierarchy);

                    View touchedView = childViewGroup;
                    if (viewHierarchy.size() > 0) {
                        touchedView = viewHierarchy.get(0);
                    }
                    mListener.onItemClick(touchedView, recyclerView.getChildPosition(childViewGroup));
                    return true;
                }

                return false;
            }

            @Override
            public void onLongPress(MotionEvent e)
            {
                View childView = recyclerView.findChildViewUnder(e.getX(), e.getY());

                if(childView != null && mListener != null)
                {
                    mListener.onItemLongClick(childView, recyclerView.getChildPosition(childView));
                }
            }
        });
    }

    public void getViewHierarchyUnderChild(ViewGroup root, float x, float y, List<View> viewHierarchy) {
        int[] location = new int[2];
        final int childCount = root.getChildCount();

        for (int i = 0; i < childCount; ++i) {
            final View child = root.getChildAt(i);
            child.getLocationOnScreen(location);
            final int childLeft = location[0], childRight = childLeft + child.getWidth();
            final int childTop = location[1], childBottom = childTop + child.getHeight();

            if (child.isShown() && x >= childLeft && x <= childRight && y >= childTop && y <= childBottom) {
                viewHierarchy.add(0, child);
            }
            if (child instanceof ViewGroup) {
                getViewHierarchyUnderChild((ViewGroup) child, x, y, viewHierarchy);
            }
        }
    }

    @Override
    public boolean onInterceptTouchEvent(RecyclerView view, MotionEvent e)
    {
        mGestureDetector.onTouchEvent(e);

        return false;
    }

    @Override
    public void onTouchEvent(RecyclerView view, MotionEvent motionEvent){}

    @Override
    public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {

    }
}

Następnie używając go z aktywności / fragmentu:

recyclerView.addOnItemTouchListener(createItemClickListener(recyclerView));

    public RecyclerItemClickListener createItemClickListener(final RecyclerView recyclerView) {
        return new RecyclerItemClickListener (context, recyclerView, new RecyclerItemClickListener.OnItemClickListener() {
            @Override
            public void onItemClick(View view, int position) {
                if (view instanceof AppCompatButton) {
                    // ... tapped on the button, so go do something
                } else {
                    // ... tapped on the item container (row), so do something different
                }
            }

            @Override
            public void onItemLongClick(View view, int position) {
            }
        });
    }

1

onInterceptTouchEvent()Podczas obsługi zdarzenia kliknięcia musisz zwrócić wartość true w środku .


1
Cześć, czy możesz to bardziej rozwinąć. Używam następującego kodu, aby powiązać przycisk usuwania btnDelete = (ImageButton) itemView.findViewById (R.id.btnDelete); btnDelete.setOnClickListener (new View.OnClickListener () {@Override public void onClick (Widok widoku) {remove (getLayoutPosition ());}});
Ashwani K

Podobnie jak onTouchEvent (), wartość zwracana wskazuje, czy zdarzenie zostało obsłużone, czy nie, a kiedy nie jest to zdarzenie przekazane do pełnego wiersza.
Eliyahu Shwartz

0

Możesz najpierw sprawdzić, czy masz podobne wpisy, jeśli otrzymasz kolekcję o rozmiarze 0, rozpocznij nowe zapytanie, aby zapisać.

LUB

bardziej profesjonalny i szybszy sposób. utwórz wyzwalacz w chmurze (przed zapisaniem)

sprawdź tę odpowiedź https://stackoverflow.com/a/35194514/1388852


0

Po prostu umieść metodę nadpisywania o nazwie getItemId Pobierz ją prawym przyciskiem myszy> generuj> zastępuj metody> getItemId Umieść tę metodę w klasie Adapter

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.