Botón de control, haga clic dentro de una fila en RecyclerView

81

Estoy usando el siguiente código para manejar los clics en las filas. ( fuente )

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) {
    }
}

Sin embargo, esto funciona, si quiero tener un botón de borrar en cada fila. No estoy seguro de cómo implementar eso con esto.

Adjunté el oyente OnClick para eliminar el botón que funciona (elimina la fila) pero también activa el onclick en la fila completa.

¿Alguien puede ayudarme a evitar hacer clic en una fila completa si se hace clic en un solo botón?

Gracias.

Ashwani K
fuente

Respuestas:

128

así es como manejo múltiples eventos onClick dentro de un recyclerView:

Editar: actualizado para incluir devoluciones de llamada (como se menciona en otros comentarios). He usado un WeakReferenceen ViewHolderpara eliminar una posible pérdida de memoria.

Definir interfaz:

public interface ClickListener {

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

Entonces el Adaptador:

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;
        }
    }
}

Luego, en su actividad / fragmento, lo que sea que pueda implementar: Clicklistener- o clase anónima si lo desea:

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
            }
        });

Para obtener en qué elemento se hizo clic, coincide con el ID de vista ievgetId () == cualquier elemento.getId ()

¡Espero que este enfoque ayude!

Mark Keen
fuente
1
Gracias, estaba usando este patrón solo para mi implementación. Sin embargo, el problema era otra cosa. Eche un vistazo aquí stackoverflow.com/questions/30287411/…
Ashwani K
1
¿De dónde se saca "esto"? No hay esto dentro del adaptador a menos que coincida con el contexto, y cuando hago eso, por alguna razón, es Casting View.OnclickListener también
Lion789
2
thisestá haciendo referencia a sí mismo, el ViewHolder (que es una clase estática separada en este caso): está configurando el oyente en el Viewholder que tiene sus datos vinculados onBindViewHolder(), no tiene nada que ver con el contexto en el Adaptador. No sé qué problemas tienes exactamente, pero esta solución funciona bien.
Mark Keen
1
@YasithaChinthaka ¿Ha intentado configurar este atributo: android:background="?attr/selectableItemBackground"en su xml para la vista?
Mark Keen
2
Solo quiero pasar y dar las gracias, esta solución es realmente fácil de seguir e implementar.
CodeGeass
50

Encuentro que normalmente:

  • Necesito usar varios oyentes porque tengo varios botones.
  • Quiero que mi lógica esté en la actividad y no en el adaptador o el visor.

Entonces, la respuesta de @ mark-keen funciona bien, pero tener una interfaz proporciona más flexibilidad:

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());
            }
        });
    }
}

Donde onClickListener está definido en su adaptador:

public MyAdapterListener onClickListener;

public interface MyAdapterListener {

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

Y probablemente establezca su constructor:

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

    rows = newRows;
    onClickListener = listener;
}

Luego, puede manejar los eventos en su Actividad o donde sea que se use su 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);
LordParsley
fuente
Esta es otra forma, y ​​se basa en mi respuesta (una similar que yo mismo uso), sin embargo, ¿cómo está pasando onClickListenera la clase Viewholder anidada estática? A menos que me esté perdiendo algo, no puedo ver cómo se lo pasa a su ViewHolder. Además, si solo usa un método de interfaz, podría usar una expresión Lambda, que condensa todo.
Mark Keen
public MyAdapterListener onClickListener; es una variable miembro definida en su adaptador en el código anterior y establecida en su constructor de adaptador. (Alternativamente, pero no se muestra arriba, también puede usar un configurador personalizado como setOnClickListener).
LordParsley
5
Solo estaba preguntando cómo está accediendo a un miembro / variable de instancia en su clase de Adaptador desde dentro de una clase anidada estática (ViewHolder).
Mark Keen
Como Mark, no pude anidar dentro del adaptador. vea mi respuesta sobre cómo evitar tener que anidar
Tony BenBrahim
@MarkKeen ... exactamente la misma pregunta que tenía.
user2695433
6

Quería una solución que no creara ningún extra objeto (es decir, oyentes) que tendrían que ser recolectados como basura más tarde, y que no requiriera anidar un titular de vista dentro de una clase de adaptador.

En la ViewHolderclase

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());
            }
        }

Puntos a tener en cuenta: la ClickHandlerinterfaz está definida, pero no inicializada aquí, por lo que no hay suposición en elonClick método que alguna vez se haya inicializado.

La ClickHandlerinterfaz se ve así:

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

En el adaptador, establezca una instancia de 'ClickHandler' en el constructor y anule onBindViewHolder, para inicializar 'clickHandler' en el titular de la vista:

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;
    }

Nota: Sé que viewHolder.clickHandler se está configurando potencialmente varias veces con el mismo valor exacto, pero esto es más barato que verificar nulos y ramificaciones, y no hay costo de memoria, solo una instrucción adicional.

Finalmente, cuando crea el adaptador, se ve obligado a pasar una ClickHandlerinstancia al constructor, así:

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
    }
});

Tenga en cuenta que aquí adapteres una variable miembro, no una variable local

Tony BenBrahim
fuente
Gracias por su respuesta :) Una cosa que quiero agregar aquí no use adapter.getItem (posición) en lugar de usar yourmodel.get (posición)
Khubaib Raza
5

Solo quería agregar otra solución si ya tiene un oyente táctil reciclador y desea manejar todos los eventos táctiles en él en lugar de tratar el evento táctil del botón por separado en el soporte de la vista. Lo clave que hace esta versión adaptada de la clase es devolver la vista del botón en la devolución de llamada onItemClick () cuando se toca, a diferencia del contenedor de elementos. A continuación, puede probar si la vista es un botón y realizar una acción diferente. Tenga en cuenta que un toque prolongado en el botón se interpreta como un toque prolongado en toda la fila.

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) {

    }
}

Luego usándolo de actividad / fragmento:

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) {
            }
        });
    }
víboras
fuente
1

Debe devolver verdadero en el interior onInterceptTouchEvent()cuando maneja el evento de clic.

Eliyahu Shwartz
fuente
1
Hola, puedes ser más detallado sobre esto. Estoy usando el siguiente código para enlazar para eliminar el botón btnDelete = (ImageButton) itemView.findViewById (R.id.btnDelete); btnDelete.setOnClickListener (nuevo View.OnClickListener () {@Override public void onClick (Ver vista) {eliminar (getLayoutPosition ());}});
Ashwani K
Al igual que onTouchEvent (), el valor de retorno indica si el evento se ha manejado o no, y cuando no es el evento pasado a la fila completa.
Eliyahu Shwartz
0

Primero puede verificar si tiene entradas similares, si obtiene una colección con tamaño 0, inicie una nueva consulta para guardar.

O

forma más profesional y rápida. crear un disparador de nube (antes de guardar)

mira esta respuesta https://stackoverflow.com/a/35194514/1388852

Hatim
fuente
0

Simplemente coloque un método de anulación llamado getItemId Consígalo haciendo clic derecho> generar> métodos de anulación> getItemId Coloque este método en la clase Adaptador

Anurag Bhalekar
fuente