Comment gérer les clics sur les boutons en utilisant le XML onClick dans les fragments

440

Pré-Honeycomb (Android 3), chaque activité a été enregistrée pour gérer les clics sur les boutons via la onClickbalise dans le XML d'une mise en page:

android:onClick="myClickMethod"

Dans cette méthode, vous pouvez utiliser view.getId()et une instruction switch pour faire la logique du bouton.

Avec l'introduction de Honeycomb, je divise ces activités en fragments qui peuvent être réutilisés dans de nombreuses activités différentes. La plupart du comportement des boutons est indépendant de l'activité, et j'aimerais que le code réside dans le fichier Fragments sans utiliser l'ancienne méthode (antérieure à 1.6) d'enregistrement du OnClickListenerpour chaque bouton.

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

Le problème est que lorsque ma disposition est gonflée, c'est toujours l'activité d'hébergement qui reçoit les clics sur les boutons, pas les fragments individuels. Existe-t-il une bonne approche pour

  • Enregistrer le fragment pour recevoir les clics sur le bouton?
  • Passer les événements de clic de l'activité au fragment auquel ils appartiennent?
smith324
la source
1
Vous ne pouvez pas gérer l'enregistrement des auditeurs dans le onCreate du fragment?
CL22
24
@jodes Oui, mais je ne veux pas avoir à utiliser setOnClickListeneret findViewByIdpour chaque bouton, c'est pourquoi a onClickété ajouté, pour simplifier les choses.
smith324
4
En regardant la réponse acceptée, je pense que l'utilisation de setOnClickListener est plus lâchement couplée que de s'en tenir à l'approche XML onClick. Si l'activité doit «transférer» chaque clic vers le fragment de droite, cela signifie que le code devra changer chaque fois qu'un fragment est ajouté. L'utilisation d'une interface pour découpler de la classe de base du fragment n'aide pas à cela. Si le fragment s'inscrit avec le bon bouton lui-même, l'activité reste complètement agnostique, ce qui est un meilleur style IMO. Voir aussi la réponse d'Adorjan Princz.
Adriaan Koster
@ smith324 doit être d'accord avec Adriaan sur celui-ci. Essayez la réponse d'Adorjan et voyez si la vie n'est pas meilleure après cela.
user1567453

Réponses:

175

Vous pouvez simplement faire ceci:

Activité:

Fragment someFragment;    

//...onCreate etc instantiating your fragments

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

Fragment:

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

En réponse à @Ameen qui voulait moins de couplage pour que les fragments soient réutilisables

Interface:

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

Activité:

XmlClickable someFragment;    

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

Fragment:

public class SomeFragment implements XmlClickable {

//...onCreateView, etc.

@Override
public void myClickMethod(View v) {
    switch(v.getId()){
        // Just like you were doing
    }
}    
Blundell
la source
52
C'est essentiellement ce que je fais maintenant, mais c'est beaucoup plus compliqué lorsque vous avez plusieurs fragments qui doivent chacun recevoir des événements de clic. Je suis simplement aggravé par les fragments en général parce que les paradigmes se sont dissous autour d'eux.
smith324
128
Je rencontre le même problème, et même si j'apprécie votre réponse, ce n'est pas du code propre du point de vue de l'ingénierie logicielle. Ce code a pour résultat que l'activité est étroitement couplée au fragment. Vous devriez pouvoir réutiliser le même fragment dans plusieurs activités sans que les activités connaissent les détails d'implémentation des fragments.
Ameen
1
Doit être "switch (v.getId ()) {" et non "switch (v.getid ()) {"
eb80
5
Au lieu de définir votre propre interface, vous pouvez utiliser le OnClickListener déjà existant comme mentionné par Euporie.
fr00tyl00p
1
J'ai à peu près pleuré en lisant ceci, c'est TELLEMENT CHAUDIÈRE ... La réponse ci-dessous de @AdorjanPrincz est la voie à suivre.
Wjdavis5
603

Je préfère utiliser la solution suivante pour gérer les événements onClick. Cela fonctionne également pour l'activité et les fragments.

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;
        }
    }
}
Adorjan Princz
la source
9
Dans onCreateView, je fais une boucle à travers tous les éléments enfants de ViewGroup v et définit le onclicklistener pour toutes les instances de bouton que je trouve. C'est bien mieux que de configurer manuellement l'écouteur pour tous les boutons.
Une personne
17
Voté. Cela rend les fragments réutilisables. Sinon, pourquoi utiliser des fragments?
boreas
44
N'est-ce pas la même technique préconisée par la programmation de Windows en 1987? Ne pas s'inquiéter. Google évolue rapidement et se concentre sur la productivité des développeurs. Je suis sûr qu'il ne faudra pas longtemps avant que la gestion des événements soit aussi bonne que 1991-eara Visual Basic.
Edward Brey
7
quelle importation avez-vous utilisé pour OnClickListener? Intellij me suggère android.view.View.OnClickListener et cela ne fonctionne pas: / (onClick ne s'exécute jamais)
Lucas Jota
4
@NathanOsman Je pense que la question était liée au xml onClick, donc les réponses acceptées fournissent la solution exacte.
Naveed Ahmad
29

Le problème, je pense, c'est que la vue est toujours l'activité, pas le fragment. Les fragments n'ont pas de vue indépendante et sont attachés à la vue des activités parentes. C'est pourquoi l'événement se termine dans l'activité, pas le fragment. C'est malheureux, mais je pense que vous aurez besoin de code pour que cela fonctionne.

Ce que j'ai fait pendant les conversions, c'est simplement ajouter un écouteur de clics qui appelle l'ancien gestionnaire d'événements.

par exemple:

final Button loginButton = (Button) view.findViewById(R.id.loginButton);
loginButton.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(final View v) {
        onLoginClicked(v);
    }
});
Brill Pappin
la source
1
Merci - J'ai utilisé cela avec une légère modification en ce sens que je passe la vue des fragments (c'est-à-dire le résultat de inflater.inflate (R.layout.my_fragment_xml_resource)) à onLoginClicked () afin qu'il puisse accéder aux sous-vues des fragments, comme un EditText, via view.findViewById () (Si je passe simplement par la vue d'activité, les appels à view.findViewById (R.id.myfragmentwidget_id) retournent null).
Michael Nelson
Cela ne fonctionne pas avec l'API 21 dans mon projet. Avez-vous des réflexions sur la façon d'utiliser cette approche?
Dark Coder
Son code assez basique, utilisé dans à peu près toutes les applications. Pouvez-vous décrire ce qui se passe pour vous?
Brill Pappin
1
Apportez mon vote positif à cette réponse pour expliquer le problème qui s'est produit parce que la disposition du fragment est attachée à la vue de l'activité.
fllo
25

