Come gestire i clic sui pulsanti utilizzando XML onClick in Fragments


440

Pre-Honeycomb (Android 3), ogni attività è stata registrata per gestire i clic sui pulsanti tramite il onClicktag nell'XML di un layout:

android:onClick="myClickMethod"

All'interno di quel metodo è possibile utilizzare view.getId()e un'istruzione switch per eseguire la logica dei pulsanti.

Con l'introduzione di Honeycomb sto suddividendo queste attività in frammenti che possono essere riutilizzati all'interno di diverse attività. La maggior parte del comportamento dei pulsanti è indipendente dall'attività e vorrei che il codice risiedesse all'interno del file Fragments senza utilizzare il vecchio metodo (pre 1.6) di registrazione OnClickListenerper ciascun pulsante.

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

Il problema è che quando il mio layout è gonfiato è ancora l'attività di hosting che riceve i clic sui pulsanti, non i singoli frammenti. C'è un buon approccio per entrambi

  • Registrare il frammento per ricevere i clic sui pulsanti?
  • Passare gli eventi clic dall'attività al frammento a cui appartengono?

1
Non riesci a gestire la registrazione degli ascoltatori all'interno di onCreate del frammento?
CL22,

24
@jodes Sì, ma non voglio usare setOnClickListenere findViewByIdper ogni pulsante, ecco perché è onClickstato aggiunto, per semplificare le cose.
smith324

4
Guardando la risposta accettata, penso che usare setOnClickListener sia accoppiato più liberamente che attenersi all'approccio XML onClick. Se l'attività deve "inoltrare" ogni clic al frammento corretto, ciò significa che il codice dovrà cambiare ogni volta che viene aggiunto un frammento. L'uso di un'interfaccia per separare dalla classe base del frammento non aiuta a farlo. Se il frammento si registra con il pulsante corretto stesso, l'attività rimane completamente agnostica, ovvero uno stile IMO migliore. Vedi anche la risposta di Adorjan Princz.
Adriaan Koster,

@ smith324 devono essere d'accordo con Adriaan su questo. Prova la risposta di Adorjan e vedi se la vita non è migliore dopo.
user1567453

Risposte:


175

Potresti semplicemente fare questo:

Attività:

Fragment someFragment;    

//...onCreate etc instantiating your fragments

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

Frammento:

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

In risposta a @Ameen che voleva meno accoppiamento, i frammenti sono riutilizzabili

Interfaccia:

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

Attività:

XmlClickable someFragment;    

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

Frammento:

