Cómo manejar los clics de botón usando el XML onClick dentro de Fragmentos


440

Antes del nido de abeja (Android 3), cada actividad se registraba para manejar los clics de botón a través de la onClicketiqueta en un XML de diseño:

android:onClick="myClickMethod"

Dentro de ese método, puede usar view.getId()una instrucción switch para hacer la lógica del botón.

Con la introducción de Honeycomb, estoy dividiendo estas actividades en fragmentos que pueden reutilizarse dentro de muchas actividades diferentes. La mayor parte del comportamiento de los botones es independiente de la actividad, y me gustaría que el código resida dentro del archivo Fragments sin usar el método anterior (anterior a 1.6) de registrar el OnClickListenerbotón para cada botón.

final Button button = (Button) findViewById(R.id.button_id);
button.setOnClickListener(new View.OnClickListener() {
    public void onClick(View v) {
        // Perform action on click
    }
});

El problema es que cuando se infla mi diseño, sigue siendo la Actividad de alojamiento la que recibe los clics de los botones, no los Fragmentos individuales. ¿Hay un buen enfoque para cualquiera

  • ¿Registrar el fragmento para recibir los clics del botón?
  • ¿Pasar los eventos de clic de la actividad al fragmento al que pertenecen?

1
¿No puede manejar el registro de oyentes dentro del onCreate del fragmento?
CL22

24
@jodes Sí, pero no quiero tener que usar setOnClickListenery findViewByIdpara cada botón, por eso onClickse agregó, para simplificar las cosas.
smith324

44
Mirando la respuesta aceptada, creo que usar setOnClickListener está más débilmente acoplado que apegarse al enfoque XML onClick. Si la actividad tiene que 'reenviar' cada clic al fragmento correcto, esto significa que el código tendrá que cambiar cada vez que se agregue un fragmento. Usar una interfaz para desacoplar de la clase base del fragmento no ayuda con eso. Si el fragmento se registra con el botón correcto en sí, la actividad permanece completamente agnóstica, lo que es mejor estilo IMO. Ver también la respuesta de Adorjan Princz.
Adriaan Koster

@ smith324 tiene que estar de acuerdo con Adriaan en este caso. Pruebe la respuesta de Adorjan y vea si la vida no es mejor después de eso.
user1567453

Respuestas:


175

Podrías hacer esto:

Actividad:

Fragment someFragment;    

//...onCreate etc instantiating your fragments

public void myClickMethod(View v) {
    someFragment.myClickMethod(v);
}

Fragmento:

public void myClickMethod(View v) {
    switch(v.getId()) {
        // Just like you were doing
    }
}    

En respuesta a @Ameen que quería menos acoplamiento para que los fragmentos sean reutilizables

Interfaz:

public interface XmlClickable {
    void myClickMethod(View v);
}

Actividad:

XmlClickable someFragment;    

//...onCreate, etc. instantiating your fragments casting to your interface.
public void myClickMethod(View v) {
    someFragment.myClickMethod(v);
}

Fragmento:

public class SomeFragment implements XmlClickable {

//...onCreateView, etc.

@Override
public void myClickMethod(View v) {
    switch(v.getId()){
        // Just like you were doing
    }
}    

52
Eso es lo que estoy haciendo ahora esencialmente, pero es mucho más complicado cuando tienes múltiples fragmentos que cada uno necesita para recibir eventos de clic. Estoy agravado con fragmentos en general porque los paradigmas se han disuelto alrededor de ellos.
smith324

128
Me encuentro con el mismo problema, y ​​aunque aprecio su respuesta, este no es un código limpio desde el punto de vista de la ingeniería de software. Este código da como resultado que la actividad esté estrechamente unida al fragmento. Debería poder reutilizar el mismo fragmento en múltiples actividades sin que las actividades conozcan los detalles de implementación de los fragmentos.
Ameen

1
Debe ser "switch (v.getId ()) {" y no "switch (v.getid ()) {"
eb80

55
En lugar de definir su propia interfaz, puede usar el OnClickListener ya existente como lo menciona Euporie.
fr00tyl00p

1
Casi lloré cuando leí esto, es MUCHO PLATO DE CALDERA ... La respuesta a continuación de @AdorjanPrincz es el camino a seguir.
Wjdavis5

603

Prefiero usar la siguiente solución para manejar eventos onClick. Esto funciona también para Actividad y Fragmentos.

public class StartFragment extends Fragment implements OnClickListener{

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {

        View v = inflater.inflate(R.layout.fragment_start, container, false);

        Button b = (Button) v.findViewById(R.id.StartButton);
        b.setOnClickListener(this);
        return v;
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
        case R.id.StartButton:

            ...

            break;
        }
    }
}