J'ai récemment résolu ce problème sans avoir à ajouter une méthode à l'activité de contexte ni à implémenter OnClickListener. Je ne sais pas si c'est une solution "valide" non plus, mais cela fonctionne.

Basé sur: https://developer.android.com/tools/data-binding/guide.html#binding_events

Cela peut être fait avec des liaisons de données: ajoutez simplement votre instance de fragment en tant que variable, puis vous pouvez lier n'importe quelle méthode avec 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>

Et le fragment liant le code serait ...

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

    ...

}
Aldo Canepa
la source
1
J'ai juste un sentiment, certains détails manquent car je ne suis pas en mesure de faire fonctionner cette solution
Tima
Peut-être qu'il vous manque quelque chose dans "Build Environment" dans la documentation: developer.android.com/tools/data-binding/…
Aldo Canepa
@Aldo Pour la méthode onClick en XML, je pense que vous devriez avoir Android: onClick = "@ {() -> fragment. ButtonClicked ()}" à la place. Aussi pour d'autres, vous devez déclarer la fonction buttonClicked () à l'intérieur du fragment et mettre votre logique à l'intérieur.
Hayk Nahapetyan
dans le xml, il devrait êtreandroid:name="fragment" android:type="com.example.testapp.fragments.CustomFragment"/>
COYG
1
Je viens d'essayer cela et les attributs nameet typede la variablebalise ne devraient pas avoir le android:préfixe. Peut-être existe-t-il une ancienne version des mises en page Android qui l'exige?
JohnnyLambada
6

ButterKnife est probablement la meilleure solution pour le problème de l'encombrement. Il utilise des processeurs d'annotation pour générer le code passe-partout dit "ancienne méthode".

Mais la méthode onClick peut toujours être utilisée, avec un gonfleur personnalisé.

Comment utiliser

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

la mise en oeuvre

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);
            }
        }
    }
}
sergio91pt
la source
6

Je préfère opter pour la gestion des clics dans le code plutôt que d'utiliser l' onClickattribut en XML lorsque vous travaillez avec des fragments.

Cela devient encore plus facile lors de la migration de vos activités en fragments. Vous pouvez simplement appeler le gestionnaire de clics (précédemment défini sur android:onClickXML) directement à partir de chaque casebloc.

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

Quand il s'agit de gérer les clics en fragments, cela me semble plus simple que android:onClick.

Ronnie
la source
5

C'est une autre façon:

1.Créez un BaseFragment comme ceci:

public abstract class BaseFragment extends Fragment implements OnClickListener

2.Utilisez

public class FragmentA extends BaseFragment 

au lieu de

public class FragmentA extends Fragment

3.Dans votre activité:

public class MainActivity extends ActionBarActivity implements OnClickListener

et

BaseFragment fragment = new FragmentA;

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

J'espère que cela aide.