public class SomeFragment implements XmlClickable {

//...onCreateView, etc.

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

52
Questo è quello che sto facendo ora essenzialmente ma è molto più disordinato quando hai più frammenti di cui ognuno ha bisogno per ricevere eventi click. Sono solo aggravato dai frammenti in generale perché i paradigmi si sono dissolti attorno a loro.
smith324

128
Sto riscontrando lo stesso problema e, anche se apprezzo la tua risposta, questo non è un codice pulito dal punto di vista dell'ingegneria del software. Questo codice fa sì che l'attività sia strettamente accoppiata al frammento. Dovresti essere in grado di riutilizzare lo stesso frammento in più attività senza che le attività conoscano i dettagli di implementazione dei frammenti.
Ameen

1
Dovrebbe essere "switch (v.getId ()) {" e non "switch (v.getid ()) {"
eb80

5
Invece di definire la tua interfaccia, puoi utilizzare OnClickListener già esistente come indicato da Euporie.
fr00tyl00p,

1
Ho pianto quando ho letto questo, è MOLTO CALDAIA ... La risposta di seguito da @AdorjanPrincz è la strada da percorrere.
Wjdavis5,

603

Preferisco utilizzare la seguente soluzione per la gestione degli eventi onClick. Questo funziona anche per attività e frammenti.

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

9
In onCreateView, eseguo il ciclo attraverso tutti gli elementi figlio di ViewGroup v e imposto il listener onclick per tutte le istanze Button che trovo. È molto meglio che impostare manualmente l'ascoltatore per tutti i pulsanti.
Una persona

17
Votato. Questo rende i frammenti riutilizzabili. Altrimenti perché usare i frammenti?
boreas,

44
Non è questa la stessa tecnica sostenuta dalla programmazione di Windows nel 1987? Da non preoccuparsi. Google si muove velocemente ed è tutto incentrato sulla produttività degli sviluppatori. Sono sicuro che non passerà molto tempo prima che la gestione degli eventi sia buona quanto quella di Visual Basic del 1991.
Edward Brey,

7
importazione delle streghe hai usato per OnClickListener? Intellij mi suggerisce android.view.View.OnClickListener e non funziona: / (onClick non funziona mai)
Lucas Jota,

4
@NathanOsman Penso che la domanda fosse correlata all'xml onClick, quindi le risposte accettate forniscono la soluzione esatta.
Naveed Ahmad,

29

Il problema secondo me è che la vista è ancora l'attività, non il frammento. I frammenti non hanno una vista indipendente e sono collegati alla vista attività padre. Ecco perché l'evento finisce nell'attività, non nel frammento. È un peccato, ma penso che avrai bisogno di un po 'di codice per farlo funzionare.

Quello che ho fatto durante le conversioni è semplicemente l'aggiunta di un listener di clic che chiama il vecchio gestore di eventi.

per esempio:

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

1
Grazie - L'ho usato con una leggera modifica in quanto sto passando la vista frammento (cioè il risultato di inflater.inflate (R.layout.my_fragment_xml_resource)) su onLoginClicked () in modo che possa accedere alle sotto-viste dei frammenti, come un EditText, tramite view.findViewById () (se passo semplicemente attraverso la vista attività, le chiamate a view.findViewById (R.id.myfragmentwidget_id) restituiscono null).
Michael Nelson,

Questo non funziona con API 21 nel mio progetto. Qualche idea su come usare questo approccio?
Darth Coder

Il suo codice piuttosto semplice, usato praticamente in ogni app. Puoi descrivere cosa sta succedendo per te?
Brill Pappin,

1
Apprezzo la mia risposta a questa risposta per la spiegazione del problema che si è verificato perché il layout del frammento è attaccato alla vista dell'attività.
fllo

25

Di recente ho risolto questo problema senza dover aggiungere un metodo al contesto Activity o implementare OnClickListener. Non sono sicuro che sia una soluzione "valida", ma funziona.

Basato su: https://developer.android.com/tools/data-binding/guide.html#binding_events

Può essere fatto con associazioni di dati: basta aggiungere l'istanza del frammento come variabile, quindi è possibile collegare qualsiasi metodo 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>

E il frammento del codice di collegamento sarebbe ...

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
Ho solo la sensazione, mancano alcuni dettagli poiché non riesco a far funzionare questa soluzione
Tima

Forse ti manca qualcosa nel "Build Environment" nella documentazione: developer.android.com/tools/data-binding/…
Aldo Canepa

@Aldo Per il metodo onClick in XML credo che dovresti avere android: onClick = "@ {() -> fragment. ButtonClicked ()}". Anche per altri, dovresti dichiarare la funzione buttonClicked () all'interno del frammento e inserire la tua logica all'interno.
Hayk Nahapetyan,

nel XML dovrebbe essereandroid:name="fragment" android:type="com.example.testapp.fragments.CustomFragment"/>
COYG

1
Ho appena provato questo e gli attributi namee typedel variabletag non dovrebbero avere il android:prefisso. Forse c'è una vecchia versione di layout Android che lo richiede?
JohnnyLambada,

6

ButterKnife è probabilmente la soluzione migliore per il problema del disordine. Utilizza processori di annotazione per generare il cosiddetto codice "vecchio metodo" del boilerplate.

Ma il metodo onClick può ancora essere utilizzato, con un gonfiatore personalizzato.

Come usare

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

Implementazione

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

Preferirei utilizzare la gestione dei clic nel codice piuttosto che utilizzare l' onClickattributo in XML quando si lavora con i frammenti.

Questo diventa ancora più semplice quando si esegue la migrazione delle attività in frammenti. Puoi semplicemente chiamare il gestore di clic (precedentemente impostato android:onClickin XML) direttamente da ciascun caseblocco.

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:
              ...
        }
    }
}

Quando si tratta di gestire i clic in frammenti, questo mi sembra più semplice di android:onClick.


5

Questo è un altro modo :

1.Creare un BaseFragment in questo modo:

public abstract class BaseFragment extends Fragment implements OnClickListener

2.Use

public class FragmentA extends BaseFragment 

invece di

public class FragmentA extends Fragment

3.Nella tua attività:

public class MainActivity extends ActionBarActivity implements OnClickListener

e

BaseFragment fragment = new FragmentA;

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

Spero che sia d'aiuto.