99
En onCreateView, recorro todos los elementos secundarios de ViewGroup v y configuro onclicklistener para todas las instancias de Button que encuentro. Es mucho mejor que configurar manualmente el oyente para todos los botones.
Una persona

17
Votado Esto hace que los fragmentos sean reutilizables. De lo contrario, ¿por qué usar fragmentos?
boreas

44
¿No es esta la misma técnica recomendada por Programming Windows en 1987? No es para preocuparse. Google se mueve rápido y se trata de la productividad del desarrollador. Estoy seguro de que no pasará mucho tiempo hasta que el manejo de eventos sea tan bueno como el Visual Basic de 1991.
Edward Brey

77
¿Qué importación de brujas has utilizado para OnClickListener? Intellij me sugiere android.view.View.OnClickListener y no funciona: / (onClick nunca se ejecuta)
Lucas Jota

44
@NathanOsman Creo que la pregunta estaba relacionada con el xml onClick, por lo que los participantes aceptados proporcionan la solución exacta.
Naveed Ahmad

29

Creo que el problema es que la vista sigue siendo la actividad, no el fragmento. Los fragmentos no tienen una vista independiente propia y se adjunta a la vista de actividades principales. Es por eso que el evento termina en la Actividad, no en el fragmento. Es desafortunado, pero creo que necesitarás un código para que esto funcione.

Lo que he estado haciendo durante las conversiones es simplemente agregar un detector de clics que llama al antiguo controlador de eventos.

por ejemplo:

final Button loginButton = (Button) view.findViewById(R.id.loginButton);
loginButton.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(final View v) {
        onLoginClicked(v);
    }
});

1
Gracias. Utilicé esto con una ligera modificación, ya que paso la vista de fragmentos (es decir, el resultado de inflater.inflate (R.layout.my_fragment_xml_resource)) a onLoginClicked () para que pueda acceder a las sub-vistas de fragmentos, como EditText, a través de view.findViewById () (Si simplemente paso por la vista de actividad, las llamadas a view.findViewById (R.id.myfragmentwidget_id) devuelve nulo).
Michael Nelson

Esto no funciona con API 21 en mi proyecto. ¿Alguna idea sobre cómo usar este enfoque?
Darth Coder

Su código bastante básico, utilizado en casi todas las aplicaciones. ¿Puedes describir lo que te está pasando?
Brill Pappin

1
Voto a favor de esta respuesta para obtener una explicación del problema que ocurrió porque el diseño del fragmento está adjunto a la vista de la actividad.
fllo

25

Recientemente resolví este problema sin tener que agregar un método a la Actividad de contexto o tener que implementar OnClickListener. No estoy seguro si es una solución "válida" tampoco, pero funciona.

Basado en: https://developer.android.com/tools/data-binding/guide.html#binding_events

Se puede hacer con enlaces de datos: simplemente agregue su instancia de fragmento como una variable, luego puede vincular cualquier método con onClick.

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    tools:context="com.example.testapp.fragments.CustomFragment">

    <data>
        <variable android:name="fragment" android:type="com.example.testapp.fragments.CustomFragment"/>
    </data>
    <LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ImageButton
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/ic_place_black_24dp"
            android:onClick="@{() -> fragment.buttonClicked()}"/>
    </LinearLayout>
</layout>

Y el fragmento de código de enlace sería ...

public class CustomFragment extends Fragment {

    ...

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        View view = inflater.inflate(R.layout.fragment_person_profile, container, false);
        FragmentCustomBinding binding = DataBindingUtil.bind(view);
        binding.setFragment(this);
        return view;
    }

    ...

}

1
Solo tengo un presentimiento, faltan algunos detalles ya que no puedo hacer que esta solución funcione
Tima

Tal vez le falta algo del "Entorno de construcción" en la documentación: developer.android.com/tools/data-binding/…
Aldo Canepa

@Aldo Para el método onClick en XML, creo que debería tener android: onClick = "@ {() -> fragment. ButtonClicked ()}" en su lugar. También para otros, debe declarar la función buttonClicked () dentro del fragmento y poner su lógica dentro.
Hayk Nahapetyan

en el xml debería serandroid:name="fragment" android:type="com.example.testapp.fragments.CustomFragment"/>
COYG

1
Acabo de probar esto y los atributos namey typede la variableetiqueta no deberían tener el android:prefijo. ¿Quizás haya una versión antigua de los diseños de Android que lo requiera?
JohnnyLambada