Euporie
la source
1 an, 1 mois et 1 jour après votre réponse: y a-t-il une raison autre que de ne pas répéter l'implémentation d'OnClickListener sur chaque classe Fragment pour créer le BaseFragment abstrait?
Dimitrios K.
5

Dans mon cas d'utilisation, j'ai 50 images vues impaires dont j'avais besoin pour me connecter à une seule méthode onClick. Ma solution consiste à parcourir les vues à l'intérieur du fragment et à définir le même écouteur onclick sur chacune:

    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);
        }
    }
Chris Knight
la source
2

Comme je vois les réponses, elles sont en quelque sorte vieilles. Récemment, Google a introduit DataBinding, qui est beaucoup plus facile à gérer sur Click ou à attribuer dans votre xml.

Voici un bon exemple que vous pouvez voir comment gérer cela:

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

Il y a aussi un très bon tutoriel sur DataBinding que vous pouvez trouver ici .

Amir
la source
2

Vous pouvez définir un rappel comme attribut de votre présentation XML. L'article Attributs XML personnalisés pour vos widgets Android personnalisés vous montrera comment le faire pour un widget personnalisé. Le mérite revient à Kevin Dion :)

J'examine si je peux ajouter des attributs stylables à la classe Fragment de base.

L'idée de base est d'avoir les mêmes fonctionnalités que View implémente lors du traitement du rappel onClick.

Nelson Ramirez
la source
1

Ajout à la réponse de Blundell,
si vous avez plus de fragments, avec beaucoup d'onClicks:

Activité:

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

} 

Dans votre fragment:

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

Si vous vous inscrivez en xml en utilisant android: Onclick = "", un rappel sera donné à l'activité respectée dans le contexte de laquelle votre fragment appartient (getActivity ()). Si une telle méthode n'est pas trouvée dans l'activité, le système lèvera une exception.

Isham
la source
merci, personne d'autre n'a expliqué pourquoi l'accident s'était produit
1

Vous voudrez peut-être envisager d'utiliser EventBus pour les événements découplés. Vous pouvez écouter les événements très facilement. Vous pouvez également vous assurer que l'événement est reçu sur le thread d'interface utilisateur (au lieu d'appeler runOnUiThread .. par vous-même pour chaque abonnement à un événement)

https://github.com/greenrobot/EventBus

de Github:

Bus d'événements optimisé pour Android qui simplifie la communication entre les activités, les fragments, les threads, les services, etc. Moins de code, une meilleure qualité

programmeur
la source
1

Je voudrais ajouter à la réponse d' Adjorn Linkz .

Si vous avez besoin de plusieurs gestionnaires, vous pouvez simplement utiliser des références lambda

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

L'astuce ici est que handlerla signature de la méthode correspond à la View.OnClickListener.onClicksignature. De cette façon, vous n'aurez pas besoin de l' View.OnClickListenerinterface.

De plus, vous n'aurez besoin d'aucune instruction switch.

Malheureusement, cette méthode n'est limitée qu'aux interfaces qui nécessitent une seule méthode ou un lambda.

Dragas
la source
1

Bien que j'aie repéré de jolies réponses en s'appuyant sur la liaison de données, je n'en ai vu aucune allant dans toute la mesure avec cette approche - dans le sens d'activer la résolution des fragments tout en permettant des définitions de disposition sans fragment dans XML.

Donc, en supposant que la liaison de données est activée, voici une solution générique que je peux proposer; Un peu long mais ça marche définitivement (avec quelques mises en garde):

Étape 1: implémentation OnClick personnalisée

Cela lancera une recherche sensible aux fragments à travers les contextes associés à la vue sur laquelle vous avez tapé (par exemple le bouton):


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

Remarque: librement basé sur l'implémentation Android d'origine (voir https://android.googlesource.com/platform/frameworks/base/+/a175a5b/core/java/android/view/View.java#3025 )

Étape 2: application déclarative dans les fichiers de mise en page

Ensuite, dans les XML sensibles à la liaison de données:

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

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

Avertissements

  • Suppose une FragmentActivityimplémentation «moderne»
  • Peut uniquement rechercher la méthode du fragment "le plus haut" (c'est-à-dire le dernier ) dans la pile (bien que cela puisse être corrigé, si nécessaire)
d4vidi
la source
0

Cela a fonctionné pour moi: (studio 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
Vinod Joshi
la source
1
Cela reproduit la réponse de @Brill Pappin .
naXa
0

Meilleure solution à mon humble avis:

en fragment:

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

puis dans onViewStateRestored de Fragment:

addClick(R.id.myButton);
user4806368
la source
0

Votre activité reçoit le rappel, comme cela a dû être le cas:

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

Si vous souhaitez que votre fragment reçoive un rappel, procédez comme suit:

mViewPagerCloth.setOnClickListener(this);

et implémenter l' onClickListenerinterface sur Fragment

Fusion froide
la source