1 anno, 1 mese e 1 giorno dopo la risposta: c'è qualche motivo se non ripetere l'implementazione di OnClickListener su ogni classe di frammenti per creare la BaseFragment astratta?
Dimitrios K.,

5

Nel mio caso d'uso, ho 50 visualizzazioni dispari di cui avevo bisogno per collegarmi a un singolo metodo onClick. La mia soluzione è quella di passare in rassegna le viste all'interno del frammento e impostare lo stesso listener onclick su ciascuno:

    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

Come vedo le risposte sono in qualche modo vecchi. Recentemente Google ha introdotto DataBinding, che è molto più facile da gestire su Click o assegnando nel tuo XML.

Ecco un buon esempio che puoi vedere come gestirlo:

<?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>

C'è anche un bellissimo tutorial su DataBinding che puoi trovare qui .


2

È possibile definire un callback come attributo del layout XML. L'articolo Attributi XML personalizzati per i tuoi widget Android personalizzati ti mostrerà come farlo per un widget personalizzato. Il merito va a Kevin Dion :)

Sto studiando se posso aggiungere attributi stilizzabili alla classe Fragment di base.

L'idea di base è quella di avere la stessa funzionalità che Visualizza implementa quando si ha a che fare con il callback onClick.


1

Aggiungendo alla risposta di Blundell,
se hai più frammenti, con molti onClicks:

Attività:

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

} 

Nel tuo frammento:

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

1

Se ti registri in xml usando android: Onclick = "", il callback verrà assegnato all'attività rispettata nel cui contesto appartiene il tuo frammento (getActivity ()). Se tale metodo non si trova nell'attività, il sistema genererà un'eccezione.


grazie, nessun altro ha spiegato perché si stava verificando l'incidente

1

Potresti prendere in considerazione l'utilizzo di EventBus per eventi disaccoppiati. Puoi ascoltare gli eventi molto facilmente. Puoi anche assicurarti che l'evento sia ricevuto sul thread dell'interfaccia utente (invece di chiamare runOnUiThread .. per te per ogni abbonamento all'evento)

https://github.com/greenrobot/EventBus

da Github:

Bus eventi ottimizzato per Android che semplifica la comunicazione tra attività, frammenti, thread, servizi, ecc. Meno codice, migliore qualità


1

Vorrei aggiungere la risposta di Adjorn Linkz .

Se hai bisogno di più gestori, puoi semplicemente usare i riferimenti lambda

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

Il trucco qui è che handlerla firma di quel metodo corrisponde alla View.OnClickListener.onClickfirma. In questo modo, non avrai bisogno View.OnClickListenerdell'interfaccia.

Inoltre, non avrai bisogno di alcuna istruzione switch.

Purtroppo, questo metodo è limitato solo alle interfacce che richiedono un singolo metodo o un lambda.


1

Sebbene io abbia individuato alcune belle risposte basandomi sull'associazione di dati, non ho visto nulla di tutto questo approccio - nel senso di abilitare la risoluzione dei frammenti consentendo nel contempo definizioni di layout prive di frammenti negli XML.

Quindi, supponendo che sia abilitata l' associazione dei dati , ecco una soluzione generica che posso proporre; Un po 'lungo ma sicuramente funziona (con alcuni avvertimenti):

Passaggio 1: implementazione OnClick personalizzata

Questo eseguirà una ricerca consapevole dei frammenti attraverso i contesti associati alla vista toccata (es. Pulsante):


// 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: liberamente basato sull'implementazione Android originale (consultare https://android.googlesource.com/platform/frameworks/base/+/a175a5b/core/java/android/view/View.java#3025 )

Passaggio 2: applicazione dichiarativa nei file di layout

Quindi, in XML consapevoli dell'associazione di dati:

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

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

Avvertenze

  • Presuppone FragmentActivityun'implementazione basata "moderna"
  • Può solo cercare il metodo del frammento "top-most" (ovvero l' ultimo ) nello stack (anche se può essere corretto, se necessario)

0

Questo ha funzionato per me: (Android Studio)

 @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
Questo duplica la risposta di @Brill Pappin .
NaXa

0

La migliore soluzione IMHO:

in frammento:

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

quindi in onViewStateRestored di Fragment:

addClick(R.id.myButton);

0

La tua attività sta ricevendo la richiamata come deve aver usato:

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

Se si desidera che il frammento riceva la richiamata, procedere come segue:

mViewPagerCloth.setOnClickListener(this);

e implementare l' onClickListenerinterfaccia su Fragment

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.