6

ButterKnife es probablemente la mejor solución para el problema del desorden. Utiliza procesadores de anotación para generar el llamado código repetitivo de "método antiguo".

Pero el método onClick todavía se puede usar, con un inflador personalizado.

Cómo utilizar

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup cnt, Bundle state) {
    inflater = FragmentInflatorFactory.inflatorFor(inflater, this);
    return inflater.inflate(R.layout.fragment_main, cnt, false);
}

Implementación

public class FragmentInflatorFactory implements LayoutInflater.Factory {

    private static final int[] sWantedAttrs = { android.R.attr.onClick };

    private static final Method sOnCreateViewMethod;
    static {
        // We could duplicate its functionallity.. or just ignore its a protected method.
        try {
            Method method = LayoutInflater.class.getDeclaredMethod(
                    "onCreateView", String.class, AttributeSet.class);
            method.setAccessible(true);
            sOnCreateViewMethod = method;
        } catch (NoSuchMethodException e) {
            // Public API: Should not happen.
            throw new RuntimeException(e);
        }
    }

    private final LayoutInflater mInflator;
    private final Object mFragment;

    public FragmentInflatorFactory(LayoutInflater delegate, Object fragment) {
        if (delegate == null || fragment == null) {
            throw new NullPointerException();
        }
        mInflator = delegate;
        mFragment = fragment;
    }

    public static LayoutInflater inflatorFor(LayoutInflater original, Object fragment) {
        LayoutInflater inflator = original.cloneInContext(original.getContext());
        FragmentInflatorFactory factory = new FragmentInflatorFactory(inflator, fragment);
        inflator.setFactory(factory);
        return inflator;
    }

    @Override
    public View onCreateView(String name, Context context, AttributeSet attrs) {
        if ("fragment".equals(name)) {
            // Let the Activity ("private factory") handle it
            return null;
        }

        View view = null;

        if (name.indexOf('.') == -1) {
            try {
                view = (View) sOnCreateViewMethod.invoke(mInflator, name, attrs);
            } catch (IllegalAccessException e) {
                throw new AssertionError(e);
            } catch (InvocationTargetException e) {
                if (e.getCause() instanceof ClassNotFoundException) {
                    return null;
                }
                throw new RuntimeException(e);
            }
        } else {
            try {
                view = mInflator.createView(name, null, attrs);
            } catch (ClassNotFoundException e) {
                return null;
            }
        }

        TypedArray a = context.obtainStyledAttributes(attrs, sWantedAttrs);
        String methodName = a.getString(0);
        a.recycle();

        if (methodName != null) {
            view.setOnClickListener(new FragmentClickListener(mFragment, methodName));
        }
        return view;
    }

    private static class FragmentClickListener implements OnClickListener {

        private final Object mFragment;
        private final String mMethodName;
        private Method mMethod;

        public FragmentClickListener(Object fragment, String methodName) {
            mFragment = fragment;
            mMethodName = methodName;
        }

        @Override
        public void onClick(View v) {
            if (mMethod == null) {
                Class<?> clazz = mFragment.getClass();
                try {
                    mMethod = clazz.getMethod(mMethodName, View.class);
                } catch (NoSuchMethodException e) {
                    throw new IllegalStateException(
                            "Cannot find public method " + mMethodName + "(View) on "
                                    + clazz + " for onClick");
                }
            }

            try {
                mMethod.invoke(mFragment, v);
            } catch (InvocationTargetException e) {
                throw new RuntimeException(e);
            } catch (IllegalAccessException e) {
                throw new AssertionError(e);
            }
        }
    }
}

6

Prefiero el manejo de clics en el código que usar el onClickatributo en XML cuando trabajo con fragmentos.

Esto se vuelve aún más fácil cuando migra sus actividades a fragmentos. Simplemente puede llamar al controlador de clics (previamente configurado android:onClicken XML) directamente desde cada casebloque.

findViewById(R.id.button_login).setOnClickListener(clickListener);
...

OnClickListener clickListener = new OnClickListener() {
    @Override
    public void onClick(final View v) {
        switch(v.getId()) {
           case R.id.button_login:
              // Which is supposed to be called automatically in your
              // activity, which has now changed to a fragment.
              onLoginClick(v);
              break;

           case R.id.button_logout:
              ...
        }
    }
}

Cuando se trata de manejar clics en fragmentos, esto me parece más simple que android:onClick.


5

Esta es otra forma:

1.Cree un Fragmento Base como este:

public abstract class BaseFragment extends Fragment implements OnClickListener

2.Utilice

public class FragmentA extends BaseFragment 

en vez de

public class FragmentA extends Fragment

3.En su actividad:

public class MainActivity extends ActionBarActivity implements OnClickListener

y

BaseFragment fragment = new FragmentA;

public void onClick(View v){
    fragment.onClick(v);
}

Espero eso ayude.


1 año, 1 mes y 1 día después de su respuesta: ¿Hay alguna otra razón además de no repetir la implementación de OnClickListener en cada clase de Fragment para crear el BaseFragment abstracto?
Dimitrios K.

5

En mi caso de uso, tengo 50 ImageViews impares que necesitaba conectar a un solo método onClick. Mi solución es recorrer las vistas dentro del fragmento y establecer el mismo oyente onclick en cada una:

    final View.OnClickListener imageOnClickListener = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            chosenImage = ((ImageButton)v).getDrawable();
        }
    };

    ViewGroup root = (ViewGroup) getView().findViewById(R.id.imagesParentView);
    int childViewCount = root.getChildCount();
    for (int i=0; i < childViewCount; i++){
        View image = root.getChildAt(i);
        if (image instanceof ImageButton) {
            ((ImageButton)image).setOnClickListener(imageOnClickListener);
        }
    }

2

Como veo respuestas, de alguna manera son viejas. Recientemente, Google introdujo DataBinding, que es mucho más fácil de manejar en Click o asignar en su xml.

Aquí hay un buen ejemplo en el que puedes ver cómo manejar esto:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
   <data>
       <variable name="handlers" type="com.example.Handlers"/>
       <variable name="user" type="com.example.User"/>
   </data>
   <LinearLayout
       android:orientation="vertical"
       android:layout_width="match_parent"
       android:layout_height="match_parent">
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.firstName}"
           android:onClick="@{user.isFriend ? handlers.onClickFriend : handlers.onClickEnemy}"/>
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.lastName}"
           android:onClick="@{user.isFriend ? handlers.onClickFriend : handlers.onClickEnemy}"/>
   </LinearLayout>
</layout>

También hay un tutorial muy bueno sobre DataBinding que puedes encontrar aquí .


2

Puede definir una devolución de llamada como un atributo de su diseño XML. El artículo Atributos XML personalizados para sus widgets personalizados de Android le mostrará cómo hacerlo para un widget personalizado. El crédito va a Kevin Dion :)

Estoy investigando si puedo agregar atributos con estilo a la clase Fragmento base.

La idea básica es tener la misma funcionalidad que View implementa cuando se trata con la devolución de llamada onClick.


1

Agregando a la respuesta de Blundell,
si tiene más fragmentos, con muchos onClicks:

Actividad:

Fragment someFragment1 = (Fragment)getFragmentManager().findFragmentByTag("someFragment1 "); 
Fragment someFragment2 = (Fragment)getFragmentManager().findFragmentByTag("someFragment2 "); 
Fragment someFragment3 = (Fragment)getFragmentManager().findFragmentByTag("someFragment3 "); 

...onCreate etc instantiating your fragments

public void myClickMethod(View v){
  if (someFragment1.isVisible()) {
       someFragment1.myClickMethod(v);
  }else if(someFragment2.isVisible()){
       someFragment2.myClickMethod(v);
  }else if(someFragment3.isVisible()){
       someFragment3.myClickMethod(v); 
  }

} 

En tu fragmento:

  public void myClickMethod(View v){
     switch(v.getid()){
       // Just like you were doing
     }
  } 

1

Si se registra en xml usando android: Onclick = "", se devolverá la llamada a la Actividad respetada en cuyo contexto pertenece su fragmento (getActivity ()). Si dicho método no se encuentra en la Actividad, el sistema lanzará una excepción.


gracias, nadie más explicó por qué ocurría el bloqueo

1

Es posible que desee considerar el uso de EventBus para eventos desacoplados. Puede escuchar eventos muy fácilmente. También puede asegurarse de que el evento se reciba en el hilo de la interfaz de usuario (en lugar de llamar a runOnUiThread ... para usted por cada suscripción de evento)

https://github.com/greenrobot/EventBus

de Github:

Bus de eventos optimizado para Android que simplifica la comunicación entre Actividades, Fragmentos, Subprocesos, Servicios, etc. Menos código, mejor calidad


1

Me gustaría agregar a la respuesta de Adjorn Linkz .

Si necesita múltiples manejadores, puede usar referencias lambda

void onViewCreated(View view, Bundle savedInstanceState)
{
    view.setOnClickListener(this::handler);
}
void handler(View v)
{
    ...
}

El truco aquí es que handlerla firma de ese método coincide con la View.OnClickListener.onClickfirma. De esta manera, no necesitará la View.OnClickListenerinterfaz.

Además, no necesitará ninguna declaración de cambio.

Lamentablemente, este método solo se limita a las interfaces que requieren un único método, o una lambda.


1

Aunque he visto algunas respuestas agradables basadas en el enlace de datos, no vi ninguna en su totalidad con ese enfoque, en el sentido de permitir la resolución de fragmentos mientras permitía definiciones de diseño sin fragmentos en XML.

Entonces, suponiendo que el enlace de datos esté habilitado, aquí hay una solución genérica que puedo proponer; Un poco largo pero definitivamente funciona (con algunas advertencias):

Paso 1: implementación personalizada de OnClick

Esto ejecutará una búsqueda con reconocimiento de fragmentos a través de contextos asociados con la vista pulsada (por ejemplo, botón):


// CustomOnClick.kt

@file:JvmName("CustomOnClick")

package com.example

import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.view.View
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import java.lang.reflect.Method

fun onClick(view: View, methodName: String) {
    resolveOnClickInvocation(view, methodName)?.invoke(view)
}

private data class OnClickInvocation(val obj: Any, val method: Method) {
    fun invoke(view: View) {
        method.invoke(obj, view)
    }
}

private fun resolveOnClickInvocation(view: View, methodName: String): OnClickInvocation? =
    searchContexts(view) { context ->
        var invocation: OnClickInvocation? = null
        if (context is Activity) {
            val activity = context as? FragmentActivity
                    ?: throw IllegalStateException("A non-FragmentActivity is not supported (looking up an onClick handler of $view)")

            invocation = getTopFragment(activity)?.let { fragment ->
                resolveInvocation(fragment, methodName)
            }?: resolveInvocation(context, methodName)
        }
        invocation
    }

private fun getTopFragment(activity: FragmentActivity): Fragment? {
    val fragments = activity.supportFragmentManager.fragments
    return if (fragments.isEmpty()) null else fragments.last()
}

private fun resolveInvocation(target: Any, methodName: String): OnClickInvocation? =
    try {
        val method = target.javaClass.getMethod(methodName, View::class.java)
        OnClickInvocation(target, method)
    } catch (e: NoSuchMethodException) {
        null
    }

private fun <T: Any> searchContexts(view: View, matcher: (context: Context) -> T?): T? {
    var context = view.context
    while (context != null && context is ContextWrapper) {
        val result = matcher(context)
        if (result == null) {
            context = context.baseContext
        } else {
            return result
        }
    }
    return null
}

Nota: basado libremente en la implementación original de Android (consulte https://android.googlesource.com/platform/frameworks/base/+/a175a5b/core/java/android/view/View.java#3025 )

Paso 2: aplicación declarativa en archivos de diseño

Luego, en XML de enlace de datos conscientes:

<layout>
  <data>
     <import type="com.example.CustomOnClick"/>
  </data>

  <Button
    android:onClick='@{(v) -> CustomOnClick.onClick(v, "myClickMethod")}'
  </Button>
</layout>

Advertencias

  • Asume una FragmentActivityimplementación 'moderna' basada
  • Solo se puede buscar el método del fragmento "top-most" (es decir, el último ) en la pila (aunque eso se puede arreglar, si es necesario)

0

Esto ha estado funcionando para mí: (estudio de Android)

 @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

        View rootView = inflater.inflate(R.layout.update_credential, container, false);
        Button bt_login = (Button) rootView.findViewById(R.id.btnSend);

        bt_login.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {

                System.out.println("Hi its me");


            }// end onClick
        });

        return rootView;

    }// end onCreateView

1
Esto duplica la respuesta de @Brill Pappin .
naXa

0

La mejor solución en mi humilde opinión:

en fragmento:

protected void addClick(int id) {
    try {
        getView().findViewById(id).setOnClickListener(this);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

public void onClick(View v) {
    if (v.getId()==R.id.myButton) {
        onMyButtonClick(v);
    }
}

luego en onViewStateRestored de Fragment:

addClick(R.id.myButton);

0

Su actividad está recibiendo la devolución de llamada como debe haber utilizado:

mViewPagerCloth.setOnClickListener((YourActivityName)getActivity());

Si desea que su fragmento reciba devolución de llamada, haga esto:

mViewPagerCloth.setOnClickListener(this);

e implementar la onClickListenerinterfaz en Fragment

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.